Hello, 宏定義魔法世界

宏,簡單來說就是按預定義的規則替換相應的文本內容,被替換的文本內容可以是對象也可以是函數。既然是替換,那就需要遵循一定的規則來執行,這里的規則就是本文要討論的主要內容,希望通過深入淺出和逐層剖析的方式可以讓大家對宏定義有更加透徹的理解,繼而能夠在實際項目中運用并發揮宏定義的magic.

使用宏定義不僅可以讓代碼看起來更加簡潔易讀,更重要的是可以進行編譯檢查。由于宏定義是在預處理的時候被執行的,因此可以在編譯之前就檢查出包括參數類型,參數完整性等相關的錯誤。比如ReactiveCocoa里面的keypath(...)宏,可以接受可變參數:
keypath(self.observerPath)keypath(self, observerPath)
如果self沒有observerPath這個成員變量,那么編譯器會直接給出錯誤提示,避免等到運行時才發現路徑無效而導致程序異常。

宏定義分為對象宏函數宏,對象宏通常是一些簡單的對象替換,比如#define M_PI 3.1415, 函數宏(宏名字后加上括號)可以接受參數如函數調用一樣在代碼中使用,比如 #define ADD(x,y) x + y. 函數宏有兩點需要注意: a). 括號與宏名之間不能有空格,否則就成對象宏了 b). 函數式宏定義的參數沒有類型,預處理器只負責做形式上的替換,而不做參數類型檢查,所以傳參時要格外小心。

Okay, 介紹完宏的基本概念之后,讓我們正式進入宏定義的魔法世界吧!

宏定義符號

在宏定義中,有四個特殊的符號:1)\ 2)# 3)## 4) ...,它們分別表示換行,字符串化,連接運算和可變多參數。

1) 換行符

我們知道在字符串里面可以使用\n來實現換行,同樣在宏定義里面也可以插入換行符而不影響其含義,只不過在宏里面是用反斜杠\來標識換行。

#define APP_VERSION() \
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]

等價于:

#define APP_VERSION() [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]

如果不加\換行標識符而直接轉行,則第二行的內容就不屬于宏定義了, APP_VERSION()會被定義成空,也就是調用APP_VERSION會不起任何作用。

#define SWAP_VALUE(a, b) { \
            typeof(a) _t = a; \
            a =  b; \
            b = _t; \ 
        }

一般聲明帶有參數列表宏定義的時候,如果函數體字符串太長,通常都會使用換行符來增強函數的可讀性。

在宏聲明中可以使用typeof來引用宏參數的類型。這個示例中typeof會獲取參數a的類型,并用這個類型定義中間變量_t來交換a和b的值。因此,該宏可以交換所有基本數據類型的變量(整數,字符,結構等)

2) 字符串化

單個#號的作用是字符串化,簡單來說就是在輸入值上加上""雙引號,使其轉換為C字符串。如果是在ObjC環境下,則可在頭部再加上@符號,出來后即是一個NSString類型。

#define STRINGIZE_(x)  #x
#define STRINGIZE2(x)  STRINGIZE_(x)
#define OCNSSTRING(x) @STRINGIZE2(x)

我們帶入實際值,比如OCNSSTRING(3),一步步展開看一下:

OCNSSTRING(3) => @STRINGIZE2(3) => @STRINGIZE_(3) => @"3" // NSString @"3"
STRINGIZE2(3) =>  STRINGIZE_(3) // 這里多加一層是為了處理宏展開的問題,見后文介紹
STRINGIZE_(3) =>  #3 => "3" // C字符串"3"

字符串化對空格的處理有兩種情況: a). 忽略傳入參數名前面和后面的空格 e.g. STRINGIZE_( abc ) => "abc" b)當傳入參數之間存在空格時,忽略其中多于一個的空格 e.g. STRINGIZE_(abc /*多個空格*/ def) => "abc def"

3) 連接運算

連接符號##用來將前后兩項合并成一個整體。它的執行分為兩步,先是分隔,然后它會將合并項之間的空格去除后完成連接操作。

#define metamacro_concat(A, B) A ## B
NSString *str      = @"This is Ryan!";
NSLog(@"%@", metamacro_concat(st, r));  // This is Ryan!

metamacro_concat的作用是將str這兩項合并成整體str,而str是字符串@"This is Ryan!"的對象,所以最終會打印出來該字符串內容。剛剛這個示例僅僅是演示了連接這個動作,那上面所說的分隔是什么意思或者說是什么場景呢?我們直接來看下面這個示例

#define A1(name, type) type name_##type##_type 
#define A2(name, type) type name##_##type##_type

帶入實參A1(a1, int)A2(a1, int),相信你看到這兩個宏定義后,會覺得沒什么特別的,理解了##的含義,再逐個替換之后(type替換成intname替換成a1##前后的項連成整體),很快就能得出答案:

A1(a1, int) => int a1_int_int; // 然而并不對!!!
A2(a1, int) => int a1_int_int; // 然而并不對!!!

是的,參照后面注釋,然而并不對!現在該是討論##分隔操作的時候了,我們先來公布下正確答案,當然我建議你可以找個編譯器實際試一下,看看結果到底是什么。

A1(a1, int) => int name_int_type; // bingo!
A2(a1, int) => int a1_int_type;   // bingo!

看到答案是不是很驚訝,為什么有的替換了,有的沒替換,name不應該都替換成a1type不應該都替換成int嗎?好了,我們來揭開謎底吧:

預處理器在解析宏的時候會先做分隔操作,就是把##的前后項分隔開。

  1. A1name_##type##_type會被分隔成name_type_type這3段,顯然name != name_;type != _type,所以第一段name_和第三段_type不會被宏替換,中間段type則被替換成int,按這個規則帶入后就可以得到最終的結果為int name_int_type
  2. A2name##_##type##_type會被分隔成name_type_type這4段,現在就一目了然了,_type不會被替換,因此帶入參數后得到最終結果為int a1_int_type

分隔的作用類似于空格。在宏定義中,預處理器一般把空格解釋成分段標志,對于每一段和前面比較,相同的就被替換。而##則會把前后項之間的空格都去除,然后再做連接操作。所以A1A2的定義也可如下:

#define A1(name, type)  type name_     ##    type     ##     _t1ype
#define A2(name, type)  type name   ##   _   ##   type   ##  _t1ype

常見的運算符比如+,-, *, /, ++以及宏定義操作符#...也是分隔標志,e.g. define add(a, b) a+bdefine add(a, b) a + b結果是一致的,+會把ab之間的空格去掉后再去做相應運算

4) 可變多參數

標識符號...用來標識該宏可以接收可變的參數數量(零個或多個符號)。在宏體中,使用__VA_ARGS__來表示那些輸入的實際參數,也就是__VA_ARGS__在預處理中將為實際的參數集所替換。需要注意的是...只能放在末尾,代替最后面的宏參數。

...__VA_ARGS__配對類似,也可以使用NAME...NAME來配對使用表示可變多參數。不同于前者,這里的NAME是你任意的參數名,并不是系統保留名: e.g. format...format

我們來看一個實現對ObjC的NSLog打印信息補充和限制只在DEBUG環境下輸出的可變參數宏定義示例:

#ifdef  DEBUG
#define NSLog(format, ...) NSLog((@"%s [Line %d] " format), __func__, __LINE__, ##__VA_ARGS__);
#else
#define NSLog(format, ...)
#endif

在這個宏定義中,如果是非DEBUG環境,那么直接替換為空,也就是NSLog將不起任何作用。我們重點討論DEBUG環境下的定義,第一個參數format將被單獨處理,接下來輸入的參數則作為一個整體被視為可變參數。比如NSLog(@"name = %@, age = %d", @"Ryan", 18), 這里的@"name = %@, age = %d"即對應宏里的format, 后面的@"Ryan"18則映射為...指代為統一的可變參數。 因為我們不確定用戶格式化字符串時會輸入多少個參數,所以我們指定為可變參數,允許用戶輸入任意數量的參數。帶入具體的實參后替換后的結果為:

NSLog((@"%s [Line %d] " "name = %@, age = %d"), __func__, __LINE__, ##@"Ryan", 18);

不知道你有沒有注意到__VA_ARGS__前面的##標識符,有了上文的介紹,我們知道它是用來做連接操作的,也就是將name = %@, age = %d和前面的參數連接后打印出來。但是__VA_ARGS__本來就是順著__LINE__后面寫的,應該不需要加##吧?YES! 確實不需要加##來做"連接"的作用,那為什么還要加呢?

既然是可變多參數,那它是包括一個case的: 參數數量為0,如果我們把##去掉,替換后宏就變成如下結果:

NSLog((@"%s [Line %d] "), __func__, __LINE__, ); // 注意最后一個逗號

有沒有發現,當可變參數的個數為0時,最后面會多一個逗號,顯然這個情況下編譯器會報錯的,那怎么才能支持0參數的情況呢?答案就是##. 當可變參數的個數為0的時候,##會把前面多余的逗號去掉,所以定義可變參數宏需要記得加上##來處理這個情況。

宏定義展開

當宏定義有多層嵌套的情況,即宏定義里面又包含另外的宏定義,這時宏展開(替換)需要遵循一定的規則,總體原則是每次只解開當前層的宏,我們直接來看下面這個示例:

#define  _ANONYMOUS1(type, var, line) type  var##line
#define  _ANONYMOUS0(type, line)      _ANONYMOUS1(type, _anonymous, line)
#define   ANONYMOUSS(type)            _ANONYMOUS0(type, __LINE__)

帶入實參ANONYMOUSS(static int);即: static int _anonymous70; 70表示該行行號。這個宏包含三層,逐一解析:

第一層:ANONYMOUSS(static int) –> _ANONYMOUS0(static int, __LINE__)
第二層:                       –> _ANONYMOUS1(static int, _anonymous, 70);
第三層:                       –> static int _anonymous70;

由于每次只能解開當前層的宏,__LINE__需要等到第二層才能被解開。所以如果我們把中間層_ANONYMOUS0去掉,直接由_ANONYMOUS1來定義ANONYMOUSS

#define  _ANONYMOUS1(type, var, line) type  var##line
#define   ANONYMOUSS(type)            _ANONYMOUS1(type, _anonymous, __LINE__)

再次帶入實參ANONYMOUSS(static int);這個情況下,最終的結果會是static int _anonymous__LINE__,預定義宏__LINE__并不會被解開!所以當你看一些有嵌套宏定義的時候(包括系統的宏定義),你會發現它們往往會加多一層中間轉換宏,加這層宏的用意是把所有宏的參數在這層里全部展開,這個我們在自己實際項目中定義復雜宏的時候也需要特別留意。

這里用到了預定義宏__LINE__,預定義宏的行為是由編譯器指定的。__LINE__返回展開該宏時在文件中的行數,其他類似的有__FILE__返回當前文件的絕對路徑;__func__是該宏所在scope的函數名稱;__COUNTER__在編譯過程中將從0開始計數,每次被調用時加1。因為唯一性,所以很多時候被用來構造獨立的變量名稱。

宏展開的另外一個規則是,在展開當前宏函數時,如果形參有###則不進行宏參數的展開,否則先展開宏參數,再展開當前宏。我們來看一道經典的C語言題目

#include <stdio.h> 

#define f(a,b) a##b  
#define g(a)   #a  
#define h(a)   g(a)  

int main() {
    printf("%s\n", h(f(1,2))); // => 12
    printf("%s\n", g(f(1,2))); // => f(1,2)
    return 0;
}

這道題的正確答案是分別是12f(1,2),后者宏g里面的參數f(1,2)不會被展開。我們對照上面宏展開的規則來分析下:
第一行h(f(1,2))由于h(a)#或者##所以先展開參數f(1,2)12再展開當前宏h(12) => g(12) => 12
第二行g(f(1,2))由于g(a)形參帶有#所以里面的f(1,2)不會被展開,最終結果就是f(1,2)

相信你應該發現了,其實h(a)在這里充當的就是中間轉換宏的角色,目的就是為了讓f(1,2)先在h(a)里面被展開,避免放到g(a)里面遇到#而無法被替換。好了,了解了宏定義的展開規則,我們再留個小作業給大家:

#define VALUE            2 
#define STRINGIZES_(s)   #s 
#define COMBINATION(a,b) int(a##e##b) 

printf("int max: %s\n", STRINGIZES_(INT_MAX)); // => ?
printf("%s\n", COMBINATION(VALUE, VALUE));     // => ?           

應該怎樣添加轉換宏才能分別打印出int max: 0x7fffffff200? P.S. INT_MAX的十六進制為0x7fffffff; 200則等于2e2, e為指數表達式,表示2乘以102次方。

宏實例分析

有了上面的介紹,我們可以選一些相對復雜的宏定義來分析了,這邊我們還是選取ReactiveCocoa里面的兩個宏。大家如果有興趣,還是強烈推薦去GitHub下載這個庫查看下,里面有很多讓人嘆為觀止的宏定義。

1) 計算參數個數

下面這個宏metamacro_argcount(...)用來計算在可變參數的情況下,傳入的實參數量。e.g. int num = metamacro_argcount(a, b, c);等價于int num = 3; 作者提到靈感是來自于P99. 這里為了方便分析,我們把支持最多參數數量的計算改成10個且做了稍許簡化

#define metamacro_argcount(...) metamacro_at(10, __VA_ARGS__,10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
#define metamacro_at(N,...)     metamacro_concat_at##N(__VA_ARGS__)

#define metamacro_concat_at10(_0,_1,_2,_3,_4,_5,_6,_7,_8,_9,...) metamacro_head(__VA_ARGS__)

#define metamacro_head(...)             metamacro_head_first(__VA_ARGS__,0)
#define metamacro_head_first(first,...) first

看起來是不是感覺很復雜?沒關系,我們一步步來,逐層帶入參數來分析。假設我們傳入5個參數metamacro_argcount(a,b,c,d,e)

STEP 1: 帶入metamacro_argcount
metamacro_argcount(a,b,c,d,e) => metamacro_at(10, a,b,c,d,e,10,9,8,7,6,5,4,3,2,1)

這里的__VA_ARGS__替換成前面傳入的可變實參a,b,c,d,e

STEP 2: 帶入metamacro_at
metamacro_at(10, a,b,c,d,e,10,9,8,7,6,5,4,3,2,1) => metamacro_concat_at10 (a,b,c,d,e,10,9,8,7,6,5,4,3,2,1)

第一個參數為N, 之后都定義為可變參數。故而N10, __VA_ARGS__a, b, c, d, e, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 這一步既修改了參數又修改了方法名。

STEP 3: 帶入metamacro_concat_at10
metamacro_concat_at10 (a,b,c,d,e,10,9,8,7,6,5,4,3,2,1) => metamacro_head(5,4,3,2,1)

這里把前面十個參數替換成_0,_1,_2,_3,_4,_5,_6,_7,_8,_9, 然后之后的參數,也就是5,4,3,2,1定義為可變參數,并作為實參傳給宏metamacro_head. 前10個參數就被drop掉了,所以你不用關心它是被替換成了_0還是___0, 總之它們不需要繼續被使用了。

STEP 4: 帶入metamacro_head
metamacro_head(5,4,3,2,1) => metamacro_head_first(5,4,3,2,1,0)

為什么在后面加個0呢?還記得前面說過的,可變參數的數量可以為零的吧,在這個場景下就變成metamacro_head_first()了,后面再用metamacro_head_first取第一個值就出錯了,所以需要額外加個0, 這樣可變參數為空的時候就變成metamacro_head_first(0), 再取第一個值就可以得到參數數量為0了。

STEP 5: 帶入metamacro_head_first
metamacro_head_first(5,4,3,2,1,0) => 5 // 直接獲取第一個值,其他的省略

是不是很cool很magic? 通過幾個宏定義的轉換,我們就能輕易的得出傳入的實參個數,而且這些結果在預處理階段就獲得了,不必等到運行階段再去計算。

2) 參數格式檢查

ReactiveCocoa里面還有個非常精妙的宏keypath(...), 可以判斷輸入的路徑參數是否合法,并且給出代碼提示。比如輸入keypath(self.path), 宏會作出判斷path是否為selfproperty, 如果該path不存在,則給出警告,避免誤寫。而且這個宏支持可變參數,還可以輸入格式為keypath(self, path), 同樣會對path做參數檢查。是不是很神奇?讓我們來揭開外衣看看它的魔法來源。

#define keypath(...) \
        metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))
#define keypath1(PATH) \
        (((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))
#define keypath2(OBJ, PATH) \
        (((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
STEP 1: 帶入metamacro_if_eq
metamacro_if_eq(1, metamacro_argcount(self,path))(keypath1(__VA_ARGS__))(keypath2(self,path))

這邊的metamacro_argcount上面討論過,是計算可變參數個數,所以metamacro_if_eq的作用就是判斷參數個數,如果個數是1就執行后面的keypath1, 若不是1就執行keypath2, 我們來看看metamacro_if_eq的定義:

/**
 * If A is equal to B, the next argument list is expanded; otherwise, the
 * argument list after that is expanded. A and B must be numbers between zero
 * and twenty, inclusive. Additionally, B must be greater than or equal to A.
 *
 * @code

// expands to true
metamacro_if_eq(0, 0)(true)(false)

// expands to false
metamacro_if_eq(0, 1)(true)(false)

 * @endcode
 *
 * This is primarily useful when dealing with indexes and counts in
 * metaprogramming.
 */
#define metamacro_if_eq(A, B) \
        metamacro_concat(metamacro_if_eq, A)(B)

限于篇幅,這邊就不展開太多了,只重點分析下keypath(...)宏的實現,至于metamacro_if_eq我們知道它的作用就可以了,不過我還是建議大家去ReactiveCocoa查看下這個宏的完整定義,并嘗試分析下metamacro_if_eq的實現原理。我相信,通過本文的介紹再加上一步步的帶入替換,應該不難理解它的實現。

STEP 2: 帶入keypath2
keypath2(self,path) (((void)(NO && ((void)self.path, NO)), # path))

這個宏整體是一個C語言的逗號表達式,我們來回憶下逗號表達式的格式: e.g. int a = (b, c); 逗號表達式取后面的值,故而a將被賦值成c, 此時b在賦值運算中就被忽略了,沒有被使用,所以編譯器會給出警告,為了消除這個warning我們需要在b前面加上(void)做個類型強轉操作。

逗號表達式的前項和NO進行了與操作,這個主要是為了讓編譯器忽略第一個值,因為我們真正賦值的是表達式后面的值。預編譯的時候看見了NO, 就會很快的跳過判斷條件。我猜你看到這兒肯定會奇怪了,既然要忽略,那為啥還要用個逗號表達式呢,直接賦值不就好了?

這里主要是對傳入的第一個參數OBJ和第二個正要輸入的PATH做了.操作,這也正是為什么輸入第二個參數時編輯器會給出正確的代碼提示(只要是作為表達式的一部分, Xcode自動會提示)。如果傳入的path不是self的屬性,那么self.path就不是一個合法的表達式,所以自然編譯就不會通過了。

STEP 3: 帶入keypath1
keypath1(self.path) (((void)(NO && ((void)self.path, NO)), strchr(# self.path, '.') + 1))

keypath1接受1個參數,所以我們直接帶入self.path. 宏的前半段和上面是一樣的,不同的是逗號表達式的后一段strchr(# self.path, '.') + 1, 函數strchar是C語言中的函數,用來查找某字符在字符串中首次出現的位置,這里用來在self.path(注意前面加了#字符串化)中查找.出現的位置,再加上1就是返回.后面path的地址了。也就是strchr('self.path', '.')返回的是一個C字符串,這個字符串從找到'self.path'中為'.'的字符開始往后,即'path'.

按照上面的分析,我們知道keypath(...)是返回一個經過檢查的合法路徑。如果在ObjC環境下,我們需要的是一個NSString, 所以我們在調用這個宏的時候,再加上@符號就OK了, e.g. @keypath(self.path) => @"self.path".

有時定義宏我們會故意加上@符號,但不是為了轉換NSString類型,也不是為了某種特別的作用,只是讓調用看起來更原生一些。

我們來看下面這個例子:

#define weakObj(obj) __weak typeof(obj) obj##Weak = obj;

在ObjC里面的block為了防止循環引用,我們會使用__weak關鍵字,這個宏就是用來實現obj的weak化,調用的時候則是weakObj(self), 但是iOS都是習慣加@符號,比如字符串是@"", 數組是@[], 就連定義協議都是@protocol, 那怎么讓我們的weakObj也能在前面加上@符號呢?

iOS開發的同學應該都記得系統的自動釋放池@autoreleasepool{}, 這里面就有個@符號,所以我們可以在weakObj的宏定義里面放一個空的autoreleasepool{}, 并且不加@符號,讓這個@符號有外面調用的時候加上,也就是這樣的:

#define weakObj(obj) autoreleasepool{} __weak typeof(obj) obj##Weak = obj;

調用的時候@weakObj里的@符號就被加到autoreleasepool{}上了,其實這個autoreleasepool{}是空的,并不起任何實際作用:

@weakObj(obj) => @autoreleasepool{} __weak typeof(obj) obj##Weak = obj;

宏知識補充

由于宏定義的實質只是文本替換,所以這個并不智能的替換會在一些環境下發生不可預知的錯誤。幸運的是,我們的前輩們發現了這些問題,并且提供了很好的解決方案,這也是我們下面要討論的許多宏定義約定俗成的格式寫法。

1) 使用do{}while(0)語句

對于函數宏,我們一般都是推薦使用do{}while(0)語句,把函數體包到do后面的{}內,為什么要這樣呢?我們看一個實例:

#difne FOO(a,b) a+b; \
                a++;

正常調用是沒有問題的,但是如果我們是在if條件語句里面調用,并且if語句沒有{}, 像下面這樣:

if (...)
   FOO(a,b) // 滿足了if條件后FOO會被執行

展開之后就會變成(顯然就不對了):

if (...)
   a+b; // a+b在滿足了if條件后會被執行
a++;    // a++不管if條件是否滿足都會被執行

如果加上do{}while(0)語句展開后就是:

if (...)
   do {         
        a+b; 
        a++;                                                            
   } while (0);

這樣就沒有問題了,但你肯定會疑惑,這個和直接包一個{}不是一樣的嗎,只要把函數體包成一個整體就可以了。是的,在這個情況下是一樣的,但是do{}while(0)還有一個功能,會去除多余的分號,我們還是看實例:

#difne FOO(a,b) { a+b; \
                  a++; }
if (...)
   FOO;
else
   ...

使用{}情況下我們展開來看:

if (...) {
    a+b; 
    a++; 
}; else // 注意這邊多出來的分號,編譯直接報錯!
   ...

如果是do{}while(0)的話會直接吸收掉這個分號:

if (...) 
   do {
       a+b; 
       a++; 
   } while(0); // 分號被do{}while(0)吸收了
else {
   ...
}

這個吸收分號的方法現在已經幾乎成了標準寫法。而且因為絕大多數的編譯器都能夠識別do{}while(0)這種無用的循環并進行優化,所以不會因為這種方法導致運行效率上的差異。

2) 使用({...})語句

GNU C里面有個({...})形式的賦值擴展。這種形式的語句在順次執行之后,會將最后一次的表達式的賦值作為返回。

#define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })

這個宏用來取輸入參數中較小的值,并將該值作為返回值返回。這里就是用到了({...})語句來實現,函數體中可以做任意的邏輯處理和運算,但最終的返回值則是最后的表達式。所以在定義宏的時候,我們可以用({...})語句來定義有返回值的函數宏,這個也是函數宏很常見的寫法,大家在實際項目中也可以注意參照使用。

最后簡單提下宏和const怎么區別使用,一般來說定義常量字符串就用const,定義代碼就用宏(可以參見iOS的API相關定義)。如果有任何不清楚的,歡迎留言討論。PS. 本文參考了很多前輩們的精彩文章,在文中以超鏈接的方式做了引用,感謝他們的分享,也希望本文能給大家帶來一點幫助。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容