C/C++核心知識點詳解
1. 變量的聲明與定義:內存分配的本質區別
核心概念
在C/C++中,變量的聲明和定義是兩個完全不同的概念:
- 聲明(Declaration):告訴編譯器變量的名稱和類型,但不分配內存空間
- 定義(Definition):不僅聲明變量,還為其分配實際的內存空間
實際應用示例
// 變量聲明:使用extern關鍵字,不分配內存
extern int global_var; // 聲明一個整型變量,告訴編譯器這個變量在別處定義// 變量定義:分配內存空間
int global_var = 100; // 定義變量并分配內存,可以初始化// 函數聲明
int add(int a, int b); // 函數聲明,不包含函數體// 函數定義
int add(int a, int b) { // 函數定義,包含完整實現return a + b;
}
重要規則
- 一個變量可以在多個地方聲明,但只能在一個地方定義
- 聲明可以重復,定義不能重復
- 使用變量前必須先聲明,使用時才需要定義
2. 不同數據類型與零值比較的標準寫法
為什么要規范比較寫法?
不同數據類型與零值比較時,寫法不當可能導致邏輯錯誤或程序崩潰。
標準比較方式
bool類型:直接判斷真假
bool is_valid = true;// 正確寫法:直接使用布爾值
if (is_valid) {// 條件為真時執行printf("數據有效\n");
} else {// 條件為假時執行printf("數據無效\n");
}
int類型:與0比較
int count = 10;// 正確寫法:將常量放在左邊(防御性編程)
if (0 != count) {// count不等于0時執行printf("計數值為:%d\n", count);
} else {// count等于0時執行printf("計數為零\n");
}
指針類型:與NULL比較
int* ptr = nullptr;// 正確寫法:將NULL放在左邊
if (NULL == ptr) {// 指針為空時執行printf("指針為空\n");
} else {// 指針不為空時執行printf("指針指向的值:%d\n", *ptr);
}
float類型:使用精度范圍比較
float value = 0.0001f;
const float EPSILON = 1e-6f; // 定義精度閾值// 正確寫法:判斷是否在精度范圍內
if ((value >= -EPSILON) && (value <= EPSILON)) {// 認為等于零printf("浮點數接近零\n");
} else {// 不等于零printf("浮點數值:%f\n", value);
}
防御性編程技巧
將常量放在比較運算符左邊的好處:
// 錯誤示例:容易寫錯
if (count = 0) { // 誤將==寫成=,編譯通過但邏輯錯誤// 永遠不會執行
}// 正確示例:編譯器會報錯
if (0 = count) { // 編譯錯誤,無法給常量賦值// 編譯器直接報錯,避免邏輯錯誤
}
3. sizeof與strlen:編譯時計算 vs 運行時計算
本質區別分析
sizeof:編譯時操作符
// sizeof是操作符,不是函數
int arr[10];
char str[] = "Hello";// 編譯時就確定結果
size_t arr_size = sizeof(arr); // 結果:40字節(10個int)
size_t str_size = sizeof(str); // 結果:6字節(包含'\0')
size_t int_size = sizeof(int); // 結果:4字節(平臺相關)
strlen:運行時庫函數
#include <string.h>char str[] = "Hello";
char* ptr = "World";// 運行時計算字符串長度
size_t len1 = strlen(str); // 結果:5(不包含'\0')
size_t len2 = strlen(ptr); // 結果:5(不包含'\0')
數組退化現象
void test_array(char arr[]) {// 數組作為參數時退化為指針printf("sizeof(arr) = %zu\n", sizeof(arr)); // 輸出指針大小(8字節)printf("strlen(arr) = %zu\n", strlen(arr)); // 輸出字符串長度
}int main() {char str[] = "Hello";printf("sizeof(str) = %zu\n", sizeof(str)); // 輸出:6printf("strlen(str) = %zu\n", strlen(str)); // 輸出:5test_array(str); // 數組退化為指針return 0;
}
性能對比
- sizeof:編譯時確定,零運行時開銷
- strlen:需要遍歷字符串,時間復雜度O(n)
4. static關鍵字:C語言 vs C++的功能擴展
C語言中的static
// 1. 局部靜態變量:函數調用間保持值
void counter() {static int count = 0; // 只初始化一次count++;printf("調用次數:%d\n", count);
}// 2. 全局靜態變量:限制作用域在當前文件
static int file_global = 100; // 只在當前文件可見// 3. 靜態函數:限制函數作用域在當前文件
static void helper_function() {printf("這是一個靜態函數\n");
}
C++中的static擴展功能
class MyClass {
private:static int class_count; // 靜態成員變量:所有對象共享int instance_id; // 實例成員變量:每個對象獨有public:MyClass() {instance_id = ++class_count; // 每創建一個對象,計數加1}// 靜態成員函數:不依賴具體對象實例static int get_count() {return class_count; // 只能訪問靜態成員// return instance_id; // 錯誤:無法訪問非靜態成員}
};// 靜態成員變量必須在類外定義
int MyClass::class_count = 0;// 使用示例
int main() {MyClass obj1, obj2, obj3;printf("創建的對象數量:%d\n", MyClass::get_count()); // 輸出:3return 0;
}
靜態變量的內存特點
- 存儲在靜態存儲區,程序結束時才銷毀
- 只初始化一次,后續調用保持上次的值
- 可以在不同函數調用間傳遞信息
5. malloc vs new:C風格 vs C++風格的內存管理
基本區別對比
malloc/free:C語言風格
#include <stdlib.h>// 分配內存
int* ptr = (int*)malloc(sizeof(int) * 10); // 分配10個int的空間
if (ptr == NULL) {printf("內存分配失敗\n");return -1;
}// 使用內存
for (int i = 0; i < 10; i++) {ptr[i] = i * i; // 需要手動初始化
}// 釋放內存
free(ptr); // 只釋放內存,不調用析構函數
ptr = NULL; // 防止懸空指針
new/delete:C++風格
// 分配單個對象
int* single_ptr = new int(42); // 分配并初始化
delete single_ptr; // 釋放單個對象// 分配數組
int* array_ptr = new int[10]; // 分配數組
delete[] array_ptr; // 釋放數組(注意使用delete[])// 分配類對象
class Person {
public:Person(const char* name) {printf("構造函數:創建 %s\n", name);}~Person() {printf("析構函數:銷毀對象\n");}
};Person* person = new Person("張三"); // 自動調用構造函數
delete person; // 自動調用析構函數
構造函數與析構函數的區別
class TestClass {
public:TestClass() { printf("對象被構造\n"); data = new int[100]; // 分配資源}~TestClass() { printf("對象被析構\n"); delete[] data; // 釋放資源}
private:int* data;
};// malloc方式:不會調用構造/析構函數
TestClass* obj1 = (TestClass*)malloc(sizeof(TestClass));
// 沒有輸出"對象被構造"
free(obj1); // 沒有輸出"對象被析構",可能導致內存泄漏// new方式:自動調用構造/析構函數
TestClass* obj2 = new TestClass(); // 輸出"對象被構造"
delete obj2; // 輸出"對象被析構"
混用的危險性
// 錯誤示例:不要混用
int* ptr1 = (int*)malloc(sizeof(int));
delete ptr1; // 錯誤:malloc的內存不能用delete釋放int* ptr2 = new int;
free(ptr2); // 錯誤:new的內存不能用free釋放
6. 宏定義的陷阱:副作用問題
標準MIN宏的實現
#define MIN(a, b) ((a) <= (b) ? (a) : (b))// 基本使用
int x = 5, y = 3;
int min_val = MIN(x, y); // 結果:3
宏的副作用問題
#define MIN(a, b) ((a) <= (b) ? (a) : (b))int main() {int x = 5;int* p = &x;// 危險的調用方式int result = MIN(++(*p), 10);// 宏展開后變成:// int result = ((++(*p)) <= (10) ? (++(*p)) : (10));// ++(*p)被執行了兩次!printf("x = %d\n", x); // x可能是7而不是6printf("result = %d\n", result);return 0;
}
更安全的實現方式
// 使用內聯函數替代宏(C++推薦)
inline int min_safe(int a, int b) {return (a <= b) ? a : b;
}// 或者使用臨時變量的宏(C語言)
#define MIN_SAFE(a, b) ({ \typeof(a) _a = (a); \typeof(b) _b = (b); \(_a <= _b) ? _a : _b; \
})
7. volatile關鍵字:處理不可預測的變量變化
volatile的作用機制
volatile告訴編譯器:這個變量可能被程序外部因素修改,不要進行優化。
典型應用場景
中斷服務程序
volatile int interrupt_flag = 0; // 中斷標志// 中斷服務函數
void interrupt_handler() {interrupt_flag = 1; // 中斷發生時設置標志
}// 主程序
int main() {while (interrupt_flag == 0) {// 等待中斷發生// 如果沒有volatile,編譯器可能優化成死循環}printf("中斷已處理\n");return 0;
}
硬件寄存器訪問
// 硬件寄存器地址
volatile unsigned int* const HARDWARE_REG = (unsigned int*)0x40000000;void read_sensor() {unsigned int value = *HARDWARE_REG; // 每次都從硬件讀取printf("傳感器值:%u\n", value);
}
多線程共享變量
volatile bool thread_running = true;void worker_thread() {while (thread_running) { // 確保每次都檢查最新值// 執行工作printf("線程運行中...\n");sleep(1);}printf("線程退出\n");
}void stop_thread() {thread_running = false; // 通知線程停止
}
volatile指針的不同含義
int value = 100;// 指向volatile變量的指針
volatile int* ptr1 = &value; // 指向的內容是volatile的
*ptr1 = 200; // 每次寫入都不會被優化// volatile指針指向普通變量
int* volatile ptr2 = &value; // 指針本身是volatile的
ptr2 = &another_value; // 指針的修改不會被優化// volatile指針指向volatile變量
volatile int* volatile ptr3 = &value; // 指針和內容都是volatile的
8. 數組名與數組地址:a vs &a的本質區別
概念解析
a
:數組名,表示數組首元素的地址&a
:數組的地址,表示整個數組的地址
代碼分析實例
#include <stdio.h>int main() {int a[5] = {1, 2, 3, 4, 5};printf("a = %p\n", a); // 數組首元素地址printf("&a = %p\n", &a); // 整個數組的地址printf("a+1 = %p\n", a+1); // 下一個元素地址(+4字節)printf("&a+1 = %p\n", &a+1); // 下一個數組地址(+20字節)// 關鍵代碼分析int* ptr = (int*)(&a + 1); // 指向數組后面的位置printf("*(a+1) = %d\n", *(a+1)); // 輸出:2(第二個元素)printf("*(ptr-1) = %d\n", *(ptr-1)); // 輸出:5(最后一個元素)return 0;
}
內存布局圖解
內存地址: 1000 1004 1008 1012 1016 1020
數組內容: [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ]↑ ↑ ↑a | &a+1&a |a+4或&a[4]
指針運算的區別
int a[5] = {1, 2, 3, 4, 5};// a+1:移動一個int的大小(4字節)
int* p1 = a + 1; // 指向a[1]// &a+1:移動整個數組的大小(20字節)
int* p2 = (int*)(&a + 1); // 指向數組后面的位置// 驗證
printf("p1指向的值:%d\n", *p1); // 輸出:2
printf("p2-1指向的值:%d\n", *(p2-1)); // 輸出:5
9. C/C++程序內存布局:五大存儲區域詳解
內存分區概述
C/C++程序運行時,內存被劃分為5個主要區域,每個區域有不同的特點和用途。
1. 程序代碼區(Text Segment)
// 存儲編譯后的機器代碼
void function1() {printf("這個函數的代碼存儲在代碼區\n");
}int main() {printf("main函數的代碼也在代碼區\n");return 0;
}
特點:只讀、共享、程序加載時確定大小
2. 全局/靜態存儲區(Data Segment)
// 已初始化的全局變量
int global_var = 100; // 存儲在已初始化數據區// 未初始化的全局變量
int uninitialized_global; // 存儲在BSS區(自動初始化為0)// 靜態變量
static int static_var = 200; // 存儲在已初始化數據區void function() {static int local_static; // 存儲在BSS區
}
特點:程序運行期間一直存在、自動初始化為0(BSS區)
3. 棧區(Stack)
void stack_demo() {int local_var = 10; // 局部變量,存儲在棧上char buffer[1024]; // 局部數組,存儲在棧上printf("local_var地址:%p\n", &local_var);printf("buffer地址:%p\n", buffer);// 函數結束時,這些變量自動銷毀
}void recursive_function(int n) {int local = n; // 每次遞歸調用都在棧上分配if (n > 0) {recursive_function(n - 1);}
}
特點:
- 自動管理(函數結束自動釋放)
- 訪問速度快
- 大小有限(通常幾MB)
- 后進先出(LIFO)
4. 堆區(Heap)
void heap_demo() {// 動態分配內存int* heap_ptr = (int*)malloc(sizeof(int) * 100);if (heap_ptr == NULL) {printf("內存分配失敗\n");return;}// 使用堆內存for (int i = 0; i < 100; i++) {heap_ptr[i] = i;}// 必須手動釋放free(heap_ptr);heap_ptr = NULL; // 防止懸空指針
}void cpp_heap_demo() {// C++風格的堆內存管理int* ptr = new int[100]; // 分配// 使用內存...delete[] ptr; // 釋放
}
特點:
- 手動管理(程序員負責分配和釋放)
- 大小靈活
- 訪問速度相對較慢
- 容易產生內存泄漏和碎片
5. 文字常量區(String Literal Pool)
void string_demo() {char* str1 = "Hello World"; // 字符串存儲在常量區char* str2 = "Hello World"; // 可能與str1指向同一地址char arr[] = "Hello World"; // 字符串復制到棧上printf("str1地址:%p\n", str1);printf("str2地址:%p\n", str2);printf("arr地址:%p\n", arr);// str1[0] = 'h'; // 錯誤:不能修改常量區內容arr[0] = 'h'; // 正確:可以修改棧上的副本
}
內存布局示意圖
高地址
┌─────────────────┐
│ 棧區 │ ← 向下增長
│ (局部變量) │
├─────────────────┤
│ ↓ │
│ │
│ ↑ │
├─────────────────┤
│ 堆區 │ ← 向上增長
│ (動態分配) │
├─────────────────┤
│ 未初始化數據 │
│ (BSS段) │
├─────────────────┤
│ 已初始化數據 │
│ (Data段) │
├─────────────────┤
│ 文字常量區 │
├─────────────────┤
│ 程序代碼區 │
└─────────────────┘
低地址
10. 字符串操作函數對比:strcpy、sprintf、memcpy
功能定位分析
strcpy:字符串到字符串的復制
#include <string.h>void strcpy_demo() {char source[] = "Hello World";char destination[20];// 復制字符串(包括結尾的'\0')strcpy(destination, source);printf("復制結果:%s\n", destination);// 注意:不檢查目標緩沖區大小,可能溢出// 更安全的版本:strncpystrncpy(destination, source, sizeof(destination) - 1);destination[sizeof(destination) - 1] = '\0'; // 確保以'\0'結尾
}
sprintf:格式化輸出到字符串
#include <stdio.h>void sprintf_demo() {char buffer[100];int age = 25;float height = 175.5f;char name[] = "張三";// 將多種數據類型格式化為字符串sprintf(buffer, "姓名:%s,年齡:%d,身高:%.1f厘米", name, age, height);printf("格式化結果:%s\n", buffer);// 更安全的版本:snprintfsnprintf(buffer, sizeof(buffer), "安全的格式化:%s", name);
}
memcpy:內存塊到內存塊的復制
#include <string.h>void memcpy_demo() {// 復制整數數組int source[] = {1, 2, 3, 4, 5};int destination[5];memcpy(destination, source, sizeof(source));// 復制結構體struct Person {char name[20];int age;};struct Person p1 = {"李四", 30};struct Person p2;memcpy(&p2, &p1, sizeof(struct Person));printf("復制的結構體:%s, %d\n", p2.name, p2.age);// 復制部分內存char str1[] = "Hello World";char str2[20];memcpy(str2, str1, 5); // 只復制前5個字符str2[5] = '\0'; // 手動添加結束符printf("部分復制:%s\n", str2); // 輸出:Hello
}
性能對比測試
#include <time.h>void performance_test() {const int TEST_SIZE = 1000000;char source[1000];char destination[1000];clock_t start, end;// 初始化源數據memset(source, 'A', sizeof(source) - 1);source[sizeof(source) - 1] = '\0';// 測試memcpy性能start = clock();for (int i = 0; i < TEST_SIZE; i++) {memcpy(destination, source, sizeof(source));}end = clock();printf("memcpy耗時:%f秒\n", (double)(end - start) / CLOCKS_PER_SEC);// 測試strcpy性能start = clock();for (int i = 0; i < TEST_SIZE; i++) {strcpy(destination, source);}end = clock();printf("strcpy耗時:%f秒\n", (double)(end - start) / CLOCKS_PER_SEC);// 測試sprintf性能start = clock();for (int i = 0; i < TEST_SIZE; i++) {sprintf(destination, "%s", source);}end = clock();printf("sprintf耗時:%f秒\n", (double)(end - start) / CLOCKS_PER_SEC);
}
使用場景選擇指南
- strcpy:純字符串復制,需要自動處理’\0’結尾
- sprintf:需要格式化多種數據類型為字符串
- memcpy:原始內存復制,最高效,適合大塊數據
11. 直接內存操作:指定地址賦值技術
基本概念
在嵌入式開發或系統編程中,經常需要直接操作特定內存地址的數據。
實現方法
void memory_operation_demo() {// 將整數值0xaa66寫入地址0x67a9int* ptr; // 聲明整型指針ptr = (int*)0x67a9; // 將地址強制轉換為整型指針*ptr = 0xaa66; // 向該地址寫入數據// 讀取驗證printf("地址0x67a9的值:0x%x\n", *ptr);
}
實際應用場景
硬件寄存器操作
// 定義硬件寄存器地址
#define GPIO_BASE_ADDR 0x40020000
#define GPIO_OUTPUT_REG (GPIO_BASE_ADDR + 0x14)
#define GPIO_INPUT_REG (GPIO_BASE_ADDR + 0x10)void gpio_control() {// 控制GPIO輸出volatile unsigned int* gpio_output = (volatile unsigned int*)GPIO_OUTPUT_REG;*gpio_output = 0xFF; // 設置所有引腳為高電平// 讀取GPIO輸入volatile unsigned int* gpio_input = (volatile unsigned int*)GPIO_INPUT_REG;unsigned int input_value = *gpio_input;printf("GPIO輸入值:0x%x\n", input_value);
}
內存映射文件操作
#include <sys/mman.h>
#include <fcntl.h>void memory_mapped_file() {int fd = open("data.bin", O_RDWR);if (fd == -1) return;// 將文件映射到內存void* mapped_addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (mapped_addr != MAP_FAILED) {// 直接操作內存就是操作文件int* data_ptr = (int*)mapped_addr;*data_ptr = 0x12345678; // 寫入數據到文件// 解除映射munmap(mapped_addr, 1024);}close(fd);
}
安全注意事項
void safe_memory_access() {// 1. 檢查地址有效性void* addr = (void*)0x67a9;if (addr == NULL) {printf("無效地址\n");return;}// 2. 使用volatile防止編譯器優化volatile int* ptr = (volatile int*)addr;// 3. 異常處理(在支持的系統上)try {*ptr = 0xaa66;} catch (...) {printf("內存訪問異常\n");}
}
12. 面向對象三大特征深度解析
1. 封裝性(Encapsulation):數據隱藏與接口設計
基本概念
封裝是將數據和操作數據的方法組合在一起,通過訪問控制來隱藏內部實現細節。
class BankAccount {
private:double balance; // 私有數據:外部無法直接訪問string account_number; // 私有數據:賬戶安全信息public:// 公有接口:提供安全的訪問方式BankAccount(string acc_num, double initial_balance) {account_number = acc_num;balance = initial_balance;}// 存款操作:控制數據修改方式bool deposit(double amount) {if (amount > 0) {balance += amount;return true;}return false; // 拒絕無效操作}// 取款操作:包含業務邏輯驗證bool withdraw(double amount) {if (amount > 0 && amount <= balance) {balance -= amount;return true;}return false; // 余額不足或金額無效}// 查詢余額:只讀訪問double get_balance() const {return balance;}
};
訪問控制級別
class AccessDemo {
private:int private_data; // 只有類內部可以訪問protected:int protected_data; // 類內部和子類可以訪問public:int public_data; // 任何地方都可以訪問void demo_access() {private_data = 1; // 正確:類內部訪問protected_data = 2; // 正確:類內部訪問public_data = 3; // 正確:類內部訪問}
};class DerivedClass : public AccessDemo {
public:void test_access() {// private_data = 1; // 錯誤:無法訪問私有成員protected_data = 2; // 正確:子類可以訪問保護成員public_data = 3; // 正確:公有成員任何地方可訪問}
};
2. 繼承性(Inheritance):代碼復用與層次結構
基本繼承概念
// 基類:動物
class Animal {
protected:string name;int age;public:Animal(string n, int a) : name(n), age(a) {}// 虛函數:允許子類重寫virtual void make_sound() {cout << name << "發出聲音" << endl;}// 普通成員函數void eat() {cout << name << "正在吃東西" << endl;}virtual ~Animal() {} // 虛析構函數
};// 派生類:狗
class Dog : public Animal {
private:string breed; // 狗特有的屬性public:Dog(string n, int a, string b) : Animal(n, a), breed(b) {}// 重寫基類的虛函數virtual void make_sound() override {cout << name << "汪汪叫" << endl;}// 狗特有的行為void wag_tail() {cout << name << "搖尾巴" << endl;}
};// 派生類:貓
class Cat : public Animal {
public:Cat(string n, int a) : Animal(n, a) {}virtual void make_sound() override {cout << name << "喵喵叫" << endl;}void climb_tree() {cout << name << "爬樹" << endl;}
};
繼承的三種方式
class Base {
public: int pub_member;
protected: int prot_member;
private: int priv_member;
};// 公有繼承:保持訪問級別
class PublicDerived : public Base {// pub_member -> public// prot_member -> protected// priv_member -> 不可訪問
};// 保護繼承:公有成員變為保護
class ProtectedDerived : protected Base {// pub_member -> protected// prot_member -> protected// priv_member -> 不可訪問
};// 私有繼承:所有成員變為私有
class PrivateDerived : private Base {// pub_member -> private// prot_member -> private// priv_member -> 不可訪問
};
3. 多態性(Polymorphism):一個接口多種實現
運行時多態(動態多態)
void demonstrate_polymorphism() {// 創建不同類型的動物對象Animal* animals[] = {new Dog("旺財", 3, "金毛"),new Cat("咪咪", 2),new Dog("小黑", 5, "土狗")};// 多態調用:同一接口,不同實現for (int i = 0; i < 3; i++) {animals[i]->make_sound(); // 根據實際對象類型調用相應函數animals[i]->eat(); // 調用基類函數}// 清理內存for (int i = 0; i < 3; i++) {delete animals[i];}
}
編譯時多態(靜態多態)
class Calculator {
public:// 函數重載:同名函數,不同參數int add(int a, int b) {return a + b;}double add(double a, double b) {return a + b;}int add(int a, int b, int c) {return a + b + c;}
};// 模板實現編譯時多態
template<typename T>
T generic_add(T a, T b) {return a + b;
}void polymorphism_demo() {Calculator calc;// 編譯器根據參數類型選擇合適的函數cout << calc.add(1, 2) << endl; // 調用int版本cout << calc.add(1.5, 2.5) << endl; // 調用double版本cout << calc.add(1, 2, 3) << endl; // 調用三參數版本// 模板多態cout << generic_add(10, 20) << endl; // int版本cout << generic_add(1.1, 2.2) << endl; // double版本
}
13. C++空類的隱式成員函數
編譯器自動生成的六個函數
當定義一個空類時,編譯器會自動生成以下成員函數:
class EmptyClass {// 編譯器自動生成以下函數:// 1. 默認構造函數// EmptyClass() {}// 2. 拷貝構造函數// EmptyClass(const EmptyClass& other) {}// 3. 析構函數// ~EmptyClass() {}// 4. 賦值運算符// EmptyClass& operator=(const EmptyClass& other) { return *this; }// 5. 取址運算符// EmptyClass* operator&() { return this; }// 6. const取址運算符// const EmptyClass* operator&() const { return this; }
};
實際驗證示例
void test_empty_class() {EmptyClass obj1; // 調用默認構造函數EmptyClass obj2(obj1); // 調用拷貝構造函數EmptyClass obj3;obj3 = obj1; // 調用賦值運算符EmptyClass* ptr1 = &obj1; // 調用取址運算符const EmptyClass* ptr2 = &obj1; // 調用const取址運算符// 析構函數在對象生命周期結束時自動調用
}
何時需要自定義這些函數
class ResourceClass {
private:int* data;size_t size;public:// 必須自定義構造函數ResourceClass(size_t s) : size(s) {data = new int[size];cout << "構造函數:分配了 " << size << " 個整數的內存" << endl;}// 必須自定義拷貝構造函數(深拷貝)ResourceClass(const ResourceClass& other) : size(other.size) {data = new int[size];memcpy(data, other.data, size * sizeof(int));cout << "拷貝構造函數:深拷貝" << endl;}// 必須自定義賦值運算符ResourceClass& operator=(const ResourceClass& other) {if (this != &other) { // 防止自賦值delete[] data; // 釋放原有資源size = other.size;data = new int[size];memcpy(data, other.data, size * sizeof(int));cout << "賦值運算符:深拷貝" << endl;}return *this;}// 必須自定義析構函數~ResourceClass() {delete[] data;cout << "析構函數:釋放內存" << endl;}
};
14. 拷貝構造函數 vs 賦值運算符
本質區別分析
調用時機不同
class TestClass {
public:int value;TestClass(int v) : value(v) {cout << "構造函數:創建對象,值=" << value << endl;}TestClass(const TestClass& other) : value(other.value) {cout << "拷貝構造函數:從現有對象創建新對象,值=" << value << endl;}TestClass& operator=(const TestClass& other) {cout << "賦值運算符:修改現有對象,從" << value << "改為" << other.value << endl;value = other.value;return *this;}
};void copy_vs_assignment_demo() {TestClass obj1(10); // 調用構造函數TestClass obj2(obj1); // 調用拷貝構造函數(創建新對象)TestClass obj3 = obj1; // 調用拷貝構造函數(不是賦值!)TestClass obj4(20); // 調用構造函數obj4 = obj1; // 調用賦值運算符(修改現有對象)
}
內存管理的區別
class StringClass {
private:char* str;size_t length;public:StringClass(const char* s) {length = strlen(s);str = new char[length + 1];strcpy(str, s);cout << "構造:" << str << endl;}// 拷貝構造函數:為新對象分配內存StringClass(const StringClass& other) {length = other.length;str = new char[length + 1]; // 分配新內存strcpy(str, other.str);cout << "拷貝構造:" << str << endl;}// 賦值運算符:需要處理現有內存StringClass& operator=(const StringClass& other) {if (this != &other) { // 防止自賦值delete[] str; // 釋放原有內存length = other.length;str = new char[length + 1]; // 分配新內存strcpy(str, other.str);cout << "賦值:" << str << endl;}return *this;}~StringClass() {cout << "析構:" << str << endl;delete[] str;}
};
自賦值問題的處理
StringClass& operator=(const StringClass& other) {// 方法1:檢查自賦值if (this == &other) {return *this;}// 方法2:異常安全的實現char* temp = new char[other.length + 1]; // 先分配新內存strcpy(temp, other.str);delete[] str; // 釋放原內存str = temp; // 指向新內存length = other.length;return *this;
}
15. 設計不可繼承的類
使用模板和友元的方法
template <typename T>
class NonInheritable {friend T; // 只有T類型可以訪問私有構造函數private:NonInheritable() {} // 私有構造函數~NonInheritable() {} // 私有析構函數
};// 可以實例化的類
class FinalClass : virtual public NonInheritable<FinalClass> {
public:FinalClass() {} // 可以調用基類的私有構造函數(因為是友元)~FinalClass() {}
};// 嘗試繼承會失敗的類
class AttemptInherit : public FinalClass {
public:AttemptInherit() {} // 編譯錯誤:無法訪問NonInheritable的構造函數~AttemptInherit() {}
};void test_inheritance() {FinalClass obj; // 正確:可以創建對象// AttemptInherit obj2; // 編譯錯誤:無法繼承
}
C++11的final關鍵字(推薦方法)
// 現代C++的簡單方法
class FinalClass final { // final關鍵字阻止繼承
public:FinalClass() {cout << "FinalClass構造函數" << endl;}void do_something() {cout << "執行某些操作" << endl;}
};// 編譯錯誤:無法繼承final類
// class DerivedClass : public FinalClass {};void modern_final_demo() {FinalClass obj;obj.do_something();
}
16. 虛函數表機制深度解析
虛函數表的工作原理
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func3() { cout << "Base::func3" << endl; } // 非虛函數
};class Derived : public Base {
public:virtual void func1() override { cout << "Derived::func1" << endl; }virtual void func4() { cout << "Derived::func4" << endl; }
};
內存布局分析
Base對象內存布局:
┌─────────────────┐
│ vptr (8字節) │ ──→ Base虛函數表
└─────────────────┘ ┌─────────────────┐│ &Base::func1 ││ &Base::func2 │└─────────────────┘Derived對象內存布局:
┌─────────────────┐
│ vptr (8字節) │ ──→ Derived虛函數表
└─────────────────┘ ┌─────────────────┐│ &Derived::func1 │ (重寫)│ &Base::func2 │ (繼承)│ &Derived::func4 │ (新增)└─────────────────┘
虛函數調用過程演示
void virtual_function_demo() {Base* ptr1 = new Base();Base* ptr2 = new Derived();// 虛函數調用:通過虛函數表ptr1->func1(); // 1. 獲取ptr1的vptr// 2. 在虛函數表中查找func1// 3. 調用Base::func1ptr2->func1(); // 1. 獲取ptr2的vptr// 2. 在虛函數表中查找func1// 3. 調用Derived::func1// 非虛函數調用:編譯時確定ptr1->func3(); // 直接調用Base::func3ptr2->func3(); // 直接調用Base::func3delete ptr1;delete ptr2;
}
訪問虛函數表的技巧(僅用于理解原理)
void access_vtable() {Derived obj;// 獲取對象的虛函數表指針void** vtable = *(void***)&obj;// 調用虛函數表中的函數typedef void(*FuncPtr)();for (int i = 0; i < 3; i++) {FuncPtr func = (FuncPtr)vtable[i];cout << "調用虛函數表第" << i << "個函數:";// 注意:這種方式調用需要傳遞this指針,實際實現更復雜}
}
17. 函數重寫、重載、隱藏的區別
重載(Overloading):同一作用域內的函數多態
class Calculator {
public:// 函數重載:函數名相同,參數不同int add(int a, int b) {cout << "兩個整數相加" << endl;return a + b;}double add(double a, double b) {cout << "兩個浮點數相加" << endl;return a + b;}int add(int a, int b, int c) {cout << "三個整數相加" << endl;return a + b + c;}// 編譯錯誤:僅返回類型不同不能重載// double add(int a, int b) { return a + b; }
};
重寫(Override):繼承關系中的虛函數替換
class Shape {
public:virtual double area() { // 虛函數cout << "Shape::area()" << endl;return 0.0;}virtual void draw() { // 虛函數cout << "Shape::draw()" << endl;}
};class Circle : public Shape {
private:double radius;public:Circle(double r) : radius(r) {}// 重寫基類虛函數virtual double area() override {cout << "Circle::area()" << endl;return 3.14159 * radius * radius;}virtual void draw() override {cout << "Circle::draw()" << endl;}
};
隱藏(Hiding):派生類函數隱藏基類同名函數
class Base {
public:void func() {cout << "Base::func()" << endl;}void func(int x) {cout << "Base::func(int)" << endl;}virtual void virtual_func() {cout << "Base::virtual_func()" << endl;}
};class Derived : public Base {
public:// 隱藏基類的所有同名函數(包括重載版本)void func(double x) {cout << "Derived::func(double)" << endl;}// 隱藏基類虛函數(參數不同,不是重寫)void virtual_func(int x) {cout << "Derived::virtual_func(int)" << endl;}
};void hiding_demo() {Derived obj;obj.func(1.5); // 調用Derived::func(double)// obj.func(); // 編譯錯誤:Base::func()被隱藏// obj.func(1); // 編譯錯誤:Base::func(int)被隱藏// 使用作用域解析符訪問被隱藏的函數obj.Base::func(); // 調用Base::func()obj.Base::func(1); // 調用Base::func(int)
}
三者對比總結表
特征 | 重載(Overloading) | 重寫(Override) | 隱藏(Hiding) |
---|---|---|---|
作用域 | 同一類中 | 基類和派生類 | 基類和派生類 |
函數名 | 相同 | 相同 | 相同 |
參數列表 | 必須不同 | 必須相同 | 可同可不同 |
virtual關鍵字 | 可有可無 | 基類必須有 | 可有可無 |
綁定時機 | 編譯時 | 運行時 | 編譯時 |
多態性 | 靜態多態 | 動態多態 | 無多態 |
18. 多態實現原理:虛函數表詳解
虛函數表的創建時機
class Animal {
public:Animal() {cout << "Animal構造函數:虛函數表指針已設置" << endl;// 此時vptr指向Animal的虛函數表}virtual void speak() {cout << "Animal發出聲音" << endl;}virtual ~Animal() {cout << "Animal析構函數" << endl;}
};class Dog : public Animal {
public:Dog() {cout << "Dog構造函數:虛函數表指針已更新" << endl;// 此時vptr指向Dog的虛函數表}virtual void speak() override {cout << "狗汪汪叫" << endl;}virtual ~Dog() {cout << "Dog析構函數" << endl;}
};
動態綁定的實現過程
void polymorphism_mechanism() {cout << "=== 多態機制演示 ===" << endl;Animal* animals[] = {new Animal(),new Dog()};for (int i = 0; i < 2; i++) {cout << "\n調用第" << i+1 << "個對象的speak()方法:" << endl;// 編譯器生成的代碼等價于:// 1. 獲取對象的虛函數表指針// 2. 在虛函數表中查找speak函數的地址// 3. 調用該地址對應的函數animals[i]->speak();}// 清理內存for (int i = 0; i < 2; i++) {delete animals[i]; // 虛析構函數確保正確析構}
}
虛函數的性能開銷
class PerformanceTest {
public:// 普通函數調用void normal_function() {// 直接函數調用,無額外開銷}// 虛函數調用virtual void virtual_function() {// 需要通過虛函數表間接調用,有輕微開銷}
};void performance_comparison() {const int ITERATIONS = 10000000;PerformanceTest obj;PerformanceTest* ptr = &obj;// 測試普通函數調用性能auto start = chrono::high_resolution_clock::now();for (int i = 0; i < ITERATIONS; i++) {obj.normal_function();}auto end = chrono::high_resolution_clock::now();auto normal_time = chrono::duration_cast<chrono::microseconds>(end - start);// 測試虛函數調用性能start = chrono::high_resolution_clock::now();for (int i = 0; i < ITERATIONS; i++) {ptr->virtual_function();}end = chrono::high_resolution_clock::now();auto virtual_time = chrono::duration_cast<chrono::microseconds>(end - start);cout << "普通函數調用時間:" << normal_time.count() << "微秒" << endl;cout << "虛函數調用時間:" << virtual_time.count() << "微秒" << endl;
}
19. 數組 vs 鏈表:數據結構選擇指南
內存布局對比
數組的連續存儲
void array_memory_layout() {int arr[5] = {10, 20, 30, 40, 50};cout << "數組內存布局:" << endl;for (int i = 0; i < 5; i++) {cout << "arr[" << i << "] = " << arr[i] << ", 地址:" << &arr[i] << endl;}// 地址連續,相鄰元素地址差為sizeof(int)
}
鏈表的分散存儲
struct ListNode {int data; // 數據域ListNode* next; // 指針域ListNode(int val) : data(val), next(nullptr) {}
};class LinkedList {
private:ListNode* head;public:LinkedList() : head(nullptr) {}void insert(int val) {ListNode* new_node = new ListNode(val); // 動態分配,地址不連續new_node->next = head;head = new_node;}void print_addresses() {cout << "鏈表節點地址:" << endl;ListNode* current = head;int index = 0;while (current) {cout << "節點" << index << ": 數據=" << current->data << ", 地址=" << current << endl;current = current->next;index++;}}~LinkedList() {while (head) {ListNode* temp = head;head = head->next;delete temp;}}
};
操作性能對比
隨機訪問性能
void random_access_test() {const int SIZE = 100000;// 數組隨機訪問:O(1)vector<int> arr(SIZE);for (int i = 0; i < SIZE; i++) {arr[i] = i;}auto start = chrono::high_resolution_clock::now();for (int i = 0; i < 10000; i++) {int index = rand() % SIZE;int value = arr[index]; // 直接通過索引訪問}auto end = chrono::high_resolution_clock::now();auto array_time = chrono::duration_cast<chrono::microseconds>(end - start);// 鏈表隨機訪問:O(n)LinkedList list;for (int i = 0; i < SIZE; i++) {list.insert(i);}start = chrono::high_resolution_clock::now();for (int i = 0; i < 100; i++) { // 減少測試次數,因為鏈表訪問很慢int index = rand() % SIZE;// 需要從頭遍歷到指定位置ListNode* current = list.head;for (int j = 0; j < index && current; j++) {current = current->next;}}end = chrono::high_resolution_clock::now();auto list_time = chrono::duration_cast<chrono::microseconds>(end - start);cout << "數組隨機訪問時間:" << array_time.count() << "微秒" << endl;cout << "鏈表隨機訪問時間:" << list_time.count() << "微秒" << endl;
}
C/C++面試核心知識點詳解
1. 變量的聲明與定義:內存分配的本質區別
核心概念
在C/C++中,變量的聲明和定義是兩個完全不同的概念:
- 聲明(Declaration):告訴編譯器變量的名稱和類型,但不分配內存空間
- 定義(Definition):不僅聲明變量,還為其分配實際的內存空間
實際應用示例
// 變量聲明:使用extern關鍵字,不分配內存
extern int global_var; // 聲明一個整型變量,告訴編譯器這個變量在別處定義// 變量定義:分配內存空間
int global_var = 100; // 定義變量并分配內存,可以初始化// 函數聲明
int add(int a, int b); // 函數聲明,不包含函數體// 函數定義
int add(int a, int b) { // 函數定義,包含完整實現return a + b;
}
重要規則
- 一個變量可以在多個地方聲明,但只能在一個地方定義
- 聲明可以重復,定義不能重復
- 使用變量前必須先聲明,使用時才需要定義
2. 不同數據類型與零值比較的標準寫法
為什么要規范比較寫法?
不同數據類型與零值比較時,寫法不當可能導致邏輯錯誤或程序崩潰。
標準比較方式
bool類型:直接判斷真假
bool is_valid = true;// 正確寫法:直接使用布爾值
if (is_valid) {// 條件為真時執行printf("數據有效\n");
} else {// 條件為假時執行printf("數據無效\n");
}
int類型:與0比較
int count = 10;// 正確寫法:將常量放在左邊(防御性編程)
if (0 != count) {// count不等于0時執行printf("計數值為:%d\n", count);
} else {// count等于0時執行printf("計數為零\n");
}
指針類型:與NULL比較
int* ptr = nullptr;// 正確寫法:將NULL放在左邊
if (NULL == ptr) {// 指針為空時執行printf("指針為空\n");
} else {// 指針不為空時執行printf("指針指向的值:%d\n", *ptr);
}
float類型:使用精度范圍比較
float value = 0.0001f;
const float EPSILON = 1e-6f; // 定義精度閾值// 正確寫法:判斷是否在精度范圍內
if ((value >= -EPSILON) && (value <= EPSILON)) {// 認為等于零printf("浮點數接近零\n");
} else {// 不等于零printf("浮點數值:%f\n", value);
}
防御性編程技巧
將常量放在比較運算符左邊的好處:
// 錯誤示例:容易寫錯
if (count = 0) { // 誤將==寫成=,編譯通過但邏輯錯誤// 永遠不會執行
}// 正確示例:編譯器會報錯
if (0 = count) { // 編譯錯誤,無法給常量賦值// 編譯器直接報錯,避免邏輯錯誤
}
3. sizeof與strlen:編譯時計算 vs 運行時計算
本質區別分析
sizeof:編譯時操作符
// sizeof是操作符,不是函數
int arr[10];
char str[] = "Hello";// 編譯時就確定結果
size_t arr_size = sizeof(arr); // 結果:40字節(10個int)
size_t str_size = sizeof(str); // 結果:6字節(包含'\0')
size_t int_size = sizeof(int); // 結果:4字節(平臺相關)
strlen:運行時庫函數
#include <string.h>char str[] = "Hello";
char* ptr = "World";// 運行時計算字符串長度
size_t len1 = strlen(str); // 結果:5(不包含'\0')
size_t len2 = strlen(ptr); // 結果:5(不包含'\0')
數組退化現象
void test_array(char arr[]) {// 數組作為參數時退化為指針printf("sizeof(arr) = %zu\n", sizeof(arr)); // 輸出指針大小(8字節)printf("strlen(arr) = %zu\n", strlen(arr)); // 輸出字符串長度
}int main() {char str[] = "Hello";printf("sizeof(str) = %zu\n", sizeof(str)); // 輸出:6printf("strlen(str) = %zu\n", strlen(str)); // 輸出:5test_array(str); // 數組退化為指針return 0;
}
性能對比
- sizeof:編譯時確定,零運行時開銷
- strlen:需要遍歷字符串,時間復雜度O(n)
4. static關鍵字:C語言 vs C++的功能擴展
C語言中的static
// 1. 局部靜態變量:函數調用間保持值
void counter() {static int count = 0; // 只初始化一次count++;printf("調用次數:%d\n", count);
}// 2. 全局靜態變量:限制作用域在當前文件
static int file_global = 100; // 只在當前文件可見// 3. 靜態函數:限制函數作用域在當前文件
static void helper_function() {printf("這是一個靜態函數\n");
}
C++中的static擴展功能
class MyClass {
private:static int class_count; // 靜態成員變量:所有對象共享int instance_id; // 實例成員變量:每個對象獨有public:MyClass() {instance_id = ++class_count; // 每創建一個對象,計數加1}// 靜態成員函數:不依賴具體對象實例static int get_count() {return class_count; // 只能訪問靜態成員// return instance_id; // 錯誤:無法訪問非靜態成員}
};// 靜態成員變量必須在類外定義
int MyClass::class_count = 0;// 使用示例
int main() {MyClass obj1, obj2, obj3;printf("創建的對象數量:%d\n", MyClass::get_count()); // 輸出:3return 0;
}
靜態變量的內存特點
- 存儲在靜態存儲區,程序結束時才銷毀
- 只初始化一次,后續調用保持上次的值
- 可以在不同函數調用間傳遞信息
5. malloc vs new:C風格 vs C++風格的內存管理
基本區別對比
malloc/free:C語言風格
#include <stdlib.h>// 分配內存
int* ptr = (int*)malloc(sizeof(int) * 10); // 分配10個int的空間
if (ptr == NULL) {printf("內存分配失敗\n");return -1;
}// 使用內存
for (int i = 0; i < 10; i++) {ptr[i] = i * i; // 需要手動初始化
}// 釋放內存
free(ptr); // 只釋放內存,不調用析構函數
ptr = NULL; // 防止懸空指針
new/delete:C++風格
// 分配單個對象
int* single_ptr = new int(42); // 分配并初始化
delete single_ptr; // 釋放單個對象// 分配數組
int* array_ptr = new int[10]; // 分配數組
delete[] array_ptr; // 釋放數組(注意使用delete[])// 分配類對象
class Person {
public:Person(const char* name) {printf("構造函數:創建 %s\n", name);}~Person() {printf("析構函數:銷毀對象\n");}
};Person* person = new Person("張三"); // 自動調用構造函數
delete person; // 自動調用析構函數
構造函數與析構函數的區別
class TestClass {
public:TestClass() { printf("對象被構造\n"); data = new int[100]; // 分配資源}~TestClass() { printf("對象被析構\n"); delete[] data; // 釋放資源}
private:int* data;
};// malloc方式:不會調用構造/析構函數
TestClass* obj1 = (TestClass*)malloc(sizeof(TestClass));
// 沒有輸出"對象被構造"
free(obj1); // 沒有輸出"對象被析構",可能導致內存泄漏// new方式:自動調用構造/析構函數
TestClass* obj2 = new TestClass(); // 輸出"對象被構造"
delete obj2; // 輸出"對象被析構"
混用的危險性
// 錯誤示例:不要混用
int* ptr1 = (int*)malloc(sizeof(int));
delete ptr1; // 錯誤:malloc的內存不能用delete釋放int* ptr2 = new int;
free(ptr2); // 錯誤:new的內存不能用free釋放
6. 宏定義的陷阱:副作用問題
標準MIN宏的實現
#define MIN(a, b) ((a) <= (b) ? (a) : (b))// 基本使用
int x = 5, y = 3;
int min_val = MIN(x, y); // 結果:3
宏的副作用問題
#define MIN(a, b) ((a) <= (b) ? (a) : (b))int main() {int x = 5;int* p = &x;// 危險的調用方式int result = MIN(++(*p), 10);// 宏展開后變成:// int result = ((++(*p)) <= (10) ? (++(*p)) : (10));// ++(*p)被執行了兩次!printf("x = %d\n", x); // x可能是7而不是6printf("result = %d\n", result);return 0;
}
更安全的實現方式
// 使用內聯函數替代宏(C++推薦)
inline int min_safe(int a, int b) {return (a <= b) ? a : b;
}// 或者使用臨時變量的宏(C語言)
#define MIN_SAFE(a, b) ({ \typeof(a) _a = (a); \typeof(b) _b = (b); \(_a <= _b) ? _a : _b; \
})
7. volatile關鍵字:處理不可預測的變量變化
volatile的作用機制
volatile告訴編譯器:這個變量可能被程序外部因素修改,不要進行優化。
典型應用場景
中斷服務程序
volatile int interrupt_flag = 0; // 中斷標志// 中斷服務函數
void interrupt_handler() {interrupt_flag = 1; // 中斷發生時設置標志
}// 主程序
int main() {while (interrupt_flag == 0) {// 等待中斷發生// 如果沒有volatile,編譯器可能優化成死循環}printf("中斷已處理\n");return 0;
}
硬件寄存器訪問
// 硬件寄存器地址
volatile unsigned int* const HARDWARE_REG = (unsigned int*)0x40000000;void read_sensor() {unsigned int value = *HARDWARE_REG; // 每次都從硬件讀取printf("傳感器值:%u\n", value);
}
多線程共享變量
volatile bool thread_running = true;void worker_thread() {while (thread_running) { // 確保每次都檢查最新值// 執行工作printf("線程運行中...\n");sleep(1);}printf("線程退出\n");
}void stop_thread() {thread_running = false; // 通知線程停止
}
volatile指針的不同含義
int value = 100;// 指向volatile變量的指針
volatile int* ptr1 = &value; // 指向的內容是volatile的
*ptr1 = 200; // 每次寫入都不會被優化// volatile指針指向普通變量
int* volatile ptr2 = &value; // 指針本身是volatile的
ptr2 = &another_value; // 指針的修改不會被優化// volatile指針指向volatile變量
volatile int* volatile ptr3 = &value; // 指針和內容都是volatile的
8. 數組名與數組地址:a vs &a的本質區別
概念解析
a
:數組名,表示數組首元素的地址&a
:數組的地址,表示整個數組的地址
代碼分析實例
#include <stdio.h>int main() {int a[5] = {1, 2, 3, 4, 5};printf("a = %p\n", a); // 數組首元素地址printf("&a = %p\n", &a); // 整個數組的地址printf("a+1 = %p\n", a+1); // 下一個元素地址(+4字節)printf("&a+1 = %p\n", &a+1); // 下一個數組地址(+20字節)// 關鍵代碼分析int* ptr = (int*)(&a + 1); // 指向數組后面的位置printf("*(a+1) = %d\n", *(a+1)); // 輸出:2(第二個元素)printf("*(ptr-1) = %d\n", *(ptr-1)); // 輸出:5(最后一個元素)return 0;
}
內存布局圖解
內存地址: 1000 1004 1008 1012 1016 1020
數組內容: [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ]↑ ↑ ↑a | &a+1&a |a+4或&a[4]
指針運算的區別
int a[5] = {1, 2, 3, 4, 5};// a+1:移動一個int的大小(4字節)
int* p1 = a + 1; // 指向a[1]// &a+1:移動整個數組的大小(20字節)
int* p2 = (int*)(&a + 1); // 指向數組后面的位置// 驗證
printf("p1指向的值:%d\n", *p1); // 輸出:2
printf("p2-1指向的值:%d\n", *(p2-1)); // 輸出:5
9. C/C++程序內存布局:五大存儲區域詳解
內存分區概述
C/C++程序運行時,內存被劃分為5個主要區域,每個區域有不同的特點和用途。
1. 程序代碼區(Text Segment)
// 存儲編譯后的機器代碼
void function1() {printf("這個函數的代碼存儲在代碼區\n");
}int main() {printf("main函數的代碼也在代碼區\n");return 0;
}
特點:只讀、共享、程序加載時確定大小
2. 全局/靜態存儲區(Data Segment)
// 已初始化的全局變量
int global_var = 100; // 存儲在已初始化數據區// 未初始化的全局變量
int uninitialized_global; // 存儲在BSS區(自動初始化為0)// 靜態變量
static int static_var = 200; // 存儲在已初始化數據區void function() {static int local_static; // 存儲在BSS區
}
特點:程序運行期間一直存在、自動初始化為0(BSS區)
3. 棧區(Stack)
void stack_demo() {int local_var = 10; // 局部變量,存儲在棧上char buffer[1024]; // 局部數組,存儲在棧上printf("local_var地址:%p\n", &local_var);printf("buffer地址:%p\n", buffer);// 函數結束時,這些變量自動銷毀
}void recursive_function(int n) {int local = n; // 每次遞歸調用都在棧上分配if (n > 0) {recursive_function(n - 1);}
}
特點:
- 自動管理(函數結束自動釋放)
- 訪問速度快
- 大小有限(通常幾MB)
- 后進先出(LIFO)
4. 堆區(Heap)
void heap_demo() {// 動態分配內存int* heap_ptr = (int*)malloc(sizeof(int) * 100);if (heap_ptr == NULL) {printf("內存分配失敗\n");return;}// 使用堆內存for (int i = 0; i < 100; i++) {heap_ptr[i] = i;}// 必須手動釋放free(heap_ptr);heap_ptr = NULL; // 防止懸空指針
}void cpp_heap_demo() {// C++風格的堆內存管理int* ptr = new int[100]; // 分配// 使用內存...delete[] ptr; // 釋放
}
特點:
- 手動管理(程序員負責分配和釋放)
- 大小靈活
- 訪問速度相對較慢
- 容易產生內存泄漏和碎片
5. 文字常量區(String Literal Pool)
void string_demo() {char* str1 = "Hello World"; // 字符串存儲在常量區char* str2 = "Hello World"; // 可能與str1指向同一地址char arr[] = "Hello World"; // 字符串復制到棧上printf("str1地址:%p\n", str1);printf("str2地址:%p\n", str2);printf("arr地址:%p\n", arr);// str1[0] = 'h'; // 錯誤:不能修改常量區內容arr[0] = 'h'; // 正確:可以修改棧上的副本
}
內存布局示意圖
高地址
┌─────────────────┐
│ 棧區 │ ← 向下增長
│ (局部變量) │
├─────────────────┤
│ ↓ │
│ │
│ ↑ │
├─────────────────┤
│ 堆區 │ ← 向上增長
│ (動態分配) │
├─────────────────┤
│ 未初始化數據 │
│ (BSS段) │
├─────────────────┤
│ 已初始化數據 │
│ (Data段) │
├─────────────────┤
│ 文字常量區 │
├─────────────────┤
│ 程序代碼區 │
└─────────────────┘
低地址
10. 字符串操作函數對比:strcpy、sprintf、memcpy
功能定位分析
strcpy:字符串到字符串的復制
#include <string.h>void strcpy_demo() {char source[] = "Hello World";char destination[20];// 復制字符串(包括結尾的'\0')strcpy(destination, source);printf("復制結果:%s\n", destination);// 注意:不檢查目標緩沖區大小,可能溢出// 更安全的版本:strncpystrncpy(destination, source, sizeof(destination) - 1);destination[sizeof(destination) - 1] = '\0'; // 確保以'\0'結尾
}
sprintf:格式化輸出到字符串
#include <stdio.h>void sprintf_demo() {char buffer[100];int age = 25;float height = 175.5f;char name[] = "張三";// 將多種數據類型格式化為字符串sprintf(buffer, "姓名:%s,年齡:%d,身高:%.1f厘米", name, age, height);printf("格式化結果:%s\n", buffer);// 更安全的版本:snprintfsnprintf(buffer, sizeof(buffer), "安全的格式化:%s", name);
}
memcpy:內存塊到內存塊的復制
#include <string.h>void memcpy_demo() {// 復制整數數組int source[] = {1, 2, 3, 4, 5};int destination[5];memcpy(destination, source, sizeof(source));// 復制結構體struct Person {char name[20];int age;};struct Person p1 = {"李四", 30};struct Person p2;memcpy(&p2, &p1, sizeof(struct Person));printf("復制的結構體:%s, %d\n", p2.name, p2.age);// 復制部分內存char str1[] = "Hello World";char str2[20];memcpy(str2, str1, 5); // 只復制前5個字符str2[5] = '\0'; // 手動添加結束符printf("部分復制:%s\n", str2); // 輸出:Hello
}
性能對比測試
#include <time.h>void performance_test() {const int TEST_SIZE = 1000000;char source[1000];char destination[1000];clock_t start, end;// 初始化源數據memset(source, 'A', sizeof(source) - 1);source[sizeof(source) - 1] = '\0';// 測試memcpy性能start = clock();for (int i = 0; i < TEST_SIZE; i++) {memcpy(destination, source, sizeof(source));}end = clock();printf("memcpy耗時:%f秒\n", (double)(end - start) / CLOCKS_PER_SEC);// 測試strcpy性能start = clock();for (int i = 0; i < TEST_SIZE; i++) {strcpy(destination, source);}end = clock();printf("strcpy耗時:%f秒\n", (double)(end - start) / CLOCKS_PER_SEC);// 測試sprintf性能start = clock();for (int i = 0; i < TEST_SIZE; i++) {sprintf(destination, "%s", source);}end = clock();printf("sprintf耗時:%f秒\n", (double)(end - start) / CLOCKS_PER_SEC);
}
使用場景選擇指南
- strcpy:純字符串復制,需要自動處理’\0’結尾
- sprintf:需要格式化多種數據類型為字符串
- memcpy:原始內存復制,最高效,適合大塊數據
11. 直接內存操作:指定地址賦值技術
基本概念
在嵌入式開發或系統編程中,經常需要直接操作特定內存地址的數據。
實現方法
void memory_operation_demo() {// 將整數值0xaa66寫入地址0x67a9int* ptr; // 聲明整型指針ptr = (int*)0x67a9; // 將地址強制轉換為整型指針*ptr = 0xaa66; // 向該地址寫入數據// 讀取驗證printf("地址0x67a9的值:0x%x\n", *ptr);
}
實際應用場景
硬件寄存器操作
// 定義硬件寄存器地址
#define GPIO_BASE_ADDR 0x40020000
#define GPIO_OUTPUT_REG (GPIO_BASE_ADDR + 0x14)
#define GPIO_INPUT_REG (GPIO_BASE_ADDR + 0x10)void gpio_control() {// 控制GPIO輸出volatile unsigned int* gpio_output = (volatile unsigned int*)GPIO_OUTPUT_REG;*gpio_output = 0xFF; // 設置所有引腳為高電平// 讀取GPIO輸入volatile unsigned int* gpio_input = (volatile unsigned int*)GPIO_INPUT_REG;unsigned int input_value = *gpio_input;printf("GPIO輸入值:0x%x\n", input_value);
}
內存映射文件操作
#include <sys/mman.h>
#include <fcntl.h>void memory_mapped_file() {int fd = open("data.bin", O_RDWR);if (fd == -1) return;// 將文件映射到內存void* mapped_addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (mapped_addr != MAP_FAILED) {// 直接操作內存就是操作文件int* data_ptr = (int*)mapped_addr;*data_ptr = 0x12345678; // 寫入數據到文件// 解除映射munmap(mapped_addr, 1024);}close(fd);
}
安全注意事項
void safe_memory_access() {// 1. 檢查地址有效性void* addr = (void*)0x67a9;if (addr == NULL) {printf("無效地址\n");return;}// 2. 使用volatile防止編譯器優化volatile int* ptr = (volatile int*)addr;// 3. 異常處理(在支持的系統上)try {*ptr = 0xaa66;} catch (...) {printf("內存訪問異常\n");}
}
12. 面向對象三大特征深度解析
1. 封裝性(Encapsulation):數據隱藏與接口設計
基本概念
封裝是將數據和操作數據的方法組合在一起,通過訪問控制來隱藏內部實現細節。
class BankAccount {
private:double balance; // 私有數據:外部無法直接訪問string account_number; // 私有數據:賬戶安全信息public:// 公有接口:提供安全的訪問方式BankAccount(string acc_num, double initial_balance) {account_number = acc_num;balance = initial_balance;}// 存款操作:控制數據修改方式bool deposit(double amount) {if (amount > 0) {balance += amount;return true;}return false; // 拒絕無效操作}// 取款操作:包含業務邏輯驗證bool withdraw(double amount) {if (amount > 0 && amount <= balance) {balance -= amount;return true;}return false; // 余額不足或金額無效}// 查詢余額:只讀訪問double get_balance() const {return balance;}
};
2. 繼承性(Inheritance):代碼復用與層次結構
// 基類:動物
class Animal {
protected:string name;int age;public:Animal(string n, int a) : name(n), age(a) {}// 虛函數:允許子類重寫virtual void make_sound() {cout << name << "發出聲音" << endl;}virtual ~Animal() {} // 虛析構函數
};// 派生類:狗
class Dog : public Animal {
public:Dog(string n, int a) : Animal(n, a) {}// 重寫基類的虛函數virtual void make_sound() override {cout << name << "汪汪叫" << endl;}
};
3. 多態性(Polymorphism):一個接口多種實現
void demonstrate_polymorphism() {// 創建不同類型的動物對象Animal* animals[] = {new Dog("旺財", 3),new Animal("未知動物", 2)};// 多態調用:同一接口,不同實現for (int i = 0; i < 2; i++) {animals[i]->make_sound(); // 根據實際對象類型調用相應函數}// 清理內存for (int i = 0; i < 2; i++) {delete animals[i];}
}
13. C++空類的隱式成員函數
編譯器自動生成的六個函數
class EmptyClass {// 編譯器自動生成以下函數:// 1. 默認構造函數// 2. 拷貝構造函數// 3. 析構函數// 4. 賦值運算符// 5. 取址運算符// 6. const取址運算符
};
14. 拷貝構造函數 vs 賦值運算符
調用時機不同
class TestClass {
public:int value;TestClass(int v) : value(v) {}TestClass(const TestClass& other) : value(other.value) {cout << "拷貝構造函數調用" << endl;}TestClass& operator=(const TestClass& other) {cout << "賦值運算符調用" << endl;value = other.value;return *this;}
};void demo() {TestClass obj1(10); // 構造函數TestClass obj2(obj1); // 拷貝構造函數TestClass obj3 = obj1; // 拷貝構造函數(不是賦值!)TestClass obj4(20); // 構造函數obj4 = obj1; // 賦值運算符
}
15. 設計不可繼承的類
C++11的final關鍵字(推薦方法)
class FinalClass final { // final關鍵字阻止繼承
public:FinalClass() {cout << "FinalClass構造函數" << endl;}void do_something() {cout << "執行某些操作" << endl;}
};// 編譯錯誤:無法繼承final類
// class DerivedClass : public FinalClass {};
16. 虛函數表機制深度解析
虛函數表的工作原理
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
};class Derived : public Base {
public:virtual void func1() override { cout << "Derived::func1" << endl; }
};void virtual_demo() {Base* ptr = new Derived();ptr->func1(); // 通過虛函數表調用Derived::func1delete ptr;
}
17. 函數重寫、重載、隱藏的區別
重載(Overloading):同一作用域內的函數多態
class Calculator {
public:int add(int a, int b) { return a + b; } // 整數版本double add(double a, double b) { return a + b; } // 浮點版本int add(int a, int b, int c) { return a + b + c; } // 三參數版本
};
重寫(Override):繼承關系中的虛函數替換
class Shape {
public:virtual double area() { return 0.0; } // 基類虛函數
};class Circle : public Shape {
public:virtual double area() override { // 重寫基類虛函數return 3.14159 * radius * radius;}
private:double radius;
};
18. 多態實現原理:虛函數表詳解
動態綁定的實現過程
void polymorphism_mechanism() {Shape* shapes[] = {new Circle(5.0),new Rectangle(4.0, 6.0)};for (int i = 0; i < 2; i++) {// 編譯器生成的代碼:// 1. 獲取對象的虛函數表指針// 2. 在虛函數表中查找area函數的地址// 3. 調用該地址對應的函數cout << "面積:" << shapes[i]->area() << endl;}for (int i = 0; i < 2; i++) {delete shapes[i];}
}
19. 數組 vs 鏈表:數據結構選擇指南
性能對比分析
數組的優勢
void array_advantages() {int arr[1000];// 1. 隨機訪問:O(1)時間復雜度int value = arr[500]; // 直接通過索引訪問// 2. 內存連續,緩存友好for (int i = 0; i < 1000; i++) {arr[i] = i * i; // 順序訪問,緩存命中率高}// 3. 空間效率高:只存儲數據,無額外指針開銷
}
鏈表的優勢
struct ListNode {int data;ListNode* next;ListNode(int val) : data(val), next(nullptr) {}
};class LinkedList {
private:ListNode* head;public:LinkedList() : head(nullptr) {}// 1. 插入操作:O(1)時間復雜度(在已知位置)void insert_at_head(int val) {ListNode* new_node = new ListNode(val);new_node->next = head;head = new_node;}// 2. 刪除操作:O(1)時間復雜度(在已知位置)void delete_node(ListNode* prev, ListNode* current) {if (prev) {prev->next = current->next;} else {head = current->next;}delete current;}// 3. 動態大小:可以根據需要增長或縮小void dynamic_resize() {// 鏈表大小可以動態變化,不需要預先分配固定大小}
};
使用場景選擇指南
-
數組適用場景:
- 需要頻繁隨機訪問元素
- 數據大小相對固定
- 對內存使用效率要求高
- 需要利用CPU緩存優化性能
-
鏈表適用場景:
- 需要頻繁插入和刪除操作
- 數據大小變化很大
- 不需要隨機訪問元素
- 內存分配需要靈活性
20. 單鏈表反轉算法實現
迭代算法實現
struct ListNode {int val;ListNode* next;ListNode(int x) : val(x), next(nullptr) {}
};// 迭代方法反轉鏈表
ListNode* reverse_iterative(ListNode* head) {if (!head) { // 判斷鏈表是否為空return head;}ListNode* prev = nullptr; // 前一個節點指針ListNode* current = head; // 當前節點指針ListNode* next = nullptr; // 下一個節點指針while (current != nullptr) { // 遍歷整個鏈表next = current->next; // 保存下一個節點current->next = prev; // 反轉當前節點的指針prev = current; // 移動prev指針current = next; // 移動current指針}return prev; // prev現在指向新的頭節點
}
遞歸算法實現
// 遞歸方法反轉鏈表
ListNode* reverse_recursive(ListNode* head) {// 基礎情況:空鏈表或只有一個節點if (!head || !head->next) {return head;}// 遞歸反轉剩余部分ListNode* new_head = reverse_recursive(head->next);// 反轉當前連接head->next->next = head; // 將下一個節點指向當前節點head->next = nullptr; // 當前節點指向空return new_head; // 返回新的頭節點
}
完整測試示例
// 創建鏈表的輔助函數
ListNode* create_list(vector<int>& values) {if (values.empty()) return nullptr;ListNode* head = new ListNode(values[0]);ListNode* current = head;for (int i = 1; i < values.size(); i++) {current->next = new ListNode(values[i]);current = current->next;}return head;
}// 打印鏈表的輔助函數
void print_list(ListNode* head) {ListNode* current = head;while (current) {cout << current->val;if (current->next) cout << " -> ";current = current->next;}cout << " -> NULL" << endl;
}// 測試函數
void test_reverse() {vector<int> values = {1, 2, 3, 4, 5};// 測試迭代方法ListNode* list1 = create_list(values);cout << "原始鏈表:";print_list(list1);ListNode* reversed1 = reverse_iterative(list1);cout << "迭代反轉后:";print_list(reversed1);// 測試遞歸方法ListNode* list2 = create_list(values);ListNode* reversed2 = reverse_recursive(list2);cout << "遞歸反轉后:";print_list(reversed2);
}
算法復雜度分析
- 時間復雜度:O(n),需要遍歷鏈表中的每個節點一次
- 空間復雜度:
- 迭代方法:O(1),只使用常數額外空間
- 遞歸方法:O(n),遞歸調用棧的深度為n
實際應用場景
- 數據結構操作:在實現棧、隊列等數據結構時需要反轉操作
- 算法題解決:許多算法問題需要鏈表反轉作為子問題
- 系統設計:在某些系統中需要反轉數據流或操作序列
總結
本文詳細介紹了C/C++20個核心知識點,涵蓋了從基礎語法到高級特性的各個方面:
- 基礎概念:變量聲明定義、數據類型比較、sizeof/strlen區別
- 內存管理:static關鍵字、malloc/new區別、內存布局
- 面向對象:三大特征、虛函數機制、多態實現
- 數據結構:數組鏈表對比、鏈表反轉算法
- 編程技巧:宏定義陷阱、volatile關鍵字、防御性編程
掌握這些知識點不僅能幫助你在面試中脫穎而出,更重要的是能提升你的編程能力和代碼質量。建議讀者結合實際項目練習,加深對這些概念的理解和應用。
記住:理論知識要與實踐相結合,多寫代碼、多調試、多思考,才能真正掌握C/C++編程的精髓。