C/C++引用与指针基础及进阶使用详解

一、引用

引用类型同指针类型一样,属于C++的一种复合类型。引用也就是为对象起了一个别名,引用类型引用另外一种类型,而非引用类型。

1.1 引用的定义

一般在初始化变量时,初始值会被拷贝到新建的对象中。而在定义引用时,程序把引用和它的初始值绑定到一起,而不是将值拷贝给引用。一旦初始化完成,引用将和它的初始值永远绑定在一起,不能再重新绑定到另一个对象,所以定义引用一定要初始化,且初始值为对象。

	int ival = 1024;
	int& refVal = ival, ival2 = 110;//refVal 是ival的另一个名字
	int& refVal4 = ival, & refVal5 = ival2;

引用只能绑定到对象上,而不能与对象或者某个表达式计算的结果绑定到一起,引用的类型都要与之绑定的对象严格匹配。

	int& refVal1;//错误:引用必须要初始化
	int& refVal2 = 10;//错误:初始值必须为对象,10只是一个字面值。
	
	double dval;
	int& refVal3 = dval;//错误:引用类型同引用引用的类型不一致

因为引用本身并不是一个对象,所以不能定义引用的引用,但是有指向指针的指针。

	int ival3 = 1024;
	int& refVal6 = ival3;
	int&& refVal7 = refVal6;//错误:不存在引用的引用

	int ival3 = 1024;
	int& refVal6 = ival3;
	int& refVal7 = refVal6;//但是这么写正确的,看引用的操作

1.2 引用的操作

定义一个引用后,对其进行的所有操作,都是在与之绑定的对象上进行的。为引用赋值,实际上是把值赋给了与引用绑定的对象;获取引用的值,实际上获取的也是与引用绑定的对象的值。

	int ival = 1024;
	int& refVal = ival;
	refVal = 2048;//实际上赋值给了ival
	cout << ival << endl;//2048
	cout << refVal << endl;//2048

同理,以引用作为初始值,实际上是以与引用绑定的对象作为初始值。

	int ival3 = 1024;
	int& refVal6 = ival3;
	int& refVal7 = refVal6;//refVal6是ival3 的另一个名字,实际上也是ival3

1.3 引用与const

const限定符

const限定符对变量起到了一个限定的作用,使用它定义的变量,其值不能被改变,值不能改变的变量,称其为常量。比如const int ci = 1024;这个定义,ci就是一个常量,一旦定义,任何有可能会改变ci值的操作都是错误的,而不改变ci值的操作,都是正确的,如算数运算等。所以所有常量的定义,都必须初始化,否则就会自相矛盾(常量一旦定义,就不能再做改变其值的操作嘛),比如这样定义就是错误的const int ci;ci不允许除了初始化之外的赋值操作)。

int i = 1;const int j = 1;j只有被限制不能做出改变其值的操作,其余都和i一样。利用一个对象a初始化另一个对象b,对象a的值不会被改变,所以对象初始化时是不是const无关紧要。

	int i = 1;
	const int ci = i; // 正确:i的值被拷贝给ci
	int j = ci;	// 正确:ci的值被拷贝给j

这里有一个很有趣的操作:

	const double d = 1.25;
	int i = d;
	cout << i << endl; // i = 1;

给人的感觉好像是把变量d的值1.25转成了1,其实不然。int i = d;这个操作,是把变量d中的值拷贝一份,再将拷贝的值强转成int类型,最后再赋值给变量i。对于值的拷贝不会改变这个值,且拷贝出来的只是一个值,至于对拷贝出来的副本如何操作,与被拷贝的值已经没有任何关系了。

对const的引用(常量引用)

可以把引用绑定到const对象上,就像绑定到其他对象上一样,我们称其为对常量的引用。与普通引用不同的是,对常量的引用(对常量的引用简称常量引用,如const int& rci = 123;rci就是一个常量引用)不能被用作修改它所绑定的对象。

	const int ci = 1024;
	const int& rci = ci; // 正确,ci是常量对象,rci是常量引用
	rci = 44; // 错误,常量引用不能修改所引用的对象

常量对象的值不能被改变,那么绑定在常量对象上面的引用,也不允许通过其改变常量值,由于非常量引用具有改变绑定对象值的权限,所以常量对象只能被常量引用所绑定。

	const int ci = 1024;
	int& ri = ci; // 错误,ci是常量对象,ri是非常量引用,ri具有改变绑定对象值的权利,所以不能让其绑定常量对象

【注】所以说,常量对象只能被常量引用绑定。

那上面这句话反过来说“常量引用只能绑定常量对象”是否成立呢?那肯定是不行的。因为常量引用的限制,只是对引用的限制,而非是对引用绑定的对象的限制。常量引用不但可以绑定非常量对象,也可以绑定非常量字面值、表达式。

	int i = 42;
	const int& ri = i;
	const int& r1 = 123;
	const int& r2 = i * 2;
	const int& r3 = r1 * r2; // 这里注意,r1和r2只是引用,本身并不是对象,而是绑定着i对象

进一步对上面概念加深理解:

	double d = 1.1;
	const int& rd = d; // 正确
	cout << d << endl; // 值为:1.1
	cout << rd << endl; // 值为:1

drd输出的值不一样,那是不是说明,rd引用绑定的对象并不是d?确实是如此,编译器在出来上面代码时,做了如下的处理:

	double d = 1.1;
	const int i = d;
	const int& rd = i;

所以,常量引用绑定非常常量对象时,首先会将非常量对象的值赋给一个临时的常量对象,然后再将常量引用绑定到这个临时的常量对象上面。

那下面代码,为什么i的值改变,ri的值没有变,也就能理解了。(再次申明,常量引用只是对引用的限制)

	int i = 42;
	const int ri = i;
	i = 1;
	cout << i << endl; // 值为:1
	cout << ri << endl; // 值为:42

1.4 引用作为函数参数和返回值

C++函数的参数传递和返回值

二、指针

指针是指向另外一种类型的复合类型,与引用类似,指针也实现了对其他对象的间接访问。但是与引用相比又有许多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值,和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。

2.1 指针的定义与操作

在定义时,*用来定义一个指针,&用来定义一个引用;在非定义语句或表达式中,*被称为解引用符,用来访问指针所指的对象,&称为取地址符,用来获取一个对象的地址。指针也是对象。

	int a = 10, *pa, &ra = a, **ppa, *pnull;
	pa = &a;
	*pa = 110;
	ppa = &pa;
	cout << a << endl;//110
	cout << *pa << endl;//110
	cout << ra << endl;//110
	cout << **ppa << endl;//110,相当于*(*ppa)

因为引用不是对象,没有实际地址,所以不能定义指向引用的指针,但是可以定义指向指针的指针,如上面的ppa就是指向指针的指针,所指对象为pa。

在这里插入图片描述

且指针类型必须与其所指对象类型严格匹配。

	int b = 11, &rb = b;
	int *rpb;
	long *pa;
	pb = &b;//错误:int和long类型不匹配

虽然不能定义指向引用的指针,但是下面操作是正确的。引用rb引的是变量b,一旦对引用初始化,引用就不再只是一个引用,而是其所引变量的别名,对已初始化引用的所有操作,都是对其所引变量的操作,包括取地址取的也是所引变量的地址。所以,不能定义指向引用的指针,这句话的前提是引用没有初始化,但引用必须初始化,嗯嗯,这就搞得人很压抑。

	int b = 11, &rb = b;
	int *rpb;
	rpb = &rb;//这是OK的
	cout << *rpb << endl;//11

由于指针是一个对象,所以可以定义指向指针的引用,或者说是引用指针的引用,都一样。

	int i = 12;
	int *p;
	int *&r = p;
	r = &i;
	*r = 121;
	cout << *r << endl;//121
	cout << *p << endl;//121

对于两个类型相同的合法指针,可以使用相等操作符(==)或不相等操作符(!=)来比较他们,比较的结果是布尔类型。若两个指针存放的地址值相同,则他们相等;反之不相等。两个指针存放的地址值相同(两个指针相等)有三种可能:a,他们都为空;b,他们都指向同一个对象;c,他们都指向同一对象的下一个对象。

【注】这里的所有操作都是使用合法指针,如果采用非法指针作为条件判断将会引起不可估计的后果,导致你很难定位到问题。

2.2 指针值

指针的值(即地址)应该属于下列4中状态之一。

  • 1、指向一个对象,即指针值为所指对象的地址值。
  • 2、指向紧邻对象所占空间的下一个位置。
  • 3、空指针,意味着指针没有指向任何对象,也就是没有初始化,如上面的pnull。
  • 4、无效指针,上述情况之外的其他值。一定要注意这种情况,拷贝或以其他方式访问无效指针的值都会引发错误,编译器也发现不了这种错误,所以一定要小心使用指针。

2.3 空指针

空指针,不指向任何对象,在试图使用一个指针之前可以先检查他是否为空。以下是生成空指针的方法:

	int* p1 = nullptr;
	int* p2 = 0;
	int* p3 = NULL;
  • nullptr是C11引入的一种方法,是一种特殊的字面值,它可以被转成任意其他的指针类型。让一个指针的值为空一般都用这种方式。
  • NULL是头文件cstdlib中定义的一个预处理变量,其值为0。现在应该尽量避免使用这种方式来为指针赋空值。
  • 0,这就涉及到一个知识点。现在很多的电脑都是64位的操作系统,所以其内存总线、数据总线的位数是64位,这里单说内存总线。内存总线的每个位,只能表示0或者1,所以每个内存单元的地址值表示是64位的整型数,这也就是为什么输出C++的一个地址值是一个8位16进制的数。当给一个指针赋值为0时,说明该指针存放的地址地址值并不存在,也就没有指向任何一个对象,所以为空。

在使用一个指针时,可以判断该指针是否为空。下面有一个C++中的隐式转换,if条件后面跟着的表达式,如果值为0,会自动换成false;如果值为非零(不管正负),自动转成true

	int* p1 = nullptr;
	if (!p1) {
    
    // 如果是空指针
		cout << p1 << endl;//00000000
	}

2.4 void* 指针

void*是一种特殊的指针类型,可以将指向任意非常量的指针转换成void*,指向任意对象的指针转换成const void*

	int a = 110;
	double b = 19;
	int* p = nullptr;
	double* pb = 0;
	p = &a;
	pb = &b;
	void* v = p;
	v = pb;

一个void*存放一个地址,我们也能知道地址值,但是我们对该地址中到底是什么类型的对象并不了解,也无法确定该如何操作这个对象,所以不能直接操作void*指针所指的对象。

	void* v = p;
	cout << v << endl;//可以输出指针所值内存的地址值
	cout << *v << endl;//报错:不能直接操作void*所指的对象

虽说不能直接操作void*所指的对象,但也不是说操作不了,如果预先知道void*所指对象的类型,就可以使用强转将其转成所指对象的指针类型。如下面操作,也可以使用C中的强转方式int* q = (int*)v; 来转换,但是这样比较危险,对于指针的强转尽量使用下面这种。

	int *q = reinterpret_cast<int*>(v);
	cout << q << endl;//ok
	cout << *q << endl;//这个也ok

void*以及void**真正使用起来不但妖,而且异常的骚,后面函数指针和数组指针会再详细介绍这个玩意。

2.5 指针和const

常量指针

如果在定义指针对象的时候,数据类型前用const修饰,被定义的指针对象就是指向常量的指针变量,指向常量的指针变量称为常量指针。常量指针不能用于改变所指对象的值。

	int a, b;
	const int ca = 0;
	const int* p = &ca; //常量指针,p是一个指针,所指内容是一个常量
	
	// 注意以下几种操作
    p = &b; // 操作成功,常量指针可以指向一个常量,也可以指向一个变量
	*p = 9; // 操作错误,指向常量的指针变量没有改变所指对象内容的权限 
	b = 1; // 操作正确,可以通过其他途径改变所指对象的值,前提是对象是一个非常量

常量指针本身是一个变量,而只是将其所指对象当做一个常量来处理,且常量指针没有规定其所指对象必须是一个常量,所以p既可以指向常量ca,也可以指向变量b。常量指针不能改变所指对象的值,但是如果所指对象是一个变量,是可以通过其他途径改变的,如变量b的值。

指针常量

如果在定义指针对象的时候,指针前面用const修饰,被定义的指针对象就是指针常量。指针常量是一个常量,一旦初始化就不能再改变其值。

	int a, b;
	const int ca = 0;
	int* const p = &b; //指针常量,使用前一定要被初始化
	
    //注意一下几种操作
	*p = 9; //操作成功,指针常量可以通过指针改变所指对象的值
	p = &a; //操作失败,指针常量一旦被初始化,就不能再改变其值
  • 常量指针本身是一个变量,const修饰的是所指对象,将其所指对象当做一个常量来处理。
  • 指针常量本身是一个常量,const修饰的是指针本身,与指针所以对象没关系。

2.6 指针作为函数参数和返回值

C++函数的参数传递和返回值

2.7 指针和数组

C/C++数组归纳整理

三、指针与引用的总结

3.1 引用使用总结

  • 1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
  • 2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
  • 3)引用与指针的区别是,指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
  • 4)使用引用的时机。流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。

3.2 指针使用建议

  • 1)代码编写时注意malloc/free, new/delete成对使用,freedelete之后将相应的指针设置为nullptr。即使这样做了,由于异常可能会导致释放内存的free/delete语句得不到执行,也会发生内存泄露。
  • 2)C++代码代码中多注意使用智能指针
  • 3)避免使用野指针,因为野指针不能通过 if (nullptr == p)的判断语句来预防,所以在创建指针变量时尽量做到要初始化,不知道指向哪就初始化为nullptr
  • 4)c/c++中,局部变量是存放在栈中的,它的特点是随函数调用时创建随函数结束时销毁,因此在程序中将局部变量的地址返回后赋值给一个指针,这个指针指向的是一个已经被回收的内存,这也是一种野指针。所以,不要在函数中返回局部变量的地址,如果代码的逻辑非要是一个局部变量的地址,那么该局部变量一定要申明为static类型。
  • 5)动态内存分配函数分配内存的时,有可能会分配失败,返回nullptr。所以,在使用内存分配函数分配内存的时候,应该用if(p ==nullptr)if(p != nullptr)进行防错处理。
  • 6)在含有指针参数的函数,也是有可能会误用到NULL指针,当调用该函数时传递的指针是个空指针时,如果没有if(p ==nullptr)if(p != nullptr)进行防错处理,也就凉凉了。

3.3 指针与引用比较

  • 引用必须要初始化,初始值必须为一个对象,且初始化完后不能再重新绑定(只能绑定一个)。
  • 引用本身并不是一个对象,它只是其引用对象的另一个名字。
  • 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
  • 指针无须在定义时赋初值(但是最好还是初始化),和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值

猜你喜欢

转载自blog.csdn.net/qq_42570601/article/details/109625602