2022-chumingqian-C++ 中的指针与引用

C++ 中的指针与引用

引言

对象

C++ Primer 中, 对象的定义:

对象是指一块能存储数据并具有某种类型的内存空间;

一个对象 a,它有值和地址 &a,运行程序时,计算机会为该对象分配存储空间,来存储该对象的值,我们通过该对象的地址,来访问存储空间中的值;

指针 p 也是对象,它同样有地址&p 和存储的值 p,只不过,p 存储的数据类型是数据的地址。如果我们要以 p 中存储的数据为地址,来访问对象的值,则要在 p 前加解引用操作符 *,即 *p

常量和变量之分

对象有常量(const)和变量之分,既然指针本身是对象,那么指针所存储的地址也有常量和变量之分;指针常量是指,指针这个对象所存储的地址是不可以改变的,而指向常量的指针的意思是,不能通过该指针来改变这个指针所指向的对象。

我们可以把引用理解成变量的别名。定义一个引用的时候,程序把该引用和它的初始值绑定在一起,而不是拷贝它。计算机必须在声明 引用 r 的同时就要对它初始化,并且,r 一经声明,就不可以再和其它对象绑定在一起了。

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)的地址,可以检索指向该地址处的值 。

  1. 指针定义时,可以不用初始化;
  2. 指针可以初始化为 NULL,
  3. 指针的在初始化后, 其地址值仍可以改变, 用于存储另外一个地址;

引用

引用是 C++语言的基础数据类型,用于在程序的不同的部分使用两个以上的变量名指向同一块地址,使得对其中任何一个变量的操作实际上都是对同一地址单元进行的。

引用的特点:

  1. 引用仅是变量的别名,而不是实实在在地定义了一个变量,因此引用本身并不占用内存,引用,表明上是为变量创建了第二个名字, 它的本质是一个指针,和目标变量的是同一个内存地址。声明引用时,目标的存储状态不会改变;
  2. 引用在定义时,必须初始化, 且不能初始化为空;只有在引用申明时, “&” 才称作引用符,且放在类型名后面;其他时候出现的 “&”, 都是指 取地址操作符;。
  3. 引用只能在初始化的时候引用一次 ,不能更改为转而引用其他变量。
  4. 对引用进行操作,实际上就是对被引用的变量进行操作;

C++ 中为什么要有 “指针”

精准控制了内存

指针可以精准控制了内存中的地址,从而高效率的传递和更改数据;因为指令集里面有一种间接寻址的方式,抽象出来就是指针了;指针有很强的灵活性,可以根据地址进行访问, 从而达到对内存的精准控制;先说说一般考虑使用指针的情况:需要对同一份数据进行操作。而操作又分为读和写两种。

如果对数据只是读操作的话,其实将数据复制一份去读和直接读,结果是没什么区别的,但是过程就不一样了,很明显,复制会浪费一些时间,直接读肯定会更快一些,所以,对于只读的情况来说,使用指针是为了更高效的进行数据的传递。

而如果涉及到对于数据的写操作(包括只写和既写又读),将数据复制一份和直接操作,肯定就有区别了,区别就在于,复制一份的话,副本更改了,原本是不会更改的,所以,对于需要写数据的情况来说,使用指针是为了更改同一份数据。

函数的参数中传递指针 (引用也是一种非空指针)

变量为了表示数据而生, 指针为了传递数据为生。

**变量:**一个东西在内存里面,而你想用语言去表示那个东西,就必须找到一个表示它。于是我们用常量或变量去表示内存里的值。比如,

int a = 2;

就是把 2 这个值,放在了内存中。**(但是你不知道它的位置,如果你有看到整个内存的能力,你有可能发现有一个 2 在 No.300 处)**并且你想要在程序中调用它,就必须有一个东西代表它,于是用变量 a 代表了这块内存中的内容.有了变量,你就可以用他表示一个值。从而你可以使用这个变量。

指针:,如果你只有这一行程序的话,那指针就没有太大的存在必要了。但是如果你有好几个函数需要读写这个值,那么这时候问题就来了。

int myMoney = 1000;

如果你的账上有 1000 元,有好几个函数要操作这个值,这时候就会产生两种需求, 读和写。在函数里修改这个值的时候是应该修改原值呢? 还是不修改原值?

如果使用 foo(myMoney)这种形式的话,就会把 myMoney 代表的内存中的内容“复制”一份到函数栈里,这样你在函数里修改这个值不会对外界有任何影响。但是,如果你想在函数中对原值进行操作,这时候就不能只传进来内容,而需要传进来一个 myMoney 的地址,这样,在函数里面就能再程序找到那块地址,把内容修改掉。

所以有了传递地址的需求。为了方便传递地址,所以有了指针,指针也是一个变量,只不过里面存的内容是一个地址。

指针的使用场景

当然,不使用动态分配而采取原始指针(raw pointer)的用法也很常见,但是大多数情况下动态分配可以取代指针,因此一般情况应该首选动态分配的方法,除非你遇到不得不用指针的情况。

  • 使用引用语义(reference semantics)的情况。有时你可能需要通过传递对象的指针(不管对象是如何分配的)以便你可以在函数中去访问/修改这个对象的数据(而不是它的一份拷贝),但是在大多数情况下,你应该优先考虑使用引用方式,而不是指针,因为引用就是被设计出来实现这个需求的。注意,采用这种方式,对象生存期依旧在其作用域内自维护。当然,如果通过传递对象拷贝可以满足要求的情况下是不需要使用引用语义。
  • 使用多态的情况。通过传递对象的指针或引用调用多态函数(根据入参类型不同,会调用不同处理函数)。如果你的设计就是可以传递指针或传递引用,显然,应该优先考虑使用传递引用的方式。
  • 对于入参对象可选的情况,常见的通过传递空指针表示忽略入参。如果只有一个参数的情况,应该优先考虑使用缺省参数或是对函数进行重载。要不然,你应该优先考虑使用一种可封装此行为的类型,比如 boost::optional 或者 std::optional
  • 通过解耦编译类型依赖减少编译时间的情况。使用指针的一个好处在于可以用于前向声名(forward declaration)指向特定类型(如果使用对象类型,则需要定义对象),这种方式可以减少参与编译的文件,从而显著地提高编译效率,具体可以看 Pimpl idiom 用法。
  • 与 C 库或 C 风格的库交互的情况。此时只能够使用指针,这种情况下,你要确保的是指针使用只限定在必要的代码段中。指针可以通过智能指针的转换得到,比如使用智能指针的 get 成员函数。如果 C 库操作分配的内存需要你在代码中维护并显式地释放时,可以将指针封装在智能指针中,通过实现 deleter 从而可以有效的地释放对象。

何时使用动态分配与指针

我在使用 C++ 进行面向对象开发时,我发现一个很让我非常困惑的问题:C++ 中经常出现使用对象指针,而不是直接使用对象本身的代码,比如下面这个例子:

Object *myObject = new Object;

// 而不是使用:
Object myObject;

// 要不就是调用对象的方法(比如 testFunc())时不使用这种方式:
myObject.testFunc();

// 而是得写成这样:
myObject->testFunc();

你的两个问题本质上是同个问题。

第一个问题是,应该何时使用动态分配(使用 new 方法)? 第二问题是,什么时候该使用指针?

最先要牢记的重点是,你应该根据实际需求选择合适的方法。一般来说,使用定义对象的方式比起使用手工动态分配(或 new 指针)的方式会更加合理以及安全。

**动态分配:**你的提问中,所列出的两种分配对象方式的主要区别在于对象的生存期。通过 Object myObject 方式定义对象,对象的生存期是在其作用域内自维护(automatic storage),这个意味着程序离开对象的作用域之后,对象将被自动销毁。当通过 new Object() 方式分配对象时,对象的生存期是动态的,这个意味着若不显式地 detete 对象,对象将一直存在。你应该只在必要的时候使用动态分配对象。换句话说,只要有可能,你应该首选定义可自维护的对象。

这里是两个常见需要动态分配对象的情况:

  1. 分配不限制作用域的对象,对象存储在其特定的内存中,而不是在内存中存储对象的拷贝。如果对象是可以拷贝/移动的,一般情况下你应该选择使用定义对象的方式。
  2. 定义的对象会消耗大量内存, 这时可能会耗尽栈空间。如果我们永远不需要考虑这个问题那该多好(实际大部分情况下,我们真不需要考虑),因为这个本身已经超出 C++ 语言的范畴,但不幸的是,在我们实际的开发过程中却不得不去处理这个问题。

当你确实需要动态分配对象时,应该将对象封装在一个智能指针(smart pointer)或其他提供 RAII 机制的类型中(类似标准的 container)。智能指针提供动态对象的所有权语义(ownership),具体可以看一下 std::unique_ptr 和 std::shared_ptr 这两个例子。如果你使用得当,基本上可以避免自己管理内存(具参见 Rule of Zero)。

C++ 中为什么要有引用

C++ 中引用的出现,最初是为了解决,运算符重载的问题;

假设没有引用,那么,用指针来 operator overloading 操作。 A operator +(const A *a, const A *_a); 那么使用的时候,&a + &b, 这样看起来不是那样简单明了。引用解决 C++ 运算符重载后代码美观的问题。

并且引用的 出现带来了一个指针无法替代的特性:引用临时对象;引用在定义的时候必须就 赋值,并且此后再也无法更改;

引用的特性

并且在实践中:使用指针,经常出现一下情况:

  1. 操作空指针
  2. 操作野指针
  3. 不知不觉中, 手动改变了指针的值,随后以为该指针正常,关键本人不知道;

为了设立一种机制, 保证上述三种情况的不发生,引用这个机制保证避免发生上述三种情况;引用:

  1. 引用不允许为空 (从而避免了操作空指针)
  2. 引用在定义的时候,就必须初始化(避免了操作 野指针)
  3. 一个引用 ,始终从一而终的指向他 初始化的那个对象 ( 避免了指针的值不会被修改)

在函数参数中的引用传递

C++ 支持把引用作为参数传给函数, 这比传一般的参数更安全;传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。

使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。

使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用 *指针变量名 的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。

引用作为函数的返回值

(1)以引用返回函数值,定义函数时需要在函数名前加& (2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。

引用作为返回值,必须遵守以下规则:

  • 1**.不能返回局部变量的引用**:主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。

  • 2.不能返回函数内部 new 分配的内存的引用:例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由 new 分配)就无法释放,造成 memory leak。

  • 3.可以返回类成员的引用,但最好是 const:这条原则可以参照《Effective C++》的 Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。

  • 4.引用与一些操作符的重载:流操作符«和»,这两个操作符常常希望被连续使用,例如:cout « “hello” « endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。