C++ 指针与引用笔记:内存地址、数组与常见错误
C++ 指针与引用笔记:内存地址、数组与常见错误
指针是 C++ 里最容易让初学者头疼、也最强大的特性之一。它直接操作内存地址,既能写出高效的代码,也最容易引发难以排查的 bug。理解了“内存”和“地址”这两个概念,指针就不神秘了。引用则可以理解为变量的“别名”,用起来比指针更直观。这一篇把两者放在一起讲清楚,并补充一些进阶但常用的知识点。
一、为什么需要指针
程序运行时的变量都放在内存里,每块内存有一个编号,叫地址(你可以把它想成内存的“门牌号”)。很多时候我们不想复制整份数据,而想“指向”那份数据去操作它——比如:
- 函数里要修改外面的变量(而不是只改副本);
- 处理一大块数组或字符串,不想整体拷贝导致性能下降;
- 运行时才决定要多少内存(动态申请);
- 实现链表、树、图这类需要“节点之间互相指向”的数据结构。
这些场景都绕不开指针。可以说,指针是 C++ 直通底层能力的钥匙。
二、内存与地址
int a = 10;
cout << &a; // 打印 a 在内存里的地址,形如 0x...
& 是取地址运算符。变量 a 占了一块内存,里面存着 10,而 &a 就是那块内存的门牌号。不同变量、不同运行时,地址一般都不一样,所以不要假设地址固定。
三、指针的定义与使用
指针本身也是变量,只不过它存的是“别的变量的地址”:
int a = 10;
int* p = &a; // p 指向 a(p 里存的是 a 的地址)
cout << *p; // 解引用:取到 p 指向的东西,输出 10
*p = 20; // 通过指针改 a 的值
cout << a; // 输出 20
int* p表示 p 是“指向 int 的指针”,*在这里是“指针类型”的标志。&a取 a 的地址。*p叫解引用,访问它指向的内存里的数据。这个*和声明里的*含义不同,一个表类型一个表“取值”,初学者要慢慢区分。
四、指针类型必须匹配
int* 指针应该指向 int,double* 指向 double。类型决定了“从这个地址往后读多少字节、按什么方式解释”。如果把 int* 错误地指向 double 再解引用,会读错数据,结果是未定义的。所以指针的类型和它所指向的变量类型要一致。指向 char 的指针和指向 int 的指针,加 1 后跳过的字节数也不同(char 跳 1 字节,int 跳 4 字节),这也是类型重要的原因。
五、空指针 nullptr
指针不指向任何东西时,应赋值为 nullptr(C++11 起推荐,旧写法 NULL 也行):
int* p = nullptr;
if (p == nullptr) cout << "空指针";
永远不要对 nullptr 解引用,会直接崩溃。养成习惯:定义指针时立刻初始化,不知道指向哪就先给 nullptr。这样万一误用,至少行为是确定的,而不是去读一块随机地址。
六、指针的运算
指针加 1,不是加一个字节,而是“指向下一个同类型元素”:
int a[3] = {10, 20, 30};
int* p = a; // 数组名 a 本身就等于 &a[0]
cout << *p; // 10
cout << *(p + 1); // 20
p++; // 现在指向 a[1]
这种“按类型跨度移动”的特性,让指针遍历数组非常自然。两个指针相减得到的是它们之间的元素个数(而不是字节数)。
七、指针与数组的关系
数组名在大多数情况下会被当成指向首元素的指针,所以 a[i] 本质上就是 *(a + i)。遍历数组既可以用下标,也可以用指针:
for (int i = 0; i < 3; i++) cout << a[i] << " ";
for (int* p = a; p < a + 3; p++) cout << *p << " ";
两者完全等价,下标写法更易读,指针写法在某些底层场景更高效。但要注意:用指针遍历时,循环的结束条件 p < a + 3 不能写成 p != nullptr,因为数组末尾并没有一个空指针标记。
八、指针与函数:传值 vs 传地址
默认传值,函数拿到的是副本:
void swap(int x, int y) {
int t = x; x = y; y = t; // 只交换了副本
}
int main() {
int a = 1, b = 2;
swap(a, b);
cout << a << b; // 还是 1 2,没变
}
改成传指针才能真的交换:
void swap(int* x, int* y) {
int t = *x; *x = *y; *y = t;
}
int main() {
int a = 1, b = 2;
swap(&a, &b);
cout << a << b; // 2 1,成功交换
}
九、引用
引用是“变量的别名”,和原变量是同一个东西:
int a = 10;
int& r = a; // r 是 a 的别名
r = 20;
cout << a; // 20
引用和指针的区别:
| 对比 | 指针 | 引用 |
|---|---|---|
| 可否为空 | 可以(nullptr) | 不可以,必须初始化绑定 |
| 可否改指向 | 可以 | 不可以,绑定后永远是那个变量 |
| 使用方式 | 需要 * 解引用 |
直接当原变量用 |
| 本质 | 存地址的变量 | 变量的别名 |
引用常作函数参数,既避免复制大对象,又能修改原值,写法也更直观:
void swap(int& x, int& y) {
int t = x; x = y; y = t;
}
swap(a, b); // 比传指针好看多了
十、const 与指针/引用
const int* p:指向的内容不能改(指向常量的指针,p 自己还能改指向)。int* const p:指针本身不能改指向(p 永远指向同一个地址)。const int* const p:既不能改指向也不能改内容。const int& r:常量引用,常用来作函数参数,防止修改且避免拷贝大对象(比如传string时用const string&是标配)。
十一、动态内存 new / delete
需要运行时才决定大小的内存,用 new 申请,delete 释放:
int* p = new int(5); // 申请一个 int,初始化为 5
cout << *p;
delete p; // 用完了必须释放
int* arr = new int[10]; // 申请数组
delete[] arr; // 数组要用 delete[]
内存模型小知识:普通局部变量放在“栈”上,函数结束自动回收;new 出来的放在“堆”上,必须手动 delete,否则一直占着。这就是为什么忘记 delete 会内存泄漏。
十二、进阶:多级指针、指针数组、指针与字符串
- 多级指针:
int** pp指向“指向 int 的指针”,常用于二维动态数组、函数参数需要修改指针本身时。 - 指针数组:
int* arr[3]是“装了 3 个 int 指针的数组”,和“指向数组的指针”int (*p)[3]写法相似但含义不同,注意括号位置。 - 指针与字符串:C 风格字符串本质是
char*指向字符数组,以\0结尾;C++ 里优先用string,需要传底层字符时用.c_str()。
十三、常见危险
- 野指针:指针未初始化就使用,指向随机地址,解引用后果难料。
- 悬空指针:指向的内存已经被
delete,指针却还在用。 - 内存泄漏:
new了却忘了delete,程序跑久了内存越占越多,严重时会拖垮整个系统。 - 重复释放:同一块内存
delete两次,行为未定义。 - 返回局部变量地址:函数里的局部变量在函数结束后销毁,返回它的指针会得到悬空指针。
好习惯:指针初始化为 nullptr,delete 之后顺手把指针也置 nullptr,这样即使误用也能早点暴露问题。
十四、练习
- 写一个函数,用指针作为参数,把传入的整数加 1。
- 用指针遍历数组,把每个元素翻倍。
- 用引用作参数实现一个三个数排序的函数。
- 用
new动态申请一个长度为 n 的数组,读入 n 个数求平均值,最后delete[]。 - 写一个函数
findMax(int* a, int n),返回数组最大值(只用指针,不用下标)。 - 思考:为什么
swap(int* x, int* y)能交换外面的变量,而swap(int x, int y)不能?
十五、指针与字符串的深入
C++ 里字符串有两种形态。C 风格字符串是 char 数组,以 \0 结尾,常用 char* 指向它。这种字符串可以修改元素,适合需要就地操作的底层场景。但要注意,用字符串字面量直接初始化的 char*,指向的是只读常量区,不能修改,强行改会运行时崩溃。所以现代写法推荐用 string,既安全又方便,需要把 string 传给只认 char* 的老接口时,用 s.c_str() 拿到底层字符指针即可。理解这两者区别,能避开一大类“为什么改不了字符串”的困惑。读入一整行文本也优先用 getline(cin, s) 配合 string,比处理 char 数组省心。
十六、函数指针简介
函数指针就是“指向函数的指针”,可以把函数当参数传给另一个函数,实现“回调函数”。比如排序时想自定义比较规则,就可以传一个比较函数进去。概念听起来玄,其实和变量指针一样:先定义指针类型,再让它指向某个函数,最后通过指针去调用。竞赛和项目里常用在事件处理、排序定制等场景,初学了解概念即可,不必深究实现细节,但要知道“函数也可以被当作数据传来传去”这件事。
十七、内存布局一图流
程序运行时,内存大致分几块:代码区放指令,全局区放全局和静态变量,栈区放局部变量(自动分配自动回收、空间小但快),堆区放 new 出来的东西(手动管理、空间大但要记得释放)。理解“局部变量在栈上、函数结束就失效”这一点,就能解释为什么不能返回局部变量的地址——那个地址指向的栈空间已经被回收了。递归写得太深也会撑爆栈,导致栈溢出;而堆上的大数组只要记得释放就能长期存在。堆和栈的区别,决定了为什么大数组要放全局或堆上。
十八、几个容易混淆的写法
*p++:先取 *p 的值,然后指针 p 自增指向下一个(后缀 ++ 优先级更高)。(*p)++:p 指向的那个值加 1,指针本身不动。*(p++) 和 *p++ 等价。面试和考试常考这些,记住核心:后缀 ++ 作用在指针上,加括号能改变作用对象。多级指针 `int** 指向“指向 int 的指针”,二维动态数组、或需要函数修改指针本身时会用到,理解了一级指针,多级只是套娃,不必害怕。遇到int p[3]和int (p)[3]` 也要分清:前者是指针数组,后者是指向数组的指针。
十九、练习拓展
- 写一个函数
int* find(int* a, int n, int x),在数组中查找 x 并返回它的地址(找不到返回 nullptr)。 - 用
new申请一个二维数组(先 new 一维指针数组,再为每个指针 new 一行),用完记得按相反顺序delete。 - 思考:为什么
swap(int* x, int* y)能交换外面的变量,而swap(int x, int y)不能?用自己的话写一遍理由。
二十、指针学习路线图与避坑
学指针最容易卡在“地址”和“解引用”这两个全新概念上。建议一开始就在纸上画内存格子,标出每个变量和它的地址,再手动模拟 *p、p++、*p++ 的结果,然后写小程序验证你的手算对不对。卡住时回到最原点的一句话:“指针里存的是地址,解引用才拿到数据”,大部分困惑都能迎刃而解。
另一个高频坑是野指针和悬空指针。养成两个习惯能避开绝大多数事故:第一,定义指针立刻初始化,不知道指哪就给 nullptr;第二,delete 之后顺手把指针也置 nullptr,这样即使后面误用,程序也会在确定性的地方崩溃,而不是去读一块随机内存制造玄学 bug。掌握了指针,链表、树、图这些需要“节点互相指向”的数据结构才真正向你敞开大门,C++ 的威力也才真正显现出来。
二十一、一句话总结怎么选
日常写代码,能用引用就别用指针:引用更安全、写法更干净,也不容易出野指针。只有在必须表示“可能没有”(用 nullptr 表达空值)、需要重新指向别处、或者做底层数据结构(比如链表节点指针、树的孩子指针)时,才用指针。这个简单的取舍,能让你大部分时间远离指针带来的麻烦,只在真正需要时才动用它的威力。把引用当默认、把指针当特例,是写好 C++ 的一条省力心得。
小结
指针难,是因为它把“变量”和“变量的地址”分开了,一不小心就会指向不该指的地方。初学时记住三条保命原则:定义即初始化、不用的置 nullptr、new 和 delete 成对出现。引用则是更安全好用的“别名”,能代替指针的地方尽量用引用。把这两样吃透,C++ 的大门就真正打开了,后面学数据结构、学算法都将得心应手。
jhp.studio