之前看过一遍《C++ Prime Plus》,可是没有做笔记,一段时间后,基本也记不住了,最近结合先关视频,再次捡起来,结合此书做出笔记,不断查漏补缺。
基础概念平时显得微不足道,但不积跬步何以至千里…
参考资料:
[1] 《C++ Prime Plus》第五版
1、C++基本内置类型和变量
1.1 基本内置类型
1.2 变量
初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值(为其分配一块地址空间),而赋值的含义是把当前对象的当前值擦出,而以一个新值来代替(已经分配过地址空间了)
变量的定义和声明:
变量的声明只规定了变量的类型和名字;
而变量的定义不仅规定了变量的类型和名字,还为变量申请了存储空间,也可能会为变量赋一个初始值(初始化)。
变量能且只能被定义一次,但是可以被多次声明。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而不要显式地初始化变量:
extern int i; //声明i而非定义i
int j; //声明并定义j
如果声明进行了初始化,即使前面加上了extern关键字,也会被当做定义。
extern double pi = 3.14; //定义
2、指针
2.1 指针基础
指针存放着某个对象的地址,要想获取该指针,需要使用操作符&。
除极个别的情况,指针类型要和他所指向的对象严格匹配。
- 指针本身就是一个对象,允许对指针的赋值和拷贝,而在指针的生命周期内他可以先后指向几个不同的的对象。
- 指针无需在定时赋初值。和其他内置类型(如char、int、float) 一样,在块作用域内的指针如果没有被初始化,也将拥有一个不确定的值。
double dval;
double *pd = &dval; //正确:初始值是double型对象的地址
double *pd2 = pd; //正确:初始值是指向double对象的指针
int *pi = pd; //错误:指针pi的类型和pd的类型不匹配
pi = &dval; //错误:试图把double型对象的地址赋给int指针
指针的值应该属于以下四种状态之一:
- 指向一个对象
- 指向紧邻对象所占空间的下一位置
- 空指针,指针没有指向任何对象(
nullptr
) - 无效指针,上诉情况以外的其他值,试图访问或者拷贝无效指针的值都将引起错误,而编译器并不负责检查此类错误
- 任何非零的指针所对应的的条件值都是
true
对于空指针,C++中有以下几种生成的方法:
int *p1 = nullptr; //等价于int *p1 = 0;
int *p2 = 0; //直接将p2初始为字面常量0
//需要首先include <cstdlib>
int *p3 = NULL; //等价于int *p3 = 0;NULL为预处理变量(运行于编译之前)
//最好使用nullptr
void*指针:
void* 指针是一种特殊的指针,可以用于存放任意对象的地址。
double obj = 3.14,*pd = &obj;
void *pv = &obj; //正确:void*能够存放任意对象的地址,obj可以是任意类型的对象
pv = pd; //pv可以存放任意类型的指针
void*
指针所能做
的事情:
- 和其余指针作比较
- 作为函数的输入输出
- 赋值给另外一个void*指针
void*
指针所不能做
的事情:
- 不能直接操作void*所指向的对象,因为并不知道找个对象到底是什么类型
总之,从void*的视角来看,内存空间仅仅只是内存空间,没有办法访问内存空间所存的对象。
2.2 指针常量和常量指针
2.2.1. 常量指针
2.2.2. 指针常量
3、引用
在C语言中,指针可以说是家常便饭,但是指针的错误使用,将会造成不可估量的错误(会破坏其余内存的内容)。故在C++中引出了一个作用类似于指针的操作方法 — 引用。
引用: 为对象另起的一个别名(外号)。
实例:
int ival = 1024;
int &refval = ival; //refval指向ival
int &refval2; //错误,引用必须初始化
int &refval3 = 1024; //错误,引用初始化必须是一个对象
double r3 = 1.2;
int &refval4 = r3; //错误,此处引用类型的初始值必须是一个int类型对象
注意:
- 引用仅是变量的别名,而不是一个实实在在的定义了一个变量。因此,引用并不占用内存,而是和目标变量共同指向目标变量的内存地址。
- 取址符号
&
不在是取变量地址,而是用来表示该变量的引用类型的 - 定义了一个引用,必须对其初始化,且初始化时初始值必须是一个对象,该对象的类型要和引用相一致
- 对引用的修改就是对变量的修改
- 引用本身不是一个对象,所以不能定义引用的引用
指针与引用的区别:
引用本身并不是一个对象,一旦定义了引用,就无法令其在绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。(从一而终)
而指针则没有这个区别,给指针赋值就是令它存放一个新的地址,修改这个值就能
令指针指向一个新的对象。
总结:
相同点: 都是地址的概念
指针指向一块内存,它的内容是所指内存的地址;引用是某快地址的别名。
不同点:
- 指针是一个实体,而引用只是别名
- 引用使用时无需解引用(*),指针需要解引用
- 引用只能定义是被初始化,之后不能改变;而指针可变
- 引用没有const,指针有const,const指针不可以改变
- 引用不能为空,指针可以为空
- 指针与引用的自增(++)运算意义不相同
指针和引用的联系:
- 引用在语言内部用指针实现
- 对一般应用而言,把引用理解成指针,不会犯严重的错误。引用是操作受限了的指针(仅容许取内容操作)。
指向指针的引用:
引用本身不是对象,因此不能定义指向引用的指针。但是指针是对象,实实在在存在的,因此存在指针的引用。
int i =42;
int *p; //p是一个int类型的指针
int *&r = p; //r是一个对指针p的引用
r = &r; //r引用了一个指针,因此给e幅值&r就是令p指向i
*r = 0; //解引用r得到i,也就是p指向的对象,将i的值改为0
4、const限定符
const对象一旦创建以后,其值就不能改变,所以const对象必须初始化。const对象初始化可以是任意的 表达式。
const int i = get_size(); //正确:运行时初始化
const int j = 42; //正确:编译时初始化
const int k; //错误:k是一个未经初始化的常量
默认情况下,const对象仅在文件内有效: 当多个文件出现了同名的const变量时,等同于在不同文件中分别定义了独立的变量。如果想在多个文件之间共享const对象,必须在变量定义之前添加extern关键字。
const和define相比较,有什么优点?
- const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行安全检,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时,可能产生想象不到的错误(边际效应)。
- 有些集成化的调试工具,可以对const进行调试,但是不能对宏常量进行调试
对const引用:(常量引用 reference to const) 把引用绑到const对象上。与普通引用相不同的是,对常量的引用不能够用来修改它所绑定的对象。
const int ci = 1014;
const int &r1 = ci; //正确:引用及其对应的对象都是常量
r1 = 42; //错误:r1是对常量的引用
int &r2 = ci; //错误:试图让一个非常量引用指向一个常量对象
注意:引用的类型必须要和所引用的对象类型相一致,但是有两个情况例外
- 初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能够转换成引用类型即可
- 允许为一个常量引用绑定非常量的对象、字面值,甚至是一般表达式
int i =42;
const int &r1 = i; //允许将const &绑定到一个非常量对象上(2.)
const int &r2 = 42; //(2.)
const int &r3 = r1*2; //正确:r3是一个常量引用
int &r4 = r1*2; //错误:r4是一个普通的非常量引用
指针和const:
指针的类型必须与其所指对象的类型相一致。但是有两个例外:
- 允许令一个指向常量的指针指向一个非常量的对象
和常量饮用一样,指向常量的指针也没有规定其所指向的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有滚定那个对象的值不能通过其他途径改变。
小结:
指针常量(底层const,所指对象是常量(指针自己以为是常量,是不是的要看你的初始化)):
- 常量指针指向的对象不能通过这个指针修改,可任然可以通过原来的声明来修改;
- 常量指针可以被赋值为变量的地址,之所以叫常量指针,是限制了通过这个指针来修改所指对象的值
- 指针还可以指向别处,因为指针本身只是一个变量,可以指向任意地址
常量指针(顶层const,指针本身是常量):
指针常量的值是指针,因为这个值是常量,所以不能被赋值
- 常量指针是一个常量
- 指针本身是常量,指向的地址不可以变化,但指向地址所对应的的内容可以变化。
constexpr
和常量表达式:
常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式
。
一个对象是不是常量表达式由它的数据类型和初始值所共同决定。
- 字面值是常量表达式
- 使用常量表达式初始化的const对象也是常量表达式
字面值类型: 算数类型、引用和指针都属于字面值类型。
const int max_files = 20; //max_files 是常量表达式
const int limit = max_files +1; //limit是常量表达式
int staff_size = 27; //staff_size不是常量表达式
const int sz = get_size(); //sz不是常量表达式
5、类型处理
5.1 类型别名
有两种方法实现类型别名:
- 使用关键字typedef
typedef double wages; //wages是double的同义词
typedef wages base,*p; //base是double的同义词,p是double*的同义词
- 新标准规定了一种新的方法,使用类型别名(alias declaration)来定义类型别名
等同于把等号左侧的名字规定为右侧类型的别名。
如果某个类型别名指代的是复合类型或常量,那么他用到声明语句里就会产生意想不到的后果。
using SI = Sales_iteml; //SI是Sales_item的同义词
5.2 auto类型说明符
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。
auto
定义的变量必须初始化。- 使用auto可以在一条语句当中声明多个变量,但该语句中所有变量的类型必须都是一样的,这与int、double等是一样的
- 编译器会以引用对象的类型作为auto类型
- auto一般会忽略顶层const,同时底层const会被保留下来
- 设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留
错误示范:
auto item;
item = val1 + val2;
正确示范:
auto = val1 + val2;
而int定义的变量可以不初始化(虽然这样非常不推荐)
int item;
item = val1 + value;//正确
auto i = 0,*p = &i; //正确:i是整数、p是整型指针
auto sz = 0,pi = 3.14; //错误:sz和pi的线、类型不一样
5.3 decltype类型指示符
decltype:选择并返回操作数的类型。 编译器分析表达式并得到它的类型,却不实际计算表达式的值。
decltype(f()) sum = x; //sum的类型就是函数f的返回类型
操作数: 是运算符作用的实体,是表达是的一个组成部分,它规定了指令中进行数学运算的量。表达式是操作数和操作符的结合。
例如:a + b:a、b是操作数,+是操作符
- 如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
- 如果表达式的内容是解引用操,则decltype将得到引用类型
/* decltype的结果可以使引用类型 */
int i = 42,*p = &i,&r = i;
decltype(r+0) b; //正确:加法的结果是int,因此b是一个未初始化的int
//局部变量未初始化值随机、全局变量未初始化值为0
decltype(*p) c; //错误:c为引用,引用必须使用对象进行初始化、
decltype(*p + 0) d; //正确:加法的结果是int,因此d是一个未初始化的int
decltype和auto的重要区别:decltype的结果类型与表达式形式密切相关。对于decltype所用的表达式来说,如果变量名加上一对括号,则得到的类型玉不加括号时会有不同。如decltype使用的是一个不加括号的变量,得到的就是该变量的类型;如加上括号,编译器就会把它当成一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型。
//decltype的表达式如果是加了括号的变量,结果将是引用
decltype((i)) a; //错误:a是int&,必须使用对象进行初始化
decltype(i) b; //正确:e是一个(未初始化的)int
切记:
decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身是一个引用是才是引用。