大家好,我是小康,一个在 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
总结
看完这些指针的坑,是不是觉得其实也没那么可怕?记住一点:指针就是个地址,搞清楚这个地址指向哪,什么时候有效,什么时候无效,基本就能避免大多数问题了。