史上最全 C/C++ 指針避坑指南:八年老鳥整理的 20 個致命錯誤
大家好,我是小康,一個在 C++ 的坑里摸爬滾打了 8 年的開發者。今天我要和大家聊聊那些讓每個程序員都頭疼的指針錯誤。
寫了這么久C++,指針還是經常讓你頭大?代碼莫名其妙崩潰,調試半天發現是指針出問題?面試官隨便問個指針問題就把你問懵了?
放心,不是你一個人!今天我們就用最通俗的語言,聊聊 C++ 指針那些"坑"。
記得我剛開始學習的時候,光是看到 int *p 這樣的代碼就覺得腦袋瓜子嗡嗡的。但是,指針這個東西吧,就像自行車,一旦掌握了要領,那騎起來就是享受!今天我就把這些年踩過的坑都給大家分享出來,保證說人話,不說教科書!
錯誤一:野指針-這是個沒拴繩的野狗啊!
int* p; // 聲明一個指針,但沒有初始化
*p = 10; // 完蛋,這就是傳說中的野指針!
這就好比你養了條狗,但是沒給它栓繩子,它想跑哪跑哪,最后把鄰居家的花園給禍禍了...
正確做法是啥? 要么給它一個合法的地址,要么直接給 nullptr:
int* p = nullptr; // 現代C++推薦用nullptr
// 或者
int x = 5;
int* p = &x;
錯誤二:忘記刪除堆內存 - 這是在浪費資源啊!
void leakMemory() {
int* p = new int(42);
// 函數結束了,但是忘記delete
} // 內存泄漏!這塊內存永遠要不回來了
這就像你上廁所占了個坑,但是用完不沖水就走了,后面的人都沒法用了。正確的做法是:
void noLeak() {
int* p = new int(42);
// 用完了記得delete
delete p;
p = nullptr; // 刪除后最好置空
}
更好的辦法是直接用智能指針,這就相當于給廁所裝了個自動沖水裝置:
#include <memory>
void modern() {
auto p = std::make_unique<int>(42);
// 函數結束會自動釋放內存,不用操心
}
錯誤三:解引用空指針 - 這不是自己給自己挖坑嗎?
int* p = nullptr;
*p = 100; // 程序崩潰!這就像試圖往一個不存在的盒子里放東西
在使用指針之前,一定要檢查:
int* p = nullptr;
if (p != nullptr) {
*p = 100;
} else {
std::cout << "哎呀,指針是空的,可不能用!" << std::endl;
}
錯誤四:delete指針后繼續使用 - 這是在玩火啊!
int* p = new int(42);
delete p; // 釋放內存
*p = 100; // 災難!這塊內存已經不屬于你了
這就像你退了房租,但還硬要住在人家房子里,這不是找打嗎?
正確做法:
int* p = new int(42);
delete p;
p = nullptr; // 刪除后立即置空
// 后面要用需要重新分配
p = new int(100);
錯誤五:數組使用單個delete刪除 - 這是在瞎搗亂啊!
int* arr = new int[10];
delete arr; // 錯!這是在用單個delete刪除動態數組
數組要用delete[ ]:
int* arr = new int[10];
delete[] arr; // 對!這才是刪除動態數組的正確姿勢
arr = nullptr;
錯誤六:指針運算越界 - 這是要翻車的節奏!
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
for(int i = 0; i <= 5; i++) { // 錯!數組只有5個元素
cout << *p++ << endl; // 最后一次訪問越界了
}
正確做法:
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
for(int i = 0; i < 5; i++) { // 對!只訪問有效范圍
cout << *p++ << endl;
}
錯誤七:返回局部變量的指針 - 這是在玩火!
int* getLocalPtr() {
int x = 42;
return &x; // 危險!x是局部變量,函數結束就沒了
}
這就像你要借別人的東西,但是人家已經搬家了,你上哪借去?
正確做法:
int* getSafePtr() {
int* p = new int(42);
return p; // 返回堆內存的指針
}
// 或者更好的做法
std::unique_ptr<int> getSaferPtr() {
return std::make_unique<int>(42);
}
錯誤八:指針類型不匹配 - 強扭的瓜不甜啊!
double d = 3.14;
int* p = &d; // 錯!類型不匹配
正確做法:
double d = 3.14;
double* p = &d; // 對!類型要匹配
錯誤九:多重指針不打基礎 - 這是在疊積木不打底!
int** pp; // 指向指針的指針
*pp = new int(42); // 危險!底下一塊積木都沒放就想往上疊
正確的搭法:
// 一層一層來,穩穩當當
int* p = new int(42); // 先放好底層積木
int** pp = &p; // 再往上疊一塊
cout << **pp << endl; // 現在這積木穩當,可以安全使用了
記住:多重指針就像搭積木,得從底層開始,一層一層穩妥地往上搭,跳著搭就容易倒塌!
錯誤十:const 和指針的位置擺錯 - 這是在挖坑自己跳啊!
最常見的三種指針和const組合:
int value = 10, other = 20;
// 三種基本組合
const int* p1 = &value; // ? *p1 = 100; ? p1 = &other;
int* const p2 = &value; // ? *p2 = 100; ? p2 = &other;
const int* const p3 = &value;// ? *p3 = 100; ? p3 = &other;
常見錯誤:
void onlyRead(int* const data) { // 錯誤用法!
*data = 100; // 竟然能改值!
data = &other; // 這個才報錯
}
void onlyRead(const int* data) { // 正確用法!
*data = 100; // 編譯報錯,保護數據不被修改
data = &other; // 允許改變指向
}
記憶技巧:
- const int* : const 在 * 左邊,鎖住值
- int* const : const 在 * 右邊,鎖住指向
- 要保護數據不被改,就用 const int*
錯誤十一:構造函數漏初始化指針 - 這是在埋定時炸彈啊
class MyClass {
int* ptr;
public:
MyClass() {
// 完蛋,忘記初始化ptr了
}
}; // 使用ptr時可能崩潰
正確做法:
class MyClass {
int* ptr;
public:
MyClass() : ptr(nullptr) { // 構造時就初始化
// 或者分配內存
ptr = new int(42);
}
};
錯誤十二:函數參數傳遞指針沒聲明const - 這是在裸奔啊!
// 下面這種寫法,數據像裸奔一樣毫無保護
void printData(int* data) {
cout << *data << endl; // 雖然只是讀數據,但是沒人知道啊!
}
正確做法:
// 加個const,數據就穿上了防護服
void printData(const int* data) {
cout << *data << endl;
}
記住:只是讀數據不修改時,一定要加const!不加const就像把數據扔在大馬路上,誰都能改。
錯誤十三:指針移動導致內存釋放失敗 - 這是在玩火!
int* p = new int[5];
for(int i = 0; i < 5; i++) {
cout<<*p<<endl;
p++; // 完蛋,循環結束后p已經不指向數組起始位置了
}
delete[] p; // 錯誤!p已經移動了
正確做法:
int* p = new int[5];
int* temp = p; // 用臨時指針做移動
for(int i = 0; i < 5; i++) {
cout<<*temp<<endl;
temp++;
}
delete[] p; // 正確!p還在起始位置
錯誤十四:指針和引用混用 - 這是在給自己找麻煩!
void func(int*& ptr) { // 指針的引用,看著就頭大
ptr = new int(42);
}
更清晰的做法:
std::unique_ptr<int>& func() { // 返回智能指針的引用
static auto ptr = std::make_unique<int>(42); // 返回 static 對象
return ptr;
}
錯誤十五:不安全的指針向下轉換 - 這是在蠻干啊!
class Base {};
class Derived : public Base {};
Derived* d = new Derived();
Base* b = d; // 向上轉換,安全
Derived* d2 = b; // 錯誤!向下轉換需要 dynamic_cast
正確做法:
Derived* d2 = dynamic_cast<Derived*>(b); // 安全的向下轉換
if( d2 != nullptr ) { // 檢查轉換是否成功
// 使用d2
}
錯誤十六:函數指針調用前未檢查 - 這是在冒險啊!
// 錯誤示例
void (*fp)(int) = nullptr;
fp(42); // 災難!沒檢查就直接調用
// 或者更糟的情況
void (*fp)(int); // 未初始化就使用
fp(42); // 更大的災難!
正確做法:
void (*fp)(int) = nullptr; // 明確初始化為nullptr
// 或者賦值一個具體函數
void foo(int x) { cout << x << endl; }
fp = foo;
// 使用前檢查
if(fp!=nullptr) {
fp(42); // 安全!
} else {
cout << "函數指針無效" << endl;
}
錯誤十七:在類里 delete this 指針 - 簡直是自殺!
// 錯誤示例
class Player {
public:
int score;
public:
void killSelf() {
delete this; // 自己把自己刪了
}
};
Player* player = new Player();
player->killSelf(); // 這下好了,后面的代碼都懸了
resetGame(); // 慘!死人也想重開一局
正確的做法:
class Player {
// 方法1:讓外面的代碼來管理生命周期
void cleanup() {
score = 0;
// 只做清理工作,不要自己刪自己
}
};
// 外部代碼負責刪除
Player* player = new Player();
player->cleanup(); // 先清理
delete player; // 再刪除
player = nullptr; // 最后置空
// 方法2:更現代的方式 - 使用智能指針
class Player {
// 類里面該做啥做啥,不用操心刪除的事
};
// 讓智能指針來管理生命周期
auto player = make_shared<Player>();
// 不用管刪除,超出作用域自動清理
記住:
- 在類的方法里刪除 this指針就像自殺,死了還想干活那肯定不行
- 對象的生命周期最好交給外部代碼或智能指針管理
- 如果非要在類里面刪除自己,那刪完就立即返回,別做其他操作
錯誤十八:智能指針互相引用 - 這是在手拉手繞圈圈!
循環引用示例:
// 錯誤示例:兩個朋友互相拉手不放
class Student {
shared_ptr<Student> bestFriend; // 我有個好朋友
public:
void makeFriend(shared_ptr<Student> other) {
bestFriend = other; // 我拉著我朋友
}
};
// 兩個學生互相成為好朋友
auto tom = make_shared<Student>();
auto jerry = make_shared<Student>();
tom->makeFriend(jerry); // tom拉住jerry
jerry->makeFriend(tom); // jerry也拉住tom
// 完蛋!他們互相拉著對方不放手,
// 即使放學了也走不了(內存不能釋放)
正確的做法:
// 正確示例:一個人拉手,一個人輕拉
class Student {
weak_ptr<Student> bestFriend; // 用weak_ptr,不牢牢抓住對方
public:
void makeFriend(shared_ptr<Student> other) {
bestFriend = other; // 輕輕拉住朋友就好
}
};
auto tom = make_shared<Student>();
auto jerry = make_shared<Student>();
tom->makeFriend(jerry);
jerry->makeFriend(tom);
// 現在好了,放學后可以松手回家了(正常釋放內存)
記住:
- 兩個對象用shared_ptr互相引用,就像兩個人死死拉住對方的手不放,誰都走不了
- 要解決這個問題,讓一方改用weak_ptr,就像輕輕牽手就好,需要的時候隨時可以松開
- 智能指針循環引用會導致內存泄漏,就像兩個人一直拉著手,永遠不能回家
注意:智能指針的循環引用很容易把人繞暈,我用兩張手繪小圖,帶大家一步步理解這個過程:
循環引用圖解:
說明:智能指針對象 tom 和 jerry 的引用計數值 count 都變成 2,導致在 main 程序退出時,各自的 count 都無法減為 0 ,從而造成內存泄漏。
使用 weak_ptr 避免循環引用:
說明:tom 和 jerry 的引用計數值 count 始終都是 1,main 程序退出時,各自的 count 都減到 0 ,內存正常釋放。
錯誤十九:指針成員的深淺拷貝 - 很容易翻車!
class Resource {
int* data;
public:
Resource() { data = newint(42); }
~Resource() { delete data; }
// 默認拷貝構造函數和賦值運算符會導致災難
// Resource(const Resource& other) = default; // 淺拷貝!
// Resource& operator=(const Resource& other) = default; // 淺拷貝!
};
void disasterExample() {
Resource r1;
Resource r2 = r1; // 淺拷貝:r1和r2的data指向同一內存
// 函數結束時,r1和r2都會delete同一個data!程序崩潰
}
正確做法:
class Resource {
int* data;
public:
Resource() { data = newint(42); }
~Resource() { delete data; }
// 實現深拷貝
Resource(const Resource& other) {
data = newint(*other.data); // 復制數據本身
}
Resource& operator=(const Resource& other) {
if (this != &other) {
delete data;
data = newint(*other.data);
}
return *this;
}
// 或者更好的方案:使用智能指針
// unique_ptr<int> data; // 禁止拷貝
// shared_ptr<int> data; // 共享所有權
};
人人都知道要深拷貝,但實際寫代碼時很容易忽略,尤其是在類有多個指針成員時。現代 C++ 建議優先使用智能指針來避免這類問題。
錯誤二十:函數內修改指針實參 - 這是在玩障眼法!
// 錯誤示例
void resetPointer(int* ptr) {
ptr = nullptr; // 以為這樣就能把外面的指針置空
}
int* p = new int(42);
resetPointer(p); // 調用函數
cout << *p; // 糟糕!p根本沒變成nullptr,還在指向原來的地方
正確做法:
// 方法1:使用指針的指針
void resetPointer(int** ptr) { // 傳入指針的地址
*ptr = nullptr; // 現在可以修改原始指針了
}
int* p = newint(42);
resetPointer(&p); // 傳入p的地址
// 現在p確實被置空了
// 方法2:使用引用
void resetPointer(int*& ptr) { // 使用指針的引用
ptr = nullptr;
}
int* p = newint(42);
resetPointer(p); // p會被置空
記住:
- 函數參數是傳值的,修改指針形參不會影響外面的指針
- 要修改外部指針,必須傳入指針的指針
- 這個問題在做指針操作時特別常見,很多人都會犯這個錯
實戰小貼士
(1) 優先使用智能指針
// 不推薦
MyClass* ptr = new MyClass();
// 推薦
unique_ptr<MyClass> ptr = make_unique<MyClass>();
(2) 指針安全法則
- 用完指針及時置空 nullptr
- 分配內存后立即考慮釋放的時機和方式
- 涉及指針的函數,第一步就是檢查指針是否為 nullptr
- 使用智能指針時,要注意循環引用
(3) 關于指針和引用的選擇:
// 需要修改指針指向時,必須傳遞指針
void updatePtr(int*& ptr); // 通過引用修改指針 - 這種情況很少見
void updatePtr(int** ptr); // 通過指針修改指針 - 更常見的做法
// 只需要訪問或修改指針指向的數據時
void process(const int* ptr); // 不修改數據時用const
void modify(int* ptr);
(4) 代碼規范建議
// 指針聲明時緊跟類型
int* ptr; // 推薦
int *ptr; // 不推薦
// 多重指針超過兩層就要考慮重構
int*** ptr; // 需要重新設計
// const的一致性
void process(const std::string* data); // 參數不修改就用const
總結
看完這些指針的坑,是不是覺得其實也沒那么可怕?記住一點:指針就是個地址,搞清楚這個地址指向哪,什么時候有效,什么時候無效,基本就能避免大多數問題了。