深入理解C++构造函数与初始化列表
目录
一、构造函数初始化方式对比
二、初始化列表
初始化列表的关键核心特性
三、必须使用初始化列表的情况
1、引用成员变量
2、const成员变量
3、无默认构造函数的类成员
总结:
四、C++11成员变量缺省值
五、初始化列表的最佳实践
1、优先使用初始化列表
效率对比示例:
2、初始化顺序规则
六、C++成员变量初始化机制详解
初始化列表的核心逻辑
1、显式初始化优先
2、未显式初始化的情况
2.1 类声明中有缺省值
2.2 类声明中无缺省值
内置类型
自定义类型
特殊类型(必须初始化)
实际应用建议
七、代码示例分析
示例1:必须使用初始化列表的情况
示例2:缺省值使用
示例3:初始化顺序问题(重要!!!)
解析:
八、总结
一、构造函数初始化方式对比
在C++中,构造函数负责在创建对象时初始化对象的成员变量。常见的一种初始化方式是在构造函数体内进行赋值:
class Date {
public:// 构造函数Date(int year = 0, int month = 1, int day = 1) {_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
虽然这种方式能够为成员变量赋予初始值,但从严格意义上讲,这属于"赋值"而非"初始化"。
在C++中,构造函数初始化成员变量主要有两种方式:
-
函数体内赋值(非真正初始化),初始化只能进行一次,而构造函数体内可以进行多次赋值
-
初始化列表(真正的成员初始化)
class Date {
public:Date(int year = 0, int month = 1, int day = 1) {_year = year; // 第一次赋值_year = 2022; // 第二次赋值 - 合法但非初始化_month = month;_day = day;}
private:int _year;int _month;int _day;
};
二、初始化列表
C++提供了更高效的初始化方式——成员初始化列表:
class Date {
public:// 使用初始化列表的构造函数Date(int year = 0, int month = 1, int day = 1): _year(year) // 初始化_year, _month(month) // 初始化_month, _day(day) // 初始化_day{}
private:int _year;int _month;int _day;
};
初始化列表的关键核心特性
-
语法形式:以冒号开始,后跟逗号分隔的成员初始化列表,每个成员后跟括号中的初始值或表达式。
-
执行时机:在对象构造时最先执行,早于构造函数体
-
效率优势:直接初始化而非先默认初始化再赋值
-
唯一性:每个成员变量在初始化列表中只能出现一次(因为初始化只能进行一次)
-
本质:初始化列表是成员变量定义和初始化的地方
-
强制性:以下成员必须使用初始化列表:
引用成员变量、const成员变量、没有默认构造函数的类类型成员
三、必须使用初始化列表的情况
以下类型的成员变量必须在初始化列表中初始化:
1、引用成员变量
int a = 10;
int& b = a;// 创建时就初始化
引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化。
class RefExample {
public:RefExample(int& ref) : _ref(ref) {} // 必须使用初始化列表
private:int& _ref; // 引用必须在定义时初始化
};
2、const成员变量
const int a = 10;//correct 创建时就初始化
const int b;//error 创建时未初始化
被const修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化。
class ConstExample {
public:ConstExample(int val) : _constVal(val) {} // 必须使用初始化列表
private:const int _constVal; // const成员必须在定义时初始化
};
3、无默认构造函数的类成员
若一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,所以实例化没有默认构造函数的类对象时必须使用初始化列表对其进行初始化。
class NoDefault {
public:NoDefault(int val) : _val(val) {} // 没有默认构造函数
private:int _val;
};class Container {
public:Container() : _member(42) {} // 必须使用初始化列表
private:NoDefault _member;
};
关于默认构造函数,需要明确以下几点特征:(回顾!!!)
- 无需传递参数即可调用的构造函数
- 包括以下三种情况:
- 编译器自动生成的隐式构造函数
- 显式定义的无参构造函数
- 参数全部带有默认值的构造函数
总结:
在定义时就必须进行初始化的变量类型,就必须放在初始化列表进行初始化。
四、C++11成员变量缺省值
C++11允许在成员变量声明时提供缺省值:
-
主要用于未在初始化列表显式初始化的成员
-
对于内置类型,未显式初始化且无缺省值时行为未定义
-
对于自定义类型,会尝试调用默认构造函数
class Date {
private:int year = 2023; // 缺省值Time t = Time(12); // 自定义类型缺省值
};
五、初始化列表的最佳实践
1、优先使用初始化列表
对于所有成员变量,优先使用初始化列表:
所有成员都会经过初始化列表、显式初始化可避免不确定行为、对自定义类型效率更高
-
内置类型:避免冗余的默认初始化。使用初始化列表和在构造函数体内进行初始化实际上是没有差别的,其差别就类似于如下代码:
// 使用初始化列表 int a = 10 // 在构造函数体内初始化(不使用初始化列表) int a; a = 10;
-
自定义类型:避免不必要的临时对象创建和赋值操作,使用初始化列表可以提高代码的效率
效率对比示例:
class Time {
public:Time(int hour = 0) : _hour(hour) {} // 高效初始化
private:int _hour;
};class Test {
public:// 高效方式:使用初始化列表Test(int hour) : _t(hour) {} // 只调用一次Time构造函数// 低效方式:构造函数体内赋值Test(int hour) { _t = Time(hour); // 先默认构造,再赋值}
private:Time _t;
};
在实例化Test类对象时,若使用初始化列表,仅需调用一次Time类的构造函数即可完成初始化。若不使用初始化列表(低效方式),则需采用以下实现方式:
class Time {
public:Time(int hour = 0) {_hour = hour;}
private:int _hour;
};class Test {
public:// 在构造函数体内初始化(不使用初始化列表)Test(int hour) {Time t(hour); // 调用Time类的构造函数_t = t; // 调用Time类的赋值运算符重载函数}
private:Time _t;
};
这种情况下实例化Test对象时:
- 初始化列表阶段隐式调用一次Time类的构造函数
- 构造局部对象t时显式调用一次Time类的构造函数
- 最后还需调用一次Time类的赋值运算符重载函数
这还是导致整体效率明显降低,所以初始化列表的方式是很有优点的!!!
2、初始化顺序规则
成员变量的初始化顺序仅取决于它们在类中的声明顺序,与初始化列表中的书写顺序无关。
class OrderExample {
public:OrderExample() : b(i++), a(i++) {} // 实际初始化顺序:先a后bvoid Print() {std::cout << "a:" << a << ", b:" << b << std::endl;}
private:int a; // 先声明int b; // 后声明static int i;
};int OrderExample::i = 0;// 使用示例
OrderExample obj;
obj.Print(); // 输出: a:0, b:1
所以我们在日常开发学习中要:按成员在类中的声明顺序初始化、与初始化列表中的书写顺序无关、建议保持声明顺序和初始化顺序一致
六、C++成员变量初始化机制详解
初始化列表的核心逻辑
C++中成员变量的初始化遵循一套明确的规则,理解这些规则对于编写正确的构造函数至关重要。
1、显式初始化优先
规则:当成员变量在初始化列表中显式初始化时,直接使用指定的值进行初始化。
class Example {
public:Example(int x) : value(x) {} // value被显式初始化为x
private:int value;
};
2、未显式初始化的情况
当成员变量未在初始化列表中显式初始化时,系统会按照以下规则处理:
2.1 类声明中有缺省值
规则:如果成员变量在类声明时提供了缺省值,则使用该缺省值初始化。
class Example {
public:Example() {} // value1使用缺省值42
private:int value1 = 42; // 声明时提供缺省值
};
2.2 类声明中无缺省值
规则:此时行为取决于成员变量类型:
内置类型
-
可能结果:初始化为随机值或编译器默认值(如0)
-
注意:C++标准未明确规定,不同编译器行为可能不同
class Example {
public:Example() {} // value2的值不确定
private:int value2; // 内置类型,无缺省值
};
自定义类型
-
规则:调用该类型的默认构造函数(编译器在三种默认构造函数选其一)
-
注意:若该类型无默认构造函数,则编译错误
class Time {
public:Time(int h) : hour(h) {} // 无默认构造函数
};class Example {
public:Example() {} // 编译错误:Time无默认构造函数
private:Time t; // 自定义类型,无默认构造
};
特殊类型(必须初始化)
以下类型成员变量必须在初始化列表中显式初始化或声明时提供缺省值,否则编译错误:
引用成员变量、const成员变量、没有默认构造函数的类类型成员
class Example {
public:Example(int& ref) : r(ref) {} // 引用必须初始化
private:int& r; // 引用成员const int c = 10; // const成员(提供缺省值)
};
实际应用建议
-
始终优先使用初始化列表:确保所有成员变量被正确初始化
-
为内置类型提供缺省值:避免未定义行为
-
注意初始化顺序:按成员声明顺序初始化,与初始化列表顺序无关
-
特殊类型必须初始化:引用、const和无默认构造的类成员
七、代码示例分析
示例1:必须使用初始化列表的情况
class Time {
public:Time(int hour) : _hour(hour) {} // 无默认构造函数
private:int _hour;
};class Date {
public:Date(int& x) : _t(12), _ref(x), _n(1) {} // 必须初始化
private:Time _t; // 无默认构造int& _ref; // 引用const int _n; // const
};
示例2:缺省值使用
class Date {
public:Date() : _month(2) {} // 仅显式初始化_monthvoid Print() const {cout << _year << "-" << _month << "-" << _day;}
private:int _year = 1; // 缺省值int _month = 1; // 被显式初始化为2int _day; // 未初始化(内置类型行为未定义)Time _t = 1; // 自定义类型缺省值
};
示例3:初始化顺序问题(重要!!!)
class A {
public:A(int a) : _a1(a), _a2(_a1) {} // 实际先初始化_a2void Print() { cout << _a1 << " " << _a2; }
private:int _a2 = 2; // 声明在前int _a1 = 2; // 声明在后
};int main() {A aa(1);aa.Print(); // 输出1 随机值(D选项正确)
}
解析:
由于成员初始化顺序按声明顺序(先_a2后_a1),导致_a2用未初始化的_a1初始化,最终_a1=1,_a2为随机值。
八、总结
-
初始化列表是C++中真正的成员初始化机制、是对象构造的核心机制
-
引用、const成员和无默认构造函数的类成员必须使用初始化列表
-
对于所有成员变量,优先使用初始化列表以提高效率,并保持声明与初始化顺序一致
-
初始化顺序由成员声明顺序决定,与初始化列表书写顺序无关,容易成为陷阱
-
构造函数体内的"初始化"实际上是赋值操作,效率较低
-
C++11缺省值提供了额外的初始化灵活性
掌握初始化列表的正确使用是编写高效C++代码的重要基础,特别是在涉及复杂类和资源管理时,正确的初始化方式可以避免许多潜在问题。