前言
在上一則教程中,敘述了關(guān)于C++
類型轉(zhuǎn)換的相關(guān)內(nèi)容,在本節(jié)教程中,將敘述 C++
的另一個內(nèi)容,也就是抽象,這也是 C++
相對于 C
語言來說獨特的一點,下面我們就來著重敘述這一點。
純虛函數(shù)
在介紹抽象類之前,需要弄明白何為純虛函數(shù),下面假定我們有這樣一個需求:
做一個“各個國家的人的調(diào)查”,調(diào)查各個國家的人的:飲食、穿衣、開車
要完成這樣一個事情,那我們現(xiàn)在就需要實現(xiàn)這樣幾個類,一個是 Human
類,其他國家的人從 Human
類里派生而來,就比如說是Chinese
和Englishman
,我們再回過頭來想,我們所要實現(xiàn)的需求是調(diào)查各個國家的人
,那么這個過程中,由Human
類派生得到 Chinese
和 Englishman
,那么在實例化對象的時候,我們實際上是不會用到Human
類去定義一個對象的,考慮到這層因素,我們在 Human
類里使用到了純虛函數(shù)的概念,類實現(xiàn)的代碼如下所示:
class Human
{
private:
int a;
public:
/*純虛函數(shù)*/
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
類的成員函數(shù)跟前幾講所寫的成員函數(shù)有所不同,而如上述 Human
類的成員函數(shù)這般寫法,也就被稱之為是純虛函數(shù)。
抽象類
上述引出了純虛函數(shù)的寫法,那純虛函數(shù)和抽象類之間有什么關(guān)系呢?實際上,抽象類就是具有純虛函數(shù)的類,那這抽象類存在的意義是什么呢?總的來說,其作用也就是:向下定義好框架,向上提供統(tǒng)一的接口,其不能夠?qū)嵗瘜ο?,基于上述幾個類的前提下,我們編寫主函數(shù)的代碼:
int main(int argc,char **argv)
{
Human h;
return 0;
}
因為抽象類不能夠?qū)嵗瘜ο螅陨鲜龃a編譯結(jié)果是錯誤的,錯誤信息如下所示:
而使用通過抽象類派生得到的派生類實例化對象是可行的,代碼如下所示:
int main(int argc, char** argv)
{
Englishman e;
Chinese g;
return 0;
}
另外需要注意的是:在派生抽象類的過程中,如果派生得到的子類沒有覆寫所有的純虛函數(shù),那么這個子類還是抽象類,比如有如下所示的代碼,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()
函數(shù)注釋掉了,那么也就是說,我們并沒有將抽象類的全部純虛函數(shù)進行覆寫,那么當前這個Chinese
類也是一個抽象類,也是不能夠進行實例化對象的,要使得 Chinese
類有作用,我們必須派生出來另一個類,代碼如下所示:
class Guangximan : public Chinese
{
void driving(void) {cout<<"drive guangxi car"<};
這個時候,就可以用 Guangximan
這個類來實例化對象了。
多文件編程
在前面的教程中,有一則教程說到了多文件編程,在 C++
中也就是將類的聲明放到頭文件中,將類的實現(xiàn)放在.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
類中,它只涉及到類成員函數(shù)的一個聲明,并沒有成員函數(shù)的實現(xiàn),我們繼續(xù)來看Chinese.cpp
的類實現(xiàn):
#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;
}
按照上述這樣一種方法,我們繼續(xù)來實現(xiàn)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
繼續(xù)看.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;
}
至此,除了主函數(shù)以外的代碼就編寫完了,我們繼續(xù)來看主函數(shù)的代碼:
#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
命令就行了,編譯結(jié)果如下所示:
image-20210222105051169
上述代碼中,如果我們想要增添功能,比如說Chinese
和Englishman
都有名字,那么就可以增添設(shè)置名字和獲取名字這兩種方法,首先是 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;
}
/*其他成員函數(shù)實現(xiàn)同上,這里省略*/
寫完了 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;
}
以這樣的方式增添功能,確實是可行的,但是我們假設(shè)一下,如果類很多,除了中國人和英國人還有很多個國家的人,如果這些類都要增加相同的功能,這個工作量就比較大了,那要如何解決這個問題呢?這個時候,我們就可以引入一個新類Human
,然后,將每個類相同的部分寫在這個類里面,其他類,諸如Englisnman
和Chinese
就可以從Human
類中繼承而來,那這個時候,增添的操作,就只需要在 Human
類中增加就好了,不需要改動Chinese
和Englishman
,工作量就小了很多。我們來看 Human
類的代碼實現(xiàn),首先是.h
代碼的實現(xiàn):
#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
代碼的實現(xiàn):
#include "Human.h"
void Human::setName(char *name)
{
this->name = name;
}
char *Human::getName(void)
{
return this->name;
}
有了 Human
類之后,我們就可以來實現(xiàn)我們所說的 Englishman
和Chinese
類了,代碼如下所示:
#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
可以看到 Englishman
和Chinese
都是繼承自Human
類,這個時候,就不需要再自己實現(xiàn)setName
和getName
了。
我們繼續(xù)來完善我們的代碼,先從主函數(shù)說起,主函數(shù)代碼如下所示:
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;
}
簡要說明一下主函數(shù)代碼的意思,其實就是定義了一個指針數(shù)組,然后遍歷整個指針數(shù)組,一次將數(shù)組內(nèi)的成員傳入test_eating()
函數(shù)內(nèi),根據(jù)傳入的參數(shù)不同執(zhí)行不同的eating
函數(shù),說到這里,實際上是跟前面一則教程中所將的抽象類和虛函數(shù)概念所結(jié)合起來的,因此,這里也是采用相同的思路,將 Human
類設(shè)置為抽象類,然后其他類由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;
}
實現(xiàn)了 Human
類的代碼之后,我們來看Chinese
和Englishman
的代碼,代碼如下所示,首先是 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
是一個道理,總的來說,上述實際上也就是本節(jié)教程中抽象類的一個多文件的實現(xiàn)。
動態(tài)鏈接庫
回顧上述的代碼中的 Makefile
文件,代碼如下所示:
Human: main.o Chinese.o Englishman.o Human.o
g++ -o $@ $^
%.o : %.cpp
g++ -c -o $@ $<
clean:
rm -f *.o Human
通過第一行代碼,我們知道只要更改main.c
,Chinese.c
和Englishman.c
以及Human.o
中的任意一個文件,都會導致重新生成一個 Human
文件,考慮到這一點,實際上我們可以將 Chinese.o
、Englishman.o
和Human.o
做成一個動態(tài)庫,至于這么做的原因是因為我們在開發(fā)一個大的項目的時候,會涉及到一個程序由多個人編寫,基本會分為兩類,一個是應(yīng)用編程,一個是類編程,那么這兩者的區(qū)別如下所示:
應(yīng)用編程:使用類
類編程:提供類編程,比如說 Englishman
,Chinese
基于此,我們將之前的程序更改為這種形式,分為應(yīng)用編程和類編程,基于上述我們對應(yīng)用編程和類編程的區(qū)別的闡述,我們可以知道在剛剛那個程序,main.c
就是應(yīng)用編程,而Englishman.c
,Chinese.c
及Human.c
就是類編程,而我們只需要更改 Makefile
就可以實現(xiàn)這樣一個功能,更改之后的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.o
、Englishman.o
和Human.o
替換成了現(xiàn)在的 libHuman.so
,也就是說現(xiàn)在的 Human
文件生成依賴于main.o
和libHuman.so
這兩個文件。第二行中的-L
是表示,編譯的時候,指定搜索庫的路徑,而整個路徑就是緊隨其后的./
,表示的是當前文件夾下。而后面的-lHuman
表示的是當前鏈接的是Human
整個庫,要完全理解這里,還需要了解下Linux
下的.so
文件。
.so
文件,被稱之為共享庫,是share object
,用于動態(tài)鏈接,說到這里,可能會有所疑惑,明明寫的是libHuman.so
,為何在后面鏈接的時候?qū)懙氖?code>-lHuman,并不是-llibHuman
呢,這就要了解一下.so
文件的命名規(guī)則,.so
文件是按照如下命名方式進行命名的:lib+函數(shù)庫名+.so+版本號信息,也就是說雖然寫的是libHuman.so
,但是實際生成的共享庫為Human
,也就是為什么后面是-lHuman
了。
繼續(xù)來看Makefile
代碼,可以看到第四行也與之前的代碼不相同,多了一個 -fPIC
,這個參數(shù)的作用是:生成位置無關(guān)目標碼,用于生成動態(tài)鏈接庫。
繼續(xù)來看第八行,其中用到了一個之前未曾用過的-shared
命令,這個命令的作用是:此選項將盡量使用動態(tài)庫,為默認選項。優(yōu)點:生成文件比較小。缺點:運行時需要系統(tǒng)提供動態(tài)庫。
至此,Makefile
代碼就完了,那么更改了的代碼與之前存在什么區(qū)別呢,我們先來回顧一下之前代碼的主函數(shù):
#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
命令進行編譯的,然后在運行可執(zhí)行代碼的時候,我們采用的是LD_LIBRARY_PATH=./ ./Human
,與前面的教程不同,這次在運行可執(zhí)行文件的時候,多了LD_LIBRARY_PATH=./
,這是因為現(xiàn)在使用了動態(tài)庫,而這條多出來的語句是來指明動態(tài)庫的路徑的。
最后,我們來測試一下,我們使用動態(tài)鏈接庫所帶來的優(yōu)點,比如,我現(xiàn)在更改了Chinese.cpp
的eating
函數(shù),代碼如下:
void Chinese::eating(void)
{
cout<<"use chopsticks to eat,test"<}
然后,如果沒有使用動態(tài)鏈接庫,那么這個時候,如果要執(zhí)行這個修改過的代碼,就需要重新生成可執(zhí)行文件,但是現(xiàn)在使用了動態(tài)鏈接庫,也就是說,不需要重新生成可執(zhí)行文件了,我們只需要重新生成動態(tài)鏈接庫就好了,編譯命令如下所示:
image-20210223191802201
可見,上述只重新生成了Human
庫文件,并沒有重新生成可執(zhí)行文件,代碼運行正確,這樣也就做到了應(yīng)用編程和類編程分離。
小結(jié)
上述便是本期教程的所有內(nèi)容,教程所涉及的代碼可以通過百度云鏈接的方式獲取。
鏈接:https://pan.baidu.com/s/1fB78jG6PdMNcXMfPwtqyvw
提取碼:cquv