引用,指针与const
两条规则:
(1)定义引用时,程序会把引用和它的初始值对象一直绑定(bind)在一起
(2)引用绑定了一个对象后不能重新绑定到另一个对象
规则(1),与拷贝不同,引用和对象绑定后,改变其中一个,另一个也会跟着改变。换而言之,引用是为一个已经存在的一个对象起的另一个名字(注意引用本身并非对象,引用即别名),所以引用只能绑定对象不能绑定字面值或者表达式的计算结果。例如,小明的别名叫小胖,叫小明起床,小明起床了,意味着小胖也起床了,小明和小胖是一回事。
int ivalue = 1;
int &ref1 = ivalue;
cout << ref1 << endl; //1
ref1 = 2;
cout << ivalue << endl; //2
ivalue = 3;
cout << ref1 << endl; //3
int &ref2 = 10; //错误,引用只能绑定对象不能绑定字面值或者表达式的计算结果```
规则(2),引用绑定了一个对象后不能重新绑定到另一个对象,意味着在引用定义后不能再出现&ref1=XXX的赋值语句(注意区分&ref1=XXX和ref1=XXX),如果引用在定义时不初始化引用,那么这个引用永远都不能初始化了,因此引用在定义时必须初始化。
int &ref3; //错误,引用在定义时必须初始化
再来看看下面的例子:
double dvalue = 3.14;
int &ref4 = dvalue; //错误,此处引用的初始值必须是int对象
上述这个例子中,编译器为了让ref4绑定一个int类型的数,会做这样的处理:
int temp = dvalue;
int &ref4 = temp;
这样ref4绑定的是一个临时量而非dvalue,那么改变ref4的值改变的也不是dvalue的值,那么定义ref4这个引用毫无作用也毫无意义,C++也把这种行为定为非法。
(1)关于内存地址:
假设内存是一个一个个的储物柜,每个柜能放一样东西,现在有166(16的6次方)个柜子,为了能够快速准确地找到每个柜子,可以给每个柜子编号。
容易想到一种编号方式从166(16的6次方),但是这个数字可能会更大,甚至会溢出。另一种编号方式可以给每个柜子分配6位数的编码,每一位从0-15(但是10-15的编号容易产生歧义,比如0000111,那么是指0000 11 1还是0000 1 11呢,因此将10-15用a-f来替换,那么每个柜子都获得一个唯一的没有歧义的编码,我们将这个编码称为地址)。
(2)指针
指针顾名思义指向某个对象,与引用不同(引用非对象,所以没有指向引用的指针),指针本身是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内可以先后指向几个不同的对象,且指针无须在定义时初始化。
假设现在要定义一个int类型的指针,可以写成int* p或者int *p,这里更倾向于使用后者int p,因为倘若使用前者int p这种定义方式:
int* p1, p2; //p1是int指针,p2是int变量,容易让人以为p2也是指针
指针存储对象的地址,要想获取对象的地址要用到取地址符(操作符&)
int ivalue1 = 1;
int *p3 = &ivalue1; // p3储存ivalue1的地址,或者说p3是指向ivalue1的指针
假设ivalue1的值放在编号为34FFCC的柜子里,那么p3存储的就是34FFCC这个地址,如果想要取出这个柜子里面ivalue1的值,可以使用解引用符(操作符*)来访问指针所指向的对象。
cout <<"p3="<< p3<<" ,*p3="<<*p3 << endl; // p3=34FFCC(假设是这个地址,现实中地址位数和地址值可能有差异) ,*p3=1
除定义时赋值外,一般来说(会有例外情况)对指针进行赋值要严格匹配,例如上述对p3进行赋值,那么赋值运算符(=)右边要是同类型对象的地址或者指针,而对*p3赋值必须是同类型对象。假设p3现在指向柜子A,对p3赋值相当于p3本来指向柜子A转而指向柜子B,改变了指针指向;对*p3赋值相当于打开柜子A,改变里面存放的数值,指针指向不变。
int ivalue2 = 2;
int *p4 = &ivalue2;
double dvalue1 = 3.2;
double *p5 = &dvalue1;
p3 = &ivalue2; //将同类型对象的地址赋给p3
p4 = p3; //将同类型指针赋给p4
p3 = &dvalue1; //错误,将double类型对象的地址赋给int类型指针
p3 = p5; //错误,将double类型指针赋给int类型指针
p3 = ivalue2; //错误,将int类型赋值给int指针
*p3 = &ivalue2; //错误,将int类型地址赋给int类型
空指针不指向任何对象,空指针可以这样定义:
int *p6 = nullptr; //等价于int *p6 = 0;
int *p7 = 0; //直接将p7初始化为字面常量0
int *p8 = NULL; //等价于int *p8 = 0;(NULL是个预处理变量,在头文件cstdlib中定义,因此使用前要 #include )
int zero = 0;
p8 = zero; //错误,不能把int变量直接赋给指针,即使int变量直接赋给指针
强烈建议初始化所有指针
int *p9;
*p9 = 1; //有的编译器会报错,有的不会报错,这样使用未初始化的指针很危险
上述例子中定义了指针p9但是没有初始化,此时p9指向哪个内存地址完全是随机的,是一个野指针,运气好的话可能分配了一个空地址,运气不好可能分配了程序所在地址,或者操作系统正在使用的内存地址等非法地址,可能导致程序崩溃或者系统发生错误等一系列不可预知的错误。所以强烈建议初始化所有指针,尽量等变量定义后再定义指向它的指针。如果不清楚指针指向何处,就把它初始化为空指针。
只要指针不是野指针,就能用于条件判断。如果指针的值是0,条件判断结果就为false,否则为true。对于两个类型相同的合法指针,还能用==和!=来比较。如果两个指针存放的地址值相同,则它们相等,否则不等。
int *p10 = 0;
int *p11 = &ivalue1;
if(p10) //false
....
if(p11) //true
.....
void*指针可以用于存放任意对象的地址,但是我们不知道这个地址里面到底是什么类型的对象,我们无法确定能在这个对象上面做哪些操作,所以不能直接操作void指针所指的对象,但是可以拿它与别的指针比较,作为函数输入输出,赋值给另一个void指针。
const int bufSize = 512;
上述语句把bufSize定义成了一个常量,任何试图给bufSize赋值的行为都将引发错误,但是对const对象可以执行不改变其内容的操作,例如算法运算,转换为布尔值等等。和引用类似,因为const对象一旦创建后其值就不能再改变,因此const对象必须初始化。
const int k;//错误,const对象必须初始化
当以编译时初始化的方式定义一个const对象时,例如上述的bufSize的定义,编译器将在变异过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufSize的地方,然后用512替换。
默认状态下,const对象仅在文件内有效。当多个文件中出现了同名const变量时,其实等同于在不同文件中分别定义了独立的变量。如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。
//file1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
//file1.h头文件
extern const int bufSize; //与file1.cc中定义的bufSize是同一个
(1)对常量的引用
可以把引用绑定到const对象上,但是与普通引用不同的是,对常量的引用不能修改它所绑定的对象。例如下面例子中,r1是对常量的引用,把42赋值给r1是不允许的。
const int ci = 1024;
const int &r1 = ci; //正确,引用及
r1 = 42; //错误,r1是对常量的引用
不能将一个非常量引用指向一个常量对象。如下面例子,假设这种操作是被允许的话,那么可以通过给r2赋值修改ci的值,而ci是const变量,const变量除了定义时初始化外是不允许被改变内容的,这里发生了矛盾。因此不能让一个非常量引用指向一个常量对象。
int &r2 = ci; //错误,试图让一个非常量引用指向一个常量对象
对const的引用可能引用一个非const对象
int i = 42;
int &r2 = i; //引用r2绑定对象i
const int &r3 = i; //r3也绑定对象i,但是不允许通过r3修改i的值
r2 = 0; //r2并非常量,i的值修改为0
r3 = 0; //错误:r3是一个常量引用
(2)指针与const
指向常量的指针不能用于改变其所指对象的值。同引用一样,不能将一个指向非常量的指针指向一个常量对象,要想存放常量对象的地址,只能使用指向常量的指针。但是可以令一个指向常量的指针指向非常量对象,只是不能通过这个指针改变变量的值。
const double pi = 3.14; //pi是一个常量,它的值不能改变
double *ptr = π //错误:*ptr是一个普通指针,要想存放常量对象的地址,只能使用指向常量的指针
const double *cptr = π //正确,要想存放常量对象的地址,只能使用指向常量的指针
*cptr = 42; //错误,不能给*cptr赋值,指向常量的指针不能用于改变其所指对象的值
double dval = 3.14;
cptr = &dval; //正确,但是不能通过cptr改变dval的值
把*放在const关键字之前用来说明指针本身是一个常量,称为常量指针。常量指针必须初始化,而且初始化后指针存放的地址不能改变。注意区别:指向常量的指针是指针指向的柜子里面的值不能变,指针的指向可能可以改变;常量指针是定义时就必须说明指向哪个柜子,这个指向不能改变,但是柜子里的值可能可以改变(如果是一个指向常量的常量指针就不能改变)。
int errNumb = 0;
int *const curErr = &errNumb; //curErr将一直只想errNumb
const double pi = 3.14;
const double *const pip = π //pip是一个指向常量的常量指针
要区别是指向常量的指针还是常量指针,可以看const修饰的是什么。例如上述例子中const修饰的是curErr,curErr存放的是地址,所以是指针本身是常量,是一个常量指针。而前面 const修饰的是 *ptr,*ptr存的是相应地址的柜子里的值,所以是指向常量的指针。
参考文献:《C++ Primer第五版》
部分摘自原文,如有笔误或者错误请指出