C++:指针避坑指南

目录

错误一:野指针

错误二:忘记删堆内存

错误三:解引用空指针

错误四:delete指针后继续使用

错误五:数组用单个delete删除

错误六:指针运算越界

错误七:返回局部变量的指针

错误八:指针类型不匹配

错误九:多重指针不打基础

错误十:const和指针的位置摆错

错误十一:构造函数漏初始化指针

错误十二:函数参数传递指针没声明const

错误十三:指针移动导致内存释放失败

错误十四:指针和引用混用

错误十五:不安全的指针向下转换

错误十六:函数指针调用前未检查

错误十七:在类里delete this 指针

错误十八:智能指针互相引用

错误十九:指针成员的深浅拷贝

错误二十:函数内修饰指针实参

实战小贴士 

1.优先使用智能指针

2.指针安全法则

3.关于指针和引用的选择

4.代码规范建议


今天我们用通俗的语言来聊一聊C/C++指针中的一些坑。指针是否经常使得你头大?代码莫名其妙就奔溃?是否有出现一下错误?

错误一:野指针

int* p;//声明一个指针,但没有初始化
*p = 10;//完了,这就是传说中的野指针

这是一只没有栓绳的野狗!就好比你养了一条狗,没有给它栓绳,它想跑哪就跑哪去,最后把邻居家的小猫小鸡花花草草给祸害了...

【问题分析】

  1. 未初始化的指针int* p; 声明了一个指向 int 类型的指针 p,但此时 p 并没有被初始化,它的值是未定义的(即它可能指向任意内存地址)。

  2. 野指针:当你试图通过 *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
}  // 内存泄漏!这块内存永远要不回来了

 这就是在浪费资源呀!这就像你上厕所占了个坑,但是用完不冲水就走了,后面的人都没法用了。

【问题分析】

  1. 动态内存分配new int(42) 在堆上分配了一块内存,并将指针 p 指向这块内存。

  2. 内存泄漏:当函数 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::vectorstd::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);  // 危险!底下一块积木都没放就想往上叠

//未初始化的指针被解引用

【问题分析】 

  1. 未初始化的指针int** pp; 声明了一个指向指针的指针 pp,但此时 pp 并没有被初始化,它的值是未定义的(即它可能指向任意内存地址)。

  2. 解引用未初始化的指针*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>();
// 不用管删除,超出作用域自动清理

记住

  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. 智能指针循环引用会导致内存泄漏,就像两个人一直拉着手,永远不能回家

注意:智能指针的循环引用很容易把人绕晕,我用两张手绘小图,带大家一步步理解这个过程:

循环引用图解

说明

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

  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

【总结】

指针就是个地址,搞清楚这个地址指向哪,什么时候有效,什么时候无效,基本就能避免大多数问题了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值