若要自定义类初始化其成员的方式,或是如要在创建类的对象时调用函数,请定义构造函数。构造函数具有与类相同的名称,没有返回值。可以定义所需数量的重载构造函数,以各种方式自定义初始化。通常,构造函数具有公共可访问性以便类定义或继承层次结构外部的代码可以创建类的对象。 但也可以将构造函数声明为 protected 或 private。
构造函数可以选择采用成员初始化表达式列表。 与在构造函数主体中赋值相比,初始化类成员是更高效的方式。 以下示例演示具有三个重载构造函数的类 Box。 最后两个构造函数使用成员初始化列表:
class Box {
public:
// Default constructor
Box() {}
// Initialize a Box with equal dimensions (i.e. a cube)
explicit Box(int i) : m_width(i), m_length(i), m_height(i) // member init list
{}
// Initialize a Box with custom dimensions
Box(int width, int length, int height)
: m_width(width), m_length(length), m_height(height)
{}
int Volume() { return m_width * m_length * m_height; }
private:
// Will have value of 0 when default constructor is called.
// If we didn't zero-init here, default constructor would
// leave them uninitialized with garbage values.
int m_width{ 0 };
int m_length{ 0 };
int m_height{ 0 };
};
声明类的实例时,编译器会基于重载决策选择要调用的构造函数:
int main()
{
Box b; // Calls Box()
// Using uniform initialization (preferred):
Box b2 {5}; // Calls Box(int)
Box b3 {5, 8, 12}; // Calls Box(int, int, int)
// Using function-style notation:
Box b4(2, 4, 6); // Calls Box(int, int, int)
}
● 构造函数可以声明为 inline、explicit、friend 或 constexpr。
● 构造函数可以初始化一个已声明为 const、volatile 或 const volatile 的对象。 该对象在构造函数完成之后成为 const。
● 若要在实现文件中定义构造函数,请为它提供限定名称,如同任何其他成员函数一样:Box::Box(){...}。
成员初始化表达式列表
构造函数可以选择具有成员初始化表达式列表,该列表会在构造函数主体运行之前初始化类成员。
首选成员初始化表达式列表,而不是在构造函数主体中赋值。 成员初始化表达式列表直接初始化成员。 以下示例演示了成员初始化表达式列表,该列表由冒号后的所有 identifier(argument) 表达式组成:
Box(int width, int length, int height)
: m_width(width), m_length(length), m_height(height)
{}
const 成员和引用类型的成员必须在成员初始化表达式列表中进行初始化。
这是因为 const 成员一旦被初始化就不能再被更改,而引用类型成员在声明时必须被绑定到一个对象,不能为空。
class MyClass
{
public:
MyClass(int value, int& ref)
:constValue(value), reference(ref){}
void print() const {
std::cout << constValue << endl;
std::cout << reference << endl;
}
private:
const int constValue;
int& reference;
};
int main()
{
int a = 10;
MyClass obj(5, a);
obj.print();
return 0;
}
默认构造函数
默认构造函数通常没有参数,但它们可以具有带默认值的参数。
class Box {
public:
Box() { /*perform any required default initialization steps*/}
// All params have default values
Box (int w = 1, int l = 1, int h = 1): m_width(w), m_height(h), m_length(l){}
...
}
默认构造函数是特殊成员函数之一。 如果类中未声明构造函数,则编译器提供隐式 inline 默认构造函数。
#include <iostream>
using namespace std;
class Box {
public:
int Volume() {return m_width * m_height * m_length;}
private:
int m_width { 0 };
int m_height { 0 };
int m_length { 0 };
};
int main() {
Box box1; // Invoke compiler-generated constructor
cout << "box1.Volume: " << box1.Volume() << endl; // Outputs 0
}
如果你依赖于隐式默认构造函数,请确保在类定义中初始化成员,如前面的示例所示。 如果没有这些初始化表达式,成员会处于未初始化状态,Volume() 调用会生成垃圾值。 一般而言,即使不依赖于隐式默认构造函数,也最好以这种方式初始化成员。
可以通过将隐式默认构造函数定义为已删除来阻止编译器生成它:
// Default constructor
Box() = delete;
调用编译器生成的默认构造函数并尝试使用括号时,系统会发出警告:
class myclass{};
int main(){
myclass mc(); // warning C4930: prototyped function not called (was a variable definition intended?)
}
此语句是“最棘手的分析”问题的示例。 可以将 myclass md(); 解释为函数声明或是对默认构造函数的调用。 因为 C++ 分析程序更偏向于声明,因此表达式会被视为函数声明。
如果声明了任何非默认构造函数,编译器不提供默认构造函数:
class Box {
public:
Box(int width, int length, int height)
: m_width(width), m_length(length), m_height(height){}
private:
int m_width;
int m_length;
int m_height;
};
int main(){
Box box1(1, 2, 3);
Box box2{ 2, 3, 4 };
Box box3; // C2512: no appropriate default constructor available
}
如果类没有默认构造函数,则无法通过单独使用方括号语法来构造该类的对象数组。 例如,在前面提到的代码块中,Box 数组无法进行如下声明:
Box boxes[3]; // C2512: no appropriate default constructor available
但是,你可以使用一组初始化表达式列表来初始化 Box 对象数组:
Box boxes[3]{ { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
复制构造函数
复制构造函数通过从相同类型的对象复制成员值来初始化对象。 如果类成员都是简单类型(如标量值),则编译器生成的复制构造函数已够用,你无需定义自己的函数。 如果类需要更复杂的初始化,则需要实现自定义复制构造函数。 例如,如果类成员是指针,则需要定义复制构造函数以分配新内存,并从其他指针指向的对象复制值。 编译器生成的复制构造函数只是复制指针,以便新指针仍指向其他指针的内存位置。
复制构造函数可能具有以下签名之一:
Box(Box& other); // Avoid if possible--allows modification of other.
Box(const Box& other);
Box(volatile Box& other);
Box(volatile const Box& other);
// Additional parameters OK if they have default values
Box(Box& other, int i = 42, string label = "Box");
定义复制构造函数时,还应定义复制赋值运算符 (=)。 有关详细信息,请参阅赋值以及复制构造函数和复制赋值运算符。
赋值:将一个对象的值分配给另一个对象时,第一个对象将复制到第二个对象。 因此,此代码将 b 的值复制到 a:
Point a, b;
...
a = b;
● 初始化:在声明新对象、按值传递函数参数或从函数返回值时,将发生初始化。
● 使用返回对类类型的引用的赋值运算符 operator=,并采用 const 引用传递的一个参数,例如 ClassName& operator=(const ClassName& x);。
● 使用复制构造函数。
● 如果不声明复制构造函数,编译器将为你生成成员的复制构造函数。 同样,如果不声明复制赋值运算符,编译器将为你生成成员的复制赋值运算符。 声明复制构造函数不会取消编译器生成的复制赋值运算符,反之亦然。 如果实现其中任一方法,我们建议也实现另一个。 实现这两者时,代码的含义是明确的。
class MyClass
{
public:
MyClass(int value) {
data = new int(value);
}
MyClass(const MyClass& other) {
data = new int(*(other.data));
}
MyClass& operator=(const MyClass& other) {
if (this == &other)
{
return *this;// 检查自赋值
}
delete data;// 释放旧内存
data = new int(*(other.data));
return *this;
}
// 析构函数
~MyClass() {
delete data;
}
private:
int* data;
};
int main()
{
MyClass obj1(10); // 使用构造函数
MyClass obj2 = obj1; // 使用复制构造函数
MyClass obj3(20);
obj3 = obj1; // 使用复制赋值运算符
}
class InnerClass
{
private:
public:
int value;
InnerClass(int value) : value(value){}
InnerClass(const InnerClass& other): value(other.value) {
/*this->value = other.value;*/
}
InnerClass& operator=(const InnerClass& other) {
if (this != &other)
{
value = other.value;
}
return *this;
}
};
class OuterClass
{
public:
InnerClass* inner;
OuterClass(int val) {
inner = new InnerClass(val);
}
OuterClass(const OuterClass& other) {
inner = new InnerClass(*(other.inner));
}
OuterClass& operator=(const OuterClass& other) {
if (this != &other)
{
delete inner;
inner = new InnerClass(*(other.inner));
}
return *this;
}
// 析构函数
~OuterClass() {
delete inner; // 释放动态分配的内存
}
private:
};
int main()
{
// 创建一个 OuterClass 对象
OuterClass obj1(42);
// 使用复制构造函数创建另一个 OuterClass 对象
OuterClass obj2 = obj1;
// 修改 obj1 的 InnerClass 实例的值
obj1.inner->value = 100;
// 打印两个对象的 InnerClass 实例的值
std::cout << "obj1.inner->value: " << obj1.inner->value << std::endl;
std::cout << "obj2.inner->value: " << obj2.inner->value << std::endl;
// 使用复制赋值运算符
OuterClass obj3(28);
obj3 = obj1;
// 打印 obj3 的 InnerClass 实例的值
std::cout << "obj3.inner->value: " << obj3.inner->value << std::endl;
// 检查自赋值情况
obj3 = obj3;
return 0;
}
规则:
- 浅复制(Shallow Copy):编译器生成的默认复制构造函数和赋值运算符执行浅复制,即逐成员复制。
- 深复制(Deep Copy):当类包含指针成员或需要特殊初始化时,必须定义自定义的复制构造函数和赋值运算符以执行深复制。深复制分配新内存并复制指针指向的对象内容。
小结
通过自定义复制构造函数和复制赋值运算符,可以正确管理动态内存,避免潜在的资源泄漏和其他问题。在编写这类代码时,务必考虑析构函数,以便在对象销毁时正确释放资源。
直接构造(编译器优化的结果)
C++ 编译器在某些情况下可以优化构造过程,以避免不必要的复制。这种优化被称为 "返回值优化" (Return Value Optimization, RVO) 或者 "命名返回值优化" (Named Return Value Optimization, NRVO)。这些优化允许编译器在创建对象时直接构造,而不是先构造临时对象再进行复制。
返回值优化 (RVO)
RVO 是指编译器优化返回局部对象的代码,将其直接构造在调用者提供的内存空间中,避免了中间临时对象的创建和析构。
命名返回值优化 (NRVO)
NRVO 是 RVO 的一种特例,适用于有明确名称的返回对象。
#include <iostream>
class MyClass {
public:
int value;
// 默认构造函数
MyClass() : value(0) {
std::cout << "Default constructor called, value: " << value << std::endl;
}
// 参数化构造函数
MyClass(int val) : value(val) {
std::cout << "Parameterized constructor called, value: " << value << std::endl;
}
// 复制构造函数
MyClass(const MyClass& other) : value(other.value) {
std::cout << "Copy constructor called, value: " << value << std::endl;
}
// 析构函数
~MyClass() {
std::cout << "Destructor called, value: " << value << std::endl;
}
};
// 返回局部对象的函数
MyClass createObject(int val) {
MyClass obj(val);
return obj; // 可能触发 NRVO
}
int main() {
std::cout << "Creating obj1" << std::endl;
MyClass obj1 = createObject(42); // 可能触发 RVO
return 0;
}