原作者:Linux教程,原文地址:C/C++ 高頻八股文面試題1000題(一)
在準備技術崗位的求職過程中,C/C++始終是繞不開的核心考察點。無論是互聯網大廠的筆試面試,還是嵌入式、后臺開發、系統編程等方向的崗位,C/C++ 都扮演著舉足輕重的角色。
本系列文章將圍繞 “C/C++ 筆試面試中出現頻率最高的 1000 道題目” 進行深入剖析與講解。由于篇幅限制,整個系列將分為 多 篇陸續發布,每篇50道題,本文為 第一篇。
通過系統梳理這些高頻考點,幫助你在面對各大廠筆試、技術面試時真正做到 心中有數、下筆有神、對答如流!
建議學習方式:
- 結合實際項目或刷題平臺反復鞏固
- 對照每道題目的解析查漏補缺
- 動手編寫示例代碼加深理解
本系列將持續更新,歡迎關注+收藏,一起攻克這 1000 道 C/C++ 高頻題,拿下心儀 Offer!
面試題1:變量的聲明和定義有什么區別?
? 簡要回答:
- 定義(Definition):為變量分配存儲空間和地址,是創建變量的實際過程。一個變量只能被定義一次。
- 聲明(Declaration):用于告訴編譯器該變量已經存在,不會分配存儲空間。變量可以多次聲明。
關鍵區別:
類型 | 是否分配內存 | 是否可重復 |
定義 | 是 | 否 |
聲明 | 否 | 是 |
extern int a; // 聲明,不分配內存空間
int a; // 定義,分配內存空間
int a = 10; // 定義并初始化
面試題2:如何用if語句正確判斷bool、int、float和指針變量是否為零值?
bool型數據: if(flag) { A; } else { B;}
int型數據: if(0==flag) { A; } else { B; }
指針變量: if(NULL==flag) { A; } else { B; }
float型數據: #define NORM (0.000001) if((flag>=-NORM) && (flag<=NORM)) { A; } else { B; }
面試題3:sizeof 和 strlen 的主要區別是什么?
- sizeof 是一個操作符,而 strlen 是 C 標準庫中定義的一個函數。
- sizeof 的操作對象既可以是數據類型,也可以是變量;而 strlen 只能接受以 \0 結尾的字符串作為輸入參數。
- sizeof 在編譯階段就會被計算出結果,而 strlen 必須等到程序運行時才能得出結果。此外,sizeof 計算的是數據類型或變量所占內存的大小,而 strlen 測量的是字符串實際字符的數量(不包括結尾的 \0)。
- 當數組作為 sizeof 的參數時,它表示整個數組的大小,不會退化為指針;而當數組傳遞給 strlen 時,則會退化為指向數組首地址的指針。
注意: 有些操作符(如 sizeof)看起來像函數,而有些函數名又類似操作符,這類名稱容易引起混淆,使用時需要特別注意區分。尤其是在處理數組名等特殊類型時,這種差異可能導致意料之外的錯誤。
面試題4:C 語言中的 static 和 C++ 中的 static 有什么區別?
在 C 語言中,static 主要用于修飾:
- 局部靜態變量:延長局部變量的生命周期,使其在程序運行期間一直存在。
- 外部靜態變量和函數:限制變量或函數的作用域為當前文件,實現封裝和信息隱藏。
而在 C++ 中,除了具備 C 語言中所有的功能之外,static 還被擴展用于:
- 定義類中的靜態成員變量和靜態成員函數。這些靜態成員屬于整個類,而不是類的某個對象,可以在多個對象之間共享。
注意: 在編程中,static 所具有的“記憶性”和“全局性”特點,使得不同調用之間可以共享數據、傳遞信息。在 C++ 中,類的靜態成員則能夠在不同的對象實例之間進行通信和數據共享。
面試題5:C 中的 malloc 和 C++ 中的 new 有什么區別?
- new 和 delete 是 C++ 中的操作符,支持重載,只能在 C++ 程序中使用;而 malloc 和 free 是標準庫函數,可以在 C 和 C++ 中通用,并且可以被覆蓋(如自定義內存管理)。
- new 在分配內存的同時會自動調用對象的構造函數,完成初始化;相應的,delete 在釋放內存時會調用對象的析構函數。而 malloc 只負責分配內存空間,free 僅用于釋放內存,它們都不會觸發構造函數或析構函數的執行。
- new 返回的是具體類型的指針,具有類型安全性;而 malloc 返回的是 void* 類型,需要顯式地進行類型轉換才能使用。
注意: 使用 malloc 分配的內存必須通過 free 來釋放,而使用 new 分配的內存則必須使用 delete 來釋放,兩者不可混用。因為它們底層實現機制不同,混用可能導致未定義行為或內存泄漏。
面試題6:寫一個“標準”宏 MIN
#define MIN(a,b) ((a)<=(b)?(a):(b))
注意:在調用時一定要注意這個宏定義的副作用。
面試題7:指針可以被聲明為 volatile 嗎?
可以,指針可以被聲明為 volatile 類型。因為從本質上講,指針本質上也是一種變量,它保存的是一個內存地址(即整型數值),這一點與普通變量并沒有本質區別。
在某些特殊場景下,比如硬件寄存器訪問、中斷服務程序中共享的數據等,指針的值可能會被程序之外的因素修改,此時就需要使用 volatile 來告訴編譯器不要對該指針進行優化,確保每次訪問都直接從內存中讀取最新值。
例如,在中斷服務程序中修改一個指向緩沖區(buffer)的指針時,就必須將該指針聲明為 volatile,以防止編譯器因優化而忽略其變化。
說明: 雖然指針具有特殊的用途——用于訪問內存地址,但從變量訪問的角度來看,它和其他變量一樣具備可變性,因此也可以像普通變量一樣使用 volatile 進行修飾。
面試題8:a 和 &a 有什么區別?請寫出以下 C 程序的運行結果
#include <stdio.h>int main(void)
{int a[5] = {1, 2, 3, 4, 5};int *ptr = (int *)(&a + 1);printf("%d, %d", *(a + 1), *(ptr - 1));return 0;
}
數組名a和&a的區別
表達式 | 類型 | 含義 |
a | int* | 指向數組首元素 a[0] 的指針 |
&a | int (*)[5] | 指向整個數組 a 的指針 |
雖然它們的地址值相同,但類型不同,因此在進行指針運算時步長也不同:
- a + 1 是以 sizeof(int) 為單位移動(即跳過一個 int)。
- &a + 1 是以 sizeof(int[5]) 為單位移動(即跳過整個數組)。
輸出結果:2, 5
面試題9:簡述 C/C++ 程序編譯時的內存分配情況
從內存分配機制角度,C/C++ 的內存管理還可以分為以下三種方式:
分配方式 | 特點描述 |
靜態分配 | 在程序編譯階段完成,如全局變量、靜態變量。在整個程序運行過程中有效,速度最快,不易出錯。 |
棧上分配 | 函數調用時在棧中為局部變量分配空間,函數返回后自動釋放。速度快但容量有限。 |
堆上分配 | 使用 malloc / new 動態申請內存,程序員負責手動釋放。靈活性強但容易出錯,如內存泄漏、碎片等問題。 |
面試題10:簡述strcpy、sprintf與memcpy的區別。
1、操作對象不同
函數 | 源對象類型 | 目標對象類型 | 說明 |
strcpy | 字符串(以 \0 結尾) | 字符串 | 用于字符串之間的拷貝 |
sprintf | 可為任意基本數據類型 | 字符串 | 格式化輸出到字符串 |
memcpy | 任意可操作的內存地址 | 任意可操作的內存地址 | 用于任意內存塊之間的拷貝 |
2、功能用途不同
- strcpy 專門用于字符串之間的拷貝,遇到 \0 停止拷貝,因此只適用于以 \0 結尾的字符串。
- sprintf 將各種類型的數據格式化后寫入字符串中,功能強大但主要用于字符串格式化拼接,不是純粹的拷貝函數。
- memcpy 用于兩個內存塊之間的直接拷貝,不依賴 \0,也不關心數據類型,適用于任意類型的數據拷貝。
3、執行效率不同
函數 | 效率評價 |
memcpy | 最高。直接進行內存拷貝,沒有格式轉換或終止判斷 |
strcpy | 中等。需要逐字節判斷是否到達 \0 |
sprintf | 最低。涉及格式解析和字符轉換,開銷較大 |
4、使用注意事項
- strcpy 不檢查目標緩沖區大小,容易造成緩沖區溢出,建議使用 strncpy。
- sprintf 同樣不檢查緩沖區邊界,推薦使用更安全的 snprintf。
- memcpy 雖然高效,但如果源和目標內存區域有重疊,應使用 memmove 替代。
面試題11:如何將地址為0x67a9的整型變量賦值為0xaa66?
volatile int *ptr = (volatile int *)0x67a9;
*ptr = 0xaa66;
面試題12:面向對象編程的三大基本特征是什么?
面向對象編程(OOP)的三大核心特征是:
- 封裝性(Encapsulation)
- 繼承性(Inheritance)
- 多態性(Polymorphism)
這三大特性是構建面向對象系統的基礎,它們共同支持代碼的模塊化、可重用性和靈活性。
特征 | 含義 | 核心作用 |
封裝 | 隱藏實現細節,提供統一接口 | 提高安全性、簡化使用 |
繼承 | 類之間共享屬性和方法 | 代碼復用、建立類的層級關系 |
多態 | 父類引用指向子類對象,動態綁定 | 實現靈活調用、提升程序擴展性 |
面試題13:C++ 的空類默認包含哪些成員函數?
缺省構造函數:用于創建對象實例時初始化對象。
缺省拷貝構造函數:當對象通過另一個同類對象進行初始化時調用。
缺省析構函數:在對象生命周期結束時自動調用,用于清理資源。
缺省賦值運算符:將一個對象的內容賦值給另一個同類的對象。
缺省取址運算符:返回對象的內存地址。
缺省取址運算符 const:對于常量對象,返回其內存地址。
注意要點:
- 僅在需要時生成:只有當程序中實際使用了這些函數時,編譯器才會為其生成相應的實現。
- 覆蓋默認行為:如果需要自定義上述任何一種行為,可以通過顯式定義相應的成員函數來覆蓋默認實現。
- 全面了解默認函數:盡管有些資料可能只提及前四個默認函數,但后兩個(取址運算符及其 const 版本)同樣是重要的,默認情況下它們也是存在的。
面試題14:請談談你對拷貝構造函數和賦值運算符的理解
拷貝構造函數與賦值運算符重載在功能上都用于對象之間的復制操作,但它們在使用場景和實現邏輯上有以下兩個主要區別:
(1)拷貝構造函數用于生成一個新的類對象,而賦值運算符則用于已有對象的重新賦值。
(2)由于拷貝構造函數是用于構造新對象的過程,因此在初始化之前無需判斷源對象是否與新建對象相同;而賦值運算符必須進行這種判斷(即自賦值檢查),以避免不必要的錯誤。此外,在賦值操作中,如果目標對象已經分配了內存資源,則需要先釋放原有資源,再進行新的內存分配和數據復制。
注意:當類中包含指針類型的成員變量時,必須顯式重寫拷貝構造函數和賦值運算符,避免使用編譯器默認生成的版本。否則可能會導致淺拷貝問題,引發內存泄漏或重復釋放等問題。
面試題15:如何用 C++ 設計一個不能被繼承的類?
template <typename T>
class A
{friend T; // 只有 T 類可以訪問 A 的私有成員
private:A() {} // 私有構造函數~A() {} // 私有析構函數
};class B : virtual public A<B>
{
public:B() {} // 構造函數~B() {} // 析構函數
};class C : virtual public B
{
public:C() {}~C() {}
};int main(void)
{B b; // 合法:B 可以實例化// C c; // 編譯失敗:因為 C 繼承自 B,而 B 是 A<B> 的子類,構造受限return 0;
}
面試題16:訪問基類的私有虛函數。請寫出以下 C++ 程序的輸出結果
#include <iostream.h>class A
{
public:virtual void g() {cout << "A::g" << endl;}private:virtual void f(){cout << "A::f" << endl;}
};class B : public A
{
public:void g(){cout << "B::g" << endl;}virtual void h(){cout << "B::h" << endl;}
};typedef void (*Fun)(void);int main(void)
{B b;Fun pFun;for (int i = 0; i < 3; i++){pFun = (Fun)*((int*)*(int*)(&b) + i);pFun();}return 0;
}
程序輸出結果:
B::g
A::f
B::h
注意:本題主要考察了面試者對虛函數的理解程度。一個對虛函數不了解的人很難正確的做出本題。 在學習面向對象的多態性時一定要深刻理解虛函數表的工作原理。
面試題17:簡述類成員函數的重寫(Override)、重載(Overload)和隱藏(Hide)的區別。
(1)重寫 和 重載 的主要區別如下:
- 作用范圍不同: 重寫的函數位于兩個不同的類中(基類和派生類),而重載的函數始終在同一個類中。
- 參數列表不同: 重寫函數與被重寫函數的參數列表必須完全相同;而重載函數與被重載函數的參數列表必須不同(包括參數個數、類型或順序)。
- virtual 關鍵字要求不同: 基類中的被重寫函數必須使用 virtual 關鍵字進行修飾,才能實現多態;而重載函數無論是否使用 virtual 都不影響其重載特性。
(2)隱藏 與 重寫、重載 的區別如下:
- 作用范圍不同: 與重寫類似,隱藏也發生在基類與派生類之間,而不是像重載那樣在同一類中。
- 參數列表不同: 隱藏函數與被隱藏函數的參數列表可以相同,也可以不同,但它們的函數名必須相同。當參數列表不同時,不論基類中的函數是否為虛函數,都會導致基類函數被隱藏,而不是被重寫。
說明: 雖然重載和重寫都是實現多態性的基礎,但它們的技術實現方式完全不同,所達成的目的也有本質區別。其中,重寫支持的是運行時多態(動態綁定),而重載實現的是編譯時多態(靜態綁定)。
面試題18:說下多態實現的原理
當編譯器檢測到一個類中包含虛函數時,會自動為該類生成一個虛函數表(vtable)。虛函數表中的每一項都是一個指向相應虛函數的指針。
同時,編譯器會在該類的實例中隱式插入一個虛函數表指針(vptr),用于指向該類對應的虛函數表。對于大多數編譯器(如 VC++),這個 vptr 通常被放置在對象內存布局的最開始位置。
在調用構造函數創建對象時,編譯器會在構造過程中自動執行一段隱藏代碼,將 vptr 指向當前類的 vtable,從而建立起對象與虛函數表之間的關聯。
此外,在構造函數內部,this 指針此時已經指向具體的派生類對象,因此可以通過 vptr 找到正確的 vtable,進而訪問到實際應調用的虛函數體。這種機制使得程序可以在運行時根據對象的實際類型來動態綁定函數調用,這就是所謂的動態聯編(Dynamic Binding),也是 C++ 實現多態的核心原理。
注意:
- 要明確區分 虛函數、純虛函數 和 虛擬繼承 的概念及其區別;
- 理解虛函數表的工作機制是掌握面向對象多態性的關鍵;
- 在 C++ 面試中,虛函數與多態是高頻考點,務必熟練掌握其實現原理和應用場景。
面試題19:鏈表和數組有什么區別?
(1)存儲形式不同: 數組是一塊連續的內存空間,聲明時必須指定其固定長度;而鏈表由一系列不連續的節點組成,每個節點包含數據部分和指向下一個節點的指針,其長度可以根據需要動態增長或縮減。
(2)數據查找效率不同: 數組支持隨機訪問,可以通過索引直接定位元素,查找效率高;而鏈表只能從頭節點開始逐個遍歷,查找效率較低。
(3)插入與刪除操作不同: 在鏈表中進行插入或刪除操作只需修改相鄰節點的指針,效率較高;而在數組中插入或刪除元素通常需要移動大量數據以保持連續性,操作效率較低。
(4)越界問題: 數組存在越界風險,使用不當可能導致程序崩潰或未定義行為;鏈表不存在越界問題,但需注意空指針異常。
說明:
選擇數組還是鏈表應根據具體應用場景來決定:
- 若應用更注重快速查詢,且數據量變化不大,優先考慮使用數組;
- 若應用頻繁進行插入和刪除操作,數據量不確定,建議使用鏈表。
數組占用空間緊湊,但長度固定;鏈表靈活可變,但額外占用空間用于保存指針信息。合理選擇數據結構有助于提升程序的效率與穩定性。
面試題20:MySQL索引哪些情況會失效?
- 查詢條件包含or,可能導致索引失效
- 如何字段類型是字符串,where時一定用引號括起來,否則索引失效
- like通配符可能導致索引失效。
- 聯合索引,查詢時的條件列不是聯合索引中的第一個列,索引失效。
- 在索引列上使用mysql的內置函數,索引失效。
- 對索引列運算(如,+、-、*、/),索引失效。
- 索引字段上使用(!= 或者 < >,not in)時,可能會導致索引失效。
- 索引字段上使用is null, is not null,可能導致索引失效。
- 左連接查詢或者右連接查詢查詢關聯的字段編碼格式不一樣,可能導致索引失效。
- mysql估計使用全表掃描要比使用索引快,則不使用索引。
面試題21:說說隊列和棧的異同。
隊列(Queue)和棧(Stack)都屬于基礎的線性數據結構,但它們在數據的插入與刪除操作方式上存在明顯差異。
- 操作特性不同: 隊列遵循“先進先出”(FIFO, First In First Out)原則,即最先插入的元素最先被移除; 棧則遵循“后進先出”(LIFO, Last In First Out)原則,即最后壓入棧的元素最先彈出。
- 應用場景不同: 隊列適用于需要按順序處理數據的場景,如任務調度、消息隊列等; 棧常用于函數調用、表達式求值、括號匹配等需要回溯的場景。
- 實現方式類似: 它們都可以通過數組或鏈表實現,但在插入和刪除的位置上有明確限制。
注意:區分“數據結構中的棧”與“程序內存中的棧區”
雖然“棧”這個詞在數據結構中表示一種存儲模型,但它也常用于描述程序運行時的內存區域,容易引起混淆。
- 棧區(Stack): 是程序運行時的一個內存區域,由編譯器自動分配和釋放,主要用于存放函數參數、局部變量等。其訪問方式類似于數據結構中的棧,具有高效快速的特點。
- 堆區(Heap): 是用于動態內存管理的一塊內存區域,通常由程序員手動申請和釋放(如使用 malloc / new 和 free / delete)。如果未手動釋放,程序結束后可能由操作系統回收。其分配方式類似于鏈表,靈活但需謹慎管理。
- 兩者是完全不同的概念: 數據結構中的“堆”和“棧”描述的是數據組織的方式,而程序內存中的“堆區”和“棧區”指的是內存的分配區域,二者本質上并無直接關聯。
面試題22:索引不適合哪些場景?
- 數據量少的不適合加索引
- 更新比較頻繁的也不適合加索引
- 區分度低的字段不適合加索引(如性別)
面試題23:編碼實現直接插入排序
直接插入排序編程實現如下:
#include<iostream.h>
void main( void )
{ int ARRAY[10] = { 0, 6, 3, 2, 7, 5, 4, 9, 1, 8 }; int i,j; for( i = 0; i < 10; i++) { cout<<ARRAY[i]<<" "; } cout<<endl; for( i = 2; i <= 10; i++ ) //將 ARRAY[2],…,ARRAY[n]依次按序插入 { if(ARRAY[i] < ARRAY[i-1]) //如果 ARRAY[i]大于一切有序的數值, //ARRAY[i]將保持原位不動 { ARRAY[0] = ARRAY[i]; //將 ARRAY[0]看做是哨兵,是 ARRAY[i]的副本 j = i - 1; do{ //從右向左在有序區 ARRAY[1..i-1]中
//查找 ARRAY[i]的插入位置 ARRAY[j+1] = ARRAY[j]; //將數值大于 ARRAY[i]記錄后移 j-- ; }while( ARRAY[0] < ARRAY[j] ); ARRAY[j+1]=ARRAY[0]; //ARRAY[i]插入到正確的位置上 } } for( i = 0; i < 10; i++) { cout<<ARRAY[i]<<" "; } cout<<endl;
}
注意:所有為簡化邊界條件而引入的附加結點(元素)均可稱為哨兵。引入哨兵后使得查找循環條件的 時間大約減少了一半,對于記錄數較大的文件節約的時間就相當可觀。類似于排序這樣使用頻率非常高 的算法,要盡可能地減少其運行時間。所以不能把上述算法中的哨兵視為雕蟲小技。
面試題24:日常工作中你是怎么優化SQL的?
可以從這幾個維度回答這個問題:
- 加索引
- 避免返回不必要的數據
- 適當分批量進行
- 優化sql結構
- 分庫分表
- 讀寫分離
面試題25:編碼實現冒泡排序
冒泡排序編程實現如下:
#include <stdio.h>
#define LEN 10 //數組長度 void main( void ) { int ARRAY[10] = { 0, 6, 3, 2, 7, 5, 4, 9, 1, 8 }; //待排序數組 printf( "\n" ); for( int a = 0; a < LEN; a++ ) //打印數組內容 { printf( "%d ", ARRAY[a] ); } int i = 0; int j = 0; bool isChange; //設定交換標志
for( i = 1; i < LEN; i++ )
{ //最多做 LEN-1 趟排序 isChange = 0; //本趟排序開始前,交換標志應為假 for( j = LEN-1; j >= i; j-- ) //對當前無序區 ARRAY[i..LEN]自下向上掃描
{
if( ARRAY[j+1] < ARRAY[j] )
{ //交換記錄 ARRAY[0] = ARRAY[j+1]; //ARRAY[0]不是哨兵,僅做暫存單元 ARRAY[j+1] = ARRAY[j]; ARRAY[j] = ARRAY[0]; isChange = 1; //發生了交換,故將交換標志置為真 }
}
printf( "\n" );
for( a = 0; a < LEN; a++) //打印本次排序后數組內容 { printf( "%d ", ARRAY[a] );
}
if( !isChange )
{ break;
} //本趟排序未發生交換,提前終止算法
printf( "\n" ); return;
}
面試題26:InnoDB與MyISAM的區別
特性 | InnoDB | MyISAM |
是否支持事務 | ? | ? |
是否支持外鍵 | ? | ? |
是否支持 MVCC | ? | ? |
全文索引 | ?(MySQL 5.7+) | ? |
鎖級別 | 行級鎖、表級鎖 | 表級鎖 |
主鍵要求 | 必須有主鍵 | 可無主鍵 |
崩潰恢復能力 | 強,支持事務恢復 | 較弱 |
存儲結構 | 索引組織表,共享/獨立表空間 | .frm/.MYD/.MYI 文件存儲 |
選擇合適的存儲引擎應根據具體業務需求進行權衡:
- 如果強調事務安全、并發寫入、數據一致性,推薦使用 InnoDB;
- 如果以讀為主、對事務要求不高、追求輕量快速,可考慮使用 MyISAM。
面試題27:編程實現堆排序
堆排序編程實現:
void createHeep(int ARRAY[], int sPoint, int Len) //生成大根堆
{while ((2 * sPoint + 1) < Len){int mPoint = 2 * sPoint + 1;if ((2 * sPoint + 2) < Len){if (ARRAY[2 * sPoint + 1] < ARRAY[2 * sPoint + 2]){mPoint = 2 * sPoint + 2;}說明:堆排序,雖然實現復雜,但是非常的實用。另外讀者可是自己設計實現小堆排序的算法。雖然和
大堆排序的實現過程相似,但是卻可以加深對堆排序的記憶和理解。
28.編程實現基數排序 }if (ARRAY[sPoint] < ARRAY[mPoint]) //堆被破壞,需要重新調整{int tmpData = ARRAY[sPoint]; //交換 sPoint 與 mPoint 的數據ARRAY[sPoint] = ARRAY[mPoint];ARRAY[mPoint] = tmpData;sPoint = mPoint;}else{break; //堆未破壞,不再需要調整}}return;
}
void heepSort(int ARRAY[], int Len) //堆排序
{int i = 0;for (i = (Len / 2 - 1); i >= 0; i--) //將 Hr[0,Lenght-1]建成大根堆{createHeep(ARRAY, i, Len);}for (i = Len - 1; i > 0; i--){int tmpData = ARRAY[0]; //與最后一個記錄交換ARRAY[0] = ARRAY[i];ARRAY[i] = tmpData;createHeep(ARRAY, 0, i); //將 H.r[0..i]重新調整為大根堆}return;
}
int main(void)
{int ARRAY[] = { 5, 4, 7, 3, 9, 1, 6, 8, 2 };printf("Before sorted:\n"); //打印排序前數組內容for (int i = 0; i < 9; i++){printf("%d ", ARRAY[i]);}printf("\n");heepSort(ARRAY, 9); //堆排序printf("After sorted:\n"); //打印排序后數組內容for (i = 0; i < 9; i++){printf("%d ", ARRAY[i]);}printf("\n");
}
面試題28:數據庫索引的原理及為什么選擇B+樹?
在探討數據庫索引時,我們首先需要理解其核心目標:提高數據檢索速度。為了達到這個目的,數據庫系統使用了不同的數據結構來實現索引。這里我們將討論為何B+樹成為大多數關系型數據庫(如MySQL)中索引的首選結構,而不是二叉樹或平衡二叉樹等其他選項。
1. 查詢速度與效率穩定性
- 二叉樹:理論上,二叉查找樹可以在O(log n)時間內完成查找、插入和刪除操作。然而,在最壞情況下(例如數據已經排序的情況下),二叉樹可能會退化成鏈表,導致時間復雜度變為O(n),這極大地影響了查詢性能。
- 平衡二叉樹(如AVL樹或紅黑樹):這些樹通過各種機制保持樹的高度盡可能小,從而保證操作的時間復雜度為O(log n)。但是,它們每個節點只存儲一個鍵值,這意味著對于大規模數據集,樹的高度仍然可能較大,導致磁盤I/O次數增加。
- B樹:B樹允許每個節點包含多個鍵值和子節點指針,這樣可以減少樹的高度,進而減少磁盤訪問次數。不過,B樹中的所有節點都存儲了實際的數據記錄,這限制了單個節點能容納的鍵值數量,間接增加了樹的高度。
- B+樹:相比B樹,B+樹將所有數據記錄僅存放在葉子節點上,非葉子節點僅用于索引。這種方式不僅減少了內部節點的大小,允許每個節點存放更多的鍵值,進一步降低了樹的高度,而且所有葉子節點按順序鏈接在一起,支持范圍查詢優化。
2. 存儲空間與查找磁盤次數
- 存儲空間:由于B+樹的非葉子節點不存儲實際數據,因此同樣的存儲空間內,B+樹可以擁有更高的分支因子,即每個節點可以有更多的孩子節點。這意味著對于相同數量的數據,B+樹通常比其他類型的樹更“矮胖”,從而減少了查找過程中所需的磁盤訪問次數。
- 查找磁盤次數:數據庫中的索引通常是基于磁盤存儲的,而磁盤I/O是相對較慢的操作。B+樹的設計旨在最小化磁盤I/O,因為它通過最大化每個節點的信息量來減少從根節點到葉子節點路徑上的節點數,即減少查找所需讀取的磁盤塊數。
面試題29:談談你對編程規范的理解或認識
編程規范不僅僅是代碼格式的統一,更是提升代碼質量、增強團隊協作效率、保障項目長期維護的重要基礎。我認為編程規范的核心目標可以歸納為四個方面:程序的可行性、可讀性、可移植性 和 可測試性。
上述四點是編程規范的總體目標,而不是簡單的條文背誦。在實際開發中,通常會結合自身編程習慣和團隊要求,從以下幾個方面落實編程規范:
- 命名清晰:變量、函數、類名要有意義,避免縮寫模糊;
- 函數設計簡潔:一個函數只做一件事,控制長度不超過一屏;
- 適當注釋:關鍵邏輯加注釋,但不過度依賴注釋;
- 統一代碼風格:使用 IDE 格式化配置、遵循團隊編碼規范;
- 模塊化設計:高內聚、低耦合,便于擴展與測試。
面試題30:簡述聚集索引與非聚集索引的區別
主要區別如下:
區別項 | 聚集索引 | 非聚集索引 |
數量限制 | 一個表只能有一個聚集索引 | 一個表可以有多個非聚集索引 |
數據存儲順序 | 索引鍵值的邏輯順序決定了數據行的物理存儲順序(即數據按索引排序存儲) | 索引的邏輯順序與數據行的物理存儲順序無關 |
葉節點內容 | 葉節點就是實際的數據頁(即索引結構和數據融合在一起) | 葉節點不包含實際數據,僅保存索引列值和指向實際數據行的指針(如行標識 RID 或聚集索引鍵) |
查詢效率 | 查詢效率高,特別是范圍查詢(如 WHERE id BETWEEN 100 AND 200) | 查詢效率相對較低,通常需要回表查找數據 |
插入/更新代價 | 插入或更新時可能導致數據頁重新排序或分裂,性能影響較大 | 插入或更新對索引維護的開銷較小 |
聚集索引決定了數據在磁盤上的物理存儲順序。因此,一旦建立了聚集索引,表中的數據就按照該索引進行組織和排序。也正因為如此,每個表只能擁有一個聚集索引。
非聚集索引是一種獨立于數據存儲結構的索引形式。它只包含索引字段的值和一個指向對應數據行的指針(RID 或主鍵),因此可以在一張表上建立多個非聚集索引以滿足不同查詢需求。
如果將數據庫索引類比為書籍目錄:
- 聚集索引就像書本內容是按照目錄順序排版的;
- 非聚集索引則像附錄的關鍵詞索引,只是告訴你某個詞在哪一頁,并不改變正文的排列順序。
面試題31:&&和&、||和|有什么區別
(1)&和|對操作數進行求值運算,&&和||只是判斷邏輯關系。
(2)&&和||在在判斷左側操作數就能 確定結果的情況下就不再對右側操作數求值。
注意:在編程的時候有些時候將&&或||替換成&或|沒有出錯,但是其邏輯是錯誤的,可能會導致不可 預想的后果(比如當兩個操作數一個是 1 另一個是 2 時。
面試題32:C++ 的引用和 C 語言的指針有什么區別?
(1)初始化要求不同:
引用在聲明時必須進行初始化,并且不會單獨分配存儲空間,它只是某個已有變量的別名;而指針在聲明時可以不初始化,在后續賦值時才會指向某個內存地址,并且會占用自身的存儲空間。
(2)可變性不同:
引用一旦綁定到一個變量后,就不能再改變,始終代表該變量;而指針可以在程序運行過程中指向不同的對象,具有更高的靈活性。
(3)空值表示不同:
引用不能為“空”,即不存在“空引用”,它必須綁定到一個有效的對象;而指針可以為空(NULL 或 nullptr),表示不指向任何對象,這在很多場景下非常有用,比如判斷是否有效等。
注意:引用作為函數參數時需謹慎使用
引用常被用作函數參數,其目的是為了實現對實參的直接修改。然而,這也帶來了一個潛在的問題:調用函數時從代碼表面看不出該參數是否為引用,容易讓人誤以為傳入的是普通值,從而忽略了函數可能會修改原始變量的風險。
面試題33:請簡述什么是臟讀、不可重復讀和幻讀
在數據庫并發操作中,由于多個事務交替執行,可能會導致數據一致性問題。其中,臟讀、不可重復讀 和 幻讀 是三種常見的并發異常現象,它們分別描述了不同場景下的數據不一致問題,具體如下:
(1)臟讀(Dirty Read): 當一個事務讀取到了另一個事務尚未提交的數據時,就可能發生臟讀。例如,事務 A 修改了一條記錄但還未提交,事務 B 讀取了這條修改后的數據,如果事務 A 回滾,則事務 B 讀到的就是無效的“臟”數據。
示例:事務 A 更新余額為 500,事務 B 讀取后顯示為 500,但事務 A 最終回滾,實際余額仍為 1000。
(2)不可重復讀(Non-repeatable Read): 在一個事務內多次執行相同的查詢,但由于其他事務對同一條數據進行了修改并提交,導致兩次查詢結果不一致。也就是說,同一行數據在同一個事務中被多次讀取,卻返回了不同的值。
示例:事務 A 第一次查詢某一行得到值為 100,之后事務 B 修改該行并提交,事務 A 再次查詢該行得到值為 200。
(3)幻讀(Phantom Read): 一個事務在執行范圍查詢時,另一個事務插入或刪除了符合條件的新數據并提交,導致前一個事務再次執行相同范圍查詢時,結果集發生了變化(多出或少了幾行),這種現象稱為幻讀。
示例:事務 A 查詢 WHERE id < 100 得到 5 條記錄,事務 B 插入一條 id = 99 的記錄并提交,事務 A 再次查詢發現變成了 6 條記錄。
總結對比表:
現象 | 描述 | 涉及操作 | 典型場景 |
臟讀 | 讀取到其他事務未提交的數據 | 讀已修改數據 | 數據不一致,可能出現錯誤數據 |
不可重復讀 | 同一事務內多次讀取同一行,結果不同 | 讀已更新數據 | 數據統計或狀態判斷出錯 |
幻讀 | 同一事務內多次范圍查詢,結果集數量變化 | 讀新增/刪除數據 | 分頁查詢、范圍條件統計受影響 |
面試題34:在高并發情況下,如何做到安全的修改同一行數據?
通常采用兩種主流機制:悲觀鎖 和 樂觀鎖。
1、使用悲觀鎖(Pessimistic Lock)
悲觀鎖的核心思想是:假設并發沖突經常發生,因此在訪問數據時就加鎖,防止其他線程修改。
實現方式:
- 在查詢數據時加上排他鎖(Exclusive Lock),例如在 SQL 中使用 SELECT ... FOR UPDATE。
適用場景:
- 寫操作頻繁;
- 并發沖突概率較高;
- 對數據一致性要求極高。
2、使用樂觀鎖(Optimistic Lock)
樂觀鎖的核心思想是:假設并發沖突較少發生,先允許線程讀取并修改數據,在提交更新時檢查是否有其他線程已經修改過該數據,若有則拒絕更新或重試。
實現方式:
版本號機制(Version Number)
- 在表中增加一個 version 字段;
- 每次更新數據時檢查版本號,并將其加一;
適用場景:
- 讀多寫少;
- 并發沖突較少;
- 希望減少鎖競爭以提升性能。
面試題35:簡述 typedef 和 #define 的區別
typedef 和 #define 都可以在 C/C++ 中用于定義別名或常量,但它們的本質不同,分別屬于不同的處理階段,具有不同的特性和使用方式。主要區別如下:
(1)用途不同:
- typedef 是用于為已有數據類型創建一個新的別名(alias),主要用于增強代碼的可讀性和可維護性;
- #define 是預處理指令,通常用于宏定義,包括常量定義、函數宏等,本質上是文本替換。
(2)處理階段不同:
- typedef 是編譯階段的一部分,它參與語法和類型檢查,因此更安全;
- #define 是預編譯階段的宏替換機制,在編譯之前進行簡單的字符串替換,不進行類型檢查,容易引入錯誤。
(3)作用域控制不同:
- typedef 受作用域限制,可以在函數內部、命名空間中定義局部別名;
- #define 定義的宏沒有作用域概念,一旦定義,從定義處開始到文件結束都有效(除非被 #undef 取消)。
(4)對指針的影響不同: 這是兩者一個非常容易出錯的區別點:
- 使用 typedef 定義指針類型時,能正確地將整個標識符作為指針類型;
- 而使用 #define 宏定義指針類型時,容易因優先級問題導致理解偏差或錯誤用法。
面試題36:簡述關鍵字const的作用和意義
在 C/C++ 中,const 是一個非常重要的關鍵字,用于聲明常量性(只讀性)。它可以修飾變量、指針、函數參數以及成員函數等,表示該標識符所代表的內容在定義后不能被修改。
主要優點:便于類型檢查、同宏定義一樣可以方便地進行參數 的修改和調整、節省空間,避免不必要的內存分配、可為函數重載提供參考。
面試題37:簡述關鍵字static的作用
使用場景 | 作用描述 |
全局變量前加 static | 限制變量作用域,只在本文件內可見 |
局部變量前加 static | 延長生命周期,保持值直到程序結束,且默認初始化為0 |
函數前加 static | 限制函數作用域,只在本文件內可見 |
類中定義 static 成員 | 所有對象共享該成員 |
類中定義 static 方法 | 只能訪問靜態成員,無 this 指針 |
面試題38:簡述關鍵字extern的作用
在 C/C++ 中,extern 是一個存儲類修飾符,主要用于聲明變量或函數是在當前文件之外定義的,即其定義位于其他源文件或庫中。它的核心作用是告訴編譯器:“這個變量或函數已經在別處定義了,請不要報錯,鏈接時去其他模塊中查找”。
特性 | 說明 |
關鍵字 | extern |
主要用途 | 聲明變量或函數在其它模塊中定義 |
是否分配內存 | 否,僅聲明 |
是否允許多次出現 | 是,可在多個文件中聲明 |
是否必須有定義 | 是,否則鏈接時報錯 |
典型應用場景 | 跨文件共享全局變量、調用外部函數 |
面試題39:簡述為什么流操作符<<和>>重載時返回引用
在 C++ 中,流操作符 <<(輸出)和 >>(輸入)經常被連續使用,例如鏈式調用多個輸出或輸入操作。為了支持這種鏈式調用,流操作符的重載函數通常返回一個對流對象的引用。這不僅提高了代碼的可讀性和簡潔性,還確保了流操作的靈活性和效率。
為什么返回引用?
支持鏈式調用:
- 當我們連續使用流操作符時,如 cout << "Hello, " << "World!" << endl;,每個 << 操作實際上是一個函數調用。
- 如果返回引用,則每次調用后返回的是同一個流對象的引用,這樣下一個 << 操作可以直接作用于這個流對象,從而實現鏈式調用。
保持流對象的狀態:
- 返回引用而不是值,意味著不會創建新的流對象副本,保證了流對象的狀態一致性。
- 這對于維護流對象內部的狀態(如錯誤狀態、緩沖區等)非常重要。
提高性能:
- 返回引用避免了不必要的對象復制,提高了程序運行效率。
操作符類型 | 返回值類型 | 是否支持鏈式調用 | 適用場景 |
流操作符 << 和 >> | 引用 | 是 | 輸入輸出流操作 |
賦值操作符 = | 引用 | 是 | 對象賦值 |
算術操作符 +, -, *, / | 對象 | 否 | 數值運算 |
注意:
- 在重載流操作符時,務必返回流對象的引用,否則將無法支持鏈式調用,導致代碼可讀性和功能性下降。
- 對于其他操作符,如算術運算符,由于涉及右值和臨時對象的創建,應返回新構造的對象而非引用。
面試題40:簡述指針常量與常量指針的區別
指針常量指的是一個指針本身是常量,也就是說,該指針一旦初始化指向某個地址后,就不能再改變其指向。
常量指針則指的是一個指向常量的指針,也就是說,不能通過該指針去修改其所指向的對象的值,但指針本身的指向可以更改。
特性 | 指針常量(Constant Pointer) | 常量指針(Pointer to Constant) |
定義形式 | T* const ptr; | const T* ptr; 或 T const* ptr; |
是否能改指向 | ? 不可更改 | ? 可更改 |
是否能改內容 | ? 可更改 | ? 不可更改 |
強調重點 | 指針本身不能變 | 指向的內容不能變 |
示例 | int* const ptr = &a; | const int* ptr = &a; |
面試題41:MySQL事務的四大特性
MySQL 中的事務(Transaction)是指作為單個邏輯工作單元執行的一系列操作,這些操作要么全部成功,要么全部失敗回滾。為了保證數據的一致性和可靠性,事務必須滿足四個基本特性,簡稱 ACID 特性,分別是:
1. 原子性(Atomicity)
- 事務是一個不可分割的工作單位,事務中的所有操作要么全部完成,要么全部不完成;
- 如果事務中任何一個步驟失敗,整個事務都會被回滾到最初狀態,就像這個事務從來沒有執行過一樣;
- 實現機制:通常通過 undo log(回滾日志) 來實現。
? 示例:銀行轉賬操作中,A 轉賬給 B,若在轉賬過程中出現異常(如斷電),系統會自動回滾,確保 A 沒扣款,B 也不會多錢。
2. 一致性(Consistency)
- 事務的執行不能破壞數據庫的完整性約束和業務邏輯規則;
- 在事務開始之前和結束之后,數據庫都處于一致狀態;
- 一致性是事務的最終目標,由其他三個特性共同保障。
? 示例:如果一個表規定字段 age 必須大于 0,則事務執行后不能讓該字段變成負數。
3. 隔離性(Isolation)
- 多個事務并發執行時,彼此之間應該互不干擾,每個事務都感覺不到其他事務的存在;
- 不同的隔離級別可以控制并發訪問的程度,避免臟讀、不可重復讀、幻讀等問題;
- 實現機制:通過 鎖機制 和 MVCC(多版本并發控制) 實現。
? 示例:兩個用戶同時修改同一行數據,數據庫應確保它們的操作不會互相干擾。
4. 持久性(Durability)
- 事務一旦提交,其對數據庫的修改就是永久性的,即使系統發生故障也不會丟失;
- 實現機制:主要依賴于 redo log(重做日志) 和 雙寫緩沖區 等機制。
? 示例:支付完成后,即使服務器突然宕機,用戶的余額也已經持久化保存。
面試題42:如何避免“野指針”的產生?
“野指針”是指指向無效內存區域的指針。它并未指向一個合法的對象或內存地址,因此對它的操作可能導致程序崩潰或不可預知的行為。為了避免“野指針”的出現,可以從以下幾個方面進行預防。
(1)指針變量未初始化的問題
指針變量在聲明時如果沒有被顯式賦值,其內容是隨機的,可能指向任意內存地址,從而形成野指針。
解決辦法:在定義指針變量時應立即進行初始化,可以將其指向一個有效的變量地址,或者直接賦值為 NULL(C 語言)或 nullptr(C++11 及以上)。
(2)釋放內存后未將指針置空的問題
當使用 free()(C 語言)或 delete / delete[](C++)釋放指針所指向的內存后,如果未將指針設為 NULL 或 nullptr,該指針就變成了野指針。
解決辦法:每次釋放完內存后,立即將對應的指針設置為 NULL 或 nullptr,以防止后續誤用。
(3)訪問超出作用域的內存問題
如果指針指向的是局部變量或臨時對象,在變量超出作用域之后,其所占用的內存會被系統回收,此時指針仍然保存著原來的地址,就會變成野指針。
解決辦法:不要返回局部變量的地址;在變量的作用域結束前及時釋放相關資源,并將指針置為 NULL 或 nullptr。
面試題43:簡述常引用的作用
在 C++ 中,常引用(const reference) 是指通過 const 關鍵字修飾的引用,表示該引用所綁定的對象不能被修改。它的主要作用是在不改變原始對象的前提下,提供對對象的安全訪問。
常引用的核心目的是:
- 防止在使用引用的過程中意外修改原始變量的值;
- 提高程序的可讀性和安全性;
- 避免不必要的拷貝操作,提升性能。
面試題44:如果一張表有近千萬條數據,導致 CRUD 操作變慢,你會如何進行優化?
優化方向 | 技術手段 | 適用場景 |
結構性優化 | 分庫分表(水平/垂直)、引入中間件 | 數據量大、并發高 |
查詢性能優化 | 索引優化、SQL 優化、覆蓋索引 | 查詢緩慢、索引缺失 |
存儲與運維 | 讀寫分離、定期維護、緩存 | 讀多寫少、數據分散 |
架構擴展 | 異步處理、引入搜索引擎 | 實時性不高、復雜查詢 |
面對千萬級數據量的表,應結合具體業務場景,采用多種優化手段協同工作,既要關注查詢性能,也要兼顧系統可擴展性和維護成本。
面試題45:簡述strcpy、sprintf與memcpy的區別
特性 | strcpy | sprintf | memcpy |
操作對象 | 字符串 → 字符串 | 多種類型 → 字符串 | 內存塊 → 內存塊 |
是否支持格式哦u化 | ? 不支持 | ? 支持 | ? 不支持 |
效率 | 中等 | 較低 | 最高 |
安全性 | 易溢出 | 易溢出 | 高(仍需手動控制長度) |
典型應用場景 | 字符串復制 | 數據格式化輸出 | 結構體/二進制數據復制 |
面試題46:簡述數據庫中使用自增主鍵可能帶來的問題
問題類型 | 是否常見 | 原因 | 推薦解決方式 |
分庫分表主鍵沖突 | ? 高頻 | 多個分片各自維護自增序列 | 改用全局唯一主鍵方案 |
鎖競爭性能瓶頸 | ? 中頻 | 自增機制加鎖保護 | 使用非自增主鍵或調整自增步長 |
主鍵用盡溢出 | ? 低頻 | 數據類型限制 | 使用 BIGINT 或提前擴容 |
安全性/可預測問題 | ? 中頻 | 主鍵遞增可被猜測 | 使用 UUID 或雪花算法生成不可預測主鍵 |
建議:
- 單體數據庫場景下,自增主鍵仍是簡單高效的首選;
- 但在分布式架構、高并發寫入、大數據量等復雜場景中,應優先考慮使用全局唯一主鍵方案,以提升系統的可擴展性和穩定性。
面試題47:MVCC熟悉嗎,它的底層原理?
MVCC是一種用于數據庫管理系統中提高并發性能的技術。它通過保存數據對象的多個版本來支持非鎖定讀取,從而減少事務間的沖突,允許更高的并發度。
MVCC 的實現依賴于以下幾個核心概念和技術:
1、隱藏列:
- 每個數據行除了用戶定義的列外,還會包含一些隱藏列
2、事務ID:
- 數據庫系統為每個新事務分配一個唯一的遞增事務ID(Transaction ID),用于標識事務的時間順序。
3、快照讀與當前讀:
- 快照讀(Snapshot Read): 讀取事務開始時的數據快照,不會看到未提交的更改或在此之后發生的更改。
- 當前讀(Current Read): 直接讀取最新的數據版本,能看到所有已提交的更改。
4、垃圾回收(GC):
- 當某些舊版本的數據不再被任何活躍事務需要時,這些過期的數據版本會被清理掉,以釋放存儲空間。
面試題48:簡述數據庫連接池的概念及其必要性
數據庫連接池是一種資源管理技術,它通過預先創建并維護一定數量的數據庫連接對象,并對外提供獲取和釋放這些連接的方法,從而減少頻繁建立和斷開數據庫連接所帶來的開銷。
連接池的工作機制:
- 在初始化時,連接池會預先創建一組數據庫連接,并將它們保存在一個池子里。
- 當應用程序需要訪問數據庫時,不是直接創建新的連接,而是從連接池中借用一個現有的連接。
- 使用完畢后,連接會被歸還到池中,而不是真正關閉,以便后續請求可以繼續使用。
數據庫連接的建立過程
- TCP三次握手: 應用程序通過 TCP 協議與數據庫服務器建立網絡連接。
- 身份驗證: 發送用戶名和密碼給數據庫服務器進行身份驗證。
- 執行SQL語句: 驗證成功后,應用程序可以向數據庫發送 SQL 查詢或命令。
- TCP四次揮手: 操作完成后,關閉連接,釋放網絡資源。
連接池的好處總結
好處 | 描述 |
資源重用 | 減少了頻繁創建和銷毀連接帶來的系統開銷 |
更快響應速度 | 快速獲取連接,減少等待時間 |
控制并發量 | 限制最大連接數,保護數據庫免受過載 |
統一管理 | 防止連接泄漏,簡化連接管理 |
面試題49:簡述構造函數是否能為虛函數?
在 C++ 中,構造函數不能是虛函數,而析構函數不僅可以是虛函數,而且在某些復雜類層次結構中,通常需要將析構函數聲明為虛函數。
構造函數為何不能為虛函數?
1、虛函數表(vtable)機制:
- 虛函數依賴于每個對象內部維護的一個虛函數表(vtable),通過該表實現動態綁定。
- 構造函數的作用是初始化對象的狀態,但在對象完全構造之前,vtable 尚未建立,因此無法支持虛函數調用。
2、構造順序問題:
- 在構造過程中,基類部分先于派生類部分被構造。如果允許構造函數為虛函數,則會導致試圖調用一個尚未完全構造的對象的方法,這顯然是不合理的。
- 實際上,在構造函數體內調用虛函數時,只會調用到基類中的版本,因為此時派生類部分還未構造完成。
函數類型 | 是否可為虛函數 | 原因 |
構造函數 | ? 不可 | 對象未完全構造前,vtable 尚未建立 |
析構函數 | ? 可以 | 確保多態刪除時,派生類部分也能正確銷毀 |
純虛析構函數 | ? 可以 | 必須有定義體,以便在派生類銷毀時隱式調用 |
面試題50:談談你對面向對象的理解
面向對象是一種程序設計思想,也是一種系統分析與設計方法。它將現實世界中的事物抽象為程序中的“對象”,通過對象之間的交互來完成系統的功能。
1、面向對象的核心思想
面向對象的核心在于從“對象”的角度出發思考問題,而不是單純地圍繞功能或流程展開設計。它強調的是:
- 萬物皆對象: 將現實世界中的實體映射為程序中的對象,每個對象都具有屬性(數據)和行為(方法)。
- 模塊化設計: 將復雜問題分解為多個相對獨立的對象,分別進行設計和實現,提高代碼的可維護性和擴展性。
- 高內聚、低耦合: 每個對象內部封裝自己的狀態和行為,對外提供清晰的接口,減少對象間的依賴關系。
2、面向對象的三大基本特性
封裝(Encapsulation):
- 將對象的屬性和行為包裝在一起,并控制對外暴露的程度;
- 提供訪問控制機制(如 private、protected、public),增強數據的安全性。
繼承(Inheritance):
- 子類可以繼承父類的屬性和方法,實現代碼復用;
- 支持類層次結構的設計,體現“is-a”關系。
多態(Polymorphism):
- 同一個接口可以有不同的實現方式;
- 主要通過虛函數(C++)、接口(Java)或抽象類實現,提升系統的靈活性和擴展性。
3、面向對象與傳統面向過程的區別
對比維度 | 面向過程(Procedural) | 面向對象(Object-Oriented) |
設計視角 | 基于功能和流程 | 基于對象和交互 |
數據與行為關系 | 分離 | 綁定在一起 |
擴展性 | 修改原有代碼多,擴展困難 | 易于通過繼承和多態擴展 |
可維護性 | 結構松散,維護成本高 | 模塊清晰,易于維護 |
4、面向對象不僅僅是指編程
雖然我們最常接觸的是“面向對象編程(OOP)”,但完整的面向對象技術還包括:
- 面向對象分析(OOA): 分析問題領域,識別出關鍵的對象及其關系。
- 面向對象設計(OOD): 在分析基礎上設計系統的結構,包括類圖、交互圖等。
- 面向對象建模(OOM): 使用 UML(統一建模語言)等工具對系統進行可視化建模。
由于篇幅限制,本期C/C++高頻面試題就寫到這兒了
(需要這份C/C++高頻面試題1000道pdf文檔的同學,文章底部關注后自取)
下期會再更新50道題,先劇透部分題目:
51.const、static作用。
52.c++面向對象三大特征及對他們的理解,引出多態實現原理、動態綁定、菱形繼承。
53.虛析構的必要性,引出內存泄漏,虛函數和普通成員函數的儲存位置,虛函數表、虛函數表指針
54.malloc、free和new、delete區別,引出malloc申請大內存、malloc申請空間失敗怎么辦
55.stl熟悉嗎,vector、map、list、hashMap,vector底層,map引出紅黑樹。優先隊列用過嗎,使用的場景。
無鎖隊列聽說過嗎,原理是什么(比較并交換)
56.實現擅長的排序,說出原理(快排、堆排)
57.四種cast,智能指針
58.tcp和udp區別
59.進程和線程區別 60.指針和引用作用以及區別
61.c++11用過哪些特性,auto作為返回值和模板一起怎么用,函數指針能和auto混用嗎
62.boost用過哪些類,thread、asio、signal、bind、function
63.單例、工廠模式、代理、適配器、模板,使用場景
64.QT信號槽實現機制,QT內存管理,MFC消息機制
65.進程間通信。會選一個詳細問
66.多線程,鎖和信號量,互斥和同步
67.動態庫和靜態庫的區別
(需要這份C/C++高頻面試題1000道pdf文檔的同學,文章底部關注后自取)