本系列主要记录我阅读C++ Primer一书时感觉需要总结的地方。本书第一章讲输入输出,比较简单,就不再专门记录。第二章讲变量,一些基础的内容略过,主要总结我感到比较重要也容易犯错的部分,即引用、指针和常量。本章其他一些内容也一并在内。本笔记只记录一些重要的内容,枝节被忽略。
变量的声明与定义
C++支持分离式编译,支持程序分为多个模块独自编译。为此,C++将声明(declaration)和定义(definition)分离。声明使得变量为程序所知,而定义创建与名字关联的实体,即申请存储空间并赋初值。
如果要声明而不定义一个变量,需要在变量名前添加extern关键字,且不能显式初始化变量。变量可以被声明多次而只能被定义一次。
声明符
一条声明由基本数据类型和声明符列表组成。一般来说声明符就是变量名。但对于像引用和指针这样的复合类型变量,声明则需要添加额外的内容。
声明引用时,需要在变量名之前添加&。声明指针前,需要在变量名前添加*。
注意不要把这里的&和*与操作符弄混。在用作操作符时,&的作用是取地址;*的作用是解引用,返回值是存储在该地址的变量。
引用
引用即别名。声明引用时,需要在变量名之前添加&。如果一条语句声明多个引用,则每个前都要加&。引用并非对象,也没有单独的存储位置,只是对一个对象起了另外的名字而已。引用必须被初始化,一旦绑定就不能再绑定到另一个对象上。引用必须绑定常量,而不是某个如3.14一样的值。(有例外)
如果绑定的对象不是常量,则可以通过引用修改被绑定变量的值,与直接赋值效果相同。
int &ref = i; //正确
int& ref = i; //这两种写法等价
int &ref2; //错误,引用必须被初始化
int &ref3 = 3 //错误,引用不能绑定一个值
int &ref4 = i*2; //错误,引用不能绑定表达式
ref = 2; //等价于i=2
指针
不同于引用,指针是一个单独的对象,指向的是某个变量的地址。声明指针前,需要在变量名前添加*。一条语句声明多个指针时,每个指针前都要加*。由于引用不是对象,没有地址,故不能定义指向引用的指针。
如果给指针赋值为0,则其为空指针,不指向任何对象。赋值为NULL或者nullptr也具有相同的效果。如果指针指向的不是常量,可以通过指针改变对应变量的值。除少数例外,指针类型必须要与指向对象的类型匹配。非常量指针可以重新赋值,指向别的对象。
特殊的是void*类型的指针可以指向任何对象,但不能对其进行操作。
int i = 0;
int *ip = &i; //ip是指向i的指针
int* ip = &i; //与上面的写法等价
int *ip2 = nullptr; //ip2是空指针,赋值为0或者NULL效果相同
*ip = 42; //给i赋值为42,等效于i=42
ip = &j; //ip变为指向j
指针也可以指向指针,可以通过在声明中增加*的个数来定义这样的类型。
也可以定义指针的引用。在声明时,从右往左阅读定义,离变量名最近的符号决定变量类型。
int *p; //p是一个指针
int *&r = p; //r是一个引用,是指针p的别名
常量
使用关键字const进行限定,可以得到常量。const对象一旦创建之后其值就不能被改变,所以必须要被初始化。
编译时,编辑器将文件中所有该变量都替换成对应的值。默认下,常量仅在文件内有效。在不同文件内定义同名const实际上是不同的变量。如果要在多个文件中贡献常量,需要用extern进行限定。在一个文件中定义,其他文件中声明并使用。每一处都要加上extern。
引用与常量
引用可以绑定到常量上。这时可以有一些例外情况。初始化常量引用时可以用任意表达式作为初始值,编译器会先将引用绑定一个临时值,再将初始值类型转换为临时值。
int i = 42;
const int &r1 = i;
const int &r2 = 42;
const int &r3 = i*2;
const double &r4 = i;
//上面语句都是正确的,但对于非常量引用则不合法
常量引用可以指向非常量,只是不能通过引用修改对象的值而已。
指针与常量
谈到指针与常量,就需要区分两个概念:常量指针与指向常量的指针。常量指针指的是指针本身就是一个常量,不能改变,即不能再指向别的对象。但可以通过常量指针改变其指向对象的值。指向常量的指针类似于常量的引用,说明不能通过该指针改变指向对象的值,其指向的也可以是一个非常量。如果要指向一个常量,则指针本身必须是指向常量的指针。
const double pi = 3.14;
double ptr = π //错误,普通指针不能指向常量
const double *cptr = &pi //正确,但不同通过cptr给pi赋值
val = 3.14;
cptr = &val; //正确,指向常量的指针也可以指向非常量
double *const p = &val; //p是一个常量指针,将永远指向val
const double *const pip = π //pip是一个指向常量的常量指针,永远指向pi
*p = 0; //正确,可以通过常量指针改变非常量的值
顶层const
顶层const表示指针本身就是常量,底层const表示指针指向的是常量。在指针的声明中,靠右的是顶层。一般地,顶层const可以表示任何变量为常量。声明引用的const全部都是底层。
int i = 42;
int *const p1 = &i; //顶层
const int a = 42; //顶层
const int *p2 = &a; //底层
const int *const p3 = p2; //右面的是顶层,左面的是底层
执行对象拷贝时,顶层const没有影响。但拷入和拷出的对象必须具有相同的底层const资格,或者对应的数据类型能够转换。非常量可以转换为常量,反之不行。
int *p = p3; //错误,p无底层const
p2 = p3; //正确
p2 = &i; //正确,非常量可以转为常量
int &r = a; //错误,常量不能变为非常量
const int &r2 = i; //正确
类型的处理
类型别名
可以通过typedef定义类型的别名。现在也可以通过using来声明。
typedef double wages;
using wages = double;
注意,复合变量或常量的类型别名会有很多复杂的结果。不能简单的将类型别名替换为其本来的名字进行解释。
typedef char *pstring //这里的pstring实际上是char *的别名
const pstring cstr = 0; //cstr是指向char的常量指针
const char *cstr = 0; //这样简单替换理解是错误的
const pstring *ps; //ps是指针,对象是指向char的常量指针
pstring的基本类型是指向常量的指针,在其前面加const表面其本身是个常量指针。而上面的错误示范中,char成了基本数据类型,而*却变成声明符的一部分,这种理解是错误的。最后一行里,基本数据类型是const pstring,即指向char的常量指针。
auto
auto可以让编译器替我们推断出变量类型,必须有初始值。auto从引用中推断出来的,是引用指向的对象的类型。即auto不会推断出来数据类型是引用。
auto一般会忽略顶层const。如果想要顶层const的话需要在auto前面加上。
decltype
decltype类型指示符返回操作数的作用类型。不同于auto,decltype可以返回引用,也会包括顶层const。
如果对象是表达式,则会返回运算的类型。加上括号或者多层括号会让操作符将括号内的内容当成表达式。
int i = 42, *p = &i, &r = i;
decltype(*p) a; //a是一个引用
decltype(r) b; //b是一个引用
decltype(i) c; //c是一个int
decltype((i)) d; //d是一个引用,双层括号结果一定是引用