文章目录
1. 基本内置类型
这一部分内容要注意的东西其实不是特别多
1)算术类型
算术类型可以分为两类:整型(字符和布尔类型也算在内)和浮点型
算术类型的尺寸
值得注意一下
-
字符型
字符:char(8位)
宽字符:wchar_t (16位)
Unicode字符:char16_t(16位)、char32_t(32位) -
浮点型
单精度float:6位有效数字
双精度double:10位有效数字
拓展双精度long double:10位有效数字
这里的有效数字是指按照IEEE标准的浮点数中按十进制可以保证的有效数字
详细可以参考
- https://blog.csdn.net/sinat_38972110/article/details/82117072?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
注意有效数字是指浮点数中尾数可以保证的精度
带符号类型与无符号类型
- 字符型被分为了三种:
char/unsigned char/signed char
- 一般编译器决定
char
为带符号还是不带符号 - int也类似,事实上这里牵扯到一个信息安全相关的知识,不带符号的更容易导致错误,但带符号的更符合现实逻辑。
建议:如何选择类型
- 知晓不可能为负,选用无符号类型
- 使用int执行整型运算,不够用则用longlong
- 算术表达式中尽可能不要用到
char
或bool
- 执行浮点数运算选用
double
:计算时间相差不多(甚至更快)
2)类型转换
类型所能表示的值的范围决定了转换的过程:
-
浮点数与整型之间的相互转换
浮点数转为整型可能存在近似处理
整型转为浮点数可能存在精度损失
-
无符号数与有符号数之间的相互转换
实际上二进制表示不发生改变,这样的转换有时可能导致不好的后果
无符号数与有符号数混用将统一为无符号数
无符号数的不谨慎使用可能产生不良后果,如下:
for (unsigned int i = k; i--; i >= 0){ //for循环将无法终止 do_something(); }
3)字面值常量
整型和浮点型字面值
- 整型:
088
八进制,88
十进制,0x88
十六进制 - 浮点型:
3.1415e0
科学计数法,0.
,.001
字符和字符串常量
略
转义字符
- 转义字符中,如果
\
后跟着数字则表示八进制,如\123
,表示八进制数123
,最多为三位
指定字面值类型
以上为primer C++书中出现的指定字符和字符串字面值的前缀和后缀符号
2. 变量
1)变量定义
语法
- 类型说明符 标识符;
初始值
-
定义了后对象可以马上使用
-
初始化不等于赋值
初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象当前的值擦除,以一个新的值来代替
-
列表初始化
利用花括号进行初始化,不仅适用于基本内置类型也适用于自定义对象。
int units_sold = {0};
- 利用列表初始化内置变量时,若初始值丢失,则可能存在风险。(即花括号中包含变量的情况)
默认初始化
- 大多数类都支持默认初始化
2)变量声明和定义的关系
- 声明:使得变量为编译器所知
- 定义:负责创建与名字关联的实体
注意:
-
如果只想声明一个变量而非定义,可以用
extern
关键字,而且不要显式初始化extern int i; // 声明而非定义
如果包含了初始化,则变成了定义。
extern int i = 10; //定义
-
函数体内部如果试图初始化一个
extern
变量,将引发错误。 -
变量只能被定义一次,但可以被多次声明
3)标识符
字母数字下划线组成,必须字母或下划线开头
命名风格可以参考谷歌开源项目文档: https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/naming/
4)作用域
时间/空间两个维度考虑
需要明白这些概念:块作用域、全局作用域、嵌套作用域
3. 复合类型
1)引用
-
左值引用与右值引用
这里指的是左值引用
定义
-
**引用相当于为对象起了一个名字,引用类型引用另外一种类型。(即别名)**例子如下:
int val = 124; int &refVal = val; // 指向val itn &refVal2; //报错 error: 'refVal2' declared as reference but not initialized
初始化
-
定义引用时,程序把引用和初始值绑定,引用无法绑定另一个对象,故必须初始化
-
用引用初始化引用是可以接受的
int &refVal3 = refVal;
-
错误的情况
a.利用常量初始
int &refVal = 10;
报错:
error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
b.利用不同类型的对象初始化:
double a; int &b = a;
报错:
7:14: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
2)指针
与引用类似,指针也实现了对其他对象的间接访问,然而指针与引用相比又有很多不同:
- 指针本身是个对象
- 指针定义时可以不必赋初值
指针的定义
-
取地址符:&
-
不能用引用定义指针:引用不是对象,没有实际地址
-
类型不匹配不可以定义
double a = 3; double *b = &a; int *c = b; //出错
报错:
error: cannot convert 'double*' to 'int*' in initialization
-
不可以将指针通过int变量赋值,即使这个变量值为0
int a = 0; int *ptr = a;
指针值
四种情况之一:
- 指向一个对象
- 指向紧邻对象的下一个位置
- 空指针
- 无效指针:这种情况可能在运行时出现难以察觉的错误
最好初始化所有指针:nullptr
利用指针访问对象
- 解引用符
3)理解复合类型的声明
(这一部分可以多看看)
定义多个变量
-
错误的观点:类型修饰符作用域某行定义的所有标识符
实际上,只作用于相距最近的标识符,所以一般这样定义
int *ptr, a; //ptr为指针,a为整性
指向指针的指针
-
指针也可以指向指针,含义是指向某块地址
int val=1024; int *ptr = &val; int **ptr = &ptr;
指向指针的引用
-
指向指针的引用为指针的别名
int i = 42; int *p; int *&r = p; // r为p的引用 *r = &1; *r = 0;
4. const限定符
初始化和const
const对象的操作被限制在不会修改其值的操作上
初始化
-
const初始化
const对象必须被初始化:
error: uninitialized const 'c' [-fpermissive]
-
利用const对象初始化其他对象:
这里的j为一个const对象,无法被更改,但可以用于初始化(第3行)
int i = 42; const int j = i; int k = j;
默认状态下,const只在文件内有效
-
当以编译时初始化的方式定义一个const对象,编译器将编译过程中所有用到该变量的地方都替换为对应的值。
-
如果某个const对象被多个文件使用,需要在每一个文件中都有这个变量的定义
-
有时候某个const对象确实需要被多个文件使用,可以通过
extern
实现,无论是声明还是定义都加入extern
关键字// file1.cpp extern const int bufSize = fcn(); // file1.h extern const int bufSize;
1)const的引用
类似于普通引用的定义,我们可以将引用绑定到常量上,不同的是无法通过引用对常量进行修改。
-
尝试修改会在编译时出错
const int a = 10; const int &b = a; b = 11;
error: assignment of read-only reference 'b'
-
非常量引用不可指向常量
int &c = a;
初始化和对const的引用
int a = 10;
const int &r1 = a; // 正确
const int &r2 = 10; // 正确
const int &r3 = r1 * 2; // 正确
int &r4 = r1 * 2; // 错误 error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
对错误的理解
-
先看一个问题
double a = 10; const int &b = a; cout << b << endl;
程序将输出10,实际上编译器对该代码进行了修改,创建了一个临时变量用于保存该对象
const int tmp = a; const int &b = tmp;
-
如果这里的b不是常量,则允许了对b引用对象的修改,而b引用的对象将是一个常量,这是不被允许的
因为既然声明了引用,便肯定想通过引用访问原变量
对const的引用可能引用一个非const对象
即const引用引用变量,这意味着无法通过引用修改变量
int a = 10;
const int &b = a;
int c = a;
b = 0; // error: assignment of read-only reference 'b'
c = 0;
2)指针和const
指向常量的指针不能修改它指向的对象得值
要想存放常量对象得地址,只可以使用常量指针
const int a = 10;
const int *b = &a;
int *c = &a; // error: invalid conversion from 'const int*' to 'int*'
常量指针也可以指向变量对象,这意味着无法通过指针修改该变量
int a = 10;
const int *b = &a;
*b = 11; // error: assignment of read-only location '* b'
const 指针
*放在const关键字之前意味着指针是一个常量,意味着指针无法指向其他变量。
int a = 10;
int b = 11;
int *const c = &a;
*c = 11;
c = &b; // error: assignment of read-only location '* b'
int *const d; // error: uninitialized const 'd'
理解这些的最好方法是从右往左阅读,离得最近的关键字将定义变量的对象类型:
- int *const:const指针
- const int*:常量指针
3)顶层const
顶层const表示指针本身是个常量,底层const表示指向的对象视为常量(对于引用也同样适用)
int a = 10;
const int b = 11; // 顶层const,不可改变b的值
int *const p1 = &a; // 顶层const,p1不可指向其他变量
const int* p2 = &b; // 底层const,p2指向的对象视为常量
const int* const p3 = &a;
执行对象的拷贝操作时,常量是顶层const还是底层const区别明显:
-
顶层const不受影响
顶层const资格不同没有关系,可以正常执行。
a = b; // 正确 p2 = p3; // 正确:p3顶层不受影响
-
底层const的影响不可忽视
拷入拷出的对象必须具有相同的底层const资格
int *p = p3; // 错误,p3为指向常量的指针 int &r = b; // 错误,b为常量 const int& r2 = a; // 正确,视r2为常量引用(不可改变值)
4)constexpr和常量表达式
常量表达式是指在编译过程中就可以得到计算结果的表达式,显然字面值属于常量表达式
一个对象是不是常量表达式由数据类型和初始值决定的:
const int a = 20;
const int b = a + 1;
int c = 10; // 不是常量表达式
const int d = get_size(); // 不是常量表达式
constexpr常量
C++11标准规定可以利用constexpr
关键字来声明变量使得编译器可以验证这个变量是否为常量表达式,声明为constexpr
类型的变量一定是一个常量且必须常量表达式初始化。
例子如下:
constexpr int a = 10;
constexpr int b = a + 1;
constexpr int c = get_size(); //只有在constexpr关键字下才是常量
字面值常量
constexpr声明时用到的类型,称为字面值类型
算术类型、引用和指针都属于字面值类型,自定义类和库不属于字面值类型
指针和引用都可以声明为constexpr
,但是constexpr
指针只可以被初始化为nullptr
或0
,或是储存于某个固定地址之中的对象。
- 如静态变量或全局变量可以用constexpr指针
- 局部变量不可以用constexpr指针
指针和constexpr
在constexpr声明中如果定义了一个指针,限定符constexpr仅仅对指针有效,与指针所指对象无关。
const int *p = nullptr; // p是一个指向整型常量的指针
constexpr int *q = nullptr; // q是一个指向整数的常量指针
- 关键的点在于constexpr将它定义的对象置为了顶层const
5. 处理类型
1)类型别名
类型别名是某种类型的同义词,使用类型别名的好处有:让复杂类型名字变简单、易于理解和使用
定义方法:
-
1
typedef double wage; typedef wage base, *p;
-
2
using SI = sales_item; //利用别名声明来定义
2)指针、常量和类型别名
注意一点,如果某个类型别名指代的是复合类型或常量,用到声明语句里会产生意想不到的后果,例如:
typedef char *pstring;
const pstring ptr = 0; // 这里ptr是一个指针,指向char的常量指针
// 错误理解:const char *ptr = 0;
// 这里的pstring的基本数据类型是指针
// char*重写了这个声明后,数据类型变成了char,*成了声明符的一部分
// 这意味着指向const char的指针
const pstring *ps; // 这里ps是指向char*的常量指针
3)auto类型说明符
编程时常常需要将表达式的值赋给变量,这要求在声明变量的时候就清楚的直到表达式的类型,但这并不容易,于是可以利用auto
类型说明符来处理这点,它可以让编译器替我们完成类型的说明。
复合类型、常量和auto
-
编译器得到的类型和初始值的类型并不完全一样,编译器会适时改变结果类型使其更符合初始化规则。
-
引用:auto类型将使得以引用初始化的对象类型为引用对象的类型
-
const:auto一般会忽略顶层const,而底层const会保留下来。
int i = 0, &r = i; auto a = r; // a为整型 const int ci = i, &cr = ci; auto b = ci; // b为整型 auto c = cr; // c为整型 auto d = &i; // d为整型指针 auto e = &ci;// e为指向常量的指针(对常量对象取地址视为const)
如果希望auto类声明的变量为顶层const,则需要明确提出
const auto f = ci; // f为常量
还可以将引用类型设为auto,此时原来的初始化规则仍适用:
auto &a = 42; // 非法 const auto &j = 42; // 合法
4)decltypel类型指示符
有时候希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这种需求,C++11引入了decltypel类型指示符,作用是选择并返回操作数的数据类型。
-
编译器通过分析表达式来得到它的类型,却不计算实际的值
decltype(f()) sum = x; // f()的返回值类型为sum的类型
这里编译器并非调用f(),而是使用它的返回值类型作为sum的类型。
-
decltype和auto的使用有所不同:返回该类型的变量(保留顶层和底层const)
const int ci = 0, &cj = ci; decltype(ci)x = 0; // const int decltype(cj)y = x; // const int & decltype(cj)z; // 错误:z为引用,必须初始化
decltype和引用
-
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
int i = 42, *p = &i, &r = i; decltype(i+0) b; // 正确,b为整型 decltype(*p) c; // 错误,*p为引用,c也为引用,需要进行初始化。
-
如果表达式的内容是解引用,则decltype将得到引用类型。如上面的
decltype(*p) c;
,c的类型为引用类型。 -
decltype的结果与表达式的形式密切相关
如:加上括号和不加括号的区别
变量名不加上括号,则得到的类型为该变量的类型;如果加上括号,将其看作表达式,最终将得到引用类型(永远如此)。
decltype((i)) d; // 错误:d为int&类型,需要初始化 decltype(i) e; // 正确:e为int类型