前言

在上一则教程中,叙述了关于C++类型转换的相关内容,在本节教程中,将叙述 C++的另一个内容,也就是抽象,这也是 C++相对于 C语言来说独特的一点,下面我们就来着重叙述这一点。

 

纯虚函数

在介绍抽象类之前,需要弄明白何为纯虚函数,下面假定我们有这样一个需求:

 

做一个“各个国家的人的调查”,调查各个国家的人的:饮食、穿衣、开车

 

要完成这样一个事情,那我们现在就需要实现这样几个类,一个是 Human类,其他国家的人从 Human类里派生而来,就比如说是ChineseEnglishman,我们再回过头来想,我们所要实现的需求是调查各个国家的人,那么这个过程中,由Human类派生得到 Chinese和 Englishman,那么在实例化对象的时候,我们实际上是不会用到Human类去定义一个对象的,考虑到这层因素,我们在 Human类里使用到了纯虚函数的概念,类实现的代码如下所示:

 

class Human
{
private:
    int a;
public:
    /*纯虚函数*/
    virtual void eating(void) = 0;
    virtual void wearing(void) = 0;
    virtual void driving(void) = 0;
};

class Englishman : public Human
{
public:
    void eating(void)  { cout<<"use knife to eat"<    void wearing(void) {cout<<"wear english style"<    void driving(void) {cout<<"drive english car"<};

class Chinese : public Human 
{
public:
    void eating(void)  { cout<<"use chopsticks to eat"<    void wearing(void) {cout<<"wear chinese style"<    void driving(void) {cout<<"drive chinese car"<};

 

我们可以看到在上述的代码中,Human类的成员函数跟前几讲所写的成员函数有所不同,而如上述 Human类的成员函数这般写法,也就被称之为是纯虚函数。

 

抽象类

上述引出了纯虚函数的写法,那纯虚函数和抽象类之间有什么关系呢?实际上,抽象类就是具有纯虚函数的类,那这抽象类存在的意义是什么呢?总的来说,其作用也就是:向下定义好框架,向上提供统一的接口,其不能够实例化对象,基于上述几个类的前提下,我们编写主函数的代码:

 

int main(int argc,char **argv)
{
    Human h;

    return 0;
}

 

因为抽象类不能够实例化对象,所以上述代码编译结果是错误的,错误信息如下所示:

 

 

而使用通过抽象类派生得到的派生类实例化对象是可行的,代码如下所示:

 

int main(int argc, char** argv)
{
    Englishman e;
    Chinese    g;

    return 0;
}

 

另外需要注意的是:在派生抽象类的过程中,如果派生得到的子类没有覆写所有的纯虚函数,那么这个子类还是抽象类,比如有如下所示的代码,Human类沿用的是上述的写法,代码不变,如果我们将上述的 Chinese类进行更改,更改后的代码如下所示:

 

class Chinese : public Human 
{
public:
    void eating(void) { cout<<"use chopsticks to eat"<    void wearing(void) {cout<<"wear chinese style"<    //void driving(void) {cout<<"drive chinese car"<};

 

如上述代码所示,我们将 driving()函数注释掉了,那么也就是说,我们并没有将抽象类的全部纯虚函数进行覆写,那么当前这个Chinese类也是一个抽象类,也是不能够进行实例化对象的,要使得 Chinese类有作用,我们必须派生出来另一个类,代码如下所示:

 

class Guangximan : public Chinese 
{
    void driving(void) {cout<<"drive guangxi car"<};

 

这个时候,就可以用 Guangximan这个类来实例化对象了。

 

多文件编程

在前面的教程中,有一则教程说到了多文件编程,在 C++中也就是将类的声明放到头文件中,将类的实现放在.cpp文件中,为了更好地阐述这种方法,我们用实例来进行讲解,首先,来看一下,所涉及到地所有文件有哪些:

 

image-20210222103409774

 

可以看到上述有6个文件,我们首先来看 Chinese.h这个文件,代码如下所示:

 

#ifndef _CHINESE_H
#define _CHINESE_H

#include 
#include 
#include 

using namespace std;

class Chinese
{
public:
    void eating(void); 
    void wearing(void);
    void drivering(void);
};

#endif

 

通过上述地.h文件可以看出,在这里的Chinese类中,它只涉及到类成员函数的一个声明,并没有成员函数的实现,我们继续来看Chinese.cpp的类实现:

 

#include "Chinese.h"

void Chinese::eating(void)
{
    cout << "use chopsticks to eat" << endl;
}

void Chinese::wearing(void)
{
    cout << "wear chinese style" << endl;
}

void Chinese::drivering(void)
{
    cout << "driver china car" << endl;
}

 

按照上述这样一种方法,我们继续来实现Englishman类中的代码,首先是Englishman.h中的代码,代码如下所示:

 

#ifndef _ENGLISHMAN_H
#define _ENGLISHMAN_H

#include 
#include 
#include 

using namespace std;

class Englishman
{
public:
    void eating(void);
    void wearing(void);
    void driver(void);
};

#endif

 

继续看.cpp中的代码,代码如下所示:

 

#include "Englishman.h"

void Englishman::eating(void)
{
    cout << "use chopsticks to eat" << endl;
}

void Englishman::wearing(void)
{
    cout << "wear chinese style" << endl;
}

void Englishman::drivering(void)
{
    cout << "driver china car" << endl;
}

 

至此,除了主函数以外的代码就编写完了,我们继续来看主函数的代码:

 

#include "Englishman.h"
#include "Chinese.h"

int main(int argc, char **argv)
{
    Englishman e;
    Chinese c;

    e.eating();
    c.eating();

    return 0;
}

 

在前面的教程中,我们就说过,如果是多文件的话,需要编写 Makefile文件,Makefile文件代码如下:

 

Human: main.o Chinese.o Englishman.o Human.o
    g++ -o $@ $^

%.o : %.cpp
    g++ -c -o $@ $<

clean:
    rm -f *.o Human 

 

上述代码就不再这里赘述了,跟之前教程中的 Makefile基本是一样的,有了Makefile之后,编译代码只需要使用 make命令就行了,编译结果如下所示:

 

image-20210222105051169

 

上述代码中,如果我们想要增添功能,比如说ChineseEnglishman都有名字,那么就可以增添设置名字和获取名字这两种方法,首先是 Chinese的代码,代码如下:

 

#ifndef _CHINESE_H
#define _CHINESE_H

#include 
#include 
#include 

using namespace std;

class Chinese{
private:
    char *name;
public:
    void setName(char *name);
    char *getName(void);
    void eating(void);
    void wearing(void);
    void driving(void);
    ~Chinese();
};

#endif

 

然后是.cpp中的代码:

 

#include "Chinese.h"

void Chinese::setName(char *name) 
{
    this->name = name;
}

char *Chinese::getName(void) 
{
    return this->name;
}

/*其他成员函数实现同上,这里省略*/

 

写完了 Chinese的代码,然后是Englishman中的代码,首先是Englishman.h中的代码:

 

#ifndef _ENGLISHMAN_H
#define _ENGLISHMAN_H

#include 
#include 
#include 

using namespace std;

class Englishman {
private:
    char *name;
public:
    void setName(char *name);
    char *getName(void);
    void eating(void);
    void wearing(void);
    void driving(void);
    ~Englishman();
};

#endif

 

紧接着,是.cpp中的代码:

 

#include "Englishman.h"

void Englishman::setName(char *name) 
{
    this->name = name;
}

char *Englishman::getName(void) 
{
    return this->name;
}

 

以这样的方式增添功能,确实是可行的,但是我们假设一下,如果类很多,除了中国人和英国人还有很多个国家的人,如果这些类都要增加相同的功能,这个工作量就比较大了,那要如何解决这个问题呢?这个时候,我们就可以引入一个新类Human,然后,将每个类相同的部分写在这个类里面,其他类,诸如EnglisnmanChinese就可以从Human类中继承而来,那这个时候,增添的操作,就只需要在 Human类中增加就好了,不需要改动ChineseEnglishman,工作量就小了很多。我们来看 Human类的代码实现,首先是.h代码的实现:

 

#ifndef _HUMAN_H
#define _HUMAN_H

#include 
#include 
#include 

using namespace std;

class Human {
private:
    char *name;

public:
    void setName(char *name);
    char *getName(void);    
};

#endif

 

然后是.cpp代码的实现:

 

#include "Human.h"

void Human::setName(char *name) 
{
    this->name = name;
}

char *Human::getName(void) 
{
    return this->name;
}

有了 Human类之后,我们就可以来实现我们所说的 EnglishmanChinese类了,代码如下所示:

#ifndef _ENGLISHMAN_H
#define _ENGLISHMAN_H

#include 
#include 
#include 

#include "Human.h"

using namespace std;

class Englishman : public Human 
{
public:
    void eating(void);
    void wearing(void);
    void driving(void);
    ~Englishman();
};

#endif

 

然后是Chinese的代码:

 

#ifndef _CHINESE_H
#define _CHINESE_H

#include 
#include 
#include 

#include "Human.h"

using namespace std;

class Chinese : public Human
{
public:
    void eating(void);
    void wearing(void);
    void driving(void);
    ~Chinese();
};

#endif

 

可以看到 EnglishmanChinese都是继承自Human类,这个时候,就不需要再自己实现setNamegetName了。

 

我们继续来完善我们的代码,先从主函数说起,主函数代码如下所示:

 

void test_eating(Human *h)
{
    h->eating();
}

int main(int argc, char **argv)
{
    Englishman e;
    Chinese c;

    Human * h[2] = {&e,&h};
    int i;
    for (i = 0; i < 2; i++)
        test_eating(h[i]);

    return 0;
}

 

简要说明一下主函数代码的意思,其实就是定义了一个指针数组,然后遍历整个指针数组,一次将数组内的成员传入test_eating()函数内,根据传入的参数不同执行不同的eating函数,说到这里,实际上是跟前面一则教程中所将的抽象类和虚函数概念所结合起来的,因此,这里也是采用相同的思路,将 Human类设置为抽象类,然后其他类由Human类派生而来,下面就来看Human类的代码:

 

#ifndef _HUMAN_H
#define _HUMAN_H

#include 
#include 
#include 

using namespace std;

class Human {
private:
    char *name;

public:
    void setName(char *name);
    char *getName(void);
    virtual void eating(void) = 0;
    virtual void wearing(void) = 0;
    virtual void driving(void) = 0; 
};

#endif

 

然后是Human.cpp的代码:

 

#include "Human.h"

void Human::setName(char *name) 
{
    this->name = name;
}

char *Human::getName(void) 
{
    return this->name;
}

 

实现了 Human类的代码之后,我们来看ChineseEnglishman的代码,代码如下所示,首先是 Englishman.h

 

#ifndef _ENGLISHMAN_H
#define _ENGLISHMAN_H

#include 
#include 
#include 

#include "Human.h"

using namespace std;

class Englishman : public Human 
{
public:
    void eating(void);
    void wearing(void);
    void driving(void);
};

#endif

 

紧接着是 Englishman.cpp的代码:

 

#include "Englishman.h"

void Englishman::eating(void) 
{ 
    cout<<"use knife to eat"<}

void Englishman::wearing(void) 
{
    cout<<"wear english style"<}

void Englishman::driving(void) 
{
    cout<<"drive english car"<}

 

Chinese的代码就不展示了,和Englishman是一个道理,总的来说,上述实际上也就是本节教程中抽象类的一个多文件的实现。

 

动态链接库

回顾上述的代码中的 Makefile文件,代码如下所示:

 

Human: main.o Chinese.o Englishman.o Human.o
    g++ -o $@ $^

%.o : %.cpp
    g++ -c -o $@ $<

clean:
    rm -f *.o Human 

 

通过第一行代码,我们知道只要更改main.cChinese.cEnglishman.c以及Human.o中的任意一个文件,都会导致重新生成一个 Human文件,考虑到这一点,实际上我们可以将 Chinese.oEnglishman.oHuman.o做成一个动态库,至于这么做的原因是因为我们在开发一个大的项目的时候,会涉及到一个程序由多个人编写,基本会分为两类,一个是应用编程,一个是类编程,那么这两者的区别如下所示:

 

应用编程:使用类

 

类编程:提供类编程,比如说 EnglishmanChinese

 

基于此,我们将之前的程序更改为这种形式,分为应用编程和类编程,基于上述我们对应用编程和类编程的区别的阐述,我们可以知道在刚刚那个程序,main.c就是应用编程,而Englishman.cChinese.cHuman.c就是类编程,而我们只需要更改 Makefile就可以实现这样一个功能,更改之后的Makefile如下所示:

 

Human: main.o libHuman.so
    g++ -o $@ $< -L./ -lHuman

%.o : %.cpp
    g++ -fPIC -c -o $@ $<

libHuman.so : Englishman.o Chinese.o Human.o
    g++ -shared -o $@ $^

clean:
    rm -f *.o Human 

 

对比于之前的Makefile,我们可以看出第一行,Chinese.oEnglishman.oHuman.o替换成了现在的 libHuman.so,也就是说现在的 Human文件生成依赖于main.olibHuman.so这两个文件。第二行中的-L是表示,编译的时候,指定搜索库的路径,而整个路径就是紧随其后的./,表示的是当前文件夹下。而后面的-lHuman表示的是当前链接的是Human整个库,要完全理解这里,还需要了解下Linux下的.so文件。

 

.so文件,被称之为共享库,是share object,用于动态链接,说到这里,可能会有所疑惑,明明写的是libHuman.so,为何在后面链接的时候写的是-lHuman,并不是-llibHuman呢,这就要了解一下.so文件的命名规则,.so文件是按照如下命名方式进行命名的:lib+函数库名+.so+版本号信息,也就是说虽然写的是libHuman.so,但是实际生成的共享库为Human,也就是为什么后面是-lHuman了。

 

继续来看Makefile代码,可以看到第四行也与之前的代码不相同,多了一个 -fPIC,这个参数的作用是:生成位置无关目标码,用于生成动态链接库。

 

继续来看第八行,其中用到了一个之前未曾用过的-shared命令,这个命令的作用是:此选项将尽量使用动态库,为默认选项。优点:生成文件比较小。缺点:运行时需要系统提供动态库。

 

至此,Makefile代码就完了,那么更改了的代码与之前存在什么区别呢,我们先来回顾一下之前代码的主函数:

 

#include "Human.h"
#include "Englishman.h"
#include "Chinese.h"

void test_eating(Human *h)
{
    h->eating();
}


int main(int argc, char **argv)
{
    Englishman e;
    Chinese c;

    Human* h[2] = {&e, &c};
    int i;
    for (i = 0; i < 2; i++)
        test_eating(h[i]);

    return 0;
}

 

然后,我们进行编译,运行:

 

image-20210223190725028

 

在上述中,我们看到编译我们是用make命令进行编译的,然后在运行可执行代码的时候,我们采用的是LD_LIBRARY_PATH=./ ./Human,与前面的教程不同,这次在运行可执行文件的时候,多了LD_LIBRARY_PATH=./,这是因为现在使用了动态库,而这条多出来的语句是来指明动态库的路径的。

 

最后,我们来测试一下,我们使用动态链接库所带来的优点,比如,我现在更改了Chinese.cppeating函数,代码如下:

 

void Chinese::eating(void) 
{ 
    cout<<"use chopsticks to eat,test"<}

 

然后,如果没有使用动态链接库,那么这个时候,如果要执行这个修改过的代码,就需要重新生成可执行文件,但是现在使用了动态链接库,也就是说,不需要重新生成可执行文件了,我们只需要重新生成动态链接库就好了,编译命令如下所示:

 

image-20210223191802201

 

可见,上述只重新生成了Human库文件,并没有重新生成可执行文件,代码运行正确,这样也就做到了应用编程和类编程分离。

 

小结

上述便是本期教程的所有内容,教程所涉及的代码可以通过百度云链接的方式获取。

 

链接:https://pan.baidu.com/s/1fB78jG6PdMNcXMfPwtqyvw

 

提取码:cquv