C++ 构造函数
构造函数是 C++ 类中的特殊成员函数,其核心作用是在创建对象时初始化对象的状态(如成员变量赋值、分配资源等)。它的名字与类名完全相同,无返回值(包括 void),且在对象创建时由编译器自动调用,无需手动触发。
1、构造函数的核心特性
- 名称与类名一致:必须与所属类的名字完全相同(大小写敏感),例如 class Person 的构造函数名为 Person。
- 无返回值:不需要声明返回类型(包括 void),声明时直接写函数名和参数列表。
- 自动调用:在使用 new 创建对象或定义栈上对象时,编译器会自动调用对应的构造函数,无法手动调用。
- 可重载:一个类可以有多个构造函数,只要它们的参数列表(个数、类型、顺序)不同,满足函数重载规则。
- 默认存在性:若用户未显式定义任何构造函数,编译器会自动生成一个默认构造函数(无参、函数体为空);若用户显式定义了构造函数,编译器则不再生成默认构造函数。
class MyClass {
private:
int value;
std::string name;
public:
// 1. 构造函数声明
MyClass(int v, const std::string& n);
};
// 2. 构造函数定义
MyClass::MyClass(int v, const std::string& n) {
value = v; // 这是赋值(Assignment),不是初始化(Initialization)!
name = n; // 同上
}
int main() {
MyClass obj(42, "Hello"); // 构造函数被自动调用
return 0;
}
2、构造函数分类
2.1默认构造函数
功能:可以不提供任何实参调用的构造函数。
- 生成:如果你没有为类定义任何构造函数,编译器会自动生成一个默认构造函数(称为合成默认构造函数)。它对于内置类型(如
int,指针)不做初始化(值是未定义的),对于类类型成员,调用其默认构造函数。 - 如果你定义了任何其他构造函数,编译器就不会再自动生成默认构造函数。此时如果你还需要默认构造,必须手动定义。
- 调用方式:
MyClass obj;或MyClass obj = MyClass();或MyClass* ptr = new MyClass;
#include <iostream>
using namespace std;
class Person {
private:
string name;
int age;
public:
// 1. 编译器自动生成的默认构造函数(用户未定义任何构造时)
// (隐式存在,函数体为空,成员变量按默认规则初始化:基本类型为随机值,类类型调用其默认构造)
// 2. 用户显式定义的无参默认构造函数
Person() {
name = "Unknown";
age = 0;
cout << "无参构造函数调用" << endl;
}
// 3. 全缺省参数的默认构造函数(参数均有默认值)
// 注意:一个类只能有一个默认构造函数,若同时定义2和3会编译错误
/*
Person(string n = "Unknown", int a = 0) {
name = n;
age = a;
cout << "全缺省构造函数调用" << endl;
}
*/
void showInfo() {
cout << "Name: " << name << ", Age: " << age << endl;
}
};
int main() {
Person p1; // 调用默认构造函数(无参或全缺省)
p1.showInfo(); // 输出:Name: Unknown, Age: 0
Person* p2 = new Person; // 动态创建对象,同样调用默认构造
p2->showInfo();
delete p2;
return 0;
}
使用 = default (C++11):
可以显式要求编译器生成合成默认构造函数。
class MyClass {
public:
MyClass(int v) : value(v) {} // 提供了带参构造函数
MyClass() = default; // 显式要求编译器生成默认构造函数
private:
int value;
};
2.3带参构造函数
最常见的构造函数,接受参数来初始化成员。
class Person {
private:
string name;
int age;
public:
// 带参构造函数(2个参数)
Person(string n, int a) {
name = n;
age = a;
cout << "带参构造函数调用" << endl;
}
// 重载带参构造函数(1个参数,仅初始化name)
Person(string n) {
name = n;
age = 18; // 默认年龄18
cout << "单参构造函数调用" << endl;
}
void showInfo() {
cout << "Name: " << name << ", Age: " << age << endl;
}
};
int main() {
Person p1("Alice", 25); // 调用2参构造
p1.showInfo(); // 输出:Name: Alice, Age: 25
Person p2("Bob"); // 调用1参构造
p2.showInfo(); // 输出:Name: Bob, Age: 18
Person* p3 = new Person("Charlie", 30); // 动态对象调用2参构造
p3->showInfo();
delete p3;
return 0;
}
2.4 拷贝构造函数
用于用一个已存在的同类型对象来初始化一个新对象。
- 调用时机:
- 用一个对象初始化另一个对象:
MyClass obj1; MyClass obj2 = obj1; - 函数按值传递参数时。
- 函数按值返回对象时(可能会被编译器优化掉,即返回值优化 RVO/NRVO)。
- 用一个对象初始化另一个对象:
- 生成:如果你没有定义,编译器会生成一个合成拷贝构造函数,它会逐个成员地进行浅拷贝(对指针成员,只拷贝地址,不拷贝指向的内容)。
class Person {
private:
string name;
int age;
public:
// 带参构造函数
Person(string n, int a) : name(n), age(a) {}
// 1. 用户显式定义的拷贝构造函数(参数为 const Person&)
Person(const Person& other) {
this->name = other.name; // 拷贝name
this->age = other.age; // 拷贝age
cout << "自定义拷贝构造函数调用" << endl;
}
// 2. 编译器自动生成的默认拷贝构造函数(隐式存在,无函数体,执行浅拷贝)
// Person(const Person& other) = default; // 显式声明使用默认拷贝构造
void showInfo() {
cout << "Name: " << name << ", Age: " << age << endl;
}
};
int main() {
Person p1("Alice", 25); // 调用带参构造
Person p2 = p1; // 调用拷贝构造(用p1初始化p2)
Person p3(p1); // 等价于p2,同样调用拷贝构造
Person p4 = Person(p1); // 等价,调用拷贝构造
p2.showInfo(); // 输出:Name: Alice, Age: 25(与p1一致)
p3.showInfo(); // 输出:Name: Alice, Age: 25
return 0;
}
4.1 浅拷贝 vs 深拷贝
- 浅拷贝:仅复制成员变量的“值”,若成员是指针,复制的是指针地址(而非指针指向的内容),导致两个对象共享同一块内存。
- 深拷贝:不仅复制成员变量的值,若成员是指针,会重新分配一块内存,并复制指针指向的内容,确保两个对象的资源独立。
示例:深拷贝的必要性(解决动态内存问题)
class String {
private:
char* str; // 动态内存成员(指针)
public:
// 带参构造函数(分配内存)
String(const char* s) {
int len = strlen(s) + 1;
str = new char[len]; // 分配内存存储字符串
strcpy(str, s);
cout << "带参构造:" << str << endl;
}
// 【错误】默认浅拷贝构造(编译器自动生成)
// 问题:两个对象的str指向同一块内存,析构时会双重释放
// String(const String& other) = default;
// 【正确】自定义深拷贝构造(重新分配内存)
String(const String& other) {
int len = strlen(other.str) + 1;
str = new char[len]; // 新对象重新分配内存
strcpy(str, other.str); // 复制字符串内容
cout << "深拷贝构造:" << str << endl;
}
// 析构函数(释放内存)
~String() {
cout << "析构:" << str << "(地址:" << (void*)str << ")" << endl;
delete[] str; // 释放动态内存
}
const char* getStr() const { return str; }
};
int main() {
String s1("Hello");
String s2 = s1; // 调用深拷贝构造
cout << "s1: " << s1.getStr() << "(地址:" << (void*)s1.getStr() << ")" << endl;
cout << "s2: " << s2.getStr() << "(地址:" << (void*)s2.getStr() << ")" << endl;
// 输出可见s1和s2的str地址不同,资源独立,析构时无双重释放
return 0;
}
输出结果(地址不同,资源独立):
带参构造:Hello
深拷贝构造:Hello
s1: Hello(地址:0x55f8d7a7a2a0)
s2: Hello(地址:0x55f8d7a7a2c0)
析构:Hello(地址:0x55f8d7a7a2c0)
析构:Hello(地址:0x55f8d7a7a2a0)
2.5 移动构造函数(C++11 新增)
参数为“当前类的右值引用(T&&)”的构造函数,用于“窃取”右值对象的资源(而非复制),避免不必要的深拷贝,提升效率。
核心概念:
- 右值(Rvalue):临时对象(如函数返回值、字面量),其生命周期短暂,使用后无需保留状态;
- 移动语义:将右值对象的资源(如指针、内存)直接“转移”给新对象,原右值对象的资源指针置空(避免析构时重复释放)。
示例:移动构造函数的使用
class String {
private:
char* str;
public:
// 带参构造
String(const char* s) : str(new char[strlen(s) + 1]) {
strcpy(str, s);
cout << "带参构造:" << str << endl;
}
// 拷贝构造(深拷贝,用于左值)
String(const String& other) : str(new char[strlen(other.str) + 1]) {
strcpy(str, other.str);
cout << "深拷贝构造:" << str << endl;
}
// 移动构造(C++11,参数为右值引用,用于右值)
String(String&& other) : str(other.str) { // 直接窃取other的str指针
other.str = nullptr; // 原右值对象的str置空,避免析构释放
cout << "移动构造:" << str << endl;
}
// 析构函数
~String() {
if (str) {
cout << "析构:" << str << endl;
delete[] str;
} else {
cout << "析构:空对象" << endl;
}
}
};
// 函数返回临时对象(右值)
String createString() {
return String("Hello Move"); // 返回临时对象,触发移动构造
}
int main() {
String s1 = createString(); // 调用移动构造(窃取临时对象的资源)
String s2 = s1; // 调用拷贝构造(s1是左值,需深拷贝)
return 0;
}
输出结果(移动构造无内存分配,效率更高):
带参构造:Hello Move
移动构造:Hello Move
深拷贝构造:Hello Move
析构:Hello Move
析构:Hello Move
析构:空对象
2.6 委托构造函数(C++ 11)
允许一个构造函数调用同一类中的另一个构造函数,减少代码冗余(避免重复初始化逻辑)。
class Person {
private:
string name;
int age;
string addr;
public:
// 主构造函数(包含核心初始化逻辑)
Person(string n, int a, string ad) : name(n), age(a), addr(ad) {
cout << "主构造函数调用" << endl;
}
// 委托构造:调用主构造函数(传递默认addr)
Person(string n, int a) : Person(n, a, "Unknown Address") {
cout << "委托构造(2参)调用" << endl;
}
// 委托构造:调用2参构造(传递默认age)
Person(string n) : Person(n, 18) {
cout << "委托构造(1参)调用" << endl;
}
};
int main() {
Person p("Alice");
// 调用顺序:1参委托构造 → 2参委托构造 → 主构造函数
return 0;
}
2.7 继承构造函数(C++ 11)
子类通过 using 父类::父类; 声明继承父类的构造函数,避免子类重复定义与父类参数一致的构造函数。
class Parent {
public:
Parent(int a) { cout << "Parent 带参构造:" << a << endl; }
Parent(string s) { cout << "Parent 带参构造:" << s << endl; }
};
class Child : public Parent {
public:
// 继承父类的所有构造函数
using Parent::Parent;
// 子类自定义构造(若有)
Child() : Parent(0) { cout << "Child 默认构造" << endl; }
};
int main() {
Child c1(10); // 调用继承的 Parent(int) 构造
Child c2("Hi"); // 调用继承的 Parent(string) 构造
return 0;
}
3、成员初始化列表
构造函数参数列表后、函数体前的 : 开头的部分,用于直接初始化类的成员变量(而非在函数体中赋值)。
- 性能:对于类类型成员(如
std::string,std::vector),使用初始化列表是直接初始化。如果在构造函数体内使用=赋值,是先执行默认初始化,然后再进行赋值操作,效率更低。 - 必须性:以下成员必须在初始化列表中初始化:
- const 成员
- 引用成员
- 没有默认构造函数的类类型成员
class Example {
public:
// 使用初始化列表 (高效且正确)
Example(int a, const std::string& str, const MyClass& obj)
: num(a), // 初始化内置类型
name(str), // 直接调用string的拷贝构造函数
refToNum(num), // 引用必须在初始化列表中绑定
constMember(42), // const成员必须在初始化列表中初始化
noDefaultObj(obj) // 没有默认构造函数的成员必须在这里初始化
{
// 函数体内只能进行赋值,不能进行初始化
// name = str; // 这效率较低,因为name先被默认构造为空字符串,然后再被赋值。
}
private:
int num;
std::string name;
int& refToNum; // 引用成员
const int constMember; // const 成员
MyClass noDefaultObj; // 假设MyClass没有默认构造函数
};
初始化顺序:成员的初始化顺序只取决于它们在类中声明的顺序,与在初始化列表中出现的顺序无关。最好让初始化列表的顺序与声明顺序保持一致,以避免混淆。
4、 构造函数初始化流程
当一个对象被创建时,其初始化顺序是严格规定的:
- 虚拟基类(按继承树深度优先,从左到右)。
- 直接基类(按声明的继承顺序,从左到右)。
class Base1 {
public:
Base1() { cout << "Base1构造\n"; }
};
class Base2 {
public:
Base2() { cout << "Base2构造\n"; }
};
class Derived : public Base1, public Base2 {
public:
Derived() { cout << "Derived构造\n"; }
};
// 创建Derived对象时输出:
// Base1构造
// Base2构造
// Derived构造
- 类类型的成员对象(按在类中的声明顺序)。
- 执行构造函数体。
5、常见问题
-
编译器何时生成默认构造函数?
仅当用户未显式定义任何构造函数时,编译器才会生成默认构造函数(无参、空函数体)。若定义了带参、拷贝等构造函数,默认构造函数会被删除。 -
拷贝构造函数的参数为什么必须是
const引用?
- 若参数是值传递:调用拷贝构造时需要先复制实参,而复制实参又会调用拷贝构造,导致无限递归;
- 加
const:确保拷贝过程中不修改原对象(逻辑安全),同时允许接收常量对象(如const Person p; Person p2 = p;)。
-
移动构造函数的参数为什么是右值引用(
T&&)而非const T&&?
因为移动构造需要修改原右值对象(如将其指针置空),const会禁止修改,导致无法“窃取”资源。 -
什么是“默认成员初始化”(C++11+)?与构造函数的优先级?
可以在成员变量声明时直接赋值(如int age = 18;),称为默认成员初始化。优先级:
- 初始化列表 > 默认成员初始化 > 编译器默认初始化。
class Person {
private:
string name = "Unknown"; // 默认成员初始化
int age = 18;
public:
Person() : age(20) {} // 初始化列表优先级更高,age最终为20
};
- 什么是“构造函数的explicit关键字”?
explicit用于禁止构造函数的隐式类型转换,仅允许显式调用,避免意外转换。
class Person {
public:
explicit Person(int age) : m_age(age) {} // explicit禁止隐式转换
private:
int m_age;
};
int main() {
Person p1(20); // 正确:显式调用
// Person p2 = 20; // 错误:explicit禁止隐式转换(int→Person)
return 0;
}
- 拷贝构造函数和移动构造函数的区别是什么?
拷贝构造函数通过拷贝另一个对象的内容来创建新对象,是“复制”语义,资源可能会被复制一份。
移动构造函数通过“窃取”另一个右值对象的资源来创建新对象,是“移动”语义,源对象被置于有效但可析构的状态(如指针置为nullptr),性能更高,因为它避免了不必要的深拷贝。
浙公网安备 33010602011771号