C++之子类继承与父类构造
文章目录
1.问题引出
笔者最近在阅读godot源码,之前学过一些半吊子的C++,对一些东西知其然不知其所以然,希望在阅读项目源码的过程中思考一些问题。言归正传,在godot的初始启动流程中,有如下代码:
//启动流程 Main.cpp文件下
Main::setup(const char *execpath, int argc, char *argv[], bool p_second_phase)
{
OS::get_singleton()->initialize();
}
//OS::get_singleton()实现 os.cpp下
OS *OS::singleton = nullptr;
uint64_t OS::target_ticks = 0;
OS *OS::get_singleton() {
return singleton;
}
上述实现了一个单例模式,返回的是一个在os.cpp
文件下的全局变量OS *OS::singleton
。该全局变量初始化的时候是一个nullptr
,虽然空指针可以调用成员函数,但是这肯定不符合单例模式的设计理念。笔者最终找到了OS *OS::singleton
实际赋值的地方:
OS::OS() {
singleton = this;
Vector<Logger *> loggers;
loggers.push_back(memnew(StdLogger));
_set_logger(memnew(CompositeLogger(loggers)));
}
这里是OS
类的构造函数,当实例化一个OS类的时候,就会将实例化对象的指针赋值到singleton
指针上。笔者又在启动流程下寻找OS的实例化位置,最终找到代码如下:
OS_LinuxBSD os;
在程序最开始的main函数位置,实例化了os,实例化的类是OS_LinuxBSD
,并非OS
。阅读代码发现存在以下继承关系,OS_LinuxBSD
继承于OS_Unix
, OS_Unix
继承于OS
。
这里引出如下几个思考:
- 当子类继承父类,实例化子类的时候,父类构造的作用
- 链式继承的时候,父类构造的作用
- C++允许多继承,多个父类构造的作用
2.原则
此处部分参考了菜鸟教程,构造原则如下所示:
- 如果一个子类没有定义自己的构造函数,当实例化对象的时候,调用父类的无参构造方法
- 如果子类定义了构造函数,不论该构造函数有参还是无参,在实例化子类对象的时候,首先调用父类的无参构造函数,然后执行自己的构造方法
- 实例化子类对象的时候,如果子类的构造函数没有显式调用父类的构造函数,则会调用父类的默认无参构造函数。
- 在实例化子类对象的时候,如果子类的构造函数没有显式调用父类的构造函数,且父类自己提供了无参构造函数,则会调用父类自己的无参构造函数。
- 在实例化子类对象的时候,如果子类的构造函数没有显式调用父类的构造函数且父类只定义了自己的有参构造函数,则会报错;在该情况下,子类必须显示调用有参构造方法。
- 如果子类调用父类带参数的构造方法,需要用初始化父类成员对象的方式。
上诉规则其实可以用一个思维导图总结如下:
总之:当且仅当父类只有有参构造的时候,需要子类显式调用;如果父类存在无参构造,即使是默认无参,子类对象实例化的时候都会调用无参构造函数。
3.解析
3.1单一继承
3.1.1 父类无参构造函数
3.1.1.1子类无定义构造函数
#include <iostream>
#include <string>
class fruit
{
public:
int price;
std::string name;
public:
//父类无参构造
fruit()
{
std::cout << "class Fruit parameterless constructor" << std::endl;
}
};
class apple : fruit
{
public:
};
int main()
{
std::cout << "-----------------------------------------------" << std::endl;
apple a1;
std::cout << "-----------------------------------------------" << std::endl;
return 0;
}
上述代码里实现了父类fruit
和子类apple
,父类提供了无参构造,子类未实现任何构造函数,此时会默认存在一个无参构造函数,当我们实例化子类对象时,执行结果如下:
子类对象实例化时会默认调用父类的无参构造函数。
3.1.1.2子类定义构造函数
#include <iostream>
#include <string>
class fruit
{
public:
int price;
std::string name;
public:
//父类无参构造
fruit()
{
std::cout << "class Fruit parameterless constructor" << std::endl;
}
};
class apple : fruit
{
public:
//子类无参构造
apple()
{
std::cout << "class Apple parameterless constructor" << std::endl;
}
//子类有参构造
apple(int price)
{
this->price = price;
std::cout << "the price is: " << this->price << std::endl;
}
};
int main()
{
std::cout << "-----------------------------------------------" << std::endl;
apple a1;
std::cout << "-----------------------------------------------" << std::endl;
apple a2(10);
std::cout << "-----------------------------------------------" << std::endl;
return 0;
}
父类是fruit
,子类是apple
,父类实现了无参构造,子类实现了无参构造和有参构造,同时没有显式调用父类构造函数。
执行结果如下:
从执行结果来看,不管是子类的无参构造函数还是有参构造函数,都会首先调用父类的无参构造函数。
如果我们没有定义父类无参构造呢,实际上也会存在一个默认无参构造,它不做任何事情。
3.1.2 父类存在无参构造函数和有参构造函数
#include <iostream>
#include <string>
class fruit
{
public:
int price;
std::string name;
public:
//父类无参构造
fruit()
{
std::cout << "class Fruit parameterless constructor" << std::endl;
}
fruit(int price)
{
this->price = price;
std::cout << "[fruit] the price is: " << this->price << std::endl;
}
};
class apple : fruit
{
public:
//子类无参构造
apple()
{
std::cout << "class Apple parameterless constructor" << std::endl;
}
//子类有参构造
apple(int price)
{
this->price = price;
std::cout << "[apple] the price is: " << this->price << std::endl;
}
};
int main()
{
std::cout << "-----------------------------------------------" << std::endl;
apple a1;
std::cout << "-----------------------------------------------" << std::endl;
apple a2(10);
std::cout << "-----------------------------------------------" << std::endl;
return 0;
}
修改后的代码如上所示,我们为fruit
类实现了一个有参构造函数,依旧实例化两个子类对象,一个无参,一个有参,执行结果如下所示:
在子类没有显式调用的情况下,都会默认首先调用父类的无参构造,让后执行自己的对应的构造函数里的内容。
3.1.3 父类仅存在有参构造函数
#include <iostream>
#include <string>
class fruit
{
public:
int price;
std::string name;
public:
//父类无参构造
/*fruit()
{
std::cout << "class Fruit parameterless constructor" << std::endl;
}*/
fruit(int price)
{
this->price = price;
std::cout << "[fruit] the price is: " << this->price << std::endl;
}
};
class apple : fruit
{
public:
//子类无参构造
apple()
{
std::cout << "class Apple parameterless constructor" << std::endl;
}
//子类有参构造
apple(int price)
{
this->price = price;
std::cout << "[apple] the price is: " << this->price << std::endl;
}
};
int main()
{
std::cout << "-----------------------------------------------" << std::endl;
apple a1;
std::cout << "-----------------------------------------------" << std::endl;
apple a2(10);
std::cout << "-----------------------------------------------" << std::endl;
return 0;
}
注释掉父类的无参构造对象,子类无显式调用,编译,结果如下:
编译器会报错,指明没有匹配的函数调用。
我们将子类构造修改为显式调用,因为无参构造函数没有参数提供,我们这里给一个默认参数:
#include <iostream>
#include <string>
class fruit
{
public:
int price;
std::string name;
public:
//父类无参构造
/*fruit()
{
std::cout << "class Fruit parameterless constructor" << std::endl;
}*/
fruit(int price)
{
this->price = price;
std::cout << "[fruit] the price is: " << this->price << std::endl;
}
};
class apple : fruit
{
public:
//子类无参构造
apple():fruit(15)
{
std::cout << "class Apple parameterless constructor" << std::endl;
}
//子类有参构造
apple(int price):fruit(price)
{
this->price = price;
std::cout << "[apple] the price is: " << this->price << std::endl;
}
};
int main()
{
std::cout << "-----------------------------------------------" << std::endl;
apple a1;
std::cout << "-----------------------------------------------" << std::endl;
apple a2(10);
std::cout << "-----------------------------------------------" << std::endl;
return 0;
}
对于子类来说,想要显式调用父类构造,只要在对应的构造函数后加上冒号然后使用父类构造函数即可。我们将子类的默认无参构造函数的调用参数设置为15,在实例化a2的时候,使用的还是子类有参构造函数里的参数,执行结果如下所示:
我们看到两个实例化对象都会调用对应的父类有参构造函数,同时使用子类无参构造的实例化对象的价格,也被设置为了子类无参构造函数调用父类有参构造函数时的参数。
3.2链式继承
我们基本理清楚了一件事情,如果父类存在无参构造函数,那么子类不显式调用时,会默认首先调用父类无参构造;如果父类不存在无参构造,则子类必须显式调用父类有参构造,否则编译器会编译不过。接下来我们将场景设置复杂一点,设置一个链式继承关系,就像我开头的案例一样,代码如下:
#include <iostream>
#include <string>
class fruit
{
public:
int price;
std::string name;
public:
//父类无参构造
fruit()
{
std::cout << "class Fruit parameterless constructor" << std::endl;
}
fruit(int price)
{
this->price = price;
std::cout << "[fruit] the price is: " << this->price << std::endl;
}
};
class apple : public fruit
{
public:
//子类无参构造
apple()
{
std::cout << "class Apple parameterless constructor" << std::endl;
}
//子类有参构造
apple(int price):fruit(price)
{
this->price = price;
std::cout << "[apple] the price is: " << this->price << std::endl;
}
};
class green_apple : public apple
{
public:
green_apple()
{
std::cout << "class green_Apple parameterless constructor" << std::endl;
}
green_apple(int price):apple(price)
{
std::cout << "[green_Apple] the price is: " << this->price << std::endl;
}
};
int main()
{
std::cout << "-----------------------------------------------" << std::endl;
green_apple a1;
std::cout << "-----------------------------------------------" << std::endl;
green_apple a2(10);
std::cout << "-----------------------------------------------" << std::endl;
return 0;
}
我们定义了第三个类型green_apple
继承于apple
,分别实现了有参和无参构造,最后在实例化的时候分别实现了无参和有参的实例化,执行结果如下:
显而易见,会最先调用最基类的构造函数,之后逐层调用。其实按照理解来说,应该可以说是不显式调用的时候,会自动塞一个父类无参构造函数给子类构造函数,如果父类没有无参构造,对于编译器来说,就不知道如何去放置有参构造函数了,所以需要我们显式调用,自己去指定调用哪一个父类构造函数。当子类调用构造函数时,会先调用父类的构造函数,当父类也继承于另一个类时,父类构造又会先调用另一个类的构造,逐层递归,最终表现为最先调用最基类的构造函数。
3.3多继承
基于前面的分析,我们其实已经可以推测出来多继承的逻辑了,其一定是按照顺序分别调用父类的构造函数,设计代码如下:
#include <iostream>
#include <string>
class fruit
{
public:
int price;
std::string name;
public:
//父类无参构造
fruit()
{
std::cout << "class Fruit parameterless constructor" << std::endl;
}
fruit(int price)
{
this->price = price;
std::cout << "[fruit] the price is: " << this->price << std::endl;
}
};
class season
{
public:
//父类无参构造
season()
{
std::cout << "class season parameterless constructor" << std::endl;
}
};
class apple : public fruit, public season
{
public:
//子类无参构造
apple()
{
std::cout << "class Apple parameterless constructor" << std::endl;
}
//子类有参构造
apple(int price):fruit(price)
{
this->price = price;
std::cout << "[apple] the price is: " << this->price << std::endl;
}
};
int main()
{
std::cout << "-----------------------------------------------" << std::endl;
apple a1;
std::cout << "-----------------------------------------------" << std::endl;
apple a2(10);
std::cout << "-----------------------------------------------" << std::endl;
return 0;
}
实现了另一个父类season
,apple
同时继承于fruit
和season
,代码执行结果如下:
无参实例化对象分别调用了fruit
和season
的无参构造函数。有参实例化对象先显式调用了fruit
的有参构造函数,之后调用了season
的无参构造函数,最后执行自己的有参构造函数部分。
4.结论
- 子类会在构造函数中默认调用父类的无参构造函数
- 父类不存在无参构造函数时,子类需要指定调用的父类有参构造函数
- 链式继承时会先调用父类的构造函数,如果父类还存在父类,则父类构造会先调用父类的父类的构造函数,依此递归,最终的结果是按照辈分逐级执行构造
- 多继承时按照继承顺序逐步调用父类构造函数
5.回到开头
由此回答我开头引入的问题,OS_LinuxBSD
创建时调用了父类的父类OS
的无参构造函数,将单例的指针设置为了自己。