史上最全 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

总结

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

责任编辑:赵宁宁 来源: 跟着小康学编程
相关推荐

2024-04-03 12:30:00

C++开发

2018-01-20 20:46:33

2023-11-22 13:22:51

C++函数

2024-08-26 08:29:55

2022-03-04 18:11:16

信服云

2023-11-01 15:32:58

2011-04-11 11:09:50

this指针

2021-06-18 12:30:36

C++函数指针编程语言

2022-01-23 14:29:25

C语言编程语言

2021-12-21 15:31:10

C++语言指针

2010-02-02 15:01:59

C++成员函数指针

2024-05-15 16:01:04

C++编程开发

2024-07-03 12:04:42

C++this​

2020-06-12 11:03:22

Python开发工具

2014-01-24 09:49:01

C++指针

2010-01-26 13:42:28

C++指针

2021-02-26 00:46:11

CIO数据决策数字化转型

2024-04-24 13:45:00

2019-10-17 09:58:01

深度学习编程人工智能

2016-01-28 14:41:06

CC++编码
点赞
收藏

51CTO技术栈公众号