2022.10.5实训笔记

169 阅读5分钟

类型兼容

在需要基类对象的任何地方,都可以使用公有派生类的对象来代替。通过公有派生,派生类得到了除构造函数,析构函数,私有成员之外的所有成员。这种规则体现在:

派生类的对象可以隐含转换为基类对象

派生类的对象可以初始化基类的引用

派生类的指针可以隐含转换为为基类的指针

class A {

};

class B: public A {

};

int main(){
    A a, * pa;
    B b;
    a = b;
    A& ra = b;
    pa = &b;
}

虽然根据类型兼容原则,可以将派生类对象的地址赋值给基类的指针,但是通过这个基类类型的指针,却只能访问到从基类继承的成员,而不是派生类自己的同样的成员函数。 

由于类型兼容的特性存在,可以在基类对象出现的场合使用派生类对象进行替换,但是替换之后派生类仅仅能发挥出基类的作用。在后续章节,将学习另一个重要特性--多态。多态的设计方法可以保证在类型兼容的前提下,基类和派生类的对象,分别以不同的方式响应相同的消息。

派生类的构造和析构函数

继承的目的是为了发展,派生类继承了基类的成员,实现了原有代码的重用,这只是一个目的,而代码的扩充才是最主要的,只有通过添加新的成员,加入新的功能,类的派生才有实际意义。

由于基类的构造函数和析构函数不能被继承,在派生类中,如果对派生类新增的成员进行初始化,就必须为派生类添加新的构造函数。但是派生类的构造函数只负责对派生类的新增成员进行初始化,对所有从基类继承来的成员,其初始化工作还是由基类的构造函数完成。同样,对派生类对象的清理工作也需要加入新的析构函数。

构造函数

派生类对于基类的很多成员(私有)是不能直接访问的,因此要完成对基类成员的初始化工作,需要调用基类的构造方法。派生类的构造函数需要以合适的初值作为参数,其中一些参数要传递给基类的构造函数,用于初始化相应的成员,另一些参数要用于派生类新增成员的初始化。

在构造派生类对象时,会首先调用基类的构造函数,来初始化它们的数据成员,然后按照构造函数初始化列表中指定的方式初始化派生类新增的成员,最后再执行派生数构造函数的函数体。

什么时候需要声明派生类的构造函数?如果对基类初始化时,需要调用基类的有参构造函数时,派生类就必须声明构造方法 提供一个将参数传递给基类构造函数的途径,保证在基类对象初如化时能获得必要的数据。如果不需要调用基类带参构造方法,也不需要调用新增成员对象的带参构造方法,派生类也可以不声明构造函数。全部采用默认构造函数。

派生类构造函数执行的一般次序
1.  如果参数是类类型,调用复制构造函数,把实参值复制到形参中
1.  调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左到右)
1.  对派生类的新增成员对象初始化,调用顺序按照他们在类中的声明顺序(根据情况,调用构造函数或复制构造函数)
1.  执行派生类的构造函数体
class Base0 {
public:
    Base0(int var):var0(var){
        printf("Base0 invoked!\n");
    }
private:
    int var0;
};

class Base1 {
public:
    Base1(int var):var1(var){
        printf("Base1 invoked!\n");
    }
private:
    int var1;
};

class Point {
public:
    Point(int x, int y):x(x),y(y){
        printf("Point Constructor...\n");
    }
    Point (const Point& p){
        this->x = p.x;
        this->y = p.y;
        printf("Point Copy Constructor...\n");
    }
    ~Point(){
        printf("Point Destructor, addr=%x\n", this);
    }
    void show(){
        printf("Point[x=%d,y=%d,addr=%x]\n", x, y, this);
    }
private:
    int x, y;
};

class Derived : public Base0, public Base1 {
public:
    Derived(Point p1, Point p2):Base0(1), Base1(2), p1(p1),p2(p2){
        printf("Derived Constructor, p1.addr=%x, p2.addr=%x\n", &p1, &p2);
    }

    void show(){
        printf("Derived[p1.addr=%x, p2.addr=%x]\n", &p1, &p2);
    }
public:
    Point p1, p2;
};

int main() {
    Point p1(1, 1);
    p1.show();
    Point p2(2, 2);
    p2.show();
    Derived d(p1, p2);
    d.show();

    return 0;
}

//输出
Point Constructor...
Point[x=1,y=1,addr=f544fec8]
Point Constructor...
Point[x=2,y=2,addr=f544fec0]
Point Copy Constructor...
Point Copy Constructor...
Base0 invoked!
Base1 invoked!
Point Copy Constructor...
Point Copy Constructor...
Derived Constructor, p1.addr=f544fed8, p2.addr=f544fed0
Point Destructor, addr=f544fed8
Point Destructor, addr=f544fed0
Derived[p1.addr=f544fea8, p2.addr=f544feb0]
Point Destructor, addr=f544feb0
Point Destructor, addr=f544fea8
Point Destructor, addr=f544fec0
Point Destructor, addr=f544fec8

析构函数

在派生过程中,基类的析构函数也不能继承下来,如果需要在析构的时候做一些事的话,就要在派生类中声明新的析构函数。它的任务是清理好派生类中新增的非对象成员(比如指针)。析构函数的调用次序与构造函数的调用次序正好相反。

class Human {
public:
    Human(){
        printf("Human Constructor ....\n");
    }
    ~Human(){
        printf("Human Destructor ....\n");
    }
};

class Student : public Human {
public:
    Student(){
        printf("Student Constructor ....\n");
    }
    ~Student(){
        printf("Student Destructor ....\n");
    }
};

int main() {
    Student s1, s2;
    return 0;
}

//输出
Human Constructor ....
Student Constructor ....
Human Constructor ....
Student Constructor ....
Student Destructor ....
Human Destructor ....
Student Destructor ....
Human Destructor ....