这是我第一次真正意义上发技术博客,对C++很多特性我了解的并不深,直到今天我才敢评论总结出来。
上面四个名词,相信很多初学者会疑惑(我也不例外),什么时候做什么事?最开始,我的代码这么写:
int max(int a, int b){ return a>b?a:b; }
int a=1, b=2;
int c = max(a,b);
以上的int是值类型,只占4个字节,万一要是对象类型,也许占100MB空间呢。毫无疑问,我们应该用引用优化。
int const& max(int const& a, int const& b) { return a>b?a:b; }
//use int c = max(a,b);
似乎代码没有什么问题,但隐藏的技巧很深
什么时候用const&, 什么时候不用const&
为什么用引用,不用指针
形参表传进来,传的是值的副本,还是指向值的地址
返回值返回的是值,还是值的副本。
也许有人和我一样,认为问题的答案如下:
const 修改值的时候加, 不修改时不加
引用是别名,指向变量的地址,但他不能改变指向的对象类型,所以很安全
形参传进的是指针是地址,类型则是副本
返回值返回的当然是值的副本
上面的答案没错,问题是怎样优化呢?因此有了上面版本的max,可问题远远不是这样的简单
为什么char *strcpy(char* dest, const char *src); 定义成这样,而不是下面这样:
char * strcpy(const char* src);
这个例子不够恰当,如果是值类型呢
struct cstr{ char* str; size_t len; };
cstr strcpy(const cstr& src);
如果有人和我一样,尝试优化形参、返回值的话,就会发现返回值得到的是副本,而真正的值因为是局部变量在子函数结束也随之销毁,如是指针不delete的话,则永远占用内存了。自然而然,我们想去优化,最好的办法便是形参中使用不加const的指针,或不加const的引用。这也是为什么标准函数strcpy定义成那样的原因。
当然,如果因为害怕而写成那样的话,也不必要,编译器自动进行RVO优化,可以直接返回值。不过为了显式RVO,我一般在最后返回值的时候构造值返回。
似乎问题圆满解决了(话说我为什么加似乎呢?)
C++11出现了,他让我迷茫了。右值引用,有意义吗?他可以优化我们的定义吗?
呃,一开始我见到一个示例,似乎将一个std::string用std::move到另外一个std::string,然后原来的那个string变成空了。呃, 貌似返回值可以用
cstr&& strmove(cstr const& _str){
。。。
return std::move(_str);
}
这样做,与RVO有区别吗?显式定义RVO?
实际上,右值引用完全等同于引用,除了他是右值。啥叫右值?
右值就在右边的,比如
int a=2; //2就是右值
int* a = new int(2);//new int(2)就是右值
这玩意有用吗?是不是右值和我有什么关系,右值可不可以修改,是不是const?
呃,右值应该可以修改,实际上引用、右值引用引发了空前的口水战,无数人认为这是C++类型败笔,过于复杂。(不过我不这么看)
究竟右值引用可以做些什么,std::move可以转移局部变量成为主函数的作用域吗?或者,下面的代码会是我们想要的吗?
class tt{
public:
tt(int*&& pv) : v(pv) {}
print() { printf("x addr:%08x\n", v); if(x!=nullptr) printf("x val:%d\n", *v); }
private:
int *v;
};
int main(){
int *p = new int(2)
tt test(std::move(p));
//tt test(p);//error
delete p;
p = nullptr;
test.print();
getchar();
return 0;
}
很遗憾,输出结果是x addr:00000000
意味着p的值没有move到tt里面
你没有想错,右值引用,真的就只能限制右边的值
那这个东西有什么用?
这就是本文的重点,改变一下上述代码成这样
tt(int*& pv, bool hold=false) :v(nullptr) {//注意,不能int*,而是int*&,前者会导致编译器不知道调用哪个版本重载
printf("use left\n");
if (pv != nullptr) v = new int(*pv);
if (hold) {
delete pv;
pv = nullptr;
}
print();
}//left ref
tt(int* const&& pv) :v(nullptr) {
printf("use right\n");
print();
v = pv;
print();
}//right ref
...
int* a= new int(11);
tt test1(a);
tt test2(new int(10));
...
好了,世界清净了,test根据不同的左/右值自动处理了。你会问为啥这样写,如果把Int改变成某个对象,你猜上述两个版本谁会调用构造函数和复制构造函数多一点?而且右值引用版本的指针,客户只能new,无法delete(delete由tt类代理),似乎好处++,坏处--
当然了,如果可能,尽量用const&或&,引用的对象存放在栈,除了安全++外,逻辑和效率也会++。
注意上述代码,形参表传的指针,其实也是值副本,不过是地址的值副本。我们需要的是指向地址的地址,所以用引用。搞清楚形参表、返回值、局部变量的作用域、生存周期,及传递方式(_stdcall与_cdecl)。很多复杂的话题都可以从基础开始,C++我认为做的还不错,如果再加上C语言typeof关键字就完美了。