深入理解c++构造函数, 复制构造函数和赋值函数重载(operator=)
注
以下代码编译及运行环境均为 Xcode 6.4, LLVM 6.1 with GNU++11 support, Mac OS X 10.10.2
调用时机
看例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | // // main.cpp // test // // Created by dabao on 15/9/30. // Copyright (c) 2015年 Peking University. All rights reserved. // #include <iostream> class Base { public : Base() { std::cout<< "constructor" <<std::endl; } Base(Base ©) { std::cout<< "copy constructor" <<std::endl; } const Base &operator=(Base ©) { std::cout<< "operator=" <<std::endl; return * this ; } }; int main( int argc, const char * argv[]) { Base a; // 1 Base b = a; // 2 Base c(a); // 3 Base d; // 4 d = a; return 0; } |
输出
1 2 3 4 5 | constructor copy constructor copy constructor constructor operator= |
1,2,3,4 是我们创建一个变量的最主要的方法(构造序列本文不讨论), 其中1,2,3是变量定义, 4是赋值. 因此很明显:
- 定义会调用构造函数, 赋值会调用赋值函数(operator=)
- 复制构造函数是一种特殊的构造函数, 参数是一个变量实例而已
- 2和3等价, 3不会调用赋值函数(手误) 2不会调用赋值函数, 出现等号未必就是赋值
- 如果没有重载以上函数, 3和4效果会一样, 但会少一次函数调用
const来捣乱
那么const又起到什么作用了呢?
继续来看例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | // // main.cpp // test // // Created by dabao on 15/9/30. // Copyright (c) 2015年 Peking University. All rights reserved. // #include <iostream> class Base { public : Base() { std::cout<< "constructor" <<std::endl; } Base(Base ©) { std::cout<< "copy constructor" <<std::endl; } const Base &operator=(Base ©) { std::cout<< "operator=" <<std::endl; return * this ; } }; Base creator() { Base ret; return ret; } int main( int argc, const char * argv[]) { Base a = creator(); // 1 Base b; b = creator(); // 2 return 0; } |
上述代码都会编译出错, 原因是 "No matching constructor". 看代码不难发现原因, creator函数返回的是Base类型, 在c++11里面, 这个称为右值(rvalue), 但是我们的复制构造函数和赋值函数的参数类型都是非const引用类型, 而右值是不允许做这种类型参数的, 所以就编译出错了. 解决方案有两个:
- 使用const引用类型
- 使用右值类型
如下所示
1 2 3 4 5 6 7 8 9 10 | Base( const Base ©) { std::cout<< "copy constructor" <<std::endl; } const Base &operator=(Base &©) { std::cout<< "operator=" <<std::endl; return * this ; } |
其中, const引用类型是最通用的作法, 它可以兼容左值和右值, 也兼容古老的编译器, 右值类型则是c++11引进的新特性(使用&&表明), 可以针对左值和右值选择不同的实现, 比如使用std::move替代operator=, 从而减少内存的申请. 因此, 如果没有特殊需要, 使用const引用类型作为复制构造函数与赋值函数的参数类型.
至此, 构造函数的坑基本说完了, 因为不牵扯到返回值和函数类型的问题, 但是赋值函数(operator=)还有更多的坑来理一理.
const继续搅局
在一个类的成员函数中, const可以出现三个地方: 返回值, 参数, 函数.
1 | const A& operator=( const A& a) const |
因此一个函数可以有8个变种, 但是c++不允许参数类型相同,返回值类型不同的重载, 因此一个函数最多有4种实现.
我们先考虑返回const类型的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | // // main.cpp // test // // Created by dabao on 15/9/30. // Copyright (c) 2015年 Peking University. All rights reserved. // #include <iostream> class A { public : const A& operator=( const A& a) const { std::cout<< "const A& operator=(const A& a) const [" <<a.x<< " > " <<x<< "]" <<std::endl; return * this ; } const A& operator=( const A& a) { std::cout<< "const A& operator=(const A& a) [" <<a.x<< " > " <<x<< "]" <<std::endl; return * this ; } const A& operator=(A& a) const { std::cout<< "const A& operator=(A& a) const [" <<a.x<< " > " <<x<< "]" <<std::endl; return * this ; } const A& operator=(A& a) { std::cout<< "const A& operator=(A& a) [" <<a.x<< " > " <<x<< "]" <<std::endl; return * this ; } std::string x; A() : x( "" ){} A(std::string x_) : x(x_) {} }; int main( int argc, const char * argv[]) { A a( "a" ), b( "b" ); const A c( "const c" ),d( "const d" ); c = d; c = b; a = d; a = b; return 0; } |
输出结果
1 2 3 4 | const A& operator=( const A& a) const [ const d > const c] const A& operator=(A& a) const [b > const c] const A& operator=( const A& a) [ const d > a] const A& operator=(A& a) [b > a] |
结果很明显, 被赋值变量决定函数, 赋值变量决定参数, a=b 等价于 a.operator(b), 这里没什么问题.
但是, 有一个很奇怪的地方, a=d 这一句, a是非const的, 调用了 const A& operator=(const A& a) [const d > a], 返回值是个const类型, 这怎么可以呢? 返回值的const是什么意思呢? 这是非常有迷惑性的. 这个问题的关键点在于:
a是这个函数的一部分, 并不是返回值的承接者. 因此 a=d 实际上是等价于 const A& ret = a.operator=(d), 也就是说, operator=的返回值类型和被赋值的变量是没有任何关系的!
加入以下代码
1 2 | const A &m = (a = d); // 1 A &n = (a = d); // 2 |
2会编译错误, 原因就在于把 const A& 绑定给 A&, 这肯定是错误的. 因此再重复一遍, operator=的返回值和被赋值变量没有任何关系.
那么返回值有什么意义呢? 这就和iostream类似了, 是为了进行串联赋值, 亦即 a=b=c
来看最后的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | // // main.cpp // test // // Created by dabao on 15/9/30. // Copyright (c) 2015年 Peking University. All rights reserved. // #include <iostream> class A { public : const A& operator=( const A& a) const { std::cout<< "const A& operator=(const A& a) const [" <<a.x<< " > " <<x<< "]" <<std::endl; return * this ; } const A& operator=( const A& a) { std::cout<< "const A& operator=(const A& a) [" <<a.x<< " > " <<x<< "]" <<std::endl; return * this ; } const A& operator=(A& a) const { std::cout<< "const A& operator=(A& a) const [" <<a.x<< " > " <<x<< "]" <<std::endl; return * this ; } const A& operator=(A& a) { std::cout<< "const A& operator=(A& a) [" <<a.x<< " > " <<x<< "]" <<std::endl; return * this ; } std::string x; A() : x( "" ){} A(std::string x_) : x(x_) {} }; int main( int argc, const char * argv[]) { A a( "a" ), b( "b" ); const A c( "const c" ),d( "const d" ); (a = b) = c; // 1 (a = c) = b; // 2 a = b = c; // 3 return 0; } |
输出
1 2 3 4 5 6 | const A& operator=(A& a) [b > a] const A& operator=( const A& a) const [ const c > a] const A& operator=( const A& a) [ const c > a] const A& operator=(A& a) const [b > a] const A& operator=( const A& a) [ const c > b] const A& operator=( const A& a) [b > a] |
可以得出如下结论:
- 1和3比较可以发现, 赋值的顺序是从右往左执行的
- 返回值是const类型, 那么再被赋值就会调用const函数了
总结
- 复制构造函数和赋值函数出现在两种不同的场景里, 不是出现等号就会调用赋值函数
- 赋值函数的返回值和被赋值变量是完全独立的
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架