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

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

# 第十章 特殊函数及数量规则 # 问题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 rep;

};

MyClass mc;

MyClass mc2 {nm};

12345678910由于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&);

};

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

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

class NonCopyable {

public:

NonCopyable() {/* . . .*/}

NonCopyable(const NonCopyable&) = delete ;

NonCopyable& operator=(const NonCopyable&) = delete ;

// . . .

private:

// . . .

};

123456789C++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

#include

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::value << '\n';

std::cout << std::is_trivial::value << '\n';

std::cout << std::is_trivial::value << '\n';

std::cout << std::is_trivial::value << '\n';

std::cout << std::is_trivial::value << '\n';

}

1234567891011121314151617181920212223242526272829303132程序输出结果如下:

true

false

true

true

false

12345# 问题76:拥有默认构造函数(default constructor)有什么好处? 我们可能会说,默认构造函数让我们能够简单地创建对象,但实际并非完全如此。

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

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

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

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

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

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

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

🎯 相关推荐

2025 年什么游戏可以搬砖挣钱比较稳?资深玩家的稳赚游戏全解析
使用mnggiflab网站录制GIF的完整教程
365bet真人网投

使用mnggiflab网站录制GIF的完整教程

📅 07-03 👀 292
微信提示音文件位置詳解及相關問題解答
bt365app官方下载登录

微信提示音文件位置詳解及相關問題解答

📅 08-04 👀 607