关键规则
- 如果派生类有基类(单继承或多继承),基类的构造函数会首先被调用。
1. 对基类进行处理
- 多继承时,按照派生类继承列表中声明的顺序(从左到右)依次调用基类的构造函数。
- 如果有虚继承,虚基类的构造函数优先于非虚基类调用,且只调用一次。
- 虚基类只在最远派生类中进行处理,并且只有最远派生类调用,其他虚继承的派生类调用被忽略,并且只执行一次
2. 对成员对象进行处理
- 在基类构造函数调用完成后,派生类中声明的成员对象的构造函数会被调用。
- 成员对象的构造顺序遵循它们在类中声明的顺序(而不是初始化列表中的顺序)。
- 如果成员对象有自己的构造函数,C++ 会根据初始化列表(如果提供)或默认构造函数来调用。
3.派生类自己的构造函数
- 最后,派生类自己的构造函数体被执行。
特殊情况
如果父类构造函数含有虚函数调用
- 在父类的构造函数中调用虚函数,还是会执行父类的构造函数,不会跑到子类中去,即使有vitual,因为此时父类都还没有构造完成,子类也就还没有构造。
#include <iostream>
using namespace std;
class A{
public:
A ():m_iVal(0){test();}//这里的test会调用父类的virtual void func()
virtual void func() { std::cout<<m_iVal<<' ';}
void test(){func();}
public:
int m_iVal;
};
class B : public A{
public:
B(){test();}//这里的test会调用父类的test()进而通过指针匹配
virtual void func(){
++m_iVal;
std::cout << m_iVal << ' ';
}
};
int main(int argc ,char* argv[]){
A*p = new B;
p->test();
return 0;
}
-
输出结果
0 1 2
- 虚函数调用规则:
- 构造函数中:绑定到当前构造的类版本。
- 构造完成后:动态绑定到实际对象类型。
- 执行流程:
- A::A() → test() → A::func() → 输出0(vtable指向A)。
- B::B() → test() → B::func() → m_iVal++,输出1(vtable指向B)。
- p->test() → B::func() → m_iVal++,输出2。
- vtable切换:
- 基类构造:指向A。
- 基类完成后,进入B::B()前:指向B。
- 输出:
- 0 1 2。
核心结论
- 构造函数中虚函数调用取决于当前类类型,非最终类型。
- vtable在基类构造后、派生类构造函数体前更新。
- 注意:在执行构造函数体之前,可以认为构造函数**已经完全完成了相应对象的初始化工作,**在C++的实现中,虚函数表的切换发生在进入派生类构造函数体之前,而不是等到整个构造函数结束。
关于拷贝构造函数的初始化问题
- 在初始化时如果对象不存在,且没有声明explict禁用赋值操作
- 编译器默认调用拷贝构造函数
#include<iostream>
using namespace std;
class MyClass {
public:
MyClass(int i = 0) { // 构造函数
cout << i;
}
MyClass(const MyClass &x) { // 拷贝构造函数
cout << 2;
}
MyClass &operator=(const MyClass &x) { // 赋值运算符
cout << 3;
return *this;
}
~MyClass() { // 析构函数
cout << 4;
}
};
int main() {
MyClass obj1(1), obj2(2); // 创建 obj1 和 obj2
MyClass obj3 = obj1; // 创建 obj3 并初始化
return 0;
}
MyClass obj3 = obj1;由于obj3为被创建,那么调用拷贝构造函数,称为复制初始化
MyClass obj3; // 先默认构造
obj3 = obj1; // 再赋值
这时会导致调用赋值运算符
拷贝构造函数的什么时候被调用呢?
场景 | 示例代码 | 说明 |
---|---|---|
对象初始化 | MyClass b = a;或者MyClass b(a); | 用已有对象初始化新对象 |
按值传递参数 | void func(MyClass x); | 函数参数创建副本 |
按值返回对象 | MyClass func() { … } | 返回局部对象(可能被优化,现如今的C++编译器普遍采用了RVO返回值优化导致返回时候不会进行拷贝) |
容器操作 | vec.push_back(a); | 插入对象到容器 |
显式调用 | new MyClass(a); | 动态分配时拷贝 |
关于子类和父类的虚函数问题
如果父类函数不是 virtual,子类将其声明为 virtual:
- 对基类无影响,基类调用仍是静态绑定。
- 从子类开始,函数成为虚函数,后续派生类可以实现多态,(即便是后续没有加virtual关键字也是多态)
#include <iostream>
class Base {
public:
void foo() { // 非虚函数
std::cout << "Base::foo()" << std::endl;
}
};
class Derived : public Base {
public:
virtual void foo() { // 子类声明为虚函数
std::cout << "Derived::foo()" << std::endl;
}
};
class GrandDerived : public Derived {
public:
void foo() { // 重写 Derived 中的虚函数
std::cout << "GrandDerived::foo()" << std::endl;
}
};
int main() {
Base* b1 = new Derived();
b1->foo(); // 输出 "Base::foo()"
Derived* d1 = new Derived();
d1->foo(); // 输出 "Derived::foo()"
Base* b2 = new GrandDerived();
b2->foo(); // 输出 "Base::foo()"
Derived* d2 = new GrandDerived();
d2->foo(); // 输出 "GrandDerived::foo()"
delete b1; delete d1; delete b2; delete d2;
return 0;
}
父类函数非虚,没有多态
- 因为 Base::foo() 不是虚函数,通过 Base* 指针调用 foo() 时**,总是执行 Base::foo()**,不会发生运行时多态。
- 即使 Derived::foo() 被声明为 virtual,它对 Base 的函数没有影响,因为多态性需要从基类开始启用。
子类声明为虚,影响后续继承
- 在 Derived 中将 foo() 声明为 virtual,意味着从 Derived 开始,这个函数变成了虚函数。
- 后续的派生类(如 GrandDerived)可以重写 Derived::foo(),并通过 Derived* 或 Derived& 调用时实现多态。
- 但这种多态仅限于 Derived 及其子类,不追溯到 Base。
隐藏而非重写
- Derived::foo() 只是隐藏了 Base::foo(),而不是重写它。
- 当通过 Base* 调用时,调用的仍然是 Base::foo(),因为 Base::foo() 不是虚函数。