• 3.2 C++高级编程_抽象类界面


    通常,我们在头文件中声明类,然后在cpp文件中实现他们。

    类的声明和实现

    首先创建五个文件:

    • Chinese.cpp 和 Chinese.h;
    • Englishman.cpp 和 Englishman.h;
    • main.cpp;

    然后,在 Chinese.h 和 Englishman.h 中分别声明Chinese类和Englishman类。

    Chinese.h

    1. #ifndef __CHINESE_H_
    2. #define __CHINESE_H_
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. class Chinese {
    9. public:
    10. void eating(void);
    11. void wearing(void);
    12. void driving(void);
    13. ~Chinese();
    14. };
    15. #endif

    Englishman.h

    1. #ifndef __ENGLISHMAN_H_
    2. #define __ENGLISHMAN_H_
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. class Englishman {
    9. public:
    10. void eating(void);
    11. void wearing(void);
    12. void driving(void);
    13. ~Englishman();
    14. };
    15. #endif

    Chinese 类和 Englishman 类中分别定义了各自的 eating,wearing等函数。

    在 Chinese.cpp 和 Englishman.cpp 中分别实现这些函数。

    Chinese.cpp

    1. #include "Chinese.h"
    2. void Chinese::eating(void)
    3. {
    4. cout << "use chopsticks to eat" << endl;
    5. }
    6. void Chinese::wearing(void)
    7. {
    8. cout << "wear Chinese style" << endl;
    9. }
    10. void Chinese::driving(void)
    11. {
    12. cout << "drive Chinese car" << endl;
    13. }
    14. Chinese::~Chinese()
    15. {
    16. cout << "~Chinese()" << endl;
    17. }

    Englishman.cpp  

    1. #include "Englishman.h"
    2. void Englishman::eating(void)
    3. {
    4. cout << "use knife to eat" << endl;
    5. }
    6. void Englishman::wearing(void)
    7. {
    8. cout << "wear English style" << endl;
    9. }
    10. void Englishman::driving(void)
    11. {
    12. cout << "drive English car" << endl;
    13. }
    14. Englishman::~Englishman()
    15. {
    16. cout << "~Englishman()" << endl;
    17. }

    最后,在 main.c 中创建main函数,在main函数中分别创建两个类的对象,并且调用类的成员函数eating。

    main.c

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include "Englishman.h"
    6. #include "Chinese.h"
    7. using namespace std;
    8. int main(int argc, char **argv)
    9. {
    10. Englishman e;
    11. Chinese c;
    12. e.eating();
    13. c.eating();
    14. return 0;
    15. }

    为了方便编译,写一个Makefile。

    Makefile

    1. TARGET=human
    2. $(TARGET) : main.o Chinese.o Englishman.o
    3. g++ -o $@ $^
    4. %.o : %.cpp
    5. g++ -o *.o *.cpp
    6. clean:
    7. rm $(TARGET) *.o

    编译并执行,可以看到程序如预期那样运行了。

    提取基类和继承

    现在,假设要增加一个读取和设置姓名的功能

    如果按照现在的结构,那么需要分别在Englishman类和Chinese类中分别增加一个私有成员name,然后再分别增加各自的成员函数 getName 和 setName 来设置和获取 name。

    如果后续还有什么新增的功能,或者功能有变动,要同时修改两份文件,这样太麻烦了,不利为维护和开发。

    可以将Englishman和Chinese类公有的功能抽象出来,建立一个基类Englishman和Chinese类只需要继承这个基类即可

    这样后续如果有什么公有的功能,那么只需要修改这个基类

    增加Human.cpp和Human.h文件,声明一个基类Human。

    Human.h

    1. #ifndef __HUMAN_H_
    2. #define __HUMAN_H_
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. class Human {
    9. private:
    10. char *name;
    11. public:
    12. void setName(char *name);
    13. char *getName(void);
    14. };
    15. #endif

    Human.cpp

    1. #include "Human.h"
    2. void Human::setName(char *name)
    3. {
    4. this->name = name;
    5. }
    6. char *Human::getName(void)
    7. {
    8. return this->name;
    9. }

    然后,修改Chinese.h和Englishman.h,让Chinese类和Englishman类继承Human类。

    Chinese.h

    1. #ifndef __CHINESE_H_
    2. #define __CHINESE_H_
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include "Human.h"
    8. using namespace std;
    9. class Chinese : public Human {
    10. public:
    11. void eating(void);
    12. void wearing(void);
    13. void driving(void);
    14. ~Chinese();
    15. };
    16. #endif

    Englishman.h

    1. #ifndef __ENGLISHMAN_H_
    2. #define __ENGLISHMAN_H_
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include "Human.h"
    8. using namespace std;
    9. class Englishman : public Human {
    10. public:
    11. void eating(void);
    12. void wearing(void);
    13. void driving(void);
    14. ~Englishman();
    15. };
    16. #endif

    这样,由于Chinese类和Englishman类继承了Human类,那么就可以直接使用Human类的setName函数和getName函数来设置和获取名字了。

    main.cpp

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include "Englishman.h"
    6. #include "Chinese.h"
    7. using namespace std;
    8. int main(int argc, char **argv)
    9. {
    10. Englishman e;
    11. Chinese c;
    12. e.setName("Bill");
    13. c.setName("zhangsan");
    14. cout << e.getName() << endl;
    15. cout << c.getName() << endl;
    16. e.eating();
    17. c.eating();
    18. return 0;
    19. }

    编译的时候,注意要修改下Makefile,把Human.cpp文件加入编译。

    Makefile

    1. TARGET=human
    2. $(TARGET) : main.o Chinese.o Englishman.o Human.o
    3. g++ -o $@ $^
    4. %.o : %.cpp
    5. g++ -c -o $@ $<
    6. clean:
    7. rm $(TARGET) *.o

    编译测试,可以看到,分别设置和获取了名字,符合需求。

    多态

    修改main.cpp,增加一个test_eating函数。

    1. void test_eating(Human *h)
    2. {
    3. h->eating();
    4. }

    在main函数中分别传入e(Englishman类)和c(Chinese类)的地址。

    1. int main(int argc, char **argv)
    2. {
    3. Englishman e;
    4. Chinese c;
    5. Human *h[2] = { &e, &c };
    6. int i;
    7. for (i = 0; i < 2; i++)
    8. {
    9. test_eating(h[i]);
    10. }
    11. return 0;
    12. }

    在Human类中增加一个成员函数eating,Chinese类和Englishman类中之前分别有实现自己的eating成员函数,这里不做赘述。

    1. class Human {
    2. private:
    3. char *name;
    4. public:
    5. void setName(char *name);
    6. char *getName(void);
    7. void eating(void)
    8. {
    9. cout << "use hand to eat" << endl;
    10. }
    11. };

    此时,我们希望传入c时输出的是“use chopsticks to eat”,传入e时,输出“use knife to eat”。

    但是显然,此时由于没有使用虚函数,调用的应该是Human类中的eating函数,也就是输出"use hand to eat"。

    将Human类中的eating函数改为虚函数,使用多态实现针对性的调用(传入同样参数,会根据这个参数属于哪个类的,然后去调用对应的类的函数)。

    编译测试,结果如下。

    纯虚函数

    事实上,我们不会创建一个Human类的对象,因为每一个人都有自己的国家等信息,Human类仅仅是作为一个基类使用。

    这时,可以使用纯虚函数定义Human类的成员函数eating。

    使用纯虚函数有两个好处:

    1. 节省代码:本来也用不到Human类的eating函数,何必还要浪费空间和时间来实现它呢;
    2. 可以阻止生成一个Human类的实例化对象:接上节所说,本次是研究各个国家的人,那么应该使用Chinese类或Englishman类来创建一个具体的对象,而不是使用Human类;

    应用编程和类编程

    将上面的程序分为应用编程和类编程。

    • 应用编程:使用类编程中提供的功能,即使用类,这里主要是main.cpp;
    • 类编程:实现项目需要的功能,即提供类,这里包括Chinese.cpp,Englishman.cpp,Human.cpp;

    需要修改Makefile,将Chinese.cpp,Englishman.cpp,Human.cpp编成一个动态库。

    Makefile

    1. TARGET=human
    2. $(TARGET) : main.o libHuman.so
    3. g++ -o $@ $< -L./ -lHuman
    4. libHuman.so : Englishman.o Chinese.o Human.o
    5. g++ -shared -o $@ $^
    6. %.o : %.cpp
    7. g++ -fPIC -c -o $@ $<
    8. clean:
    9. rm $(TARGET) *.o

    其中,-fPIC表示使用位置无关码(生成的机器代码,该代码与内存地址无关,即不对将其加载到RAM中的位置进行任何假设),-shared表示编程成一个动态库文件

    编译生成可执行文件human。

    但是此时编译会报错,报找不到动态库文件。

     需要指定一下找库的路径,指定在当前路径下找库文件,然后执行human。

    LD_LIBRARY_PATH=./ ./human

    或者

    1. export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
    2. ./human

    两者的结果是一样的,最终human可以成功执行。

    这样写Makefile有什么好处呢

    答:将程序分成了应用编程类编程,比如说要修改Chinese,Englishman,那么现在不需要修改用用程序,只需要重新生成动态库文件libHuman.so

    修改Chinese.cpp,修改eating函数输出的语句,在末尾增加输出adjust。

    然后只需要重新编译libHuman.so,不需要重新编译应用文件human。

    测试结果如下,可以看到输出的语句已经是修改之后的了。

    抽象类界面

    再思考一下,如果修改的是头文件,是否也是只需要重新编译库文件而不用编译应用文件呢

    修改Englishman.cpp和Englishman.h。

    Englishman.h

    Englishman.cpp

    修改main函数,创建Englishman对象e时,会调用对应的带参数的构造函数。

    main.cpp

    此时,程序编译和执行都没有问题。

    但是,如果我们修改了头文件Englishman.h呢?

    修改头文件Englishman.h和Englishman.cpp。

    Englishman.h

    Englishman.cpp

    然后只编译库文件,没有重新编译human。

    测试执行,发生了core dump。

    此时,只有重新clean,然后重新make,才能正常使用。

    显然,这样是有问题的,不符合我们之前应用编程和类编程的分工需要。(类编程的修改,应该不会影响到应用编程,即类编程修改只需要更新库文件,不需要重新编译应用程序

    问题的根源在于,为什么不重新编译应用程序会发生core dump(崩溃)

    答:因为main.cpp中需要包含Englishman.h,修改了包含的头文件,当然需要重新修改对应的.cpp文件。

    那么,是否能够将Englishman.h,Chinese.h这些头文件剔除出main.cpp,只包含相对固定的Human.h呢?

    答:是可以的,这里引入抽象类界面的概念。

    即应用程序app只和相对固定的Human.h联系,然后Human.h下面包含两个类Chinese和Englishman。

    这样子,这两个类的修改就不会影响到app了,因为app根本就没有包含它们。

    代码结构大致如下:

    修改Human.h。

    Human.h

    增加声明两个函数,分别用于创建Englishman和Chinese对象。

    然后,分别在Chinese.cpp和Englishman.cpp中实现这两个函数。

    Chinese.cpp

    Englishman.cpp

    修改main.cpp,将Englishman.h和Chinese.h这两个头文件屏蔽掉,并且使用Human.h中声明的CreateEnglishman和CreateChinese来创建对象。

    此时编译和执行都是没有问题的。

    然后和之前一样,修改头文件Englishman.h。

    Englishman.h

    Englishman.cpp也要对应修改。

    Englishman.cpp

    此时,只编译库文件,不重新编译应用程序human,然后执行。

    程序可以正常运行。

    显然,这是由于CreateEnglishman函数和CreateChinese函数实际上是在库文件中实现的,当app调用到这两个函数时,其实执行的是库文件中的代码,所以只需要重新编译库即可。

    通过抽象类界面,我们就实现了应用编程和类编程的分离。

    优化 

    上面使用了抽象类界面的代码,在创建了两个对象后,没有释放,修改代码加上释放的操作。

    修改代码,使用delete删除对象。

    给Human类增加析构函数。

    Human.h

    增加析构函数的声明,注意析构函数需要声明为虚函数。

    Human.cpp

    实现Human类的析构函数。

    编译测试,可以看到对应的析构函数被依次调用。

  • 相关阅读:
    webservice接口测试
    Vue render渲染函数
    文心一言 vs GPT-4 —— 全面横向比较
    Arrays的用法(常见方法的使用)
    01背包面试题系列(一)
    2022年大一期末作业——音乐网页(纯html+css+js实现)
    结构化分析建模的基本步骤
    【精讲】vue框架 路由(hash及history)区别、ui组件库
    在Linux系统下部署Llama2(MetaAI)大模型教程
    搜维尔科技:CATIA为建筑、基础设施和城市规划提供虚拟孪生力量
  • 原文地址:https://blog.csdn.net/qq_33141353/article/details/126238685