「BUAA-C++」Lec5:继承和组合


C++中有两种实现代码重用(reuse的方式——继承(inheritance)和组合(composition

继承inheritance

继承:共性和特性的关系——

  • 子类拥有父类所有数据成员和函数,这是所有子类的共性。
  • 子类可以父类实现的函数进行重写, 成为子类自己的特性。(一代更比一代强)

通常情况下,一旦在子类中对父类函数进行了改写,在子类中的该函数中通常会以某种方式调用了父类的同名函数。相反,如果子类和父类的同名函数没有任何关系,那么“继承”则没有任何意义。

继承的语法如下所示

class [子类名]public [父类名] {}

父类名前通常使用关键字public来修饰。当然使用private在语法上是没有任何问题的,但是这样的话继承方式就会变成私有继承。如果是私有继承,则子类中无法使用父类的public方法。
私有继承的存在仅仅是为了维护语法完整性,没有实质性的作用

重载函数的重写

如果父类中有重载函数,且子类想要对其重写,则它应该重写父类所有的重载函数。

class Base {
public:
    void func() {cout << "base func()" << endl;};
    void func(int i) {cout << "base func(int)" << endl;};
};

class Dervied : public Base {
public:
    void func() {cout << "dervied func()" << endl;};
    void func(int i) {cout << "dervied func(int)" << endl;};
}

构造函数的调用问题

在调用子类构造函数时,会默认调用父类的无参构造函数调用顺序是—— 先父类,后子类。(在调用子类析构函数时,顺序是先子类,后父类

//父类
class Base {
private:
    int i;
public:
    Base();
};

Base::Base() {
    cout << "constructor of Base" << endl;
}

//子类
class Dervied : public Base {
private:
    int j;
public:
    Dervied();
};

Dervied::Dervied() {
    cout << "constructor of Derived" << endl;
}
//输出为
//constructor of Base
//constructor of Derived

但是,如果此时父类中只有有参构造函数,则需要在子类构造函数中显式地调用父类构造函数。显式调用的语法为——Dervied::Dervied(...) : Base(...)
//父类
class Base {
private:
    int i;
public:
    Base(int i);
};

Base::Base(int i) {
    this->i = i;
    cout << "constructor of Base" << endl;
}

//子类
class Dervied : public Base {
private:
    int j;
public:
    Dervied(int i, int j);
};

Dervied::Dervied(int i, int j) : Base(i){
    this->j = j;
    cout << "constructor of Derived" << endl;
}

多重继承问题

和Java不同,C++是允许使用多重继承的,即 “一个儿子可以有多个父亲” 。多重继承的语法为——

class [子类名]public [父类名], public [父类名];

子类同样可以使用所有父类中的所有public函数和所有public成员变量, 如下所示
class Base1 {
public:
    void f();
};

class Base2 {
public:
    void g();
};

class Derived : public Base1, public Base2 {

};

int main() {
    Derived d;
    d.f(); //调用父类Base1中的f()函数
    d.g(); //调用父类Base2中的g()函数
}

但是如果两个父类有同名的函数,则在调用该函数时编译器会报错。
class Base1 {
public:
    void f();
    void g();
};

class Base2 {
    void g();
};

class Derived : public Base1, public Base2 {
};

int main() {
    Derived d;
    d.f(); 
    d.g(); //error!!!
}

当然这个问题也是能解决的。我们可以用组合消除多重继承,即可以将Base2作为子类的一个组成部分, 然后在子类中重写父类Base1g()函数,重写后的函数内容为——调用数据成员Base2g()函数。
class Derived : public Base1{
private: 
    Base2 base2;
public:
    void g();
};

void Derived::g(); {
    base2.g();

}

注意:data member不一定是属性。在上一个例子中,base2Deriveddata member,但是,我们只是用它来访问Base2类中的g()函数。因此base2不能称作Derived的属性!

组合composition

组合其实描述的就是在一个类里内嵌了其他类的对象作为成员的情况,它们之间的关系是一种包含与被包含的关系。在下面这个例子中,发动机类(Engine)作为汽车类(Car)的一个data member,他们之间的关系就是组合。

class Engine {
private:
    int power;
public:
    Engine();
};
Engine::Engine() {
    cout << "constructor of Engine" << endl;
}


class Car {
private:
    Engine engine; //engine成为Car类中的data member
public:
    Car();
};

Car::Car() {
    cout << "constructor of Car" << endl;
}

构造函数的调用问题

组合中构造函数的调用和继承相似,即当Car的构造函数被调用时,Engine的构造函数也会被调用(默认调用无参构造函数)。这一点也很显然,我们想要造一辆汽车时,作为其组成部分的Engine当然也会被造出来,否则汽车就无法被开动。

在调用顺序上,同样是被包含的类的构造函数首先被调用,如下所示——

int main() {
    Car car;
    return 0;
}
//输出:
//constructor of Engine
//constructor of Car

但是,如果此时被包含的类中只有含参构造函数,则需要在外层类显式地调用被包含类的构造函数。调用的语法和继承中的也很相似,可以从下面的例子看出——
class Engine {
    int part;
public:
    Engine(int power);
};
Engine::Engine(int part) {
    cout << "constructor of Engine" << endl;
}

class Car {
private:
    Engine engine;
public:
    Car(int power);
};

Car::Car(int power) : engine(power){
    cout << "constructor of Car" << endl;
}

构造初始化列表

上面被包含的类的对象的初始化方式同样适用于int等基本类型,这种方式叫做构造初始化列表,具体方式是"在构造函数后以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式"。

class Student() {
    int id;
    int grade;
public:
    Student(int, int);
}

Student::Student(int id, int grade) : id(id), grade(grade) {

}

因为const对象或引用类型只能初始化,不能二次赋值,也不能在构造函数中进行赋值,所以他们只能通过构造初始化列表的方式进行赋初值(初始化)

需要注意的是,初始化顺序与构造初始化列表中的顺序无关,只与类中变量被定义的顺序有关。在下面这个例子中,无论构造初始化列表中的顺序怎样,首先被初始化的都是i,而因为用来初始化ij是未定义的,所以最后i的值一个 “垃圾数”

class Test {
public:
    int i;
    int j;
    Test(int a);
};

Test::Test(int a) : i(j), j(a) { //把j(a)放在i(j)前面时结果也是一样的

}

int main() {
    Test t(1);
    return 0;
}


文章作者: Hyggge
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Hyggge !
  目录