从Qt谈到C++(二):继承时的含参基类与初始化列表
提出疑问
当我们新建一个Qt的图形界面的工程时,我们可以看看它自动生成的框架代码,比如我们的主窗口名称为MainWindow,我们来看看mainwindow.cpp文件:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { }
不同于一般的继承,这里的父类的括号里带有参数,我们通常都使用过不带参数,甚至不带括号的基类名称。这里的基类为什么带着参数呢?
C++继承与构造函数
不能继承父类构造函数
C++中类的继承与Java中的不同,C++的派生类不能继承父类的构造函数和析构函数,只能继承父类的公有成员。所以这就会造成一种结果——我们无法使用基类的构造函数来对子类进行初始化!
类比Java
这里我假设大家都有Java的背景知识,没有也没关系,请听我讲。在Java中我们可以使用关键字super来直接调用父类的构造函数。比如我们定义两个类:Rectangle (矩形),Square (正方形)。从数学角度讲,正方形是一种特殊的矩形,所以我们的Square类继承自Rectangle类。在Java中像这样:
class Rectangle{ public Rectangle(int x,int y){ length = x; width = y; } public void area(){ System.out.println("The area is "+length*width); } private int length; private int width; } class Square extends Rectangle{//Java中使用extends关键字表示继承 public Square(int x) { super(x, x); } }在派生类Square的构造函数中我们使用了,super这一关键字,它会默认调用基类的构造函数来初始化派生类。所以它相当于用一个整型x来初始化长方形的长和宽,所以我们得到的是一个正方形。可以验证一下,我们再使用一个类来验证这个Square是否可用,关键代码如下:
public static void main(String[] args) { Square s = new Square(4); s.area(); }打印结果就是 The area is 16
C++实现方案
先依样画葫芦,写个C++版Rectangle:
class Rectangle { public: Rectangle(int x,int y); void area(); private: int length; int width; }; Rectangle::Rectangle(int x, int y) { length = x; width = y; } void Rectangle::area() { cout<<"The area is "<<length*width<<endl; }派生类的声明部分,我们也可以实现;
class Square:public Rectangle { public: Square(int x); };
这只是声明了Square的构造函数,但是我们该如何实现呢?我们的C++可是没有super这一关键字的,而且C++派生类不会继承基类的构造函数。
有人说我可不可以这样:
Square::Square(int x):Rectangle { Rectangle::Rectangle(x,x); }
答案当然是 NO!!。为了解决这一矛盾,C++提供了继承含参基类的实现方法,实现构造函数的方法就是这样:
Square::Square(int x):Rectangle(x,x) { }看懂没有,不要奇怪这个空函数体,我们所需要的初始化操作已经完成。在main函数中试试:
int main() { Square s(4); s.area(); return 0; }注意在声明部分就是和普通的继承声明是一样的,基类也不用加参。
By the Way,讲一下一个类的构造过程。
类的构造过程
- 首先,它的基类(如果有)的构造函数被调用。
- 然后,它的成员的构造函数被调用(如果有)。
- 最后,它自己的构造函数被调用。
当然了,这是题外话。
初始化列表
我们再回过头来看看,最初的那段Qt代码:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { }现在,前面的 QMainWindow(parent) 这部分我们刚才讲完了。接下来看看后面的
ui(new Ui::MainWindow)其实这就是构造函数的初始化列表。其实理解起来要容易的多。我们的类MainWindow有一个成员ui,它是MainWindow类型(这里ui的类型和它所处的类是相同的,这是特殊的情况,我们不用计较)。举个更一般的例子。
还记得我们刚才的矩形么?我们可以使用这种方法来初始化它。
Rectangle::Rectangle(int x, int y):length(x),width(y) { }效果等价于刚才的:
Rectangle::Rectangle(int x, int y) { length = x; width = y; }明白了吧,但是其实两种有点不同,那就是系统先调用初始化列表来初始化,接着会调用构造函数体内部的代码来初始化,也就是说,后者会覆盖掉前者。
Rectangle::Rectangle(int x, int y):length(x),width(y) { length = 2*x; width = 2*y; }然后main函数中:
int main() { Rectangle r(3,4); r.area(); return 0; }它的输出结果 是 The area is 48
顺便一提,在一个对象初始化的时候会先调用其基类的构造函数,其成员对象的构造函数,最后是自己的构造函数。而析构的时候顺序恰好相反。