深信服C/C++面試題目總結


內容收集自網絡

C/C++語言

1. 鏈表和數組的相同點和不同點

特點 優點 缺點
數組 存儲區域連續
需要預先分配空間,可能造成空間浪費或者不利於擴展
插入/刪除效率低
隨機讀取效率高
隨機訪問性強
查找速度快
插入和刪除效率低
可能浪費內存
內存空間要求高,必須有足夠的連續內存空間
數組大小固定,不能動態拓展
鏈表 存儲區域不要求連續,每個數據都保存了下一個數據的地址(如果有)
數據的增加/刪除容易,易於擴展
數據查找效率低,只能順序查找

插入刪除速度快
內存利用率高,不會浪費內存
大小沒有固定,拓展很靈活
不能隨機查找,必須從第一個開始遍歷,查找效率低

性能

- 數組 鏈表
讀取 O(1) O(n)
插入 O(n) O(1)
刪除 O(n) O(1)

2. 靜態鏈表及數組的實現

概念
對於沒有指針的編程語言,可以用數組替代指針,來描述鏈表。讓數組的每個元素由data和cur兩部分組成,其中cur相當於鏈表的next指針,這種用數組描述的鏈表叫做靜態鏈表,這種描述方法叫做遊標實現法。
特點
這種儲存結構仍需要預先分配一個較大的空間,但在作線性表的插入和刪除操作是不需移動元素,僅需修改指針,故仍具有鏈式存儲結構的主要有點。
優點
在插入和刪除時候,只需要修改遊標,不需要移動元素,從而改進了順序存儲結構中插入和刪除操作需要移動大量元素的缺點。
缺點
沒有解決連續存儲分配帶來的表長難以確定的問題;失去了順序存儲結構隨機存取的特性。

3.使用strcpy注意的問題

來自MSDN的定義

char *strcpy(
   char *strDestination,
   const char *strSource
);

參數
strDestination:目標字符串。
strSource:null 終止的源字符串。
返回值
其中每個函數都會返回目標字符串。 沒有保留任何返回值以指示錯誤。
說明
src和dst所指內存區域不可以重疊且dst必須有足夠的空間來容納src的字符串。
注意

因爲strcpy不會檢查strDestination是否有足夠空間 ,它會直接複製strSource,很可能會造成緩衝區溢出。 因此,建議你使用 strcpy_s

strcpy_s的聲明

// C定義
errno_t strcpy_s(
   char *dest,
   rsize_t dest_size,
   const char *src
);
C++定義
errno_t strcpy_s(
   char (&dest)[size],
   const char *src
); 

爲了避免緩衝區溢出問題,我們也可以使用memcpy來完成的字符串的拷貝工作,memcpy是用來在內存中複製數據的,它會把指定長度的內存塊複製到另一塊內存中而不管內存的中存放的是什麼數據
memcpy的聲明

void *memcpy(
	void *str1, 
	const void *str2, 
	size_t n
);
// 作用:從存儲區 str2 複製 n 個字符到存儲區 str1

返回值
該函數返回一個指向目標存儲區 str1 的指針。

strcpy、memcpy、strncpy都會遇到內存重疊的問題
https://blog.csdn.net/magic_world_wow/article/details/79662257
https://blog.csdn.net/weibo1230123/article/details/80382614

4. 鏈表反轉

鏈表相關問題總是涉及大量指針的操作,鏈表的反轉關鍵就是調整指針的方向。以下圖爲例,(a)表示原鏈表,(b)表示在進行反轉的鏈表。

假設i節點之前都已反轉完畢,進行到i時,我們需要知道i的前一個節點h,i節點本身以及(原來)i節點的下一個節點j。相應的這裏涉及到3個指針pPre、pNode、pNext,於是寫下如下代碼

struct ListNode
{// 定義鏈表節點
    int       m_nValue;
    ListNode* m_pNext;
};

ListNode* ReverseList(ListNode* pHead)
{
    ListNode* pReversedHead = nullptr;	// 反轉後的頭指針
    ListNode* pNode = pHead;			// 從源鏈表的頭結點開始
    ListNode* pPrev = nullptr;
    while(pNode != nullptr)
    {
        ListNode* pNext = pNode->m_pNext;

        if(pNext == nullptr)
            pReversedHead = pNode;

        pNode->m_pNext = pPrev;

        pPrev = pNode;		// 跟新pPre
        pNode = pNext;		// 反轉節點後移
    }

    return pReversedHead;
}

5. 判斷含括號的表達式是否合法

這數與簡單的符號匹配問題,使用棧來解決,遇到左括號就入棧,右括號出棧。主要分爲四種情況:

  1. 左右括號數量相同且正確匹配;
  2. 左右括號數量相同但不能正確匹配;
  3. 左括號多餘;
  4. 右括號多餘。

每次出棧前將當前的右括號和棧頂元素比較,看是否匹配,這是正確匹配的第一個問題;如果還有右括號而棧已經空了,說明右括號多了,如果最後棧不空,說明左括號多了。C++代碼實現如下。

//判斷括號是否合法--C++
#include <iostream>
#include <string>
#include <stack>
using namespace std;

int Match(char ch1,char ch2)
{// 匹配函數
    int t = 0;
    if(ch1 == '(' && ch2 == ')')    t = 1;
    if(ch1 == '[' && ch2 == ']')    t = 1;
    if(ch1 == '{' && ch2 == '}')    t = 1;
    return t;
}

bool chkLegal(string A) 
{// 檢查函數
	if(A.size() == 0)   return false;
    stack<char> Stack;		// 定義一個棧容器
    for(int i=0;i<A.size();i++){
        if(A[i]=='['||A[i]=='('||A[i]=='{')
            Stack.push(A[i]);	// 左括號入棧
        if(A[i]==']'||A[i]==')'||A[i]=='}')
         {
            if(Stack.empty())   return false;               // 如果棧空還有右括號,不匹配
            if(!Match(Stack.top(), A[i]))  return  false;   // 如果沒有對應匹配
            Stack.pop();
         }
    }
    if(Stack.empty())   return true;                        // 如果數量正確匹配
    else    return false;
}

int main(int argc, char *argv[])
{
    string A = "[a+b*(5-4)]";                   // 測試用例A,期望數出1
    string B = "[a+b*{5-4)]";                   // 測試用例B,期望數出0
    string C = "{[())]}";                       // 測試用例C,期望輸出0
    string D = "{[a+b*(5-4)]";                  // 測試用例D,期望輸出0
    string E = "";								// 測試用例D,期望輸出0	
    cout << "String A is: " << chkLegal(A) << endl;
    cout << "String B is: " << chkLegal(B) << endl;
    cout << "String C is: " << chkLegal(C) << endl;
    cout << "String D is: " << chkLegal(D) << endl;
    cout << "String E is: " << chkLegal(E) << endl;
    return 0;
}

6. Map的底層實現?爲什麼使用紅黑樹

C++ STL底層實現

Name Description
vector 底層數據結構爲數組 ,支持快速隨機訪問
list 底層數據結構爲雙向鏈表,支持快速增刪
deque 底層數據結構爲一箇中央控制器和多個緩衝區,詳細見STL源碼剖析P146,支持首尾(中間不能)快速增刪,也支持隨機訪問
stack 底層一般用list或deque實現,封閉頭部即可,不用vector的原因應該是容量大小有限制,擴容耗時
queue 底層一般用list或deque實現,封閉頭部即可,不用vector的原因應該是容量大小有限制,擴容耗時
priority_queue 底層數據結構一般爲vector爲底層容器,堆heap爲處理規則來管理底層容器實現(優先隊列)
set 底層數據結構爲紅黑樹,有序,不重複
multiset 底層數據結構爲紅黑樹,有序,可重複
map 底層數據結構爲紅黑樹,有序,不重複
multimap 底層數據結構爲紅黑樹,有序,可重複
hash_set 底層數據結構爲hash表,無序,不重複
hash_multiset 底層數據結構爲hash表,無序,可重複
hash_map 底層數據結構爲hash表,無序,不重複
hash_multimap 底層數據結構爲hash表,無序,可重複

map是key:value鍵值對的組合,map類型通常被稱爲關聯數組(associative array)。與之相對,set就是關鍵字的簡單組合。
map的模板函數

template <class Key,
    class Type,
    class Traits = less<Key>,
    class Allocator=allocator<pair <const Key, Type>>>
class map;

參數

  • Key
    要存儲在映射中的鍵數據類型。
  • Type
    要存儲在映射中的元素數據類型。
  • Traits
    一種提供函數對象的類型,該函數對象可將兩個元素值作爲排序鍵進行比較,以確定其在映射中的相對順序。 此參數爲可選自變量,默認值是二元謂詞 less。
    在 C++ 14 中可以通過指定沒有類型參數的 std:: less <> 謂詞來啓用異類查找。
  • Allocator
    一種表示存儲的分配器對象的類型,該分配器對象封裝有關映射的內存分配和解除分配的詳細信息。 此參數爲可選參數,默認值爲 allocator<pair<const Key, Type> >。

用法參考

https://www.w3cschool.cn/cpp/cpp-fu8l2ppt.html
https://blog.csdn.net/qq_38984851/article/details/81237993
https://www.jianshu.com/p/834cc223bb57

紅黑樹的特性
(1)每個節點或者是黑色,或者是紅色。
(2)根節點是黑色。
(3)每個葉子節點(NIL)是黑色。 [注意:這裏葉子節點,是指爲空(NIL或NULL)的葉子節點!]
(4)如果一個節點是紅色的,則它的子節點必須是黑色的。
(5)從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。

注意
a. 特性(3)中的葉子節點,是隻爲空(NIL或null)的節點。
b. 特性(5),確保沒有一條路徑會比其他路徑長出倆倍。因而,紅黑樹是相對是接近平衡的二叉樹。
紅黑樹示意圖:
在這裏插入圖片描述

參考
https://www.cnblogs.com/xuxinstyle/p/9556998.html
https://www.jianshu.com/p/e136ec79235c

平衡二叉樹

  • 紅黑樹是在AVL樹的基礎上提出來的。
  • 平衡二叉樹又稱爲AVL樹,是一種特殊的二叉排序樹。其左右子樹都是平衡二叉樹,且左右子樹高度之差的絕對值不超過1。
  • AVL樹中所有結點爲根的樹的左右子樹高度之差的絕對值不超過1。
  • 將二叉樹上結點的左子樹深度減去右子樹深度的值稱爲平衡因子BF,那麼平衡二叉樹上的所有結點的平衡因子只可能是-1、0和1。只要二叉樹上有一個結點的平衡因子的絕對值大於1,則該二叉樹就是不平衡的。

紅黑樹較AVL樹的優點
AVL 樹是高度平衡的,頻繁的插入和刪除,會引起頻繁的rebalance,導致效率下降;紅黑樹不是高度平衡的,算是一種折中,插入最多兩次旋轉,刪除最多三次旋轉。

所以紅黑樹在查找,插入刪除的性能都是O(logn),且性能穩定,所以STL裏面很多結構包括map底層實現都是使用的紅黑樹。

7. 重載、重寫(覆蓋)和隱藏的定義與區別

定義

  • 重載: 在同一作用域中,同名函數的形式參數(參數個數、類型或者順序)不同時,構成函數重載
  • 重寫/覆蓋(override): 派生類中與基類同返回值類型、同名和同參數的虛函數重定義,構成虛函數覆蓋,也叫虛函數重寫。
  • 隱藏: 指不同作用域中定義的同名函數構成隱藏(不要求函數返回值和函數參數類型相同)。

override與final
使用override關鍵字來說明派生類中的虛函數。
把某個函數指點爲 final ,意味着任何嘗試覆蓋該函數的操作都將引發錯誤。
區別
重載和重寫的區別
(1)範圍區別:重寫和被重寫的函數在不同的類中,重載和被重載的函數在同一類中。
(2)參數區別:重寫與被重寫的函數參數列表一定相同,重載和被重載的函數參數列表一定不同。
(3)virtual的區別:重寫的基類必須要有virtual修飾,重載函數和被重載函數可以被virtual修飾,也可以沒有。

隱藏和重寫,重載的區別
(1)與重載範圍不同:隱藏函數和被隱藏函數在不同類中。
(2)參數的區別:隱藏函數和被隱藏函數參數列表可以相同,也可以不同,但函數名一定同;當參數不同時,無論基類中的函數是否被virtual修飾,基類函數都是被隱藏,而不是被重寫。

內容 作用域 virtual 函數名 形參列表 返回值類型
重載 相同 可有可無 相同 不同 可同可不同
隱藏 不同 可有可無 相同 可同可不同 可同可不同
重寫/覆蓋 不同 相同 不同 相同

參考
https://blog.csdn.net/weixin_39640298/article/details/88725073
https://www.cnblogs.com/zhangjxblog/p/8723291.html

8. virtual關鍵字是爲了實現什麼,具體怎麼實現?

當一個方法聲明包含virtual修飾符,這個方法就是虛方法。如果沒有virtual修飾符,那麼就不是虛方法。C++中的虛函數(Virtual Function)的作用主要是實現了多態的機制,虛函數是通過一張虛函數表(Virtual Table)來實現的。

9. Hash表及底層實現機制

在數組,線性表、樹等數據結構中,記錄的查找效率依賴與比較次數。如果能將關鍵字和儲存位置建立一個映射關係,那麼就可是實現不經過任何比較,一次便能得到所查記錄。即,如果存在映射關係ff能確定給定值KK的位置,那麼可以稱這個映射關係ff哈希(Hash)函數,由這個思想建立的表爲哈希表。

哈希函數構造方法

  • 直接定址法
  • 數字分析法
  • 平方取中法
  • 摺疊法
  • 除留取餘法
  • 隨機法

衝突處理方法

  • 開放定址法再哈希法
  • 鏈地址法
  • 建立一個公共溢出區

底層實現
數組+鏈表,用鏈表處理衝突。如果衝突元素較多,可將鏈表轉換爲紅黑樹來提高查找性能能

參考
https://blog.csdn.net/sinat_35866463/article/details/83316487
https://blog.csdn.net/ACmeinan/article/details/79595960
https://blog.csdn.net/qq_41891803/article/details/82787112
https://www.nowcoder.com/discuss/3098?pos=28&type=0&order=0
https://www.kanzhun.com/gsmsh10802673.html
https://www.nowcoder.com/discuss/116569?type=2

10. BSF(Breadth First Search)之中國象棋跳馬問題

題目描述

現在棋盤的大小不一定,由p,q給出,並且在棋盤中將出現障礙物(限制馬的行動,與象棋走法相同)

輸入

第一行輸入n表示有n組測試數據。
每組測試數據第一行輸入2個整數p,q,表示棋盤的大小(1<=p,q<=100)。
每組測試數據第二行輸入4個整數,表示馬的起點位置與終點位置。(位置的取值範圍同p,q)
第三行輸入m表示圖中有多少障礙。
接着跟着m行,表示障礙的座標。

輸出

馬從起點走到終點所需的最小步數。
如果馬走不到終點,則輸入“can not reach!”

C++代碼

//中國象棋中的跳馬問題
#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>
#include <queue>
#include <algorithm>
using namespace std;

struct position
{// 位置結構體
    int x;  // row
    int y;  // col
};
struct node
{// 節點屬性
    int x;      // 節點位置信息
    int y;
    int sum;    // 到達該節點的距離信息
};
int vis[109][109];  // 訪問數組
int barrier[109][109];  // 障礙數組
// dir[]和cir[]目標位置信息和障礙信息是一個相對值,即將輸入的起點作爲原點考慮
position dir[8]={{-2,-1},{-2,1},{-1,2},{1,2},{2,1},{2,-1},{1,-2},{-1,-2}}; // 如果以馬當前位置爲座標原點,則該數組表示可以到達的八個方向的點
position cir[8]={{-1,0},{-1,0},{0,1},{0,1},{1,0},{1,0},{0,-1},{0,-1}};     // 分別對應上述目標點的障礙 
queue<node> que;    // 輔助隊列
int ans,n,m,sx,sy,ex,ey,flag;   // ans:目標路徑長度
                                // m,n確定期盼
                                // (sx,sy)即起點座標
                                // (ex.ey)即要到達的終點座標
                                // flag:能否到達的標記

int is_right(int a,int b)
{// 判斷位置信息
    if(a>0&&a<=n&&b>0&&b<=m)
        return 1;
    return 0;
}
int BFS()
{//廣度優先搜索
    int row,col,tx,ty;
    ans=flag=0;     // 距離和標記初始化
    node t,m;       // 定義兩個節點,t爲起始節點,m爲遍歷輔助節點
    t.x=sx,t.y=sy,t.sum=ans;    // t爲起始節點
    que.push(t);    // 節點入隊
    vis[sx][sy]=1;  // 標記已經走過的點
    while(!que.empty())
    {
        t=que.front(); //隊首節點
        que.pop();      // 出隊
        row=t.x,col=t.y;// 當前隊首節點的座標信息
        if(row==ex && col==ey)
        {// 如果節點爲終點
            flag=1;
            ans=t.sum;
            return ans;
        }
        for(int i=0;i<8;i++)
        {
            tx=t.x+dir[i].x;    // 遍歷訪問節點座標信息
            ty=t.y+dir[i].y;
            if(!barrier[t.x+cir[i].x][t.y+cir[i].y])
            {//沒有障礙
                if(is_right(tx,ty) &&!vis[tx][ty] && !barrier[tx][ty])
                {// 節點合法,未被訪問過且節點無障礙
                    m.x=tx;         // 記錄訪問節點信息
                    m.y=ty;
                    m.sum=t.sum+1;  // 距離增加
                    que.push(m);    // 訪問節點入隊
                    vis[tx][ty]=1;  // 訪問標誌數組置1,表示以訪問
                }
            }
        }
    }
}
int main(void)
{
    int t,ob,obx,oby;   // t:測試組數
                        // ob障礙數
                        // (obx,oby)障礙座標
                        
    cin>>t; // 輸入測試數據組數t
    while(t--)
    {
        while(!que.empty()) // 如果隊列非空
            que.pop();
        memset(vis,0,sizeof(vis));  // 初始化訪問數組爲0,extern void *memset(void *buffer, int c, int count)
        memset(barrier,0,sizeof(barrier));  // 初始化障礙數組爲0
        scanf("%d%d%d%d%d%d%d",&n,&m,&sx,&sy,&ex,&ey,&ob);  // nm確定棋盤大小,1<=n,m<=100
                                                            // 起點座標(sx,sy),終點座標(ex,ey)
                                                            // 障礙數量ob
    
        for(int i=0;i<ob;i++)
        {
            scanf("%d%d",&obx,&oby);                        // 輸入障礙座標
            vis[obx][oby]=1;                                // 訪問標記置1
            barrier[obx][oby]=1;                            // 障礙標記置1
        }
        BFS();  // 調用算法,起點信息(sx,sy)已經輸入
        if(flag==1)
            printf("%d",ans);
        else
            printf("can not reach!");
        if(t)
            printf("\n");
    }
    return 0;
}

代碼的管關鍵是是BSF()實現部分,需要理解好訪問標誌數組vis[]和輔助隊列que的作用
Reference:https://blog.csdn.net/qq_41759198/article/details/81510147

11.斐波那契數列之青蛙跳臺階的問題

題目描述

一隻青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法

這是典型屬於求Fibonacci第n項的問題

方法一:遞歸

#include <iostream>
#include <cstdio>
using namespace std;

long long Fibonacci_Recursion(unsigned int n)
{
    if(n <= 0)
        return 0;

    if(n == 1)
        return 1;

    return Fibonacci_Recursion(n - 1) + Fibonacci_Recursion(n - 2);
}

int main()
{
    int n;
    while(cin>>n)
        cout << Fibonacci_Recursion(n) << endl;

    return 0;
}

雖然教科書上多以遞歸的方式講解,因爲很直觀,但在實際操作中遞歸會有大量重複的計算,導致算法時間效率很低

方法二:循環

#include <iostream>
#include <cstdio>
using namespace std;

long long Fibonacci_Loop(unsigned int n)
{
    int result[2] = {0, 1};     // 0級和1級
    if(n < 2)
        return result[n];

    long long  fibNMinusOne = 1;
    long long  fibNMinusTwo = 0;	// 青蛙問題此處設爲 1
    long long  fibN = 0;
    for(unsigned int i = 2; i <= n; ++ i)
    {
        fibN = fibNMinusOne + fibNMinusTwo;

        fibNMinusTwo = fibNMinusOne;
        fibNMinusOne = fibN;
    }

     return fibN;
}
int main()
{
    int n;
    while(cin>>n)
        cout << Fibonacci_Recursion(n) << endl;

    return 0;
}

以上兩種得出的結果是斐波那契數列:1,1,2,3,5,8…,但是注意,實際在處理青蛙跳的時候1到n階臺階應是 1,2,3,5,8…,所以處理是要具體考慮。

青蛙跳臺階的擴展

一隻青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。

數學推理

f(n)=f(n1)+f(n2)+f(n3)+...+f(n(n1))+f(nn)f(n) = f(n-1) + f(n-2) + f(n-3) + ... + f(n-(n-1)) + f(n-n)
    =f(0)+f(1)+f(2)+f(3)+...+f(n2)+f(n1)= f(0) + f(1) + f(2) + f(3) + ... + f(n-2)+f(n-1)

f(n1)=f(0)+f(1)+f(2)+f(3)+...+f((n1)1)f(n-1) = f(0) + f(1)+f(2)+f(3) + ... + f((n-1)-1)
     =f(0)+f(1)+f(2)+f(3)+...+f(n2)= f(0) + f(1) + f(2) + f(3) + ... + f(n-2)
So
f(n)=2f(n1) f(n)=2*f(n-1)

C++代碼

#include <iostream>
#include <cstdio>
using namespace std;

long long Jump(int n)
{
    if(n <= 0)
        return 0;

    else if(n == 1)
        return 1;
    else
        return 2*Jump(n-1);

}

int main()
{
    int n;
    while(cin>>n)
        cout << Jump(n) << endl;
    return 0;
}

12. 頂層const和底層const

指針本身是一個對象,它又可以指向另外一個對象。因此,指針本身是不是常量以及指針所指的是不是一個常量就是兩個相互獨立的問題。用名詞頂層const(top-level const)表示指針本身是常量,而用名詞底層 const(low-level const)。
更一般的,頂層 const可以表示任意的對象是常量,這一點對任何數據類型都適用,如算術類型、類、指針等。指針類型既可以是頂層 const也可以是底層 const。

int i = 0;
int *const p1 = &i;			// 不能改變p1的值,這是一個頂層 const
const int ci = 42;			// 不能改變ci的值,這是一個頂層 const
const int *p2 = &ci;		// 允許改變p2的值,這是一個底層 const
const int *const p3 = p2;	// 靠右的是頂層 const,靠左的是底層 const
const int &r = ci;			// 用於聲明引用的 const都是底層 const

當執行對象拷貝操作時,常量是頂層const還是底層const區別明顯。其中頂層const不受什麼影響:

i = ci;			// 正確:拷貝ci的值,ci是一個頂層const,對此操作無影響
p2 = p3;		// 正確:p2和p3指向的對象類型相同,p3頂層 const的部分不受影響

另一方面,底層const的限制卻不能忽視。當執行對象的拷貝操作時,拷入和拷出的對象必須具有相同的底層const資格,或者兩個對象的數據類型必須能夠相互轉換。一般來說,非常量可一轉換爲常量,反之則不行:

int *p = p3;		// 錯誤:p3包含底層 const的定義,而p沒有
p2 = p3;			// 正確:p2和p3都是底層 const
p2 = &i;			// 正確:int*能轉換成 const int*
int &r = ci;		// 錯誤:普通的int&不能綁定到int常量上
const int &r2 = i;	// 正確:const int& 可以綁定到int常量上

參考
《C++ Primer》
https://blog.csdn.net/qq_19528953/article/details/50922303

13. 可以用memcmp比較兩個struct嗎?會有什麼問題?

memcmp()函數是逐個字節進行比較的,而struct存在字節對齊,字節對齊時補的字節內容是隨機的,會產生垃圾值。所以在一下兩個前提條件下比較是沒有問題的

  1. 結構體成員都是同樣字節長度的數據類型,即長度一致,不會由字節對齊而產生垃圾值;
  2. 如果結構體在賦值前調用memset進行了清零初始化操作,那麼字節對齊是填充的內容均是0。

例1 直接使用memcmp()比較

#include <iostream>
#include <string.h>
using namespace std;

struct foo
{
   short a;
   int   b;
};

int main()
{
    foo c,d;
    c.a = 1;c.b = 2;
    d.a = 1;d.a = 2;

    if (0 == memcmp(&c,&d,sizeof(c)))
    {
        cout<<"equal"<<endl;
    }
    else
        cout<<"No-equal"<<endl;

    return 0;
}

輸出結果

No-equal

Press any key to continue.

例2 使用memset()初始化

#include <iostream>
#include <string.h>
using namespace std;

struct foo
{
   short a;
   int   b;
};

int main()
{
    foo c,d;
    memset(&c,0,sizeof(c));
    memset(&d,0,sizeof(d));
   // c.a = 1;c.b = 2;
   // d.a = 1;d.a = 2;

    if (0 == memcmp(&c,&d,sizeof(c)))
    {
        cout<<"equal"<<endl;
    }
    else
        cout<<"No-equal"<<endl;

    return 0;
}

輸出結果

equal

Press any key to continue.

因此,更好的建議方法是重載操作符==

#include <iostream>
using namespace std;

struct foo {
    short a;
    int b;
    bool operator==(const foo& rhs) // 操作運算符重載
    {
        return( a == rhs.a) && (b == rhs.b);

    }
};

int main(int argc,char* argv[])
 {
    foo a,b;
    a.a = 1;a.b = 2;
    b.a = 1;b.b = 2;

    if (a == b)
    {
        cout<<"equal"<<endl;
    }
    else
        cout<<"No-equal"<<endl;

    return 0;
}

輸出結果

equal

Press any key to continue.

14. 堆、棧的區別

堆(Heap)與棧(Stack)是開發人員必須面對的兩個概念,在理解這兩個概念時,需要放到具體的場景下,因爲不同場景下,堆與棧代表不同的含義。一般情況下,有兩層含義:
(1)程序內存佈局場景下,堆與棧表示的是兩種內存管理方式;

  • 分配方式:堆是動態分配;棧是靜態或動態分配
  • 管理方式:堆由程序員來釋放;棧有OS自動分配釋放
  • 存儲內容:堆存放內容由程序員填充;棧存放函數返回地址、相關參數、局部變量和寄存器等
  • 空間大小:每個進程擁有的棧的大小要遠遠小於堆的大小。
  • 分配效率:堆由C/C++庫函數支持,實現複雜;棧由操作系統自動分配,會在硬件層級對棧提供支持,效率高
  • 生長方向:堆的生長方向向上,內存地址由低到高;棧的生長方向向下,內存地址由高到低。

(2)數據結構場景下,堆與棧表示兩種常用的數據結構。

  • 操作方式

參考
https://blog.csdn.net/K346K346/article/details/80849966

15. 堆排序和快排的區別?

快速排序(Quick Sort)是對冒泡排序的一種改進,應用了分治的思想。通過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字比另一部分記錄的關鍵字小,則可分別對這兩部分記錄繼續進行排序(遞歸),以達到整個序列有序。
C++代碼

#include <cstdio>
#include<cstdlib>

void quickSort(int* arr,int low,int high);
int Partition(int* arr,int low,int high);

int main(int argc,char* argv[])
{
    int i,a[]={4,2,6,9,1,3,5};
    int length = sizeof(a)/sizeof(a[0]);
    quickSort(a,0,length-1);
    printf("After sorted:  ");
    for(i=0;i<length;i++)
        printf("%d ",a[i]);
}

void quickSort(int* arr,int low,int high)
{
    if(low<high)
    {
        int pivotloc = Partition(arr,low,high);
        quickSort(arr,low,pivotloc-1);
        quickSort(arr,pivotloc+1,high);
    }
}

int Partition(int* arr,int low,int high)
{
    int pivotkey = arr[low];
    while(low<high)
    {
        while(low<high && arr[high] >= pivotkey)
            --high;
        arr[low] = arr[high];
        while(low<high && arr[low] <= pivotkey)
            ++low;
        arr[high] = arr[low];
    }
    arr[low] = pivotkey;
    return low;
}

堆排序(Heap Sort)是指利用堆積樹(堆)這種數據結構所設計的一種排序算法,它是選擇排序的一種。可以利用數組的特點快速定位指定索引的元素。堆分爲大根堆和小根堆,是完全二叉樹。

簡單選擇排序的基本思想:每一趟都是在(n-i+1),(i=1,2,3…n-1)個記錄中選取關鍵字最小的記錄作爲有序序列的第i個記錄。總的時間複雜度爲O(n2)O(n^2)

#include <stdio.h>
#include <stdlib.h>

void SimpleSelectSort(int *a,int n)
{
    int i,j,mins,temp;
    if(n <= 0)  return -1;
    for(i = 0; i < n; i++)
    {
        mins = i;
        for(j = i+1; j < n; j++)	// 從後面n-i+1箇中找到最小的元素的下標
            if(a[j] < a[mins])
                mins = j;

        if(i != mins)
        {// 將最小元素進行交換
            temp = a[i];
            a[i] = a[mins];
            a[mins] = temp;
        }
    }
}

int main(int argc,char* argv[])
{
    int i,n = 8;
    int a[] = {3,6,4,2,5,1,8,7};        // 測試代碼

    printf("Before Sorted order:\t");
    for(i = 0; i < n;i++)
        printf("%d ",a[i]);

    SimpleSelectSort(a, n);

    printf("\nAfter Sorted order:\t");
    for(i = 0; i < n; i++)
        printf("%d ",a[i]);
    printf("\n");
    return 0;
}

的定義:n個元素的序列{k1,k2,,kn}\lbrace k_1,k_2,\dots,k_n \rbrace當且僅當滿足如下關係時,稱之爲堆。
{kik2ikik2i+1(){kik2ikik2i+1(),(i=1,2,,n2) \begin{cases} k_i \leq k_{2i} \\ k_i \leq k_{2i+1} \end{cases} (小根堆) 或 \begin{cases} k_i \geq k_{2i} \\ k_i \geq k_{2i+1} \end{cases} (大根堆) ,(i=1,2,\dots,\lfloor\frac{n}{2} \rfloor)
若將此序列所存儲的向量R[1..n]R[1..n]看做是一棵完全二叉樹的存儲結構,則堆實質上是滿足如下性質的完全二叉樹:樹中任一非葉子結點的關鍵字均不大於(或不小於)其左右孩子(若存在)結點的關鍵字。(注意不要和二叉排序樹混淆)
對於一個小頂堆,若在輸出堆頂的最小值之後, 使剩餘n-1個元素的序列再次篩選形成一個堆,再輸出次小值,由此反覆進行,便能得到一個有序的序列,這個過程就稱之爲堆排序。

從上面對於堆排序的敘述我們知道,進行一次堆排序,我們要解決兩個問題:

  1. 如何初始化一個堆
  2. 如何在輸出堆頂元素之後,調整堆內元素,使其再次形成一個堆。

下面給出一個c的參考代碼

#include<stdio.h>
 
int c=0;	// 用於記錄排序進行的交換次數
 
/*heapadjust()函數的功能是實現從a[m]到a[n]的數據進行調整,使其滿足大頂堆的特性*/
/*a[]是待處理的數組,m是起始座標, n是終止座標*/
void heapadjust(int a[], int m, int n)
{// 調整是從上往下調整
    int i, temp;
    temp=a[m];		// 想象一下從根節點開始調整,即m=1;
 
    for(i=2*m;i<=n;i*=2)//從m開始,比較它的左孩子和右孩子
    {
        /*如果左孩子小於右孩子,則將i++,這樣i(如果右孩子大,由於前面進行了i++的操作,
        實際是 i+1,也就是右孩子的索引,反之不進行 i++ 操作, 也就是左孩子的索引)的值
        就是最大孩子的下標值*/        
        if(i+1<=n && a[i]<a[i+1])	i++;
        /*如果最大的孩子小於temp,則不做任何操作,退出循環;否則交換a[m]和
        a[i]的值,將最大值放到a[i]處*/
        if(a[i]<temp)	break;
        a[m]=a[i];
        m=i;
    }
    a[m]=temp;
}
 
void crtheap(int a[], int n)//初始化create一個大頂堆
{
    int i;
    for(i=n/2; i>0; i--)//n/2爲最後一個雙親節點,依次向前建立大頂堆
    {
        heapadjust(a, i, n);		// 調整
    }
}
 
/*swap()函數的作用是將a[i]和a[j]互換*/
void swap(int a[], int i, int j)
{
    int temp;
    temp=a[i];
    a[i]=a[j];
    a[j]=temp;
    c++;
}
 
void heapsort(int a[], int n)
{
    int i;
 
    crtheap(a, n);
    for(i=n; i>1; i--)
    {
        swap(a, 1, i);//將第一個數,也就是從a[1]到a[i]中的最大的數,放到a[i]的位置
        heapadjust(a, 1, i-1);//對剩下的a[1]到a[i-1],再次進行堆排序,選出最大的值,放到a[1]的位置
    }
}
 
int main(void)
{
    int i;
    int a[10]={-1,5,2,6,0,3,9,1,7,4};
    printf("排序前:");
    for(i=1;i<10;i++)
    {
        printf("%d ",a[i]);
    }
    heapsort(a, 9);
    printf("\n\n共交換數據%d次\n\n", c);
    printf("排序後:");
    for(i=1;i<10;i++)
        printf("%d ",a[i]);
    printf("\n");
    return 0;
}

堆排序的時間複雜度爲O(nlogn)O(n\log{n})

參考
https://blog.csdn.net/kuweicai/article/details/54710409
https://blog.csdn.net/yuzhihui_no1/article/details/44258297#

16. 可變參數函數的定義及實現

  • 1)首先在函數裏定義一個va_list型的變量,這裏是arg_ptr,這個變量是指向參數的指針.
  • 2)然後用va_start宏初始化變量arg_ptr,這個宏的第二個參數是第一個可變參數的前一個參數,是一個固定的參數.
  • 3)然後用va_arg返回可變的參數,並賦值給整數j. va_arg的第二個參數是你要返回的參數的類型,這裏是int型.
  • 4)最後用va_end宏結束可變參數的獲取.然後你就可以在函數裏使用第二個參數了.如果函數有多個可變參數的,依次調用va_arg獲取各個參數.

printf 函數的實現
C中的定義:

int printf(
   const char *format [,
   argument]...
);

stdio.h中的聲明:

_Check_return_opt_
_CRT_STDIO_INLINE int __CRTDECL printf(
    _In_z_ _Printf_format_string_ char const* const _Format,
    ...)
#if defined _NO_CRT_STDIO_INLINE
;
#else
{
    int _Result;
    va_list _ArgList;
    __crt_va_start(_ArgList, _Format);
    _Result = _vfprintf_l(stdout, _Format, NULL, _ArgList);
    __crt_va_end(_ArgList);
    return _Result;
}
#endif

通俗一點

int printf(char *fmt, ...)
{
     int n;
     va_list args;
     va_start(args, fmt);
     n = vsprintf(sprint_buf, fmt, args);
     va_end(args);
     return n;
//va_start(arg,format),初始化參數指針arg,將函數參數format右邊第一個參數地址賦值給arg
//format必須是一個參數的指針,所以,此種類型函數至少要有一個普通的參數, 
//從而提供給va_start ,這樣va_start才能找到可變參數在棧上的位置。 
//va_arg(arg,char),獲得arg指向參數的值,同時使arg指向下一個參數,char用來指名當前參數型
//va_end 在有些實現中可能會把arg改成無效值,這裏,是把arg指針指向了 NULL,避免出現野指針 
}

參考
https://blog.csdn.net/weixin_34341229/article/details/92583270
https://blog.csdn.net/c1204611687/article/details/86133774
https://blog.csdn.net/huaweitman/article/details/38348655
https://wenku.baidu.com/view/c555861ea8114431b90dd85b.html

17. 緩衝出溢出、內存泄漏、內存溢出

顧名思義,緩衝區溢出的含義是爲緩衝區提供了多於其存儲容量的數據,就像往杯子裏倒入了過量的水一樣。
由於棧是低地址方向增長的,因此局部數組buffer的指針在緩衝區的下方。當把data的數據拷貝到buffer內時,超過緩衝區區域的高地址部分數據會“淹沒”原本的其他棧幀數據,根據淹沒數據的內容不同,可能會有產生以下情況:

  1. 淹沒了其他的局部變量。如果被淹沒的局部變量是條件變量,那麼可能會改變函數原本的執行流程。這種方式可以用於破解簡單的軟件驗證。

  2. 淹沒了ebp的值。修改了函數執行結束後要恢復的棧指針,將會導致棧幀失去平衡。

  3. 淹沒了返回地址。這是棧溢出原理的核心所在,通過淹沒的方式修改函數的返回地址,使程序代碼執行“意外”的流程!

  4. 淹沒參數變量。修改函數的參數變量也可能改變當前函數的執行結果和流程。

  5. 淹沒上級函數的棧幀,情況與上述4點類似,只不過影響的是上級函數的執行。當然這裏的前提是保證函數能正常返回,即函數地址不能被隨意修改(這可能很麻煩!)。

如果在data本身的數據內就保存了一系列的指令的二進制代碼,一旦棧溢出修改了函數的返回地址,並將該地址指向這段二進制代碼的其實位置,那麼就完成了基本的溢出攻擊行爲。

參考
https://www.cnblogs.com/kexianting/p/8805591.html
https://blog.csdn.net/weixin_37749370/article/details/81662401
https://www.cnblogs.com/clingyu/p/8546626.html

內存泄露:程序在向系統申請分配內存空間後(new),在使用完畢後未釋放。結果導致一直佔據該內存單元,我們和程序都無法再使用該內存單元,直到程序結束,這是內存泄露。
內存溢出:程序向系統申請的內存空間超出了系統能給的。比如內存只能分配一個int類型,我卻要塞給他一個long類型,系統就出現oom。

參考
https://blog.csdn.net/zkl99999/article/details/45486035
https://www.jianshu.com/p/86643b5afa6a

操作系統

1. 同步、死鎖

同步機制規則 產生死鎖的必要條件 處理死鎖地方法
空閒讓進 互斥條件 預防死鎖
忙則等待 請求和保持條件 避免死鎖
有限等待 不可搶佔條件 檢測死鎖
讓權等待 循環等待條件 接觸死鎖

2. 進程間常用的通信方式有哪些?

進程間通信(IPC,InterProcess Communication)是指在不同進程之間傳播或交換信息。

IPC的方式通常有管道(包括無名管道和命名管道)、消息隊列、信號量、共享存儲、Socket、Streams等。其中 Socket和Streams支持不同主機上的兩個進程IPC。

  • 管道:通常指無名管道,是 UNIX 系統IPC最古老的形式,使用pipe文件。
  • FIFO:也稱爲命名管道,它是一種文件類型。
  • 消息隊列:是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標識。
  • 信號量(semaphore):與已經介紹過的 IPC 結構不同,它是一個計數器。信號量用於實現進程間的互斥與同步,而不是用於存儲進程間通信數據。
  • 共享內存(Shared Memory):指兩個或多個進程共享一個給定的存儲區。
  • 套接字:包含基於文件型和基於網絡型。其優勢在於,它不僅適用於同一臺計算機內部的進程通信,也適用與網絡環境中不同計算機間的進程通信。

參考
https://blog.csdn.net/wangdd_199326/article/details/81321562
https://blog.csdn.net/wh_sjc/article/details/70283843

3. epoll和select/poll的區別

均爲I/O複用的處理方法。

參考《Unix網絡編程》
https://www.cnblogs.com/aspirant/p/9166944.html
https://blog.csdn.net/a1414345/article/details/73385556

4. 進程與線程的區別

進程的典型定義
(1) 進程是程序的一次執行。
(2) 進程是一個程序及其數據在處理及上順序執行時所發生的活動。
(3) 進程是具有獨立功能額程序在一個數據集合上運行的過程,踏實系統進行資源分配和調度的一個獨立單位。(在引入線程的OS中,把線程作爲獨立調度和分派的單位)

根本區別:進程是操作系統資源分配的基本單位,而線程是任務調度和執行的基本單位

在開銷方面:每個進程都有獨立的代碼和數據空間(程序上下文),程序之間的切換會有較大的開銷;線程可以看做輕量級的進程,同一類線程共享代碼和數據空間,每個線程都有自己獨立的運行棧和程序計數器(PC),線程之間切換的開銷小。

所處環境:在操作系統中能同時運行多個進程(程序);而在同一個進程(程序)中有多個線程同時執行(通過CPU調度,在每個時間片中只有一個線程執行)

內存分配方面:系統在運行的時候會爲每個進程分配不同的內存空間;而對線程而言,除了CPU外,系統不會爲線程分配內存(線程所使用的資源來自其所屬進程的資源),線程組之間只能共享資源。

包含關係:沒有線程的進程可以看做是單線程的,如果一個進程內有多個線程,則執行過程不是一條線的,而是多條線(線程)共同完成的;線程是進程的一部分,所以線程也被稱爲輕權進程或者輕量級進程。

參考
《計算機操作系統》
https://blog.csdn.net/kuangsonghan/article/details/80674777
https://blog.csdn.net/wsq119/article/details/82154305
線程,進程。多進程,多線程。併發,並行的區別
多進程和多線程的區別及適用場景
併發處理請求—多進程、多線程、異步

5. 系統調用與函數調用

系統調用:操作系統爲用戶提供了一系列接口,這些接口提供了對硬件設備的操作。舉個例子我們用printf想終端打印hello world,程序中調用printf,而printf實際上調用的是write,從而打印信息到終端。

庫函數:庫函數是對系統調用的封裝。系統調用作爲內核提供給用戶的接口,它執行的效率是比較高效和精簡的,但有時候我們需要對獲取的信息進行一些處理,我們把這些處理過程封裝起來提供給程序員,有利於編碼。

庫函數有可能包含一個系統調用,有可能包含幾個系統調用,也有可能不包含系統調用,一些簡單的操作就涉及到內核的功能。

參考
https://blog.csdn.net/qq_31759205/article/details/80602357
https://blog.csdn.net/qq_41727218/article/details/88218308

6. Linux信號量

信號的名字和編號
每個信號都有一個名字和編號,這些名字都以“SIG”開頭,例如“SIGIO ”、“SIGCHLD”等等。
信號定義在signal.h頭文件中,信號名都定義爲正整數。
具體的信號名稱可以使用kill -l來查看信號的名字以及序號,信號是從1開始編號的,不存在0號信號。kill對於信號0又特殊的應用。

常用的:
SIGKILL 9 TermKILL信號
SIGTERM 15 Term強制中止信號
信號的處理:
信號的處理有三種方法,分別是:忽略、捕捉和默認動作

參考
https://www.jianshu.com/p/f445bfeea40a

7.可重入函數,線程安全

線程安全:一個函數被稱爲線程安全的(thread-safe),當且僅當被多個併發進程反覆調用時,它會一直產生正確的結果。如果一個函數不是線程安全的,我們就說它是線程不安全的(thread-unsafe)。我們定義四類(有相交的)線程不安全函數。
可重入函數:可重入函數是線程安全函數的一種,其特點在於它們被多個線程調用時,不會引用任何共享數據。

參考
https://blog.csdn.net/ypt523/article/details/80380272
https://www.cnblogs.com/xiangshancuizhu/archive/2012/10/22/2734497.html

8. 阻塞與非阻塞

阻塞是指在函數執行時如果條件不滿足,程序將永遠停在那條函數那裏不在往下執行,而非阻塞則是函數不管條件是否滿足都會往下執行.

9. 內存管理

CPU三類總線

  • 地址總線的寬度決定了CPU的尋址能力
  • 數據總線的寬度決定了CPU與其他器件進行數據傳送時的一次數據傳送量
  • 控制總線的寬度決定了CPU對系統中其他器件的控制能力

將各類存儲器看作一個邏輯存儲器
在這裏插入圖片描述
對通用計算機而言,存儲層次至少應該具有三個層級:最高層爲CPU寄存器,中間爲主存,最底層是輔存。在較高檔的計算機中們還可以根據具體的功能細分爲寄存器、高速緩存、主存儲器、磁盤緩存、固定磁盤、可移動存儲介質等6層。
在這裏插入圖片描述
內存分配策略包括連續分配、分頁、分段、段頁式。

網絡知識

1. TCP有限狀態機

握手
在這裏插入圖片描述
揮手
在這裏插入圖片描述
有限狀態機
在這裏插入圖片描述
爲什麼需要三次握手(四次揮手後爲什麼還要等待2MSL)
爲什麼A還要發送一次確認呢?這主要是爲了防止已失效的連接請求報文段突然又傳送到了B,因而產生錯誤。

所謂已失效的連接請求報文段是這樣產生的。A發送連接請求,但因連接請求報文丟失而未收到確認,於是A重發一次連接請求,成功後建立了連接。數據傳輸完畢後就釋放了連接。現在假定A發出的第一個請求報文段並未丟失,而是在某個網絡節點長時間滯留了,以致延誤到連接釋放以後的某個時間纔到達B。本來這是一個早已失效的報文段。但B收到此失效的連接請求報文段後,就誤以爲A又發了一次新的連接請求,於是向A發出確認報文段,同意建立連接。假如不採用三次握手,那麼只要B發出確認,新的連接就建立了。

由於A並未發出建立連接的請求,因此不會理睬B的確認,也不會向B發送數據。但B卻以爲新的運輸連接已經建立了,並一直等待A發來數據,因此白白浪費了許多資源。

採用TCP三次握手的方法可以防止上述現象發生。例如在剛纔的情況下,由於A不會向B的確認發出確認,連接就不會建立。

參考
《計算機網絡》
https://blog.csdn.net/xy010902100449/article/details/48274635

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