学C的时候,指针是劝退级别的存在。到了C++,指针不仅还在,还多了个"亲戚"叫引用,还有new/delete…
刚开始觉得C++怎么更复杂了?后来慢慢发现——这些新东西不是为了增加复杂度,而是为了让指针用起来更安全、更优雅。
这篇就聊聊从C转C++时,指针与内存管理这块的变化和心得。
一、老伙计:C风格的指针
先快速过一遍,C里的指针用法C++基本都支持:
int a = 10;
int *p = &a; // 指向int的指针
*p = 20; // 解引用,修改a的值

int arr[10];
int *p2 = arr; // 数组名退化为指针

void func(int *p); // 指针作为函数参数
int *func2(); // 指针作为返回值

指针的各种操作:加减、比较、解引用、数组指针、函数指针…C++里都一样。
😅 回忆一下:刚学C的时候,谁没被指针搞晕过?int* p、int p、int p, q… 各种写法含义还不一样,太考验眼神了。
二、C++里指针的新变化

  1. nullptr:更安全的空指针
    C里我们用NULL表示空指针,其实NULL就是个宏,定义为((void*)0)或者0。
    但NULL有个问题:它本质上是个整数0,在函数重载的时候会出问题:
    void func(int x) {
    cout << “int版本” << endl;
    }

void func(char *p) {
cout << “指针版本” << endl;
}

func(NULL); // 调用哪个?

答案是调用int版本!因为NULL本质是0,是整数。这就很坑了。
C++11引入了nullptr,专门表示空指针,类型是nullptr_t,可以隐式转换成任何指针类型,但不会转换成整数:
func(nullptr); // 调用指针版本,正确!
建议:C++里空指针一律用nullptr,别用NULL了。更安全,也更清晰。
2. new / delete:C++版的内存分配
C里用malloc/free管理动态内存,C++有了更好用的new/delete:
// 分配单个对象
int *p = new int; // 分配一个int,未初始化
int *p2 = new int(10); // 分配一个int,初始化为10
// 释放
delete p;
delete p2;
// 分配数组
int *arr = new int[10]; // 分配10个int的数组
// 释放数组
delete[] arr;
new vs malloc的区别:
new 是C++运算符,malloc 是C库函数
new 会调用构造函数,malloc 只分配内存(后面讲类的时候会深有体会)
new 直接指定类型,返回对应类型的指针,不用强转
new 失败会抛异常,malloc 失败返回NULL
new/delete 要配对,new[]/delete[] 要配对
重要:new 出来的数组,一定要用 delete[] 释放,不能用 delete。反之亦然。虽然有些编译器上可能碰巧不崩,但这是未定义行为。
个人感受:刚学C++的时候觉得手动管理内存也没啥,直到写了个几千行的项目,到处都是内存泄漏和野指针,才知道智能指针有多香。
三、引用:指针的"优雅替代品"
引用是C++新增的特性,相当于给变量起了个别名。

  1. 基本用法
    int a = 10;
    int& b = a; // b是a的引用,就是a的别名
    b = 20; // 修改b就是修改a
    cout << a << endl; // 输出20
    引用的特点:
    必须初始化,不能"空引用"
    一旦绑定,就不能再指向别的变量
    操作引用就是操作原变量,不用解引用
  2. 引用 vs 指针
    表格
    特性 引用 指针
    必须初始化 是 否
    可以为空 否(理论上) 是
    可以改变指向 否 是
    需要解引用 否 是
    大小 和原变量一样大 指针本身有大小(4或8字节)
    怎么选?
    优先用引用,更安全、更直观
    需要改变指向、需要为空的时候,用指针
    函数传参的时候,能用引用就别用指针(除非传空是合理的)
  3. 引用的常见用途
    用途1:函数传参(修改实参)
    void swap(int& a, int& b) {
    int tmp = a;
    a = b;
    b = tmp;
    }
    swap(x, y); // 直接传变量,不用取地址,比指针直观多了
    用途2:函数传参(避免拷贝大对象)
    // 传值会拷贝整个vector,慢
    void process(vector v);

// 传引用,不用拷贝,快
void process(const vector& v); // 不修改就加const,更安全
用途3:函数返回引用
// 比如重载[]运算符的时候
int& vector::operator[](int index) {
return data[index];
}
// 这样就可以直接赋值了
v[0] = 10;
注意:不要返回局部变量的引用!函数结束局部变量就销毁了,引用变成悬空的,和返回局部变量指针一样危险。
四、const 与指针、引用的组合
这块刚学的时候特别容易搞混,什么常量指针、指针常量…各种排列组合。

  1. const 和指针
    const int *p1; // 指向常量的指针:指向的值不能改,指向可以改
    int const p2; // 和上面一样,const在左边都是指向常量

int const p3 = &a; // 指针常量:指针本身不能改,指向的值可以改
// const在
右边就是指针本身是常量

const int *const p4 = &a; // 两者都不能改

记忆小技巧:看const在*的左边还是右边
左边:指向的东西是const(不能通过指针修改值)
右边:指针本身是const(指针不能改指向)
2. const 和引用
int a = 10;
const int& r1 = a; // 常量引用:不能通过引用修改a的值
r1 = 20; // 错误!

int& const r2 = a; // 这个没啥意义,因为引用本来就不能改绑定

常量引用用得特别多,特别是函数传参的时候:
传const引用:避免拷贝,又保证不会修改原对象
还能接受右值(临时对象)

void print(const string& s) {
cout << s << endl;
}
print(“hello”); // 可以,临时字符串绑定到const引用

五、我的踩坑记录
坑1:new 数组用 delete 释放
int *arr = new int[10];
delete arr; // 错误!应该用 delete[] arr
少了个[],结果是未定义的。可能只调用第一个元素的析构函数,可能直接崩溃。
坑2:返回局部变量的引用/指针
int& func() {
int a = 10;
return a; // 错误!a是局部变量,函数结束就销毁了
}
这种错误编译器可能会给警告,但不一定报错。用的时候会出各种奇怪的问题。
坑3:悬空指针和野指针
int *p = new int;
delete p;
*p = 10; // 错误!p已经被释放了,是悬空指针
释放内存后,最好把指针置为nullptr,避免误用。
七、一些实用建议

  1. 空指针用 nullptr,别用 NULL
    更安全,类型更准确。
  2. 传参优先用引用,其次用指针
    需要修改实参或者避免拷贝的时候,优先用引用。只有当"传空"是合理情况的时候,才用指针。
  3. 不修改就加 const
    函数参数、返回值、成员函数…能加const的地方都加上。既安全,又能让编译器帮你查错。
  4. 用容器代替手动分配的数组
    vector、string这些容器已经帮你把内存管理好了,别自己new数组了。
    写在最后
    指针这东西,C里是又爱又恨——爱它的灵活,恨它的容易出错。
    到了C++,多了引用、智能指针这些工具,指针的"杀伤力"被控制住了不少。虽然刚开始学的时候觉得概念更多了,但用习惯了之后会发现:写代码的时候操心内存的时间少了,专注逻辑的时间多了。
    当然,指针的基本功还是要扎实的——毕竟底层的原理没变,只是多了一层包装。理解了指针,才能理解引用和智能指针到底是怎么回事。

更多推荐