尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

char *p[10]到底分配几块内存?一文搞懂指针数组与内存布局

char *p[10]到底分配几块内存?一文搞懂指针数组与内存布局 C语言里关于指针和内存的题目往往看起来特别简单但一细想就全是坑。char *p[10]到底分配了几块内存我见过不少人脱口而出“10块”也有人说是“1块”还有人搬出“数组名是常量指针”之类的理论来搅浑水。这篇文章就把这个问题彻底讲透顺带把指针数组、内存布局、字符串初始化的底层机制都梳理一遍让你以后再遇到类似的题能一眼看穿考点在哪。1. 先说结论char *p[10]的标准答案是什么直接给结论char *p[10]作为一个局部变量定义它只分配了一块连续的内存区域用来存放10个char *类型的指针变量。这块内存的大小等于sizeof(char *) * 10。在32位平台上指针是4字节所以总大小是4 * 10 40字节在64位平台上指针是8字节所以总大小是8 * 10 80字节。这块内存是连续的也就是说p[0]、p[1]、p[2]一直到p[9]这10个指针变量是紧挨着排布在内存里的。为什么很多人会脱口而出“10块”因为他们把“10个指针变量”和“10块内存”画了等号。这其实混淆了一个关键概念指针变量本身是一块内存指针指向的目标是另一块内存。char *p[10]只保证了“用来装指针的盒子”有10个至于这些盒子里装的是什么地址、那个地址上有没有合法的内存这个定义本身完全不负责。打个比方char *p[10]相当于你租了一个有10个抽屉的储物柜每个抽屉的尺寸刚好能放下一把钥匙指针。这个操作只保证“柜子有了10个抽屉有了”但抽屉里有没有钥匙、钥匙能打开哪扇门完全是另一回事。你后续初始化了p[0] hello才算是往0号抽屉里放了一把指向字符串常量的钥匙。如果什么都不做就直接用strcpy(p[0], hello)那等于拿一把还没配好的钥匙去开门不开机崩溃才怪。所以关于“char *p[10]分配了几块内存”这个问题最严谨的答案是它自身只分配了1块连续内存内部包含10个指针大小的槽位。但结合不同的使用方式整个程序可能涉及的内存块数会完全不同下面详细拆解。2. 从内存布局拆解这10个指针到底“住”在哪里要真正理解char *p[10]的内存情况绕不开C语言的内存分区。我把这个数组放到不同位置它的“家”就不一样这直接影响到它能怎么用、生命周期有多长。2.1 栈上的char *p[10]如果char *p[10]定义在函数内部比如main函数或任意子函数里它就是一个局部变量内存分配在栈区。void foo(void) { char *p[10]; // 栈上分配大小 10 * sizeof(char *) // 此时p里的10个元素全部是随机值垃圾值 }栈区内存的特点是函数调用时分配函数返回时自动回收不需要程序员手动管理。但这带来一个严重问题——这10个指针变量的初值是不确定的。它们可能是上一次函数调用留下的残留数据也可能是任意随机数。你要是敢直接拿p[0]去访问内存轻则读到脏数据重则段错误。所以栈上定义的指针数组用之前必须逐个初始化这是铁律。栈区的另一特点是空间有限一般Linux下默认栈大小是8MB左右Windows下主线程栈默认1MB。所以如果你定义char *p[1000000]在栈上直接就会栈溢出崩溃。2.2 全局区/静态区的char *p[10]如果char *p[10]定义在函数外面或者加了static修饰那它就住在全局区静态区。char *p[10]; // 全局变量BSS段 static char *q[10]; // 静态局部变量同样是BSS段 int main(void) { return 0; }全局区和栈上有本质区别这块内存在程序启动时就被分配好了而且会被系统自动清零。也就是说p[0]到p[9]的初值全部是NULL而不是随机垃圾值。这算是个不错的默认状态至少你误用的时候更容易暴露问题对NULL指针操作通常会立刻崩溃而不是读到脏数据后继续带病运行把 bug 藏到更深的地方。2.3 堆上的char *p[10]还有一种玩法是用malloc在堆上动态分配这块内存char **p (char **)malloc(sizeof(char *) * 10);这时候p本身是一个char **类型的指针变量可以理解为“指向指针数组的指针”而它指向的那块堆内存才是那10个指针槽位所在的地方。这块堆内存不会被初始化同样需要手动赋值或清零用完还要free还给系统。虽然这种动态分配的方式在语法层面跟char *p[10]不完全一样但它们在实际使用中的语义高度相似很多底层代码里用char **来模拟动态大小的指针数组就是这个套路。这三种内存位置的区别列个表对比一下最直观定义位置存储区域初值生命周期释放方式函数内局部变量栈随机值垃圾值函数返回即结束自动不能手动free函数外/static全局区/静态区全0NULL整个程序运行期程序结束自动回收malloc动态分配堆随机值直到free为止必须手动free这里有个面试官特别爱挖的坑对栈上的指针数组调用free(p)会怎样答程序直接崩溃。free只能释放堆上由malloc/calloc/realloc分配的内存对栈地址调用free属于未定义行为实际操作中几乎都会触发段错误。3. 关键分歧点char *p[10]和字符串常量到底什么关系很多文章在讲这个概念时会顺手写一行char *p[10] {hello, world, ...}然后说“p[0]指向字符串常量”。这一下就把问题弄复杂了——因为它引入了一种完全不同的内存区域只读数据区字符串常量区。3.1 初始化后才出现“第二块内存”看这个代码char *p[10] { 星期一, 星期二, 星期三, // ... };这里的实际情况是栈上或全局区有一块连续内存存放10个指针这是第一块内存每个字符串字面量本身被编译器安排在只读数据区.rodata段或类似区域各占一块连续内存这是第二、第三、第四块内存……所以如果题目问的是“这个定义加初始化之后程序里一共涉及几块内存”答案就变成了1 N其中N是字符串字面量的个数。10个指针配上10个字符串字面量那就是11块有实际意义的内存区域。如果再算上字符串结尾的\0所占的字节每块字符串内存的大小是“字符串字符数 1”。这里有个超级常见的坑用char *p[10]去存字符串并尝试修改。char *p[10]; p[0] hello; p[0][0] H; // 崩溃字符串字面量在只读区p[0] hello只是让p[0]这个指针指向了只读区的首地址并没有把字符串复制到任何地方。你通过p[0][0] H去修改实际上是在修改只读区的数据。这行为虽然不属于“内存不足”但属于典型的“权限不足”在Linux上会直接段错误Segmentation fault在Windows上可能报访问冲突。博主当年第一次踩这个坑时debug了整整一下午最后发现罪魁祸首就是把字符串常量赋给了char *而不是const char *。3.2 如果换一种定义char p[10][20]为了加深理解对比一下二维字符数组char p[10][20]。这个定义表示一块连续内存总共10 * 20 200字节每个元素是char[20]类型的数组。注意这块内存里直接存放字符数据本身而不是存放指针。char p[10][20]; strcpy(p[0], hello); // 直接把字符串复制进p[0]这块20字节的区域 p[0][0] H; // 合法这块内存是可读写的栈/全局内存char *p[10]和char p[10][20]的区别本质上就是“指针数组”和“二维字符数组”的区别前者先分配指针的容器再让每个指针去指向别处的字符串后者直接把所有字符空间一次性买断字符串数据就内嵌在数组里。搞清楚这一点很多花里胡哨的面试题都迎刃而解。3.3 还有更迷糊的char (*p)[10]有经验的面试官不会只问char *p[10]他们大概率还会跟进一个问题“那char (*p)[10]是什么”这俩长得太像了但含义天差地别表达式含义内存形态char *p[10]指针数组10个指针10个指针槽位共40/80字节char (*p)[10]数组指针指向含10个char的数组只有一个指针占4/8字节char (*p)[10]里的p是一个指针变量它指向的目标是“一个长度为10的char数组”。这个指针自己只占一个指针大小的内存4或8字节但它指向的那块内存是某个char[10]数组。很多初学者在笔试时看到这种声明直接懵技巧是看p先和谁结合*p要先和括号结合再和下标结合那就是数组指针p[10]下标先和p结合再解引用那就是指针数组。4. 内存大小的计算细节从sizeof到指针运算搞清楚内存块数之后还得会算内存大小。这部分一旦涉及sizeof和指针运算又是天道酬勤型送分题的翻译官。4.1 sizeof的正确打开方式char *p[10]; printf(%zu\n, sizeof(p)); // 4032位或8064位整个数组占用的字节数 printf(%zu\n, sizeof(p[0])); // 4或8单个指针变量的字节数 printf(%zu\n, sizeof(*p)); // 4或8相当于p[0]因为*p就是第一个元素 printf(%zu\n, sizeof(char *)); // 4或8系统平台决定注意sizeof是在编译期求值的它只关心类型不关心指针指向哪里、指向的内容是什么。所以哪怕p[0]是个野指针sizeof(p[0])依然是4或8不会崩溃。这一点可以用来区分栈上的数组和已退化为指针的数组在函数形参里sizeof(p)只能拿到一个指针的大小因为形参在编译器眼里已经是char **了。4.2 数组退化和char **的暧昧关系函数形参里写void foo(char *p[10])编译器其实会把它当成void foo(char **p)处理数组名在这里退化为指向首元素的指针。这带来一个隐蔽的坑你在foo里写sizeof(p)得到的是指针大小8字节而不是数组总大小80字节。很多人在子函数里遍历数组时不小心把sizeof(p)/sizeof(p[0])当成元素个数用结果算出来永远是1或2循环就直接跑飞了。正确做法是额外传入数组长度参数或者用宏定义固定长度。4.3 指针与跨步长char *p[10]里如果你写p 1它实际上跳过了sizeof(char *)个字节也就是从p[0]的地址跳到p[1]的地址。如果你写*(p 1)就是取p[1]。但如果换成char (*p)[10]那p 1跳过的是一整个char[10]数组也就是10个字节。这个差异在遍历内存时经常导致踩踏越界尤其是二进制数据处理和协议解析场景里非常容易出问题。5. 真正容易出错的操作给char *p[10]赋值这块内容是实际开发中出错率最高的我按场景拆开讲每个都配了反例和正解。5.1 逐个赋值字符串字面量char *p[10]; p[0] hello; // OK让p[0]指向只读字符串 p[1] world; // OK // p的剩余元素仍然是垃圾值绝对不能使用这种写法本身没错但前提是你只读不写。而且千万别忘了剩余未赋值的元素是垃圾值遍历时如果不检查NULL用strlen(p[i])会直接炸。5.2 用strcpy把字符串复制进“指针指向的内存”很多新手希望能把字符串“拷给”指针数组char *p[10]; strcpy(p[0], hello); // 崩溃p[0]是垃圾值没指向合法内存理解起来很简单strcpy需要一个合法的目标缓冲区而p[0]现在要么是垃圾值要么是NULL根本没有一块足够大的可写内存供它写入。想让这段代码正常得先保证p[0]指向一块至少6字节的可写内存char buf[10]; char *p[10]; p[0] buf; // p[0]指向栈上的buf strcpy(p[0], hello); // 现在没问题了把hello\0写进buf或者走堆分配char *p[10]; p[0] (char *)malloc(10); if (p[0] ! NULL) { strcpy(p[0], hello); } free(p[0]); // 用完释放5.3 动态分配N个字符串这是工程里最常见也最实用的形态char *p[10]; int i; for (i 0; i 10; i) { p[i] (char *)malloc(64); if (p[i] NULL) { // 处理分配失败最好把之前已分配的全部释放 while (--i 0) { free(p[i]); } return -1; } snprintf(p[i], 64, item-%d, i); } // 用完之后 for (i 0; i 10; i) { free(p[i]); }这里就要算一下“一共有几块内存”了栈上或全局/堆上有1块指针数组内存堆上有10块由malloc分配的内存总共有11块。少了任何一块忘记释放那就是内存泄漏。用valgrind一查10个definitely lost的节点就是这10个字符串缓冲区。5.4 从函数返回指针数组别直接返回局部指针数组因为函数返回时栈内存就废了。常见正确做法是用static修饰或者干脆在调用方传入数组让函数填充void fill_strings(char *arr[], int n) { static const char *data[] {aaa, bbb, ccc}; for (int i 0; i n i 3; i) { arr[i] (char *)data[i]; // 指向静态字符串 } } int main(void) { char *p[10] {0}; fill_strings(p, 10); // p[0]到p[2]指向字符串常量p[3]到p[9]是NULL return 0; }6. 面试官视角这题背后到底想考什么说实话面试官问“char *p[10]分配了几块内存”这种题未必是真的想知道“1块还是10块”而是想通过这个问题考察几个更深层次的东西。6.1 是否理解指针即地址的思想指针变量本身就是一个存了地址的普通变量它也有自己的存储空间。很多人学了指针之后总觉得指针就是“内存里神秘的东西”见到char *p[10]就开始脑补一堆字符串数据。实际上编译器眼里这就是一个长度为10的数组元素是整数大小的指针而已。理解了“指针也是数据”这个问题就成功了一半。6.2 能否区分声明与初始化、定义与使用char *p[10]这行代码本身只是“声明并定义了一个指针数组”它并没有创建任何字符串、没有复制任何数据也没有在堆上申请任何空间。所有的字符串内容都是后续通过初始化列表、赋值、malloc等方式一个一个“接”上去的。不要把声明阶段跟后续操作混为一谈这是C语言基础是否扎实的分水岭。6.3 是否具备内存安全意识不管答案是1块还是10块最终考察的落点都在“你知不知道哪些内存要手动管理、哪些不用”。栈上的数组会自动释放堆上的malloc必须手动free指向字符串常量的指针不能修改内容。把这些道理想明白才算真正啃下了内存管理这块硬骨头。6.4 追问变体也是考点有经验的面试官不会停止在字面问题上通常会顺着往下问sizeof(p)是多少答取决于平台64位是80sizeof(*p)是多少答4或8一个指针大小p 1跳了几个字节答4或8字节char (*p)[10]又是什么答数组指针这10个指针能不能直接用free(p[i])释放答只有指向堆内存的才能释放指向栈或字符串常量区的不能把这些追问全部答好比背一百道概念题都管用。7. 实际调试中的避坑心得最后聊聊我几次真实调试过程中跟char *p[10]这块内存有关的经典翻车现场希望能帮大家提前避坑。7.1 多线程环境下的静态指针数组如果你用了static char *p[10]去存储每个线程的名称结果多个线程同时往里写会互相覆盖。这次教训之后我在涉及并发的地方一律用调用方传入缓冲区的方式而不是共享一个全局数组。7.2 memset和数组清零char *p[10]定义后如果想快速把所有指针置为NULL可以memset(p, 0, sizeof(p));但注意别写成memset(p, 0, 10)那只会清零前10个字节。64位平台上10个指针占80字节你只清零10个字节剩下的指针还是垃圾值。这个bug我见过不止一次而且非常隐蔽因为它只在某些特定的内存布局下崩溃。7.3 编译器的-Wall真的会帮你把char *p[10] {a, b}剩下未初始化的元素置为NULL是C标准的行为但如果你用char *p[10]; p[0] a;这种局部手写赋值编译器开启-Wall后通常会给未初始化使用的变量一个警告。养成习惯定义指针数组后先全部显式初始化为NULL或对应的合法地址能省掉大量排查时间。7.4 valgrind才是检查内存的照妖镜一旦出现“程序跑起来偶尔崩”“数据被莫名其妙改掉”这类问题别犹豫直接上valgrind --leak-checkfull ./your_program。它会明确告诉你哪一行对哪块内存做了非法读写、哪些内存是definitely lost。比起肉眼review代码用工具把每次指针操作盯死效率高得多。8. 一句话记住核心char *p[10]自身是一块连续内存里面装着10个指针它会额外“牵涉”多少块内存取决于你往这些指针里塞了什么地址。基础概念吃透了这题就不是死记答案而是真能跟人讲清楚“为什么”。希望这篇拆解能帮你在面试或实际开发中对指针数组有一个清醒的认知。记住一个问题看似简单背后展开的知识面能有多大往往才是一个程序员基本功的试金石。
返回列表