深入理解C++对象模型

1. C++程序设计模型支持的三种程序设计模型:
1.1. 程序模型(procedural model、OP)可以理解为过程化模型就像C一样
char boy[] = "Tom";
char* p_son;
...
p_son = new char[strlen(boy) + 1];
strcpy(p_son,boy);
...
if(!strcmp(p_son,boy))
{
    take_to_disneyland(boy);
}

可以看出面向过程模型就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了,如上图代码所示,对字符串的处理分为申请内存、复制数据,比较数据差异几个步骤,每个步骤都需要手动调用C库函数,且步骤的顺序是固定的,可以把面向过程理解为自顶向下的编程,其特点为代码容易理解,主要是算法+数据结构,确定也显而易见,对于复杂系统,难以复用

1.2. 抽象数据类型模型(abstract data type model、ADT) 可以理解为非多态的数据封装模型,和OO相比拥有更快的速度而且空间更紧凑(因为不需要virtual)。例如CString ,string 类
string boy = "Tom";
string son;
...
//string::operator=()
son = boy;
...
//string::operator==()
if(son == boy)
    take_to_disneyland(boy);

可以看到ADT模型这里对字符串进行了封装,并对外提供了一系列接口,这便是抽象的过程,对字符串的处理不再需要手动地去使用C库函数,取而代之的是一系列诸如“=” ,“==”的针对字符串类的抽象符号重载,这便是高度实物的抽象化,可复用性明显增强了

1.3. 面向对象模型(object-oriented model、OO)
void check_in(Library_materials* p_mat)
{
    if(p_mat->late())
        p_mat->fine();
    p_mat->check_in();

    if(Lender* p_lend = p_mat->reserved())
        p_mat->notify(p_lend);
}

Library_materials即为抽象的base class(用以提供共通接口),真正的subtype例如Book、Video、Laptop等等都可以从它派生而来,其特点为集齐了封装、抽象、继承、多态特点,适用大型复杂系统且易复用

2. C++对于程序模型的设计

早期的C++对象模型设计并不是一蹴而就的,而是经过一系列的对比选择、优化等流程,首先我们来看下面一个例子来看几种可能的模型,对于class Point类

class Point
{
    public:
        Point(float val);
        virtual ~Point();

        float x() const;
        static int PointCount();

    protected:
        virtual ostream& print(ostream& os) const;

        float m_x;
        static int m_iPointCount;
}

Point存在static 、nonstatic data member;static 、nonstatic 、virtual function member,其在运行时会是怎样的表现呢,编译器如何模塑出这个类的data member呢和function members呢,首先是一种较为简单的实现

2.1. 简单对象模型(A Simple Object Model)
简单对象模型.png

简单对象模型在内存上的结构十分简洁,一个类的成员函数和成员变量都有自己的一个slot(可以理解为槽),每一个slot相当于一个指针,指向对应的函数或变量,内存连续分布,这种设计可以让编译器做更少的事,且类的内存存放的都是同一种类型,因此也较容易管理(类中的成员可以用slot的索引值来寻址,指针大小*索引),但是显而易见的是,空间和执行期的效率较低,毕竟函数在内存布局中也占有一个槽,且成员变量的每次访问都需要经由slot去跳转地址访问,这个模型没有应用到C++实际的编译器产品中,但是其根据slot索引去寻址的观念,被应用到C++的"指向成员的指针"观念中。

class CDerive{
public :
    int m_iBase, m_iDervie;
}

//pmInt指针为数据成员指针,可以指向CDerive对象的所有int数据成
//员  这里会给指针赋值4,即m_iDervie在class中的偏移量,这其实跟slot索引去寻址如出一辙
int (CDerive::*pmInt) = &CDerive::m_iDervie;
    002A1730  mov         dword ptr [pmInt],4  

CDerive Dervie;
    002A1737  lea         ecx,[Dervie]  
    002A173A  call        CDerive::CDerive (02A10A0h)  
    002A173F  mov         dword ptr [ebp-4],0  

Dervie.*pmInt = 8;
    002A1746  mov         eax,dword ptr [pmInt]  
    002A1749  mov         dword ptr Dervie[eax],8  

2.2. 表格驱动对象模型(A Table-driven Object Model)
表格驱动对象模型.png

简单对象模型实际上每个类的大小是不固定的(每个类的成员变量跟函数数量不确定,slot数量不确定),为了将所有的类的所有对象都有一致的内存表达方式,表格驱动对象模型出现了,类本身只含有两个指针(可以理解为slot),一个指针指向数据表,一个指向函数表,数据表本身包含数据,函数表则包含多个slot,每个slot指向对应的成员函数,这个模型也没有实际应用到C++编译器上(成员变量访问效率还是低,且对于类的成员函数界定不明确),但是成员函数表这个观念却成为C++实现虚函数机制的一个有效方案。

2.3. C++对象模型(The C++ Object Model)
image.png

Stroustrup(C++之父)当初设计的C++对象模型是从简单对象模型派生而来的,空间上采取了折中的方法,非静态成员变量直接存储在类对象内存中,静态成员变量及静态、非静态非虚函数放在类对象内存之外,即不会在内存布局中存放指向函数的slot指针,这样就可以使得非静态成员变量访问效率提高的同时内存空间也在一个可以接受的范围,对于剩下的虚函数部分,则由两个步骤支持:
1.每一个类有多少虚函数,就产生多少个指针,指向对应的虚函数地址,这些指针顺序放置于表格之中,这个表格即virtual table
2.每一个类对象会在内存中添加相应的指针,指向相关的virtual table,这个指针通常被叫做vptr

3. 解释各种情况下的C++模型构建

上述只是简单介绍了C++对象模型的选择依据及大体结构,事实上,对于不同的情况,模型的结构也会有差异,大体可以分为单继承、多继承、菱形继承,单一虚继承、虚拟菱形继承。

3.1 单继承
class CBase
{
public:
    CBase()
    {
        printf("CBase\r\n");
    }
    virtual ~CBase()
    {
        printf("~CBase\r\n");
    }

    void SetNumber(int iNumber)
    {
        m_iBase = iNumber;
    }
    int GetNumber()
    {
        return m_iBase;
    }

    virtual void Show()
    {
        printf("CBase Show\r\n");
    }

    static void Print()
    {
        printf("~CBase Print\r\n");
    }

public:
    int m_iBase;
};

class CDerive : public CBase
{
public:
    virtual ~CDerive()
    {
        printf("~CDerive\r\n");
    }
    virtual void say(){};
    void ShowNumber(int iNumber)
    {
        SetNumber(iNumber);
        m_iDervie = iNumber + 1;
        printf("%d\r\n",GetNumber());
        printf("%d\r\n",m_iDervie);
    }

public:
    int m_iDervie;
};

int main(int argc,char* argv[])
{
    CDerive Dervie;
}

可以看到CBase有一个非静态成员变量,两个虚函数,三个非静态成员函数,一个静态成员函数,若不考虑继承关系,单独的CBase内存结构如下图所示


image.png

如前面所讲的一致,单一类若存在虚函数,编译器便会创建一个虚函数表,表中指针分别指向对应的虚函数地址(按照虚函数声明顺序依次对应放置),使用一个指针(Vptr)存储虚函数表地址,其余的函数处于代码段且不会占用到虚函数表的slot。

对于单继承类CDerive ,其内存结构如下所示


image.png

CDerive 在CBase的基础上多了一个m_iDerive变量(置于尾部),对于虚函数表,我们可以认为编译器先复制了一份CBase的虚函数表,在它的基础上找到那些被当前类重写的函数,把对应slot指向的地址改为重写的函数地址,也即上面的CDerive ::~CDerive ,此时编译器发现当前类还有一个自己的虚函数,将虚函数表添加一项slot,指向该虚函数,然后把对象的Vptr指针指向新的虚函数表地址处,值得注意的是,类的所有对象共享一份虚函数表,如上面所展示CBase虚函数表跟CDerive虚函数表,可以该类的所有对象共用,即同一类所有对象Vptr指向同一个虚函数表地址,虚函数表在msvc编译环境下放置于常量段中,可以如下验证:


image.png
image.png
image.png
image.png

可以看到对于同一个类产生的CDerive对象,Vptr指向的虚函数表地址是一致的,我们通过地址找到对应内存,可以看到,虚函数表地址处开始的12个字节存储的正是3个虚函数的地址(每个四字节),通过虚函数表记录的虚函数地址我们可以通过反汇编工具找到对应的代码段位置。

3.2 多继承(非菱形继承)
class CSofa
{
public:
    CSofa()
    {
        m_iColor = 2;
    }
    virtual ~CSofa()
    {
        printf("~CSofa\r\n");
    }
    virtual int GetColor()
    {
        return m_iColor;
    }
    virtual int SitDown()
    {
        return printf("CSofa SitDown\r\n");
    }

protected:
    int m_iColor;;
};

class CBed
{
public:
    CBed()
    {
        m_iLength = 4;
        m_iWidth = 5;
    }
    virtual ~CBed()
    {
        printf("~CBed\r\n");
    }
    virtual int GetArea()
    {
        return m_iLength * m_iWidth;
    }
    virtual int Sleep()
    {
        return printf("CBed Sleep\r\n");
    }

protected:
    int m_iLength;
    int m_iWidth;
};

class CSofaBed : public CSofa, public CBed
{
public:
    CSofaBed()
    {
        m_iHeight = 6;
    }
    virtual ~CSofaBed()
    {
        printf("~CSofaBed\r\n");
    }
    virtual int SitDown()
    {
        return printf("CSofaBed SitDown\r\n");
    }
    virtual int Sleep()
    {
        return printf("CSofaBed Sleep\r\n");
    }
    virtual int GetHeight()
    {
        return m_iHeight;
    }

protected:
    int m_iHeight;
};


int main(int argc, char* argv[])
{
    CSofaBed sofaBed;
}

多继承一个首要的问题是类的虚函数表需要几个,虚函数指针(Vptr)需要几个,内存如何布局?

  • 直接继承的有虚函数的父类有多少个,便有多少个虚函数表,也会有相应数量的虚函数指针,对于本子类新增的虚函数,则统一放在第一个虚函数表中
  • 内存布局中,父类按照声明顺序排列
    其对象模型如下图所示:


    image.png

    可以看到非菱形多继承在单继承的基础上对多个父类进行布局,对父类虚函数表‘’拷贝‘后‘’重写’的步骤其实是一样的,只是需要考虑本子类新增的虚函数放置的位置,目前的编译器大多将其放于第一个基类(存在虚函数表)的虚函数表中

3.3 菱形继承

菱形继承也称为钻石型继承或重复继承,它指的是基类被某个派生类简单重复继承了多次。这样,派生类对象中拥有多份基类实例


image.png
#include "stdio.h"

class CFurniture
{
public:
    CFurniture()
    {
        m_iPrice = 0;
    }
    virtual ~CFurniture()
    {
        printf("~CFurniture()\r\n");
    }
    virtual int GetPrice()
    {
        return printf("CFurniture GetPrice\r\n");
    }

public:
    int m_iPrice;
};

class CSofa : public CFurniture
{
public:
    CSofa()
    {
        m_iPrice = 1;
        m_iColor = 2;
    }
    virtual ~CSofa()
    {
        printf("~CSofa\r\n");
    }
    virtual int GetColor()
    {
        return printf("CSofa GetColor\r\n");
    }
    virtual int SitDown()
    {
        return printf("CSofa SitDown\r\n");
    }

protected:
    int m_iColor;
};

class CBed : public CFurniture
{
public:
    CBed()
    {
        m_iPrice = 3;
        m_iLength = 4;
        m_iWidth = 5;
    }
    virtual ~CBed()
    {
        printf("~CBed\r\n");
    }
    virtual int GetArea()
    {
        return printf("CBed GetArea\r\n");
    }
    virtual int Sleep()
    {
        return printf("CBed Sleep\r\n");
    }

protected:
    int m_iLength;
    int m_iWidth;
};

class CSofaBed : public CSofa, public CBed
{
public:
    CSofaBed()
    {
        m_iHeight = 6;
    }
    virtual ~CSofaBed()
    {
        printf("~CSofaBed\r\n");
    }
    virtual int SitDown()
    {
        return printf("CSofaBed SitDown\r\n");
    }
    virtual int Sleep()
    {
        return printf("CSofaBed Sleep\r\n");
    }
    virtual int GetHeight()
    {
        return printf("CSofaBed GetHeight\r\n");
    }

protected:
    int m_iHeight;
};


int main(int argc, char* argv[])
{
    CSofaBed sofaBed;
    sofaBed.m_iPrice = 1;//error
    sofaBed.CSofa::m_iPrice = 1;//success
    sofaBed.CBed::m_iPrice = 1;//success
}

其内存布局如下图所示:


image.png

可以看到由于CSofaBed 间接继承了CFurniture两次,导致内存中有两个m_iPrice数据成员,增大了空间,也易引发歧义

3.4 简单虚继承

为了解决上述菱形继承所带来的空间及歧义问题,虚继承出现了,在C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr),在Microsoft Visual C++中,虚基类表指针总是在虚函数表指针之后,因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的是偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。我们通过一张图来更好地理解。


image.png
class CFurniture
{
public:
    CFurniture()
    {
        m_iPrice = 0;
    }
    virtual int GetPrice()
    {
        return printf("CFurniture GetPrice\r\n");;
    }

public:
    int m_iPrice;
};

class CSofaVirtual : virtual public CFurniture
{
public:
    CSofaVirtual()
    {
        m_iPrice = 1;
        m_iColor = 2;
    }
    virtual int GetColor()
    {
        return printf("CSofaVirtual GetColor\r\n");;
    }
    virtual int SitDown()
    {
        return printf("CSofaVirtual SitDown\r\n");
    }

protected:
    int m_iColor;
};

此时CSofaVirtual 的内存模型如下图所示:


image.png

我们使用指针对内存进行访问及打印输出,以便更好地理解模型

int main(int argc, char* argv[])
{
    typedef void(*Fun)(void);

    CSofaVirtual sofa;

    cout << "CSofaVirtual对象内存大小为:" << sizeof(CSofaVirtual) << endl;

    //取得CSofaVirtual的虚函数表
    cout << "[0]CSofaVirtual::vptr";
    cout << "\t地址:" << (int *)(&sofa) << endl;

    //输出虚表CSofaVirtual::vptr中的函数
    for (int i = 0; i<2; ++i)
    {
        cout << "  [" << i << "]";
        Fun fun1 = (Fun)*((int *)*(int *)(&sofa) + i);
        fun1();
        cout << "\t地址:\t" << *((int *)*(int *)(&sofa) + i) << endl;
    }

    cout << "[1]vbptr ";
    cout << "\t地址:" << (int *)(&sofa) + 1 << endl;  //虚表指针的地址

    //输出虚基类指针条目所指的内容
    for (int i = 0; i < 2; i++)
    {
        cout << "  [" << i << "]";

        cout << *(int *)((int *)*((int *)(&sofa) + 1) + i);

        cout << endl;
    }

    //[2]
    cout << "[2]CSofaVirtual::m_iColor=" << *(int*)((int *)(&sofa) + 2);
    cout << "\t地址:" << (int *)(&sofa) + 2;
    cout << endl;
    cout << "-------------------------------------------------" << endl;

    //[3]
    cout << "[3]CFurniture::vptr";
    cout << "\t地址:" << (int *)(&sofa) + 3 << endl;

    //输出CFurniture::vptr中的虚函数
    for (int i = 0; i<1; ++i)
    {
        cout << "  [" << i << "]";
        Fun fun1 = (Fun)*((int *)*((int *)(&sofa) + 3) + i);
        fun1();
        cout << "\t地址:\t" << *((int *)*((int *)(&sofa) + 3) + i) << endl;
    }

    //[5]
    cout << "[4]CFurniture::m_iPrice=" << *(int*)((int *)(&sofa) + 4);
    cout << "\t地址: " << (int *)(&sofa) + 4;

    cout << endl;

}
image.png

分析后我们可以得出,vbptr存储的第二个字段(本例中为8)的数值即为基类CFurniture相对于vbptr的偏移值,当需要访问CFurniture的数据时,通过vbptr存储的偏移值去寻址。

3.5 虚拟菱形继承

好了,现在我们使用虚继承来优化菱形继承

class CFurniture
{
public:
    CFurniture()
    {
        m_iPrice = 0;
    }
    virtual int GetPrice()
    {
        return printf("CFurniture GetPrice\r\n");
    }

protected:
    int m_iPrice;
};

class CSofa :virtual public CFurniture
{
public:
    CSofa()
    {
        m_iPrice = 1;
        m_iColor = 2;
    }
    virtual int GetColor()
    {
        return printf("CSofa GetColor\r\n");
    }
    virtual int SitDown()
    {
        return printf("CSofa SitDown\r\n");
    }

protected:
    int m_iColor;;
};

class CBed :virtual public CFurniture
{
public:
    CBed()
    {
        m_iPrice = 3;
        m_iLength = 4;
        m_iWidth = 5;
    }
    virtual int GetArea()
    {
        return printf("CBed GetArea\r\n");
    }
    virtual int Sleep()
    {
        return printf("CBed Sleep\r\n");
    }

protected:
    int m_iLength;
    int m_iWidth;
};

class CSofaBed : public CSofa, public CBed
{
public:
    CSofaBed()
    {
        m_iHeight = 6;
    }
    virtual int SitDown()
    {
        return printf("CSofaBed SitDown\r\n");
    }
    virtual int Sleep()
    {
        return printf("CSofaBed Sleep\r\n");
    }
    virtual int GetHeight()
    {
        return printf("CSofaBed GetHeight\r\n");
    }

protected:
    int m_iHeight;
};

其内存布局如下图所示:


image.png

对比单一虚继承

image.png

可以看到与单一虚继承相比,此例的虚拟菱形继承多了次父类CBed跟最派生类CSofaBed,需要考虑的主要有两点,一是次父类CBed跟CSofaBed内存如何布局,二是CSofaBed类新增的虚函数应该放置于哪张虚函数表

  • 次父类按照声明的顺序顺序放置,最派生类放置于所有次父类之后,最后是虚祖父类
  • CSofaBed新增的虚函数放置于第一张虚函数表处

我们使用指针对内存进行访问及打印输出,以便更好地理解模型

int main(int argc,char* argv[])
{
    typedef void(*Fun)(void);

    CSofaBed sofaBed;

    cout << "sofaBed对象内存大小为:" << sizeof(sofaBed) << endl;

    //取得CSofa的虚函数表
    cout << "[0]CSofa::vptr";
    cout << "\t地址:" << (int *)(&sofaBed) << endl;

    //输出虚表CSofa::vptr中的函数
    for (int i = 0; i<3; ++i)
    {
        cout << "  [" << i << "]";
        Fun fun1 = (Fun)*((int *)*(int *)(&sofaBed) + i);
        fun1();
        cout << "\t地址:\t" << *((int *)*(int *)(&sofaBed) + i) << endl;
    }

    //[1]
    cout << "[1]CSofa::vbptr ";
    cout << "\t地址:" << (int *)(&sofaBed) + 1 << endl;  //虚表指针的地址
    //输出虚基类指针条目所指的内容
    for (int i = 0; i < 2; i++)
    {
        cout << "  [" << i << "]";

        cout << *(int *)((int *)*((int *)(&sofaBed) + 1) + i);

        cout << endl;
    }


    //[2]
    cout << "[2]CSofa::m_iColor=" << *(int*)((int *)(&sofaBed) + 2);
    cout << "\t地址:" << (int *)(&sofaBed) + 2;
    cout << endl;
    cout << "-------------------------------------------------"<<endl;

    //[3]
    cout << "[3]CBed::vptr";
    cout << "\t地址:" << (int *)(&sofaBed) + 3 << endl;

    //输出CBed::vptr中的虚函数
    for (int i = 0; i<2; ++i)
    {
        cout << "  [" << i << "]";
        Fun fun1 = (Fun)*((int *)*((int *)(&sofaBed) + 3) + i);
        fun1();
        cout << "\t地址:\t" << *((int *)*((int *)(&sofaBed) + 3) + i) << endl;
    }

    //[4]
    cout << "[4]CBed::vbptr ";
    cout << "\t地址:" << (int *)(&sofaBed) + 4 << endl;  //虚表指针的地址
    //输出虚基类指针条目所指的内容
    for (int i = 0; i < 2; i++)
    {
        cout << "  [" << i << "]";

        cout << *(int *)((int *)*((int *)(&sofaBed) + 4) + i);

        cout << endl;
    }

    //[5]
    cout << "[5]CBed::m_iLength=" << *(int*)((int *)(&sofaBed) + 5);
    cout << "\t地址: " << (int *)(&sofaBed) + 5;
    cout << endl;

    //[6]
    cout << "[6]CBed::m_iWidth=" << *(int*)((int *)(&sofaBed) + 6);
    cout << "\t地址: " << (int *)(&sofaBed) + 6;
    cout << endl;
    cout << "-------------------------------------------------" << endl;

    //[7]
    cout << "[6]CSofaBed::m_iHeight=" << *(int*)((int *)(&sofaBed) + 7);
    cout << "\t地址: " << (int *)(&sofaBed) + 7;
    cout << endl;
    cout << "-------------------------------------------------" << endl;

    //[8]
    cout << "[8]CFurniture::vptr";
    cout << "\t地址:" << (int *)(&sofaBed) + 8 << endl;

    //输出CFurniture::vptr中的虚函数
    for (int i = 0; i<1; ++i)
    {
        cout << "  [" << i << "]";
        Fun fun1 = (Fun)*((int *)*((int *)(&sofaBed) + 8) + i);
        fun1();
        cout << "\t地址:\t" << *((int *)*((int *)(&sofaBed) + 8) + i) << endl;
    }

    //[9]
    cout << "[9]CFurniture::m_iPrice=" << *(int*)((int *)(&sofaBed) + 9);
    cout << "\t地址: " << (int *)(&sofaBed) + 9;
    cout << endl;

    getchar();
}
image.png

可以看到,虚拟菱形继承主要解决的便是class包含多个虚基类派生类产生的数据二义性及冗余的问题,msvc编译器采用的是虚基表存储虚基类偏移的方式,虚基类成为一个共享部分,虚基类派生类访问都通过虚基表指针去间接访问,当然,不同的编译器为了支持虚拟菱形继承所采用的方法也不尽相同,如Sun编译器采用了虚函数表存储虚基类偏移量的方式,通过对虚函数表负索引得到偏移的方式进行间接访问虚基类

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350