2022-chumingqian-C++ 中的指针与引用
C++ 中的指针与引用
引言
对象
对象是指一块能存储数据并具有某种类型的内存空间;
一个对象
指针*
*p
。
常量和变量之分
对象有常量(const)和变量之分,既然指针本身是对象,那么指针所存储的地址也有常量和变量之分;指针常量是指,指针这个对象所存储的地址是不可以改变的,而指向常量的指针的意思是,不能通过该指针来改变这个指针所指向的对象。
我们可以把引用理解成变量的别名。定义一个引用的时候,程序把该引用和它的初始值绑定在一起,而不是拷贝它。计算机必须在声明 引用
int a,b,*p,&r=a;//正确
r=3;//正确:等价于a=3
int &rr;//出错:引用必须初始化
p=&a;//正确:p中存储a的地址,即p指向a
*p=4;//正确:p中存的是a的地址,对a所对应的存储空间存入值4
p=&b//正确:p可以多次赋值,p存储b的地址
实际上,引用是一个指针常量:指向的地址不变,地址指向的内容可以改变;引用的一个优点是它一定不为空,因此相对于指针,它不用检查它所指对象是否为空,这增加了效率。
指针
指针:它保存一个值(或
- 指针定义时,可以不用初始化;
- 指针可以初始化为
NULL , - 指针的在初始化后, 其地址值仍可以改变, 用于存储另外一个地址;
引用
引用是
引用的特点:
- 引用仅是变量的别名,而不是实实在在地定义了一个变量,因此引用本身并不占用内存,引用,表明上是为变量创建了第二个名字, 它的本质是一个指针,和目标变量的是同一个内存地址。声明引用时,目标的存储状态不会改变;
- 引用在定义时,必须初始化, 且不能初始化为空;只有在引用申明时, “&” 才称作引用符,且放在类型名后面;其他时候出现的 “&”
, 都是指 取地址操作符;。 - 引用只能在初始化的时候引用一次 ,不能更改为转而引用其他变量。
- 对引用进行操作,实际上就是对被引用的变量进行操作;
C++ 中为什么要有 “指针”
精准控制了内存
指针可以精准控制了内存中的地址,从而高效率的传递和更改数据;因为指令集里面有一种间接寻址的方式,抽象出来就是指针了;指针有很强的灵活性,可以根据地址进行访问, 从而达到对内存的精准控制;先说说一般考虑使用指针的情况:需要对同一份数据进行操作。而操作又分为读和写两种。
如果对数据只是读操作的话,其实将数据复制一份去读和直接读,结果是没什么区别的,但是过程就不一样了,很明显,复制会浪费一些时间,直接读肯定会更快一些,所以,对于只读的情况来说,使用指针是为了更高效的进行数据的传递。
而如果涉及到对于数据的写操作(包括只写和既写又读
函数的参数中传递指针 (引用也是一种非空指针)
变量为了表示数据而生, 指针为了传递数据为生。
int a = 2;
就是把
指针:,如果你只有这一行程序的话,那指针就没有太大的存在必要了。但是如果你有好几个函数需要读写这个值,那么这时候问题就来了。
int myMoney = 1000;
如果你的账上有
如果使用
所以有了传递地址的需求。为了方便传递地址,所以有了指针,指针也是一个变量,只不过里面存的内容是一个地址。
指针的使用场景
当然,不使用动态分配而采取原始指针(raw pointer)的用法也很常见,但是大多数情况下动态分配可以取代指针,因此一般情况应该首选动态分配的方法,除非你遇到不得不用指针的情况。
- 使用引用语义(reference semantics)的情况。有时你可能需要通过传递对象的指针(不管对象是如何分配的)以便你可以在函数中去访问
/ 修改这个对象的数据(而不是它的一份拷贝) ,但是在大多数情况下,你应该优先考虑使用引用方式,而不是指针,因为引用就是被设计出来实现这个需求的。注意,采用这种方式,对象生存期依旧在其作用域内自维护。当然,如果通过传递对象拷贝可以满足要求的情况下是不需要使用引用语义。 - 使用多态的情况。通过传递对象的指针或引用调用多态函数(根据入参类型不同,会调用不同处理函数
) 。如果你的设计就是可以传递指针或传递引用,显然,应该优先考虑使用传递引用的方式。 - 对于入参对象可选的情况,常见的通过传递空指针表示忽略入参。如果只有一个参数的情况,应该优先考虑使用缺省参数或是对函数进行重载。要不然,你应该优先考虑使用一种可封装此行为的类型,比如
boost::optional 或者std::optional - 通过解耦编译类型依赖减少编译时间的情况。使用指针的一个好处在于可以用于前向声名(forward declaration)指向特定类型(如果使用对象类型,则需要定义对象
) ,这种方式可以减少参与编译的文件,从而显著地提高编译效率,具体可以看Pimpl idiom 用法。 - 与
C 库或C 风格的库交互的情况。此时只能够使用指针,这种情况下,你要确保的是指针使用只限定在必要的代码段中。指针可以通过智能指针的转换得到,比如使用智能指针的get 成员函数。如果C 库操作分配的内存需要你在代码中维护并显式地释放时,可以将指针封装在智能指针中,通过实现deleter 从而可以有效的地释放对象。
何时使用动态分配与指针
我在使用
Object *myObject = new Object;
// 而不是使用:
Object myObject;
// 要不就是调用对象的方法(比如 testFunc())时不使用这种方式:
myObject.testFunc();
// 而是得写成这样:
myObject->testFunc();
你的两个问题本质上是同个问题。
第一个问题是,应该何时使用动态分配(使用
new 方法) ? 第二问题是,什么时候该使用指针?
最先要牢记的重点是,你应该根据实际需求选择合适的方法。一般来说,使用定义对象的方式比起使用手工动态分配(或
这里是两个常见需要动态分配对象的情况:
- 分配不限制作用域的对象,对象存储在其特定的内存中,而不是在内存中存储对象的拷贝。如果对象是可以拷贝
/ 移动的,一般情况下你应该选择使用定义对象的方式。 - 定义的对象会消耗大量内存, 这时可能会耗尽栈空间。如果我们永远不需要考虑这个问题那该多好(实际大部分情况下,我们真不需要考虑
) ,因为这个本身已经超出C++ 语言的范畴,但不幸的是,在我们实际的开发过程中却不得不去处理这个问题。
当你确实需要动态分配对象时,应该将对象封装在一个智能指针(smart pointer)或其他提供
C++ 中为什么要有引用
假设没有引用,那么,用指针来
operator overloading 操作。A operator +(const A *a, const A *_a);
那么使用的时候,&a + &b, 这样看起来不是那样简单明了。引用解决C++ 运算符重载后代码美观的问题。
并且引用的 出现带来了一个指针无法替代的特性:引用临时对象;引用在定义的时候必须就 赋值,并且此后再也无法更改;
引用的特性
并且在实践中:使用指针,经常出现一下情况:
- 操作空指针
- 操作野指针
- 不知不觉中, 手动改变了指针的值,随后以为该指针正常,关键本人不知道;
为了设立一种机制, 保证上述三种情况的不发生,引用这个机制保证避免发生上述三种情况;引用:
- 引用不允许为空 (从而避免了操作空指针)
- 引用在定义的时候,就必须初始化(避免了操作 野指针)
- 一个引用 ,始终从一而终的指向他 初始化的那个对象
( 避免了指针的值不会被修改)
在函数参数中的引用传递
使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用 *指针变量名
的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。
引用作为函数的返回值
(1)以引用返回函数值,定义函数时需要在函数名前加
引用作为返回值,必须遵守以下规则:
-
1**. 不能返回局部变量的引用** :主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了" 无所指" 的引用,程序会进入未知状态。 -
2. 不能返回函数内部new 分配的内存的引用:例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new 分配)就无法释放,造成memory leak 。 -
3. 可以返回类成员的引用,但最好是const :这条原则可以参照《Effective C++》的Item 30 。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针) ,那么对该属性的单纯赋值就会破坏业务规则的完整性。 -
4. 引用与一些操作符的重载:流操作符« 和» ,这两个操作符常常希望被连续使用,例如:cout « “hello” « endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。