C/C++ 指针避坑20条

一:野指针-这是个没拴绳的野狗啊!

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>();
// 不用管删除,超出作用域自动清理

记住

  1. 在类的方法里删除 this指针就像自杀,死了还想干活那肯定不行

  2. 对象的生命周期最好交给外部代码或智能指针管理

  3. 如果非要在类里面删除自己,那删完就立即返回,别做其他操作

十八:智能指针互相引用 - 这是在手拉手绕圈圈!

循环引用示例

// 错误示例:两个朋友互相拉手不放
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);    
// 现在好了,放学后可以松手回家了(正常释放内存)

记住

  1. 两个对象用shared_ptr互相引用,就像两个人死死拉住对方的手不放,谁都走不了

  2. 要解决这个问题,让一方改用weak_ptr,就像轻轻牵手就好,需要的时候随时可以松开

  3. 智能指针循环引用会导致内存泄漏,就像两个人一直拉着手,永远不能回家

错误十九:指针成员的深浅拷贝 - 很容易翻车!

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. 函数参数是传值的,修改指针形参不会影响外面的指针

  2. 要修改外部指针,必须传入指针的指针

  3. 这个问题在做指针操作时特别常见,很多人都会犯这个错

实战小贴士

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

总结

看完这些指针的坑,是不是觉得其实也没那么可怕?记住一点:指针就是个地址,搞清楚这个地址指向哪,什么时候有效,什么时候无效,基本就能避免大多数问题了。

相关推荐
YH_DevJourney7 分钟前
Linux-C/C++《C/9、信号:基础》(基本概念、信号分类、信号传递等)
linux·c语言·c++
让我们一起加油好吗31 分钟前
【数学】数论干货(疑似密码学基础)
c语言·visualstudio·密码学
终极定律33 分钟前
qt:输入控件操作
开发语言·qt
FL162386312942 分钟前
[C++]使用纯opencv部署yolov12目标检测onnx模型
c++·opencv·yolo
JenKinJia1 小时前
Windows10配置C++版本的Kafka,并进行发布和订阅测试
开发语言·c++
煤炭里de黑猫1 小时前
Lua C API :lua_insert 函数详解
开发语言·lua
笨鸟笃行1 小时前
爬虫第七篇数据爬取及解析
开发语言·爬虫·python
编程乐趣1 小时前
一文掌握DeepSeek本地部署+Page Assist浏览器插件+C#接口调用+局域网访问!全攻略来了!
开发语言·c#
java1234_小锋1 小时前
一周学会Flask3 Python Web开发-response响应格式
开发语言·python·flask·flask3
Jelena157795857921 小时前
使用Java爬虫获取1688 item_get_company 接口的公司档案信息
java·开发语言·爬虫