常見C++面試題(三)

strcpy和memcpy有什麼區別?strcpy是如何設計的,memcpy呢?
 
strcpy提供了字符串的複製。即strcpy只用於字符串複製,而且它不只複製字符串內容以外,還會複製字符串的結束符。(保證dest能夠容納src。)
memcpy提供了通常內存的複製。即memcpy對於須要複製的內容沒有限制,所以用途更廣。
 
strcpy的原型是:char* strcpy(char* dest, const char* src);
char * strcpy(char * dest, const char * src) // 實現src到dest的複製
{
  if ((src == NULL) || (dest == NULL)) //判斷參數src和dest的有效性
  {
 
      return NULL;
  }
  char *strdest = dest;        //保存目標字符串的首地址
  while ((*strDest++ = *strSrc++)!='\0'); //把src字符串的內容複製到dest下
  return strdest;
}

memcpy的原型是:void *memcpy( void *dest, const void *src, size_t count );html

void *memcpy(void *memTo, const void *memFrom, size_t size)
{
  if((memTo == NULL) || (memFrom == NULL)) //memTo和memFrom必須有效
         return NULL;
  char *tempFrom = (char *)memFrom;             //保存memFrom首地址
  char *tempTo = (char *)memTo;                  //保存memTo首地址      
  while(size -- > 0)                //循環size次,複製memFrom的值到memTo中
         *tempTo++ = *tempFrom++ ;  
  return memTo;
}

strcpy和memcpy主要有如下3方面的區別。
一、複製的內容不一樣。strcpy只能複製字符串,而memcpy能夠複製任意內容,例如字符數組、整型、結構體、類等。
二、複製的方法不一樣。strcpy不須要指定長度,它遇到被複制字符的串結束符"\0"才結束,因此容易溢出。memcpy則是根據其第3個參數決定複製的長度。
三、用途不一樣。一般在複製字符串時用strcpy,而須要複製其餘類型數據時則通常用memcpylinux

常見的str函數:
  • strtok
  • extern char *strtok( char *s, const char *delim );

    功能:分解字符串爲一組標記串。s爲要分解的字符串,delim爲分隔符字符串。c++

    說明:strtok()用來將字符串分割成一個個片斷。當strtok()在參數s的字符串中發現到參數delim的分割字符時則會將該字符改成 \0 字符。在第一次調用時,strtok()必需給予參數s字符串,日後的調用則將參數s設置成NULL。每次調用成功則返回被分割出片斷的指針。當沒有被分割的串時則返回NULL。全部delim中包含的字符都會被濾掉,並將被濾掉的地方設爲一處分割的節點。git

  • strstr
  • char * strstr( const char * str1, const char * str2 );

    功能:從字符串 str1 中尋找 str2 第一次出現的位置(不比較結束符NULL),若是沒找到則返回NULL。github

  • strchr
  • char * strchr ( const char *str, int ch );

    功能:查找字符串 str 中首次出現字符 ch 的位置
    說明:返回首次出現 ch 的位置的指針,若是 str 中不存在 ch 則返回NULL。web

  • strcpy/strncpy
  • char * strcpy( char * dest, const char * src );

    功能:把 src 所指由NULL結束的字符串複製到 dest 所指的數組中。
    說明:src 和 dest 所指內存區域不能夠重疊且 dest 必須有足夠的空間來容納 src 的字符串。返回指向 dest 結尾處字符(NULL)的指針。面試

    相似的:算法

    strncpy編程

    char * strncpy( char * dest, const char * src, size_t num );
  • strcat/strncat
  • char * strcat ( char * dest, const char * src );

    功能:把 src 所指字符串添加到 dest 結尾處(覆蓋dest結尾處的'\0')並添加'\0'。
    說明:src 和 dest 所指內存區域不能夠重疊且 dest 必須有足夠的空間來容納 src 的字符串。
    返回指向 dest 的指針。數組

    相似的 strncat

    char * strncat ( char * dest, const char * src, size_t num );
  • strcmp/strncmp
  • int strcmp ( const char * str1, const char * str2 );

    功能:比較字符串 str1 和 str2。
    說明:
    當s1<s2時,返回值<0
    當s1=s2時,返回值=0 
    當s1>s2時,返回值>0

    相似的:

    strncmp

    int strncmp ( const char * str1, const char * str2, size_t num );
  • strlen
size_t strlen ( const char * str );

 

功能:計算字符串 str 的長度
說明:返回 str 的長度,不包括結束符NULL。(注意與 sizeof 的區別)

相似的 strnlen:它從內存的某個位置(能夠是字符串開頭,中間某個位置,甚至是某個不肯定的內存區域)開始掃描,直到碰到第一個字符串結束符'\0'或計數器到達如下的maxlen爲止,而後返回計數器值。

size_t strnlen(const char *str, size_t maxlen);

2、mem 系列

1.memset

void * memset ( void * ptr, int value, size_t num );

功能:把 ptr 所指內存區域的前 num 個字節設置成字符 value。
說明:返回指向 ptr 的指針。可用於變量初始化等操做

舉例:

複製代碼
#include <stdio.h>
#include <string.h>

int main ()
{
    char str[] = "almost erery programer should know memset!";
    memset(str, '-', sizeof(str)); 
    printf("the str is: %s now\n", str);
    return 0;
}
複製代碼

2.memmove

void * memmove ( void * dest, const void * src, size_t num );

功能:由 src 所指內存區域複製 num 個字節到 dest 所指內存區域。
說明:src 和 dest 所指內存區域能夠重疊,但複製後 src 內容會被更改。函數返回指向dest的指針。

舉例:

複製代碼
#include <stdio.h>
#include <string.h>

int main ()
{
    char str[] = "memmove can be very useful......";
    memmove(str + 20, str + 15, 11); 
    printf("the str is: %s\n", str);
    return 0;
}
複製代碼
the str is: memmove can be very very useful.

3.memcpy

void * memcpy ( void * destination, const void * source, size_t num );

相似 strncpy。區別:拷貝指定大小的內存數據,而無論內容(不限於字符串)。

memcpy和memmove做用是同樣的,惟一的區別是,當內存發生局部重疊的時候,memmove保證拷貝的結果是正確的,memcpy不保證拷貝的結果的正確。(memcpy更快)

但當源內存和目標內存存在重疊時,memcpy會出現錯誤,而memmove能正確地實施拷貝,但這也增長了一點點開銷。

memmove的處理措施:

(1)當源內存的首地址等於目標內存的首地址時,不進行任何拷貝

(2)當源內存的首地址大於目標內存的首地址時,實行正向拷貝

(3)當源內存的首地址小於目標內存的首地址時,實行反向拷貝

4.memcmp

int memcmp ( const void * ptr1, const void * ptr2, size_t num );

相似 strncmp

5.memchr

void * memchr ( const void *buf, int ch, size_t count); 

功能:從 buf 所指內存區域的前 count 個字節查找字符 ch。
說明:當第一次遇到字符 ch 時中止查找。若是成功,返回指向字符 ch 的指針;不然返回NULL。


類的析構函數爲何設計成虛函數?

析構函數的做用與構造函數正好相反,是在對象的生命期結束時,釋放系統爲對象所分配的空間,即要撤消一個對象。

用對象指針來調用一個函數,有如下兩種狀況:

  1. 若是是虛函數,會調用派生類中的版本。(在有派生類的狀況下)

  2. 若是是非虛函數,會調用指針所指類型的實現版本。

析構函數也會遵循以上兩種狀況,由於析構函數也是函數嘛,不要把它看得太特殊。 當對象出了做用域或是咱們刪除對象指針,析構函數就會被調用。

當派生類對象出了做用域,派生類的析構函數會先調用,而後再調用它父類的析構函數, 這樣能保證分配給對象的內存獲得正確釋放。

可是,若是咱們刪除一個指向派生類對象的基類指針,而基類析構函數又是非虛的話, 那麼就會先調用基類的析構函數(上面第2種狀況),派生類的析構函數得不到調用。

補充構造函數爲何不能是虛函數:

1. 從存儲空間角度,虛函數對應一個指向vtable虛函數表的指針,這你們都知道,但是這個指向vtable的指針實際上是存儲在對象的內存空間的。問題出來了,若是構造函數是虛的,就須要經過 vtable來調用,但是對象尚未實例化,也就是內存空間尚未,怎麼找vtable呢?因此構造函數不能是虛函數。
2. 從使用角度,虛函數主要用於在信息不全的狀況下,能使重載的函數獲得對應的調用。構造函數自己就是要初始化實例,那使用虛函數也沒有實際意義呀。因此構造函數沒有必要是虛函數。虛函數的做用在於經過父類的指針或者引用來調用它的時候可以變成調用子類的那個成員函數。而構造函數是在建立對象時自動調用的,不可能經過父類的指針或者引用去調用,所以也就規定構造函數不能是虛函數。
3. 構造函數不須要是虛函數,也不容許是虛函數,由於建立一個對象時咱們老是要明確指定對象的類型,儘管咱們可能經過實驗室的基類的指針或引用去訪問它但析構卻不必定,咱們每每經過基類的指針來銷燬對象。這時候若是析構函數不是虛函數,就不能正確識別對象類型從而不能正確調用析構函數。
4. 從實現上看,vbtl在構造函數調用後才創建,於是構造函數不可能成爲虛函數從實際含義上看,在調用構造函數時還不能肯定對象的真實類型(由於子類會調父類的構造函數);並且構造函數的做用是提供初始化,在對象生命期只執行一次,不是對象的動態行爲,也沒有必要成爲虛函數。
5. 當一個構造函數被調用時,它作的首要的事情之一是初始化它的VPTR。所以,它只能知道它是「當前」類的,而徹底忽視這個對象後面是否還有繼承者。當編譯器爲這個構造函數產生代碼時,它是爲這個類的構造函數產生代碼——既不是爲基類,也不是爲它的派生類(由於類不知道誰繼承它)。因此它使用的VPTR必須是對於這個類的VTABLE。並且,只要它是最後的構造函數調用,那麼在這個對象的生命期內,VPTR將保持被初始化爲指向這個VTABLE, 但若是接着還有一個更晚派生的構造函數被調用,這個構造函數又將設置VPTR指向它的 VTABLE,等.直到最後的構造函數結束。VPTR的狀態是由被最後調用的構造函數肯定的。這就是爲何構造函數調用是從基類到更加派生類順序的另外一個理由。可是,當這一系列構造函數調用正發生時,每一個構造函數都已經設置VPTR指向它本身的VTABLE。若是函數調用使用虛機制,它將只產生經過它本身的VTABLE的調用,而不是最後的VTABLE(全部構造函數被調用後纔會有最後的VTABLE)。

 


說兩種進程間通訊的方式。

1. 管道pipe:管道是一種半雙工的通訊方式,數據只能單向流動,並且只能在具備親緣關係的進程間使用。進程的親緣關係一般是指父子進程關係。
2. 命名管道FIFO:有名管道也是半雙工的通訊方式,可是它容許無親緣關係進程間的通訊。
3. 內存映射MemoryMapping
4. 消息隊列MessageQueue:消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。
5. 共享存儲SharedMemory:共享內存就是映射一段能被其餘進程所訪問的內存,這段共享內存由一個進程建立,但多個進程均可以訪問。共享內存是最快的 IPC 方式,它是針對其餘進程間通訊方式運行效率低而專門設計的。它每每與其餘通訊機制,如信號兩,配合使用,來實現進程間的同步和通訊。
6. 信號量Semaphore:信號量是一個計數器,能夠用來控制多個進程對共享資源的訪問。它常做爲一種鎖機制,防止某進程正在訪問共享資源時,其餘進程也訪問該資源。所以,主要做爲進程間以及同一進程內不一樣線程之間的同步手段。
7. 套接字Socket:套解口也是一種進程間通訊機制,與其餘通訊機制不一樣的是,它可用於不一樣及其間的進程通訊。
8. 信號 ( sinal ) : 信號是一種比較複雜的通訊方式,用於通知接收進程某個事件已經發生。

 詳情查看博客中關於進程通訊方式的總結線程同步的總結


 問了一些調試的問題,VS爲何能進行斷點單步調式,原理是啥?(騰訊實習生面試也問過這個問題,軟中斷)

 調試斷點依賴於父進程和子進程之間的通訊,打斷點實際上就是在被調試的程序中,改變斷點附近程序的代碼,這個斷點使得被調試的程序暫時中止,而後發送信號給父進程(調試器進程),而後父進程可以獲得子進程的變量和狀態,達到調試的目的。修改斷點附近程序的指令爲int3,含義是,使得當前用戶態程序發生中斷,告訴內核當前程序有斷點,那麼內核中會向當前進程發送sigtrap信號,使得當前進程暫停。父進程調用wait函數,等待子進程的運行狀態發生改變。 調試的大致原理:經過設置被調試的進程ptrace字段,標誌這個進程被trace,斷點附近的程序代碼被替換成了int 3,中斷程序,引起了do_int3函數,致使了被trace進程的暫停,這樣父進程就能經過ptrace系統調用得到子進程的運行狀況了。
 
軟中斷:
什麼是硬件中斷?
CPU有一個單獨的執行序列,會一條指令一條指令的順序執行。要處理相似I/O或者硬件時鐘這樣的異步事件,CPU就要用到中斷。硬件中斷一般是一個專門的電信號,鏈接到一個特殊的「響應電路」上。這個電路會感知中斷的到來。而後讓CPU中止當前的執行流,保存當前的狀態,而後跳轉到一個預約義的地址處去執行,這個地址上有一箇中斷處理例程。當中斷處理例程完成它的工做後,CPU就從以前中止的地方恢復執行。
軟中斷的原理相似:CPU支持特殊指令來模擬一箇中斷。當執行到這個指令以後,CPU將其當作一箇中斷,中止當前的正常的執行流,保存狀態而後跳轉到一個處理例程中執行。這種「陷阱」讓許多現代操做系統得以有效的完成不少複雜的任務——任務調度,虛擬內存,內存保護,調試等。
 
int3指令:
上文所提到的特殊指令就是經過int3完成的,陷阱指令。——對預約義的中斷處理例程的調用。x86支持int指令帶有一個8位的操做數,用來指定所發生的中斷號。所以,理論上能夠支持256種「陷阱」。前32個由CPU本身保留,這裏第3號就是咱們感興趣的——稱爲「trap to debugger」。
 
一個例子:
經過 int 3 指令在調試器中設定斷點

要在被調試進程中的某個目標地址上設定一個斷點,調試器須要作下面兩件事情:

1.  保存目標地址上的數據

2.  將目標地址上的第一個字節替換爲int 3指令

而後,當調試器向操做系統請求開始運行進程時(經過前一篇文章中提到的PTRACE_CONT),進程最終必定會碰到int 3指令。此時進程中止,操做系統將發送一個信號。這時就是調試器再次出馬的時候了,接收到一個其子進程(或被跟蹤進程)中止的信號,而後調試器要作下面幾 件事:

1.  在目標地址上用原來的指令替換掉int 3

2.  將被跟蹤進程中的指令指針向後遞減1。這麼作是必須的,由於如今指令指針指向的是已經執行過的int 3以後的下一條指令。

3.  因爲進程此時仍然是中止的,用戶能夠同被調試進程進行某種形式的交互。這裏調試器可讓你查看變量的值,檢查調用棧等等。

4.  當用戶但願進程繼續運行時,調試器負責將斷點再次加到目標地址上(因爲在第一步中斷點已經被移除了),除非用戶但願取消斷點。


內存泄漏怎麼處理的?
 
內存泄露:
 
什麼是內存泄露?
這個問題,在博客中好像有一個文章,雖然是轉載的可是對內存泄露作了一些說明,就是當咱們用new或者malloc申請了內存,可是沒有用delete或者ree及時的釋放了內存,結果致使一直佔據該內存。內存泄漏形象的比喻是「操做系統可提供給全部進程的存儲空間被某個進程榨乾」,最終結果是程序運行時間越長,佔用存儲空間愈來愈多,最終用盡所有存儲空間,整個系統崩潰。
程序退出之後,能不能回收內存?
程序結束後,會釋放 其申請的全部內存,這樣是能夠解決問題。可是你的程序仍是有問題的,就如你寫了一個函數,申請了一塊內存,可是沒有釋放,每調用一次你的函數就會白白浪費一些內存。若是你的程序不停地在運行,就會有不少的內存被浪費,最後可能你的程序會由於用掉內存太多而被操做系統殺死。
 
智能指針:Effective C++ 建議咱們將對象放到智能指針裏,能夠有效的避免內存泄露。
 
什麼是智能指針?
 
一種相似指針的數據類型,將對象存儲在智能指針中,能夠不須要處理內存泄露的問題,它會幫你調用對象的析構函數自動撤銷對象(主要是智能指針本身的析構函數用了delete ptr,delete會自動調用指針對象的析構函數,前提該內存是在堆上的,若是是在棧上就會出錯),釋放內存。所以,你要作的就是在析構函數中釋放掉數據成員的資源。
template <class T> class auto_ptr
{
    T* ptr;
public:
    explicit auto_ptr(T* p = 0) : ptr(p) {}
    ~auto_ptr()                 {delete ptr;}
    T& operator*()              {return *ptr;}
    T* operator->()             {return ptr;}
    // ...
};

 從上面auto_ptr能夠看出來,智能指針將基本類型指針封裝爲類對象指針(這個類確定是個模板,以適應不一樣基本類型的需求),並在析構函數裏編寫delete語句刪除指針指向的內存空間。

  • 全部的智能指針類都有一個explicit構造函數,以指針做爲參數。好比auto_ptr的類模板原型爲:
    templet<class T>
    class auto_ptr { explicit auto_ptr(X* p = 0) ; ... };

    所以不能自動將指針轉換爲智能指針對象,必須顯式調用:

    shared_ptr<double> pd; double *p_reg = new double; pd = p_reg;                               // not allowed (implicit conversion)
    pd = shared_ptr<double>(p_reg);           // allowed (explicit conversion)
    shared_ptr<double> pshared = p_reg;       // not allowed (implicit conversion)
    shared_ptr<double> pshared(p_reg);        // allowed (explicit conversion)

     

  • 對所有三種智能指針都應避免的一點:
    string vacation("I wandered lonely as a cloud."); shared_ptr<string> pvac(&vacation);   // No

    pvac過時時,程序將把delete運算符用於非堆內存,這是錯誤的。

四種智能指針:

STL一共給咱們提供了四種智能指針:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文章暫不討論)。

模板auto_ptr是C++98提供的解決方案,C+11已將將其摒棄,並提供了另外兩種解決方案。然而,雖然auto_ptr被摒棄,但它已使用了好多年:同時,若是您的編譯器不支持其餘兩種解決力案,auto_ptr將是惟一的選擇。

爲何摒棄auto_ptr?

先來看下面的賦值語句:

auto_ptr< string> ps (new string ("I reigned lonely as a cloud.」);
auto_ptr<string> vocation; vocaticn = ps;

上述賦值語句將完成什麼工做呢?若是ps和vocation是常規指針,則兩個指針將指向同一個string對象。這是不能接受的,由於程序將試圖刪除同一個對象兩次——一次是ps過時時,另外一次是vocation過時時。要避免這種問題,方法有多種:

  • 定義陚值運算符,使之執行深複製。這樣兩個指針將指向不一樣的對象,其中的一個對象是另外一個對象的副本,缺點是浪費空間,因此智能指針都未採用此方案。
  • 創建全部權(ownership)概念。對於特定的對象,只能有一個智能指針可擁有,這樣只有擁有對象的智能指針的構造函數會刪除該對象。而後讓賦值操做轉讓全部權。這就是用於auto_ptr和unique_ptr 的策略,但unique_ptr的策略更嚴格。
  • 建立智能更高的指針,跟蹤引用特定對象的智能指針數。這稱爲引用計數。例如,賦值時,計數將加1,而指針過時時,計數將減1,。當減爲0時才調用delete。這是shared_ptr採用的策略。

四種智能指針:

 

列1 列2
auto_ptr 內部使用一個成員變量,指向一塊內存資源(構造函數),
並在析構函數中釋放內存資源。(未實現深複製,所以拷貝一個auto_ptr將會有刪除兩次一個內存的潛在問題)
unique_ptr 獨享全部權的智能指針:
一、擁有它指向的對象

二、沒法進行復制構造,沒法進行復制賦值操做。即沒法使兩個unique_ptr指向同一個對象。可是能夠進行移動構造和移動賦值操做(全部權轉讓)

三、保存指向某個對象的指針,當它自己被刪除釋放的時候,會使用給定的刪除器釋放它指向的對象
shared_ptr 使用計數機制來代表資源被幾個指針共享。能夠經過成員函數use_count()來查看資源的全部者個數。
拷貝構造時候,計數器會加一。當咱們調用release()時,當前指針會釋放資源全部權,計數減一。當計數等於0時,資源會被釋放
會有死鎖問題,引入weak_ptr:,若是說兩個shared_ptr相互引用,那麼這兩個指針的引用計數永遠不可能降低爲0,資源永遠不會釋放。
weak_ptr 構造和析構不會引發引用記數的增長或減小。協助shared_ptr,沒有重載*和->但可使用lock得到一個可用的shared_ptr對象
 
VLD(Visual Leak Detector):一個檢測內存泄露的工具

Visual C++內置內存泄露檢測工具,可是功能十分有限。VLD就至關強大,能夠定位文件、行號,能夠很是準確地找到內存泄漏的位置,並且還免費、開源

在使用的時候只要將VLD的頭文件和lib文件放在工程文件中便可。

也能夠一次設置,新工程就不用從新設置了。只介紹在Visual Studio 2003/2005中的設置方法,VC++ 6.0相似:

    1. 打開Tools -> Options -> Projects and Solutions -> VC++ Directories;
    2. 而後點擊include files下拉列表,在末尾把VLD安裝目錄中的include文件夾添加進來;
    3. 一樣點擊lib下拉列表,把VLD的lib也添加進來;
    4. 在須要檢測內存泄漏的源文件中添加
#include 「vld.h」

順序無所謂,可是必定不能在一些預編譯的文件前(如stdafx.h)。我是加在stdafx.h文件最後。

  1. 把安裝目錄下dll文件夾中的全部dll文件拷貝到工程Debug目錄,也就是Debug版.exe生成的位置。點擊Debug –> Start Debugging 調試程序,在OUTPUT窗口中就會顯示程序運行過程當中的內存泄漏的文件、行號還有內容了。
---------- Block 2715024 at 0x04D8A368: 512 bytes ----------
  Call Stack:
    d:\kangzj\documents\visual studio 2005\projects\rsip.root\readtiff\readtiff\segmentflag.cpp (56): CSegmentFlag::GetFlagFromArray
    d:\kangzj\documents\visual studio 2005\projects\rsip.root\readtiff\readtiff\wholeclassdlg.cpp (495): segmentThreadProc
    f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\thrdcore.cpp (109): _AfxThreadEntry
    f:\dd\vctools\crt_bld\self_x86\crt\src\threadex.c (348): _callthreadstartex
    f:\dd\vctools\crt_bld\self_x86\crt\src\threadex.c (331): _threadstartex
    0x7C80B729 (File and line number not available): GetModuleFileNameA
 
 CRT庫也有內存檢測工具,博客裏有總結,不贅述了。
 
STL組件裏,有sort函數,它用了哪些排序算法?
 STL提供的各式算法裏,sort算法是最複雜,最龐大的一個。這個算法接受兩個randomaccessiterator(隨機存取迭代器),而後將元素從小到大排序。
縱觀STL的container,關係型container例如map和set利用RB樹自動排序,不須要用到sort。stack和queue和priority-queue都有特定的出入口,不容許用戶進行排序。
剩下vector,list和deque,list的迭代器屬於bidirectional-iterator,剩下vector和deque適合用sort算法。若是要對list和slist排序,應該使用member function sort。
 
STL的 sort 算法,數據量大的時候採用Quick Sort,分段遞歸排序。一旦分段後的數據量小於某個門檻,爲避免quick sort的遞歸調用帶來過大的額外負擔,就改用插入排序,還會改用堆排序。
 

std::sort的實現

template <class RandomAccessIterator>
inline void sort(RandomAccessIterator first, RandomAccessIterator last) {
    if (first != last) {
        __introsort_loop(first, last, value_type(first), __lg(last - first) * 2);
        __final_insertion_sort(first, last);
    }
}

 

它是一個模板函數,只接受隨機訪問迭代器。if語句先判斷區間有效性,接着調用__introsort_loop,它就是STL的Introspective Sort實現。在該函數結束以後,最後調用插入排序。咱們來揭開該算法的面紗:

template <class RandomAccessIterator, class T, class Size>
void __introsort_loop(RandomAccessIterator first,
                      RandomAccessIterator last, T*,
                      Size depth_limit) {
    while (last - first > __stl_threshold) {
        if (depth_limit == 0) {
            partial_sort(first, last, last);
            return;
        }
        --depth_limit;
        RandomAccessIterator cut = __unguarded_partition
          (first, last, T(__median(*first, *(first + (last - first)/2),
                                   *(last - 1))));
        __introsort_loop(cut, last, value_type(first), depth_limit);
        last = cut;
    }
}

 

 

 

咱們來比較一下二者的區別,試想,若是一個序列只須要遞歸兩次即可結束,即它能夠分紅四個子序列。原始的方式須要兩個遞歸函數調用,接着二者各自調 用一次,也就是說進行了7次函數調用,以下圖左邊所示。可是STL這種寫法每次劃分子序列以後僅對右子序列進行函數調用,左邊子序列進行正常的循環調用, 以下圖右邊所示。

兩種遞歸調用對比

二者區別就在於STL節省了接近一半的函數調用,因爲每次的函數調用有必定的開銷,所以對於數據量很是龐大時,這一半的函數調用可能可以省下至關可觀的時 間。真是爲了效率無所不用其極,使人驚歎!更關鍵是這並無帶來太多的可讀性的下降,稍稍一經分析便可以讀懂。這種稍稍以犧牲可讀性來換取效率的作法在 STL的實現中比比皆是,本文後面還會有例子。(more)

 

調試器工做原理(2):實現斷點

本文是關於調試器工做原理探究系列的第二篇。在開始閱讀本文前,請先確保你已經讀過本系列的第一篇(基礎篇)

本文的主要內容

這裏我將說明調試器中的斷點機制是如何實現的。斷點機制是調試器的兩大主要支柱之一 ——另外一個是在被調試進程的內存空間中查看變量的值。咱們已經在第一篇文章中稍微涉及到了一些監視被調試進程的知識,但斷點機制仍然仍是個迷。閱讀完本文以後,這將再也不是什麼祕密了。

軟中斷

要在x86體系結構上實現斷點咱們要用到軟中斷(也稱爲「陷阱」trap)。在咱們深刻細節以前,我想先大體解釋一下中斷和陷阱的概念。

CPU有一個單獨的執行序列,會一條指令一條指令的順序執行。要處理相似IO或者硬件時鐘這樣的異步事件時CPU就要用到中斷。硬件中斷一般是一個 專門的電信號,鏈接到一個特殊的「響應電路」上。這個電路會感知中斷的到來,而後會使CPU中止當前的執行流,保存當前的狀態,而後跳轉到一個預約義的地 址處去執行,這個地址上會有一箇中斷處理例程。當中斷處理例程完成它的工做後,CPU就從以前中止的地方恢復執行。

軟中斷的原理相似,但實際上有一點不一樣。CPU支持特殊的指令容許經過軟件來模擬一箇中斷。當執行到這個指令時,CPU將其當作一箇中斷——中止當 前正常的執行流,保存狀態而後跳轉到一個處理例程中執行。這種「陷阱」讓許多現代的操做系統得以有效完成不少複雜任務(任務調度、虛擬內存、內存保護、調 試等)。

一些編程錯誤(好比除0操做)也被CPU當作一個「陷阱」,一般被認爲是「異常」。這裏軟中斷同硬件中斷之間的界限就變得模糊了,由於這裏很難說這種異常究竟是硬件中斷仍是軟中斷引發的。我有些偏離主題了,讓咱們回到關於斷點的討論上來。

關於int 3指令

看過前一節後,如今我能夠簡單地說斷點就是經過CPU的特殊指令——int 3來實現的。int就是x86體系結構中的「陷阱指令」——對預約義的中斷處理例程的調用。x86支持int指令帶有一個8位的操做數,用來指定所發生的 中斷號。所以,理論上能夠支持256種「陷阱」。前32個由CPU本身保留,這裏第3號就是咱們感興趣的——稱爲「trap to debugger」。

很少說了,我這裏就引用「聖經」中的原話吧(這裏的聖經就是Intel’s Architecture software developer’s manual, volume2A):

「INT 3指令產生一個特殊的單字節操做碼(CC),這是用來調用調試異常處理例程的。(這個單字節形式很是有價值,由於這樣能夠經過一個斷點來替換掉任何指令的第一個字節,包括其它的單字節指令也是同樣,而不會覆蓋到其它的操做碼)。」

上面這段話很是重要,但如今解釋它仍是太早,咱們稍後再來看。

使用int 3指令

是的,懂得事物背後的原理是很棒的,可是這到底意味着什麼?咱們該如何使用int 3來實現斷點機制?套用常見的編程問答中出現的對話——請用代碼說話!

實際上這真的很是簡單。一旦你的進程執行到int 3指令時,操做系統就將它暫停。在Linux上(本文關注的是Linux平臺),這會給該進程發送一個SIGTRAP信號。

這就是所有——真的!如今回顧一下本系列文章的第一篇,跟蹤(調試器)進程能夠得到全部其子進程(或者被關聯到的進程)所獲得信號的通知,如今你知道咱們該作什麼了吧?

就是這樣,再沒有什麼計算機體系結構方面的東東了,該寫代碼了。

手動設定斷點

如今我要展現如何在程序中設定斷點。用於這個示例的目標程序以下:

我如今使用的是彙編語言,這是爲了不當使用C語言時涉及到的編譯和符號的問題。上面列出的程序功能就是在一行中打印「Hello,」,而後在下一行中打印「world!」。這個例子與上一篇文章中用到的例子很類似。

我但願設定的斷點位置應該在第一條打印以後,但剛好在第二條打印以前。咱們就讓斷點打在第一個int 0x80指令以後吧,也就是mov edx, len2。首先,我須要知道這條指令對應的地址是什麼。運行objdump –d:

經過上面的輸出,咱們知道要設定的斷點地址是0x8048096。等等,真正的調試器不是像這樣工做的,對吧?真正的調試器能夠根據代碼行數或者函 數名稱來設定斷點,而不是基於什麼內存地址吧?很是正確。可是咱們離那個標準還差的遠——若是要像真正的調試器那樣設定斷點,咱們還須要涵蓋符號表以及調 試信息方面的知識,這須要用另外一篇文章來講明。至於如今,咱們還必須得經過內存地址來設定斷點。

看到這裏我真的很想再扯一點題外話,因此你有兩個選擇。若是你真的對於爲何地址是0x8048096,以及這表明什麼意思很是感興趣的話,接着看下一節。若是你對此毫無興趣,只是想看看怎麼設定斷點,能夠略過這一部分。

題外話——進程地址空間以及入口點

坦白的說,0x8048096自己並無太大意義,這只不過是相對可執行鏡像的代碼段(text section)開始處的一個偏移量。若是你仔細看看前面objdump出來的結果,你會發現代碼段的起始位置是0x08048080。這告訴了操做系統 要將代碼段映射到進程虛擬地址空間的這個位置上。在Linux上,這些地址能夠是絕對地址(好比,有的可執行鏡像加載到內存中時是不可重定位的),由於在 虛擬內存系統中,每一個進程都有本身獨立的內存空間,並把整個32位的地址空間都看作是屬於本身的(稱爲線性地址)。

若是咱們經過readelf工具來檢查可執行文件的ELF頭,咱們將獲得以下輸出:

注意,ELF頭的「entry point address」一樣指向的是0x8048080。所以,若是咱們把ELF文件中的這個部分解釋給操做系統的話,就表示:

1.  將代碼段映射到地址0x8048080處

2.  從入口點處開始執行——地址0x8048080

可是,爲何是0x8048080呢?它的出現是因爲歷史緣由引發的。每一個進程的地址空間的前128MB被保留給棧空間了(注:這一部分緣由可參考 Linkers and Loaders)。128MB恰好是0x80000000,可執行鏡像中的其餘段能夠從這裏開始。0x8048080是Linux下的連接器ld所使用的 默認入口點。這個入口點能夠經過傳遞參數-Ttext給ld來進行修改。

所以,獲得的結論是這個地址並無什麼特別的,咱們能夠自由地修改它。只要ELF可執行文件的結構正確且在ELF頭中的入口點地址同程序代碼段(text section)的實際起始地址相吻合就OK了。

經過int 3指令在調試器中設定斷點

要在被調試進程中的某個目標地址上設定一個斷點,調試器須要作下面兩件事情:

1.  保存目標地址上的數據

2.  將目標地址上的第一個字節替換爲int 3指令

而後,當調試器向操做系統請求開始運行進程時(經過前一篇文章中提到的PTRACE_CONT),進程最終必定會碰到int 3指令。此時進程中止,操做系統將發送一個信號。這時就是調試器再次出馬的時候了,接收到一個其子進程(或被跟蹤進程)中止的信號,而後調試器要作下面幾 件事:

1.  在目標地址上用原來的指令替換掉int 3

2.  將被跟蹤進程中的指令指針向後遞減1。這麼作是必須的,由於如今指令指針指向的是已經執行過的int 3以後的下一條指令。

3.  因爲進程此時仍然是中止的,用戶能夠同被調試進程進行某種形式的交互。這裏調試器可讓你查看變量的值,檢查調用棧等等。

4.  當用戶但願進程繼續運行時,調試器負責將斷點再次加到目標地址上(因爲在第一步中斷點已經被移除了),除非用戶但願取消斷點。

相關文章
相關標籤/搜索