signed

QiShunwang

“诚信为本、客户至上”

C++笔记——引用,指针与const

2021/1/28 15:35:53   来源:

引用,指针与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 <cstdlib>)
	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限定符

	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 = &pi;	//错误:*ptr是一个普通指针,要想存放常量对象的地址,只能使用指向常量的指针
	const double *cptr = &pi;	//正确,要想存放常量对象的地址,只能使用指向常量的指针
	*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 = &pi;	//pip是一个指向常量的常量指针

要区别是指向常量的指针还是常量指针,可以看const修饰的是什么。例如上述例子中const修饰的是curErr,curErr存放的是地址,所以是指针本身是常量,是一个常量指针。而前面 const修饰的是 *ptr,*ptr存的是相应地址的柜子里的值,所以是指向常量的指针。

参考文献:《C++ Primer第五版》
部分摘自原文,如有笔误或者错误请指出