承接上文:上一节学习了类的成员变量、成员函数与工程编码规范,我们发现对象创建后成员变量默认是随机垃圾值,需要手动调用setter赋值。本节学习构造函数,对象创建时自动执行的特殊成员函数;再深入初始化列表,解决普通函数体内无法初始化const成员、引用成员的硬性语法问题,掌握对象初始化底层逻辑。
学习说明
构造函数是面向对象里对象生命周期的起点。很多新手分不清:在构造函数函数体内赋值,和初始化列表初始化有本质区别。 函数体内是赋值操作;初始化列表才是真正的初始化。const成员、引用成员必须在初始化列表完成初始化,不能在函数体内赋值,这是本节核心难点。
本节学习目标
- 掌握构造函数基础语法、调用时机、构造函数特征
- 理解默认构造函数,以及自定义构造后默认构造消失的规则
- 学会初始化列表写法,分清初始化 vs 赋值的本质差异
- 掌握const成员、引用成员为什么只能用初始化列表初始化
- 掌握初始化列表的执行顺序(和书写顺序无关,按成员声明顺序初始化)
一、构造函数基础
核心特点
- 函数名和类名完全相同,没有返回值,连void都不能写
- 对象创建的一瞬间自动调用,无法手动调用
- 可以重载(多个构造函数,参数列表不同)
- 如果自己不写任何构造,编译器自动生成默认无参构造;只要手写任意构造函数,编译器不再提供默认构造
#include <iostream> #include <string> using namespace std; class Person { private: string m_name; int m_age; public: // 无参构造函数 Person() { cout << "无参构造函数执行" << endl; m_name = "未知"; m_age = 0; } // 有参构造函数,构造重载 Person(string name, int age) { cout << "有参构造函数执行" << endl; m_name = name; m_age = age; } void showInfo() { cout << m_name << " " << m_age << endl; } }; int main() { Person p1; // 自动调用无参构造 Person p2("李四", 25); // 自动调用有参构造 p1.showInfo(); p2.showInfo(); return 0; }上面写法是构造函数体内赋值,不是初始化。普通成员可以这么写,但const、引用成员不支持这种写法。
补充验证示例:手写有参构造后默认构造消失
#include <iostream> #include <string> using namespace std; class Person { public: Person(string name) { cout << "有参构造执行" << endl; } }; int main() { Person p1("张三"); // 正确 // Person p2; // 编译报错!手写构造后,编译器不再生成默认无参构造 return 0; }C++11 default显式生成默认构造进阶示例
#include <iostream> #include <string> using namespace std; class Person { private: string m_name; public: Person(string name) { m_name = name; } Person() = default; // 显式告诉编译器,生成默认无参构造 }; int main() { Person p1("张三"); Person p2; // 正常编译运行 return 0; }二、初始化列表语法
初始化列表写在构造函数参数括号后面、函数体{}之前,用冒号开头。
#include <iostream> #include <string> using namespace std; class Person { private: string m_name; int m_age; public: // 初始化列表写法 Person(string name, int age) : m_name(name), m_age(age) { cout << "初始化列表执行完成,进入构造函数体" << endl; } void showInfo() { cout << m_name << " " << m_age << endl; } }; int main() { Person p("王五", 30); p.showInfo(); return 0; }核心规则:
- 初始化列表先执行,然后才进入构造函数大括号内部
- 初始化 = 创建变量同时给初始值;函数体内 = 变量已经创建完成,再修改值(赋值)
三、重点:const成员、引用成员只能使用初始化列表
const变量、引用变量,定义的时候就必须初始化,一旦创建完成,后续不能赋值。 放到构造函数体内时,成员已经完成创建,此时再赋值直接编译报错。只能在初始化列表阶段完成初始化。
#include <iostream> using namespace std; class Test { private: const int m_a; // const成员,必须初始化 int& m_ref; // 引用成员,必须初始化 public: // 只能在初始化列表初始化 const 和引用成员 Test(int val) : m_a(100), m_ref(val) { // m_a = 200; // 错误!const不能赋值 // m_ref = val; // 错误!引用已经绑定,不能在这里初始化 } }; int main() { int val = 42; Test t(val); // m_a = 100, m_ref绑定到变量val return 0; }四、初始化列表容易踩坑:初始化顺序
⚠️初始化顺序由成员在类中声明的先后顺序决定,和初始化列表书写顺序无关!这是高频隐蔽bug。
#include <iostream> using namespace std; class Demo { private: int m_x; int m_y; public: Demo(int val) : m_y(val), m_x(m_y) { // 先初始化m_x,再初始化m_y! // m_x(m_y) 此时m_y还未初始化,m_x得到随机垃圾值 cout << "m_x=" << m_x << " m_y=" << m_y << endl; } }; int main() { Demo d(10); // 输出:m_x=随机值 m_y=10 // 因为先初始化m_x(m_y),此时m_y还未初始化 return 0; }坑点说明:列表写的是先m_y再m_x,但类内声明顺序是m_x在前,程序优先初始化m_x,此时m_y还没赋值,导致m_x为随机值。
五、工程编码推荐
✅ 推荐:优先使用初始化列表初始化所有成员,而不是构造体内赋值
- 语法统一,同时支持普通成员、const成员、引用成员
- 对于自定义类型成员,减少一次临时对象拷贝,性能更好
- 代码可读性强,一眼看清对象创建时各个成员初始值
❌ 不推荐:全部放到构造函数体内赋值,仅简单Demo可以使用
性能对比示例,用于思考题:
#include <iostream> #include <string> using namespace std; class Student { private: string m_name; public: // 写法A:函数体内赋值 Student(string name) { m_name = name; // 先默认构造m_name,再赋值,两次操作 } // 写法B:初始化列表 Student(string name) : m_name(name) { } // 直接初始化,一次操作 };六、高频坑点总结
- 手写任意构造函数后,编译器不再提供默认无参构造。再写
Person p;会编译报错。 - 区分:初始化列表是初始化,函数体内代码是赋值,二者本质不一样。
- const成员、引用成员只能在初始化列表初始化,构造函数体内赋值直接编译失败。
- 初始化列表执行顺序 ≠ 书写顺序,按类内成员声明顺序初始化,极易写出隐藏bug。
七、本节思考题
- 写了一个有参构造函数,再定义
Person p;为什么编译报错? - const成员变量能不能在构造函数
{}里面赋值?为什么? - 初始化列表里面成员的初始化顺序,是按列表书写顺序还是类内声明顺序?举个例子。
- 对比上面Student类的写法A和写法B,二者有什么区别?为什么工程上推荐写法B?
下期预告
下一节:析构函数实战:内存释放场景与手写规范。