MySQL 指針的藝術--base_list

引言

             讀MySQL源碼已經有一段時間了,對於MySQL這個龐然大物,讀起來真是費時費力,即使配備gdb、mysql internal 外加一些講解MySQL的書籍讀起來還是朦朦朧朧,究其原因還是自己功夫不到家了,再接再厲吧,少年!

List

           在讀代碼的過程中,發現大量的List<T>類型的變量,所以想要深入瞭解一下MySQL的鏈表是如何實現的。
           MySQL的list相關源碼主要爲sql目錄下的sql_list.h以及sql_list.cc這個兩個文件。其中最重要的類是base_list,類圖如下
   
            Sql_alloc是MySQL封裝的一個內存管理使用的類,這個類幾乎相當於Java中的Object,只要是涉及到內存的結構基本都會是Sql_alloc的子類或者子孫類,有機會會在MySQL的內存管理上進行說明。很自然的base_list就成爲了Sql_alloc的一個子類,這是一個基本的單鏈表類,list_node是鏈表中的節點抽象,雖然list_node也繼承Sql_alloc,但是在list_node內部基本是沒有任何的內存方法的調用。
            base_list的功能極爲豐富,具體接口如下

Public Member Functions

bool  operator== (const base_list &rhs) const
void  empty ()
  base_list (const base_list &tmp)
  base_list (const base_list &rhs, MEM_ROOT *mem_root)
  base_list (bool error)
bool  push_back (void *info)
bool  push_back (void *info, MEM_ROOT *mem_root)
bool  push_front (void *info)
void  remove (list_node **prev)
void  concat (base_list *list)
void *  pop (void)
void  disjoin (base_list *list)
void  prepand (base_list *list)
void  sort (Node_cmp_func cmp, void *arg)
  Sort the list. 
void  swap (base_list &rhs)
list_node *  last_node ()
list_node *  first_node ()
void *  head ()
void **  head_ref ()
bool  is_empty () const
list_node *  last_ref ()


           這不僅是一個鏈表還可以用作是stack。這裏主要想說一下push_back函數,這裏充分體現了二維指針的強大,MySQL中的這種技巧隨處可見。在介紹push_back實現之前先說一下list_node的結構
struct list_node :public Sql_alloc
{  
  list_node *next;
  void *info;
  list_node(void *info_par,list_node *next_par)
    :next(next_par),info(info_par)
  {}  
  list_node()         /* For end_of_list */
  {
    info= 0;
    next= this;
  }
}; 

           可以看到list_node的next指針聲明在了第一個位置,這是一個相當重要的技巧,MySQL中的其他鏈表節點基本也都是將next放在第一個位置,原因就在push_back的實現中。在來看一個base_list的結構
class base_list :public Sql_alloc
{  
protected:
  list_node *first,**last;
   
public:
  uint elements;
...
...
};

          這也是一個比較通常的封裝,但是不平常的是last是一個二維指針,當我讀到這段代碼的時候一直疑惑,爲什麼用二維指針呢?答案還是push_back的實現中,push_back代碼如下
  inline bool push_back(void *info)
  {
    if (((*last)=new list_node(info, &end_of_list)))
    {
      last= &(*last)->next;
      elements++;
      return 0;
    }
    return 1;
  }
        在sql_list.h中有一extern變量 爲list_node end_of_list,他的特點是end_of_list的next就是它本身,這個節點作爲base_list的尾節點,初始化bast_list時,first指向它,*last指向它。
        push_backd實現的及其精簡,這裏大致說一下實現原理:first永遠指向第一個元素,*last永遠指向最後一個元素即end_of_list,所以*last永遠等於倒數第二個元素的next值,所以第一行new一個新的元素賦值給*last,可以直接將新的元素串聯到鏈表上,並自定將新元素變爲倒數第二個元素(最後一個元素爲end_of_list),然後重置last,讓last = &(*last)->next,*last是剛剛new已經pash_back的元素,*last->next就是&end_of_list的值,這是一個list_node*類型的,然後再取地址,其實就是取道了end_of_list.next,其實還是它本身。有點饒,下面將代碼抽離出來,gdb一下,看一下過程。

小Daemon

        
struct node{
  struct node *next;
  int a;
}; 
int main()
{  
  node node1,node2;
  node1.a=1;
  node2.a=2;
  node2.next=NULL;
  node1.next=&node2;                                                       node *p = &node1;
  node **q = (node **)p;
  return 1;
}
調試一下在我的機器上運行如下:
----------------------------------------------------------
0x7fffffffd9e0 | node1的地址{next = 0x7fffffffd9f0, a = 1}
---------------------------------------------------------
0x7fffffffd9f0  | node2的地址{next = 0x0, a = 2}                                                                                                       
---------------------------------------------------------

所以這裏p = 0x7fffffffd9e0,十分好理解。我的機器是64位機器所以sizeof(node) = 16,其中指針8,int 4,剩下的4個浪費掉。所以node1 node2之間剛好差兩個字節,16位。此時node **q = (node **)p; 會將p原本指向一個16位的結構體struct node強制轉化爲一個指向struct node*的8位的二維指針q的值是0x7fffffffd9e0,*q的值爲0x7fffffffd9f0,爲node2的地址,node.next的值。如果在聲明node時候將next與a順序調換,是得不到這個效果的。

抽離的base_list實現

#include<stdio.h>                                                                                                                                     
struct list_node
{
  list_node *next;
  void *info;
  list_node(void *info_par,list_node *next_par)
    :next(next_par),info(info_par)
  {}
  list_node()         /* For end_of_list */
  {
    info= 0;
    next= this;
  }
};
list_node end_of_list;
 
class base_list
{
public:
  list_node *first,**last;
  unsigned int elements;
  void empty() { elements=0; first= &end_of_list; last=&first;}
  base_list() { empty(); }
  bool push_back(void *info)
  {
    if (((*last)=new list_node(info, &end_of_list)))
    {
      last= &(*last)->next;
      elements++;
      return 0;
    }
    return 1;
  }
 
};
int main()
{
  int a=1,b=2,c=3,d=4;
  base_list list;
  list.push_back((void*)(&a));
  list.push_back((void*)(&b));
  list.push_back((void*)(&c));
  list.push_back((void*)(&d));
  return 0;
}             

first指向鏈表的第一個元素,*last指向最後一個元素,last指向last->next待添加到鏈表末尾的那個元素調試一下在我的機器上運行如下:
-----------------------------------------------------------------------------
0x601050       | end_of_list的地址{next = 0x601050 <end_of_list>, info = 0x0},first *last的值
----------------------------------------------------------------------------
0x7fffffffd9f0 | 0x601050 ,last的值
---------------------------------------------------------------------------
push_back(a)之後的執行
(*last)=new list_node(info, &end_of_list) 申請一個節點next指向end_of_list
*last的值爲0x601050所以first 的值也會更新爲這個新申請的節點
*last以及first被更新爲0x602010,他們的next值爲ox601050
last= &(*last)->next;
(*last)->next指向end_of_list值爲0x601050
&(*last)->next的值到底是什麼呢?即哪一個地址存儲的是0x601050這個值
答案是0x602010
與上面的解釋一樣,(list_node*)0x602010是first *last, (list_node**)0x602010是一個指向list_node*的指針,剛好這個地址的前八位爲first->next
即(*last)->next 0x601050 所以last=0x602010此時的*last = 0x601050 即指向了最後一個元素
-----------------------------------------------------------------------------
0x601050       | end_of_list的地址{next = 0x601050 <end_of_list>, info = 0x0}
----------------------------------------------------------------------------
0x602010       | {next = 0x601050 <end_of_list>, info = &a}, first指向這個
---------------------------------------------------------------------------
0x602010       | last等於這個地址值,*last = 0x601050 所以要清楚對於*last的修改就是讀first->next的修改,繼續往下看
---------------------------------------------------------------------------
push_back(b)之後
-----------------------------------------------------------------------------
0x601050       | end_of_list的地址{next = 0x601050 <end_of_list>, info = 0x0}
----------------------------------------------------------------------------
0x602010       | {next = 0x602030 , info = &a}, first指向這個
---------------------------------------------------------------------------
0x602030       | {next = 0x601050 <end_of_list>, info = &b} 
---------------------------------------------------------------------------
0x602030       | last等於這個地址值,*last = 0x601050 所以要清楚對於*last的修改就是讀first->next的修改,繼續往下看
---------------------------------------------------------------------------
接下來的過程重複這個過程完成鏈表的建立。

        這就是MySQL中二維指針的神奇。代碼簡介,效率上並沒有看出比一維指針好(可能是本人愚鈍)。

base_list::remove的實現

  void remove(list_node **prev)
  {
    list_node *node=(*prev)->next;
    if (!--elements)
      last= &first;
    else if (last == &(*prev)->next)
      last= prev;
    delete *prev;
    *prev=node;
  }

         可以看到由於list_node結構的特殊,next位於第一個位置,所以對一個list_node *p 直接取地址&p 則會直接得到p的前一個節點,所以在remove中只傳一個參數就可以了,prev就是待刪除節點的前一個節點,*prev就是要刪除的節點。

總結

        1. 二維指針實現的鏈表可以讓代碼簡介,雖然是單鏈表但是由於list_node的特殊結構可以直接得到節點的前一個元素,相當於雙鏈表,如果與雙鏈表進行比較則相當於省去了一個*prev的內存
        2. 雖然二維指針+特殊的list_node結構代碼優美,但是自我感覺代碼可獨性一般(MySQL開發人員都是大牛所以估計沒有這個問題),這種技巧也不太容易掌握。


參考

       1. MySQL的源碼文檔 http://www.iskm.org/mysql56/index.html
       2. MySQL源碼
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章