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

本文通过具体示例介绍C++面向对象编程的核心概念和技术,包括类的声明与实现、继承与多态、抽象类和纯虚函数的应用。通过创建不同国籍人物的类,展示如何利用C++特性进行高效模块化编程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

类的声明和实现

首先创建五个文件:

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

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

Chinese.h

#ifndef __CHINESE_H_
#define __CHINESE_H_

#include <stdio.h>
#include <iostream>
#include <string.h>
#include <unistd.h>

using namespace std;

class Chinese {
public:
    void eating(void);

    void wearing(void);

    void driving(void);

    ~Chinese();
};

#endif

Englishman.h

#ifndef __ENGLISHMAN_H_
#define __ENGLISHMAN_H_

#include <stdio.h>
#include <iostream>
#include <string.h>
#include <unistd.h>

using namespace std;

class Englishman {
public:
    void eating(void);

    void wearing(void);

    void driving(void);

    ~Englishman();
};

#endif

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

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

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::driving(void)
{
    cout << "drive Chinese car" << endl;
}

Chinese::~Chinese()
{
    cout << "~Chinese()" << endl;
}

Englishman.cpp  

#include "Englishman.h"

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

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

void Englishman::driving(void)
{
    cout << "drive English car" << endl;
}

Englishman::~Englishman()
{
    cout << "~Englishman()" << endl;
}

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

main.c

#include <stdio.h>
#include <iostream>
#include <string.h>
#include <unistd.h>

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

using namespace std;

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

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

    return 0;
}

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

Makefile

TARGET=human

$(TARGET) : main.o Chinese.o Englishman.o
    g++ -o $@ $^

%.o : %.cpp
    g++ -o *.o *.cpp

clean:
    rm $(TARGET) *.o

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

提取基类和继承

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

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

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

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

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

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

Human.h

#ifndef __HUMAN_H_
#define __HUMAN_H_

#include <stdio.h>
#include <iostream>
#include <string.h>
#include <unistd.h>

using namespace std;

class Human {
private:
    char *name;
public:
    void setName(char *name);

    char *getName(void);
};

#endif

Human.cpp

#include "Human.h"

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

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

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

Chinese.h

#ifndef __CHINESE_H_
#define __CHINESE_H_

#include <stdio.h>
#include <iostream>
#include <string.h>
#include <unistd.h>

#include "Human.h"

using namespace std;

class Chinese : public Human {
public:
    void eating(void);

    void wearing(void);

    void driving(void);

    ~Chinese();
};

#endif

Englishman.h

#ifndef __ENGLISHMAN_H_
#define __ENGLISHMAN_H_

#include <stdio.h>
#include <iostream>
#include <string.h>
#include <unistd.h>

#include "Human.h"

using namespace std;

class Englishman : public Human {
public:
    void eating(void);

    void wearing(void);

    void driving(void);

    ~Englishman();
};

#endif

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

main.cpp

#include <stdio.h>
#include <iostream>
#include <string.h>
#include <unistd.h>

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

using namespace std;

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

    e.setName("Bill");
    c.setName("zhangsan");

    cout << e.getName() << endl;
    cout << c.getName() << endl;

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

    return 0;
}

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

Makefile

TARGET=human

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

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

clean:
	rm $(TARGET) *.o

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

多态

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

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

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

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;
}

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

class Human {
private:
    char *name;
public:
    void setName(char *name);

    char *getName(void);

    void eating(void)
    {
        cout << "use hand to eat" << endl;
    }
};

此时,我们希望传入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


TARGET=human

$(TARGET) : main.o libHuman.so
	g++ -o $@ $< -L./ -lHuman

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

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

clean:
	rm $(TARGET) *.o

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

编译生成可执行文件human。

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

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

LD_LIBRARY_PATH=./ ./human

或者

export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./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类的析构函数。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值