CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go系统接口编程
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go系统接口编程
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • C++语言面试问题集锦 目录与说明
  • 第一章 auto与类型推导
  • 第二章 关键字static及其不同用法
  • 第三章 多态、继承和虚函数
  • 第四章 Lambda函数
  • 第五章 C++中如何使用 const 限定符
  • 第六章 Modern C++的一些最佳实践
  • 第七章 智能指针
  • 第八章 引用、万能引用等
  • 第九章 C++20相关问题
  • 第十章 特殊函数及数量规则
    • 问题69:解释三法则(the rule of three)
    • 问题70:解释五法则(the rule of five)
    • 问题71:解释零法则(the rule of zero)
    • 问题72:std::move移动了什么?
    • 问题73:什么是析构函数?如何对它进行重载?
    • 问题74:对于未使用/不支持的特殊函数,是应该显式删除,还是声明为私有?
    • 问题75:C++中的平凡类(trivial class)是什么?
    • 问题76:拥有默认构造函数(default constructor)有什么好处?
  • 第十一章 C++面向对象设计
  • 第十二章 程序质量
  • 第十三章 标准模板库
  • 第十四章 杂项
  • cppinterviewmostaskedquestions
zhangxf
2025-03-27
目录

第十章 特殊函数及数量规则

# 第十章 特殊函数及数量规则

# 问题69:解释三法则(the rule of three)

如果一个类需要自定义析构函数、自定义复制构造函数或自定义复制赋值运算符,那么它几乎肯定需要同时定义这三者。

当你按值返回或传递对象、操作容器等时,这些成员函数就会被调用。如果它们没有被自定义,编译器会生成它们(自C++98起)。

自C++98起,编译器会尝试生成:

  • 默认构造函数(T()),它会调用每个类成员和基类的默认构造函数。
  • 复制构造函数(T(const T& other)),它会调用每个成员和基类的复制构造函数。
  • 复制赋值运算符(T& operator=(const T& other)),它会调用每个类成员和基类的复制赋值运算符。
  • 析构函数(~T()),它会调用每个类成员和基类的析构函数。需要注意的是,这个默认生成的析构函数永远不是虚函数(除非它所在的类继承自一个有虚析构函数的类) 。

如果你有一个类持有原始指针、智能指针、文件描述符、数据库连接或其他管理资源的类型,那么编译器生成的函数很可能是不正确的,你应该手动实现它们。

这就引出了三法则:如果你手动实现了复制构造函数、复制赋值运算符或析构函数中的任何一个,编译器就不会生成你未编写的其他函数。所以,如果你需要编写这三个函数中的任何一个,那么可以认为你也需要编写其余的函数。

这就是C++98引入的三法则,明天我们将介绍五法则。

# 问题70:解释五法则(the rule of five)

前面我们讨论了三法则。你还记得它的内容吗?

让我们回顾一下。

如果你手动实现了复制构造函数、复制赋值运算符或析构函数中的任何一个,编译器就不会生成你未编写的其他函数。这是因为编译器认为,如果你必须实现其中一个,那么几乎可以肯定你也需要其他的函数。

三法则是由C++98引入的,而五法则是由C++11引入的。

多出来的两个是什么呢?

这与C++11引入的移动语义(move semantics)有关。所以,如果你手动实现了以下任何一个特殊函数,那么其他特殊函数都不会被生成,你必须自己实现所有这些函数:

  • 复制构造函数:T(const T&)
  • 复制赋值运算符:operator=(const T&)
  • 移动构造函数:T(X&&)
  • 移动赋值运算符:operator=(T&&)
  • 析构函数:~T()

接下来我们将介绍零法则。

# 问题71:解释零法则(the rule of zero)

在过去的两天里,我们讨论了C++98引入的三法则,以及C++11引入的移动语义所补充的五法则。

让我们回顾一下五法则:

如果你手动实现了以下任何一个特殊函数,那么其他特殊函数都不会被生成,你必须自己实现所有这些函数:

  • 复制构造函数:T(const T&)
  • 复制赋值运算符:operator=(const T&)
  • 移动构造函数:T(X&&)
  • 移动赋值运算符:operator=(T&&)
  • 析构函数:~T()

今天,我们用零法则来结束这一系列规则的小专题。

它是C++核心准则中定义的一条规则的昵称:

C.20:如果你可以避免定义默认操作,那就不要定义。

如果所有成员都有各自的特殊函数,那就无需再定义,一个都不用。

class  MyClass  {
public:
    //  . . . 未声明默认操作
private:
    std::string name;
    std::map<int , int> rep;
};

MyClass mc; 
MyClass mc2 {nm}; 
1
2
3
4
5
6
7
8
9
10

由于std::map和std::string都有所有的特殊函数,所以MyClass中无需定义任何特殊函数。

其理念是,如果一个类需要声明任何特殊函数,那么它应该专门处理所有权问题,而其他类则不应声明这些特殊函数。

所以请记住,如果你需要任何特殊函数,就要实现所有特殊函数,但首先应尽量避免需要它们。

# 问题72:std::move移动了什么?

std::move什么都没移动。在运行时,它根本不会执行任何操作,甚至不会生成一个字节的可执行代码。

实际上,std::move只是一个工具,用于将其输入的任何内容转换为右值引用(rvalue reference)。

因此,std::move这个名字不太恰当,也许叫rvalue_cast会更好,但它就叫这个名字,我们只要记住它并不会移动任何东西就行。它返回一个右值引用,而右值引用是移动操作的候选对象。对一个对象使用std::move,是在告诉编译器这个对象可以被移动。这就是std::move名字的由来:便于指定哪些对象可以被移动。

值得注意的是,从常量变量进行移动是不可能的,因为移动构造函数(move constructor)和移动赋值(move assignment)会改变执行移动操作的对象。然而,如果你尝试从常量对象进行移动,编译器不会报错,甚至不会给出警告。对常量对象的移动请求会被悄悄地转换为复制操作。

# 问题73:什么是析构函数?如何对它进行重载?

析构函数是类的一个特殊成员函数。它与类名相同,并且前面有一个波浪号(~)前缀(如~MyClass)。只要对象超出作用域,如果析构函数存在,它就会自动执行。

析构函数没有参数,不能是const、volatile或static的,并且和构造函数一样,它没有返回类型。

默认情况下,析构函数由编译器生成,但你需要注意五法则的应用。如果手动实现了其他4个特殊函数中的任何一个,析构函数就不会被生成。快速回顾一下,除析构函数外的特殊函数还有:

  • 复制构造函数
  • 赋值运算符
  • 移动构造函数
  • 移动赋值运算符

如果类获取了必须释放的资源,就需要析构函数。记住,你应该编写遵循资源获取即初始化(RAII,Resource Acquisition Is Initialization)原则的类,这意味着在构造时获取资源,在析构时释放资源。例如释放连接、关闭文件句柄、保存事务等操作。

如前所述,析构函数没有参数,不能是const、volatile或static的,并且一个类只能有一个析构函数,因此它不能被重载。

另一方面,析构函数可以是虚函数,不过这是明天要讨论的话题。

# 问题74:对于未使用/不支持的特殊函数,是应该显式删除,还是声明为私有?

首先要回答的问题是,为什么要选择这些做法呢?

你可能不希望一个类被复制或移动,所以想要让调用者无法访问相关的特殊函数。一种选择是将它们声明为私有(private)或受保护(protected),另一种选择是显式删除它们。

class  NonCopyable  {
public:
    NonCopyable() {/* . . .*/}
    //  . . .
private:
    NonCopyable(const  NonCopyable&); 
    NonCopyable& operator=(const  NonCopyable&); 
};
1
2
3
4
5
6
7
8

在C++11之前,除了将不需要的特殊函数声明为私有且不实现它们之外,没有其他选择。通过这种方式,可以禁止复制对象(当时还没有移动的概念)。不实现这些函数有助于防止在成员函数、友元函数中意外使用,或者在忽略访问说明符时的误用。不过,这样在链接时会出现问题。

从C++11开始,你可以通过声明它们为= delete来简单地标记为删除:

class  NonCopyable  {
public:
    NonCopyable() {/* . . .*/}
    NonCopyable(const  NonCopyable&) = delete ;
    NonCopyable& operator=(const  NonCopyable&) = delete ;
    //  . . .
private:
    //  . . .
};
1
2
3
4
5
6
7
8
9

C++11的方式更好,原因如下:

  • 它比将函数放在私有部分更明确,将函数放在私有部分可能只是一种错误处理方式;
  • 如果你试图进行复制,在编译时就会报错。

被删除的函数应该声明为公共(public),而不是私有。这不是编译器的强制要求,但有些编译器可能只会抱怨你调用了私有函数,而不会提示该函数已被删除。

# 问题75:C++中的平凡类(trivial class)是什么?

在C++中,如果一个类或结构体(struct)具有编译器提供的或显式默认的特殊成员函数,那么它就是一个平凡类型。它占据连续的内存区域,可以有不同访问说明符的成员。在这种情况下,C++编译器可以自由选择成员的排列顺序。因此,你可以使用memcpy函数处理这样的对象,但不能在C程序中可靠地使用它们。一个平凡类型T可以被复制到char或unsigned char数组中,然后再安全地复制回T类型的变量。请注意,由于对齐要求,类型成员之间可能存在填充字节。

平凡类型有平凡的默认构造函数、平凡的拷贝和移动构造函数、平凡的拷贝和移动赋值运算符,以及平凡的析构函数。在每种情况下,“平凡”意味着构造函数/运算符/析构函数不是用户提供的,并且属于一个满足以下条件的类:

  • 没有虚函数(virtual functions)或虚基类(virtual base classes);
  • 没有具有相应非平凡构造函数/运算符/析构函数的基类;
  • 没有具有相应非平凡构造函数/运算符/析构函数的类类型数据成员。

一个类是否为平凡类,可以通过std::is_trivial特性类(trait class)来验证。它会检查该类是否是平凡可拷贝的(std::is_trivially_copyable)以及是否是平凡可默认构造的(std::is_trivially_default_constructible)。

一些示例:

#include  <iostream>
#include  <type_traits>

class A  {
public:
    int  m;
};

class  B  {
public:
    B() {}
};

class  C  {
public:
    C() = default ;
};

class  D  : C {}; 

class  E  {
    virtual  void  foo() {}
};

int  main() {
    std::cout << std::boolalpha;
    std::cout << std::is_trivial<A>::value << '\n';
    std::cout << std::is_trivial<B>::value << '\n';
    std::cout << std::is_trivial<C>::value << '\n';
    std::cout << std::is_trivial<D>::value << '\n';
    std::cout << std::is_trivial<E>::value << '\n';
}
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

程序输出结果如下:

true
false
true
true
false
1
2
3
4
5

# 问题76:拥有默认构造函数(default constructor)有什么好处?

我们可能会说,默认构造函数让我们能够简单地创建对象,但实际并非完全如此。

确实,不传递任何参数就能创建对象很简单,但只有当创建的对象可以完全使用时,这种简单创建才有意义。如果创建的对象仍需初始化,那么这种简单创建就毫无价值,实际上甚至会产生误导和危害。

另一方面,标准库的许多特性都要求有默认构造函数。

以std::vector为例,当你创建一个包含10个元素的向量(std::vector<T> ts(10);)时,10个默认构造的T对象会被添加到新向量中。

拥有默认构造函数还有助于定义一个刚被移动走的对象的状态。

值得注意的是,有默认构造函数并不意味着你必须定义它。只要有可能,就让编译器生成它。例如,如果一个默认构造函数只是对数据成员进行默认初始化,那么你最好使用类内成员初始化器,让编译器生成默认构造函数。

所以,只要有可能,你就应该有一个默认构造函数,因为它能让你使用更多的语言和标准库特性,但同时也要确保默认构造函数创建的对象是完全可用的。

接下来,我们将探讨面向对象设计的一些内容、继承、C++如何处理多态、奇特递归模板模式(Curiously Recurring Template Pattern)等等。

上次更新: 2025/03/27, 20:29:48
第九章 C++20相关问题
第十一章 C++面向对象设计

← 第九章 C++20相关问题 第十一章 C++面向对象设计→

最近更新
01
第一章 auto与类型推导
03-27
02
第二章 关键字static及其不同用法
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式