从C到C++:指针、引用与内存管理的新思维
学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++里指针的新变化
- 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++新增的特性,相当于给变量起了个别名。
- 基本用法
int a = 10;
int& b = a; // b是a的引用,就是a的别名
b = 20; // 修改b就是修改a
cout << a << endl; // 输出20
引用的特点:
必须初始化,不能"空引用"
一旦绑定,就不能再指向别的变量
操作引用就是操作原变量,不用解引用 - 引用 vs 指针
表格
特性 引用 指针
必须初始化 是 否
可以为空 否(理论上) 是
可以改变指向 否 是
需要解引用 否 是
大小 和原变量一样大 指针本身有大小(4或8字节)
怎么选?
优先用引用,更安全、更直观
需要改变指向、需要为空的时候,用指针
函数传参的时候,能用引用就别用指针(除非传空是合理的) - 引用的常见用途
用途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 与指针、引用的组合
这块刚学的时候特别容易搞混,什么常量指针、指针常量…各种排列组合。
- 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,避免误用。
七、一些实用建议
- 空指针用 nullptr,别用 NULL
更安全,类型更准确。 - 传参优先用引用,其次用指针
需要修改实参或者避免拷贝的时候,优先用引用。只有当"传空"是合理情况的时候,才用指针。 - 不修改就加 const
函数参数、返回值、成员函数…能加const的地方都加上。既安全,又能让编译器帮你查错。 - 用容器代替手动分配的数组
vector、string这些容器已经帮你把内存管理好了,别自己new数组了。
写在最后
指针这东西,C里是又爱又恨——爱它的灵活,恨它的容易出错。
到了C++,多了引用、智能指针这些工具,指针的"杀伤力"被控制住了不少。虽然刚开始学的时候觉得概念更多了,但用习惯了之后会发现:写代码的时候操心内存的时间少了,专注逻辑的时间多了。
当然,指针的基本功还是要扎实的——毕竟底层的原理没变,只是多了一层包装。理解了指针,才能理解引用和智能指针到底是怎么回事。
更多推荐
所有评论(0)