构造函数初始化列表

构造函数初始化列表用于显式初始化类成员,避免非内置类型二次创建,提高效率。初始化顺序遵循成员在类中的声明顺序,对于无默认构造函数的对象是必需的。通过实例分析了使用和不使用初始化列表的差异。

构造函数初始化列表

与普通构造函数的区别

构造函数初始化列表用于在显式初始化类成员。在平常的使用构造函数创建对象时,如果对象中将另一个对象作为成员,那么在调用构造函数之前,编译器会用成员对象的默认构造函数进行初始化。如果不想让编译器用默认的构造函数进行初始化,就要使用构造函数初始化列表来显式规定成员对象的初始化形式。

构造函数进行的操作是,将成员先初始化,再用给定值赋值给初始化后的成员,而构造函数初始化列表则是直接对成员进行初始化,两者之间的区别跟下面两种创建变量的方式区别类似:

//在创建的时候直接初始化,相当于构造函数初始化列表
int x = 3;

//先创建x,再赋值给x,相当于普通构造函数的初始化方式
int x;
x = 3;

防止非内置类型二次创建

对于内置类型,两种方式的初始化方法都是一样的效果。但是对于成员是一个对象的情况,则会有很大的区别,它会导致对成员对象进行多次不必要的复制,进而降低运行效率。有些时候如果管理不当,甚至会导致内存泄漏,更严重的是内存混乱,从而程序崩溃。下面的例子展示了这样的情况:

class Car{
	private:
		char * brand;
		double price;
	public:
		Car(){
			cout << "Using default constructor for Car." << endl;
			brand = new char[3];
			strcpy(brand, "No");
			price = 0;
		};
		Car(const char * b, double p = 0){
			cout << "Using constructor for Car." << endl;
			price = p;
			brand = new char[strlen(b) + 1];
			strcpy(brand, b);
		}
		Car(const Car & c){
			cout << "Using copy constructor for Car." << endl;
			brand = new char[strlen(c.brand) + 1];
			strcpy(brand, c.brand);
			price = c.price;
		}
		~Car(){
			cout << "deleting car" << endl;
			delete [] brand;
		}
		void show(){
			cout << "The " << brand << " car worth of $" << price << endl;
		}
		Car operator=(const Car & c){
			Car res(c);
			return res;
		}
};

class Person{
	private:
		char * name;
		Car car;
	public:
		Person(){
			cout << "Using defualt constructor for Person." << endl;
		}
		Person(const char * n, Car c){
			cout << "Using constructor for Person." << endl;
			name = new char[strlen(n) + 1];
			strcpy(name, n);
			car = c;
		}

Car c("Ford", 23);
Person p("Jhon", c);

相比之下,Person类的构造函数比较简单,而Car类的构造函数则要复杂得多,这是为了对内存进行管理,以防止内存泄漏。要想知道Car类的构造函数为什么要这么写,可以看我的另一篇文章,不过不看也没有关系,不影响对构造函数初始化列表的理解。

运行上面的程序,会得到下面的输出结果:

Using constructor for Car.
Using copy constructor for Car.
Using default constructor for Car.
Using constructor for Person.
Using copy constructor for Car.
deleting car
deleting car
deleting car
deleting car

显然这个程序为了创建一个Person对象,创建并销毁了太多Car对象,下面解释为什么会产生这些输出。

  • 第一行没有什么特殊的,调用Car中定义的构造函数,创建对象。
  • 第二行,开始进入Person类的构造函数Person(const char * n, Car c),这个函数中Car是以值传递的,所以要复制一个副本,所以调用了Car类的复制构造函数。
  • 第三行,重点来了,这里调用的是Car类的默认构造函数,并且这个时候还没有进入Person类构造函数的函数体内执行代码,因为第一行代码是打印语句,而打印语句在第四行。说明编译器在进入Person类构造函数执行代码之前,用Car的默认构造函数创建了一个Car对象。这就是之前说的,先用默认构造函数创建对象,再对这个对象赋值。
  • 第四行,开始进入Person类的构造函数并执行代码。
  • 第五行,使用Car的复制构造函数将外部传进来的Car对象赋值给内部对象。

综上所述,为了初始化Person类中的Car成员,创建了三个Car对象。

下面通过构造函数初始化列表的方式对其进行改进,将Person类中的构造函数Person(const char * n, Car c)替换成:

Person(const char * n, Car c):car(c){
			cout << "Using constructor initiation list." << endl;
			name = new char[strlen(n) + 1];
			strcpy(name, n);
		}

该程序输出如下所示:

Using constructor for Car.
Using copy constructor for Car.
Using copy constructor for Car.
Using constructor initiation list.
deleting car
deleting car

可以看到,使用了构造函数初始化列表之后,编译器没有再调用Car的默认构造函数。而是直接用Car的复制构造函数初始化car成员。

初始化顺序

另一个需要注意的点是,构造函数初始化列表初始化成员的顺序是根据成员在类中的声明顺序来的,而不是成员在列表中出现的顺序。例如:

class Test{
	private:
		int x, y;
	public:
		Test(int a): y(a), x(y){}
		void show(){
			cout << "x = " << x << ", y = " << y << endl;
		}
};

Test t(3);
t.show();

程序的输出结果为:

x = 0, y = 3

逻辑上来说,应该xy都等于3,但是因为是先初始化x,然后再初始化y,所以初始化x的时候y的值还是0,所以x被初始化成了0

必须用构造函数初始化列表的类型

必须用构造函数初始化列表的类型有以下特点:

  • 初始化之后不能修改
  • 没有默认构造函数的对象

因为如果不适用构造函数初始化列表,编译器就会调用成员对象的默认构造函数先初始化对象,所以没有默认构造函数的对象必须使用构造函数初始化列表。

因为普通构造函数初始化成员的过程是先将其初始化为系统默认值,再将给定值复制给初始化之后的变量。所以那些初始化之后就不能改变的变量就只能使用构造函数初始化列表。例如引用变量const变量

<think>我们正在讨论构造函数初始化列表的使用方法。根据之前提供的引用内容,我们可以总结如下: 1. 初始化列表的语法:以冒号开始,然后是逗号分隔的成员初始化列表,每个成员后面跟着用括号括起来的初始值。 2. 初始化列表构造函数体执行之前执行。 3. 有些成员必须使用初始化列表进行初始化,例如常量成员(const)和引用成员(引用型)。 现在,我将详细解释构造函数初始化列表的使用方法。 ### 构造函数初始化列表的使用方法 #### 1. **基本语法** 构造函数初始化列表位于构造函数参数列表之后,函数体之前,以冒号开头,成员初始化器之间用逗号分隔。每个成员初始化器由成员变量名和括号中的初始值(或构造函数的参数)组成。 格式: ```cpp ClassName(参数列表) : member1(值1), member2(值2), ... { // 构造函数体 } ``` #### 2. **为什么需要初始化列表?** - **必须使用初始化列表的情况**: - 当成员是**常量成员(const)**时,因为常量只能在创建时初始化,不能赋值。 - 当成员是**引用型**时,因为引用必须在创建时绑定到某个对象,且之后不能更改。 - 当成员是**没有默认构造函数型**时,因为编译器无法默认初始化该成员,必须显式调用其构造函数。 - **效率考虑**: 对于型的成员,使用初始化列表直接调用其构造函数进行初始化,比在构造函数体内先调用默认构造函数再赋值效率更高(避免两次构造)。 #### 3. **使用示例** ##### 示例1:初始化常量成员和引用成员 ```cpp class MyClass { public: const int const_member; int& ref_member; MyClass(int a, int& b) : const_member(a), ref_member(b) { // 构造函数体 } }; ``` 这里,`const_member`是常量整型,必须在初始化列表初始化;`ref_member`是一个引用,也必须在初始化列表中绑定到某个变量。 ##### 示例2:初始化成员对象(无默认构造函数) 假设有一个`AnotherClass`没有默认构造函数(即没有无参构造函数): ```cpp class AnotherClass { public: AnotherClass(int x) { /* ... */ } }; class MyClass { AnotherClass obj; public: MyClass(int x) : obj(x) { // 必须使用初始化列表初始化obj // 如果写成: MyClass(int x) { obj = AnotherClass(x); } 是错误的,因为AnotherClass没有默认构造函数 } }; ``` ##### 示例3:普通成员初始化(推荐方式) 即使对于普通成员变量,也推荐使用初始化列表: ```cpp class Data { private: int year; int month; int day; public: Data(int y, int m, int d) : year(y), month(m), day(d) { } }; ``` #### 4. **初始化顺序** 成员变量的初始化顺序是按照它们在定义中声明的顺序,而不是初始化列表中的顺序。因此,在初始化列表中应尽量按照声明顺序书写,避免混淆。 例如: ```cpp class Test { int a; int b; public: Test(int val) : b(val), a(b) { } // 错误!因为a先于b声明,所以a先初始化,此时b还未初始化,用b的值初始化a是未定义行为 }; ``` 正确的做法是调整初始化列表的顺序与声明顺序一致,或者避免使用未初始化的成员。 #### 5. **委托构造函数C++11)** 在C++11中,构造函数可以调用同一个的另一个构造函数,这称为委托构造函数。它也是在初始化列表中调用。 ```cpp class Date { int year, month, day; public: // 主构造函数 Date(int y, int m, int d) : year(y), month(m), day(d) {} // 委托构造函数:委托给主构造函数 Date() : Date(1970, 1, 1) {} // 默认日期 Date(int y) : Date(y, 1, 1) {} // 只指定年份 }; ``` #### 6. **注意事项** - 初始化列表中的成员初始化顺序必须与定义中的成员声明顺序一致,以避免混淆和可能的错误。 - 对于内置型(如int, double, 指针等),在初始化列表初始化与在构造函数体内赋值在性能上没有区别,但为了代码一致性,建议也放在初始化列表中。 - 如果成员是型,且没有在初始化列表初始化,则编译器会尝试使用该的默认构造函数初始化。如果没有默认构造函数,则编译错误。 ### 总结 构造函数初始化列表C++初始化成员的高效且必要的方式,尤其在以下情况必须使用: 1. 初始化常量成员(const) 2. 初始化引用成员 3. 初始化没有默认构造函数成员 此外,使用初始化列表还可以提高代码效率(避免不必要的默认构造和赋值操作)和清晰度。 §§相关问题§§ 1. 为什么常量成员必须在初始化列表初始化? 2. 如果成员变量是型且有默认构造函数,是否可以不使用初始化列表? 3. 委托构造函数的使用场景是什么? 4. 如何避免初始化顺序导致的错误? </think> ### 构造函数初始化列表使用方法详解 构造函数初始化列表C++中高效初始化成员的核心机制,其使用方法如下: #### 1. **基本语法结构** ```cpp 名(参数列表) : 成员1(初始值), 成员2(初始值), ... { // 构造函数体 } ``` - 以**冒号开头**,**逗号分隔**成员初始化项 - 每个成员使用`成员名(初始值)`格式初始化 - 在构造函数体**执行前**完成初始化[^1][^3] #### 2. **必须使用初始化列表的场景** | 场景 | 示例 | 原因 | |------|------|------| | 常量成员 | `const int MAX_SIZE;` | 常量创建后不可修改 | | 引用成员 | `int& ref;` | 引用必须绑定初始对象 | | 无默认构造的成员 | `class A{ A(int); };` | 无法默认初始化[^2] | ```cpp class Data { public: Data(int year) : MAX_YEAR(2100), // const常量 refYear(year), // 引用 aObj(year) // 无默认构造的 {} private: const int MAX_YEAR; int& refYear; A aObj; // 假设A无默认构造函数 }; ``` #### 3. **初始化顺序规则** 成员初始化**严格按声明顺序执行**(与初始化列表顺序无关): ```cpp class Test { int a; int b; public: Test(int x) : b(x), a(b) {} // 危险!a先初始化但b未定义 }; ``` > 正确做法:调整声明顺序或避免依赖[^4] #### 4. **效率优化技巧** 对型成员使用初始化列表可避免**双重构造**: ```cpp // 低效方式(构造函数体内赋值) MyClass() { obj = MyObject(10); // 先默认构造 + 再赋值 } // 高效方式(初始化列表) MyClass() : obj(10) {} // 直接构造 ``` 可节省一次构造函数调用和一次赋值操作[^2] #### 5. **委托构造函数C++11)** 复用构造函数逻辑的特殊初始化列表: ```cpp class Date { public: // 主构造函数 Date(int y, int m, int d) : year(y), month(m), day(d) {} // 委托构造函数(复用主构造) Date() : Date(1970, 1, 1) {} Date(int y) : Date(y, 1, 1) {} }; ``` > 适用于多个构造函数共享初始化逻辑的场景[^4] #### 6. **基础初始化建议** ```cpp // 推荐方式(显式初始化) class Widget { int width = 0; // C++11初始化 int height; public: Widget() : height(0) {} // 初始化列表 }; // 避免(未初始化风险) class BadWidget { int width; // 未初始化! int height; public: BadWidget() {} }; ``` ### 使用流程指南 1. **识别特殊成员**:检查const、引用、无默认构造的成员 2. **声明顺序检查**:确保成员声明顺序合理 3. **编写初始化项**:按`成员(值)`格式列出 4. **添加委托构造**(可选):复用已有构造函数 5. **验证初始化**:使用调试器检查成员初始值 > 💡 **最佳实践**:始终优先使用初始化列表,即使基础型也显式初始化,可避免未定义行为并提升性能[^2][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值