目录
- 指针到底是什么
- 声明、取址与解引用
- nullptr、野指针与悬垂指针
- const 与指针的三种组合
- 指针与数组、指针算术
- 指针作为函数参数
- 函数指针
- 动态内存:new 与 delete
- 智能指针:unique / shared / weak
1. 指针到底是什么
程序运行时,每个变量都住在内存的某个地址上。你可以把内存想象成一长排编号的房间,每个房间有一个唯一的门牌号(地址),里面放着变量的值。而指针就是一个用来存放「地址」的变量——它不直接存你要的数据,而是告诉你数据住在哪。
为什么要绕这么一层?因为很多场景下我们并不知道数据在内存中的确切位置,只能在运行时通过地址去「找到」并操作它:动态分配的对象、数组遍历、函数的按引用传递、以及构建链表 / 树等数据结构,全都依赖指针。
内存(示意) ┌──────────────────┐ ┌──────────────────┐ │ 地址 0x7ffe1298 │ │ 地址 0x7ffe12a0 │ │ p = 0x7ffe12a0 │ ───────────────► │ x = 42 │ └──────────────────┘ 指向 └──────────────────┘ 指针 p(存 x 的地址) 变量 x(int)一句话记忆:普通变量存值,指针变量存地址。指针就是「地址的容器」。
2. 声明、取址与解引用
三个符号要分清:int* p声明「p 是指向 int 的指针」;一元运算符&取地址;一元运算符*(解引用)取指针指向位置的值。
#include<iostream>intmain(){intx=42;int*p=&x;// &x 取出 x 的地址,存入指针 pstd::cout<<x<<"\n";// 值:42std::cout<<&x<<"\n";// 地址:0x7ffe12a0std::cout<<p<<"\n";// 地址:0x7ffe12a0(和 &x 相同)std::cout<<*p<<"\n";// 解引用,得 42*p=100;// 通过指针修改 x 的值std::cout<<x<<"\n";// 现在是 100return0;}声明时的*是「类型的一部分」,运行时的*才是解引用,别混淆。三种写法等价,都是指针:
int*p1;// 星号靠近类型(推荐,风格清晰)int*p2;// 星号靠近变量名int*p3;// 同一行声明多个,注意:int* p, q 中 q 是 int 而非指针易错点:
int* p, q;里q是普通int,不是指针。要两个指针请写int *p, *q;。
指针本身也有大小。64 位系统上,无论指向什么类型,指针都是 8 字节(存的是地址);32 位系统上是 4 字节。
3. nullptr、野指针与悬垂指针
指针刚诞生时最常见的问题,就是它到底「指向哪里」。有三种危险状态必须分清:
| 名称 | 含义 | 示例 |
|---|---|---|
| 空指针 | 不指向任何对象的指针,用nullptr表示(C++11 起) | int* p = nullptr; |
| 野指针 | 未初始化就使用的指针,指向随机地址 | int* p; *p = 5; |
| 悬垂指针 | 曾指向有效对象,但对象已被销毁,指针还残留旧地址 | delete p;之后再解引用*p |
空指针其实是最「安全」的一种,因为你可以显式检查它再解引用:
int*p=nullptr;if(p!=nullptr){// 非空才解引用*p=1;}else{std::cout<<"p 是空的,不能解引用";}为什么用 nullptr 而不用 NULL:在旧代码里
NULL常被定义为整数0,会造成重载歧义(比如同时有接收int和接收指针的重载时,NULL会被当成整数)。nullptr是真正的空指针常量,类型为std::nullptr_t,语义干净。
悬垂指针最隐蔽:释放内存后地址还在,但内容已经被回收,解引用属于未定义行为,可能崩溃也可能得到脏数据。习惯是释放后立刻置空:
int*p=newint(10);deletep;p=nullptr;// 置空,避免悬垂4. const 与指针的三种组合
const放在*的左边还是右边,意思完全不同。诀窍:看const修饰的是「指针本身」还是「指针指向的东西」。
| 写法 | 含义 | 能否修改 *p / p |
|---|---|---|
const int* p | 指向 const int 的指针 | *p 不可改,p 可改 |
int* const p | const 指针(指针本身不可改) | *p 可改,p 不可改 |
const int* const p | 两者都不可改 | *p 不可改,p 不可改 |
inta=1,b=2;constint*p1=&a;// 只读指针:不能通过 p1 改 ap1=&b;// 可以:换个指向// *p1 = 99; // 编译错误:不能改指向的值int*constp2=&a;// 指针本身只读:不能再指向别处*p2=99;// 可以:通过它修改 a// p2 = &b; // 编译错误:不能换指向constint*constp3=&a;// 都只读最佳实践:函数参数「只想读、不想改」时,用const int*或const T&,既能防误改,也能把信息传达给调用者。
5. 指针与数组、指针算术
在大多数表达式里,数组名会自动退化为指向其首元素的指针。这是指针和数组纠缠不清的根源,也是指针算术的核心。
intarr[5]={10,20,30,40,50};int*p=arr;// 等价于 int* p = &arr[0];std::cout<<*p<<"\n";// 10:首元素std::cout<<*(p+2)<<"\n";// 30:p+2 是 &arr[2]std::cout<<p[3]<<"\n";// 40:p[i] 就是 *(p+i)指针算术的推进单位是「指向类型的大小」,不是 1 字节。p+1前进sizeof(int)字节,所以p+2才指向arr[2]。用循环遍历数组最常见:
int*end=arr+5;// 尾后指针(不指向元素,只作边界)for(int*it=arr;it!=end;++it){std::cout<<*it<<" ";// 逐个输出 10 20 30 40 50}数组名 vs 指针:两个区别
①sizeof(arr)是整个数组的字节数(5×4=20),而sizeof(p)只是指针本身的 8 字节。
② 数组名不是可修改的左值,不能做arr = p这样的赋值;指针可以。
把数组当参数传进函数时,它同样退化为指针——这意味着函数里拿不到数组长度,必须额外传一个长度参数(或用std::array/std::vector)。
6. 指针作为函数参数
传参有几种方式,效果不同。指针传参的核心价值:在函数内部修改调用者的变量,且不用拷贝整个大对象。
| 方式 | 函数内改动影响调用者吗 | 开销 |
|---|---|---|
传值void f(int x) | 不影响(改的是副本) | 拷贝一份 |
传指针void f(int* p) | 影响(通过*p修改原变量) | 只拷贝 8 字节地址 |
传引用void f(int& r) | 影响(语法更简洁,C++ 推荐) | 同上,语义上是别名 |
voidswap(int*a,int*b){intt=*a;*a=*b;// 通过指针修改调用者里的变量*b=t;}intmain(){intx=1,y=2;swap(&x,&y);// 传入地址,x、y 真的被交换了std::cout<<x<<" "<<y;// 2 1}处理「可能为空」的参数时,指针比引用灵活:调用者可以传nullptr表示「无」,函数里先判断;而引用不允许为空。
现代 C++ 的建议:需要修改调用者的对象,优先用引用;需要「可能没有对象」、或要表达「拥有 / 可空」语义时,用指针;传递大对象且只读,用
const T&。指针别滥用,能用引用就用引用。
7. 函数指针
函数本身也有地址——它在内存里也占一段空间。函数指针就是指向函数的指针,可以用来把函数当作参数传递(回调),或实现运行时动态选择行为。
intadd(inta,intb){returna+b;}intmul(inta,intb){returna*b;}int(*fp)(int,int)=add;// fp 指向 addstd::cout<<fp(3,4)<<"\n";// 7:直接调用fp=mul;// 换指向 mulstd::cout<<(*fp)(3,4)<<"\n";// 12:解引用后再调用intapply(int(*fn)(int,int),intx,inty){returnfn(x,y);// 把函数当参数传入(回调)}读函数指针声明有诀窍:int (*fp)(int,int)里,先看括号内fp是「指向……的指针」,再看它指向的东西——返回int、接收两个int的函数。C++11 起可用std::function或auto让这类声明更友好。
8. 动态内存:new 与 delete
栈上的局部变量在函数结束时自动销毁,而动态内存由new在堆上分配,必须由你手动用delete释放——否则就是内存泄漏。这是指针最经典的用途:你不知道运行时要多大空间,只能动态申请。
int*p=newint(7);// 分配一个 int,初始化为 7deletep;// 释放p=nullptr;// 置空防悬垂intn=100;int*arr=newint[n];// 动态数组,长度运行时才知道// ... 使用 arr[i] ...delete[]arr;// 注意:数组要用 delete[],不能写成 delete| 分配 | 对应释放 | 失败时 |
|---|---|---|
new T | delete | 抛std::bad_alloc |
new T[n] | delete[] | 同上 |
new (std::nothrow) T | delete | 返回nullptr,不抛异常 |
配对的铁律:
new与delete、new[]与delete[]必须严格配对。混用(比如用delete释放new[]的数组)是未定义行为。new T()(带括号)会做值初始化,new T对内置类型不做初始化。
手动管理内存极易出错——忘释放就是泄漏,释放两次就崩溃。这也是下一节智能指针存在的根本原因:用对象的析构函数自动回收内存。
9. 智能指针:unique / shared / weak
C++11 引入的智能指针把「谁拥有这块内存、谁负责释放」交给编译器管理,是现代 C++ 处理指针的首选。它仍是指针,但用起来像普通对象,离开作用域时自动释放。
| 类型 | 所有权模型 | 适用场景 |
|---|---|---|
unique_ptr | 独占所有权,不可复制,只可移动 | 绝大多数「我的对象归我管」的默认选择 |
shared_ptr | 共享所有权,引用计数,计数归零才释放 | 多个对象需要共同拥有同一资源 |
weak_ptr | 观察者,不增加引用计数 | 打破 shared_ptr 的循环引用、缓存等 |
#include<memory>autou=std::make_unique<int>(5);// 独占;作用域结束自动 delete// auto u2 = u; // 编译错误:unique_ptr 不可复制autou2=std::move(u);// 转移所有权autos=std::make_shared<int>(9);autos2=s;// 可复制,引用计数 +1// 两个指针都离开作用域后,内存才被释放std::weak_ptr<int>w=s;// 观察,不增计数if(autosp=w.lock()){// lock() 升级为 shared_ptr,为空则说明对象已销毁std::cout<<*sp;}循环引用陷阱:两个
shared_ptr互相持有对方,引用计数永远不为零,内存永远不释放。此时用weak_ptr打破环:让其中一方持weak_ptr观察,不参与计数。
智能指针同样支持箭头访问成员:u->method()与*u解引用,用法和裸指针几乎一致,却把「释放」这件事交给了 RAII(资源获取即初始化)。