1. 程式人生 > >常見C++面試題及基本知識點總結(一)

常見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。

在標準c++中,int的定義長度要依靠你的機器的字長,也就是說,如果你的機器是32位的,int的長度為32位,如果你的機器是64位的,那麼int的標準長度就是64位。

從上面的一段文字中,我們可以看出,首先根據結構體內部成員的自身對齊值得到結構體的自身對齊值(內部成員最大的長度),如果沒有修改系統設定的預設補齊長度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)初始化:未經初始化的全域性靜態變數會被程式自動初始化為0(自動物件的值是任意的,除非他被顯示初始化)

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

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

2.全域性變數

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

1)記憶體中的位置:靜態儲存區(靜態儲存區在整個程式執行期間都存在)

2)初始化:未經初始化的全域性靜態變數會被程式自動初始化為0(自動物件的值是任意的,除非他被顯示初始化)

3)作用域:全域性靜態變數在宣告他的檔案之外是不可見的。準確地講從定義之處開始到檔案結尾。

注:static修飾全域性變數,併為改變其儲存位置及生命週期,而是改變了其作用域,使當前檔案外的原始檔無法訪問該變數,好處如下:(1)不會被其他檔案所訪問,修改(2)其他檔案中可以使用相同名字的變數,不會發生衝突。對全域性函式也是有隱藏作用。

對類中的

    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引用;
(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的底層實現主要是由紅黑樹實現的。

紅黑樹:

性質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 要求接收方必須有累積確認的功能,這樣可以減小傳輸開銷
  • 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)