libuv源碼分析之queue

libuv的queue實現得很博大精深。嚴重考驗了c指針的理解。今天就分享一下他的實現。首先從一個typedef開始

typedef void *QUEUE[2];

這個是c語言中定義類型別名的一種方式。比如我們定義一個變量

QUEUE q

就相當於

void *q[2];

即一個數組,他每個元素是void型的指針。

下面我們接着分析四個舉足輕重的宏定義,理解他們就相當於理解了libuv的隊列。在分析之前,我們先來回顧一下數組指針和二維數組的知識。

int a[2];
// 數組指針
int (*p)[2] = a;
// *(*(p+0)+1)取元素的值

二維數組

int a[2][2];

我們知道二維數組在內存中的佈局是一維。

但是爲了方便理解我們畫成二維的。

  1. &a代表二維數組的首地址。類型是int (*)[2][2],他是一個指針,他指向的元素是一個二維數組。假設int是四個字節。數組首地址是0,那麼&a + 1等於16.
  2. a代表第一行的首地址,類型是int (*)[2],他是一個指針,指向的元素是一個一維數組。a+1等於8。
  3. a[0]也是第一行的首地址,類型是int *。
  4. &a[0]也是第一行的首地址,類型是int (*)[2];
  5. 如果int (p) = &a[0],那麼我們想取數組某個值的時候,可以使用((p+i) + j)的方式。(p+i)即把範圍固定到第一行(這時候的指針類型是init ),(*(p+i) + j)即在第一行的範圍內定位到某一列,然後通過解引用取得內存的值。

下面開始分析libuv的具體實現

1 QUEUE_NEXT

#define QUEUE_NEXT(q)       (*(QUEUE **) &((*(q))[0]))

QUEUE_NEXT看起來是獲取當前節點的next字段的地址。但是他的實現非常巧妙。我們逐步分析這個宏定義。首先我們先看一下QUEUE_NEXT是怎麼使用的。

 void *p[2][2];
 QUEUE* q = &p[0]; // void *(*q)[2] = &p[0];
 QUEUE_NEXT(q);

我們看到QUEUE_NEXT的參數是一個指針,他指向一個大小爲2的數組,數組裏的每個元素是void 。內存佈局如下。

因爲libuv的數組只有兩個元素。相當於
p[2][2]變成了*p[2][1]。所以上面的代碼簡化爲。

 void *p[2];
 QUEUE* q = &p; // void *(*q)[2] = &p;
 QUEUE_NEXT(q);


根據上面的代碼我們逐步展開宏定義。

  1. q指向整個數組p的首地址,*(q)還指向數組第一行的首地址(這時候指針類型爲void *,見上面二維數組的分析5)。
  2. (*(q))[0]即把指針定位到第一行第一列的內存地址(這時候指針類型還是void *,見上面二維數組的分析5)。
  3. &((*(q))[0])把2中的結果(即void *)轉成二級指針(void **),然後強制轉換類型(QUEUE **) 。爲什麼需要強制轉成等於QUEUE **呢?因爲需要保持類型。轉成QUEUE 後(即void * ()[2])。說明他是一個二級指針,他指向一個指針數組,每個元素指向一個大小爲2的數組。這個大小爲2的數組就是下一個節點的地址。

    在libuv中如下
  4. (QUEUE **) &(((q))[0])解引用取得q下一個節點的地址(作爲右值),或者修改當前節點的next域內存裏的值(作爲左值),類型是void (*)[2]。

2 QUEUE_PREV

 #define QUEUE_PREV(q)       (*(QUEUE **) &((*(q))[1])

prev的宏和next是類似的,區別是prev得到的是當前節點的上一個節點的地址。不再分析。

3 QUEUE_PREV_NEXT、QUEUE_NEXT_PREV

#define QUEUE_PREV_NEXT(q)  (QUEUE_NEXT(QUEUE_PREV(q))
#define QUEUE_NEXT_PREV(q)  (QUEUE_PREV(QUEUE_NEXT(q))

這兩個宏就是取當前節點的前一個節點的下一個節點和取當前節點的後一個節點的前一個節點。那不就是自己嗎?這就是libuv隊列的亮點了。下面我們看一下這些宏的使用。

1 刪除節點QUEUE_REMOVE


#define QUEUE_REMOVE(q)                                                       \
  do {                                                                        \
    QUEUE_PREV_NEXT(q) = QUEUE_NEXT(q);                                       \
    QUEUE_NEXT_PREV(q) = QUEUE_PREV(q);                                       \
  }                                                                           \
  while (0)

在這裏插入圖片描述
1 QUEUE_NEXT(q); 拿到q下一個節點的地址,即p
2 QUEUE_PREV_NEXT(q)分爲兩步,第一步拿到q前一個節點的地址。即o。然後再執行QUEUE_NEXT(o),分析之前我們先看一下關於指針變量作爲左值和右值的問題。

int zym = 9297;
int *cyb = &zym;
int hello = *cyb; // hello等於9297
int *cyb = 1101;

我們看到一個指針變量,如果他在右邊,對他解引用(*p)的時候,得到的值是他指向內存裏的值。而如果他在左邊的時候,p就是修改他自己內存裏的值。我們回顧對QUEUE_NEXT宏的分析。他返回的是一個指針void ()[2]。所以 QUEUE_PREV_NEXT(q) = QUEUE_NEXT(q); 的效果其實是修改q的前置節點(o)的next指針的內存。讓他指向q的下一個節點(p),就這樣完成了q的刪除。

2 頭插法插入隊列QUEUE_INSERT_TAIL

// q插入h,h是頭節點
#define QUEUE_INSERT_TAIL(h, q)                                               \
  do {                                                                        \
    QUEUE_NEXT(q) = (h);                                                      \
    QUEUE_PREV(q) = QUEUE_PREV(h);                                            \
    QUEUE_PREV_NEXT(q) = (q);                                                 \
    QUEUE_PREV(h) = (q);                                                      \
  }                                                                           \
  while (0)

在這裏插入圖片描述
還有很多操作隊列的方式,但是主要理解了四個宏的意義,就很容易理解了這些操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章