常見C++面試題及基本知識點總結(一)


【轉載請注明出處】:http://www.cnblogs.com/LUO77/p/5771237.html 

1. 結構體和共同體的區別。

定義:

結構體struct:把不同類型的數據組合成一個整體,自定義類型。

共同體union:使幾個不同類型的變量共同占用一段內存。

地址:

struct和union都有內存對齊,結構體的內存布局依賴於CPU、操作系統、編譯器及編譯時的對齊選項。

關於內存對齊,先讓我們看四個重要的基本概念:
1.數據類型自身的對齊值:
對於char型數據,其自身對齊值為1,對於short型為2,對於int,float,double類型,其自身對齊值為4,單位字節。
2.結構體或者類的自身對齊值:其成員中自身對齊值最大的那個值。
3.指定對齊值:#pragma pack(n),n=1,2,4,8,16改變系統的對齊系數
4.數據成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中小的那個值。

 常見數據類型及其長度:

注意long int和int一樣是4byte,long double和double一樣是8byte。(關於long double,ANSI C標准規定了double變量存儲為 IEEE 64 位(8 個字節)浮點數值,但並未規定long double的確切精度。所以對於不同平台可能有不同的實現。有的是8字節,有的是10字節,有的是12字節或16字節。)

在標准c++中,int的定義長度要依靠你的機器的字長,也就是說,如果你的機器是32位的,int的長度為32位,如果你的機器是64位的,那么int的標准長度就是64位。經測試,在64位操作系統下,int的長度還是32位的。

從上面的一段文字中,我們可以看出,首先根據結構體內部成員的自身對齊值得到結構體的自身對齊值(內部成員最大的長度),如果沒有修改系統設定的默認補齊長度4的話,取較小的進行內存補齊。

結構體struct:不同之處,stuct里每個成員都有自己獨立的地址。sizeof(struct)是內存對齊后所有成員長度的加和。

共同體union:當共同體中存入新的數據后,原有的成員就失去了作用,新的數據被寫到union的地址中。sizeof(union)是最長的數據成員的長度。

總結: struct和union都是由多個不同的數據類型成員組成, 但在任何同一時刻, union中只存放了一個被選中的成員, 而struct的所有成員都存在。在struct中,各成員都占有自己的內存空間,它們是同時存在的。一個struct變量的總長度等於所有成員長度之和。在Union中,所有成員不能同時占用它的內存空間,它們不能同時存在。Union變量的長度等於最長的成員的長度。對於union的不同成員賦值, 將會對其它成員重寫, 原來成員的值就不存在了, 而對於struct的不同成員賦值是互不影響的。

 


 

 2.static 和const分別怎么用,類里面static和const可以同時修飾成員函數嗎。

 static的作用:

對變量:

1.局部變量:

在局部變量之前加上關鍵字static,局部變量就被定義成為一個局部靜態變量。

  1)內存中的位置:靜態存儲區

  2)初始化:局部的靜態變量只能被初始化一次,且C中不可以用變量對其初始化,而C++可以用變量對其初始化。(詳見:http://www.cnblogs.com/novice-dxx/p/7094690.html

  3)作用域:作用域仍為局部作用域,當定義它的函數或者語句塊結束的時候,作用域隨之結束。

 注:當static用來修飾局部變量的時候,它就改變了局部變量的存儲位置(從原來的棧中存放改為靜態存儲區)及其生命周期(局部靜態變量在離開作用域之后,並沒有被銷毀,而是仍然駐留在內存當中,直到程序結束,只不過我們不能再對他進行訪問),但未改變其作用域。

2.全局變量

在全局變量之前加上關鍵字static,全局變量就被定義成為一個全局靜態變量。

1)內存中的位置:靜態存儲區(靜態存儲區在整個程序運行期間都存在)

2)初始化:未經初始化的全局靜態變量會被程序自動初始化為0(自動對象的值是任意的,除非他被顯示初始化)

3)作用域:全局靜態變量在聲明他的文件之外是不可見的。准確地講從定義之處開始到文件結尾。

注:static修飾全局變量,並未改變其存儲位置及生命周期,而是改變了其作用域,使當前文件外的源文件無法訪問該變量,好處如下:(1)不會被其他文件所訪問,修改(2)其他文件中可以使用相同名字的變量,不會發生沖突。對全局函數也是有隱藏作用。而普通全局變量只要定義了,任何地方都能使用,使用前需要聲明所有的.c文件,只能定義一次普通全局變量,但是可以聲明多次(外部鏈接)。注意:全局變量的作用域是全局范圍,但是在某個文件中使用時,必須先聲明。

對類中的:

    1.成員變量

    用static修飾類的數據成員實際使其成為類的全局變量,會被類的所有對象共享,包括派生類的對象。因此,static成員必須在類外進行初始化(初始化格式: int base::var=10;),而不能在構造函數內進行初始化,不過也可以用const修飾static數據成員在類內初始化 。

    特點:

    1. 不要試圖在頭文件中定義(初始化)靜態數據成員。在大多數的情況下,這樣做會引起重復定義這樣的錯誤。即使加上#ifndef #define #endif或者#pragma once也不行。 
    2. 靜態數據成員可以成為成員函數的可選參數,而普通數據成員則不可以。
    3. 靜態數據成員的類型可以是所屬類的類型,而普通數據成員則不可以。普通數據成員的只能聲明為 所屬類類型的指針或引用。

2.成員函數

    1. 用static修飾成員函數,使這個類只存在這一份函數,所有對象共享該函數,不含this指針。
    2. 靜態成員是可以獨立訪問的,也就是說,無須創建任何對象實例就可以訪問。base::func(5,3);當static成員函數在類外定義時不需要加static修飾符。
    3. 在靜態成員函數的實現中不能直接引用類中說明的非靜態成員,可以引用類中說明的靜態成員。因為靜態成員函數不含this指針。 

不可以同時用const和static修飾成員函數。

C++編譯器在實現const的成員函數的時候為了確保該函數不能修改類的實例的狀態,會在函數中添加一個隱式的參數const this*。但當一個成員為static的時候,該函數是沒有this指針的。也就是說此時const的用法和static是沖突的。

我們也可以這樣理解:兩者的語意是矛盾的。static的作用是表示該函數只作用在類型的靜態變量上,與類的實例沒有關系;而const的作用是確保函數不能修改類的實例的狀態,與類型的靜態變量沒有關系。因此不能同時用它們。

const的作用:

 1.限定變量為不可修改。

2.限定成員函數不可以修改任何數據成員。

3.const與指針:

const char *p 表示 指向的內容不能改變。

char * const p,就是將P聲明為常指針,它的地址不能改變,是固定的,但是它的內容可以改變。


 

 3.指針和引用的區別,引用可以用常指針實現嗎。

本質上的區別是,指針是一個新的變量,只是這個變量存儲的是另一個變量的地址,我們通過訪問這個地址來修改變量。

而引用只是一個別名,還是變量本身。對引用進行的任何操作就是對變量本身進行操作,因此以達到修改變量的目的。

(1)指針:指針是一個變量,只不過這個變量存儲的是一個地址,指向內存的一個存儲單元;而引用跟原來的變量實質上是同一個東西,只不過是原變量的一個別名而已。如:
int a=1;int *p=&a;
int a=1;int &b=a;
上面定義了一個整形變量和一個指針變量p,該指針變量指向a的存儲單元,即p的值是a存儲單元的地址。
而下面2句定義了一個整形變量a和這個整形a的引用b,事實上a和b是同一個東西,在內存占有同一個存儲單元。
(2)可以有const指針,但是沒有const引用(const引用可讀不可改,與綁定對象是否為const無關)
(3)指針可以有多級,但是引用只能是一級(int **p;合法 而 int &&a是不合法的)
(4)指針的值可以為,但是引用的值不能為NULL,並且引用在定義的時候必須初始化;
(5)指針的值在初始化后可以改變,即指向其它的存儲單元,而引用在進行初始化后就不會再改變了。
(6)"sizeof引用"得到的是所指向的變量(對象)的大小,而"sizeof指針"得到的是指針本身的大小;
(7)指針和引用的自增(++)運算意義不一樣;
指針傳參的時候,還是值傳遞,試圖修改傳進來的指針的值是不可以的。只能修改地址所保存變量的值。
引用傳參的時候,傳進來的就是變量本身,因此可以被修改。

4.什么是多態,多態有什么用途。

  1. 定義:“一個接口,多種方法”,程序在運行時才決定調用的函數。
  2. 實現:C++多態性主要是通過虛函數實現的,虛函數允許子類重寫override(注意和overload的區別,overload是重載,是允許同名函數的表現,這些函數參數列表/類型不同)。
多態與非多態的實質區別就是函數地址是早綁定還是晚綁定。如果函數的調用,在編譯器編譯期間就可以確定函數的調用地址,並生產代碼,是靜態的,就是說地址是早綁定的。而如果函數調用的地址不能在編譯器期間確定,需要在運行時才確定,這就屬於晚綁定。

3.目的:接口重用。封裝可以使得代碼模塊化,繼承可以擴展已存在的代碼,他們的目的都是為了代碼重用。而多態的目的則是為了接口重用。

4.用法:聲明基類的指針,利用該指針指向任意一個子類對象,調用相應的虛函數,可以根據指向的子類的不同而實現不同的方法。

補充一下關於重載、重寫、隱藏(總是不記得)的區別:

Overload(重載):在C++程序中,可以將語義、功能相似的幾個函數用同一個名字表示,但參數或返回值不同(包括類型、順序不同),即函數重載。
(1)相同的范圍(在同一個類中);
(2)函數名字相同;
(3)參數不同;
(4)virtual 關鍵字可有可無。
Override(重寫):是指派生類函數覆蓋基類函數,特征是:
(1)不同的范圍(分別位於派生類與基類);
(2)函數名字相同;
(3)參數相同;
(4)基類函數必須有virtual 關鍵字。
注:重寫基類虛函數的時候,會自動轉換這個函數為virtual函數,不管有沒有加virtual,因此重寫的時候不加virtual也是可以的,不過為了易讀性,還是加上比較好。 Overwrite(隱藏):隱藏,是指派生類的函數屏蔽了與其同名的基類函數,規則如下: (1)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual關鍵字,基類的函數將被隱藏(注意別與重載混淆)。 (2)如果派生類的函數與基類的函數同名,並且參數也相同,但是基類函數沒有virtual關鍵字。此時,基類的函數被隱藏(注意別與覆蓋混淆)。

補充一下虛函數表:

多態是由虛函數實現的,而虛函數主要是通過虛函數表(V-Table)來實現的。

如果一個類中包含虛函數(virtual修飾的函數),那么這個類就會包含一張虛函數表,虛函數表存儲的每一項是一個虛函數的地址。如下圖:

這個類的每一個對象都會包含一個虛指針(虛指針存在於對象實例地址的最前面,保證虛函數表有最高的性能),這個虛指針指向虛函數表。

注:對象不包含虛函數表,只有虛指針,類才包含虛函數表,派生類會生成一個兼容基類的虛函數表。

  • 原始基類的虛函數表

  下圖是原始基類的對象,可以看到虛指針在地址的最前面,指向基類的虛函數表(假設基類定義了3個虛函數)

  • 單繼承時的虛函數(無重寫基類虛函數

假設現在派生類繼承基類,並且重新定義了3個虛函數,派生類會自己產生一個兼容基類虛函數表的屬於自己的虛函數表

  Derive class 繼承了 Base class 中的三個虛函數,准確的說,是該函數實體的地址被拷貝到 Derive類的虛函數表,派生類新增的虛函數置於虛函數表的后面,並按聲明順序存放

  • 單繼承時的虛函數(重寫基類虛函數

現在派生類重寫基類的x函數,可以看到這個派生類構建自己的虛函數表的時候,修改了base::x()這一項,指向了自己的虛函數。

  • 多重繼承時的虛函數(Derived ::public Base1,public Base2)

這個派生類多重繼承了兩個基類base1,base2,因此它有兩個虛函數表。

  

  它的對象會有多個虛指針(據說和編譯器相關),指向不同的虛函數表。

  多重繼承時指針的調整:

Derive b;
Base1* ptr1 = &b;   // 指向 b 的初始地址
Base2* ptr2 = &b;   // 指向 b 的第二個子對象

因為 Base1 是第一個基類,所以 ptr1 指向的是 Derive 對象的起始地址,不需要調整指針(偏移)。

因為 Base2 是第二個基類,所以必須對指針進行調整,即加上一個 offset,讓 ptr2 指向 Base2 子對象。

當然,上述過程是由編譯器完成的。

Base1* b1 = (Base1*)ptr2;
b1->y();                   // 輸出 Base2::y()
Base2* b2 = (Base2*)ptr1;
b2->y();                   // 輸出 Base1::y()

其實,通過某個類型的指針訪問某個成員時,編譯器只是根據類型的定義查找這個成員所在偏移量,用這個偏移量獲取成員。由於 ptr2 本來就指向 Base2 子對象的起始地址,所以b1->y()調用到的是Base2::y(),而 ptr1 本來就指向 Base1 子對象的起始地址(即 Derive對象的起始地址),所以b2->y()調用到的是Base1::y()

  • 虛繼承時的虛函數表

  虛繼承的引入把對象的模型變得十分復雜,除了每個基類(MyClassA和MyClassB)和公共基類(MyClass)的虛函數表指針需要記錄外,每個虛擬繼承了MyClass的父類還需要記錄一個虛基類表vbtable的指針vbptr。MyClassC的對象模型如圖4所示。

  

   虛基類表每項記錄了被繼承的虛基類子對象相對於虛基類表指針的偏移量。比如MyClassA的虛基類表第二項記錄值為24,正是MyClass::vfptr相對於MyClassA::vbptr的偏移量,同理MyClassB的虛基類表第二項記錄值12也正是MyClass::vfptr相對於MyClassA::vbptr的偏移量。(虛函數與虛繼承深入探討

對象模型探討:

 

1.沒有繼承情況,vptr存放在對象的開始位置,以下是Base1的內存布局

m_iData :100


 2.單繼承的情況下,對象只有一個vptr,它存放在對象的開始位置,派生類子對象在父類子對象的最后面,以下是D1的內存布局

B1:: m_iData : 100

B1::vptr : 4294800

B2::vptr : 4294776

D::m_iData :300


4. 虛擬繼承情況下,虛父類子對象會放在派生類子對象之后,派生類子對象的第一個位置存放着一個vptr,虛擬子類子對象也會保存一個vptr,以下是VD1的內存布局

 

 Unknown : 4294888

B1::vptr :4294864

VD1::vptr :        4294944

VD1::m_iData :  200

VD2::Unknown : 4294952

VD::m_iData : 500

B1::m_iData :  100

5. 棱形繼承的情況下,非虛基類子對象在派生類子對象前面,並按照聲明順序排列,虛基類子對象在派生類子對象后面

VD1::Unknown : 4294968

VD2::vptr :    4   294932

VD2::m_iData : 300

B1::vptr :       4294920

B1::m_iData :  100

 

補充一下純虛函數:

  • 定義: 在很多情況下,基類本身生成對象是不合情理的。為了解決這個問題,方便使用類的多態性,引入了純虛函數的概念,將函數定義為純虛函數(方法:virtual ReturnType Function()= 0;)純虛函數不能再在基類中實現,編譯器要求在派生類中必須予以重寫以實現多態性。同時含有純虛擬函數的類稱為抽象類,它不能生成對象。
  • 特點:

1,當想在基類中抽象出一個方法,且該基類只做能被繼承,而不能被實例化;(避免類被實例化且在編譯時候被發現,可以采用此方法)

2,這個方法必須在派生類(derived class)中被實現;

  • 目的:使派生類僅僅只是繼承函數的接口。
補充一下純虛函數:
  • 定義:稱帶有純虛函數的類為抽象類。
  • 作用:為一個繼承體系提供一個公共的根,為派生類提供操作接口的通用語義。
  • 特點:1.抽象類只能作為基類來使用,而繼承了抽象類的派生類如果沒有實現純虛函數,而只是繼承純虛函數,那么該類仍舊是一個抽象類,如果實現了純虛函數,就不再是抽象類。
      2.抽象類不可以定義對象。
補充一下多重繼承和虛繼承:
多重繼承:
定義:派生類繼承多個基類,派生類為每個基類(顯式或隱式地)指定了訪問級別—— publicprotected 或  private
    class Panda : public Bear, public Endangered {
    }

構造:

    1. 派生類的對象包含每個基類的基類子對象。
    2. 派生類構造函數初始化所有基類(多重繼承中若沒有顯式調用某個基類的構造函數,則編譯器會調用該基類默認構造函數),派生類只能初始化自己的基類,並不需要考慮基類的基類怎么初始化。
    3. 多重繼承時,基類構造函數按照基類構造函數在類派生列表中的出現次序調用。
析構:總是按構造函數運行的 逆序調用析構函數。(基類的析構函數最好寫成virtual,否則再子類對象銷毀的時候,無法銷毀子類對象部分資源。) 假定所有根基類都將它們的析構函數適當定義為虛函數,那么,無論通過哪種指針類型刪除對象,虛析構函數的處理都是一致的。
 
拷貝構造/賦值:如果要為派生類編寫拷貝構造函數,則需要為調用基類相應拷貝構造函數並為其傳遞參數,否則只會拷貝派生類部分。
深拷貝與淺拷貝:
淺拷貝:默認的復制構造函數只是完成了對象之間的位拷貝,也就是把對象里的值完全復制給另一個對象,如A=B。這時,如果B中有一個成員變量指針已經申請了內存,那A中的那個成員變量也指向同一塊內存。
    這就出現了問題:當B把內存釋放了(如:析構),這時A內的指針就是野指針了,出現運行錯誤。 深拷貝:自定義復制構造函數需要注意,對象之間發生復制,資源重新分配,即A有5個空間,B也應該有5個空間,而不是指向A的5個空間。

虛繼承與虛基類:

定義:在多重繼承下,一個基類可以在派生層次中出現多次。(派生類對象中可能出現多個基類對象)在 C++ 中,通過使用虛繼承解決這類問題。虛繼承是一種機制,類通過虛繼承指出它希望共享其虛基類的狀態。在虛繼承下,對給定虛基類,無論該類在派生層次中作為虛基類出現多少次,只繼承一個共享的基類子對象。共享的基類子對象稱為虛基類

用法: istream 和  ostream 類對它們的基類進行虛繼承。通過使基類成為虛基類, istream 和  ostream 指定,如果其他類(如  iostream 同時繼承它們兩個,則派生類中只出現它們的公共基類ios的一個副本。通過在派生列表中包含關鍵字  virtual 設置虛基類:
    class istream : public virtual ios { ... };
    class ostream : virtual public ios { ... };
    // iostream inherits only one copy of its ios base class
    class iostream: public istream, public ostream { ... };

5.各個排序算法的時間復雜度和穩定性,快排的原理。

排序深入探討

  • 插入排序

  每次將一個待排序的數據,跟前面已經有序的序列的數字一一比較找到自己合適的位置,插入到序列中,直到全部數據插入完成。

  • 希爾排序

  先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成的)分別進行直接插入排序,然后依次縮減增量再進行排序,待整個序列中的元素基本有序(增量足夠小)時,再對全體元素進行一次直接插入排序。由於希爾排序是對相隔若干距離的數據進行直接插入排序,因此可以形象的稱希爾排序為“跳着插

  • 冒泡排序

通過交換使相鄰的兩個數變成小數在前大數在后,這樣每次遍歷后,最大的數就“沉”到最后面了。重復N次即可以使數組有序。

冒泡排序改進1:在某次遍歷中如果沒有數據交換,說明整個數組已經有序。因此通過設置標志位來記錄此次遍歷有無數據交換就可以判斷是否要繼續循環。

冒泡排序改進2:記錄某次遍歷時最后發生數據交換的位置,這個位置之后的數據顯然已經有序了。因此通過記錄最后發生數據交換的位置就可以確定下次循環的范圍了。

  • 快速排序

“挖坑填數+分治法”,首先令i =L; j = R; 將a[i]挖出形成第一個坑,稱a[i]為基准數。然后j--由后向前找比基准數小的數,找到后挖出此數填入前一個坑a[i]中,再i++由前向后找比基准數大的數,找到后也挖出此數填到前一個坑a[j]中。重復進行這種“挖坑填數”直到i==j。再將基准數填入a[i]中,這樣i之前的數都比基准數小,i之后的數都比基准數大。因此將數組分成二部分再分別重復上述步驟就完成了排序。

  • 選擇排序

數組分成有序區和無序區,初始時整個數組都是無序區,然后每次從無序區選一個最小的元素直接放到有序區的最后,直到整個數組變有序區。

  • 堆排序

堆的插入就是——每次插入都是將新數據放在數組最后,而從這個新數據的父結點到根結點必定是一個有序的數列,因此只要將這個新數據插入到這個有序數列中即可。

堆的刪除就是——堆的刪除就是將最后一個數據的值賦給根結點,然后再從根結點開始進行一次從上向下的調整。調整時先在左右兒子結點中找最小的,如果父結點比這個最小的子結點還小說明不需要調整了,反之將父結點和它交換后再考慮后面的結點。相當於從根結點開始將一個數據在有序數列中進行“下沉”。

因此,堆的插入和刪除非常類似直接插入排序,只不是在二叉樹上進行插入過程。所以可以將堆排序形容為“樹上插

  • 歸並排序

歸並排序主要分為兩步:分數列(divide),每次把數列一分為二,然后分到只有兩個元素的小數列;合數列(Merge),合並兩個已經內部有序的子序列,直至所有數字有序。用遞歸可以實現。

  • 基數排序(桶排序)

基數排序,第一步根據數字的個位分配到每個桶里,在桶內部排序,然后將數字再輸出(串起來);然后根據十位分桶,繼續排序,再串起來。直至所有位被比較完,所有數字已經有序。

   


 

6.vector中size()和capacity()的區別。

 

size()指容器當前擁有的元素個數(對應的resize(size_type)會在容器尾添加或刪除一些元素,來調整容器中實際的內容,使容器達到指定的大小。);capacity()指容器在必須分配存儲空間之前可以存儲的元素總數。

 

size表示的這個vector里容納了多少個元素,capacity表示vector能夠容納多少元素,它們的不同是在於vector的size是2倍增長的。如果vector的大小不夠了,比如現在的capacity是4,插入到第五個元素的時候,發現不夠了,此時會給他重新分配8個空間,把原來的數據及新的數據復制到這個新分配的空間里。(會有迭代器失效的問題)

各容器的特點:

 


7.map和set的原理。

(map和set的四個問題)

map和set的底層實現主要是由紅黑樹實現的。

紅黑樹:

性質1 節點是 紅色黑色
性質2 根節點是 黑色
性質3 每個葉節點(NIL節點,空節點)是 黑色的。
性質4 每個 紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
性質5 從任一節點到其每個葉子的所有路徑都包含相同數目的 黑色節點。
這些約束的好處是:保持了樹的相對平衡,同時又比AVL的插入刪除操作的復雜性要低許多。

深入探討紅黑樹


 

8.tcp為什么要三次握手,tcp為什么可靠。

為什么不能兩次握手:(防止已失效的連接請求又傳送到服務器端,因而產生錯誤)

假設改為兩次握手,client端發送的一個連接請求在服務器滯留了,這個連接請求是無效的,client已經是closed的狀態了,而服務器認為client想要建立

一個新的連接,於是向client發送確認報文段,而client端是closed狀態,無論收到什么報文都會丟棄。而如果是兩次握手的話,此時就已經建立連接了。

服務器此時會一直等到client端發來數據,這樣就浪費掉很多server端的資源。

(校注:此時因為client沒有發起建立連接請求,所以client處於CLOSED狀態,接受到任何包都會丟棄,謝希仁舉的例子就是這種場景。但是如果服務器發送對這個延誤的舊連接報文的確認的同時,客戶端調用connect函數發起了連接,就會使客戶端進入SYN_SEND狀態,當服務器那個對延誤舊連接報文的確認傳到客戶端時,因為客戶端已經處於SYN_SEND狀態,所以就會使客戶端進入ESTABLISHED狀態,此時服務器端反而丟棄了這個重復的通過connect函數發送的SYN包,見第三個圖。而連接建立之后,發送包由於SEQ是以被丟棄的SYN包的序號為准,而服務器接收序號是以那個延誤舊連接SYN報文序號為准,導致服務器丟棄后續發送的數據包)

三次握手的最主要目的是保證連接是雙工的,可靠更多的是通過重傳機制來保證的。 

TCP可靠傳輸的實現:

TCP 連接的每一端都必須設有兩個窗口——一個發送窗口和一個接收窗口。TCP 的可靠傳輸機制用字節的序號進行控制。TCP 所有的確認都是基於序號而不是基於報文段。
發送過的數據未收到確認之前必須保留,以便超時重傳時使用。發送窗口沒收到確認不動,和收到新的確認后前移。

發送緩存用來暫時存放: 發送應用程序傳送給發送方 TCP 准備發送的數據;TCP 已發送出但尚未收到確認的數據。

接收緩存用來暫時存放:按序到達的、但尚未被接收應用程序讀取的數據; 不按序到達的數據。

必須強調三點:
    1>   A 的發送窗口並不總是和 B 的接收窗口一樣大(因為有一定的時間滯后)。
    2>   TCP 標准沒有規定對不按序到達的數據應如何處理。通常是先臨時存放在接收窗口中,等到字節流中所缺少的字節收到后,再按序交付上層的應用進程。
    3>   TCP 要求接收方必須有累積確認的功能,這樣可以減小傳輸開銷(累積確認:一般地講,如果發送方發了包1,包2,包3,包4;接受方成功收到包1,包2,包3。
那么接受方可以發回一個確認包,序號為4(4表示期望下一個收到的包的序號;當然你約定好用3表示也可以),那么發送方就知道包1到包3都發送接收成功,必要時重發包4。一個確認包確認了累積到某一序號的所有包。而不是對沒個序號都發確認包。)
  • TCP報文格式

  (1)序號:Seq序號,占32位,用來標識從TCP源端向目的端發送的字節流,發起方發送數據時對此進行標記。
  (2)確認序號:Ack序號,占32位,只有ACK標志位為1時,確認序號字段才有效,Ack=Seq+1。
  (3)標志位:共6個,即URG、ACK、PSH、RST、SYN、FIN等,具體含義如下:
    (A)URG:緊急指針(urgent pointer)有效。
    (B)ACK:確認序號有效。
    (C)PSH:接收方應該盡快將這個報文交給應用層。
    (D)RST:重置連接。
    (E)SYN:發起一個新連接。
    (F)FIN:釋放一個連接。

 需要注意的是:
  (A)不要將確認序號Ack與標志位中的ACK搞混了。
  (B)確認方Ack=發起方Req+1,兩端配對。

  • 三次握手

TCP三次即建立TCP連接,指建立一個TCP連接時,需要客戶端服務端總共發送3 個包以確認連接的建立。在socket編程中,這一過程中由客戶端執行connect來觸發,流程如下:

(1)第一次握手:Client將標志位SYN置為1(表示要發起一個連接),隨機產生一個值seq=J,並將該數據包發送給Server,Client進入SYN_SENT狀態,等待Server確認。
(2)第二次握手:Server收到數據包后由標志位SYN=1知道Client請求建立連接,Server將標志位SYN和ACK都置為1,ack=J+1,隨機產生一個值seq=K,並將該數據包發送給Client以確認連接請求,Server進入SYN_RCVD狀態。
(3)第三次握手:Client收到確認后,檢查ack是否為J+1,ACK是否為1,如果正確則將標志位ACK置為1,ack=K+1,並將該數據包發送給Server,Server檢查ack是否為K+1,ACK是否為1,如果正確則連接建立成功,Client和Server進入ESTABLISHED狀態,完成三次握手,隨后Client與Server之間可以開始傳輸數據了。

 

SYN攻擊:
  在三次握手過程中,Server發送SYN-ACK之后,收到Client的ACK之前的TCP連接稱為半連接(half-open connect),此時Server處於SYN_RCVD狀態,當收到ACK后,Server轉入ESTABLISHED狀態。SYN攻擊就是Client在短時間內偽造大量不存在的IP地址,並向Server不斷地發送SYN包,Server回復確認包,並等待Client的確認,由於源地址是不存在的,因此,Server需要不斷重發直至超時,這些偽造的SYN包將產時間占用未連接隊列,導致正常的SYN請求因為隊列滿而被丟棄,從而引起網絡堵塞甚至系統癱瘓。SYN攻擊時一種典型的DDOS攻擊,檢測SYN攻擊的方式非常簡單,即當Server上有大量半連接狀態且源IP地址是隨機的,則可以斷定遭到SYN攻擊了,使用如下命令可以讓之現行:
  #netstat -nap | grep SYN_RECV
ddos攻擊:
分布式拒絕服務(DDoS:Distributed Denial of Service)攻擊指借助於客戶/服務器技術,將多個計算機聯合起來作為攻擊平台,對一個或多個目標發動DDoS攻擊,從而成倍地提高拒絕服務攻擊的威力。通常,攻擊者使用一個偷竊帳號將DDoS主控程序安裝在一個計算機上,在一個設定的時間主控程序將與大量代理程序通訊,代理程序已經被安裝在網絡上的許多計算機上。代理程序收到指令時就發動攻擊。利用客戶/服務器技術,主控程序能在幾秒鍾內激活成百上千次代理程序的運行。

 

  • 四次揮手

所謂四次揮手(Four-Way Wavehand)即終止TCP連接,就是指斷開一個TCP連接時,需要客戶端和服務端總共發送4個包以確認連接的斷開。在socket編程中,這一過程由客戶端或服務端任一方執行close來觸發,整個流程如下圖所示:

由於TCP連接時全雙工的,因此,每個方向都必須要單獨進行關閉,這一原則是當一方完成數據發送任務后,發送一個FIN來終止這一方向的連接,收到一個FIN只是意味着這一方向上沒有數據流動了,即不會再收到數據了,但是在這個TCP連接上仍然能夠發送數據,直到這一方向也發送了FIN。首先進行關閉的一方將執行主動關閉,而另一方則執行被動關閉,上圖描述的即是如此。
 (1)第一次揮手:Client發送一個FIN,用來關閉Client到Server的數據傳送,Client進入FIN_WAIT_1狀態。
 (2)第二次揮手:Server收到FIN后,發送一個ACK給Client,確認序號為收到序號+1(與SYN相同,一個FIN占用一個序號),Server進入CLOSE_WAIT狀態。
 (3)第三次揮手:Server發送一個FIN,用來關閉Server到Client的數據傳送,Server進入LAST_ACK狀態。
 (4)第四次揮手:Client收到FIN后,Client進入TIME_WAIT狀態,接着發送一個ACK給Server,確認序號為收到序號+1,Server進入CLOSED狀態,完成四次揮手。

 為什么需要TIME_WAIT

TIMEWAIT狀態也稱為2MSL等待狀態

 1)為實現TCP這種全雙工(full-duplex)連接的可靠釋放

這樣可讓TCP再次發送最后的ACK以防這個ACK丟失(另一端超時並重發最后的FIN)。這種2MSL等待的另一個結果是這個TCP連接在2MSL等待期間,定義這個連接的插口(客戶的IP地址和端口號,服務器的IP地址和端口號)不能再被使用。這個連接只能在2MSL結束后才能再被使用。

2)為使舊的數據包在網絡因過期而消失

每個具體TCP實現必須選擇一個報文段最大生存時間MSL(Maximum Segment Lifetime)。它是任何報文段被丟棄前在網絡內的最長時間。

為什么建立連接是三次握手,而關閉連接卻是四次揮手呢?

 這是因為服務端在LISTEN狀態下,收到建立連接請求的SYN報文后,把ACK和SYN放在一個報文里發送給客戶端。而關閉連接時,當收到對方的FIN報文時,僅僅表示對方不再發送數據了但是還能接收數據,我們也未必全部數據都發送給對方了,所以我們不可以立即close,也可以發送一些數據給對方后,再發送FIN報文給對方來表示同意現在關閉連接,因此,我們的ACK和FIN一般都會分開發送。


 9.函數調用和系統調用的區別。

什么是系統調用?(常見Linux及其分類表

所謂系統調用就是用戶在程序中調用操作系統所提供的一個子功能,也就是系統API,系統調用可以被看做特殊的公共子程序。系統中的各種共享資源都由操作系統統一掌管,因此在用戶程序中,凡是與資源有關的操作(如存儲分配、進行I/O傳輸及管理文件等),都必須通過系統調用方式向操作系統提出服務請求,並由操作系統代為完成。通常,一個操作系統提供的系統調用命令有幾十個乃至上百個之多。這些系統調用按照功能大致可以分為以下幾類:

  • 設備管理:完成設備的請求或釋放,以及設備啟動等功能。
  • 文件管理:完成文件的讀、寫、創建及刪除等功能
  • 進程控制:完成進程的創建、撤銷、阻塞、及喚醒的功能
  • 進程通信:完成進程之間的消息傳遞或信號的傳遞
  • 內存管理:完成內存的分配、回收以及獲取作業占用內存區大小及始址等功能。

顯然,系統調用運行在系統的核心態。通過系統調用的方式來使用系統功能,可以保證系統的穩定性和安全性,防止用戶隨意更改或訪問系統的數據或命令。系統調用命令式由操作系統提供的一個或多個子程序模塊來實現的。

下圖詳細闡述了,Linux系統中系統調用的過程:(int 0x80中斷向量是dos系統返回,int 3中斷向量是斷點指令——可以查中斷向量表)

 

庫是可重用的模塊,處於用戶態。
系統調用是操作系統提供的服務,處於內核態,不能直接調用,而要使用類似int 0x80的軟中斷陷入內核,所以庫函數中有很大部分是對系統調用的封裝。

既然如此,如何調用系統調用?

用戶是處於用戶態,具有的權限是非常有限,肯定是不能直接使用內核態的服務,只能間接通過有訪問權限的API函數內嵌的系統調用函數來調用。

介紹下系統調用的過程:
首先將API函數參數壓到上,然后將函數內調用系統調用的代碼放入寄存器通過陷入中斷進入內核將控制權交給操作系統操作系統獲得控制后將系統調用代碼拿出來跟操作系統一直維護的一張系統調用表做比較已找到該系統調用程序體的內存地址接着訪問該地址執行系統調用。執行完畢后,返回用戶程序

例子:

int main()
{
    int fd = create("filename",0666);
    exit(0);
}
在執行main函數時,是在user mode下執行,當遇到create函數時,繼續在user mode下執行,然后將filename和0666兩個參數壓入棧中寄存器,接着調用庫函數create,系統仍然處於user mode。這里的庫函數create實際上 調用了內核的系統調用create,執行到這里后,系統將create系統調用的unique number壓入寄存器,然后 執行指令trap使系統進入kernel mode(執行int $0x80產生中斷)。這時系統意識到要進行系統調用的invoke,於是從剛才的寄存器中取出create系統調用的unique number,從系統調用表中得知要invoke的系統調用是create,然后執行。執行完畢返回庫函數create的調用,庫函數負責檢查系統調用的執行情況(檢查某些寄存器的值),然后庫函數create根據檢查的結果返回響應的值。
 
這里 trap指令類似於一個系統中斷並且是軟中斷,而系統調用create類似於一個中斷處理函數所有的系統調用都與上邊的情況類似,靠 中斷機制切換到內核模式實現
系統調用通常比庫函數要慢,因為要把上下文環境切換到內核模式。

 補充一下系統調用和庫函數的區別:

系統調用:是操作系統為用戶態運行的進程和硬件設備(如CPU、磁盤、打印機等)進行交互提供的一組接口,即就是設置在應用程序和硬件設備之間的一個接口層。可以說是操作系統留給用戶程序的一個接口。再來說一下,linux內核是單內核,結構緊湊,執行速度快,各個模塊之間是直接調用的關系。放眼望整個linux系統,從上到下依次是用戶進程->linux內核->硬件。其中系統調用接口是位於Linux內核中的,如果再稍微細分一下的話,整個linux系統從上到下可以是:用戶進程->系統調用接口->linux內核子系統->硬件,也就是說Linux內核包括了系統調用接口和內核子系統兩部分;或者從下到上可以是:物理硬件->OS內核->OS服務->應用程序,其中操作系統起到“承上啟下”的關鍵作用,向下管理物理硬件,向上為操作系服務和應用程序提供接口,這里的接口就是系統調用了。
       一般地,操作系統為了考慮實現的難度和管理的方便,它只提供一少部分的系統調用,這些系統調用一般都是由C和匯編混合編寫實現的,其接口用C來定義,而具體的實現則是匯編,這樣的好處就是執行效率高,而且,極大的方便了上層調用。

庫函數:顧名思義是把函數放到庫里。是把一些常用到的函數編完放到一個文件里,供別人用。別人用的時候把它所在的文件名用#include<>加到里面就可以了。一般是放到lib文件里的。一般是指編譯器提供的可在c源程序中調用的函數。可分為兩類,一類是c語言標准規定的庫函數,一類是編譯器特定的庫函數。(由於版權原因,庫函數的源代碼一般是不可見的,但在頭文件中你可以看到它對外的接口)
      libc中就是一個C標准庫,里面存放一些基本函數,這些基本函數都是被標准化了的,而且這些函數通常都是用匯編直接實現的。
       庫函數一般可以概括的分為兩類,一類是隨着操作系統提供的,另一類是由第三方提供的。隨着系統提供的這些庫函數把系統調用進行封裝或者組合,可以實現更多的功能,這樣的庫函數能夠實現一些對內核來說比較復雜的操作。比如,read()函數根據參數,直接就能讀文件,而背后隱藏的比如文件在硬盤的哪個磁道,哪個扇區,加載到內存的哪個位置等等這些操作,程序員是不必關心的,這些操作里面自然也包含了系統調用。而對於第三方的庫,它其實和系統庫一樣,只是它直接利用系統調用的可能性要小一些,而是利用系統提供的API接口來實現功能(API的接口是開放的)。部分Libc庫中的函數的功能的實現還是借助了系統掉調用,比如printf的實現最終還是調用了write這樣的系統調用;而另一些則不會使用系統調用,比如strlen, strcat, memcpy等。

實時上,系統調用所提供給用戶的是直接而純粹的高級服務,如果想要更人性化,具有更符合特定情況的功能,那么就要我們用戶自己來定義,因此就衍生了庫函數,它把部分系統調用包裝起來,一方面把系統調用抽象了,一方面方便了用戶級的調用。系統調用和庫函數在執行的效果上很相似(當然庫函數會更符合需求),但是系統調用是運行於內核狀態;而庫函數由用戶調用,運行於用戶態。

系統調用是為了方便使用操作系統的接口,而庫函數則是為了人們編程的方便。

 


 10.線程和進程,線程可以共享進程里的哪些東西。 知道協程是什么嗎

進程,是並發執行的程序在執行過程中分配和管理資源的基本單位,每一個進程都有一個自己的地址空間,即進程空間或(虛空間)。進程空間的大小 只與處理機的位數有關,一個 16 位長處理機的進程空間大小為 216 ,而 32 位處理機的進程空間大小為 232 。進程至少有 5 種基本狀態,它們是:初始態,執行態,等待狀態,就緒狀態,終止狀態。

線程,在網絡或多用戶環境下,一個服務器通常需要接收大量且不確定數量用戶的並發請求,為每一個請求都創建一個進程顯然是行不通的,——無論是從系統資源開銷方面或是響應用戶請求的效率方面來看。因此,操作系統中線程的概念便被引進了。線程,是進程的一部分,一個沒有線程的進程可以被看作是單線程的。線程有時又被稱為輕權進程或輕量級進程,也是 CPU 調度的一個基本單位。

共享進程的地址空間,全局變量(數據和堆)。在一個進程中,各個線程共享堆區,而進程中的線程各自維持自己的棧。

Each thread has its own:

  • 棧區和棧指針(Stack area and stack pointer)
  • 寄存器(Registers)
  • 調度優先級Scheduling properties (such as policy or priority)
  • 信號(阻塞和懸掛)Signals (pending and blocked signals)
  • 普通變量Thread specific data ( automatic variables )
線程是指進程內的一個執行單元,也是進程內的可調度實體.
與進程的區別:
(1)地址空間:進程內的一個執行單元;進程至少有一個線程;它們共享進程的地址空間;而進程有自己獨立的地址空間;
(2)資源擁有:進程是資源分配和擁有的單位,同一個進程內的線程共享進程的資源
(3)線程是處理器調度的基本單位,但進程不是.
4)二者均可並發執行.

進程和線程都是由操作系統所體會的程序運行的基本單元,系統利用該基本單元實現系統對應用的並發性。進程和線程的區別在於:

簡而言之,一個程序至少有一個進程,一個進程至少有一個線程. 
線程的划分尺度小於進程,使得多線程程序的並發性高。 
另外,進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率。 
線程在執行過程中與進程還是有區別的。每個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。 
從邏輯角度來看,多線程的意義在於一個應用程序中,有多個執行部分可以同時執行。但操作系統並沒有將多個線程看做多個獨立的應用,來實現進程的調度和管理以及資源分配。這就是進程和線程的重要區別。

進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位. 
線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源. 
一個線程可以創建和撤銷另一個線程;同一個進程中的多個線程之間可以並發執行.

協程:

定義:協程其實可以認為是比線程更小的執行單元。為啥說他是一個執行單元,因為他自帶CPU上下文。

協程切換:協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。

     (我們在自己在進程里面完成邏輯流調度,碰着i\o我就用非阻塞式的。那么我們即可以利用到異步優勢,又可以避免反復系統調用,還有進程切換造成的開銷,分分鍾給你上幾千個    邏輯流不費力。這就是協程。) 

協程的調度完全由用戶控制,一個線程可以有多個協程,用戶創建了幾個線程,然后每個線程都是循環按照指定的任務清單順序完成不同的任務,當任務被堵塞的時候執行下一個任務,當恢復的時候再回來執行這個任務,任務之間的切換只需要保存每個任務的上下文內容,就像直接操作棧一樣的,這樣就完全沒有內核切換的開銷,可以不加鎖的訪問全局變量,所以上下文的切換非常快;另外協程還需要保證是非堵塞的且沒有相互依賴,協程基本上不能同步通訊,多采用一步的消息通訊,效率比較高。

多線程和多進程的優劣:

 

多線程還是多進程的爭執由來已久,這種爭執最常見到在B/S通訊中服務端並發技術的選型上,比如WEB服務器技術中,Apache是采用多進程的(perfork模式,每客戶連接對應一個進程,每進程中只存在唯一一個執行線程),Java的Web容器Tomcat、Websphere等都是多線程的(每客戶連接對應一個線程,所有線程都在一個進程中)。

 

 

 

多進程:fork

多線程:pthread_create

 


 11.mysql的數據庫引擎有哪些,他們的區別

ISAM

  ISAM是一個定義明確且歷經時間考驗的數據表格管理方法,它在設計之時就考慮到數據庫被查詢的次數要遠大於更新的次數。因此,ISAM執行讀取操作的速度很快,而且不占用大量的內存和存儲資源。ISAM的兩個主要不足之處在於,它不支持事務處理,也不能夠容錯:如果你的硬盤崩潰了,那么數據文件就無法恢復了。如果你正在把ISAM用在關鍵任務應用程序里,那就必須經常備份你所有的實時數據,通過其復制特性,MYSQL能夠支持這樣的備份應用程序。

MYISAM

  MYISAM是MYSQL的ISAM擴展格式和缺省的數據庫引擎。除了提供ISAM里所沒有的索引和字段管理的大量功能,MYISAM還使用一種表格鎖定的機制,來優化多個並發的讀寫操作。其代價是你需要經常運行OPTIMIZE TABLE命令,來恢復被更新機制所浪費的空間。MYISAM還有一些有用的擴展,例如用來修復數據庫文件的MYISAMCHK工具和用來恢復浪費空間的MYISAMPACK工具。

    MYISAM強調了快速讀取操作,這可能就是為什么MYSQL受到了WEB開發如此青睞的主要原因:在WEB開發中你所進行的大量數據操作都是讀取操作。所以,大多數虛擬主機提供商和INTERNET平台提供商只允許使用MYISAM格式。

     HEAP

  HEAP允許只駐留在內存里的臨時表格。駐留在內存使得HEAP比ISAM和MYISAM的速度都快,但是它所管理的數據是不穩定的,而且如果在關機之前沒有進行保存,那么所有的數據都會丟失。在數據行被刪除的時候,HEAP也不會浪費大量的空間,HEAP表格在你需要使用SELECT表達式來選擇和操控數據的時候非常有用。要記住,用完表格后要刪除表格。 

    INNODB和BERKLEYDB

  INNODB和BERKLEYDB(BDB)數據庫引擎都是造就MYSQL靈活性的技術的直接產品,這項技術就是MySql++ API。在使用MySql的時候,你所面對的每一個挑戰幾乎都源於ISAM和MYIASM數據庫引擎不支持事務處理也不支持外來鍵。盡管要比ISAM和MYISAM引擎慢很多,但是INNODB和BDB包括了對事務處理和外來鍵的支持,這兩點都是前兩個引擎所沒有的。如前所述,如果你的設計需要這些特性中的一者或者兩者,那你就要被迫使用后兩個引擎中的一個了。


 

12.makefile嗎,一個文件依賴庫a,庫a依賴庫b,寫makefile的時候,a要放在b的前面還是后面

  • Makefile概述:

什么是makefile?或許很多Winodws的程序員都不知道這個東西,因為那些Windows的IDE都為你做了這個工作,但我覺得要作一個好的和professional的程序員,makefile還是要懂。這就好像現在有這么多的HTML的編輯器,但如果你想成為一個專業人士,你還是要了解HTML的標識的含義。特別在Unix下的軟件編譯,你就不能不自己寫makefile了,會不會寫makefile,從一個側面說明了一個人是否具備完成大型工程的能力。

因為,makefile關系到了整個工程的編譯規則。一個工程中的源文件不計數,其按類型、功能、模塊分別放在若干個目錄中,makefile定義了一系列的規則來指定,哪些文件需要先編譯,哪些文件需要后編譯,哪些文件需要重新編譯,甚至於進行更復雜的功能操作,因為makefile就像一個Shell腳本一樣,其中也可以執行操作系統的命令。

makefile帶來的好處就是——“自動化編譯”,一旦寫好,只需要一個make命令,整個工程完全自動編譯,極大的提高了軟件開發的效率。make是一個命令工具,是一個解釋makefile中指令的命令工具,一般來說,大多數的IDE都有這個命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可見,makefile都成為了一種在工程方面的編譯方法。

現在講述如何寫makefile的文章比較少,這是我想寫這篇文章的原因。當然,不同產商的make各不相同,也有不同的語法,但其本質都是在“文件依賴性”上做文章,這里,我僅對GNU的make進行講述,我的環境是RedHat Linux 8.0,make的版本是3.80。必竟,這個make是應用最為廣泛的,也是用得最多的。而且其還是最遵循於IEEE 1003.2-1992 標准的(POSIX.2)。

在這篇文檔中,將以C/C++的源碼作為我們基礎,所以必然涉及一些關於C/C++的編譯的知識,相關於這方面的內容,還請各位查看相關的編譯器的文檔。這里所默認的編譯器是UNIX下的GCC和CC。

  • 編譯和連接:

編譯:

定義:一般來說,無論是C、C++、還是pas,首先要把源文件編譯成中間代碼文件,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即 Object File,這個動作叫做編譯(compile)。

描述:編譯時,編譯器需要的是語法的正確,函數與變量的聲明的正確。只要所有的語法正確,編譯器就可以編譯出中間目標文件。一般來說,每個源文件都應該對應於一個中間目標文件(O文件或是OBJ文件)。

連接:

定義:然后再把大量的Object File合成執行文件,這個動作叫作鏈接(link)。

描述:通常是你需要告訴編譯器頭文件的所在位置(頭文件中應該只是聲明,而定義應該放在C/C++文件中),鏈接時,主要是鏈接函數和全局變量,所以,我們可以使用這些中間目標文件(O文件或是OBJ文件)來鏈接我們的應用程序。鏈接器並不管函數所在的源文件,只管函數的中間目標文件(Object File),在大多數時候,由於源文件太多,編譯生成的中間目標文件太多,而在鏈接時需要明顯地指出中間目標文件名,這對於編譯很不方便,所以,我們要給中間目標文件打個包,在Windows下這種包叫“庫文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。

總結一下,源文件首先會生成中間目標文件,再由中間目標文件生成執行文件。在編譯時,編譯器只檢測程序語法,和函數、變量是否被聲明。如果函數未被聲明,編譯器會給出一個警告,但可以生成Object File。而在鏈接程序時,鏈接器會在所有的Object File中找尋函數的實現,如果找不到,那到就會報鏈接錯誤碼(Linker Error),在VC下,這種錯誤一般是:Link 2001錯誤,意思說是說,鏈接器未能找到函數的實現。你需要指定函數的Object File.

  •  Makefile

make命令執行時,需要一個 Makefile 文件,以告訴make命令需要怎么樣的去編譯和鏈接程序。

首先,我們用一個示例來說明Makefile的書寫規則。我們的規則是:
1)如果這個工程沒有編譯過,那么我們的所有C文件都要編譯並被鏈接。
2)如果這個工程的某幾個C文件被修改,那么我們只編譯被修改的C文件,並鏈接目標程序。
3)如果這個工程的頭文件被改變了,那么我們需要編譯引用了這幾個頭文件的C文件,並鏈接目標程序。

只要我們的Makefile寫得夠好,所有的這一切,我們只用一個make命令就可以完成,make命令會自動智能地根據當前的文件修改的情況來確定哪些文件需要重編譯,從而自己編譯所需要的文件和鏈接目標程序。

 

  Makefile的規則:

  target…:dependecies…

    command

target也就是一個目標文件,可以是Object File,也可以是執行文件。還可以是一個標簽(Label),對於標簽這種特性,在后續的“偽目標”章節中會有敘述。
dependicies就是,要生成那個target所需要的文件或是目標。
command也就是make需要執行的命令。(任意的Shell命令)
這是一個文件的依賴關系,也就是說,target這一個或多個的目標文件依賴於dependicies中的文件,其生成規則定義在command中。說白一點就是說,dependicies中如果有一個以上的文件比target文件要新的話,command所定義的命令就會被執行。這就是Makefile的規則。也就是Makefile中最核心的內容。(深入探討makefile

  注意事項:

  1.命令要以[Tab]為開始

  2.有clean

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM