1. C++11 以后都有哪些默认构造函数?
1) 默认构造函数
2)析构函数
3)拷贝构造函数
4)拷贝赋值运算符构造函数
5)移动构造函数(C++11新增)
6)移动赋值运算符构造函数(C++新增)
2. 构造函数举例
#include <iostream>class Point {
public:int x, y;// 拷贝构造函数Point(const Point& other) : x(other.x), y(other.y) {std::cout << "Copy constructor called\n";}
};int main() {Point p1 = {1, 2};Point p2(p1); // 调用拷贝构造函数return 0;
}
#include <iostream>
#include <algorithm> // For std::copyclass Data {
public:int* values;size_t size;Data() : values(nullptr), size(0) {}explicit Data(size_t size) : size(size) {values = new int[size];std::fill(values, values + size, 0); // 初始化为0}~Data() {delete[] values;}// 拷贝构造函数Data(const Data& other) : size(other.size) {values = new int[size];std::copy(other.values, other.values + size, values);}// 拷贝赋值运算符Data& operator=(const Data& other) {if (this != &other) { // 检查自赋值delete[] values; // 释放原有资源size = other.size;values = new int[size];std::copy(other.values, other.values + size, values);}return *this;}
};int main() {Data d1(5); // 创建一个大小为5的Data对象Data d2 = d1; // 调用拷贝构造函数Data d3;d3 = d2; // 调用拷贝赋值运算符return 0;
}
class MyClass {
public:int* data;// 移动构造函数MyClass(MyClass&& other) : data(other.data) {other.data = nullptr; // 将源对象的指针置为空,防止其析构时释放内存}
};
class MyClass {
public:int* data;// 移动赋值运算符MyClass& operator=(MyClass&& other) {if (this != &other) { // 检查自赋值delete[] data; // 释放原有资源data = other.data;other.data = nullptr; // 将源对象的指针置为空,防止其析构时释放内存}return *this; // 返回当前对象的引用}
};
3. 什么情况不会生成默认构造函数?
-
用户定义了其他构造函数: 如果类中定义了至少一个构造函数,编译器将不会自动生成默认构造函数。
-
类中有不具有默认构造函数的非静态成员对象: 如果类包含一个或多个没有默认构造函数的非静态成员对象,编译器不会生成默认构造函数。
-
类中有虚拟函数: 如果类中有虚拟函数(即至少有一个函数声明为
virtual
),编译器不会生成默认构造函数。 -
类中有引用成员: 引用需要在创建时被初始化,因此如果类包含引用类型的成员,必须提供一个构造函数来初始化这些成员。
-
类中有常量成员: 常量成员也必须在创建时初始化,所以如果类包含常量类型的成员,编译器不会生成默认构造函数。
-
类是抽象类: 如果类是抽象类(包含至少一个纯虚函数),则不会有默认构造函数,因为抽象类不能实例化。
-
类中有自定义的默认构造函数: 如果已经显式定义了默认构造函数,则不需要生成另一个。
-
类中有
:
初始化的非静态成员: 如果类成员使用了:
初始化(即在成员声明时直接赋值),则需要构造函数来初始化这些成员。 -
类中有动态分配的成员: 如果类中包含动态分配的成员(如指针成员),通常需要自定义构造函数来正确地分配内存。
-
类继承自一个没有默认构造函数的基类: 如果派生类继承自一个没有默认构造函数的基类,派生类也不会有默认构造函数。
-
类中有
std::initializer_list
成员: 如果类包含std::initializer_list
类型的成员,需要自定义构造函数来初始化。 -
类中有数组成员: 如果类包含数组类型的成员,通常需要自定义构造函数来初始化数组。
4. 什么是POD类型的对象?
在C++中,POD是“Plain Old Data”的缩写,指的是最普通的C++数据类型和对象。POD类型包括了C++中的原始数据类型(如int
、double
等)、POD结构体(不含用户定义的构造函数、虚函数、静态成员、引用成员等的类)、POD联合体、POD数组以及它们的数组。
4.1. POD类型满足以下条件:
-
不包含用户自定义的构造函数:POD类型不能包含任何构造函数。
-
不包含虚函数:POD类型不能包含任何虚函数。
-
不包含静态成员变量:POD类型不能包含静态成员变量。
-
不包含引用成员:POD类型不能包含引用类型的成员。
-
不包含常量成员:POD类型不能包含常量成员,除非它们是字面量。
-
不包含基类或派生自其他类:POD类型不能有继承关系。
-
成员变量也必须是POD类型:POD类型的成员变量也只能是POD类型。
-
标准布局类型:POD类型必须是标准布局类型(Standard-Layout Type),这意味着其成员的排列方式在不同的实现中是一致的。
//POD 类型举例
class PODStruct {int x;double y[3];char z;
};int main() {int i; // POD类型PODStruct s; // POD类型char str[] = "Hello, World!"; // POD类型
}//非POD类型举例class NonPODStruct {NonPODStruct() : value(0) {} // 用户自定义的构造函数int value;
};class NonPODStruct2 {static int static_member; // 静态成员变量int value;
};class NonPODStruct3 {int& ref_member; // 引用成员int value;
};class NonPODStruct4 {const int const_member; // 常量成员int value;
};class NonPODClass {
public:virtual void foo() {} // 虚函数
};int main() {NonPODStruct non_pod1; // 非POD类型NonPODStruct2 non_pod2; // 非POD类型NonPODStruct3 non_pod3; // 非POD类型NonPODStruct4 non_pod4; // 非POD类型NonPODClass non_pod5; // 非POD类型
}
4.2 POD类型的作用:
-
简单的内存布局: POD类型具有简单的内存布局,这意味着它们的内存表示直接对应于它们的成员变量的内存表示的串联。这种简单性使得POD类型易于理解和使用。
-
高性能: 由于POD类型的内存布局简单,它们可以高效地在函数间传递和存储。编译器可以优化POD类型的访问和操作,从而提高程序性能。
-
内存对齐: POD类型通常具有自然的内存对齐,这意味着它们的内存布局符合硬件的对齐要求,这有助于提高访问速度。
-
与C语言的兼容性: POD类型可以无缝地与C语言代码互操作,因为它们的内存布局与C语言结构体相同。
-
字节级操作: POD类型可以进行字节级操作,如内存拷贝和比较,而不需要调用构造函数、析构函数或赋值运算符。
-
默认初始化: C++标准保证POD类型的默认初始化将所有字节设置为零,这简化了某些类型的初始化过程。
4.3 POD类型的使用场景:
-
与C代码互操作: 当需要与C语言库或API交互时,POD类型非常有用,因为它们可以直接映射到C语言的结构体。
-
性能敏感的应用程序: 在性能至关重要的场景中,如嵌入式系统、游戏开发或实时系统,POD类型可以提供高效的数据处理。
-
内存操作: 当需要手动管理内存或进行低层次的内存操作时,POD类型是理想的选择,因为它们可以确保内存布局的一致性和简单性。
-
数据结构的序列化: POD类型可以轻松地进行序列化和反序列化,因为它们的内存布局是已知的,这使得它们适合于网络通信和文件存储。
-
数组操作: 当处理固定大小的数据数组时,POD类型可以提供高效的存储和访问方式。
-
科学计算和数值分析: 在科学计算和数值分析中,POD类型常用于表示向量、矩阵和其他数学结构。
-
内存对齐要求: 在需要特定内存对齐的硬件或平台编程中,POD类型的自然对齐特性非常有用。
-
简单的结构体: 对于只包含数据而没有成员函数的简单结构体,POD类型是合适的选择。
-
缓存和数据存储: POD类型可以用于缓存和数据存储,因为它们可以被直接映射到存储介质。
-
位字段: 在需要精确控制内存布局的场景中,如位字段操作,POD类型可以提供这种控制。
5. C++11中 explicit
的作用和举例
在C++中,explicit
关键字用于类声明中的构造函数,其作用是防止编译器使用该构造函数进行类型转换,从而增加代码的明确性和安全性。
-
防止隐式转换: 使用
explicit
关键字可以防止编译器在需要时自动调用该构造函数进行隐式类型转换。这可以避免一些不直观的转换,减少潜在的错误。 -
增加代码明确性: 当构造函数被声明为
explicit
时,对象的创建必须显式地使用构造函数进行,这使得代码的意图更加明确。
#include <iostream>
#include <string>class MyClass {
public:int value;// 显式构造函数explicit MyClass(int val) : value(val) {}// 隐式构造函数(用于演示)MyClass(const std::string& val) : value(std::stoi(val)) {}
};int main() {MyClass obj1 = 10; // 正确:显式调用构造函数MyClass obj2(MyClass(20)); // 正确:显式调用构造函数MyClass obj3 = "30"; // 错误:隐式构造函数被阻止,需要显式调用// MyClass obj4("40"); // 正确:显式调用隐式构造函数std::cout << "obj1: " << obj1.value << std::endl;std::cout << "obj2: " << obj2.value << std::endl;// std::cout << "obj3: " << obj3.value << std::endl;// std::cout << "obj4: " << obj4.value << std::endl;return 0;
}
6. C++11 中显示删除构造函数
在C++中,显式删除函数(也称为删除成员函数)是使用delete
关键字来防止编译器自动生成默认的特殊成员函数(如拷贝构造函数、赋值运算符、移动构造函数和移动赋值运算符)。从C++11开始,可以在类内部直接使用= delete;
来删除特定的函数。
-
与
default
一起使用: 如果显式定义了默认构造函数、拷贝构造函数、移动构造函数、拷贝赋值运算符或移动赋值运算符中的任何一个,就需要显式地定义其他的,否则默认的会被删除。
class MyClass {
public:MyClass() = default; // 显式默认构造函数MyClass(const MyClass&) = default; // 显式默认拷贝构造函数MyClass& operator=(const MyClass&) = default; // 显式默认拷贝赋值运算符MyClass(MyClass&&) = default; // 显式默认移动构造函数MyClass& operator=(MyClass&&) = default; // 显式默认移动赋值运算符// 删除拷贝赋值运算符MyClass& operator=(const MyClass&) const = delete;
};int main() {// ...return 0;
}
-
类模板: 在类模板中,如果模板参数会影响某些操作的可行性,可以使用SFINAE(Substitution Failure Is Not An Error)技巧来条件性地删除函数。
template <typename T>
class MyClass {
public:void swap(MyClass& other) {// ...}// 特化,当T是不可移动的类型时,删除移动操作void swap(MyClass& other, typename std::enable_if<!std::is_move_constructible<T>::value>::type* = 0) = delete;
};int main() {// ...return 0;
}
-
删除函数与继承: 如果派生类需要删除从基类继承的特定成员函数,可以在派生类中再次使用
delete
关键字。
class Base {
public:Base& operator=(const Base&) = delete; // 基类中删除赋值运算符
};class Derived : public Base {
public:// 派生类继承了基类的赋值运算符删除using Base::operator=;
};int main() {Derived d1, d2;// d1 = d2; // 编译错误:赋值运算符被删除return 0;
}
-
异常安全: 删除某些操作(如拷贝或移动操作)可能会影响异常安全的保证,因为这些操作可能在资源管理中起作用。
class NonCopyable {
public:NonCopyable() = default;NonCopyable(const NonCopyable&) = delete; // 删除拷贝构造函数NonCopyable& operator=(const NonCopyable&) = delete; // 删除拷贝赋值运算符NonCopyable(NonCopyable&&) = default; // 保留移动构造函数NonCopyable& operator=(NonCopyable&&) = default; // 保留移动赋值运算符
};int main() {NonCopyable a;// NonCopyable b = a; // 编译错误:拷贝构造函数被删除// NonCopyable c;// c = a; // 编译错误:拷贝赋值运算符被删除return 0;
}