C++ 11默认与删除函数
引言
在C++编程中,类通常需要一些特殊的成员函数,如构造函数、析构函数、拷贝构造函数、赋值运算符等。在C++11之前,如果程序员没有显式定义这些函数,编译器会自动生成默认版本。但有时我们可能希望更精确地控制这些函数,这就是C++11引入"默认函数"和"删除函数"特性的原因。
通过这篇文章,你将了解:
- 什么是默认函数(
= default
) - 什么是删除函数(
= delete
) - 如何在实际编程中使用它们
- 这些特性如何帮助你写出更清晰、更安全的代码
默认函数(= default)
什么是默认函数?
在C++中,如果你没有为类定义某些特殊成员函数,编译器会自动生成它们的默认实现。这些特殊成员函数包括:
- 默认构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数(C++11新增)
- 移动赋值运算符(C++11新增)
C++11引入的= default
语法允许程序员显式地要求编译器生成这些特殊成员函数的默认版本。
语法
class ClassName {
public:
// 使用=default请求编译器生成默认版本
ClassName() = default;
~ClassName() = default;
ClassName(const ClassName&) = default;
ClassName& operator=(const ClassName&) = default;
// C++11新增
ClassName(ClassName&&) = default;
ClassName& operator=(ClassName&&) = default;
};
为什么要使用默认函数?
-
明确意图:通过显式使用
= default
,你向其他开发者表明你是有意让编译器生成默认实现,而不是忘记实现这些函数。 -
保持默认行为:当你需要定义其他构造函数时,默认构造函数不会被自动生成。使用
= default
可以确保默认构造函数仍然可用。 -
性能优化:编译器生成的默认函数通常经过了优化,可能比自己手写的版本更高效。
示例
#include <iostream>
class Point {
private:
int x, y;
public:
// 自定义构造函数
Point(int x_val, int y_val) : x(x_val), y(y_val) {}
// 显式请求默认构造函数
Point() = default;
// 显式请求默认拷贝构造函数
Point(const Point&) = default;
void print() const {
std::cout << "Point(" << x << ", " << y << ")" << std::endl;
}
};
int main() {
Point p1; // 使用默认构造函数
Point p2(10, 20); // 使用自定义构造函数
Point p3 = p2; // 使用默认拷贝构造函数
std::cout << "p1: ";
p1.print(); // 输出未初始化的值(取决于编译器)
std::cout << "p2: ";
p2.print(); // 输出 Point(10, 20)
std::cout << "p3: ";
p3.print(); // 输出 Point(10, 20)
return 0;
}
输出:
p1: Point(0, 0) // 可能的输出,具体取决于编译器的行为
p2: Point(10, 20)
p3: Point(10, 20)
注意,默认构造函数不会初始化基本类型的成员变量(如int
、float
等),所以p1
的输出可能是随机值。在实际代码中应当避免这种情况。
删除函数(= delete)
什么是删除函数?
C++11引入的= delete
语法允许程序员显式地禁用某些函数。当尝试调用被删除的函数时,编译器会产生错误。
语法
class ClassName {
public:
// 使用=delete删除函数
ClassName(const ClassName&) = delete;
ClassName& operator=(const ClassName&) = delete;
};
为什么要使用删除函数?
-
防止对象复制:通过删除拷贝构造函数和拷贝赋值运算符,可以禁止对象被复制。
-
禁止隐式类型转换:通过删除某些构造函数,可以防止意外的类型转换。
-
限制特定操作:通过删除某些成员函数或友元函数,可以禁止特定操作,提高代码安全性。
示例1:禁止复制
#include <iostream>
class NonCopyable {
public:
NonCopyable() = default;
// 删除拷贝构造函数和拷贝赋值运算符
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
void doSomething() {
std::cout << "Doing something..." << std::endl;
}
};
int main() {
NonCopyable obj1;
obj1.doSomething(); // 正常工作
// NonCopyable obj2 = obj1; // 编译错误:拷贝构造函数被删除
// NonCopyable obj3;
// obj3 = obj1; // 编译错误:拷贝赋值运算符被删除
return 0;
}
示例2:防止隐式转换
#include <iostream>
#include <string>
class MyString {
private:
std::string data;
public:
// 允许从std::string构造
MyString(const std::string& str) : data(str) {}
// 禁止从C风格字符串隐式转换
explicit MyString(const char* str) : data(str) {}
// 完全禁止从数字类型转换
MyString(int) = delete;
MyString(double) = delete;
void print() const {
std::cout << "MyString: " << data << std::endl;
}
};
int main() {
std::string str = "Hello";
MyString ms1(str); // 正常
ms1.print();
MyString ms2("World"); // 正常,显式转换
ms2.print();
// MyString ms3 = "Test"; // 编译错误:需要显式转换
// MyString ms4 = 123; // 编译错误:从int构造函数被删除
// MyString ms5 = 3.14; // 编译错误:从double构造函数被删除
return 0;
}
输出:
MyString: Hello
MyString: World
实际应用场景
实现单例模式
单例模式是一种常见的设计模式,它确保一个类只有一个实例,并提供全局访问点。使用删除函数可以使单例模式的实现更加清晰和安全。
#include <iostream>
class Singleton {
private:
// 私有静态实例
static Singleton* instance;
// 私有构造函数
Singleton() = default;
public:
// 删除拷贝构造函数和赋值运算符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 获取实例的公共方法
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
void doSomething() {
std::cout << "Singleton is doing something..." << std::endl;
}
};
// 初始化静态成员
Singleton* Singleton::instance = nullptr;
int main() {
Singleton* s1 = Singleton::getInstance();
s1->doSomething();
Singleton* s2 = Singleton::getInstance();
std::cout << "s1 and s2 point to the same instance: " << (s1 == s2 ? "Yes" : "No") << std::endl;
// Singleton copy = *s1; // 编译错误:拷贝构造函数被删除
// Singleton s3; // 编译错误:构造函数是私有的
return 0;
}
输出:
Singleton is doing something...
s1 and s2 point to the same instance: Yes
约束特定行为的基类
通过使用删除函数,可以创建一个基类,禁止子类进行特定操作。
#include <iostream>
// 不可复制的基类
class Noncopyable {
protected:
Noncopyable() = default;
~Noncopyable() = default;
public:
Noncopyable(const Noncopyable&) = delete;
Noncopyable& operator=(const Noncopyable&) = delete;
};
// 使用Noncopyable作为基类
class MyResource : public Noncopyable {
private:
int* data;
public:
MyResource() : data(new int[100]) {
std::cout << "Resource allocated" << std::endl;
}
~MyResource() {
delete[] data;
std::cout << "Resource freed" << std::endl;
}
void use() {
std::cout << "Using resource..." << std::endl;
}
};
int main() {
MyResource res1;
res1.use();
// MyResource res2 = res1; // 编译错误:基类的拷贝构造函数被删除
// MyResource res3;
// res3 = res1; // 编译错误:基类的赋值运算符被删除
return 0;
}
输出:
Resource allocated
Using resource...
Resource freed
默认函数与删除函数的规则
特殊成员函数的自动生成规则
在C++11中,特殊成员函数的自动生成规则变得更加复杂:
-
如果你定义了析构函数,编译器不会自动生成移动构造函数和移动赋值运算符。
-
如果你定义了拷贝构造函数、拷贝赋值运算符、移动构造函数或移动赋值运算符中的任何一个,编译器不会自动生成其他任何特殊成员函数。
-
如果你定义了任何构造函数,编译器不会自动生成默认构造函数。
使用= default
可以覆盖这些规则,强制编译器生成默认实现。
"三/五法则"
C++中有一个著名的"三法则"(在C++11后扩展为"五法则"):如果你需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,你通常需要定义所有三个。在C++11中,这扩展到包括移动构造函数和移动赋值运算符,成为"五法则"。
使用= default
和= delete
可以帮助你更容易地遵循这些规则。
最佳实践
-
明确表达意图:如果你希望使用编译器生成的默认实现,使用
= default
;如果你想禁止某个函数,使用= delete
。 -
避免资源泄漏:对于管理资源的类,通常应该删除拷贝操作或实现移动语义。
-
删除危险函数:如果某个函数在你的类中没有合理的语义,应该将其删除,避免误用。
-
使用
= default
在头文件中声明,在源文件中定义:这样可以提高编译效率。
// 在头文件中
class MyClass {
public:
MyClass() = default;
~MyClass();
// ...
};
// 在源文件中
MyClass::~MyClass() = default;
- 考虑所有特殊成员函数:按照"五法则",如果你定义或删除了任何特殊成员函数,请考虑其他特殊成员函数的行为。
总结
C++11的默认函数(= default
)和删除函数(= delete
)特性给程序员提供了更精确控制类的特殊成员函数的能力。它们不仅能提高代码的清晰度和安全性,还能防止一些常见的编程错误。
- 默认函数允许你显式地请求编译器生成默认实现,表明这是有意为之,而不是遗忘。
- 删除函数允许你禁止某些函数,防止对象被复制、禁止隐式类型转换,或限制其他可能导致问题的操作。
通过合理使用这两个特性,你可以编写更加健壮、更加有表达力的C++代码。
练习
-
创建一个
UniquePtr
类,模仿std::unique_ptr
的行为,使用删除函数禁止复制,但允许移动。 -
设计一个
Configuration
类,它只能通过单例模式访问,并使用删除函数防止复制和多实例创建。 -
创建一个
Matrix
类,它有默认构造函数、参数化构造函数和析构函数。使用= default
显式地请求编译器生成默认实现。
进一步阅读
- C++11标准
- Effective Modern C++,作者Scott Meyers
- C++ Core Guidelines关于特殊成员函数的部分