构造函数部分
C++的构造函数是类的一种特殊成员函数,当对象被创建时自动调用,用于初始化对象的成员变量。构造函数的名称必须与类名相同,并且没有返回类型(包括void
)。它是实现对象初始化的关键部分。以下是构造函数的一些用法和概念:
1. 默认构造函数(Default Constructor)
如果你没有定义任何构造函数,C++编译器会自动为你生成一个默认构造函数。默认构造函数不带参数,用于默认初始化对象的成员变量。
class MyClass {
public:
MyClass() { // 默认构造函数
// 初始化代码
}
};
2. 参数化构造函数(Parameterized Constructor)
参数化构造函数允许你在创建对象时传递参数,并使用这些参数初始化成员变量。
class MyClass {
public:
int x;
MyClass(int val) { // 参数化构造函数
x = val;
}
};
使用参数化构造函数创建对象:
MyClass obj(10); // x 被初始化为 10
3. 拷贝构造函数(Copy Constructor)
拷贝构造函数用于创建一个新对象,并将已有对象的值拷贝到新对象中。它的参数通常是传递一个同类的对象引用。
class MyClass {
public:
int x;
MyClass(int val) : x(val) {} // 参数化构造函数
MyClass(const MyClass &obj) { // 拷贝构造函数
x = obj.x;
}
};
使用拷贝构造函数创建对象:
MyClass obj1(10);
MyClass obj2 = obj1; // obj2 使用 obj1 的值初始化
4. 委托构造函数(Delegating Constructor)
C++11 引入了委托构造函数,允许一个构造函数调用同一个类中的另一个构造函数来实现初始化。这可以减少代码重复。
class MyClass {
public:
int x, y;
MyClass(int val) : MyClass(val, 0) {} // 委托给另一个构造函数
MyClass(int val1, int val2) : x(val1), y(val2) {}
};
5. 初始化列表(Initializer List)
初始化列表用于在构造函数的函数体执行之前初始化成员变量,尤其是在成员变量是常量或引用时。
class MyClass {
public:
const int x;
MyClass(int val) : x(val) { // 使用初始化列表初始化 x
// 构造函数体
}
};
6. 显式构造函数(Explicit Constructor)
如果构造函数前面加上explicit
关键字,C++编译器将不会允许通过隐式类型转换来调用该构造函数。这可以避免意外的类型转换。
class MyClass {
public:
explicit MyClass(int val) {
// 构造函数体
}
};
MyClass obj = 10; // 错误,不能进行隐式转换
MyClass obj(10); // 正确,显式调用构造函数
7. 析构函数与构造函数配对
与构造函数相对应的是析构函数,它在对象的生命周期结束时调用,用于释放资源。构造函数初始化对象,析构函数负责清理对象。
class MyClass {
public:
MyClass() {
// 构造函数体
}
~MyClass() {
// 析构函数体
}
};
总结
C++构造函数在对象的生命周期管理中扮演了重要角色,通过不同类型的构造函数(如默认、参数化、拷贝构造函数等)可以灵活地控制对象的初始化行为,确保对象在使用前处于有效状态。
构造函数初始化列表部分
构造函数初始化列表,是一种用于在构造函数中直接初始化成员变量的语法:
MyOpenGLWidget::MyOpenGLWidget(QWidget* parent): QOpenGLWidget(parent)
{
// 构造函数的初始化代码
}
咋一看,还以为是类继承呢(可以包含 父类的实例化,所以开起来像继承一样)。 其实是构造函数后面带个冒号,冒号后面的就是列表。
这里看上去其实挺容易迷惑的,由于C++申明一般写在头文件里面,所以头文件里面一般是一个class。 而实现过程写到cpp文件里面。比如上面这段代码。
MyOpenGLWidget::MyOpenGLWidget 前面这个MyOpenGLWidget,表面后面函数是归属哪个类的。是的,C++的函数可以 放到 class外面(C# 表示震惊!),只要用类名限定一下即可。
而后面这个::MyOpenGLWidget 是构造函数,由于是构造函数,所以和类名相同。紧接着后面又有冒号,跟着一个父类。
感觉很像是一个类继承了另一个类!(特别是C#写习惯的同学,会这么认为。。。)但其实这是个构造函数!注意这里并没有class关键字!
初始化列表(Initializer List)
在C++中,初始化列表是一种用于在构造函数中直接初始化成员变量的语法。它允许在构造函数的函数体执行之前初始化成员变量,尤其适用于以下情况:
- 常量成员变量:常量必须在初始化时赋值,不能在构造函数体内赋值。
- 引用成员变量:引用必须在初始化时绑定到对象上。
- 没有默认构造函数的类类型成员变量:如果一个成员变量的类型没有默认构造函数,必须通过参数进行初始化。
初始化列表的语法如下:
class MyClass {
public:
int a;
const int b;
int &c;
MyClass(int x, int y, int &z) : a(x), b(y), c(z) {
// 构造函数体
}
};
在这个例子中,a
、b
和c
都是通过初始化列表进行初始化的。这意味着在进入构造函数体之前,它们已经被赋予了初值。
为什么使用初始化列表?
- 效率:通过初始化列表,成员变量在对象创建时就被初始化,不会先被默认初始化然后再赋值,从而避免了不必要的开销。
- 必须:某些成员(如
const
、引用
)必须在对象创建时初始化,否则会导致编译错误。
构造时继承父类
在继承时,派生类的构造函数可以调用基类的构造函数以初始化基类的部分。C++中,基类的构造函数是在派生类的构造函数之前执行的,因此,初始化列表中调用基类构造函数是一种常见的做法。
语法
在派生类的构造函数的初始化列表中调用基类的构造函数:
class Base {
public:
int base_val;
Base(int val) : base_val(val) {
// 基类构造函数体
}
};
class Derived : public Base {
public:
int derived_val;
Derived(int baseVal, int derivedVal) : Base(baseVal), derived_val(derivedVal) {
// 派生类构造函数体
}
};
在这个例子中,Derived
类继承自Base
类,并且在Derived
类的构造函数中,通过初始化列表调用了Base
类的构造函数来初始化基类成员base_val
。
基类的构造顺序
-
基类构造函数先执行:无论在初始化列表中是否调用基类构造函数,基类的构造函数总是在派生类的构造函数之前执行。这是因为派生类的成员变量可能依赖于基类的成员变量。
-
成员初始化顺序:即使在初始化列表中成员变量的顺序不同,成员变量的初始化顺序依然按照它们在类定义中出现的顺序执行。
示例:父类与子类的构造
以下是一个示例,展示了父类与子类的构造顺序:
class Base {
public:
int x;
Base(int a) : x(a) {
std::cout << "Base Constructor" << std::endl;
}
};
class Derived : public Base {
public:
int y;
Derived(int a, int b) : Base(a), y(b) {
std::cout << "Derived Constructor" << std::endl;
}
};
int main() {
Derived obj(5, 10);
return 0;
}
输出:
Base Constructor
Derived Constructor
在这个示例中,创建Derived
类对象时,首先调用了Base
类的构造函数,然后才调用Derived
类的构造函数。通过这种方式,派生类可以确保其基类部分在使用之前已经正确初始化。
总结
初始化列表是一种高效且必需的初始化方式,特别是当你需要初始化const
成员、引用成员或者必须提供参数初始化的成员时。在继承结构中,基类的构造在派生类之前执行,派生类可以通过初始化列表来控制基类如何初始化。这种机制确保了对象创建过程中的正确性和效率。
关于顺序
构造函数初始化列表,里面可以构建父类,可以对类成员赋值,或构建类成员。
能构建父类,原因是,父类也可以看作是子类的成员之一,和其他成员并没有什么不同:
所以这么写,也是可以的:
MyOpenGLWidget::MyOpenGLWidget(QWidget* parent): a(1),QOpenGLWidget(parent)
{
// 构造函数的初始化代码
}
在C++中,当构造派生类对象时,基类的构造函数必须在派生类的构造函数中调用,而这个调用是在初始化列表中指定的。然而,基类的构造函数在初始化列表中的位置并不影响它的执行顺序。
构造顺序规则
-
基类先于派生类初始化:无论基类构造函数在初始化列表中的位置如何,基类对象总是先于派生类对象进行初始化。这是C++标准规定的。
-
成员初始化顺序与声明顺序一致:派生类中的成员变量初始化顺序与它们在类中声明的顺序一致,而不是它们在初始化列表中的顺序。这意味着即使在初始化列表中按照不同顺序列出成员变量,编译器仍然会按照它们的声明顺序进行初始化。
我们来举一个例子,演示基类与派生类的构造顺序以及类成员变量的初始化顺序。这个例子中不涉及引用变量,只涉及普通的类类型成员变量。
示例:基类和派生类的构造顺序
#include <iostream>
class Base {
public:
int base_val;
Base(int val) : base_val(val) {
std::cout << "Base Constructor: base_val = " << base_val << std::endl;
}
};
class Member1 {
public:
int member_val1;
Member1(int val) : member_val1(val) {
std::cout << "Member1 Constructor: member_val1 = " << member_val1 << std::endl;
}
};
class Member2 {
public:
int member_val2;
Member2(int val) : member_val2(val) {
std::cout << "Member2 Constructor: member_val2 = " << member_val2 << std::endl;
}
};
class Derived : public Base {
public:
Member1 m1;
Member2 m2;
// 初始化列表中的顺序与成员初始化的顺序无关
Derived(int baseVal, int val1, int val2) : m2(val2), m1(val1), Base(baseVal) {
std::cout << "Derived Constructor" << std::endl;
}
};
int main() {
Derived obj(10, 20, 30);
return 0;
}
输出:
Base Constructor: base_val = 10
Member1 Constructor: member_val1 = 20
Member2 Constructor: member_val2 = 30
Derived Constructor
解释
-
基类构造顺序:虽然在
Derived
类的初始化列表中,Base
构造函数被放在最后调用,但它仍然是最先执行的。基类Base
的构造函数在派生类的成员变量之前被调用。这是C++标准规定的顺序。 -
成员变量初始化顺序:派生类的成员变量
m1
和m2
的初始化顺序不是由初始化列表中的顺序决定的,而是由它们在类定义中的顺序决定的。在Derived
类中,m1
先被声明,因此它先于m2
初始化,即使在初始化列表中m2
被列在m1
之前。 -
派生类构造函数的执行:在基类构造函数和所有成员变量构造函数都完成之后,派生类的构造函数体才会执行。
结论
-
基类构造函数始终优先执行:无论在初始化列表中基类构造函数的位置如何,它总是在派生类成员变量初始化之前执行。
-
成员变量按照声明顺序初始化:派生类的成员变量初始化顺序与它们在类中的声明顺序一致,与初始化列表中的顺序无关。