C语言(八):指针下(进阶)-核心考点与高频面试题解析

指针篇(下):进阶核心 + 面试高频考点全解析

指针的基础内容上手后,进阶部分才是掌握 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. 二维数组传参的三大核心规则

  1. 列号绝对不能省略:编译器需要通过列号计算每一行的地址偏移(arr[i][j] = arr + i*列数 + j),省略列号会直接编译报错;
  2. 行号可省略:仅作代码提示,无实际内存限制作用;
  3. 形参本质是数组指针,三种写法完全等价。

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. 核心实现原理

  1. void*通用指针:可接收任意类型数据的首地址,但不能直接解引用、不能直接做地址运算;
  2. 强转char*做地址偏移:char*的步长为 1 字节,能通过(char*)base + j*size精准计算任意类型元素的地址;
  3. 逐字节交换:不管是 int、结构体还是其他类型,都能通过遍历字节完成交换,实现通用的交换逻辑;
  4. 回调函数解耦:排序的比较规则由用户自定义的回调函数决定,实现了排序逻辑与数据类型的解耦。

九、sizeof 与 strlen 的核心差异(面试必考)

sizeofstrlen是 C 语言极易混淆的两个标识符,基础篇仅零散使用,二者的核心差异是笔试面试的基础必考题,一张表就能清晰区分。

1. 核心差异对比表

特性sizeofstrlen
本质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.核心思路

  1. 利用异或特性:相同数字异或结果为 0,0 异或任意数字结果为该数字本身,将数组中所有数字异或后,结果为两个目标数字的异或值;
  2. 找到异或结果中第一个为 1 的二进制位:该位表示两个目标数字在这一位上不同,以此为依据将数组分为两组;
  3. 分组异或:将两组数字分别异或,最终得到的两个结果就是数组中只出现一次的两个数字;
  4. 指针操作:通过指针遍历数组,p++的步长由指针类型决定(此处为 int*,步长 4 字节),精准访问每个数组元素。

十一、核心总结

  1. 复杂指针类型的区分核心是优先级规则() > [] > *,先结合的标识符决定其本质(先结合()是指针,先结合[]是数组);
  2. void*是 C 语言的通用指针,可接收任意类型地址,是实现通用编程的核心,需强转为char*后才能做地址运算;
  3. 回调函数是 C 语言实现函数解耦的关键,标准库qsort的通用性正是基于void*通用指针 + 用户自定义回调函数实现;
  4. 二维数组传参列号不可省略,形参本质是数组指针,这是与一维数组传参的核心区别;
  5. sizeof是编译期计算内存大小的操作符,strlen是运行期计算字符串长度的库函数,二者无关联,切勿混淆;
  6. 指针运算仅支持 ± 整数、指针 - 指针、关系运算 ,指针 + 指针、乘除运算均为非法操作,指针 ± 整数的步长由指针类型决定。

十二、指针上下篇整体衔接

指针基础篇围绕指针的本质(内存地址)、基础操作、一维数组与指针的关联展开,是理解指针的基础;进阶篇则聚焦复杂指针类型、通用编程思想、面试高频考点,是对指针知识的综合应用和深化。二者结合构成了 C 语言指针的完整知识体系,而贯穿始终的核心底层逻辑不变:指针的本质是内存地址,指针的类型决定了解引用的权限和运算的步长

所有示例代码均可直接复制到编译器运行,建议动手敲一遍、修改参数尝试不同的效果,只有通过实际操作,才能真正把指针的知识点吃透,应对考试和面试也会更得心应手。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值