总结|关于c++中的临时变量

为什么写这样一篇文章?
本人是c++的初学者, 刚接触类这个概念没多久, 但是遇到了许多问题困扰我, 其中有一个问题尤为致命, 我问了许多前辈, 他们许多都没能如愿帮我彻底解决这个问题, 而写这篇文章, 一是为了帮助自己再次梳理一遍近期的困惑, 二也是为了帮助后来者理解这方面的一系列问题.
当然, 由于是初学者, 我并不保证我下面的话不会有很多错误出现, 甚至有些在您看来有些滑稽之谈. 请您谅解
首先: 什么是临时变量?
如果你学过C语言, 那我这里提到的临时变量对你来说应该是一个新的概念, 他不像你记忆中想象的那样: 例如 你要交换a, b两个整数时声明的第三个变量temp就是临时变量, 这个观点是十分错误的! 在C++中, 临时变量的产生是你不能直接看出来的. 至于怎么产生我们下面会聊到.

// 交换例子 int a = 5, b = 10, temp; //temp并不是本文所指的临时变量 temp = a; a = b; b = temp;

例子:
在c++用, 我们有了一个新的概念一一引用. 就像一个不能改变指向对象的指针一样.
那如果我们现在有如下代码:
int a = 5; int &b = a;

这显然是没有问题的, 因为我们有一个int类型的变量, 我们用了int类型的引用去指向它.
可是如果我们玩点花样, 不选择使用一个同类型的引用呢?
比如我们这样:
double a = 10.8; int &b = a;

这个时候编译器会报错, 他告诉我不能用int&去指向一个double. 那如果这时候我把a强制类型转换会如何?
像这样:
double a = 10.8; int &b = (int)a;

很倒霉的是, 编译器又报错了, 他告诉我不是常引用的变量b不能作为左值. 这时候就很神奇, 这跟常引用又有什么关系呢? 不过我们还是遵循编译器所期待的那样,修改为如下的代码:
double a = 10.8; const int &b = (int)a; //也可不写(int), 它会自动转换 const int &b = a;

经历过上述稍微有些繁琐的操作后, 我们终于实现了我们的目的, 与此同时我们也抛出了一个问题, 就是为什么我们要在引用前加const呢?
经过几番折腾, 我了解到, 我们在把一个double类型变量(强制)转换为int类型时, 它会产生临时变量. 我们相当于(int)a后, 得到了一个没有名字的变量, 它的类型为int, 值为10, 我们把这个临时变量(10)作为右值给了b变量. 并在执行完该语句后临时变量会被释放.
而临时变量要赋给一个引用类型时, 我们必须要用const修饰, 原因我会在后文中给出.
我们现在来深入理解一下上述操作: 其实我们是可以接受在写 const int &b = a; (这里省写了类型转换, 方便理解) 时, 我们并不是把a赋值给了b, 因为b得到的是10, 而a在进行类型强制转换时, 是不会影响到a原本的值的, 他该是10.8就还是10.8 并不会变成10. 所以这个10一定不是从a那里得来的, 而是从别的地方一一临时变量.
既然如此,我们可以得出一个结论: b引用的变量并不是a, 而是一个int类型的临时变量.
那么如果我们通过修改a的值, 那b的值理论上就不应该会改变.
double a = 10.8; const int &b = (int)a; double &c = a; //也可以用const修饰c, 不会产生什么影响 cout << "修改前: " << endl; printf("a = %g, b = %d, c = %g\n", a, b, c); //a = 10.8, b = 10, c = 10.8 a = 25.3; cout << "修改后: " << endl; printf("a = %g, b = %d, c = %g\n", a, b, c); //a = 25.3, b = 10, c = 25.3

上述代码就验证了我们的说法, b的值没有改变.
当然我们在平时写代码时虽然也不会照上面代码那样写, 但当你把其中的double或者int换成自定义的用户类型时, 在这一个过程中产生临时变量的事情我们就不能再忽略了, 因为他会体现在调用构造函数上.
我上面的一系列操作也仅仅是为了让你理解你在C语言中没能理解到的临时变量这一新概念, 而发生类型强制转换则是产生临时变量的方法之一.
临时变量:
通过上述实例我引入了一个非常简单的产生临时变量的方法, 那么这里我们就来剖析一下临时变量有什么样的特性. 首先, 我们可以认为临时变量都被const修饰:
但是我也在网上的某篇文上了解到, 有某本书上说过临时变量是可以作为左值的, 但是绝大多数的文章也都告诉我临时变量是一个const类型, 我选择从众, 在后文中提到的临时变量也都认为被const所修饰.
既然他本身是一个const类型, 那么我们就可以理解前文为什么需要一个const int&类型的变量去接收这个临时变量了. 我们可以这样理解, 你在定义某个变量时, 如果你没有说明他是一个const类型, 那么编译器完全有理由去相信你是会修改这个变量的. 你修改一个临时变量, 临时变量本身被const修饰, 无法更改. 况且即使没有const修饰, 这也将会是毫无意义的, 因为临时变量他可能随时就会消散, 而你去修改它, 编译器耗费大量资源后, 又把它释放了, 如果编译器会说话, 你猜他会不会来句: 您逗我玩呢?
但是在这里我要说明一下: 我们不是说这个临时变量是不能传递给一个变量(特指变量被没有const修饰), 如下述代码显然是成立的, 毕竟我们之前也都是这么做的:
double a = 10.8; int b = (int)a; b = 20;

我们是说临时变量不能赋给非!常引用的变量. 但在这里我要特别强调: 我们需要用一个常引用作为左值的主要原因并不是因为临时变量被const修饰过, 而是因为我们如果不用const来限制我们程序员自己, 我们可能会无意中修改了临时变量, 这些操作都是毫无意义的.
据此,c++编译器加入了临时变量不能作为非const引用的这个语义限制,其意在于限制这个非!常规用法的潜在错误
临时变量的产生: 其实看到这里, 你也应该会好奇了, 那么我们除了在类型转换时会产生临时变量, 那么还有吗?
答案是肯定的, 大体上归结为以下3点:
  1. 我们刚才所提到的类型转换
  2. 在调用函数时, 将实参的值传递给形参时
  3. 在函数返回时, 返回值也会以临时变量的形式返回.
由于提到本人是个初学者, 我只遇到过上述三种情况, 并不一定全面, 而后两种的产生途径解释我会在后文中说明.
注: 如果考虑到非编译器自动产生临时变量的方法, 其实还有一种用户生成临时变量的方法, 后文会提到.
正文(一级加粗): 呃, 你没有看错, 这里才算到正文, 前面都是铺垫, 但是这里我们也要经过点铺垫再到核心问题.
常引用与引用: 首先我想特殊说明一下常引用与引用在函数之间的传递, 其实这和常量与非常量在函数间传递是一样的.
1. 当某个函数我们以 普通的引用 作为形参时: 不能接受常量作为实参传递
因为我们以普通引用作为形参, 在函数里编译器有理由认为我们会对这个形参进行修改, 那么如果你传递的是一个常量, 把常量给了这个引用, 说白了不就是告诉编译器您要修改一个常量吗? 这当然是不成立的
2. 当某个函数我们以常引用作为形参时: 可以接受常量或变量作为实参传递
当以常引用作为形参的时候, 我们向编译器说明我们不希望修改形参, 所以这里实参是可以接受变量传递的, 我不修改它而已. 而常量作为实参传递也当然可以.
特殊说明: const所修饰的变量并不一定完全无法修改, 只是为了限制程序员自身. 在此不做过多的赘述. 特别强调: 当以引用作为形参时我们也是传递的地址, 从而不会产生临时变量 临时变量的产生途径2和3: 首先我们引入一段代码, 并且我们来熟悉一下这段代码, 便于我们后续理解.
#include typedef long long ll; using namespace std; class node { //这个类名随便起了, 我习惯了node private: int x, y; public: node() { cout << "默认构造函数" << endl; } //默认构造函数 node(int a, int b) :x(a), y(b) { cout << "构造函数1" << endl; } //构造函数 node(const node& a) { cout << "复制构造函数" << endl; *this = a; } //复制构造函数 ~node() { cout << "析构函数END" << endl; } //析构函数 }; /* 下面的可以先不用看了 */ node fact(node t) { return t; } int main(void) { node a; fact(a); cout << "END" << endl; return 0; }

OK, 上述代码熟悉完类体就可以了, 那么我们接下来可以来解释临时变量的产生途径2和3:
首先, 我们在main函数里声明了一个对象node a; , 然后我们把a这个对象作为参数传递给了fact()函数, 注意: 此处我们是传递的值.
随后我们在fact函数中return了这个形参. (虽然在main函数中忽略了fact的返回值, 但是不影响代码的正确性)
执行结果如下:
默认构造函数 //构造node a 复制构造函数 //由于把值传递给形参, 所以此时会产生临时变量, 把临时变量给形参(二者会共用同一地址) 复制构造函数 //返回值时产生了临时变量 析构函数END //在执行完fact()函数时释放掉的临时变量(也可以理解为形参t) 析构函数END //在执行完fact(a); 语句后所释放掉的返回值临时变量 END 析构函数END //释放a对象

我们为了能更好的说明某些问题, 我们可以添加一些输出地址的操作, 例如:
#include typedef long long ll; using namespace std; class node { private: int x, y; public: node() { cout << "默认构造函数" << endl; } node(int a, int b) :x(a), y(b) { cout << "构造函数1" << endl; } node(const node& a) { cout << "复制构造函数" << endl; *this = a; cout << this << endl; } //增加了输出当前被生成对象的地址 ~node() { cout << "析构函数END" << endl; } }; node fact(node t) { cout << &t << endl; return t; } //增加了输出形参的地址 int main(void) { node a; cout << &a << endl; //增加了输出对象a的地址 fact(a); cout << "END" << endl; return 0; }

经过操作后, 我们再次运行得到以下结果:
默认构造函数 003AF7A4 //a的地址(我们的值不相同很正常) 只是以这个为例 复制构造函数 003AF69C //这是复制实参a后产生的临时变量的地址 003AF69C //这是形参t的地址 复制构造函数 003AF6C8 //这是返回值临时变量的地址 析构函数END 析构函数END END 析构函数END

OK, 到此我们就能理解后两种说明产生临时变量的方法了.
问题: 现在我们开始说一个很重要的问题, 其实除了上述三种途径, 我们还可以自己通过构造函数来构造一个临时变量, 对于我这个题而言, 我可以用 node(1, 2); 这条语句通过构造函数1来创造一个临时变量.(这样写是成立的)
那么如果我这时想用这个临时变量来初始化一个新的对象, 那我可以通过 node a = node(1, 2);
代码如下:
class node { private: int x, y; public: node() { cout << "默认构造函数" << endl; } node(int a, int b) :x(a), y(b) { cout << "构造函数1" << endl; } node(const node& a) { cout << "复制构造函数" << endl; *this = a; } ~node() { cout << "析构函数END" << endl; } }; //类体结束 int main(void) { node a = node(1, 2); return 0; }

注: 之后的类体内容也都以上述代码为标准, 不再显示在后文中.
运行结果如下:
构造函数1 析构函数END

这时候, 如果我把复制构造函数的参数中的const给删除, 变为
node(const node& a) { cout << "复制构造函数" << endl; *this = a; }

此时main函数中的语句就会报错, 由于编译器不同, 大体有两种错误提示:
  1. “初始化”: 无法从"node"转换为"node" (VS报错)
  2. 没有找到 node(node)类型的构造函数 (CB DevC++)
这里特殊说明: 如果你改写了我的代码, 如下:
node a; a = node(1, 2);

这样是对的, 但是更改了问题的本质, 语句 node a = node(1, 2); 中表示声明a的同时在给a初始化, 而 node a; a = node(1, 2); 则表示在给a赋值, 等号表示赋值符号了, 是有本质区别的.
或者我这样给你说明, 构造函数, 他只能在初始化对象时调用, 而初始化, 指的是在定义某个变量时对他赋初值的操作. 第二种写法先是声明了a对象, 然后没有给他进行赋值, 这时候里面的值会是随机的. 而在==a = node(1, 2); ==操作时, 此时便是赋值操作了, 因此他也不会去调用构造函数(对于a而言, 并不是指 node(1, 2) 这个临时变量产生不会调用构造函数). 要实在还不明白可以直接将赋值号重载来理解.
总结下来就是 : 第一种相当于初始化, 而第二种相当于赋值.
OK, 那么到此我们就抛出一个大问号了, 为什么会有这种东西出现呢? 我又没调用这个复制构造函数, 为啥我改了他我的代码会错?
这里我不多卖关子了, 根据刚才形参与临时变量地址的问题, 我们同样可以输出这个临时变量与我创建的这个a的地址, 在复制构造函数中加回const后, 在构造函数1里输出临时变量的地址, 在主函数里输出a的地址, 我们会发现这两个地址是相同的.
这时你是不是会稍微有点头绪, 我们可以大胆猜测, 编译器这时候偷了个懒, 他直接把临时变量的地址给了a, 完成了初始化, 省略了再把临时变量复制给a对象这步操作, 所以这也能解释为什么我们运行时明明没有调用复制构造函数, 但是修改它却报了错的原因一一我们原本就是需要用复制构造函数把临时变量赋值给a的, 只是编译器在执行的时候略去了这一步, 而在检查时没有略去.
所以在我们写复制构造函数的时候, 请务必遵循规范, 加上const..
下面我们引入一段代码, 来稍加进行分析:
node fact() { return node(1, 2); } int main(void) { node a = fact(); return 0; }

这段代码我们让fact()函数返回了一个临时变量, 用他来给a进行初始化, 这时候由于本身我们返回值就是一个临时变量 所以会省去一次复制构造函数的调用. 又根据我们刚才的猜想, 此时他会用临时变量直接初始化a, 而不再进行复制构造函数的调用.
输出结果如下:
构造函数1 析构函数END

【总结|关于c++中的临时变量】同样我们可以通过输出临时变量与a的地址确定我的上述说法.
END 本文到此结束, 希望对您有所帮助

    推荐阅读