构造函数概述
基本概念
- 定义:构造函数是与类同名的特殊成员函数
- 返回值:没有返回值(包括void类型)
- 调用时机:在创建对象时自动调用
- 主要作用:对象初始化工作,如数据成员赋值、资源分配、设置初始状态等
核心特性
- 自动调用:创建对象时由编译器自动调用,不能手动显式调用
- 重载支持:可以定义多个构造函数,支持不同的初始化方式
- 无返回类型:连void都不需要写
构造函数的底层机制
编译器隐式调用:构造函数在对象创建时由编译器自动插入调用代码,程序员不能直接调用。
对象内存布局:在对象内存分配完成后(栈或堆),构造函数被调用,负责初始化对象的数据成员。
初始化与赋值的区别:
- 初始化:在对象创建时直接赋予初始值(如使用初始化列表)。
- 赋值:先创建对象(可能未初始化),再赋予值。
- 初始化通常效率更高,尤其是对于类类型成员。
构造函数分类
默认构造函数
特点:
- 不带任何参数
- 如果没有定义任何构造函数,编译器会自动生成一个空的默认构造函数
- 如果定义了其他构造函数,编译器不再自动生成默认构造函数
示例:
cpp
class Point {
public:
// 默认构造函数
Point() {
x = 0;
y = 0;
}
// 成员变量
int x, y;
};
// 使用示例
Point p1; // 调用默认构造函数
Point p2{}; // C++11 统一初始化
编译器生成的默认构造函数:
- 如果类中没有定义任何构造函数,编译器会隐式生成一个合成默认构造函数。
- 合成默认构造函数会:
- 调用类类型成员的默认构造函数。
- 对于内置类型成员,不会进行初始化(值是未定义的)。
= default 与 = delete:
= default显式要求编译器生成默认构造函数。= delete禁止编译器生成默认构造函数。
带参数的构造函数
特点:
- 接受参数用于初始化对象属性
- 支持多种初始化方式
示例:
cpp
class Point {
public:
// 带参数的构造函数
Point(int xVal, int yVal) {
x = xVal;
y = yVal;
}
// 重载的构造函数
Point(int val) {
x = val;
y = val;
}
int x, y;
};
// 使用示例
Point p1(10, 20); // 调用两个参数的构造函数
Point p2(5); // 调用一个参数的构造函数
函数重载解析:
- 编译器根据传递的参数数量和类型选择最匹配的构造函数。
- 如果存在多个匹配,可能产生二义性错误。
复制构造函数
作用:
- 用已存在的对象创建新对象
- 实现对象的深拷贝或浅拷贝
- 在以下情况自动调用:
- 对象作为函数参数传递(值传递)
- 对象作为函数返回值(值返回)
- 用已有对象初始化新对象
示例:
cpp
class Person {
private:
int age;
int height;
int weight;
public:
// 带参数的构造函数
Person(int a, int h, int w) : age(a), height(h), weight(w) {}
// 复制构造函数
Person(const Person& other) {
age = other.age;
height = other.height;
weight = other.weight;
}
};
// 使用示例
int main() {
Person p1(20, 30, 100); // 调用带参构造函数
Person p2(p1); // 调用复制构造函数
Person p3 = p1; // 调用复制构造函数
return 0;
}
合成复制构造函数:
- 如果未定义复制构造函数,编译器会生成一个。
- 合成复制构造函数执行浅拷贝(按成员复制)。
深拷贝与浅拷贝:
- 浅拷贝:只复制指针值,不复制指向的资源。
- 深拷贝:复制指针指向的整个资源。
- 当类包含动态分配的资源时,必须自定义复制构造函数实现深拷贝。
移动构造函数(C++11)
背景:
- 为支持移动语义,C++11引入了移动构造函数。
作用:
- 用于从临时对象(右值)"窃取"资源,避免不必要的深拷贝。
示例:
cpp
class MyVector {
int* data;
size_t size;
public:
// 移动构造函数
MyVector(MyVector&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
};
构造函数的进阶特性
初始化列表
优势:
- 效率更高(直接初始化而非先定义后赋值)
- 必须用于常量成员和引用成员的初始化
- 初始化顺序与声明顺序一致
示例
cpp
class Student {
private:
const int id; // 常量成员
string& nameRef; // 引用成员
int age;
public:
// 使用初始化列表的构造函数
Student(int studentId, string& name, int studentAge)
: id(studentId), nameRef(name), age(studentAge) {
// 构造函数体
}
};
成员初始化顺序:
- 初始化顺序由类中成员的声明顺序决定,与初始化列表中的顺序无关。
- 不遵循声明顺序可能导致依赖错误。
效率优势:
- 初始化列表直接在成员变量创建时赋值,避免先调用默认构造函数再赋值的开销。
委托构造函数
作用:一个构造函数可以调用同一个类的另一个构造函数
示例
cpp
class Rectangle {
private:
int width, height;
public:
// 委托构造函数
Rectangle() : Rectangle(1, 1) {} // 委托给两个参数的构造函数
Rectangle(int size) : Rectangle(size, size) {} // 委托给两个参数的构造函数
Rectangle(int w, int h) : width(w), height(h) {} // 实际工作的构造函数
};
委托链:
- 委托构造函数可以形成一个链式调用,但不能形成环。
- 最终必须有一个构造函数完成所有成员的初始化。
explicit 关键字
作用:防止构造函数的隐式转换
cpp
class MyClass {
private:
int value;
public:
explicit MyClass(int v) : value(v) {} // 禁止隐式转换
};
// 使用示例
MyClass obj1(10); // 正确:显式调用
// MyClass obj2 = 10; // 错误:explicit 禁止隐式转换
隐式转换的场景:
- 单参数构造函数允许编译器进行隐式类型转换。
explicit阻止这种转换,要求必须显式调用构造函数。
特殊注意事项
构造函数与内存管理
cpp
class DynamicArray {
private:
int* data;
int size;
public:
// 构造函数 - 分配资源
DynamicArray(int sz) : size(sz) {
data = new int[size]; // 动态内存分配
}
// 析构函数 - 释放资源
~DynamicArray() {
delete[] data; // 释放动态内存
}
};
RAII原则:
- 构造函数用于获取资源,析构函数用于释放资源。
- 确保资源在对象生命周期内有效,避免资源泄漏。
默认构造函数的必要性
cpp
class Container {
private:
vector<int> items;
public:
// 如果注释掉默认构造函数,下面的代码会编译错误
Container() = default; // 显式要求编译器生成默认构造函数
// 其他构造函数
Container(const vector<int>& initialItems) : items(initialItems) {}
};
// 使用示例
Container containers[10]; // 需要默认构造函数
STL容器与默认构造函数:
- 许多STL容器(如
vector)要求元素类型必须有默认构造函数,以便在扩容时构造新元素。
聚合类初始化:
- 如果类满足聚合类的条件,可以使用列表初始化而不需要默认构造函数。