通常,我们在头文件中声明类,然后在cpp文件中实现他们。
首先创建五个文件:
然后,在 Chinese.h 和 Englishman.h 中分别声明Chinese类和Englishman类。
Chinese.h
- #ifndef __CHINESE_H_
- #define __CHINESE_H_
-
- #include
- #include
- #include
- #include
-
- 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
- #include
- #include
- #include
-
- 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
- #include
- #include
- #include
-
- #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
- #include
- #include
- #include
-
- 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
- #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
Englishman.h
- #ifndef __ENGLISHMAN_H_
- #define __ENGLISHMAN_H_
-
- #include
- #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类和Englishman类继承了Human类,那么就可以直接使用Human类的setName函数和getName函数来设置和获取名字了。
main.cpp
- #include
- #include
- #include
- #include
-
- #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。
使用纯虚函数有两个好处:
将上面的程序分为应用编程和类编程。
需要修改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类的析构函数。
编译测试,可以看到对应的析构函数被依次调用。