目录
1.引言
对于学过C++的来说,构造函数是非常熟悉不过的了。但是你真正了解它吗?构造函数内部初始化变量的顺序是怎么样的?多种继承构造函数的执行顺序?等等。今天我从前两天编程时遇到的一个问题说起,代码如下(只列举了部分关键代码):
cpp
#pragma once
#include <QObject>
#include <memory>
class cCtkContext {};
class CLinkCoreFactory
{
public:
explicit CLinkCoreFactory(cCtkContext* pContext) : m_pContext(pContext) {}
//...
private:
cCtkContext* m_pContext;
};
class IDataNotify
{
public:
virtual void notify() = 0;
//...
};
class CThirdIInterface
{
public:
explicit CThirdIInterface(CLinkCoreFactory* pFactory)
: m_pFactory(pFactory) {}
//...
private:
CLinkCoreFactory* m_pFactory;
};
class CDeviceManager : public QObject
, public IDataNotify
, public CThirdIInterface
{
public:
CDeviceManager(cCtkContext* pContext, QObject* parent = nullptr)
: QObject(parent)
, m_linkCoreFactory(new CLinkCoreFactory(pContext))
, CThirdIInterface(m_linkCoreFactory.get())
{}
public:
void notify() override {}
//...
private:
std::unique_ptr<CLinkCoreFactory> m_linkCoreFactory;
};
int main()
{
cCtkContext context;
CDeviceManager manager(&context);
return 0;
}
在CDeviceManager内部定义了m_linkCoreFactory,CDeviceManager又继承于CThirdIInterface,CThirdIInterface又依赖于CLinkCoreFactory,于是CDeviceManager构造的时候,在写法上先构造了变量m_linkCoreFactory,再构造类CThirdIInterface,但是调试运行的时候,最后发现CThirdIInterface中的m_pFactory是无个效指针,为什么会这样呢?下面就这个问题深入讲解一下。
2.默认构造函数
如果类中没有定义任何构造函数,编译器会自动提供一个默认构造函数,这个构造函数不接受任何参数,并且不进行任何初始化操作(对象内的成员变量将使用它们的默认构造函数进行初始化,对于内置类型,这通常意味着它们将不会被初始化)。但是,一旦定义了任何构造函数(包括带参数的构造函数),编译器将不再自动生成默认构造函数。如果你需要默认构造函数,你必须显式地定义它。
示例如下:
cpp
#include <iostream>
using namespace std;
class Box {
public:
//使用default关键字定义默认构造函数,length、breadth、height不会初始化
Box()=default;
//也可以自定义构造函数,初始化length、breadth、height
#if 0
Box() : length(0.0),breadth(0.0),height(0.0) {}
#endif
//也可以自定义带参数的构造函数,初始化length、breadth、height
Box(double len, double bre, double hei)
: length(len)
, breadth(bre)
, height(hei)
{
}
// 成员函数来计算盒子的体积
double getVolume(void) {
return length * breadth * height;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
int main() {
Box Box1(3.3, 3.3, 3.3); // 声明 Box1 为 Box 类的一个对象
// Box1 的三个边初始化为 3.3
// 输出 Box1 的体积
cout << "Volume of Box1 : " << Box1.getVolume() << endl;
return 0;
}
在Box内部,定义了一个默认构造函数,可以定义多个自定义构造函数,即是说构造函数是可以重载的。
3.自定义构造函数
在C++中,自定义构造函数允许你为类的实例提供特定的初始化逻辑。构造函数初始化列表是构造函数体之前的一个冒号(:
)后跟一个以逗号分隔的初始化器列表。它用于初始化成员变量,特别是那些不能通过赋值来初始化的成员(如const
成员、引用成员或没有默认构造函数的类类型的成员)。
上面示例中的:
cpp
Box(double len, double bre, double hei)
: length(len)
, breadth(bre)
, height(hei)
{
}
就属于自定义构造函数。
4.带继承关系类的构造函数
在C++中,子类(派生类)不能直接继承父类(基类)的构造函数。但是,子类可以定义自己的构造函数,并在其构造函数内部调用父类的构造函数(如果父类有可访问的非私有构造函数)以进行必要的初始化。这是通过使用成员初始化列表来实现的,在初始化列表中可以显式地调用父类的构造函数。
以下是一个简单的示例,展示了如何在带有继承的C++类中处理构造函数:
cpp
#include <iostream>
using namespace std;
// 基类
class Base {
public:
// 基类的构造函数
Base(int val) : a(val) {
cout << "Base class constructor called, a = " << a << endl;
}
// 基类的另一个构造函数(可选)
Base() : a(0) {
cout << "Base class default constructor called, a = " << a << endl;
}
private:
int a;
};
// 派生类
class Derived : public Base {
public:
// 派生类的构造函数,显式调用基类的构造函数
Derived(int val1, int val2) : Base(val1), b(val2) {
cout << "Derived class constructor called, b = " << b << endl;
}
// 如果需要,也可以定义默认构造函数(不显式调用Base的构造函数时,会调用Base的默认构造函数)
Derived() : Base(), b(0) {
cout << "Derived class default constructor called, b = " << b << endl;
}
private:
int b;
};
int main() {
// 使用自定义构造函数创建Derived对象
Derived obj1(5, 10);
// 使用默认构造函数创建Derived对象(同时也会调用Base的默认构造函数)
Derived obj2;
return 0;
}
在上面的代码中,Derived
类继承自Base
类。Derived
类有两个构造函数:一个接受两个参数(一个用于Base
类,一个用于自身),另一个是无参数的默认构造函数。在Derived
的构造函数中,通过成员初始化列表显式地调用了Base
类的构造函数。
如果Base
类没有可访问的默认构造函数,并且你试图在Derived
类中定义一个不接受任何参数的构造函数而不显式调用Base
类的某个构造函数,那么编译器会报错,因为它不知道应该使用Base
类的哪个构造函数。
此外,构造函数初始化列表是初始化成员变量的首选方式,因为它比在构造函数体内赋值更高效,特别是对于常量成员、引用成员以及没有默认构造函数的类类型成员。
注意事项
•初始化顺序:派生类的构造函数首先初始化基类部分,然后初始化派生类自己的成员变量。这意味着,即使你在派生类构造函数体内调用基类构造函数,基类部分也已经在调用之前被初始化了。
5.带多重继承关系类的构造函数
在C++中,多重继承允许一个类(派生类)同时从多个基类继承属性和行为。在处理多重继承时,派生类的构造函数需要负责初始化所有基类以及自身的成员变量。这通常是通过构造函数初始化列表来完成的,其中可以明确指定如何调用各个基类的构造函数。
下面是一个C++多重继承的例子,展示了如何在构造函数中初始化多个基类:
cpp
#include <iostream>
using namespace std;
// 第一个基类
class Base1 {
public:
Base1(int val) : a(val) {
cout << "Base1 constructor called, a = " << a << endl;
}
private:
int a;
};
// 第二个基类
class Base2 {
public:
Base2(int val) : b(val) {
cout << "Base2 constructor called, b = " << b << endl;
}
private:
int b;
};
// 派生类,继承自Base1和Base2
class Derived : public Base1, public Base2 {
public:
// 派生类的构造函数
// 必须显式调用所有基类的构造函数
Derived(int val1, int val2, int val3) : Base1(val1), Base2(val2), c(val3) {
cout << "Derived constructor called, c = " << c << endl;
}
private:
int c;
};
int main() {
// 创建Derived类的实例
Derived obj(1, 2, 3);
return 0;
}
在这个例子中,Derived
类同时从Base1
和Base2
两个基类继承。Derived
类的构造函数通过构造函数初始化列表显式地调用了Base1
和Base2
的构造函数,并传递了相应的参数。同时,它也初始化了自己的成员变量c
。
需要注意的是,构造函数初始化列表中基类的调用顺序与它们在派生类声明中的顺序相同,而不是与它们在初始化列表中出现的顺序相同 。在这个例子中,Base1
的构造函数将在Base2
的构造函数之前被调用,尽管在初始化列表中Base2
的调用出现在Base1
之前。
此外,如果基类中有默认构造函数(即不接受任何参数的构造函数),并且派生类的构造函数没有显式调用任何基类构造函数,那么将隐式调用基类的默认构造函数(如果存在的话)。但是,如果基类没有可访问的默认构造函数,并且派生类的构造函数没有显式调用基类的某个构造函数,那么编译器将报错。
现在我们回顾一下在文章开头的例子调试发现CThirdIInterface中的m_pFactory是无个效指针,原因就是CDeviceManager的构造函数:
cpp
CDeviceManager(cCtkContext* pContext, QObject* parent = nullptr)
: QObject(parent)
, m_linkCoreFactory(new CLinkCoreFactory(pContext))
, CThirdIInterface(m_linkCoreFactory.get())
{}
CThirdIInterface构造函数的调用实际先于m_linkCoreFactory的构造,导致传入CThirdIInterface构造函数的指针pFactory是空的。
6.带虚继承关系类的构造函数
虚继承(Virtual Inheritance)用于解决多重继承中可能出现的菱形继承(Diamond Inheritance)问题,即一个类从多个基类继承,而这些基类又共同继承自同一个基类,导致基类在派生类中被多次实例化的问题。通过虚继承,可以确保基类在继承体系中只被实例化一次。
在虚继承中,派生类的构造函数依然需要负责初始化所有基类(包括虚基类和其他非虚基类)以及自身的成员变量。但是,与常规继承不同的是,虚基类的构造函数只在最底层的派生类中被调用一次,且调用顺序由其在继承体系中的深度决定(从最顶层的基类开始,向下直到最底层的派生类)。
示例如下:
cpp
#include <iostream>
using namespace std;
// 虚基类
class VirtualBase {
public:
VirtualBase() {
cout << "VirtualBase constructor called" << endl;
}
};
// 第一个基类,虚继承自VirtualBase
class Base1 : virtual public VirtualBase {
public:
Base1() {
cout << "Base1 constructor called" << endl;
}
};
// 第二个基类,也虚继承自VirtualBase
class Base2 : virtual public VirtualBase {
public:
Base2() {
cout << "Base2 constructor called" << endl;
}
};
// 派生类,同时继承自Base1和Base2
class Derived : public Base1, public Base2 {
public:
Derived() {
cout << "Derived constructor called" << endl;
}
};
int main() {
// 创建Derived类的实例
Derived obj;
return 0;
}
在这个例子中,Derived
类从Base1
和Base2
继承,而Base1
和Base2
都虚继承自VirtualBase
。当你创建一个Derived
类的实例时,构造函数的调用顺序如下:
1) VirtualBase
的构造函数首先被调用(因为VirtualBase
是最顶层的基类,并且只会被实例化一次)。
-
接着是
Base1
的构造函数(注意,尽管Base1
虚继承自VirtualBase
,但VirtualBase
的构造函数已经在前面被调用了,所以这里不会再次调用它)。 -
然后是
Base2
的构造函数(同样,VirtualBase
的构造函数不会被重复调用)。 -
最后是
Derived
的构造函数。
输出将是:
cpp
VirtualBase constructor called
Base1 constructor called
Base2 constructor called
Derived constructor called
注意事项
•构造函数的调用顺序:在虚继承中,最派生类的构造函数负责首先调用虚基类的构造函数,然后是其他非虚基类的构造函数。
•初始化责任:由于虚基类只被初始化一次,因此最派生类负责调用虚基类的构造函数。
7.总结
构造函数是C++中非常重要的概念,它们用于在对象创建时初始化对象的成员变量。构造函数没有返回类型,并且其名称必须与类名相同。通过构造函数,可以确保对象在创建时处于有效的状态。