很久没登陆CSDN,最后一次发帖是换工作的散分贴,之后背井离乡,一去就是八年。八年前的自己对技术充满热情,但是有些井底之蛙,也有些偏激。八年过去,恍然大悟,技术无论大小,总应该有些积累,有些沉淀,有些能让自己和后来之人收益之处,于是乎开始着手写技术文章,文章或许浅显,但或多或少可以与君交流提高。废话那么多,那么第一章就说点简单的吧。
C++函数的数据传递,这里主要指参数传递与返回值。写这篇的目的主要是总结如何高效的使用函数。
先来说说函数参数的传递,一个简单的示例:
class param_obj{
};
class return_obj{
};
return_obj func(param_obj);
这里的参数传递采用的是按值传递
如果读过《effective c++》,或许记得书中第二十条,使用常量引用(pass-by-reference-to-const)来代替按值传递(pass-by-value)。
很明显传递引用省去了拷贝构造函数的执行,对于复杂对象例如其包含指向堆的指针或者是多个基类的子类,那么省下的不止一个函数的执行。关于其他理由,可直接看第二十条描述,这里不做赘述。
说下常量引用,或许很多人包括我自己曾喜欢直接传递非常量引用。这里按值传递,本身就不希望通过该变量带回数据(也不推荐),所以不给函数参数对象的修改权限是合情合理的。那么有一点值得注意的就是,该对象的get方法,甚至那些计算但不修改对象的方法都应加常量限定符const。常量对象只能调用常量方法。
于是示例代码如下:
class param_obj{
public:
int getValue(); const
int valueSquare(); const
private:
int value;
};
class return_obj{
};
return_obj func(const param_obj&);
习惯使用常量限定符来描述那些不改变类的方法是有很多好处的,且也最符合设计逻辑
接下来看函数返回值。
没错,《effective c++》第21条谈到了返回值,主要谈到不要为了效率而返回引用或者指针等,具体请自行查阅,这里不详述。
这里笔者主要想提两种方式
第一是利用RVO(return value optimization),返回值优化。看如下的例子。
#include<iostream>
using namespace std;
class Data{
public:
Data(){};
Data(const Data &data){
cout<<"copy constructor"<<endl;
};
Data &operator=(const Data&){
cout<<"assignment operator"<<endl;
return *this;
};
};
Data func(){
Data f;
cout<<"f address:"<<&f<<endl;
return f;
}
int main(void){
Data a;
a = func();
cout<<"a address:"<<&a<<endl;
Data b = func();
cout<<"b address:"<<&b<<endl;
return 0;
}
g++ 4.8 编译执行结果如下
f address:0x7fff2018d72f
assignment operator
a address:0x7fff2018d72e
f address:0x7fff2018d72d
b address:0x7fff2018d72d
一般函数返回对象时会调用copy构造函数,而这里可以看出两次返回都没有调用copy构造函数,所以这里都进行了RVO。当然此处因为返回的是命名对象,使用的又称为NRVO。
再看对象b的例子,是不是惊奇的发现赋值操作也被优化掉了,对象b的指针和函数内对象的指针一致。这里我们是否可以得出一条结论:
在一个变量得到它所需值的时候定义它
这也正是《effective c++》第26条所强调的,不过那条的描述并不关注这里的优化。
对于RVO,很多编译器是默认打开,特别是gnu的编译器,所以广大码农可能无意识就享受到了RVO。当然我们能做到的更好的一步就是在赋值函数返回对象时去定义那个对象。
是不是发现曾经往往以为的要返回指针或是引用才是高效的在这些编译优化中迎刃而解呢。可以仔细阅读自己项目中,是否有很多为了效率而丢失的便利性的写法,例如输出参数是vector时,在外部定义,再通过引用传递给函数这种
vector<int> things;
getThings(things);
void getThings(vector<int> &things){
...
}
可以大胆的改为
vector<int> things = getThings();
vector<int> getThings(){
vector<int> things;
...
return things;
}
这种写法的好处是很多的,调用处更简洁,返回的vector也可以直接嵌套于for语句或作为其他函数输入。
除了RVO,另一种可以利用的是右值引用。这个是C++11的特性。需打开编译开关。
如下示例
#include<iostream>
using namespace std;
class Data{
public:
Data(){};
Data(const Data &data){
cout<<"copy constructor"<<endl;
};
Data &operator=(const Data&){
cout<<"assignment operator"<<endl;
return *this;
};
Data &operator=(Data &&){
cout<<"rvalue assignment operator"<<endl;
return *this;
};
};
Data func(){
Data f;
return f;
}
int main(void){
Data a;
a = func();
Data b;
b = a;
return 0;
}
输出
rvalue assignment operator
assignment operator
可以看出, 因为赋值符号右边是函数返回的局部对象,调用了传递右值的赋值操作。右值一般后面不会再被引用,所以这里可做的优化便是右值中的变量可以直接转移,包括指针,无需新申请内存。关于更具体的左右值概念,可以查看网上资料。这里不做详述。
了解RVO 与 右值引用在很多情况下可以写出更合理与高效的编码。 当然根据项目实际情况,返回指针或是引用也会出现,当然相信仔细琢磨也能找出更优雅的解决方案。
做个小结:
关于参数传递:
使用常引用代替值传递
学会用const限定方法。
关于返回值:
RVO与NRVO解决了临时对象的copy
学会在函数返回赋值时定义对象
右值引用能有效改善赋值的效率,特别有指向堆的内容时