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