指针篇(下):进阶核心 + 面试高频考点全解析
指针的基础内容上手后,进阶部分才是掌握 C 语言核心、应对期末笔试和校招面试的关键。这篇把指针进阶的重点内容拆解清楚,搭配可直接运行的代码示例,没有晦涩的表述,从易混概念到实战应用,一步步吃透指针高阶用法。
引言
在指针基础篇中,我们掌握了指针的本质、一维数组与指针的关联、基础的指针操作等内容,而进阶部分则聚焦于字符指针的高阶用法、数组指针与指针数组的区分、二维数组传参、函数指针及回调函数这些核心考点,还有标准库qsort的使用与模拟实现,这些内容不仅是考试常考的重点,也是实际开发和面试中高频问到的知识点。接下来的内容全程搭配实战代码,所有示例均可直接复制运行,帮大家把抽象的指针知识落地。
一、字符指针变量(面试高频核心考点)
字符指针char*是指针最常用的进阶场景,专门用于指向字符串,核心考察常量字符串的存储特性与内存规则,基础篇仅涉及单个字符的指针指向,本节为纯进阶内容。
1. 指向常量字符串的核心特性
C 语言没有专门的字符串类型,字符串是以\0为结束标志的字符数组,其中常量字符串(如"abcdef")会被编译器存储在程序的只读数据区,内容不可修改,字符指针仅存储其首元素的地址。
#include <stdio.h>
int main()
{
// p仅存储常量字符串首字符'a'的地址,非字符串整体赋值
const char* p = "abcdef";
printf("%c\n", *p); // 解引用取首字符,输出'a'
printf("%s\n", p); // %s从首地址开始打印,直到遇到'\0'结束,输出"abcdef"
// *p = 'g'; // 非法操作!编译可通过但运行直接崩溃,只读内存不可修改
return 0;
}
核心规范:用const char*指向常量字符串,是规范的写法,能明确告知编译器与开发者 “此字符串内容不可修改”,从语法层面规避误操作。
2. 经典面试题:字符数组 vs 字符指针
这道题核心考察常量字符串的内存分配与编译器优化,是笔试必考题型,跑一遍代码就能清晰理解:
#include <stdio.h>
int main()
{
// 字符数组——栈上分配两个独立内存空间,存储相同字符串
char str1[] = "hello Luminous.";
char str2[] = "hello Luminous.";
// 字符指针——指向只读数据区的同一个常量字符串
const char* str3 = "hello Luminous.";
const char* str4 = "hello Luminous.";
// 比较的是数组首元素的地址,两个独立内存地址不同
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n"); // 最终输出此行
// 编译器做常量合并优化,两个指针指向同一块只读内存,地址相同
if (str3 == str4)
printf("str3 and str4 are same\n"); // 最终输出此行
else
printf("str3 and str4 are not same\n");
return 0;
}
核心结论:需要修改字符串内容时,用字符数组(栈区内存可修改);仅读取字符串内容时,用字符指针(无需额外拷贝字符串,节省内存)。
二、数组指针变量
基础篇已经讲解指针数组(本质是数组,元素为指针)的基础定义,本节为其易混进阶概念数组指针,二者是笔试面试的核心区分考点,记住优先级规则就能轻松分辨。
1. 核心定义与格式
数组指针是专门存储整个数组地址的指针,本质是指针,指向一段固定长度、指定类型的数组。
- 定义格式:
数据类型 (*指针名)[数组长度]; - 优先级规则:
() > [] > *,(*指针名)先与括号结合,明确其本质是指针,再与[]结合,表示指向的数组规格。 - 示例:
int (*p)[5];→ 定义一个指针,指向包含 5 个int类型元素的一维数组。
2. 指针数组 vs 数组指针 核心区分
| 写法 | 本质 | 核心判断(优先级) |
|---|---|---|
int *p[5] | 指针数组 | 先结合[],是数组 |
int (*p)[5] | 数组指针 | 先结合(),是指针 |
3. 进阶应用:指针数组模拟二维数组
指针数组的核心进阶用法是模拟二维数组,和真正的二维数组相比,它支持指向不同长度的一维数组,灵活性更高:
#include <stdio.h>
int main()
{
// 定义3个独立的一维数组,作为模拟二维数组的“行”
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
// 指针数组:每个元素都是int*,存储3个一维数组的首地址
int* parr[3] = { arr1, arr2, arr3 };
// 模拟二维数组的访问,parr[i][j]等价于*(*(parr + i) + j)
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
运行结果:
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
核心区别:真正的二维数组内存是连续的,而指针数组模拟的二维数组,每个 “行” 是独立的一维数组,内存不连续,仅通过指针数组关联。
三、二维数组传参的本质
基础篇仅讲解了一维数组传参的本质,二维数组传参是数组指针的核心应用场景,也是面试高频考点,记住三个核心规则就能掌握。
1. 二维数组名的本质
二维数组的本质是数组的数组,例如int arr[3][5]表示一个包含 3 个元素的数组,每个元素是一个包含 5 个int的一维数组。
- 二维数组名
arr,代表数组首行的地址,类型为数组指针int (*)[5],而非首元素地址。
2. 二维数组传参的三大核心规则
- 列号绝对不能省略:编译器需要通过列号计算每一行的地址偏移(
arr[i][j] = arr + i*列数 + j),省略列号会直接编译报错; - 行号可省略:仅作代码提示,无实际内存限制作用;
- 形参本质是数组指针,三种写法完全等价。
3. 二维数组传参的三种等价写法
// 写法1:完整二维数组形式(行号+列号)
void fun(int arr[3][5], int row) {}
// 写法2:省略行号(最常用,简洁直观)
void fun(int arr[][5], int row) {}
// 写法3:数组指针形式(最推荐,明确体现传参本质)
void fun(int (*arr)[5], int row) {}
4. 二维数组的数组指针访问实战
#include <stdio.h>
// 形参用数组指针接收二维数组的首行地址
void print(int (*p)[5], int r, int c)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
// 两种完全等价的访问方式
// 方式1:指针解引用 *(*(p + i) + j)
// 方式2:下标访问 p[i][j](语法糖,更易读)
printf("%d ", p[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
print(arr, 3, 5); // 二维数组名arr作为首行地址传入
return 0;
}
四、函数指针变量
函数指针是 C 语言指针进阶的核心概念,专门指向函数的入口地址,是实现回调函数、函数指针数组的基础,用简易计算器的实战代码来理解,会非常直观。
1. 函数地址的核心特性
函数名本身就是函数的入口地址,&函数名与函数名完全等价,都表示函数的地址,这和数组名有本质区别:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
// 以下两行输出完全相同的地址,&Add与Add等价
printf("%p\n", &Add);
printf("%p\n", Add);
return 0;
}
2. 函数指针实战:简易计算器
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void Display()
{
printf("===== 简易计算器 =====\n");
printf("1. 加法 (+) 2. 减法 (-)\n");
printf("3. 乘法 (*) 4. 除法 (/)\n");
printf("0. 退出程序\n");
printf("=======================\n");
}
// 四则运算核心函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) {
// 防除数为0异常
if (b == 0) {
printf("错误:除数不能为0!\n");
return -1;
}
return a / b;
}
// 函数指针作为参数:统一调用逻辑
void calc(int (*p)(int, int))
{
int a, b;
printf("请输入两个操作数(空格分隔):");
scanf("%d%d", &a, &b);
// 单独处理除法异常
if (p == div && b == 0) return;
printf("运算结果:%d\n\n", p(a, b));
}
int main()
{
int input = 1;
do
{
Display();
printf("请选择操作(0-4):");
scanf("%d", &input);
switch (input)
{
case 1: calc(add); break;
case 2: calc(sub); break;
case 3: calc(mul); break;
case 4: calc(div); break;
case 0: printf("退出程序~\n"); break;
default: printf("输入无效,请重新选择!\n\n"); break;
}
} while (input);
return 0;
}
3. 核心知识点解析
- 函数指针定义:
int (*p)(int, int)表示指针p指向「接收两个 int 参数、返回 int 的函数」; - 调用方式:
p(a,b)与(*p)(a,b)完全等价,前者更简洁,编译器会自动解析; - 实际开发中,给函数指针传参能有效统一调用逻辑,减少冗余代码。
五、函数指针数组与转移表
用switch实现的计算器会有大量冗余的条件判断,而函数指针数组(转移表) 能直接通过下标调用函数,代码更简洁、扩展性更强,是实际开发中常用的技巧。
1. 转移表版计算器(优化后)
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
// 四则运算函数(接口保持一致)
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) {
if (b == 0) { printf("错误:除数不能为0!\n"); return -1; }
return a / b;
}
// 空函数:处理非法输入
int empty(int a, int b) { printf("输入无效!\n"); return 0; }
int main()
{
int a, b, op;
// 函数指针数组(转移表):下标0-4对应空/加/减/乘/除
int (*calc[5])(int, int) = { empty, add, sub, mul, div };
while (1) {
printf("\n===== 转移表计算器 =====\n");
printf("1.加 2.减 3.乘 4.除 0.退出\n");
printf("请选择操作:");
scanf("%d", &op);
if (op == 0) { printf("退出程序~\n"); break; }
if (op < 0 || op > 4) { printf("输入错误!\n"); continue; }
printf("请输入两个操作数:");
scanf("%d%d", &a, &b);
// 下标直接调用,无需switch/if判断
if (op != 4 || b != 0) {
printf("结果:%d\n", calc[op](a, b));
}
}
return 0;
}
2. 核心优势
- 扩展性极强:新增功能(如取模运算)时,只需新增函数并将地址加入转移表,无需修改原有业务逻辑;
- 代码更简洁:消除大量重复的
case判断,可读性更高; - 访问效率高:数组下标访问的时间复杂度为 O (1),比
switch的条件判断更高效。
六、回调函数
回调函数是 C 语言实现函数解耦的核心手段,也是标准库qsort的核心原理,简单来说,回调函数就是 “自己定义的函数,通过函数指针传给其他函数,由其他函数在合适的时机调用”。
1. 回调函数实战:qsort 排序学生信息(按姓名 / 年龄)
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 学生结构体定义
struct student
{
char name[10];
int age;
};
// 回调函数1:按姓名字典序升序
int cmp_student_name(const void* x, const void* y)
{
return strcmp(((const struct student*)x)->name, ((const struct student*)y)->name);
}
// 回调函数2:按年龄升序
int cmp_student_age(const void* x, const void* y)
{
return ((const struct student*)x)->age - ((const struct student*)y)->age;
}
// 打印学生信息
void print_student(struct student a[], int len)
{
for (int i = 0; i < len; i++)
{
printf("姓名:%s,年龄:%d\n", a[i].name, a[i].age);
}
printf("-------------------------\n");
}
int main()
{
// 初始化学生数据
struct student a[] = { {"张三", 20}, {"李四", 19}, {"王五", 21} };
int len = sizeof(a) / sizeof(a[0]);
printf("原数据:\n");
print_student(a, len);
// 按姓名排序(传入姓名比较的回调函数)
qsort(a, len, sizeof(struct student), cmp_student_name);
printf("按姓名排序后:\n");
print_student(a, len);
// 按年龄排序(传入年龄比较的回调函数)
qsort(a, len, sizeof(struct student), cmp_student_age);
printf("按年龄排序后:\n");
print_student(a, len);
return 0;
}
2. 运行结果
原数据:
姓名:张三,年龄:20
姓名:李四,年龄:19
姓名:王五,年龄:21
-------------------------
按姓名排序后:
姓名:李四,年龄:19
姓名:王五,年龄:21
姓名:张三,年龄:20
-------------------------
按年龄排序后:
姓名:李四,年龄:19
姓名:张三,年龄:20
姓名:王五,年龄:21
-------------------------
3. 回调函数核心规则
qsort的比较函数必须遵循固定的接口:int (*)(const void*, const void*),保证通用性;- 返回值规则:返回值 <0,第一个元素排在第二个元素前面;返回值 = 0,两个元素顺序不变;返回值> 0,第一个元素排在第二个元素后面;
const void*的使用:保护原数据不被修改,使用时需强制转换为对应的数据类型。
七、标准库 qsort 通用排序函数
qsort是 C 语言标准库(stdlib.h)的通用排序函数,基于快速排序实现,支持任意类型数据(int、字符串、结构体)的排序,其通用性的核心就是void*通用指针 + 回调函数。
1. qsort 函数原型
void qsort(
void *base,
size_t nitems,
size_t size,
int (*compar)(const void *, const void *)
);
2. 参数全解析
| 参数 | 含义 |
|---|---|
void *base | 待排序数据的首地址,void*为通用指针,可接收任意类型数据的地址 |
size_t nitems | 待排序的元素个数(size_t为无符号整数类型) |
size_t size | 单个元素的字节大小 |
int (*compar)(const void *, const void *) | 比较函数的函数指针(回调函数),由用户自定义排序规则 |
八、qsort 函数的模拟实现
为了彻底理解qsort的通用排序原理,我们基于冒泡排序(逻辑简单,适合模拟)实现自定义的通用排序函数my_qsort,核心是void*通用指针 + 回调函数 + 逐字节交换,这也是面试中的高频编程题。
1. 自定义 my_qsort 完整可运行代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
// 通用交换函数:逐字节交换,支持任意类型数据
void Swap(char *buf1, char *buf2, size_t size)
{
for(size_t i = 0; i < size; i++)
{
char temp = *buf1;
*buf1 = *buf2;
*buf2 = temp;
buf1++;
buf2++;
}
}
// 模拟qsort:基于冒泡排序的通用排序函数
void my_qsort(void* a, int count, int size, int(*point)(void* a, void* b))
{
for (int i = 0; i < count - 1; i++)
{
for (int j = 0; j < count - 1 - i; j++)
{
if (point((char*)a + size * j, (char*)a + size * (j + 1)) > 0)
{
_swap((char*)a + j * size, (char*)a + (j + 1) * size, size);
}
}
}
}
// 测试:复用学生排序的回调函数
struct student
{
char name[10];
int age;
};
int cmp_student_age(const void* x, const void* y)
{
return ((const struct student*)x)->age - ((const struct student*)y)->age;
}
void print_student(struct student a[], int len)
{
for (int i = 0; i < len; i++)
{
printf("姓名:%s,年龄:%d\n", a[i].name, a[i].age);
}
}
int main()
{
struct student a[] = { {"张三", 20}, {"李四", 19}, {"王五", 21} };
int len = sizeof(a) / sizeof(a[0]);
printf("自定义qsort按年龄排序后:\n");
my_qsort(a, len, sizeof(struct student), cmp_student_age);
print_student(a, len);
return 0;
}
2. 核心实现原理
void*通用指针:可接收任意类型数据的首地址,但不能直接解引用、不能直接做地址运算;- 强转
char*做地址偏移:char*的步长为 1 字节,能通过(char*)base + j*size精准计算任意类型元素的地址; - 逐字节交换:不管是 int、结构体还是其他类型,都能通过遍历字节完成交换,实现通用的交换逻辑;
- 回调函数解耦:排序的比较规则由用户自定义的回调函数决定,实现了排序逻辑与数据类型的解耦。
九、sizeof 与 strlen 的核心差异(面试必考)
sizeof和strlen是 C 语言极易混淆的两个标识符,基础篇仅零散使用,二者的核心差异是笔试面试的基础必考题,一张表就能清晰区分。
1. 核心差异对比表
| 特性 | sizeof | strlen |
|---|---|---|
| 本质 | C 语言关键字 / 操作符 | C 语言标准库函数(需包含 <string.h>) |
| 计算内容 | 变量 / 类型占用的内存字节数 | 字符串有效字符个数(不含结束符\0) |
| 结束标志 | 无需结束标志,直接计算内存总大小 | 必须以\0为结束标志,遇到\0立即停止 |
| 操作对象 | 任意类型(变量、数组、指针、类型名) | 仅能操作字符串(char*/ 字符数组) |
| 计算时机 | 编译期计算,结果为常量 | 运行期计算,结果为变量 |
| 返回值类型 | size_t(无符号整数) | size_t(无符号整数) |
2. 经典示例代码
#include <stdio.h>
#include <string.h>
int main()
{
char str[] = "abcdef";
char *p = str;
char str2[10] = "abc";
printf("sizeof(str) = %zd\n", sizeof(str)); // 7(6个字符+1个'\0')
printf("strlen(str) = %zd\n", strlen(str)); // 6(有效字符,不含'\0')
printf("sizeof(p) = %zd\n", sizeof(p)); // 8(64位系统,指针大小固定)
printf("sizeof(str2) = %zd\n", sizeof(str2)); // 10(数组总内存大小)
printf("strlen(str2) = %zd\n", strlen(str2)); // 3(遇'\0'停止)
return 0;
}
十、经典指针笔试题解析
指针笔试题综合考察数组名的本质、指针运算规则、sizeof/strlen的使用,也是面试的压轴考点,下面这道指针 + 位运算的经典题,是校招中高频出现的题型。
1.题型:找数组中唯一出现一次的两个数字
#include <stdio.h>
// 找出数组中只出现一次的两个数字(其余数字出现两次)
void findSingleNumbers(int* a, int n, int* res) {
if (a == NULL || res == NULL || n < 2) {
printf("输入参数无效!\n");
return;
}
int sum = 0;
int* p = a;
for (int i = 0; i < n; i++, p++) {
sum ^= *p;
}
int mask = 1;
while ((sum & mask) == 0) {
mask <<= 1;
}
int x = 0, y = 0;
p = a;
for (int i = 0; i < n; i++, p++) {
if (*p & mask) {
x ^= *p;
}
else {
y ^= *p;
}
}
*res = x;
*(res + 1) = y;
}
int main() {
int a[] = { 1, 2, 3, 4, 5, 1, 2, 3, 4, 6 };
int n = sizeof(a) / sizeof(a[0]);
int res[2];
findSingleNumbers(a, n, res);
printf("数组中只出现一次的两个数字是:%d 和 %d\n", res[0], res[1]);
return 0;
}
2.运行结果
数组中只出现一次的两个数字是:5 和 6
3.核心思路
- 利用异或特性:相同数字异或结果为 0,0 异或任意数字结果为该数字本身,将数组中所有数字异或后,结果为两个目标数字的异或值;
- 找到异或结果中第一个为 1 的二进制位:该位表示两个目标数字在这一位上不同,以此为依据将数组分为两组;
- 分组异或:将两组数字分别异或,最终得到的两个结果就是数组中只出现一次的两个数字;
- 指针操作:通过指针遍历数组,
p++的步长由指针类型决定(此处为 int*,步长 4 字节),精准访问每个数组元素。
十一、核心总结
- 复杂指针类型的区分核心是优先级规则:
() > [] > *,先结合的标识符决定其本质(先结合()是指针,先结合[]是数组); void*是 C 语言的通用指针,可接收任意类型地址,是实现通用编程的核心,需强转为char*后才能做地址运算;- 回调函数是 C 语言实现函数解耦的关键,标准库
qsort的通用性正是基于void*通用指针 + 用户自定义回调函数实现; - 二维数组传参列号不可省略,形参本质是数组指针,这是与一维数组传参的核心区别;
sizeof是编译期计算内存大小的操作符,strlen是运行期计算字符串长度的库函数,二者无关联,切勿混淆;- 指针运算仅支持 ± 整数、指针 - 指针、关系运算 ,指针 + 指针、乘除运算均为非法操作,指针 ± 整数的步长由指针类型决定。
十二、指针上下篇整体衔接
指针基础篇围绕指针的本质(内存地址)、基础操作、一维数组与指针的关联展开,是理解指针的基础;进阶篇则聚焦复杂指针类型、通用编程思想、面试高频考点,是对指针知识的综合应用和深化。二者结合构成了 C 语言指针的完整知识体系,而贯穿始终的核心底层逻辑不变:指针的本质是内存地址,指针的类型决定了解引用的权限和运算的步长。
所有示例代码均可直接复制到编译器运行,建议动手敲一遍、修改参数尝试不同的效果,只有通过实际操作,才能真正把指针的知识点吃透,应对考试和面试也会更得心应手。
661

被折叠的 条评论
为什么被折叠?



