【C语言进阶】死磕指针:从内存原理到指针数组的深度解析

🎬 胖咕噜的稞达鸭个人主页
🔥 个人专栏 : 《数据结构《C++初阶高阶》
《Linux系统学习》
《算法日记》

⛺️技术的杠杆,撬动整个世界!


指针是C语言的灵魂,也是许多初学者的噩梦。它赋予了程序员直接操作内存的强大能力,但同时也带来了内存泄漏和野指针的风险。本文系统地梳理了指针的核心概念、运算规则以及与数组的纠葛。

第一部分:指针的基础(内存与类型)

1. 内存与地址

  • 内存单元 :内存被划分为一个个内存单元,每个单元的大小是 1字节(Byte)

  • 编址 :为了有效访问内存,每个单元都有一个编号,这个编号就是地址 ,在C语言中也被称为指针

  • 地址空间

    • 32位机器 :有32根地址线,能寻址 2 32 2^{32} 232 个字节(4GB),地址长度为 4字节
    • 64位机器 :有64根地址线,地址长度为 8字节

2. 指针变量

指针变量是用来存放地址的变量。

c 复制代码
int a = 10;
int *p = &a; // p是整型指针,存放a的地址

3. 指针类型的意义

既然所有指针在同一平台下的大小是固定的(4或8字节),为什么还需要区分 int*char* 等类型?主要有两个原因:

  • 解引用的权限(访问步长)

    • int* 解引用时访问 4 个字节。
    • char* 解引用时访问 1 个字节。
    • 这决定了你通过指针能操作多大的内存区域。
  • 指针运算的步长

    • int* p; p+1 会跳过 4 个字节。
    • char* p; p+1 会跳过 1 个字节。
    • type* p; p+n 跳过 n × s i z e o f ( t y p e ) n \times sizeof(type) n×sizeof(type) 个字节。

4. 特殊指针:void*

void* 是无具体类型的指针,可以接收任意类型的地址。

  • 限制 :不能直接进行解引用操作(*p)和指针运算(p++),因为不知道步长。
  • 用途 :常用于函数参数(如 memcpy),实现泛型编程。

第二部分:指针运算与关键字

1. const 与指针的"恩怨纠葛"

const 修饰指针时,位置不同,含义截然不同:

代码定义 记忆口诀 含义
const int *p; const在*左边 锁内容 :指针指向的内容不能改(*p不可变),指针指向可以改。
int const *p; const在*左边 同上。
int * const p; const在*右边 锁指向 :指针的指向不能改(p不可变),指针指向的内容可以改。
const int * const p; 两边都有 全锁:指向和内容都不能改。

2. 指针运算

  • 指针 +/- 整数:根据类型步长向前或向后移动。

  • 指针 - 指针

    • 前提:两个指针必须指向同一块连续空间(如同一个数组)。
    • 结果:得到两个指针之间的元素个数(不是字节数)。
    • 应用:可以用来模拟实现 strlen 函数。
  • 指针的关系运算:指针可以比较大小(地址的高低)。

3. 野指针

野指针是指向位置不可知(随机、不正确、受限)的指针。

  • 成因

    1. 指针未初始化(默认为随机值)。
    2. 指针越界访问(数组越界)。
    3. 指针指向的空间已释放(释放后未置空)。
  • 规避

    • 初始化时明确赋值,暂不确定指向则赋值为 NULL
    • 使用 assert 断言来防错(#include <assert.h>)。
    • 释放内存后立即将指针置为 NULL

第三部分:指针与数组的深度关系

1. 数组名的本质

通常情况下,数组名就是数组首元素的地址

即:arr 等同于 &arr[0]

唯二的特例:

  1. sizeof(数组名):计算的是整个数组的大小(单位字节)。
  2. &数组名:取出的是整个数组的地址

易错点辨析:

虽然 arr&arr 打印出来的地址值是一样的,但它们的含义不同:

  • arr + 1:跳过一个元素(4字节)。
  • &arr + 1:跳过整个数组(40字节)。

2. 数组传参

当数组作为参数传递给函数时,数组名会退化为指针(指向首元素)。

  • 后果 :在函数内部无法通过 sizeof(arr) 计算数组大小(结果永远是指针的大小4/8字节)。
  • 对策:必须在传参时同时传递数组的元素个数。
c 复制代码
// 错误写法
void test(int arr[]) {
    int sz = sizeof(arr) / sizeof(arr[0]); // 结果错误,永远是1或2
}

// 正确写法
void bubble_sort(int arr[], int sz) { ... }

第四部分:进阶指针概念

1. 二级指针

指针变量也是变量,它自己在内存中也有地址。存放指针变量地址的指针,就是二级指针

c 复制代码
int a = 10;
int *p = &a;    // 一级指针
int **pp = &p;  // 二级指针
  • *pp 访问的是 p
  • **pp 访问的是 a

2. 指针数组

指针数组本质是数组 ,只是数组中存放的元素是指针

  • 定义int* arr[3]; (存放3个整型指针的数组)。

  • 应用:可以使用指针数组来模拟二维数组。

    c 复制代码
    int arr1[] = {1,2,3};
    int arr2[] = {4,5,6};
    int* parr[2] = {arr1, arr2};
    // parr[i][j] 即可访问对应元素

【C语言面试宝典】死磕指针:9个最容易挂掉的底层考点

在C语言的面试中,指针是绝对的重灾区。很多同学觉得"懂了",但一做题就错。这些都是面试官最爱问的"细节杀手"。


考点一:指针的大小(架构决定命运)

面试提问:

"int *char * 哪个占用的内存更大?"

标准回答:

一样大!

指针变量是用来存放地址的。地址的长度只与**硬件架构(地址总线宽度)**有关,与指针指向的数据类型无关。

  • 32位环境(x86) :所有类型的指针(int*, char*, struct*, void*)统统是 4字节
  • 64位环境(x64) :所有类型的指针统统是 8字节

避坑指南:

千万不要因为 double 占8字节,char 占1字节,就觉得 double*char* 大。在同一台机器同一个编译环境下,sizeof(p) 永远是固定的。


考点二:指针类型的意义(步长与权限)

面试提问:

"既然所有指针大小都一样,为什么还要区分 int*char*?为什么不直接弄个通用指针?"

核心考点:

这是考察你对指针运算底层逻辑的理解。类型决定了两件事:

  1. 解引用的权限(视野大小)

    • int *p; *p 一次访问 4个字节
    • char *p; *p 一次访问 1个字节
    • 面试题场景 :给你一个 int n = 0x11223344;,让你用 char* 指针把它改成 0x11223300。这就需要利用 char* 只能访问低地址1个字节的特性。
  2. 指针运算的步长(跳跃距离)

    • int *p; p+1 地址增加 4
    • char *p; p+1 地址增加 1
    • 公式type *p; p+n 的实际地址变化是 n * sizeof(type)

考点三:const 与指针的"罗生门"

面试提问:

"请解释 const int *pint * const p 的区别。"

秒杀口诀:看 const* 的哪一边

  1. 左定值 (const *)

    • const int * p;int const * p;
    • const* 左边,修饰的是 *p(目标内容)。
    • 后果指针指向可以改p = &b OK),但不能通过指针改内容*p = 20 Error)。
    • 场景:函数参数不想被修改时(如 strlen(const char* str))。
  2. 右定向 (* const)

    • int * const p;
    • const* 右边,修饰的是 p(指针本身)。
    • 后果指针指向不能改p = &b Error),但可以通过指针改内容*p = 20 OK)。
    • 场景:类似于引用的效果,指针一旦绑定就不能换人。
  3. 双重锁

    • const int * const p;
    • 指哪里不能改,内容也不能改。

考点四:指针减指针(由地址变计数)

面试提问:

"两个指针相减,结果代表什么?是字节数吗?"

标准回答:

不是字节数!
指针 - 指针 的结果是两个指针之间的 元素个数

前提条件:

两个指针必须指向同一块连续空间(通常是同一个数组)。

代码实战(手写 strlen):

c 复制代码
int my_strlen(char *s) {
    char *start = s;
    while(*s != '\0') {
        s++;
    }
    return s - start; // 此时 s 指向末尾,start 指向开头,相减即为字符个数
}

考点五:野指针(内存杀手)

面试提问:

"什么是野指针?如何避免?"

定义:

指向位置不可知(随机、非法、已回收)的指针。

三大成因(必背):

  1. 未初始化int *p; (局部变量不初始化是随机值,乱指)。
  2. 越界访问 :数组大小为10,你访问 arr[10]arr[12]
  3. 指"亡"灵 :空间被 free 释放了,或者是函数返回了局部变量的地址,但指针还在用。

规避法则(工程规范):

  1. 指针初始化(不知道指谁就指 NULL)。
  2. 小心数组越界。
  3. 指针指向的空间释放后,立即置为 NULL
  4. 使用 assert 断言拦截非法指针。

考点六:数组名的"人格分裂"

面试提问:

"数组名 arr 到底代表什么?是数组首元素的地址,还是整个数组?"

标准回答(黄金法则):

绝大多数情况下,数组名就是数组首元素的地址 (即 arr 等同于 &arr[0])。

唯二的特例(必须死记):

遇到以下两种情况时,数组名代表整个数组

  1. sizeof(数组名):计算的是整个数组的总大小(字节数)。
  2. &数组名:取出的是整个数组的地址。

考点七:arr&arr 的步长差异(高频笔试题)

面试提问:

"arr&arr 打印出来的地址是一样的,那它们有什么区别?arr+1&arr+1 的结果一样吗?"

核心解析:

这是考察你对指针类型步长的理解。虽然数值一样,但它们的"跨度"完全不同。

  • arr :类型是 int*(假设是int数组),指向首元素。

    • arr + 1:跳过一个元素(4字节)。
  • &arr :类型是 int(*)[N](数组指针),指向整个数组。

    • &arr + 1跳过整个数组! (如果是 int arr[10],一下跳过 40 字节)。

代码实战(心中要有图):

c 复制代码
int arr[10] = {0};
// 假设 arr 的地址是 0x0012FF40

printf("%p\n", arr + 1);  
// 结果:0x0012FF44 (加了4)

printf("%p\n", &arr + 1); 
// 结果:0x0012FF68 (加了40 -> 0x28)
// 面试官经常让你算这个地址是多少!

考点八:数组传参的"降维打击"

面试提问:

"我在函数里求数组参数的 sizeof,为什么结果总是4或者8?"

代码场景:

c 复制代码
void test(int arr[]) {
    printf("%d\n", sizeof(arr)); // 输出 4 (32位系统)
}

int main() {
    int arr[10] = {0};
    test(arr);
}

核心原理:
数组传参,传的不是整个数组,而是首元素的地址!

为了节省内存和提高效率,C语言在函数传参时,数组名会**退化(Decay)**为指向首元素的指针。

  • 后果 :在函数内部,arr 不再是数组,而是一个普通的指针变量。
  • 面试坑点 :不要在函数内部计算数组大小!必须在外部算好,作为一个整数参数 size 传进去。

考点九:指针数组 vs 二级指针

面试提问 1:指针数组

"如何用一维数组模拟一个二维数组?"

解析:

使用指针数组int* arr[3])。

  • 数组的每个元素都是一个指针。
  • 每个指针指向一个独立的一维数组。
  • 内存布局 :这三个一维数组在内存中不一定是连续的 ,但可以通过 arr[i][j] 的方式访问,效果和二维数组一样。

面试提问 2:二级指针

"什么时候需要使用二级指针(int **pp)?"

解析:

二级指针主要用于存放一级指针的地址。面试中常见的应用场景:

  1. 修改指针的指向:如果你想在函数内部修改一个外部指针变量(比如链表节点的插入删除),你需要传这个指针的地址(即二级指针)。
  2. 指针数组传参 :当函数参数是 char *arr[](如 main 函数的 argv)时,它在函数接收时就是 char **argv

总结:面试避坑指南

指针的学习是一个从"晕头转向"到"豁然开朗"的过程。理解指针的关键在于:

  1. 分清类型:关注指针指向什么类型,这决定了步长和访问权限。
  2. 分清层级:是一级指针、二级指针,还是数组指针。
  3. 内存视角:时刻牢记指针里面存的是地址,通过地址去操作内存。

做指针相关的笔试题时,请默念以下"三步心法":

  1. 看名字 :有没有 sizeof&?没有就是首元素地址。
  2. 看类型 :是指针还是数组?是 int* 还是 int(*)[10]
  3. 算步长:加1到底是加4个字节,还是加整个数组的大小?

只要搞清楚 "数组名什么时候降维""&arr 的步长是多少",这一章的面试题基本难不倒你。

相关推荐
lly2024062 小时前
Pandas 相关性分析
开发语言
CHINAHEAO2 小时前
Bagisto修复php弃用警告,看着难受
开发语言·php
博大世界2 小时前
Python打包成exe文件方法
开发语言·python
_F_y2 小时前
应用层协议HTTP
网络·网络协议·http
yongui478342 小时前
双线性四边形等参单元程序(MATLAB实现)
开发语言·matlab
TT哇2 小时前
@AllArgsConstructor
java·开发语言
lkbhua莱克瓦242 小时前
TCP通信练习1——多发多收
java·开发语言·网络·网络协议·tcp/ip·tcp练习
Filotimo_2 小时前
在java后端开发中,docker虚拟化容器用处
java·开发语言·docker
learning-striving2 小时前
单臂路由配置实验
网络·计算机网络·ensp·交换机·网络命令