目录
今天我们用通俗的语言来聊一聊C/C++指针中的一些坑。指针是否经常使得你头大?代码莫名其妙就奔溃?是否有出现一下错误?
错误一:野指针
int* p;//声明一个指针,但没有初始化
*p = 10;//完了,这就是传说中的野指针
这是一只没有栓绳的野狗!就好比你养了一条狗,没有给它栓绳,它想跑哪就跑哪去,最后把邻居家的小猫小鸡花花草草给祸害了...
【问题分析】
未初始化的指针:
int* p;
声明了一个指向int
类型的指针p
,但此时p
并没有被初始化,它的值是未定义的(即它可能指向任意内存地址)。野指针:当你试图通过
*p = 10;
来访问或修改p
所指向的内存时,由于p
没有指向任何有效的内存地址,这会导致未定义行为(Undefined Behavior)。这种情况下,程序可能会崩溃、产生不可预测的结果,或者在某些情况下看似正常运行,但实际上隐藏着严重的错误。
【如何避免野指针】
1.初始化指针:在声明指针时,要么给合法的地址,要么直接给nullptr。尽量将其初始化为
nullptr
(C++11 及以后)或NULL
(C++11 之前),以明确表示指针当前不指向任何有效的内存地址。2.在使用指针前检查其有效性:在解引用指针之前,确保指针指向有效的内存地址。
3.动态分配内存:如果你需要指针指向一个有效的内存地址,可以使用
new
或malloc
动态分配内存。4.使用智能指针:在现代 C++ 中,推荐使用智能指针(如
std::unique_ptr
或std::shared_ptr
)来管理动态内存,避免手动管理内存带来的问题。
//1.
int* p = nullptr; //现代C++推荐使用nullptr
//或者
int x = 5;
int* p = &x;
//2.
if (p != nullptr) {
*p = 10;
}
//3.
int* p = new int; // 动态分配内存
*p = 10; // 现在可以安全地使用 p
delete p; // 使用完毕后释放内存
//4.
#include <memory>
std::unique_ptr<int> p = std::make_unique<int>(10);
// 不需要手动释放内存,智能指针会自动管理
【小结】
野指针是 C/C++ 中常见的错误之一,通常是由于未初始化的指针或已释放的内存被继续使用导致的。为了避免这类问题,务必在使用指针前确保它指向有效的内存地址,并考虑使用智能指针等现代 C++ 特性来简化内存管理。
错误二:忘记删堆内存
void leakMemory() {
int* p = new int(42);
// 函数结束了,但是忘记delete
} // 内存泄漏!这块内存永远要不回来了
这就是在浪费资源呀!这就像你上厕所占了个坑,但是用完不冲水就走了,后面的人都没法用了。
【问题分析】
动态内存分配:
new int(42)
在堆上分配了一块内存,并将指针p
指向这块内存。内存泄漏:当函数
leakMemory
结束时,指针p
是局部变量,它的生命周期结束了,但p
指向的内存并没有被释放(即没有调用delete p
)。这块内存将永远无法被回收,导致内存泄漏。
【内存泄漏的后果】
内存浪费:程序占用的内存会逐渐增加,最终可能导致系统内存耗尽。
性能下降:内存泄漏会导致程序运行变慢,甚至崩溃。
难以调试:内存泄漏通常不会立即导致程序崩溃,而是随着时间推移逐渐显现问题,因此很难定位。
【如何避免】
1.手动管理内存:确保每次使用
new
分配内存后,都要在适当的地方调用delete
释放内存。2.使用智能指针:现代 C++ 推荐使用智能指针(如
std::unique_ptr
或std::shared_ptr
)来自动管理内存,避免手动调用delete
。
std::unique_ptr
:独占所有权,内存会在指针离开作用域时自动释放。
std::shared_ptr
:共享所有权,内存会在最后一个指向它的shared_ptr
离开作用域时释放。3.避免裸指针:尽量避免直接使用裸指针(如
int*
),而是使用容器(如std::vector
、std::array
)或智能指针来管理资源。4.使用 RAII 原则:RAII(Resource Acquisition Is Initialization)是 C++ 的核心思想之一,通过对象的生命周期来管理资源(如内存、文件句柄等)。智能指针就是 RAII 的典型应用。
void noLeak() {
int* p = new int(42);
//使用p
// 用完了记得delete
delete p;
p = nullptr; // 删除后最好置空
}
//2.1
#include <memory>
void noLeakMemory() {
std::unique_ptr<int> p = std::make_unique<int>(42); // 动态分配内存
// 不需要手动释放内存,智能指针会自动管理
} // p 离开作用域时,内存自动释放
//2.2
#include <memory>
void noLeakMemory() {
std::shared_ptr<int> p = std::make_shared<int>(42); // 动态分配内存
// 不需要手动释放内存,智能指针会自动管理
} // p 离开作用域时,内存自动释放
内存泄漏是 C++ 中常见的问题,通常是由于动态分配的内存没有被正确释放导致的。为了避免内存泄漏:
尽量使用智能指针或容器来管理资源。
如果必须使用裸指针,请确保在适当的地方调用
delete
。遵循 RAII 原则,将资源管理与对象的生命周期绑定。
错误三:解引用空指针
int* p = nullptr;
*p = 100; // 程序崩溃!这就像试图往一个不存在的盒子里放东西
在使用指针之前,一定要检查:
//1.检查指针
int* p = nullptr;
if (p != nullptr) {
*p = 100;
} else {
std::cout << "指针是空的,可不能用!" << std::endl;
}
//2.new
int* p = nullptr; // 初始化为 nullptr
p = new int; // 动态分配内存
*p = 100; // 现在可以安全地解引用 p
std::cout << *p << std::endl; // 输出 100
delete p; // 使用完毕后释放内存
//3.malloc
int* p = nullptr; // 初始化为 nullptr
p = (int*)malloc(sizeof(int)); // 动态分配内存
if (p != nullptr) { // 检查分配是否成功
*p = 100; // 现在可以安全地解引用 p
std::cout << *p << std::endl; // 输出 100
free(p); // 使用完毕后释放内存
}
//4.指针指向现有变量
int value = 42; // 定义一个变量
int* p = nullptr; // 初始化为 nullptr
p = &value; // 让 p 指向 value
*p = 100; // 现在可以安全地解引用 p
std::cout << value << std::endl; // 输出 100
//5.使用智能指针
#include <memory>
std::unique_ptr<int> p = std::make_unique<int>(42); // 动态分配内存并初始化为 42
*p = 100; // 现在可以安全地解引用 p
std::cout << *p << std::endl; // 输出 100
// 不需要手动释放内存,智能指针会自动管理
#include <memory>
std::shared_ptr<int> p = std::make_shared<int>(42); // 动态分配内存并初始化为 42
*p = 100; // 现在可以安全地解引用 p
std::cout << *p << std::endl; // 输出 100
// 不需要手动释放内存,智能指针会自动管理
错误四: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是局部变量,函数结束就没了
}
这就像你要借别人的东西,但是人家已经搬家了,你上哪借去?
正确做法:
//1.返回动态分配的内存
int* getDynamicPtr() {
int* p = new int(42); // 动态分配内存
return p; // 返回指针
}
int main() {
int* p = getDynamicPtr();
std::cout << *p << std::endl; // 输出 42
delete p; // 释放内存
return 0;
}
//2.智能指针
#include <memory>
std::unique_ptr<int> getUniquePtr() {
return std::make_unique<int>(42); // 动态分配内存并返回智能指针
}
int main() {
std::unique_ptr<int> p = getUniquePtr();
std::cout << *p << std::endl; // 输出 42
// 不需要手动释放内存,智能指针会自动管理
return 0;
}
//3.返回静态或全局变量
int* getStaticPtr() {
static int x = 42; // 静态变量,生命周期持续到程序结束
return &x; // 返回静态变量的地址
}
int main() {
int* p = getStaticPtr();
std::cout << *p << std::endl; // 输出 42
return 0;
}
//4.返回值,而非指针
int getValue() {
int x = 42;
return x; // 返回值
}
int main() {
int value = getValue();
std::cout << value << std::endl; // 输出 42
return 0;
}
错误八:指针类型不匹配
double d = 3.14;
int* p = &d; // 错!类型不匹配
正确做法:
double d = 3.14;
double* p = &d; // 对!类型要匹配
错误九:多重指针不打基础
int** pp; // 指向指针的指针
*pp = new int(42); // 危险!底下一块积木都没放就想往上叠
//未初始化的指针被解引用
【问题分析】
未初始化的指针:
int** pp;
声明了一个指向指针的指针pp
,但此时pp
并没有被初始化,它的值是未定义的(即它可能指向任意内存地址)。解引用未初始化的指针:
*pp = new int(42);
试图通过pp
解引用并赋值。由于pp
是未初始化的,它可能指向任意内存地址,解引用未初始化的指针会导致未定义行为(Undefined Behavior)
正确的搭法:
// 一层一层来,稳稳当当
int* p = new int(42); // 先放好底层积木
int** pp = &p; // 再往上叠一块
cout << **pp << endl; // 现在这积木稳当,可以安全使用了
//1.初始化指针
int* p = nullptr; // 初始化一个指针
int** pp = &p; // 初始化指向指针的指针
*pp = new int(42); // 现在可以安全地解引用 pp
std::cout << **pp << std::endl; // 输出 42
delete *pp; // 释放内存
//2.动态分配内存
int** pp = new int*; // 动态分配内存
*pp = new int(42); // 现在可以安全地解引用 pp
std::cout << **pp << std::endl; // 输出 42
delete *pp; // 释放内存
delete pp; // 释放指向指针的指针
//3.智能指针
#include <memory>
void safeMemoryManagement() {
std::unique_ptr<int> p = std::make_unique<int>(42); // 动态分配内存
std::unique_ptr<int>* pp = &p; // 指向智能指针的指针
std::cout << **pp << std::endl; // 输出 42
} // p 离开作用域时,内存自动释放
记住:多重指针就像搭积木,得从底层开始,一层一层稳妥地往上搭,跳着搭就容易倒塌!
错误十: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
【总结】
指针就是个地址,搞清楚这个地址指向哪,什么时候有效,什么时候无效,基本就能避免大多数问题了。