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