NDK开发---C++学习(六):继承、多态

字数 3223阅读 111

前言

前面我们已经介绍过了C++中的类与函数,不熟悉的,可以去看看
NDK开发---C++学习(三):类与函数(上)
NDK开发---C++学习(四):类与函数(中)
NDK开发---C++学习(五):类与函数(下)
本篇将介绍C++中的继承(多继承、虚拟继承)、多态、虚函数等。

继承

继承代码重用的基本工具,是类的一个重要特征。它克服了面向过程程序设计语言没有软件复用语言机制的缺点,使软件复用变得简单、易行,可以通过继承复用已有的程序资源,提高软件开发的效率,缩短软件开发的周期。
继承是在已知类的基础上创建新类的过程,已有类称为基类,新类称为派生类。派生类不仅能够继承基类的功能,而且还能够对基类的功能进行补充、修改或重定义。
C++的继承可分为公有继承、保护继承和私有继承,也称为公有派生、保护派生和私有派生。在C++中,继承的语法形式如下(下文中关于基类和派生类的,一律采用父类和子类去替代。):

class 子类类名:[继承方式] 父类类名 {
    子类成员声明与定义;
}

其中,继承方式可以是public、protected、private,分别对应公有继承、保护继承和私有继承。如果省略继承方式,C++默认为private继承。
相信作为Android开发的你,对于继承,应该是了然于胸,那么接下来我们来看看C++继承的一个简单例子

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Human {
protected:
    char* name;
    int age;
public:
    void say() {
        cout << "说话" << endl;
    }
};

class Man : public Human {
private:
    char* brother;
public:
    void chasing() {
        cout << "泡妞" << endl;
    }
};

void work(Human &h) {
    h.say();
}

void main() {
    Man m;
    m.say();    //说话

    //1.父类类型的引用或指针
    Human* h_p = &m;
    h_p->say();

    Human &h1 = m;
    h1.say();

    work(m);

    //子类对象初始化父类类型的对象
    Human h2 = m;

    getchar();
}

在C++中没有extend,取而代之的是:,可以调用父类的方法

Man m;
m.say();    

父类类型的引用或指针指向子类对象

Human* h_p = &m;
h_p->say();

Human &h1 = m;
h1.say();

子类对象初始化父类类型的对象

Human h2 = m;

1. 公有继承

继承方式为public的继承称为公有继承。

1. 公有继承不改变父类成员在子类中的访问权限。在公有继承方式下,父类中的public、private和protected成员在子类中保持它们在基类中相同的访问权限。
2. 在子类中定义的成员函数不能直接访问父类的私有成员,只能通过父类的public成员或protected成员访问它们。

2. 私有继承

继承方式为private的继承称为私有继承。

1. 在私有继承方式下,父类的public和protected成员在子类中都变成了private成员,不再是子类的公有接口函数,不能被父类的外部函数访问。
2. 虽然是父类的public和protected成员在子类中都变成了private成员,但它们与父类本身的private成员是有区别的,它们可被子类的成员函数直接访问,而父类中的private成员不能被子类直接访问。

3. 保护继承

继承方式为protected的继承称为保护继承。

在保护继承方式下,父类的public成员在子类中的访问权限被修改为protected权限,父类的protected成员在子类中仍为protected成员,父类的private成员在子类中仍为private成员。

向父类构造函数传参

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Human {
protected:
    char* name;
    int age;

public:
    Human(char* name, int age) {
        this->name = name;
        this->age = age;
    }
    void say() {
        cout << "说话" << endl;
    }
};

class Man : public Human {
public:
    void chasing() {
        cout << "泡妞" << endl;
    }
private:
    char* brother;
};

void main() {
    Man m;

    getchar();
}

运行报错,Man没有合适的构造函数,这是为什么呢?明明默认是有无参构造函数的啊。因为Man继承于HumanHuman只有一个有参构造参数,看过我之前的博客的同学肯定知道,只要有有参构造函数,就会覆盖无参构造函数。所以Man的无参构造函数也已经被覆盖了。
我们还可以这样理解:子类的构造函数除了要负责本类成员的初始化外,还要调用父类和成员对象的构造函数,并向它们传递参数,以完成父类子对象和成员对象的建立和初始化,而此例中的子类Man的无参构造显然不能完成上述操作,需要一个有参构造函数来完成父类Human的初始化。
那么针对这个问题,我们可以这样做,给Man添加一个有参构造函数

Man(char* brother, char* h_name, int h_age) : Human(h_name, h_age){
        this->brother = brother;
    }
void main() {
    Man m("jack", "john", 23);

    getchar();
}

编译运行通过。
看到这里大家是不是觉得特别像之前我介绍的构造函数的成员初始化列表NDK开发---C++学习(四):类与函数(中),不熟悉的,我已插眼,可以随时TP。
我们在Man类中添加Human对象属性,去复习下构造函数的成员初始化列表

class Man : public Human {
private:
    char* brother;
    Human h1;

public:
    Man(char* brother, char* h_name, int h_age, char* s_name, int s_age) : Human(h_name, h_age), h1(s_name, s_age){
        this->brother = brother;
    }
    void chasing() {
        cout << "泡妞" << endl;
    }
};

void main() {
    Man m("jack", "john", 23, "rose", 25);

    getchar();
}

构造函数与析构函数调用顺序

#include<iostream>
using namespace std;
class Human {
protected:
    char* name;
    int age;

public:
    Human(char* name, int age) {
        this->name = name;
        this->age = age;
        cout << "父类构造函数" << endl;
    }

    ~Human() {
        cout << "父类析构函数" << endl;
    }

    void say() {
        cout << "说话" << endl;
    }
};

class Man : public Human {
private:
    char* brother;

public:
    //给父类构造函数传参
    Man(char* brother, char* h_name, int h_age) : Human(h_name, h_age) {
        this->brother = brother;
        cout << "子类构造函数" << endl;
    }

    ~Man() {
        cout << "子类析构函数" << endl;
    }

    void chasing() {
        cout << "泡妞" << endl;
    }
};

void func() {
    Man m("jack", "john", 23);
}

void main() {
    func();

    getchar();
}

运行打印结果为:

父类构造函数
子类构造函数
子类析构函数
父类析构函数

也就是父类构造函数先调用,子类析构函数先调用。

子类对象调用父类的成员

子类不仅可以添加父类没有的新成员,而且还可以对父类的成员函数进行重定义(类似于Java中的重写)或重载(类似于Java中的重载)。
重定义是指子类可以定义与父类具有相同函数原型的成员函数(即具有相同的函数名、参数列表和返回值类型),而重载则要求成员函数名相同、参数列表不同,与返回值类型无关。
需要指出的是:子类对父类成员函数的重定义或重载会影响父类成员函数在子类中的可见性,父类的同名成员函数会被子类重载的同名函数所隐藏

using namespace std;
class Human {
public:
    char* name;
    int age;

public:
    Human(char* name, int age) {
        this->name = name;
        this->age = age;
        cout << "父类构造函数" << endl;
    }

    ~Human() {
        cout << "父类析构函数" << endl;
    }

    void say() {
        cout << "Human说话" << endl;
    }
};

class Man : public Human {
private:
    char* brother;

public:
    //给父类构造函数传参
    Man(char* brother, char* h_name, int h_age) : Human(h_name, h_age) {
        this->brother = brother;
        cout << "子类构造函数" << endl;
    }

    ~Man() {
        cout << "子类析构函数" << endl;
    }

    void chasing() {
        cout << "泡妞" << endl;
    }

    void say() {
        cout << "Man说话" << endl;
    }
};

void main() {
    Man m("jack", "john", 23);
    m.say();

    getchar();
}

运行打印输出

Man说话

很明显m调用say函数的时候,找的是子类的say函数,因为子类的say函数重定义了父类中的say函数,父类的say函数隐藏,如果我们想要调用父类的say函数的话,该怎样去做呢?别着急,让我逐一叙述:

m.Human::say();

这样调用的就是父类的say函数,当然我们还可以修改父类属性的值

m.Human::age = 10;

多继承

C++允许一个类从一个或多个父类派生。如果一个类只有一个父类,就称为单继承;如果一个类具有两个以上的父类,就称为多继承。多继承的形式如下:

class 子类类名:[继承方式] 父类类名1,[继承方式] 父类类名2,... {
    子类成员声明或定义;
}

其中,继承方式可以是private、protected、private,分别对应公有继承、保护继承和私有继承,其含义与单继承相同。
C++在解析子类的成员函数调用时,按照以下次序查找成员函数所属的类。

1. 在子类中查找该函数,若找到就确定该函数是子类的成员函数。
2. 如果在子类中没有找到该成员函数,就在父类中查找该成员函数。

多继承方式下的二义性

在多继承中,子类继承了多个父类的成员,当两个不同父类拥有同名成员时,容易产生命名冲突问题。

class A {
public:
    char* name;
};

class B1 : public A {

};

class B2 : public A {

};

class C : public B1, public B2 {

};

如果有下面的成员引用:

C c;
c.name = "john";        //错误,二义性冲突

C类中拥有A类数据成员和成员函数的两份备份,在引用来源于A的成员时容易产生二义性
对于成员属性name的调用,编译器首先在C类中查找,结果没有找到,接下来编译器会在C的父类中查找,但在B1B2的两个父类中都找到了name,编译器不能确定调用哪个父类的name,因此产生二义性的命名冲突。
解决上述二义性命名冲突的办法是指明成员调用所属的类。例如,可以这样:

C c;
//指定父类显示调用
c.B1::name = "john";    //正确

但这样的调用方式并未解决本质问题,在同一个对象c中存在A的两份不同数据成员,不仅浪费存储空间,而且还容易产生数据的不一致性。为了解决这类问题,C++引用了虚拟继承。

虚拟继承

利用C++提供的关键字virtual限定继承方式,将公共父类指定为虚父类,就可以使该父类的成员在子类中只有一份备份。虚父类的定义形式如下:

class 子类类名 : virtual [继承方式] 父类类名1, virtual [继承方式] 父类类名2, ... {
    子类成员声明或定义;
}

针对上述例子

class A {
public:
    char* name;
};

class B1 : virtual public A {

};

class B2 : virtual public A {

};

class C : public B1, public B2 {

};

void main() {
    C c;
    c.name = "john";        

    getchar();
}

通过对公共父类的虚拟继承,子类只保留了虚父类的一份数据成员备份,现在通过子类对象引用虚父类中的成员就不会产生二义性的命名冲突了。

虚函数

我们先看一个例子:
创建一个头文件Plane.h

#pragma once
//普通飞机
class Plane {
public:
    void fly();
    void land();
};

创建cpp文件Plane.cpp

#include "Plane.h"
#include <iostream>
using namespace std;
void Plane::fly() {
    cout << "普通飞机起飞" << endl;
}

void Plane::land() {
    cout << "普通飞机着陆" << endl;
}

再创建一个头文件Helicopter.h

#pragma once
#include"Plane.h"
//直升飞机
class Helicopter : public Plane {
public:
    void fly();
    void land();
};

创建cpp文件Helicopter.cpp

#include "Plane.h"
#include "Helicopter.h"
#include <iostream>
using namespace std;
void Helicopter::fly() {
    cout << "直升飞机起飞" << endl;
}

void Helicopter::land() {
    cout << "直升飞机着陆" << endl;
}

业务逻辑

#include "Plane.h"
#include "Helicopter.h"
void bizPlay(Plane &p) {
    p.fly();
    p.land();
}

void main() {
    Plane p1;
    bizPlay(p1);    

    Helicopter p2;
    bizPlay(p2);    
                    
    getchar();
}

运行打印结果为:

普通飞机起飞
普通飞机着陆
普通飞机起飞
普通飞机着陆

这显然不是我们想要看见的,我们想要在调用bizPlay(p2)调用子类的函数。C++给出了一种更好的解决方案——虚函数
只有类的成员函数才能被定义为虚函数,不属于任何类的普通函数不能被定义成虚函数。虚函数的运行机制可以概括如下:如果父类中的非静态成员函数被定义为虚函数,且子类重写了父类的虚函数,则通过指向父类对象的指针或引用调用子类对象中的虚函数时,就会调用到该指针(或引用)实际所指对象的成员函数。
虚函数的定义方法非常简单,把限定词virtual加在类成员函数的声明前面,就将此函数指定为虚函数了,形式如下:

class X {
    virtual 返回值类型 函数名(参数表);
}

virtual的意义在于指示编译器,对这类函数采取迟后联编(动态绑定)(关于多态与联编这方面的知识可闪现到文末进行了解)的方法,在程序运行过程中才确定与之相对应的函数。而没有用virtual限定的函数则采用早期联编(静态绑定)的方式,在编译过程中就把函数调用与它的函数实现关联起来。

#pragma once
//普通飞机
class Plane {
public:
    virtual void fly();
    virtual void land();
};
#pragma once
#include"Plane.h"
//直升飞机
class Helicopter : public Plane {
public:
    void fly();
    void land();
};

再次运行打印结果为:

普通飞机起飞
普通飞机着陆
直升飞机起飞
直升飞机着陆

虚函数的特性:

一旦将某个类成员函数声明为虚函数后,该成员函数在子类的继承体系中就永远为虚函数了。即使子类在重写该函数时并没有将它声明为虚函数,它仍然是虚函数。

发生动态多态的条件:

1. 继承
2. 父类的引用或者指针指向子类的对象
3. 函数的重写

纯虚函数

纯虚函数是指在声明时被初始化为0的虚类成员函数。纯虚函数在父类中声明,但它在父类中没有具体的函数实现代码,要求继承它的子类为纯虚函数提供实现代码。
纯虚函数的声明形式如下:

class X {
    virtual 返回值类型 函数名(参数列表) = 0;
}

接着我们来看一个例子:

//形状
class Shape {
    //纯虚函数
    virtual void sayArea() = 0;
};

//圆
class Circle : public Shape {
private:
    int r;
public: 
    Circle(int r) {
        this->r = r;
    }
};

void main() {
    Circle c(10);    //错误

    getchar();
}

上述代码中Circle c(10)是错误的,因为Circle继承Shape这个抽象类了,必须重写里面的纯虚函数。

//形状
using namespace std;
class Shape {
    //纯虚函数
    virtual void sayArea() = 0;
};

//圆
class Circle : public Shape {
private:
    int r;
public: 
    Circle(int r) {
        this->r = r;
    }

    void sayArea() {
        cout << (3.14 * r * r) << endl;
    }
};

void main() {
    Circle c(10);

    getchar();
}

重写之后就不存在问题了。

接口

class Drawable {
    virtual void draw() = 0;
};

多态与联编

可能很多人还不清楚什么是多态和联编,下面我将逐一介绍:
多态就是指不同对象收到相同消息时会执行不同的操作。通俗地讲,就是用一个相同的名字定义许多不同的函数,这些函数可以针对不同数据类型实现相同或相似的功能,即所谓的“一个接口,多种实现”。
C++中的多态性与联编这一概念密切相关。一个源程序需要经过编译、连接才能形成可执行文件,在这个过程中要把调用函数名与对应函数关联在一起,这个过程就是绑定,又称联编。
联编又分为静态联编和动态联编,静态联编(静态绑定)是指在程序执行前,编译器根据函数调用提供的信息,在程序编译时就把调用函数名与具体函数绑定在一起。
动态联编(动态绑定)是指在程序编译时还不能确定函数调用所对应的具体函数,只有在程序运行过程中根据具体的数据类型才能够确定函数调用所对应的具体函数,即在程序运行时才把函数名与具体函数绑定在一起。
静态联编和动态联编都能够实现多态性,采用静态联编实现的多态就称为静态多态性,采用动态联编实现的多态就称为动态多态性。静态多态性是通过函数重载和运算符重载在程序编译时通过静态绑定实现的。动态多态性是通过继承和虚函数在程序执行时通过动态绑定实现的。平常所说的面向对象程序设计的多态性,常指运行时的多态性。
静态多态性在编译时就确定了调用的具体函数,不需要在执行程序时从多个同名函数中匹配调用函数,所以执行速度快。而动态多态性需要在执行程序时从多个函数中匹配调用函数,所以它比静态多态性的执行效率低,但它提供了更多的灵活性、问题的抽象性和程序的可维护性。

展望

关于C++的知识目前已经基本介绍完了,接下来将介绍NDK相关的技术文章,敬请期待!
喜欢本篇博客的简友们,就请来一波点赞,您的每一次关注,将成为我前进的动力,谢谢!

推荐阅读更多精彩内容