C++中有两个地方一直感觉很隐晦。第一个是引用;其次就是临时对象了。这个可能和我没有系统的知识学习有关,到现在都没把C++Primer看一遍。寒假决定就干这一件事,把C++Primer系统的梳理一遍,并且做详细总结。
若有错误 请指正 万分感想
开篇:
一个经典的示例:
#include<iostream>
using namespace std;
void Swap(int a, int b){
int temp; //这个地方的temp算是临时对象嘛?
temp = a;
a = b;
b = temp;
}
int main(){
//...
}
首先解决下第一个坑:临时对象和局部变量。
千万不要把局部变量和临时对象混淆了。只要你能在程序中看见的变量,都不会是临时对象。
临时对象都是const的,并且由编译器感知的。
为什么要总结临时变量?
1.说明:
对我而言,主要是它让我不舒服了,引用,指针,临时变量这几个方面着实让我难受,一方面可能是自己水平不够,看的一些书中着实没让我看明白。
对程序而言,研究它却能提升效率。顾名思义,临时。最常见的例子就是在类里面了,因为临时对象的产生会调用析构和构造函数,产生一定的开销。
2.临时对象的说明:
临时对象是一个没有命名的非堆对象。
关于堆的一些概念,大家可以翻看我转载的一篇博文,那里面粗浅的介绍了,更细致的了解的话,可以看看汇编,计算机组成原理等知识,因为我目前没有接触,所以我无法卖弄。只能简单讲一下自己已经的知道的东西。
关于临时对象的产生情景,我在文中介绍三种情况,如果你感觉有别的,那么可能是被我合并到某一种中。
三种情形:
1)类型转换时产生:
示例:
//简单而能说明问题的例子。字符统计函数。
#include <iostream>
using namespace std;
int CountChar(const string& p, char c){
int count = 0;
for (int i = 0; i <p.size(); i++){
if (p[i] == c)
count++;
else
continue;
}
return count;
}
int main(){
char str[200];
cout << "Enter the array of str :";
cin.getline(str,200);
cout << "Enter the char that you want to count :";
char c;
cin >> c;
cout<<"The number of char c is "<<CountChar(str, c);
system("pause");
return 0;
}
看一下上面程序中函数的调用过程:
int CountChar(const string& p, char c);//这个是我定义的函数原型。
<pre name="code" class="cpp">CountChar(str, c);//这个是函数的调用语句。我这个地方的str明明是一个数组名称,明明和我的函数原型不一致。
那为什么可以调用成功,运行成功?这个地方就要涉及到今天要谈及的话题了,类型转换。
首先明确一下,不是啥都能进行转换的,我们之所以可以进行转换,是因为string内部存在相应的构造函数。
下面继续举个例子看一下是怎么进行转换的。
#include<iostream>
using namespace std;
class Base{
private:
int x;
public:
Base(){
cout << "Using the default Cstor" << endl;
}
Base(int x_x) :x(x_x){
cout << "Using the defined Cstor" << endl;
}
~Base(){
cout << "Using the Destructor" << endl;
}
Base(const Base& temp){
x = temp.x;
cout << "Using the Copy Cstor" << endl;
}
Base operator+(const Base& temp){
return Base(x + temp.x);
}
};
int main(){
//Base b=100;
b = 100;
system("pause");
return 0;
}
在上面的代码中,Base b=100; 为何会通过编译?原因就是调用上面我定义的构造函数。如果你注释了那几行那么这种行为就是非法的。编译器会调用构造函数,并且以100为参数,产生了一个临时对象。然后调用拷贝构造函数进行初始化,注意这个地方的用词是初始化,而不是赋值。
其实像这样的语句: int var1=10;double var2=12.1;
那么这样的语句:double var3=var1+var2 也是要产生临时对象的。这个地方需要进行类型转换。
2)按值传递:
示例:
#include <iostream>
using namespace std;
class Base{
private:
int x;
public:
Base(int x_x = 0) :x(x_x){
cout << "Using the Cstor" << endl;
}
~Base() { cout << "Using the Destructor" << endl; }
Base(const Base& temp){
cout << "Using the Copy Cstor" << endl;
x = temp.x;
}
};
/*Base Test(Base temp){
cout << "Do nothing " << endl;
return temp;
}*/
//void Test(Base temp){ ... }
int main(){
Base base_one(1); //调用构造函数。
// Base base_two=Test(base_one);//这个地方会调用拷贝构造函数
Test(base_one);
system("pause");
return 0;
这个地方调用了Test函数,传递进去的是什么?是base_one 自身嘛?
在进行测试的时候我们发现竟然会调用拷贝构造函数!为什么呢?
这个地方就涉及到今天的第二个话题了。
按值传递,传递进去根本不是自身,而是调用副本构造函数产生的临时对象。
如果你不想进行拷贝,那么我推荐使用引用进行处理,关于引用可以参照前面的博文,引用笔记。
3) 按值返回:
示例:
//测试下返回值时的临时对象问题。
#include <iostream>
using namespace std;
class Base{
private:
int var;
public:
Base(int v_var = 0) :var(v_var){
cout << "Using the Constructor" << endl;
}
~Base(){ cout << "Using the Destrucotr" << endl; }
Base(const Base& other){
this->var = other.var;
cout << "Using the Copy Constructor" << endl;
}
friend Base operator+(const Base& p1, const Base& p2);
Base operator=(const Base& p){
this->var = p.var;
cout << "Using the assignment operator" << endl;
return (*this);
}
};
Base operator+(const Base& p1, const Base& p2){
/*Base tmp;
tmp.var=p1.var + p2.var;
return tmp;*/
return Base(p1.var + p2.var);
}
int main(){
/*Base base_one(3);//一次构造函数,一次析构
Base base_two(4);//同上
Base base_three = base_two + base_one;//这个地方并没有调用Copy Cstor。
Base base_four;*/
//Base b1=100;
Base b1;
b1 = 100;
//base_four = base_one + base_two;//这个地方可以对比下赋值和初始化的.
system("pause");
return 0;
}
来看一下这个函数:
Base operator+(const Base& p1, const Base& p2){
/*Base tmp;
tmp.var=p1.var + p2.var;
return tmp;*/
return Base(p1.var + p2.var);
}
这个地方可以看见我注释了一个返回值。感兴趣可以测试下两种情况。
测试过后我们可以发现,当函数返回的时候,竟然又调用了拷贝构造函数,为什么呢?
这个地方就涉及到了返回值的问题。返回的不是自身,而是拷贝。
优化也就是用引用或者指针了。
关于具体的优化方案就不总结了,因为那个一般是编译器做的,平台不同不好测试。但是良好的代码书写习惯却是能避免许多不必要的浪费。
示例:
Base operator+(const Base& p1, const Base& p2){
/*Base tmp;
tmp.var=p1.var + p2.var;
return tmp;*/
return Base(p1.var + p2.var);
}
这个地方我的返回语句是:
return Base(p1.var + p2.var);
不要小看这个写法,可以省去一笔不小的空间花销。
其次常见的优化措施就是用好引用和指针。
最后一种就是初始化和赋值,推荐初始化不推荐赋值。
关于初始化和赋值的问题我举个例子:
#include <iostream>
using namespace std;
class Base{
private:
int x;
public:
Base(int x_x = 0):x(x_x){
cout << "Using the Cstor" << endl;
}
};
int main(){
Base b1(10);//这个叫初始化。
Base b2;//这个叫声明
b2 = b1; //这个叫赋值。
//可以对比下两者,声明的时候直接初始化同先声明后初始化的差异,用类的构造函数就可以看见了。
/*int x = 1;//这个是初始化操作。
int y;
y = 1; //这个就是赋值。
//这个地方可能看不出明显的效果,复杂点的例子可以看下上面类的例子*/
}
临时对象生命周期相关:
一般情况中:
C++中临时对象的摧毁是在完整表达式求值过程中的最后一个步骤。
完整表达式是指包含临时表达式最外围的那个。任何一个由子表达式生成的临时对象都有等到完整表达式被求解完毕后才能被销毁。这个还是很符合常理的。如果你在一个嵌套表达式中,求解的过程产生的临时对象还未被使用就已经被销毁,那么运算估计是很难进行下去的,毕竟计算机本质上就是在处理数字。
两个特殊情况:
1)当一个对象被用来初始化一个对象:
示例:
#include <iostream>
using namespace std;
class Base{
int x;
public:
Base(int x_x = 0) :x(x_x){}
Base(const Base& temp){
x = temp.x;
cout << "Using the Copy Cstor" << endl;
}
Base operator+(const Base& temp){
return Base(x + temp.x);
}
};
int main(){
Base b1(1);
Base b2(3);
Base b3 = b1 + b2;//这个地方b1+b2会调用+函数,那么函数是要产生一个临时对象的,然后调用拷贝构造函数进行初始化。
//这个地方还是强调一下赋值和初始化.如果你写成这个样子,那么
Base b4;
b4 = b1 + b2; //效果绝对和上面的初始化是不一样的。自行测试。
system("pause");
return 0;
}
上面的用b1+b2来初始化b3,这个过程中b1+b2表达式在求解过程中产生的临时对象并没有马上被销毁,而是一直等到拷贝给b3后才销毁。如果销毁了,那么b3怎么办?所以不管语言层面还是编译器角度都是要考虑基本的规则。
2)当一个临时对象同引用绑到一起:
临时对象将保留知道初始化的引用生命结束或者是临时对象的生命周期结束。
#include <iostream>
using namespace std;
class Base{
int x;
public:
Base(int x_x = 0) :x(x_x){
cout << "Using the Cstor"<<endl;
}
Base(const Base& temp){
x = temp.x;
cout << "Using the Copy Cstor" << endl;
}
int Getx(){
return x;
}
Base operator+(const Base& temp){
return Base(x + temp.x);
}
};
int main(){
Base b1(1);
Base b2(3);
const Base& b3 = b1 + b2;//这个地方b1+b2会调用+函数,那么函数是要产生一个临时对象的,然后调用拷贝构造函数进行初始化。
cout << b3.Getx() << endl;
system("pause");
return 0;
}
上面我们为b1+b2的临时对象建立了引用。这个时候的临时对象的生命周期是符合上面所说的。
如果你发现你进行调试的结果和我的不一样,那么可能是编译器有优化,不代表你错或者我错。当然很可能犯错,有错误清指出。
参考文献:
优快云 博文
博客园博文
<<More Effective C++>>