我在學(xué)習(xí)面向?qū)ο缶幊讨皩W(xué)了C,所以它有助于我在C中構(gòu)建面向?qū)ο笙到y(tǒng),來理解OOP的基本含義。你可能在學(xué)習(xí)C之前就學(xué)了OOP語言,所以這章也可能會(huì)起到一種銜接作用。這個(gè)聯(lián)系中,你將會(huì)構(gòu)建一個(gè)簡(jiǎn)單的對(duì)象系統(tǒng),但是也會(huì)了解更多關(guān)于C預(yù)處理器的事情。
這個(gè)練習(xí)會(huì)構(gòu)建一個(gè)簡(jiǎn)單的游戲,在游戲中你會(huì)在一個(gè)小型的城堡中殺死彌諾陶洛斯,并沒有任何神奇之處,只是四個(gè)房間和一個(gè)壞家伙。這個(gè)練習(xí)同時(shí)是一個(gè)多文件的項(xiàng)目,并且比起之前的一些程序看起來更像一個(gè)真正的C程序。我在這個(gè)賈少C預(yù)處理器的原因,是你需要它來在你自己的程序中創(chuàng)建多個(gè)文件。
C預(yù)處理器是個(gè)模板處理系統(tǒng),它主要的用途是讓C代碼的編程更加容易,但是它通過一個(gè)語法感知的模板機(jī)制來實(shí)現(xiàn)。以前人們主要使用C預(yù)處理器來儲(chǔ)存常量,以及創(chuàng)建“宏”來簡(jiǎn)化復(fù)雜的代碼。在現(xiàn)代C語言中你會(huì)實(shí)際上使用它作為代碼生成器來創(chuàng)建模板化的代碼片段。
C預(yù)處理器的工作原理是,如果你給它一個(gè)文件,比如.c文件,它會(huì)處理以#(井號(hào))字符開頭的各種文本。當(dāng)它遇到一個(gè)這樣的文本時(shí),它會(huì)對(duì)輸入文件中的文本做特定的替換。C預(yù)處理器的主要優(yōu)點(diǎn)是他可以包含其他文件,并且基于該文件的內(nèi)容對(duì)它的宏列表進(jìn)行擴(kuò)展。
一個(gè)快速查看預(yù)處理器所做事情的方法,是對(duì)上個(gè)練習(xí)中的代碼執(zhí)行下列命令:
cpp ex18.c | less
這會(huì)產(chǎn)生大量輸出,但是如果你滾動(dòng)它,會(huì)看到你使用#include包含的其他文件的內(nèi)容。在原始的代碼中向下滾動(dòng),你可以看到cpp如何基于頭文件中不同的#define宏來轉(zhuǎn)換代碼。
C編譯器與cpp的集成十分緊密,這個(gè)例子只是向你展示它是如何在背后工作的。在現(xiàn)代C語言中,cpp系統(tǒng)也集成到C的函數(shù)中,你或許可以將它當(dāng)做C語言的一部分。
在剩余的章節(jié)中,我們會(huì)使用更多預(yù)處理器的語法,并且像往常一樣解釋它們。
我們所創(chuàng)建的OOP系統(tǒng)是一個(gè)簡(jiǎn)單的“原型”風(fēng)格的對(duì)象系統(tǒng),很像JavaScript。你將以設(shè)置為字段的原型來開始,而不是類,接著將他們用作創(chuàng)建其它對(duì)象實(shí)例的基礎(chǔ)。這個(gè)“沒有類”的設(shè)計(jì)比起傳統(tǒng)的基于類的對(duì)象系統(tǒng)更加易于實(shí)現(xiàn)和使用。
我打算將數(shù)據(jù)類型和函數(shù)聲明放在一個(gè)單獨(dú)的頭文件中,叫做object.h。這個(gè)是一個(gè)標(biāo)準(zhǔn)的C技巧,可以讓你集成二進(jìn)制庫(kù),但其它程序員任然需要編譯。在這個(gè)文件中,我使用了多個(gè)高級(jí)的C預(yù)處理器技巧,我接下來準(zhǔn)備簡(jiǎn)略地描述它們,并且你會(huì)在后續(xù)的步驟中看到。
#ifndef _object_h
#define _object_h
typedef enum {
NORTH, SOUTH, EAST, WEST
} Direction;
typedef struct {
char *description;
int (*init)(void *self);
void (*describe)(void *self);
void (*destroy)(void *self);
void *(*move)(void *self, Direction direction);
int (*attack)(void *self, int damage);
} Object;
int Object_init(void *self);
void Object_destroy(void *self);
void Object_describe(void *self);
void *Object_move(void *self, Direction direction);
int Object_attack(void *self, int damage);
void *Object_new(size_t size, Object proto, char *description);
#define NEW(T, N) Object_new(sizeof(T), T##Proto, N)
#define _(N) proto.N
#endif
看一看這個(gè)文件,你會(huì)發(fā)現(xiàn)我使用了幾個(gè)新的語法片段,你之前從來沒見過它們:
#ifndef
你已經(jīng)見過了用于創(chuàng)建簡(jiǎn)單常量的#define,但是C預(yù)處理器可以根據(jù)條件判斷來忽略一部分代碼。這里的#ifndef是“如果沒有被定義”的意思,它會(huì)檢查是否已經(jīng)出現(xiàn)過#define _object_h,如果已出現(xiàn),就跳過這段代碼。我之所以這樣寫,是因?yàn)槲覀兛梢詫⑦@個(gè)文件包含任意次,而無需擔(dān)心多次定義里面的東西。
#define
有了上面保護(hù)該文件的#ifndef,我們接著添加_object_h的定義,因此之后任何試圖包含此文件的行為,都會(huì)由于上面的語句而跳過這段代碼。
#define NEW(T,N)
這條語句創(chuàng)建了一個(gè)宏,就像模板函數(shù)一樣,無論你在哪里編寫左邊的代碼,都會(huì)展開成右邊的代碼。這條語句僅僅是對(duì)我們通常調(diào)用的Object_new制作了一個(gè)快捷方式,并且避免了潛在的調(diào)用錯(cuò)誤。在宏這種工作方式下,T、N還有New都被“注入”進(jìn)了右邊的代碼中。T##Proto語法表示“將Proto連接到T的末尾”,所以如果你寫下NEW(Room, "Hello."),就會(huì)在這里變成RoomProto。
#define _(N)
這個(gè)宏是一種為對(duì)象系統(tǒng)設(shè)計(jì)的“語法糖”,將obj->proto.blah簡(jiǎn)寫為obj->_(blah)。它不是必需的,但是它是一個(gè)接下來會(huì)用到的有趣的小技巧。
object.h是聲明函數(shù)和數(shù)據(jù)類型的地方,它們?cè)?code>object.c中被定義(創(chuàng)建),所以接下來:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "object.h"
#include <assert.h>
void Object_destroy(void *self)
{
Object *obj = self;
if(obj) {
if(obj->description) free(obj->description);
free(obj);
}
}
void Object_describe(void *self)
{
Object *obj = self;
printf("%s.\n", obj->description);
}
int Object_init(void *self)
{
// do nothing really
return 1;
}
void *Object_move(void *self, Direction direction)
{
printf("You can't go that direction.\n");
return NULL;
}
int Object_attack(void *self, int damage)
{
printf("You can't attack that.\n");
return 0;
}
void *Object_new(size_t size, Object proto, char *description)
{
// setup the default functions in case they aren't set
if(!proto.init) proto.init = Object_init;
if(!proto.describe) proto.describe = Object_describe;
if(!proto.destroy) proto.destroy = Object_destroy;
if(!proto.attack) proto.attack = Object_attack;
if(!proto.move) proto.move = Object_move;
// this seems weird, but we can make a struct of one size,
// then point a different pointer at it to "cast" it
Object *el = calloc(1, size);
*el = proto;
// copy the description over
el->description = strdup(description);
// initialize it with whatever init we were given
if(!el->init(el)) {
// looks like it didn't initialize properly
el->destroy(el);
return NULL;
} else {
// all done, we made an object of any type
return el;
}
}
這個(gè)文件中并沒有什么新東西,除了一個(gè)小技巧之外。Object_new函數(shù)通過把原型放到結(jié)構(gòu)體的開頭,利用了structs工作機(jī)制的一個(gè)方面。當(dāng)你在之后看到ex19.h頭文件時(shí),你會(huì)明白為什么我將Object作為結(jié)構(gòu)體的第一個(gè)字段。由于C按順序?qū)⒆侄畏湃虢Y(jié)構(gòu)體,并且由于指針可以指向一塊內(nèi)存,我就可以將指針轉(zhuǎn)換為任何我想要的東西。在這種情況下,即使我通過calloc獲取了一大塊內(nèi)存,我仍然可以使用Object指針來指向它。
當(dāng)我開始編寫ex19.h文件時(shí),我會(huì)把它解釋得更詳細(xì)一些,因?yàn)楫?dāng)你看到它怎么用的時(shí)候才能更容易去理解它。
上面的代碼創(chuàng)建了基本的對(duì)象系統(tǒng),但是你需要編譯它和將它鏈接到ex19.c文件,來創(chuàng)建出完整的程序。object.c文件本身并沒有main函數(shù),所以它不可能被編譯為完整的程序。下面是一個(gè)Makefile文件,它基于已經(jīng)完成的事情來構(gòu)建程序:
CFLAGS=-Wall -g
all: ex19
ex19: object.o
clean:
rm -f ex19
這個(gè)Makefile所做的事情僅僅是讓ex19依賴于object.o。還記得make可以根據(jù)擴(kuò)展名構(gòu)建不同的文件嗎?這相當(dāng)于告訴make執(zhí)行下列事情:
make時(shí),默認(rèn)的all會(huì)構(gòu)建ex19。ex19時(shí),也需要構(gòu)建object.o,并且將它包含在其中。make并不能找到object.o,但是它能發(fā)現(xiàn)object.c文件,并且知道如何把.c文件變成.o文件,所以它就這么做了。object.o文件構(gòu)建完成,它就會(huì)運(yùn)行正確的編譯命令,從ex19.c和object.o中構(gòu)建ex19。一旦你編寫完成了那些文件,你需要使用對(duì)象系統(tǒng)來實(shí)現(xiàn)實(shí)際的游戲,第一步就是把所有數(shù)據(jù)類型和函數(shù)聲明放在ex19.h文件中:
#ifndef _ex19_h
#define _ex19_h
#include "object.h"
struct Monster {
Object proto;
int hit_points;
};
typedef struct Monster Monster;
int Monster_attack(void *self, int damage);
int Monster_init(void *self);
struct Room {
Object proto;
Monster *bad_guy;
struct Room *north;
struct Room *south;
struct Room *east;
struct Room *west;
};
typedef struct Room Room;
void *Room_move(void *self, Direction direction);
int Room_attack(void *self, int damage);
int Room_init(void *self);
struct Map {
Object proto;
Room *start;
Room *location;
};
typedef struct Map Map;
void *Map_move(void *self, Direction direction);
int Map_attack(void *self, int damage);
int Map_init(void *self);
#endif
它創(chuàng)建了三個(gè)你將會(huì)用到的新對(duì)象:Monster,Room,和Map。
看一眼object.c:52,你可以看到這是我使用Object *el = calloc(1, size)的地方?;厝タ?code>object.h的NEW宏,你可以發(fā)現(xiàn)它獲得了另一個(gè)結(jié)構(gòu)體的sizeof,比如Room,并且分配了這么多的空間。然而,由于我像一個(gè)Object指針指向了這塊內(nèi)存,并且我在Room的開頭放置了Object proto,所以就可以將Room當(dāng)成Object來用。
詳細(xì)分解請(qǐng)見下面:
NEW(Room, "Hello."),C預(yù)處理器會(huì)將其展開為Object_new(sizeof(Room), RoomProto, "Hello.")。Object_new的內(nèi)部我分配了Room大小的一塊內(nèi)存,但是用Object *el來指向它。Room.proto字段放在開頭,這意味著el指針實(shí)際上指向了能訪問到完整Object結(jié)構(gòu)體的,足夠大小的一塊內(nèi)存。它不知道這塊內(nèi)存叫做proto。Object *el指針,通過*el = proto來設(shè)置這塊內(nèi)存的內(nèi)容。要記住你可以復(fù)制結(jié)構(gòu)體,而且*el的意思是“el所指向?qū)ο蟮闹怠?,所以整條語句意思是“將el所指向?qū)ο蟮闹蒂x給proto”。proto的正確數(shù)據(jù),這個(gè)函數(shù)接下來可以在Object上調(diào)用init,或者destroy。但是最神奇的一部分是無論誰調(diào)用這個(gè)函數(shù)都可以將它們改為想要的東西。結(jié)合上面這些東西,我就就可以使用者一個(gè)函數(shù)來創(chuàng)建新的類型,并且向它們提供新的函數(shù)來修改它們的行為。這看起來像是“黑魔法”,但它是完全有效的C代碼。實(shí)際上,有少數(shù)標(biāo)準(zhǔn)的系統(tǒng)函數(shù)也以這種方式工作,我們將會(huì)用到一些這樣的函數(shù)在網(wǎng)絡(luò)程序中轉(zhuǎn)換地址。
編寫完函數(shù)定義和數(shù)據(jù)結(jié)構(gòu)之后,我現(xiàn)在就可以實(shí)現(xiàn)帶有四個(gè)房間和一個(gè)牛頭人的游戲了。
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "ex19.h"
int Monster_attack(void *self, int damage)
{
Monster *monster = self;
printf("You attack %s!\n", monster->_(description));
monster->hit_points -= damage;
if(monster->hit_points > 0) {
printf("It is still alive.\n");
return 0;
} else {
printf("It is dead!\n");
return 1;
}
}
int Monster_init(void *self)
{
Monster *monster = self;
monster->hit_points = 10;
return 1;
}
Object MonsterProto = {
.init = Monster_init,
.attack = Monster_attack
};
void *Room_move(void *self, Direction direction)
{
Room *room = self;
Room *next = NULL;
if(direction == NORTH && room->north) {
printf("You go north, into:\n");
next = room->north;
} else if(direction == SOUTH && room->south) {
printf("You go south, into:\n");
next = room->south;
} else if(direction == EAST && room->east) {
printf("You go east, into:\n");
next = room->east;
} else if(direction == WEST && room->west) {
printf("You go west, into:\n");
next = room->west;
} else {
printf("You can't go that direction.");
next = NULL;
}
if(next) {
next->_(describe)(next);
}
return next;
}
int Room_attack(void *self, int damage)
{
Room *room = self;
Monster *monster = room->bad_guy;
if(monster) {
monster->_(attack)(monster, damage);
return 1;
} else {
printf("You flail in the air at nothing. Idiot.\n");
return 0;
}
}
Object RoomProto = {
.move = Room_move,
.attack = Room_attack
};
void *Map_move(void *self, Direction direction)
{
Map *map = self;
Room *location = map->location;
Room *next = NULL;
next = location->_(move)(location, direction);
if(next) {
map->location = next;
}
return next;
}
int Map_attack(void *self, int damage)
{
Map* map = self;
Room *location = map->location;
return location->_(attack)(location, damage);
}
int Map_init(void *self)
{
Map *map = self;
// make some rooms for a small map
Room *hall = NEW(Room, "The great Hall");
Room *throne = NEW(Room, "The throne room");
Room *arena = NEW(Room, "The arena, with the minotaur");
Room *kitchen = NEW(Room, "Kitchen, you have the knife now");
// put the bad guy in the arena
arena->bad_guy = NEW(Monster, "The evil minotaur");
// setup the map rooms
hall->north = throne;
throne->west = arena;
throne->east = kitchen;
throne->south = hall;
arena->east = throne;
kitchen->west = throne;
// start the map and the character off in the hall
map->start = hall;
map->location = hall;
return 1;
}
Object MapProto = {
.init = Map_init,
.move = Map_move,
.attack = Map_attack
};
int process_input(Map *game)
{
printf("\n> ");
char ch = getchar();
getchar(); // eat ENTER
int damage = rand() % 4;
switch(ch) {
case -1:
printf("Giving up? You suck.\n");
return 0;
break;
case 'n':
game->_(move)(game, NORTH);
break;
case 's':
game->_(move)(game, SOUTH);
break;
case 'e':
game->_(move)(game, EAST);
break;
case 'w':
game->_(move)(game, WEST);
break;
case 'a':
game->_(attack)(game, damage);
break;
case 'l':
printf("You can go:\n");
if(game->location->north) printf("NORTH\n");
if(game->location->south) printf("SOUTH\n");
if(game->location->east) printf("EAST\n");
if(game->location->west) printf("WEST\n");
break;
default:
printf("What?: %d\n", ch);
}
return 1;
}
int main(int argc, char *argv[])
{
// simple way to setup the randomness
srand(time(NULL));
// make our map to work with
Map *game = NEW(Map, "The Hall of the Minotaur.");
printf("You enter the ");
game->location->_(describe)(game->location);
while(process_input(game)) {
}
return 0;
}
說實(shí)話這里面并沒有很多你沒有見過的東西,并且你只需要理解我使用頭文件中宏的方法。下面是需要學(xué)習(xí)和理解的一些重要的核心知識(shí):
MonsterProto,RoomProto和MapProto。Object_new的實(shí)現(xiàn)方式,如果你沒有在你的原型中設(shè)置一個(gè)函數(shù),它會(huì)獲得在object.c中創(chuàng)建的默認(rèn)實(shí)現(xiàn)。Map_init中我創(chuàng)建了一個(gè)微型世界,然而更重要的是我使用了object.h中的NEW宏來創(chuàng)建全部對(duì)象。要把這一概念記在腦子里,可以試著把使用NEW的地方替換成Object_new的直接調(diào)用,來觀察它如何被替換。_(N)為我做了這些事情。如果你觀察代碼monster->_(attack)(monster, damage),你會(huì)看到我使用了宏將其替換成monster->proto.attack(monster, damage)。通過重新將這些調(diào)用寫成原始形式來再次學(xué)習(xí)這個(gè)轉(zhuǎn)換。另外,如果你被卡住了,手動(dòng)運(yùn)行cpp來查看究竟發(fā)生了什么。srand和rand,它們可以設(shè)置一個(gè)簡(jiǎn)單的隨機(jī)數(shù)生成器,對(duì)于游戲已經(jīng)夠用了。我也使用了time來初始化隨機(jī)數(shù)生成器。試著研究它們。getchar來從標(biāo)準(zhǔn)輸入中讀取單個(gè)字符。試著研究它。下面是我自己的游戲的輸出:
$ make ex19
cc -Wall -g -c -o object.o object.c
cc -Wall -g ex19.c object.o -o ex19
$ ./ex19
You enter the The great Hall.
> l
You can go:
NORTH
> n
You go north, into:
The throne room.
> l
You can go:
SOUTH
EAST
WEST
> e
You go east, into:
Kitchen, you have the knife now.
> w
You go west, into:
The throne room.
> s
You go south, into:
The great Hall.
> n
You go north, into:
The throne room.
> w
You go west, into:
The arena, with the minotaur.
> a
You attack The evil minotaur!
It is still alive.
> a
You attack The evil minotaur!
It is dead!
> ^D
Giving up? You suck.
$
我把所有assert檢查留給你作為練習(xí),我通常把它們作為軟件的一部分。你已經(jīng)看到了我如何使用assert來保證程序正確運(yùn)行。然而現(xiàn)在我希望你返回去并完成下列事情:
assert來保證參數(shù)正確。例如在Object_new中要添加assert(description != NULL)。Object_new在調(diào)用calloc之后應(yīng)該進(jìn)行assert(el != NULL)的檢查。NULL),或者添加一個(gè)斷言來確保返回值是有效的。例如,Object_new中,你需要在最后的返回之前添加assert(el != NULL),由于它不應(yīng)該為NULL。if語句,確保都有對(duì)應(yīng)的else語句,除非它用于錯(cuò)誤檢查并退出。switch語句,確保都有一個(gè)default分支,來處理非預(yù)期的任何情況。花費(fèi)一些時(shí)間瀏覽函數(shù)的每一行,并且找到你犯下的任何錯(cuò)誤。記住這個(gè)練習(xí)的要點(diǎn)是從“碼農(nóng)”轉(zhuǎn)變?yōu)椤昂诳汀?。試著找到使它崩潰的辦法,然后盡可能編寫代碼來防止崩潰或者過早退出。
Makefile文件,使之在執(zhí)行make clean時(shí)能夠同時(shí)清理object.o。Makefile使之能夠通過運(yùn)行make test來測(cè)試該游戲。.o。然后,使用它來編寫另一個(gè)小游戲。如果你正確編寫的話,你會(huì)在新游戲中創(chuàng)建新的Map和main函數(shù)。