2.1 基本内置类型
2.1.2 类型转换
首先了解下取模和取余的区别!!![取模与取余的区别]
- 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。如8bit大小的unsigned char 可以表示0到255区间内的值,当把-1赋给8bit大小的unsigned char所得到的结果是255.
- 当我们赋值给带符号类型一个超出它表示范围的值时,结果是未定义的。此时程序可能继续工作,可能崩溃,也可能产生垃圾数据。
提示:切勿混用带符号类型和无符号类型
2.1.3 字面值常量
2.2 变量
2.2.1 变量定义
初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来代替。
用花括号来初始化变量,这种初始化形式称为列表初始化。这种初始化形式有一个重要的特点:如果我们使用列表初始化且初始值存在丢失的风险,则编译器将报错。
long double ld = 3.1415926536;
int a{ld}, b = {ld}; //错误:转换未执行,因为存在丢失信息的风险
int c(ld), d = (ld); //正确:转换执行,确实丢失部分值
cout << a << " , " << b << endl;
cout << c << " , " << d << endl;
但是使用不同的编译器会发生不同的处理情况:
VS 2019 msvc编译器:
VSCode MinGW-64 G++ 编译器:
如果定义一个变量时没有指定初值,则变量被默认初始化。定义于任何函数体之外的变量被初始化为0。定义在函数体内部的变量将不被初始化。一个未初始化的变量的值是未定义的,如果试图拷贝或者以其他形式访问此类值将引发错误。
建议初始化每一个内置类型的变量。虽然并非必须这么做,但如果我们不能确保初始化后程序安全,那么这么做不失为一种简单可靠的办法。
2.2.2 变量声明与定义的关系
如果想声明一个变量而非定义它,就在变量名前加关键字extern,而且不要显示地初始化变量:
extern int i; //声明i而非定义i
int j; //声明并定义j
extern int pi = 3.1415926; //定义
在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。
变量能且只能被定义一次,但是可以多次被声明!
如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其它用到该变量的文件必须对其进行声明,却绝对不能重复定义。
2.2.3 标识符
C++的标识符有数字,字母和下划线组成,其中必须以字母或下划线开头。用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧连大写字母开头。此外,定义在函数体外的标识符不能以下划线开头。
变量命名规范
- 标识符要能体现实际含义。
- 变量名一般用小写字母,如index,不要使用Index或INDEX
- 用户自定义的类名一般以大写字母开头,如Sales_item。
- 如果标识符有多个单词组成,则单词间应有明显区分,如student_loan 或 studentLoan
2.2.4 名字的作用域
2.3 复合类型
本章主要介绍其中的两种:引用和指针
2.3.1 引用
引用必须被初始化!
除了两种例外情况,其它所有引用的类型都要和与之绑定的对象严格匹配。而且,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。
int &ref = 10; //错误,引用类型的初始值必须是一个对象
double dval = 3.14;
int &ref2 = dval; //错误,此处引用类型的初始值必须是int类型对象
特殊情况:
1. const int &r2 = 42; // ok: r1 is a reference to const
2. TO-DO
2.3.2 指针
第一,指针本身就是一个对象,允许对指针进行赋值和拷贝,而且指针的生命周期内它可以先后指向几个不同的对象。第二,指针无须在定义时赋初值。和其它内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
获取对象的地址
指针存放某个对象的地址,要想获取该地址,需要使用取地址符(&):
int ival = 42;
int *p = &ival; // p存放变量ival的地址,或者说p是指向变量ival的指针
一般情况下,指针的类型和它所指向的对象严格匹配。
指针值
指针的值(即地址)应属于下列四种状态之一:
1. 指向一个对象。
2. 指向紧邻对象所占空间的下一个位置。
3. 空指针,意味着指针没有指向任何对象。
4. 无效指针,也就是上述情况之外的其他值。
利用指针访问对象
解引用符(操作符*)
空指针
空指针(null pointer)不指向任何对象。几个生成空指针的方法:
int *p1 = nullptr; //等价于 int *p1 = 0
int *p2 = 0; //直接将p2初始化为字面常量0
//需要首先#include cstdlib
int *p3 = NULL; //等价于 int *p3 = 0
得到空指针最直接的办法就是用字面值nullptr来初始化指针,这也是C++11新标准引入的一种方法。nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。
过去的程序还会用到一个名为NULL的预处理变量来给指针赋值,这个变量在头文件cstdlib中定义,它的值就是0。
当用到一个预处理变量时,预处理器会自动地将它替换为实际值,因此用NULL初始化指针和用0初始化指针是一样的。在新标准下,C++程序最好用nullptr,同时尽量避免使用NULL。
建议:初始化所有指针!
在可能的情况下,尽量等定义了对象后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr和0,这样程序就能知道它没有指向任何具体的对象了。
赋值和指针
有时候想搞清楚一条赋值语句到底是改变了指针的值还是改变了指针所指对象的值不太容易,最好的办法就是记住赋值永远改变的是等号左侧的对象。
int i = 42;
int *pi = &i;
pi = &val; //pi的值被改变,现在pi指向了val。意思是为pi赋予一个新的值,也就是改变了那个存放在pi内的地址值
*pi = 0; //val的值被改变,指针pi并没有改变。则*pi(也就是指针pi指向的那个对象)发生改变
void* 指针
void * 是一种特殊的指针类型,可用于存放任意对象的地址。
面试题 指针和引用的主要区别?
答:指针“指向”内存中的某个对象,而引用“绑定”到内存中的某个对象。二者主要区别在两个方面:
第一,指针本身就是一个对象,能够对指针赋值和拷贝,而且在指针生命周期内它可以指向几个不同的对象;引用不是一个对象,无法令引用重新绑定到另外一个对象。
第二,指针无须在定义时赋初值,和其它内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值;引用则必须在定义时赋初值。
2.3.3 理解复合类型的声明
面向一条比较复杂的指针或引用的声明时,从右往左阅读有助于理解它真实的含义。
2.4 const 限定符
const对象一旦创建后其值就不能改变,所以const对象必须初始化。
const int bufSize = 512; //输入缓冲区大小
默认状态下,const对象仅在文件内有效。
如果想在多个文件中之间共享const对象,必须在变量的定义之前添加extern关键字。
解决办法就是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了:
//file_1.cc 定义并初始化一个常量,该常量能被其它文件访问
extern const int bufSize = fcn();
//file_1.h头文件
extern const int bufSize; //与file_1.cc定义的bufSize 是同一个
2.4.1 初始化和对const的引用
引用的类型必须与其所引用对象的类型一致,但其一中例外情况就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能够转换成引用的类型即可。
允许为一个常量引用绑定非常量的对象,字面值,甚至是个一般表达式:
int i = 42;
const int &r1 = i; //允许将const int &绑定到一个普通int对象上
const int &r2 = 42; //r2是一个常量引用
const int &r3 = r1 * 2; //r3是一个常量引用
int &r4 = r1 * 2; //错误,r4是一个普通的非常量引用
double dval = 3.14;
const int &ri = dval;
2.4.2 指针与const
指向常量的指针不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针:
const double pi = 3.14; //
double *ptr = π //错误,ptr是一个普通指针
const double *cptr = π//正确
*cptr = 42; //错误,不能给*cptr赋值
const指针
常量指针必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。即不变的是指针本身的值而非指向的那个值。
int errNumb = 0;
int *const curErr = &errNumb; //curErr将一直指向errNumb
*curErr = 1;//正确 注意这点!
const double pi = 3.14;
const double *const pip = π //pip是一个指向常量对象的常量指针
2.4.3 顶层const
指针本身是一个对象,它又可以指向另一个对象。因此指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。用名词顶层const表示指针本身是个常量,而用名词底层const表示指针所指的对象是一个常量。
指针类型既可以是顶层const,又可以是底层const。
int i = 0;
int *const p1 = &i; //不能改变p1的值,这是一个顶层const
const int ci = 42; //不允许改变ci的值,这是一个顶层const
const int *p2 = &ci; //允许改变p2的值,这是一个底层const
const int *const p3 = p2; //靠右的const是顶层const,靠左的const是底层const
2.4.4 constexpr和常量表达式
常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式。
指针和constexpr
在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:
const int *p = nullptr; //p是一个指向整型常量的指针
constexpr int *q = nullptr; //q是一个指向整数的常量指针
2.5 处理类型
2.5.1 类型别名
有两种方法可用于定义类型别名,传统的方法是使用关键字 typedef
typedef double wages;
新标准规定了一种新方法,使用别名声明。
using SI = Sales_Item;
指针,常量和类型别名
typedef char *pstring;
const pstring cstr = 0; //cstr 是指向char的常量指针
const pstring *ps; //ps是一个指针,它的对象是指向char的常量指针
2.5.2 auto类型说明符
C++11 新标准引入了auto类型说明符,auto让编译器通过初始值来推算变量的类型。显然,auto定义的变量必须要有初始值:
auto一般会忽略掉顶层const,同时底层const会保留下来。
2.5.3 decltype类型指示符
//decltype的表达式如果是加上了括号的变量,结果将是引用
decltype((i)) d; //错误:d是int&,必须初始化
decltype(i) d; //正确:e是一个未初始化的int
面试题 说明由decltype和由auto指定类型有何区别?
auto和decltype的区别主要在三个方面:
第一,auto类型说明符用编译器计算变量的初始值来推断其类型,而decltype虽然也用编译器分析表达式并得到它的类型,但是不实际计算表达式的值。
第二,编译器推断出的auto类型有时和初始值的类型并不完全一样,编译器会适当改变结果类型使其更符合初始化规则。例如,auto一般会忽略掉顶层const,而把底层const保留下来。与之相反,decltype会保留变量的顶层const。
第三,与auto不同,decltype的结果类型与表达式形式密切相关,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则编译器将推断得到引用类型。