当前位置: 首页 > news >正文

EffctiveC++_02第二章

文章目录

    • 条款05:了解 C++ 默默编写并调用哪些函数
      • 说明与介绍
      • 代码对比示例
      • 运行结果示例(可能因编译器不同略有差异)
      • 关键要点解析
        • 1. 空类并非真的“空”
        • 2. 编译器生成函数的默认行为
        • 3. 编译器拒绝生成的情况
        • 4. C++11 移动操作的自动生成
      • 总结

条款05:了解 C++ 默默编写并调用哪些函数

说明与介绍

核心思想:当你定义一个空类(没有声明任何成员函数)时,C++ 编译器会为你自动声明(如果代码需要它们)一些成员函数。如果你没有声明,编译器会默默生成它们的默认版本。这些函数包括:

  1. 默认构造函数:如果没有声明任何构造函数,编译器生成一个无参的默认构造函数。
  2. 析构函数:编译器生成的析构函数是非虚的(除非基类有虚析构函数)。
  3. 拷贝构造函数:用于以同类型对象初始化另一个对象。
  4. 拷贝赋值运算符:用于将同类型对象赋值给另一个对象。

C++11开始,编译器还可能自动生成:
5.移动构造函数:用于以同类型右值对象初始化另一个对象。
6.移动赋值运算符:用于将同类型右值对象赋值给另一个对象。

编译器自动生成的函数行为

  • 它们都是publicinline的。
  • 默认构造函数调用基类和成员对象的默认构造函数。
  • 析构函数调用基类和成员对象的析构函数(注意是非虚的)。
  • 拷贝构造函数和拷贝赋值运算符以成员逐一的方式拷贝非静态成员(对于数组成员,逐个拷贝元素)。
  • 移动构造函数和移动赋值运算符以成员逐一的方式移动非静态成员。

什么情况下编译器会拒绝生成?

  • 如果你声明了任何构造函数(包括拷贝构造、移动构造),编译器就不再生成默认构造函数。
  • 如果你声明了移动操作或拷贝操作中的某些函数,编译器可能不再生成其他默认函数(具体规则复杂,详见 C++11 的“=default”和“=delete”机制)。
  • 如果成员变量具有不可拷贝的类型(如const成员、引用成员、或者删除了拷贝操作的成员类型),编译器不会生成拷贝赋值运算符(有时也会影响拷贝构造)。
  • 如果基类的相应操作是不可访问的(private)或被删除,编译器生成的版本也会被删除。

代码对比示例

// 文件名: clause05.cpp// 编译: g++ -std=c++11 clause05.cpp -o clause05#include<iostream>usingnamespacestd;// ========== 辅助类,用于观察构造/析构/拷贝 ==========classTracked{public:string name;Tracked(conststring&n="default"):name(n){cout<<"Tracked 构造: "<<name<<endl;}Tracked(constTracked&rhs):name(rhs.name+"(copy)"){cout<<"Tracked 拷贝构造: "<<name<<endl;}Tracked&operator=(constTracked&rhs){name=rhs.name+"(assign)";cout<<"Tracked 赋值: "<<name<<endl;return*this;}~Tracked(){cout<<"Tracked 析构: "<<name<<endl;}};// ========== 1. 空类,编译器自动生成默认函数 ==========classEmpty{};// 验证:编译器生成的函数确实存在voidtestEmpty(){cout<<"\n=== 测试空类 Empty ==="<<endl;Empty e1;// 调用默认构造函数(编译器生成)Emptye2(e1);// 调用拷贝构造函数(编译器生成)e1=e2;// 调用拷贝赋值运算符(编译器生成)// 析构函数自动调用cout<<"Empty 对象操作完成"<<endl;}// ========== 2. 带成员对象的类,观察编译器生成函数的调用 ==========classNamedObject{public:// 只声明了一个构造函数,因此编译器不会生成默认构造函数NamedObject(conststring&name,intvalue):nameValue(name),objectValue(value){}// 没有声明拷贝构造、拷贝赋值、析构,编译器会生成它们// 为了观察,我们提供访问函数constTracked&getName()const{returnnameValue;}intgetValue()const{returnobjectValue;}private:Tracked nameValue;// 成员对象,有构造/析构/拷贝intobjectValue;};voidtestNamedObject(){cout<<"\n=== 测试 NamedObject ==="<<endl;NamedObjectno1("first",100);// 调用我们声明的构造函数NamedObjectno2("second",200);NamedObjectno3(no1);// 调用编译器生成的拷贝构造函数// 拷贝构造函数会逐一拷贝成员:// nameValue 用 no1.nameValue 拷贝构造// objectValue 用 no1.objectValue 拷贝(int 直接拷贝)no2=no1;// 调用编译器生成的拷贝赋值运算符// 拷贝赋值运算符会逐一赋值成员:// nameValue = no1.nameValue (调用 Tracked 的赋值运算符)// objectValue = no1.objectValuecout<<"no3 的 name: "<<no3.getName().name<<endl;cout<<"no2 的 name: "<<no2.getName().name<<endl;// 析构时自动调用 Tracked 析构}// ========== 3. 编译器拒绝生成的情况 ==========classContainsRef{public:ContainsRef(int&ref):someRef(ref){}// 必须初始化引用成员// 没有声明拷贝赋值运算符,但编译器不会生成,因为引用成员不能重新绑定// 如果试图赋值,会导致编译错误private:int&someRef;// 引用成员constintsomeConst;// const 成员(也需要初始化,但这里没初始化会报错)// 注意:如果 both 引用和 const 成员存在,必须通过初始化列表初始化,// 否则编译错误。这里为了简洁只保留引用成员演示。};voidtestContainsRef(){cout<<"\n=== 测试包含引用成员的类 ==="<<endl;intx=10,y=20;ContainsRefcr1(x);ContainsRefcr2(y);// ContainsRef cr3(cr1); // 尝试拷贝构造:可能编译失败(因为引用成员不可拷贝构造?实际编译器会尝试拷贝引用,即让 cr3.someRef 绑定到 cr1.someRef 绑定的对象,这是允许的,因为引用可以绑定到原引用绑定的对象)// 但重要的是拷贝赋值:cr1 = cr2; // 错误!编译器不会生成拷贝赋值运算符,因为引用成员无法重新赋值cout<<"ContainsRef 对象演示完成(赋值不可用)"<<endl;}// ========== 4. C++11 移动操作的自动生成 ==========classMovable{public:Movable()=default;Movable(conststring&s):data(newstring(s)){cout<<"Movable 构造: "<<*data<<endl;}// 没有声明拷贝/移动操作,编译器会生成它们(但移动操作仅在需要时生成)// 自定义析构函数会抑制移动操作的自动生成?实际上规则:// 如果用户声明了析构函数,编译器不再自动生成移动操作(但拷贝操作仍生成,不过已弃用)。// 为了演示,我们不声明析构函数,让编译器默认生成。~Movable(){if(data)cout<<"Movable 析构: "<<*data<<endl;deletedata;}// 拷贝构造函数(编译器生成)// 拷贝赋值运算符(编译器生成)// 移动构造函数(编译器可能生成,取决于是否被使用且条件满足)// 移动赋值运算符(类似)voidprint()const{if(data)cout<<*data<<endl;elsecout<<"null"<<endl;}private:string*data=nullptr;};voidtestMovable(){cout<<"\n=== 测试 C++11 移动操作 ==="<<endl;Movablem1("hello");Movablem2(std::move(m1));// 应该调用移动构造函数(如果生成)// 移动后 m1 的数据被移走,处于有效但未指定状态cout<<"m1: ";m1.print();cout<<"m2: ";m2.print();Movablem3("world");m3=std::move(m2);// 移动赋值cout<<"m2: ";m2.print();cout<<"m3: ";m3.print();}intmain(){testEmpty();testNamedObject();testContainsRef();testMovable();return0;}

运行结果示例(可能因编译器不同略有差异)

=== 测试空类 Empty === Empty 对象操作完成 === 测试 NamedObject === Tracked 构造: first Tracked 构造: second Tracked 拷贝构造: first(copy) Tracked 赋值: first(assign) no3 的 name: first(copy) no2 的 name: first(assign) Tracked 析构: first(assign) Tracked 析构: first(copy) Tracked 析构: second Tracked 析构: first === 测试包含引用成员的类 === ContainsRef 对象演示完成(赋值不可用) === 测试 C++11 移动操作 === Movable 构造: hello Movable 析构: (null? 取决于实现) m1: null m2: hello Movable 构造: world m2: null m3: world Movable 析构: world Movable 析构: hello Movable 析构:

关键要点解析

1. 空类并非真的“空”
  • 即使你定义一个空类Empty,编译器也会在需要时生成默认构造、拷贝构造、拷贝赋值、析构函数。这些函数都是 inline 的 public 成员。
  • 所以代码Empty e1;Empty e2(e1);都可以通过编译,尽管你没写任何函数。
2. 编译器生成函数的默认行为
  • 对于NamedObject,我们只声明了一个自定义构造函数,因此编译器不再生成默认构造函数。但拷贝构造、拷贝赋值、析构仍然生成。
  • 生成的拷贝构造函数:以成员逐一的方式用no1的成员初始化no3的成员。这里nameValueTracked类型,会调用Tracked的拷贝构造函数;objectValueint,直接复制。
  • 生成的拷贝赋值运算符:以成员逐一的方式将no1的成员赋给no2的成员。这会导致调用Tracked的赋值运算符。
  • 注意:编译器生成的拷贝赋值运算符要求所有成员的类型都提供可用的赋值运算符。如果某个成员是const或引用,编译器会拒绝生成拷贝赋值(因为没有合法方式给const或引用赋值)。
3. 编译器拒绝生成的情况
  • ContainsRef包含一个引用成员someRef。引用必须在构造时初始化,并且不能重新绑定。因此编译器无法生成拷贝赋值运算符(赋值意味着尝试改变引用的指向,不可能)。如果你尝试使用拷贝赋值(如cr1 = cr2),编译将报错。
  • 同理,如果成员是const对象,也不能被赋值,因此拷贝赋值运算符会被隐式删除。
4. C++11 移动操作的自动生成
  • 在 C++11 中,如果类没有用户声明的拷贝操作、移动操作、析构函数,编译器会自动生成移动构造函数和移动赋值运算符(当需要时)。它们执行成员逐一的移动。
  • 在上例中,Movable没有声明拷贝构造/赋值、移动构造/赋值,也没有声明析构函数?其实我们声明了析构函数(~Movable()),这会抑制移动操作的自动生成!这是 C++11 的一个重要规则:如果用户声明了析构函数,编译器就不会自动生成移动操作。但我们的示例中声明了析构函数,所以实际不会生成移动操作,代码中的std::move会回退到拷贝操作(因为移动操作不存在)。为了真正演示移动操作,我们应移除自定义析构函数,或者显式=default移动操作。
  • 所以实际运行结果可能显示的是拷贝行为而非移动。正确的演示需要调整代码(例如不声明析构函数,或显式声明移动操作为default)。这里保留代码以说明问题,并提醒读者注意规则。

总结

  • 编译器会默默地为类生成一些成员函数(默认构造、析构、拷贝构造、拷贝赋值,C++11 还可能生成移动构造和移动赋值)。
  • 这些默认函数的行为是“成员逐一初始化/赋值/析构”。
  • 当你声明了某些函数(如自定义构造函数、拷贝操作、移动操作、析构函数)时,编译器可能会停止生成其他默认函数。
  • 理解这些隐式生成的函数对于控制类的行为至关重要,特别是在资源管理、多态和异常安全方面。
http://www.cnnetsun.cn/news/1391839.html

相关文章:

  • 番茄小说下载器:Rust重写的高性能离线阅读解决方案
  • Windows Cleaner系统清理工具全攻略:让C盘重获新生的实用指南
  • 【GitHub项目推荐--Yazi:极速异步终端文件管理器】⭐⭐⭐⭐⭐
  • 颠覆传统显示:3种虚拟显示驱动技术提升效率60%
  • AI全身全息感知Holistic Tracking:5分钟快速部署,小白也能玩转543个关键点检测
  • 别再堆砌“张三说、李四讲”!百考通帮你写出有脉络、有批判的高质量综述
  • 零基础玩转AI修图:Qwen-Image-2512-ComfyUI快速上手指南
  • STM32CubeMX实战:5个HAL库/LL库常见BUG及修复方案(附代码)
  • 基于Git-RSCLIP的遥感图像风格迁移应用
  • 杰理之连接的设备暂停一段时间再播放dac概率卡顿【篇】
  • 3步彻底解决魔兽争霸3帧率卡顿:WarcraftHelper实战优化指南
  • Terragrunt生态系统:插件与扩展功能全解析
  • GitHub Linguist数据可视化:语言分布图表生成终极指南 [特殊字符]
  • Nanbeige 4.1-3B镜像免配置教程:预编译Whl包加速安装流程
  • Nanbeige 4.1-3B快速部署:基于NVIDIA Container Toolkit的GPU直通配置
  • 简单的停车场管理系统的C语言实现示例
  • Z-Image-GGUF在工业检测中的应用:生成缺陷样本扩充数据集
  • 5步掌握Beyond Compare 5密钥生成:从诊断到实战的完整技术指南
  • Qwen3-32B-Chat百度搜索高频词覆盖:开源大模型部署教程+GPU算力适配
  • 如何高效处理文件系统操作:GitHub_Trending/ch/checkout的FsHelper错误处理机制详解
  • Clawdbot企业微信联动实战:采购单自动审查,AI嵌入工作流真实案例
  • InstructPix2Pix与软件测试:自动化测试图像生成
  • Realistic Vision V5.1 计算机基础关联:从计算机组成原理看GPU算力对扩散模型的意义
  • skill-icons完全指南:从入门到精通,打造专业级GitHub技能展示区
  • 老男孩92期Linux云运维工程师课程,视频无水印,资料无密码,还送配套VMware、Xshell工具。内容包括Linux初级到高级、ES、Docker、K8S、Prometheus、Ceph等完整课
  • 解决Python包安装报错:ERROR: No matching distribution found for xxx的实用指南
  • 如何使用Fuzzywuzzy实现电信行业用户号码相似性检索的终极指南
  • 书匠策AI:文献综述的“智能导航仪”,开启学术写作新纪元!
  • Llama-3.2V-11B-cot 效果对比评测:多模态理解能力超越传统方案的案例
  • Meixiong Niannian画图引擎参数详解:随机种子-1的多样性熵值与采样分布