通常,我们在头文件中声明类,然后在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。
使用纯虚函数有两个好处:
- 节省代码:本来也用不到Human类的eating函数,何必还要浪费空间和时间来实现它呢;
- 可以阻止生成一个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类的析构函数。
编译测试,可以看到对应的析构函数被依次调用。