简介:本资源是一份面向C语言初学者与求职者的经典笔试复习资料,聚焦内存管理、指针运算、关键字语义及多线程基础等高频考点,助力开发者夯实底层原理、高效备战技术面试。文档为单个Word文件(.doc),共122KB,内容结构清晰,涵盖sizeof与strlen的本质区别、栈与堆的内存分配机制、大小端存储解析、struct指针偏移计算、const/static/volatile关键字作用、数组地址运算及线程间通信方式等核心问题,并附带典型代码示例与逐行解析。预览可见多组对比实验(如str1/str2/str3的sizeof与strlen结果差异、p1/p2指针数组的字节计算),帮助读者直观理解抽象概念。目前已有319人学习下载,适合作为C语言进阶笔记、面试速查手册或教学补充材料,尤其适合需快速厘清易混淆知识点的应届生与转岗工程师。
1. 这不是题库,是C语言内存认知的“校准器”:一份能让你在嵌入式/驱动/底层岗笔试中少踩70%指针坑的实战手记
你有没有遇到过这种场景:写完一段指针偏移代码,本地跑通,一上板子就段错误;malloc了一块内存,反复确认没越界,却总在某个特定编译选项下崩溃;面试官问“sizeof(str)和strlen(str)差在哪”,你张嘴就答“一个算大小一个算长度”,结果他追问“那char s[5] = {'h','e','l','l','o'},strlen(s)返回多少?为什么?”——你卡住了。这不是记不住知识点,而是对C语言底层内存模型的理解还浮在水面。这份《C语言笔试试题汇集(超经典)》根本不是拿来背答案的题海,它是一套用21个高频真题反向锤炼你内存直觉的“校准器”。它专治三类人:刚从学校出来、对栈/堆/常量区只有模糊概念的应届生;写了三年业务代码、第一次接触裸机驱动、被volatile和大小端搞到怀疑人生的转岗工程师;还有那些在Linux内核模块里调kmalloc时,突然发现连struct BBB *p; p+0x200都算不明白的老手。它不讲语法糖,只抠内存地址怎么跳、字节怎么排、编译器怎么骗你、运行时怎么翻车。全文所有题目都来自真实笔试现场,每一道背后都藏着一个血泪调试现场——比如第5题struct BBB *p; p+0x200=?,某次某高校实验室调试CAN总线固件时,就因为没算对结构体对齐后的实际偏移,导致DMA地址配置错位,花了整整两天才定位到。这不是理论题,是已经验证过的“防翻车清单”。
2.sizeof与strlen:两个看似简单、实则分隔新手与老手的“内存分水岭”
这两个操作符/函数,是C语言笔试里出现频率最高、但错误率也最高的组合。它们表面都在“算长度”,本质却站在内存世界的两个对立面:一个是编译期的静态快照,一个是运行时的动态扫描。理解它们的差异,不是为了答题,而是为了写出不会在不同平台、不同优化等级下行为突变的代码。
2.1 编译期快照:sizeof的6个硬核事实
sizeof不是函数,是编译器内置的操作符。它的值在代码编译完成那一刻就已固化,不依赖任何运行时数据。这决定了它的能力边界和陷阱。
#include <stdio.h> int main() { char str1[] = "Hello"; // 自动补'\0' char str2[5] = {'H','e','l','l','o'}; // 无'\0' char str3[6] = {'H','e','l','l','o','\0'}; // 显式'\0' char *p1 = "Hello"; char *p2[] = {"hello", "world"}; int n = 10; int *q = &n; printf("sizeof(str1) = %zu\n", sizeof(str1)); // 输出: 6 printf("sizeof(str2) = %zu\n", sizeof(str2)); // 输出: 5 printf("sizeof(str3) = %zu\n", sizeof(str3)); // 输出: 6 printf("sizeof(p1) = %zu\n", sizeof(p1)); // 输出: 4 (32位) 或 8 (64位) printf("sizeof(p2) = %zu\n", sizeof(p2)); // 输出: 8 (32位: 2指针×4字节) printf("sizeof(n) = %zu\n", sizeof(n)); // 输出: 4 printf("sizeof(q) = %zu\n", sizeof(q)); // 输出: 4 或 8 return 0; }逻辑说明与参数说明:
sizeof(str1)返回6,因为"Hello"字面量在编译时被当作char[6]处理(5字符+1个隐式\0),数组名str1未退化,sizeof拿到的是整个数组的尺寸。sizeof(str2)返回5,因为显式声明了[5],编译器严格按此分配空间,不加\0,sizeof忠实地返回5。sizeof(p1)返回指针大小(32位为4),p1是char*类型,sizeof计算的是指针变量本身占多少字节,而非它指向的内容。sizeof(p2)返回2 * sizeof(char*),p2是char*[2]类型,即一个包含2个char*指针的数组,sizeof计算的是这个数组的总大小。- 关键点:
sizeof对数组名和指针变量的处理天壤之别。前者是“实体”,后者是“地址”。
2.2 运行时扫描:strlen的3个致命前提
strlen是一个标准库函数,原型为size_t strlen(const char *s);。它不做任何假设,只做一件事:从s指向的地址开始,逐字节读取,直到遇到第一个\0,返回中间经过的字节数。它的一切行为,都建立在三个脆弱的前提上。
#include <stdio.h> #include <string.h> int main() { char str1[] = "Hello"; // 有'\0' char str2[5] = {'H','e','l','l','o'}; // 无'\0' char str3[6] = {'H','e','l','l','o','\0'}; // 有'\0' printf("strlen(str1) = %zu\n", strlen(str1)); // 输出: 5 printf("strlen(str2) = %zu\n", strlen(str2)); // 输出: ? (未定义行为!) printf("strlen(str3) = %zu\n", strlen(str3)); // 输出: 5 return 0; }逻辑说明与参数说明:
strlen(str1)和strlen(str3)都返回5,因为它们都以\0结尾,strlen能安全停止。strlen(str2)的行为是未定义的(Undefined Behavior)。str2只有5个字符,没有\0,strlen会继续向后内存地址读取,直到撞上某个偶然的\0(可能在栈上其他变量里,也可能在非法地址),结果完全不可预测:可能返回一个巨大数字,可能程序直接崩溃(SIGSEGV)。这是C语言里最经典的“玄学bug”源头之一。strlen的参数必须是char*,且该指针必须指向一个以\0结尾的有效字符串。它不能用于int[]、float*等非字符类型,也不能用于未初始化或未正确终止的字符数组。
2.3 真实世界的交叉验证:为什么sizeof能当数组维数,strlen不能?
面试官最爱问:“为什么char buf[20]; int len = sizeof(buf);合法,而int len = strlen(buf);不行?” 这个问题直指核心:sizeof是编译器的“上帝视角”,strlen是运行时的“盲人摸象”。
sizeof(buf)合法,是因为buf是具有完整类型信息的数组名。编译器在符号表里清楚地记录着buf的类型是char[20],所以sizeof能直接查表得到20。这也是为什么sizeof能用来定义另一个数组:char copy[sizeof(buf)];。strlen(buf)在buf未初始化时是灾难。即使buf被初始化为""(空字符串),strlen(buf)返回0,但这0是运行时算出来的,无法在编译期用于定义数组大小。C99以前,int arr[strlen(buf)];是非法的(VLA是C99特性,且不推荐用于嵌入式)。
关键结论:当你需要一个编译期确定的、与数据内容无关的尺寸(如分配缓冲区、定义数组),用
sizeof;当你需要一个运行时确定的、与数据内容强相关的长度(如解析网络包、处理用户输入),用strlen,但必须100%确保输入是\0终止的。
2.4 避坑:sizeof与strlen的5个高频翻车现场
现象 → 原因 → 解决,全是血泪经验。
现象:函数传入字符数组,
sizeof(arr)在函数内返回指针大小(4或8),而非数组真实大小。
原因:C语言中,数组作为函数参数传递时,会自动“退化”为指针。函数签名void func(char arr[])和void func(char *arr)完全等价,arr在函数体内就是一个char*,sizeof(arr)自然就是指针大小。
解决:永远不要在函数内部对形参数组用sizeof来获取长度。必须显式传入长度:void func(char *arr, size_t len),或使用哨兵值(不推荐)。现象:
sizeof("Hello")返回6,但sizeof(p)(p指向同一字符串)返回4,误以为字符串字面量和指针一样。
原因:"Hello"是字符串字面量,类型是char[6](编译器生成的只读数组),sizeof作用于它,得到数组大小。而p是char*变量,sizeof(p)得到的是指针变量本身的大小。两者类型完全不同。
解决:牢记字符串字面量是数组,不是指针。sizeof("Hello") == 6是铁律;sizeof(p) == sizeof(char*)是另一条铁律。现象:
strlen()在调试时返回正常值,但开启-O2优化后程序崩溃。
原因:strlen的未定义行为在高优化等级下会被编译器“激化”。例如,编译器可能假设strlen的参数必有\0,从而生成向后读取的向量化指令,一旦遇到非法内存,立即触发硬件异常。
解决:对所有传给strlen的指针,用assert或静态分析工具(如clang --analyze)强制检查其是否以\0结尾。宁可多一次memchr扫描,也不要赌运气。现象:
sizeof(struct {int a; char b;})在不同编译器/平台上结果不同(如8 vs 12)。
原因:sizeof对结构体的计算,受内存对齐规则支配。char b后面可能有3字节填充,以保证下一个int成员地址是4的倍数。这个填充是编译器根据目标平台ABI决定的,不是strlen那种纯逻辑问题。
解决:用#pragma pack(1)或__attribute__((packed))强制取消对齐(仅限必要场景,如网络协议打包),否则接受对齐带来的空间开销,并用offsetof宏精确计算成员偏移。现象:
sizeof用于函数调用,如sizeof(func()),结果与预期不符。
原因:sizeof可以作用于函数调用表达式,但它返回的是该函数返回类型的大小,而非函数执行的开销。例如,short f(); sizeof(f())返回sizeof(short)即2,与f()内部做了什么都无关。
解决:sizeof函数调用是合法但极少用的冷知识。日常开发中几乎不需要,看到就警惕——你可能真正想测的是函数执行时间(用clock())或栈帧大小(用调试器)。
3. 栈、堆、全局区与常量区:一张图看懂C程序的“内存四国演义”
如果你把C程序的内存想象成一个国家,那么栈、堆、全局区、常量区就是它的四个自治领地。每个领地有自己独立的法律(分配/释放规则)、警察(管理机制)和居民(数据类型)。混淆它们,就像在海关混用签证——轻则被拒,重则被驱逐(段错误)。
3.1 栈区(Stack):编译器的“速记本”,快进快出,但容量有限
栈是函数调用的基石。每次函数调用,编译器就在栈顶划出一块新区域(栈帧),存放参数、局部变量、返回地址。函数返回,这块区域自动“擦除”。它的特点是后进先出(LIFO)、分配释放极快、空间由系统限制(Linux默认8MB,Windows约1MB)。
void stack_demo() { int a = 10; // 栈上分配,4字节 char s[] = "abc"; // 栈上分配,4字节("abc\0") char *p3 = "123456"; // p3变量在栈上(4字节),"123456\0"在常量区 static int c = 0; // c在全局区(静态区),不在栈上! }逻辑说明与参数说明:
int a和char s[]是典型的栈变量,生命周期与stack_demo函数绑定。s[]是数组,内容存于栈;p3是指针,它自己(4字节)存于栈,但它指向的字符串字面量"123456"存于常量区。static int c是陷阱!它虽在函数体内声明,但存储位置是全局区(静态区),生命周期贯穿整个程序运行期,不是栈变量。- 栈溢出(Stack Overflow)是嵌入式开发的头号杀手。一个递归过深的函数,或一个声明了
char big_buf[1024*1024]的局部数组,都会瞬间耗尽栈空间,导致程序崩溃。解决方案:将大数组声明为static(放全局区)或malloc(放堆)。
3.2 堆区(Heap):程序员的“自由市场”,随心所欲,但需自律
堆是唯一由程序员手动控制内存生命周期的区域。通过malloc、calloc、realloc申请,free释放。它像一块巨大的、可分割的土地,程序员是开发商,可以随时买地(malloc)、扩建(realloc)、卖地(free),但必须自己记账,否则就会产生“内存泄漏”(买了不卖)或“野指针”(卖了还去住)。
#include <stdlib.h> #include <string.h> void heap_demo() { char *p1 = (char*)malloc(10); // 申请10字节堆内存 char *p2 = (char*)malloc(20); // 申请20字节堆内存 if (p1 && p2) { strcpy(p1, "123456"); // "123456\0" 拷贝到p1指向的堆内存 // ... 使用p1, p2 ... free(p1); // 必须释放!p1变成野指针 free(p2); // 必须释放! p1 = p2 = NULL; // 释放后置NULL,避免二次释放 } }逻辑说明与参数说明:
malloc(10)返回一个void*,必须强制转换为所需类型指针(C++中不需要,C中习惯转换)。它只分配内存,不初始化,p1指向的10字节内容是随机的。strcpy(p1, "123456")是危险操作!"123456"长度为7(含\0),但p1只申请了10字节,勉强够用。如果写成strcpy(p1, "12345678901"),就会越界写入,破坏相邻堆块的元数据,导致后续free时崩溃。这就是“堆损坏(Heap Corruption)”。free(p1)后,p1仍指向原地址,但该地址的内存已归还系统。此时若再访问p1,就是“野指针访问”,行为未定义。最佳实践是free后立即将指针置为NULL。
3.3 全局区(静态区)与常量区:程序的“不动产”,一劳永逸,但不可更改
全局区分为两块:已初始化全局/静态变量区和未初始化全局/静态变量区(BSS段)。它们在程序启动时由操作系统一次性分配好,程序结束时由系统回收。常量区则专门存放字符串字面量等只读数据。
// 全局变量(已初始化) int global_init = 100; // 存于.data段(已初始化全局区) // 全局变量(未初始化) int global_uninit; // 存于.bss段(未初始化全局区),启动时自动清零 // 静态变量(已初始化) static int static_init = 200; // 存于.data段,但作用域仅限本文件 // 静态变量(未初始化) static int static_uninit; // 存于.bss段,作用域仅限本文件 // 字符串字面量 char *p_const = "Hello World"; // p_const变量在栈或全局区,"Hello World\0"在.rodata段(常量区) void func() { static int local_static = 300; // 第一次调用时初始化,之后保持值,存于.data段 }逻辑说明与参数说明:
.data段存放显式初始化的全局/静态变量,.bss段存放未初始化或初始化为0的全局/静态变量。.bss段在磁盘上不占空间(只记录大小),加载时由OS快速清零,节省磁盘空间。- 常量区(
.rodata)是只读的。试图修改p_const[0] = 'h'会导致段错误(SIGSEGV)。这是保护程序稳定性的关键机制。static关键字在这里的作用是限制链接属性(linkage),让变量/函数只在当前编译单元(.c文件)内可见,避免命名冲突。它不改变存储位置(全局static仍在全局区,局部static仍在全局区)。
3.4 避坑:栈、堆、全局、常量区的4个生死攸关陷阱
现象 → 原因 → 解决,每一个都曾让项目延期。
现象:函数返回局部数组的地址,调用方得到乱码或崩溃。
原因:char buf[100]; return buf;——buf是栈变量,函数返回时栈帧被销毁,buf的内存被回收。返回的地址指向一片“废弃工地”,后续任何访问都是未定义行为。
解决:绝不要返回局部数组地址。方案有三:(1) 用static char buf[100](线程不安全);(2) 用malloc在堆上分配并要求调用方free(最常用);(3) 由调用方传入缓冲区指针和长度(最安全,如snprintf)。现象:
malloc返回NULL,程序未检查直接解引用,崩溃。
原因:嵌入式系统内存紧张,malloc失败是常态。malloc失败返回NULL是POSIX标准,但很多新手忽略检查。
解决:每一次malloc后必须检查返回值。养成肌肉记忆:char *p = malloc(size); if (!p) { /* 处理错误 */ return -1; }。在资源受限环境,优先考虑静态分配或内存池。现象:全局变量在多线程环境下被意外修改,逻辑错乱。
原因:全局/静态变量是所有线程共享的。如果没有同步机制(互斥锁、原子操作),多个线程同时读写同一个变量,会产生竞态条件(Race Condition)。
解决:对共享全局变量的访问,必须加锁。更优方案是设计为无状态(stateless),或使用线程局部存储(__thread或thread_local)。现象:
char *p = "abc"; p[0] = 'x';在某些编译器下能运行,在另一些下崩溃。
原因:字符串字面量存储在常量区(.rodata),该段内存通常被标记为只读。在支持MMU的系统(如Linux x86),写入会触发段错误;在裸机或简单RTOS上,可能“侥幸”成功,但破坏了常量区的完整性,后患无穷。
解决:永远不要尝试修改字符串字面量。需要修改,请用char arr[] = "abc";(栈上可写数组)或char *p = strdup("abc");(堆上可写副本)。
4. 大小端、指针运算与结构体偏移:揭开内存地址的“字节级密码本”
当你用p+1时,CPU到底在地址线上加了多少?0x12345678这个数,是0x12住在0x0000还是0x78住在0x0000?这些问题的答案,就是C语言与硬件对话的“密码本”。不懂它,你的指针就是蒙眼射箭;懂了它,你才能精准操控DMA、解析网络包、调试寄存器映射。
4.1 大小端(Endianness):字节序的“左右手定则”
大小端之争,本质是人类读数习惯(高位在前)与机器存储效率(低位在前更利于ALU运算)的妥协。x86、ARM(默认)、RISC-V是小端;PowerPC、SPARC、MIPS(可选)是大端。
#include <stdio.h> #include <stdint.h> int check_endianness() { uint32_t num = 0x12345678; uint8_t *ptr = (uint8_t*)# // 小端:ptr[0] = 0x78, ptr[1] = 0x56, ptr[2] = 0x34, ptr[3] = 0x12 // 大端:ptr[0] = 0x12, ptr[1] = 0x34, ptr[2] = 0x56, ptr[3] = 0x78 return (ptr[0] == 0x78) ? 1 : 0; // 1=小端, 0=大端 } int main() { printf("This machine is %s-endian.\n", check_endianness() ? "little" : "big"); return 0; }逻辑说明与参数说明:
- 核心技巧:用
uint8_t*(字节指针)去“窥探”一个整型变量的内存布局。&num拿到num的地址,(uint8_t*)&num将其强制解释为字节指针,ptr[0]就是最低地址处的字节。- 小端(Little-Endian):低字节存低地址。
0x12345678的LSB(Least Significant Byte)是0x78,它存放在&num这个最低地址上。- 大端(Big-Endian):高字节存低地址。
0x12345678的MSB(Most Significant Byte)是0x12,它存放在&num这个最低地址上。- 网络字节序(Network Byte Order)是大端。因此,跨网络传输整数时,必须用
htonl()(host to network long)等函数转换,否则小端机器发出去的0x12345678,大端机器会收到0x78563412。
4.2 指针运算:p + n背后的“类型乘法”
p + n不是简单的地址加n,而是p + n * sizeof(*p)。这是C语言指针安全的基石,也是初学者最容易误解的地方。
#include <stdio.h> struct BBB { long num; // 通常8字节 int index; // 通常4字节 char *name; // 通常8字节 }; // 结构体总大小:8+4+8 = 20,但因对齐,实际为24字节(假设long=8, int=4, ptr=8) int main() { struct BBB *p = (struct BBB*)0x1000000; printf("p = 0x%lx\n", (unsigned long)p); printf("p+0x200 = 0x%lx\n", (unsigned long)(p + 0x200)); printf("(char*)p+0x200 = 0x%lx\n", (unsigned long)((char*)p + 0x200)); printf("(long)p+0x200 = 0x%lx\n", (unsigned long)((long)p + 0x200)); return 0; }逻辑说明与参数说明:
p + 0x200:p是struct BBB*,sizeof(struct BBB)假设为24(0x18),所以p + 0x200 = 0x1000000 + 0x200 * 0x18 = 0x1000000 + 0x4800 = 0x1004800。这是结构体数组的第0x200个元素的地址。(char*)p + 0x200:char*的sizeof(*p)是1,所以(char*)p + 0x200 = 0x1000000 + 0x200 = 0x1000200。这是从p起始地址向后偏移0x200字节的地址,最常用的“字节级偏移”。(long)p + 0x200:先将指针p强制转换为long整数(地址值),再加0x200,结果是0x1000000 + 0x200 = 0x1000200。这是纯粹的数值加法,与指针类型无关。- 关键:
p + n的步长由p的基类型决定。int* p; p+1跳4字节;double* p; p+1跳8字节;char* p; p+1跳1字节。
4.3 结构体成员偏移:offsetof宏的底层实现
offsetof(type, member)是标准库宏,用于计算结构体中某个成员相对于结构体起始地址的字节偏移。它的实现,完美诠释了C语言的“指针即地址”哲学。
#include <stdio.h> #include <stddef.h> // 标准offsetof的等效实现(简化版) #define MY_OFFSETOF(type, member) ((size_t)&(((type*)0)->member)) struct test { char a; int b; char c; }; int main() { printf("offsetof(struct test, a) = %zu\n", offsetof(struct test, a)); // 0 printf("offsetof(struct test, b) = %zu\n", offsetof(struct test, b)); // 4 (假设a后有3字节填充) printf("offsetof(struct test, c) = %zu\n", offsetof(struct test, c)); // 8 (b后无填充,c在b后) printf("MY_OFFSETOF(struct test, b) = %zu\n", MY_OFFSETOF(struct test, b)); // 同上 return 0; }逻辑说明与参数说明:
((type*)0):将整数0强制转换为type*指针。这是一个空指针,但offsetof宏只用它来计算地址,并不解引用,所以是安全的。&(((type*)0)->member):对空指针的member成员取地址。编译器知道member在type中的固定偏移,因此能计算出&(((type*)0)->member)的值,这个值就是member的偏移量(因为基地址是0)。sizeof(struct BBB)的计算,正是offsetof的延伸:offsetof(last_member) + sizeof(last_member),再加上末尾可能的填充。- 实际应用:在驱动开发中,寄存器映射常定义为结构体,
volatile struct REGS *regs = (volatile struct REGS*)0x40000000;,然后regs->status就等价于读取0x40000000 + offsetof(status)地址的值。
4.4 避坑:大小端与指针运算的3个“字节级”雷区
现象 → 原因 → 解决,关乎硬件交互成败。
现象:用
memcpy从网络包拷贝一个uint32_t字段,直接赋值给变量,值是错的。
原因:网络包是大端,你的CPU是小端。memcpy(&val, packet_ptr, 4)只是把4个字节原样搬过来,val的内存布局是小端的,但字节顺序是大端的,所以val的值是颠倒的。
解决:必须用ntohl()(network to host long)转换:val = ntohl(*(uint32_t*)packet_ptr);。或者,用memcpy到uint8_t buf[4],再手动拼接:val = (buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];。现象:
struct BBB *p; p+1的地址,与p + sizeof(struct BBB)不相等。
原因:p+1的步长是sizeof(struct BBB),但sizeof(struct BBB)包含了编译器为对齐添加的填充字节。如果结构体中有char和int混排,sizeof会大于各成员sizeof之和。p+1跳过的正是这个“对齐后”的大小。
解决:永远相信p+1,不要自己算p + sizeof(struct BBB)。p+1是标准、安全的;自己算容易忽略对齐,导致越界。现象:
char *p = "abc"; int *ip = (int*)p; printf("%d", *ip);输出一个巨大负数或崩溃。
原因:p指向char,ip指向int,但p的地址可能不是int的对齐边界(如int需4字节对齐,而p地址是奇数)。在ARM等严格对齐架构上,未对齐访问会触发SIGBUS。
解决:禁止未对齐指针解引用。如果必须按不同类型解读同一块内存,先用memcpy拷贝到对齐的变量中:int val; memcpy(&val, p, sizeof(val));。
5.const、static、volatile:C语言的“三权分立”,掌控变量的生杀大权
const、static、volatile这三个关键字,是C语言赋予程序员对变量进行精细控制的“宪法”。它们不改变变量的值,却深刻影响编译器如何对待它、链接器如何链接它、CPU如何访问它。忽略它们,就像开车不系安全带——平时没事,出事就是大事。
5.1const:编译器的“紧箍咒”,宣告“此地不可改”
const的本质是向编译器承诺:“我保证,这个东西,我的代码里,谁都不许改它。” 编译器信守承诺,会进行相应优化,并在你违背诺言时发出警告或错误。
#include <stdio.h> int main() { const int a = 10; // a是常量整数 int const b = 20; // 等价于上一行,a和b都是const int const int *p1 = &a; // p1是指向常量int的指针:*p1不可改,p1可改 int * const p2 = &a; // p2是常量指针:p2不可改,*p2可改(但a本身是const,所以*p2也不能改,双重保护) const int * const p3 = &a; // p3是常量指针,指向常量int:p3和*p3都不可改 // p1 = &b; // OK: p1可以指向别处 // *p1 = 5; // ERROR: *p1是const,不可改 // p2 = &b <p> <a href="https://download.csdn.net/download/lizhiming183/4761101" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>