构造函数是C++面向对象编程的基石,理解它们对于编写健壮、高效的代码至关重要。
什么是构造函数?
构造函数是一个特殊的成员函数,它在创建对象时自动调用 ,用于初始化对象的内存状态。它的名称与类名完全相同,并且没有返回类型(连void
都没有)。
构造函数的核心特点:
- 与类同名
- 无返回类型
- 自动调用(无法手动调用)
- 通常被声明为
public
(除非有特殊设计,如单例模式) - 可以重载(一个类可以有多个构造函数)
现在,让我们深入探讨各类构造函数。
1. 默认构造函数
默认构造函数是不需要任何参数就能调用的构造函数。
两种形式:
- 编译器合成的默认构造函数 :如果你没有为类声明任何构造函数,编译器会自动为你生成一个。注意:它对内置类型(如
int
,double
, 指针)的成员变量不进行初始化(值是未定义的),对类类型的成员变量则调用其自身的默认构造函数。 - 用户定义的默认构造函数:你可以显式定义一个。
示例代码:
cpp
class MyClass {
public:
int data;
std::string name;
// 用户定义的默认构造函数(无参)
MyClass() {
data = 0; // 显式初始化
name = "Unknown";
}
// 或者使用成员初始化列表的更优写法
// MyClass() : data(0), name("Unknown") {}
};
int main() {
MyClass obj1; // 调用默认构造函数
MyClass obj2{}; // C++11 列表初始化,也调用默认构造函数
MyClass* obj3 = new MyClass; // 动态分配,也调用默认构造函数
return 0;
}
关键点:
-
一旦你定义了任何 构造函数,编译器将不再自动生成默认构造函数。
-
如果你需要一个默认构造函数但又已经定义了其他构造函数,可以使用
= default
来显式要求编译器生成。cppclass MyClass { public: MyClass(int x) { ... } // 参数化构造函数 MyClass() = default; // 显式要求编译器生成默认构造函数 };
2. 参数化构造函数
参数化构造函数接受一个或多个参数,用于在创建对象时提供初始值。
示例代码:
cpp
class Date {
private:
int day, month, year;
public:
// 参数化构造函数
Date(int d, int m, int y) : day(d), month(m), year(y) { // 使用成员初始化列表
// 可以在这里添加验证逻辑,例如检查日期是否合法
}
void display() {
std::cout << day << "/" << month << "/" << year << std::endl;
}
};
int main() {
Date today(26, 10, 2023); // 调用参数化构造函数
Date birthday{10, 5, 1990}; // C++11 统一初始化语法,推荐使用
today.display(); // 输出:26/10/2023
// Date errorDate; // 错误!因为我们已经定义了构造函数,编译器不再生成默认构造函数。
return 0;
}
成员初始化列表:
- 注意上面代码中的
: day(d), month(m), year(y)
。这是成员初始化列表。 - 强烈推荐使用 ,因为它直接在成员变量被创建时初始化它们,效率高于在构造函数体内使用赋值操作(
day = d;
)。对于常量成员 (const
)和引用成员 (&
),必须使用初始化列表。
3. 拷贝构造函数
拷贝构造函数用于用一个已存在的对象来初始化一个新对象 。它接受一个对本类类型的常量引用作为参数。
形式: ClassName(const ClassName& other)
何时被调用?
- 用一个对象初始化另一个对象时:
MyClass obj1; MyClass obj2 = obj1;
或MyClass obj2(obj1);
- 对象作为值传递给函数参数时。
- 函数返回值一个对象时(可能会因编译器RVO优化而省略)。
示例代码:
cpp
class StringWrapper {
private:
char* m_data;
size_t m_size;
public:
// 参数化构造函数
StringWrapper(const char* str) {
m_size = strlen(str);
m_data = new char[m_size + 1]; // 动态分配内存
strcpy(m_data, str);
}
// 1. 拷贝构造函数(深拷贝)
StringWrapper(const StringWrapper& other) : m_size(other.m_size) {
std::cout << "拷贝构造函数被调用!" << std::endl;
m_data = new char[m_size + 1];
strcpy(m_data, other.m_data);
}
// 析构函数
~StringWrapper() {
delete[] m_data;
}
void print() {
std::cout << m_data << std::endl;
}
};
int main() {
StringWrapper str1("Hello");
StringWrapper str2 = str1; // 调用拷贝构造函数
str1.print(); // Hello
str2.print(); // Hello
return 0;
}
// 析构时不会出错,因为str1和str2拥有各自独立的内存(深拷贝)。
深浅拷贝问题:
- 如果类中没有动态分配的资源(如指针),使用编译器自动生成的拷贝构造函数(浅拷贝)就足够了,它只是简单地逐位复制。
- 如果类管理着动态资源(如上面的
char* m_data
),必须自定义拷贝构造函数实现深拷贝 。否则,两个对象的指针会指向同一块内存,导致双重释放(double free)的运行时错误。
4. 移动构造函数(C++11 引入)
移动构造函数是C++11为支持移动语义 而引入的,它用于将资源(如动态内存)从一个即将销毁的临时对象"移动"到新对象中,从而避免不必要的深拷贝,提升性能。
形式: ClassName(ClassName&& other) noexcept
(noexcept
很重要,标准库容器在重新分配内存时会使用移动构造函数,它要求该操作不抛异常)。
示例代码:
cpp
class StringWrapper {
// ... 其他成员同上 ...
// 2. 移动构造函数
StringWrapper(StringWrapper&& other) noexcept : m_data(nullptr), m_size(0) {
std::cout << "移动构造函数被调用!" << std::endl;
// "窃取" 临时对象的资源
m_data = other.m_data;
m_size = other.m_size;
// 将临时对象置于有效但可析构的状态
other.m_data = nullptr;
other.m_size = 0;
}
};
StringWrapper createString() {
return StringWrapper("Temporary String"); // 这是一个右值
}
int main() {
StringWrapper str3 = createString(); // 这里会优先调用移动构造函数(如果存在)
str3.print(); // Temporary String
// 如果没有移动构造函数,则会调用拷贝构造函数,性能较低。
return 0;
}
关键点:
- 参数是 右值引用 (
ClassName&&
)。 - 它"偷走"源对象的资源,并将源对象置于一个有效但可安全析构 的状态(通常将其指针设为
nullptr
)。 - 对于管理昂贵资源的类,实现移动构造函数是性能优化的关键。
5. 委托构造函数(C++11 引入)
委托构造函数允许一个构造函数调用同一个类的另一个构造函数,以避免代码重复。
示例代码:
cpp
class Employee {
private:
int m_id;
std::string m_name;
std::string m_department;
public:
// 目标构造函数
Employee(int id, const std::string& name, const std::string& dept)
: m_id(id), m_name(name), m_department(dept) {
std::cout << "三参数构造函数" << std::endl;
}
// 委托构造函数:委托给上面的三参数构造函数
Employee(int id, const std::string& name) : Employee(id, name, "Unassigned") {
std::cout << "委托构造函数" << std::endl;
}
// 默认构造函数也可以委托
Employee() : Employee(0, "Unknown", "Unassigned") {}
};
6. 转换构造函数(单参数构造函数)
任何只接受一个参数的构造函数(除了拷贝/移动构造函数),都定义了一种从参数类型到该类类型的隐式转换规则。
示例代码:
cpp
class MyNumber {
private:
int value;
public:
// 转换构造函数:从 int 到 MyNumber
MyNumber(int v) : value(v) {}
void display() {
std::cout << "Value: " << value << std::endl;
}
};
void printNumber(MyNumber num) {
num.display();
}
int main() {
MyNumber num = 42; // 隐式转换:int 42 被转换为 MyNumber 对象
printNumber(100); // 隐式转换:int 100 被转换为 MyNumber 对象
return 0;
}
防止隐式转换:
-
隐式转换有时会带来意想不到的错误。可以使用
explicit
关键字来禁止它。cppclass MyNumber { public: explicit MyNumber(int v) : value(v) {} // 显式构造函数 }; // MyNumber num = 42; // 错误!转换是显式的,无法进行隐式转换。 MyNumber num(42); // 正确!直接初始化。 MyNumber num2 = MyNumber(42); // 正确!显式转换。 printNumber(MyNumber(100)); // 正确!必须显式转换。 // printNumber(100); // 错误!
总结
构造函数类型 | 语法示例 | 主要用途 |
---|---|---|
默认构造函数 | MyClass(); |
创建对象时不提供初始化值 |
参数化构造函数 | MyClass(int a, string s); |
创建对象时提供初始化值 |
拷贝构造函数 | MyClass(const MyClass& other); |
用一个已存在对象初始化新对象(深拷贝) |
移动构造函数 | MyClass(MyClass&& other) noexcept; |
从临时对象"转移"资源,提升性能 |
委托构造函数 | MyClass() : MyClass(0, "") {} |
在一个构造函数中调用另一个,避免重复代码 |
转换构造函数 | MyClass(int x); |
定义从参数类型到类类型的隐式转换(可用explicit 禁用) |
理解并正确使用这些构造函数,是编写出正确、高效、易于维护的C++代码的关键。对于资源管理类(如智能指针、容器),三/五法则(需要自定义拷贝构造/赋值、移动构造/赋值、析构函数中的一个,通常需要自定义全部)是重要的指导原则。