资讯动态

第六章 指针

发布时间:2026/10/29 9:50:50 来源:尧图企业网站定制
第六章 指针一、指针与指针变量1.1 地址是什么内存是一整条按字节编号的街。每个字节一个编号从零开始往上排。变量占连续的几间房int 占四间double 占八间char 占一间。首字节那个编号就是这个变量的地址。┌────────────────────────────────────────────┐ │ 地址就是门牌号 │ ├────────────────────────────────────────────┤ │ 内存按字节编号变量占连续的几间 │ │ │ │ ------------------------------------ │ │ |地址 |住着 |值 | │ │ ------------------------------------ │ │ |4096 |a |78 | │ │ |4100 |b |92 | │ │ ------------------------------------ │ │ │ │ 记下首字节的编号 │ │ 门牌只在这趟程序里作数 │ └────────────────────────────────────────────┘地址本身是个整数一长串通常按十六进制写前面带 0x。 是一元取地址运算符放在变量前面拿它的地址。int a 78; cout a endl; // 一长串十六进制 cout (void*)a endl; // 转 void* 再打稳妥姿势cout 见到指针得分类型对待。char* 会被当成字符串打内容不打地址别的指针类型主流编译器给的是地址。想让所有指针都打地址转成 void* 是通用做法。还有个细节每次运行地址可能都不一样。操作系统有个地址空间随机化的机制专门防着有人摸清内存布局。所以地址看个模样就行别把某次运行的具体地址当常量写进代码。1.2 指针变量声明与初始化指针变量是专门存地址的变量。声明在类型和名字之间加一个星号。int a 78; int *p a; // p 存 a 的地址 double d 1.5; double *q d;星号贴哪儿有两种风格int* p 和 int *p 都合法意思一模一样。社区为这个吵过很多年没有结论。本书用贴着变量名的写法理由只有一个一行声明多个指针时不看错。int *p, *q; // 两个都是指针 int* p, q; // p 是指针q 是 int星号只管一个类型不是装饰是编译器干活的依据。p 加一跳多少字节*p 读几个字节全靠 int 这两个字母。类型看错读出来的数就是乱的。初始化比普通变量更要紧。没初始化的普通变量读出来是垃圾数没初始化的指针去解引用等于往一个随机地址写数据程序当场炸都是轻的。所以指针声明的时候要么给地址要么给 nullptr。int *p nullptr; // 空指针不指向任何东西 int *bad; // 野指针谁都不知道它指哪 *bad 5; // 大概率当场崩溃1.3 取地址与解引用 记下门牌* 照门牌推门。两个操作互为反作用*(a) 就是 a。┌─────────────────────────────────────────────────┐ │ 一对反操作 │ ├─────────────────────────────────────────────────┤ │ 记门牌* 按门牌进屋 │ │ │ │ int a 78; a 住在 4096 │ │ int *p a; p 记下 4096 │ │ *p 80; 顺着门牌进屋78 换成 80 │ │ │ │ 和 * 配成一对 │ │ 左边写右边读两条路 │ └─────────────────────────────────────────────────┘解引用在等号两边是两个方向左边改数据右边读数据。int a 78; int *p a; cout *p endl; // 读78 *p 80; // 写a 变成 80 cout a endl; // 80星号在声明里和在使用里干两个活儿。int *p 里的星号是说 p 的类型是指针*p 里的星号是说去取 p 指向的那份数据。记住一条声明处看类型使用处看动作。空指针解引用是经典事故。nullptr 就是给这种情况准备的解引用之前先查一眼这个习惯能省一堆调试时间。1.4 指针的大小指针变量自己占多大三十二位系统四字节六十四位系统八字节。关键是它跟指向的类型无关int* 和 double* 一样大。int *p nullptr; double *q nullptr; char *r nullptr; cout sizeof(p) sizeof(q) sizeof(r) endl; // 64 位系统下 8 8 8指针里存的是地址地址长度固定所以指针大小固定。类型信息编译期用完就扔运行时指针里只有地址。知道这条能避一个错觉别以为 double* 比 int* 大它俩一样大。二、指针的运算指针不是整数不能随便加减乘除。能做的运算只有几种每一种都围着地址打转。2.1 加减整数指针加一不是地址加一字节是跳到下一个元素。跳多远看它指向什么类型。int* 加一跳四字节char* 加一跳一字节double* 加一跳八字节。┌──────────────────────────────────────────────┐ │ 加一跳多远 │ ├──────────────────────────────────────────────┤ │ 同一个加一跳过的字节数不一样 │ │ │ │ -------------------------------------- │ │ |指针类型 |指向的类型 |加一跳 | │ │ -------------------------------------- │ │ |int * |int |4 字节 | │ │ |char * |char |1 字节 | │ │ |double * |double |8 字节 | │ │ -------------------------------------- │ │ │ │ 地址一样长步长不一样 │ │ 类型看错一步跨错一片 │ └──────────────────────────────────────────────┘int scores[5] {78, 92, 55, 100, 88}; int *p scores; // p 指向首元素 cout *(p 2) endl; // 55跳到第三格 cout p[2] endl; // 同一个数两种写法单位是元素不是字节这是指针运算最容易记错的一条。字节数是编译器算的写代码的时候心里数的是第几个。加减负数往回跳加减零原地不动都合法。字符串上的指针加减最直观一个字符一跳正好一格。char s[] hello; char *p s; cout *p endl; // h cout *(p 1) endl; // e cout *p endl; // 还是 hp 自己没动注意最后一行。p 1 是个新指针p 自己没动想挪窝得写 p p 1或者干脆 p 自增。2.2 指针相减两个同类型指针可以相减结果是中间差几个元素不是差几个字节。结果类型叫 ptrdiff_t有符号能表示前后关系。int scores[5] {78, 92, 55, 100, 88}; cout scores[4] - scores[0] endl; // 4 cout scores[0] - scores[4] endl; // -4五个元素的数组首尾下标差四指针相减也是四。这个操作在老代码里常用来算一段数据的元素个数现在有标准库手写的场合不多认得出就行。相加没意义两个地址加一块儿不知道指向哪儿编译器直接拦。2.3 指针比较指针之间能比大小等不等、谁在前谁在后都合法。同一数组内部下标小的地址也小这个顺序靠得住。int scores[5] {78, 92, 55, 100, 88}; int *p scores[1]; int *q scores[3]; cout (p q) endl; // 1 cout (p scores[1]) endl; // 1不同数组之间的指针比大小标准没给保证比出来可能是真的但没意义。要比就比同一段内存里的位置关系或者干脆只比相等。指针运算合法不合法一张表收拢。运算合法吗结果p n合法往后跳 n 个元素p - n合法往前跳 n 个元素p2 - p1合法差几个元素p1 p2不合法两个地址相加没意义p 乘个数不合法地址乘二没意义p q合法比相等p 小于 q同段内存合法比位置前后三、用指针处理一维数组3.1 数组名就是首元素地址数组名的本质是首元素的地址。把它和 scores[0] 打印出来一模一样。┌───────────────────────────────────────────────────────┐ │ 方括号是糖 │ ├───────────────────────────────────────────────────────┤ │ 数组名就是首元素的地址 │ │ │ │ --------------------------------------------- │ │ |scores[0]|scores[1]|scores[2]|scores[3]|scores[4]| │ │ --------------------------------------------- │ │ |78 |92 |55 |100 |88 | │ │ |4096 |4100 |4104 |4108 |4112 | │ │ --------------------------------------------- │ │ │ │ scores 和 scores[0] 是同一个地址 │ │ 想挪窝先拷给指针变量 │ └───────────────────────────────────────────────────────┘int scores[5] {78, 92, 55, 100, 88}; cout (scores scores[0]) endl; // 1这条一认前几章攒下的迷糊散了一半。为什么数组传进函数能改原数组因为传的根本就是地址。为什么方括号里能写变量当下标因为底层就是地址加法。数组名和指针变量只差一条数组名是常量改不了指向。scores 自增编译直接报错指针变量 p 自增随便跳。一个是钉死的门牌一个是随身带的小本本。int scores[5] {78, 92, 55, 100, 88}; scores; // 编译错数组名不能改 int *p scores; p; // 合法p 指向 scores[1]3.2 三种等价写法同一个格子三种写法编译出来一样。┌──────────────────────────────────────────────────────┐ │ 指的都是第三格 │ ├──────────────────────────────────────────────────────┤ │ 写法有三个指的是同一格 │ │ │ │ ---------------------------------------------- │ │ |写法 |样子 |指的是 | │ │ ---------------------------------------------- │ │ |下标 |scores[2] |第三格 | │ │ |地址加法 |*(scores 2) |同一格 | │ │ |指针下标 |p[2] |还是同一格 | │ │ ---------------------------------------------- │ │ │ │ p 指哪儿指针写法跟到哪儿 │ │ 下标锚在数组上指针锚在指针上 │ └──────────────────────────────────────────────────────┘int scores[5] {78, 92, 55, 100, 88}; cout scores[2] endl; // 55 cout *(scores 2) endl; // 55 int *p scores; cout p[2] endl; // 55方括号只是糖。scores[i] 到机器码里就是 *(scores i)先算地址再取数一步都没多。知道这个有两个好处。读老代码的时候指针写法混着下标写法出现不会发懵。写泛型代码的时候指针配加减法比下标通用下标只对数组有意义指针对任何一段连续内存都有意义。3.3 用指针遍历遍历不一定非写下标指针一样能走。int scores[5] {78, 92, 55, 100, 88}; for (int *p scores; p scores 5; p) { cout *p ; } cout endl; // 78 92 55 100 88p 小于 scores 加五 是循环条件p 走到末元素的后一位停。这个后一位只拿来比大小不解引用合法。标准库里的 begin 和 end 就是这个思路一个指头一个指尾后一位。日常读写代码下标和范围 for 够用可读性更好。指针遍历的场合在另外两处。一段内存不知道是什么容器只有首地址和长度比如下一章要讲的函数参数。手写底层数据结构链表和树都指着它。3.4 数组进函数现在收拾开篇那颗雷。为什么 swap 传值不灵数组传参就灵。void swapValue(int a, int b) { int t a; a b; b t; } int main() { int x 10, y 20; swapValue(x, y); cout x y endl; // 10 20纹丝不动 }参数是副本。函数里那对 a 和 b 是另外两间房跟 x 和 y 各住各的换得再干净也传不回来。想动原数据得把地址传进去。┌────────────────────────────────────────────────────┐ │ 门牌换家具 │ ├────────────────────────────────────────────────────┤ │ 值传递换副本传地址才换得动 │ │ │ │ --------------------------------------------- │ │ |写法 |调完 x 和 y | │ │ --------------------------------------------- │ │ |void f(int a, int b) |纹丝不动改的是副本 | │ │ |void f(int *a, int *b) |真的换了进屋动的手 | │ │ --------------------------------------------- │ │ │ │ 副本改得再热闹原值纹丝不动 │ │ 不递门牌函数里白换 │ └────────────────────────────────────────────────────┘void swapPtr(int *a, int *b) { int t *a; *a *b; *b t; } int main() { int x 10, y 20; swapPtr(x, y); cout x y endl; // 20 10真的换了 }两个 在调用处把门牌号递过去。两个 * 在函数里顺着门牌推门动的才是原数据。一门之隔功夫全在星号和 上。数组传参是同一件事的另一个面。形参写 int a[]编译器看成 int *a地址传进来函数里改的就是原数组。void doubleAll(int a[], int n) { for (int i 0; i n; i) { a[i] * 2; } }这里藏着一个坑。数组退化成指针之后sizeof 就失灵了main 里 scores 是二十字节函数里 a 是八字节编译器数不出长度。┌───────────────────────────────────────────────┐ │ 数组名的隐身术 │ ├───────────────────────────────────────────────┤ │ 数组传进函数名字悄悄变了 │ │ │ │ ---------------------------------------- │ │ |main 里 |函数参数里 | │ │ ---------------------------------------- │ │ |int scores[5] |int a[] 或 int *a | │ │ |sizeof 得 20 |sizeof 得 8 | │ │ |改它就是改原数组 |改的也是原数组 | │ │ ---------------------------------------- │ │ │ │ sizeof 问不出长度元素个数自己数 │ │ 退化从写进参数表那刻开始 │ └───────────────────────────────────────────────┘所以长度必须单独传一个参数。这个 n 不是可选是必选所有数组参数的标准姿势。int scores[5] {78, 92, 55, 100, 88}; cout sizeof(scores) endl; // 20 doubleAll(scores, 5); for (int x : scores) { cout x ; } // 156 184 110 200 176四、动态内存分配前面的数组长度声明那刻定死。运行期想定长度比如让用户输一个数当人数数组做不到得上动态内存。4.1 new 与 deletenew 在堆上要一块内存递回来一个指针。delete 把这块内存还给系统。一借一还必须成对。int *p new int(42); // 要一间 int放 42 cout *p endl; // 42 delete p; // 还回去 p nullptr; // 置空防止后面误用栈和堆是两套规矩。函数里声明的变量住栈离开作用域自动回收不用操心。new 出来的住堆没有作用域概念活到 delete 那一刻忘了还就一直占着。┌─────────────────────────────────────┐ │ 两套内存规矩 │ ├─────────────────────────────────────┤ │ 栈上自动打扫堆上自己动手 │ │ │ │ 栈 作用域内有效出了门自动清 │ │ 堆 手动要手动还忘了就一直在 │ │ │ │ 借还成对方括号也成对 │ │ 忘了 delete内存只进不出 │ └─────────────────────────────────────┘数组版本要加方括号借的时候加还的时候也加两边必须对上。int *p new int[5]{78, 92, 55, 100, 88}; cout p[2] endl; // 55 delete[] p;new int[5] 后面那对花括号是 C11 的初始化写法跟数组的部分初始化一个规矩不给的值补零。delete[] 的方括号告诉编译器这是连续一片要逐个收拾漏了是未定义行为小类型看不出来类类型出事。4.2 动态数组运行期定长度的数组终于能写了。int n; cout 几个学生: ; cin n; int *scores new int[n]; for (int i 0; i n; i) { cout 第 i 1 个: ; cin scores[i]; } int sum 0; for (int i 0; i n; i) { sum scores[i]; } cout 平均分 static_castdouble(sum) / n endl; delete[] scores;输入 5依次输 78 92 55 100 88平均分八十二点六跟第五章手写数组那个版本结果一样。摆法不同算法一字不改这就是指针把数据的摆法解耦出来的好处。new int[n] 要出来的内存没初始化跟数组一个规矩读之前先写值。想让它全是零写 new intn一对空括号全部置零。4.3 内存泄漏与悬空指针new 了不 delete这块内存就漏了。指针变量没了地址还没人记得系统以为这块有人用永远收不回去。┌─────────────────────────────────────────────┐ │ 忘记归还的账 │ ├─────────────────────────────────────────────┤ │ 泄漏不崩攒着才要命 │ │ │ │ 循环第一次 new 一块忘记 delete │ │ │ │ │ v │ │ 循环第一千次 内存又少一块 │ │ │ │ │ v │ │ 内存耗尽 程序被杀日志里全是无辜 │ │ │ │ 进了循环小漏变大漏 │ │ 每块 new 都该有个 delete 在等它 │ └─────────────────────────────────────────────┘void leaky() { int *p new int[1000]; // 函数结束p 没了一千个 int 没人认领 }单次调用看不出来漏几千字节而已。放在循环里就是灾难服务器跑几天内存就见底。防泄漏的规矩就一条借了就得还函数有几个出口每个出口前对一遍。比泄漏更急的是悬空指针。delete 之后指针还指着那块已经归还的内存这时候用它读到什么全看系统把这块地分给谁了。int *p new int(42); delete p; // *p 5; 错的这块地已经还了 p nullptr; // 置空后面能用 if (p) 防一手还有一条老代码里的常客函数返回局部变量的地址。函数一返回栈帧拆了地址还在内容已经换了人家返回去就是个悬空指针。int *bad() { int a 42; return a; // 错的a 活不过这个函数 }编译器对这种写法会给警告看见 warning 别当耳旁风。想返回数据返回值拷贝一份或者在堆上 new 出来让调用方负责 delete或者用后面会讲的引用和智能指针。五、一些特殊指针5.1 void 指针void* 是不知道类型的地址。只知道有这么一块内存在哪儿多大不管里面装什么也不管。int a 78; void *vp a; cout (void*)vp endl; // 能打地址void* 不能直接解引用不知道读几个字节。也不能加减不知道步长。用之前必须先转到具体类型。int *ip static_castint*(vp); cout *ip endl; // 78第二章讲过四种强制转换日常 static_cast 一把够用。void* 的场合还有个 reinterpret_cast按位重新解释转出来的东西对不对全看你自己。第二章那句到指针这章再见现在见到了记住一句reinterpret 是把刀能不拔就不拔剩下的 cast 到类那章还有一批。C 风格写法 (int*)vp 也能编新代码用 static_cast意图清楚编译器能查。5.2 const 与指针第二章留了个尾巴const 指针那几种组合四种写法摆一起看。┌────────────────────────────────────────────────────────┐ │ 两种特殊指针 │ ├────────────────────────────────────────────────────────┤ │ void 不知道类型const 分两种管法 │ │ │ │ ------------------------------------------------ │ │ |写法 |管得住什么 |典型错法 | │ │ ------------------------------------------------ │ │ |const int *p |管指向的数据 |*p 5 不行 | │ │ |int * const p |管指针自己 |p q 不行 | │ │ |const int * const p |两个都管 |只读不动 | │ │ ------------------------------------------------ │ │ │ │ void* 只知道地在哪儿不问里面装什么 │ │ const 站的位置决定它管哪一头 │ └────────────────────────────────────────────────────────┘int a 78, b 92; const int *p1 a; // 指向常量 int * const p2 a; // 常量指针 const int * const p3 a; // 两个都管 // *p1 5; // 错数据不让改 p1 b; // 对指向能换 // p2 b; // 错指针不让改 *p2 5; // 对数据能改 // p3 两个都不能动判断方法一条看 const 挨着谁。挨着星号管的是数据挨着指针名管的是指针。实用场合两个。传参不想让函数改数据用 const int *p调用方看得见这份承诺。存一份地址不打算换了用 int * const p写死在声明里。C 惯例是能加 const 就加编译期挡错误比运行时调试便宜得多。5.3 二级指针指针变量自己的地址用两个星号接。int a 78; int *p a; int **pp p; cout **pp endl; // 78 cout (*pp p) endl; // 1*pp 就是 p**pp 拆两层读先拆一层拿到 p再拆一层拿到 a。改 *pp 就是改 p 的指向p 从此指着别处。int b 92; *pp b; cout *p endl; // 92p 被 pp 隔着改掉了二级指针不是拿来炫技的。函数里要改一个指针变量本身比如让它指向新申请的内存就得传它的地址也就是二级指针。动态二维数组也用它一个指针数组每个元素再指向一行。这两个场合下一章和数据结构那章都会见这里先把拆两层的功夫练熟。5.4 野指针与悬空指针野指针是没初始化的指针指向哪儿全看内存里恰好躺了什么。悬空指针是 delete 之后没置空的指针指着一块已经归还的地。两个都是事故源放一起收拾。int *p1; // 野内存残留 int *p2 new int(42); delete p2; // p2 悬空 int *p3 nullptr; // 空安全三条军规。声明就初始化没值可给就给 nullptr。delete 之后紧跟一句置空一行都不省。指针变量出了作用域就当他没了别把里面的地址存到别处再用。做到这三条九成的指针崩溃跟自己无关。六、小结┌──────────────────────────────────────────────────────────┐ │ 本章收口 │ ├──────────────────────────────────────────────────────────┤ │ 一章学完手上该有这几样 │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 地址 变量的门牌号 记 * 进屋 │ │ │ └────────────────────────────────────────────────────┘ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 运算 加减跳元素相减数差距 │ │ │ └────────────────────────────────────────────────────┘ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 数组 数组名就是地址三种写法等价 │ │ │ └────────────────────────────────────────────────────┘ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 堆 new 和 delete 成对忘了就漏 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ 门牌认熟了C 这扇门才算推开 │ │ 函数的门从指针这把钥匙开 │ └──────────────────────────────────────────────────────────┘知识一句话最容易栽在哪地址内存按字节编号把某次运行的地址当常量指针变量存地址的变量声明了不初始化解引用顺着地址读写数据空指针上解引用指针大小固定与类型无关以为 double* 更大加减跳元素不跳字节拿字节当单位数相减差几个元素拿两个不同数组的指针相减数组名首元素地址拿数组名自增三种写法下标地址糖等价见到指针写法不认识传参退化数组参数变指针函数里 sizeof 失灵new 与 delete成对出现漏 delete 漏方括号void 指针先转类型再用直接解引用const 组合就近管数据管指针四种组合记混附录指针速查卡类别写法一句话提醒取地址a一元运算符解引用*p星号在使用处声明int *p a星号贴着变量名空指针int *p nullptr解引用前查一眼加减p1、p-1跳一个元素相减p2 - p1差几个元素比较p q、p 小于 q同段内存内才可靠数组名scores scores[0]常地址不能自增三种写法a[i]、*(ai)、p[i]等价单个 newnew int(42)配 delete数组 newnew int5配 delete[]置零new intn空括号全清零void 指针static_castint*(vp)先转再用const 数据const int *p靠近星号管数据const 指针int * const p靠近名字管指针二级指针int **pp p拆两层读常见现象病根药方改完函数里没反应传的是副本改传地址程序莫名崩溃野指针解引用声明就初始化崩溃在 delete 之后悬空指针还在用delete 完置空内存越跑越少new 了没 delete成对写出口查打印指针是字符串char* 被 cout 当串打转 void* 再打函数里 sizeof 变小数组退化成指针长度单独传步长数不对拿字节当单位记住跳的是元素编译错在 scores 自增数组名是常量挪给指针变量

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑