笨辦法學(xué)C 練習(xí)19:一個簡單的對象系統(tǒng)

練習(xí)19:一個簡單的對象系統(tǒng)

原文:Exercise 19: A Simple Object System

譯者:飛龍

我在學(xué)習(xí)面向?qū)ο缶幊讨皩W(xué)了C炊汤,所以它有助于我在C中構(gòu)建面向?qū)ο笙到y(tǒng)老虫,來理解OOP的基本含義。你可能在學(xué)習(xí)C之前就學(xué)了OOP語言,所以這章也可能會起到一種銜接作用盒音。這個聯(lián)系中扣溺,你將會構(gòu)建一個簡單的對象系統(tǒng),但是也會了解更多關(guān)于C預(yù)處理器的事情镐确。

這個練習(xí)會構(gòu)建一個簡單的游戲包吝,在游戲中你會在一個小型的城堡中殺死彌諾陶洛斯饼煞,并沒有任何神奇之處,只是四個房間和一個壞家伙诗越。這個練習(xí)同時(shí)是一個多文件的項(xiàng)目砖瞧,并且比起之前的一些程序看起來更像一個真正的C程序。我在這個賈少C預(yù)處理器的原因嚷狞,是你需要它來在你自己的程序中創(chuàng)建多個文件块促。

C預(yù)處理器如何工作

C預(yù)處理器是個模板處理系統(tǒng),它主要的用途是讓C代碼的編程更加容易床未,但是它通過一個語法感知的模板機(jī)制來實(shí)現(xiàn)竭翠。以前人們主要使用C預(yù)處理器來儲存常量,以及創(chuàng)建“宏”來簡化復(fù)雜的代碼薇搁。在現(xiàn)代C語言中你會實(shí)際上使用它作為代碼生成器來創(chuàng)建模板化的代碼片段斋扰。

C預(yù)處理器的工作原理是,如果你給它一個文件啃洋,比如.c文件褥实,它會處理以#(井號)字符開頭的各種文本。當(dāng)它遇到一個這樣的文本時(shí)裂允,它會對輸入文件中的文本做特定的替換损离。C預(yù)處理器的主要優(yōu)點(diǎn)是他可以包含其他文件,并且基于該文件的內(nèi)容對它的宏列表進(jìn)行擴(kuò)展绝编。

一個快速查看預(yù)處理器所做事情的方法僻澎,是對上個練習(xí)中的代碼執(zhí)行下列命令:

cpp ex18.c | less

這會產(chǎn)生大量輸出,但是如果你滾動它十饥,會看到你使用#include包含的其他文件的內(nèi)容窟勃。在原始的代碼中向下滾動,你可以看到cpp如何基于頭文件中不同的#define宏來轉(zhuǎn)換代碼逗堵。

C編譯器與cpp的集成十分緊密秉氧,這個例子只是向你展示它是如何在背后工作的。在現(xiàn)代C語言中蜒秤,cpp系統(tǒng)也集成到C的函數(shù)中汁咏,你或許可以將它當(dāng)做C語言的一部分。

在剩余的章節(jié)中作媚,我們會使用更多預(yù)處理器的語法攘滩,并且像往常一樣解釋它們。

原型對象系統(tǒng)

我們所創(chuàng)建的OOP系統(tǒng)是一個簡單的“原型”風(fēng)格的對象系統(tǒng)纸泡,很像JavaScript漂问。你將以設(shè)置為字段的原型來開始,而不是類,接著將他們用作創(chuàng)建其它對象實(shí)例的基礎(chǔ)蚤假。這個“沒有類”的設(shè)計(jì)比起傳統(tǒng)的基于類的對象系統(tǒng)更加易于實(shí)現(xiàn)和使用栏饮。

Object頭文件

我打算將數(shù)據(jù)類型和函數(shù)聲明放在一個單獨(dú)的頭文件中,叫做object.h磷仰。這個是一個標(biāo)準(zhǔn)的C技巧抡爹,可以讓你集成二進(jìn)制庫,但其它程序員任然需要編譯芒划。在這個文件中冬竟,我使用了多個高級的C預(yù)處理器技巧,我接下來準(zhǔn)備簡略地描述它們民逼,并且你會在后續(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

看一看這個文件,你會發(fā)現(xiàn)我使用了幾個新的語法片段拼苍,你之前從來沒見過它們:

#ifndef

你已經(jīng)見過了用于創(chuàng)建簡單常量的#define笑诅,但是C預(yù)處理器可以根據(jù)條件判斷來忽略一部分代碼。這里的#ifndef是“如果沒有被定義”的意思疮鲫,它會檢查是否已經(jīng)出現(xiàn)過#define _object_h吆你,如果已出現(xiàn),就跳過這段代碼俊犯。我之所以這樣寫妇多,是因?yàn)槲覀兛梢詫⑦@個文件包含任意次,而無需擔(dān)心多次定義里面的東西燕侠。

#define

有了上面保護(hù)該文件的#ifndef者祖,我們接著添加_object_h的定義,因此之后任何試圖包含此文件的行為绢彤,都會由于上面的語句而跳過這段代碼七问。

#define NEW(T,N)

這條語句創(chuàng)建了一個宏,就像模板函數(shù)一樣茫舶,無論你在哪里編寫左邊的代碼械巡,都會展開成右邊的代碼。這條語句僅僅是對我們通常調(diào)用的Object_new制作了一個快捷方式饶氏,并且避免了潛在的調(diào)用錯誤讥耗。在宏這種工作方式下,T嚷往、N還有New都被“注入”進(jìn)了右邊的代碼中葛账。T##Proto語法表示“將Proto連接到T的末尾”,所以如果你寫下NEW(Room, "Hello.")皮仁,就會在這里變成RoomProto

#define _(N)

這個宏是一種為對象系統(tǒng)設(shè)計(jì)的“語法糖”,將obj->proto.blah簡寫為obj->_(blah)贷祈。它不是必需的趋急,但是它是一個接下來會用到的有趣的小技巧。

Object源文件

object.h是聲明函數(shù)和數(shù)據(jù)類型的地方势誊,它們在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;
    }
}

這個文件中并沒有什么新東西,除了一個小技巧之外粟耻。Object_new函數(shù)通過把原型放到結(jié)構(gòu)體的開頭查近,利用了structs工作機(jī)制的一個方面。當(dāng)你在之后看到ex19.h頭文件時(shí)挤忙,你會明白為什么我將Object作為結(jié)構(gòu)體的第一個字段霜威。由于C按順序?qū)⒆侄畏湃虢Y(jié)構(gòu)體,并且由于指針可以指向一塊內(nèi)存册烈,我就可以將指針轉(zhuǎn)換為任何我想要的東西戈泼。在這種情況下,即使我通過calloc獲取了一大塊內(nèi)存赏僧,我仍然可以使用Object指針來指向它大猛。

當(dāng)我開始編寫ex19.h文件時(shí),我會把它解釋得更詳細(xì)一些淀零,因?yàn)楫?dāng)你看到它怎么用的時(shí)候才能更容易去理解它挽绩。

上面的代碼創(chuàng)建了基本的對象系統(tǒng),但是你需要編譯它和將它鏈接到ex19.c文件驾中,來創(chuàng)建出完整的程序琼牧。object.c文件本身并沒有main函數(shù),所以它不可能被編譯為完整的程序哀卫。下面是一個Makefile文件巨坊,它基于已經(jīng)完成的事情來構(gòu)建程序:

CFLAGS=-Wall -g

all: ex19

ex19: object.o

clean:
  rm -f ex19

這個Makefile所做的事情僅僅是讓ex19依賴于object.o。還記得make可以根據(jù)擴(kuò)展名構(gòu)建不同的文件嗎此改?這相當(dāng)于告訴make執(zhí)行下列事情:

  • 當(dāng)我運(yùn)行make時(shí)趾撵,默認(rèn)的all會構(gòu)建ex19
  • 當(dāng)它構(gòu)建ex19時(shí)共啃,也需要構(gòu)建object.o占调,并且將它包含在其中。
  • make并不能找到object.o移剪,但是它能發(fā)現(xiàn)object.c文件究珊,并且知道如何把.c文件變成.o文件,所以它就這么做了纵苛。
  • 一旦object.o文件構(gòu)建完成剿涮,它就會運(yùn)行正確的編譯命令言津,從ex19.cobject.o中構(gòu)建ex19

游戲?qū)崿F(xiàn)

一旦你編寫完成了那些文件取试,你需要使用對象系統(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)建了三個你將會用到的新對象:MonsterRoom瞬浓,和Map初婆。

看一眼object.c:52,你可以看到這是我使用Object *el = calloc(1, size)的地方猿棉“跖眩回去看object.hNEW宏,你可以發(fā)現(xiàn)它獲得了另一個結(jié)構(gòu)體的sizeof萨赁,比如Room弊琴,并且分配了這么多的空間。然而位迂,由于我像一個Object指針指向了這塊內(nèi)存访雪,并且我在Room的開頭放置了Object proto,所以就可以將Room當(dāng)成Object來用掂林。

詳細(xì)分解請見下面:

  • 我調(diào)用了NEW(Room, "Hello.")臣缀,C預(yù)處理器會將其展開為Object_new(sizeof(Room), RoomProto, "Hello.")
  • 執(zhí)行過程中泻帮,在Object_new的內(nèi)部我分配了Room大小的一塊內(nèi)存精置,但是用Object *el來指向它。
  • 由于C將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ū)ο蟮闹怠被鹋罚哉麠l語句意思是“將el所指向?qū)ο蟮闹蒂x給proto”。
  • 由于這個謎之結(jié)構(gòu)體被填充為來自proto的正確數(shù)據(jù)茎截,這個函數(shù)接下來可以在Object上調(diào)用init苇侵,或者destroy。但是最神奇的一部分是無論誰調(diào)用這個函數(shù)都可以將它們改為想要的東西企锌。

結(jié)合上面這些東西榆浓,我就就可以使用者一個函數(shù)來創(chuàng)建新的類型,并且向它們提供新的函數(shù)來修改它們的行為撕攒。這看起來像是“黑魔法”陡鹃,但它是完全有效的C代碼烘浦。實(shí)際上,有少數(shù)標(biāo)準(zhǔn)的系統(tǒng)函數(shù)也以這種方式工作杉适,我們將會用到一些這樣的函數(shù)在網(wǎng)絡(luò)程序中轉(zhuǎn)換地址谎倔。

編寫完函數(shù)定義和數(shù)據(jù)結(jié)構(gòu)之后柳击,我現(xiàn)在就可以實(shí)現(xiàn)帶有四個房間和一個牛頭人的游戲了猿推。

#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í)現(xiàn)一個原型涉及到創(chuàng)建它的函數(shù)版本蹬叭,以及隨后創(chuàng)建一個以“Proto”結(jié)尾的單一結(jié)構(gòu)體。請參照MonsterProto状知,RoomProtoMapProto秽五。
  • 由于Object_new的實(shí)現(xiàn)方式,如果你沒有在你的原型中設(shè)置一個函數(shù)饥悴,它會獲得在object.c中創(chuàng)建的默認(rèn)實(shí)現(xiàn)坦喘。
  • Map_init中我創(chuàng)建了一個微型世界,然而更重要的是我使用了object.h中的NEW宏來創(chuàng)建全部對象西设。要把這一概念記在腦子里瓣铣,可以試著把使用NEW的地方替換成Object_new的直接調(diào)用,來觀察它如何被替換贷揽。
  • 使用這些對象涉及到在它們上面調(diào)用函數(shù)棠笑,_(N)為我做了這些事情。如果你觀察代碼monster->_(attack)(monster, damage)禽绪,你會看到我使用了宏將其替換成monster->proto.attack(monster, damage)蓖救。通過重新將這些調(diào)用寫成原始形式來再次學(xué)習(xí)這個轉(zhuǎn)換。另外印屁,如果你被卡住了循捺,手動運(yùn)行cpp來查看究竟發(fā)生了什么。
  • 我使用了兩個新的函數(shù)srandrand雄人,它們可以設(shè)置一個簡單的隨機(jī)數(shù)生成器从橘,對于游戲已經(jīng)夠用了。我也使用了time來初始化隨機(jī)數(shù)生成器柠衍。試著研究它們洋满。
  • 我使用了一個新的函數(shù)getchar來從標(biāo)準(zhǔn)輸入中讀取單個字符。試著研究它珍坊。

你會看到什么

下面是我自己的游戲的輸出:

$ 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.
$

審計(jì)該游戲

我把所有assert檢查留給你作為練習(xí)牺勾,我通常把它們作為軟件的一部分。你已經(jīng)看到了我如何使用assert來保證程序正確運(yùn)行阵漏。然而現(xiàn)在我希望你返回去并完成下列事情:

  • 查看你定義的每個函數(shù)驻民,一次一個文件翻具。
  • 在每個函數(shù)的最上面,添加assert來保證參數(shù)正確回还。例如在Object_new中要添加assert(description != NULL)裆泳。
  • 瀏覽函數(shù)的每一行,找到所調(diào)用的任何函數(shù)柠硕。閱讀它們的文檔(或手冊頁)工禾,確認(rèn)它們在錯誤下返回什么。添加另一個斷言來檢查錯誤是否發(fā)生蝗柔。例如闻葵,Object_new在調(diào)用calloc之后應(yīng)該進(jìn)行assert(el != NULL)的檢查。
  • 如果函數(shù)應(yīng)該返回一個值癣丧,也確保它返回了一個錯誤值(比如NULL)槽畔,或者添加一個斷言來確保返回值是有效的。例如胁编,Object_new中厢钧,你需要在最后的返回之前添加assert(el != NULL),由于它不應(yīng)該為NULL嬉橙。
  • 對于每個你編寫的if語句早直,確保都有對應(yīng)的else語句,除非它用于錯誤檢查并退出憎夷。
  • 對于每個你編寫的switch語句莽鸿,確保都有一個default分支,來處理非預(yù)期的任何情況拾给。

花費(fèi)一些時(shí)間瀏覽函數(shù)的每一行祥得,并且找到你犯下的任何錯誤。記住這個練習(xí)的要點(diǎn)是從“碼農(nóng)”轉(zhuǎn)變?yōu)椤昂诳汀苯谩T囍业绞顾罎⒌霓k法级及,然后盡可能編寫代碼來防止崩潰或者過早退出。

附加題

  • 修改Makefile文件额衙,使之在執(zhí)行make clean時(shí)能夠同時(shí)清理object.o饮焦。
  • 編寫一個測試腳本,能夠以多種方式來調(diào)用該游戲窍侧,并且擴(kuò)展Makefile使之能夠通過運(yùn)行make test來測試該游戲县踢。
  • 在游戲中添加更多房間和怪物。
  • 把游戲的邏輯放在其它文件中伟件,并把它編譯為.o硼啤。然后,使用它來編寫另一個小游戲斧账。如果你正確編寫的話谴返,你會在新游戲中創(chuàng)建新的Mapmain函數(shù)煞肾。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市嗓袱,隨后出現(xiàn)的幾起案子籍救,更是在濱河造成了極大的恐慌,老刑警劉巖渠抹,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝙昙,死亡現(xiàn)場離奇詭異,居然都是意外死亡逼肯,警方通過查閱死者的電腦和手機(jī)耸黑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門桃煎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來篮幢,“玉大人,你說我怎么就攤上這事为迈∪唬” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵葫辐,是天一觀的道長搜锰。 經(jīng)常有香客問我,道長耿战,這世上最難降的妖魔是什么蛋叼? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮剂陡,結(jié)果婚禮上狈涮,老公的妹妹穿的比我還像新娘。我一直安慰自己鸭栖,他們只是感情好歌馍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著晕鹊,像睡著了一般松却。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上溅话,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天晓锻,我揣著相機(jī)與錄音,去河邊找鬼飞几。 笑死砚哆,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的循狰。 我是一名探鬼主播窟社,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼券勺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了灿里?” 一聲冷哼從身側(cè)響起关炼,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎匣吊,沒想到半個月后儒拂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡色鸳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年社痛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片命雀。...
    茶點(diǎn)故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蒜哀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吏砂,到底是詐尸還是另有隱情撵儿,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布狐血,位于F島的核電站淀歇,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏匈织。R本人自食惡果不足惜浪默,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望缀匕。 院中可真熱鬧纳决,春花似錦、人聲如沸弦追。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽劲件。三九已至掸哑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間零远,已是汗流浹背苗分。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留牵辣,地道東北人摔癣。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親择浊。 傳聞我的和親對象是個殘疾皇子戴卜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)琢岩,斷路器投剥,智...
    卡卡羅2017閱讀 134,656評論 18 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法担孔,內(nèi)部類的語法江锨,繼承相關(guān)的語法,異常的語法糕篇,線程的語...
    子非魚_t_閱讀 31,631評論 18 399
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,116評論 25 707
  • 【01】 還有不到一個月浮毯,又一屆新生,就將走入大學(xué)校園了泰鸡。 經(jīng)歷了小學(xué)六年、中學(xué)六年的寒窗苦讀壳鹤,經(jīng)歷了黑色高三和高...
    朱若霞閱讀 955評論 2 9
  • 這是意大利盛龄、瑞士旅行記錄終站。 自由行準(zhǔn)備攻略:超詳細(xì)歐洲自由行準(zhǔn)備攻略(意大利瑞士) 第一站羅馬:漫步羅馬——古...
    琴鳴閱讀 504評論 0 0