C 语言(七)|指针进阶:数组指针、函数指针与复杂声明
深入探讨C语言指针的高级用法,包括指针数组、数组指针、函数指针、指针与字符串、内存管理等核心知识点。
版权声明:本文为作者原创,版权归作者所有。如需转载或引用,请联系作者获得授权,并在使用时注明出处。
前言
指针是C语言的精髓,掌握指针意味着你真正理解了计算机的内存管理机制。本篇文章我们继续深入探讨指针的高级用法,包括指针数组、数组指针、函数指针、命令行参数、字符串与指针、动态内存管理等核心知识点。这些内容是C语言学习的重要里程碑,也是面试中频繁考察的重点。
一、指针数组与数组指针
1.1 指针数组
定义:指针数组是一个数组,其元素是指针。
语法格式:type *array_name[array_size];
#include <stdio.h>
int main() {
int a = 10, b = 20, c = 30;
int *arr[3] = {&a, &b, &c}; // 指针数组
for (int i = 0; i < 3; i++) {
printf("arr[%d] = %p, *arr[%d] = %d\n", i, arr[i], i, *arr[i]);
}
return 0;
}
1.2 数组指针
定义:数组指针是一个指针,指向一个数组。
语法格式:type (*pointer_name)[array_size];
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr; // 数组指针,指向整个数组
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
printf("p = %p\n", p);
printf("*p = %p\n", *p);
printf("(*p)[0] = %d\n", (*p)[0]);
printf("(*p)[2] = %d\n", (*p)[2]);
return 0;
}
1.3 区分指针数组与数组指针
记忆技巧:看*与数组名的优先级。
int arr[5]:[]优先级高于,所以是"指针的数组"—— 指针数组int (p)[5]:括号优先级最高,先与p结合,所以是"指向数组的指针"—— 数组指针
#include <stdio.h>
int main() {
int a = 1, b = 2, c = 3, d = 4, e = 5;
int *p1[5] = {&a, &b, &c, &d, &e}; // 指针数组:5个int*组成的数组
int arr[5] = {1, 2, 3, 4, 5};
int (*p2)[5] = &arr; // 数组指针:指向int[5]的指针
printf("指针数组大小:%zu\n", sizeof(p1)); // 5 * 8 = 40(64位系统)
printf("数组指针大小:%zu\n", sizeof(p2)); // 8(64位系统)
return 0;
}
二、二维数组与指针
2.1 二维数组名的理解
关键结论:
- 二维数组名是指向"第一行数组"的指针类型(
int(*)[列数]) arr[i]等价于*(arr+i),表示第i行的首地址arr[i][j]等价于((arr+i)+j),表示第i行第j列的元素
#include <stdio.h>
int main() {
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
printf("arr = %p\n", arr);
printf("arr[0] = %p\n", arr[0]);
printf("&arr[0][0] = %p\n", &arr[0][0]);
printf("*arr = %p\n", *arr); // arr[0]的地址
printf("**arr = %d\n", **arr); // arr[0][0]的值
printf("arr + 1 = %p\n", arr + 1); // arr[1]的地址
printf("*(arr + 1) = %p\n", *(arr + 1)); // arr[1]的地址
printf("*(*(arr + 1) + 2) = %d\n", *(*(arr + 1) + 2)); // arr[1][2]的值
return 0;
}
2.2 二维数组的遍历方式
#include <stdio.h>
int main() {
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 方式1:下标法
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%2d ", arr[i][j]);
}
printf("\n");
}
// 方式2:指针法
printf("\n");
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%2d ", *(*(arr + i) + j));
}
printf("\n");
}
// 方式3:行列指针
printf("\n");
for (int (*p)[4] = arr; p < arr + 3; p++) {
for (int *q = *p; q < *p + 4; q++) {
printf("%2d ", *q);
}
printf("\n");
}
return 0;
}
三、函数指针
3.1 理解函数指针
函数也有地址:函数是存储在代码区的代码块,函数名就是这段代码的首地址。
函数指针的定义:return_type (*pointer_name)(parameters);
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
int (*p)(int, int) = add; // 函数指针,指向add函数
printf("add的地址:%p\n", add);
printf("p的值:%p\n", p);
printf("(*p)(3, 4) = %d\n", (*p)(3, 4)); // 通过函数指针调用
printf("p(3, 4) = %d\n", p(3, 4)); // 直接通过指针调用
return 0;
}
3.2 函数指针的使用场景
场景1:作为参数传递(回调函数)
#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) { return b != 0 ? a / b : 0; }
int calc(int a, int b, int (*op)(int, int)) {
return op(a, b);
}
int main() {
printf("%d\n", calc(10, 5, add)); // 15
printf("%d\n", calc(10, 5, sub)); // 5
printf("%d\n", calc(10, 5, mul)); // 50
printf("%d\n", calc(10, 5, div)); // 2
return 0;
}
场景2:作为返回值
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int (*select(char op))(int, int) {
if (op == '+') return add;
if (op == '-') return sub;
return NULL;
}
int main() {
int (*p)(int, int) = select('+');
printf("%d\n", p(10, 5)); // 15
return 0;
}
3.3 函数指针实战:简易计算器
#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.4 函数指针数组与转移表
#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;
}
3.5 回调函数与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;
}
3.6 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;
}
四、字符指针变量(面试高频核心考点)
4.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*指向常量字符串,是规范的写法,能明确告知编译器与开发者 "此字符串内容不可修改",从语法层面规避误操作。
4.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;
}
核心结论:需要修改字符串内容时,用字符数组(栈区内存可修改);仅读取字符串内容时,用字符指针(无需额外拷贝字符串,节省内存)。
4.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
核心区别:真正的二维数组内存是连续的,而指针数组模拟的二维数组,每个 "行" 是独立的一维数组,内存不连续,仅通过指针数组关联。
4.4 字符串数组
#include <stdio.h>
int main() {
const char *names[] = {
"Alice",
"Bob",
"Charlie",
"David"
};
int len = sizeof(names) / sizeof(names[0]);
for (int i = 0; i < len; i++) {
printf("names[%d] = %s, 地址 = %p\n", i, names[i], names[i]);
}
return 0;
}
4.3 常见字符串操作函数(使用指针)
#include <stdio.h>
int my_strlen(const char *s) {
int len = 0;
while (s[len] != '\0') {
len++;
}
return len;
}
char *my_strcpy(char *dest, const char *src) {
int i = 0;
while (src[i] != '\0') {
dest[i] = src[i];
i++;
}
dest[i] = '\0';
return dest;
}
char *my_strcat(char *dest, const char *src) {
int i = 0, j = 0;
while (dest[i] != '\0') i++;
while (src[j] != '\0') {
dest[i++] = src[j++];
}
dest[i] = '\0';
return dest;
}
int my_strcmp(const char *s1, const char *s2) {
while (*s1 && *s2 && *s1 == *s2) {
s1++;
s2++;
}
return *s1 - *s2;
}
int main() {
char str1[100] = "Hello";
char str2[] = "World";
printf("my_strlen(str1) = %d\n", my_strlen(str1));
my_strcpy(str1, str2);
printf("my_strcpy: %s\n", str1);
my_strcat(str1, "!");
printf("my_strcat: %s\n", str1);
printf("my_strcmp(\"abc\", \"abd\") = %d\n", my_strcmp("abc", "abd"));
return 0;
}
五、命令行参数
5.1 main函数的参数
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("参数个数:%d\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
参数说明:
argc:命令行参数的个数(包括程序名)argv:指针数组,每个元素指向一个参数字符串
5.2 使用示例
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("用法:%s <数字1> <数字2>\n", argv[0]);
return 1;
}
int a = atoi(argv[1]);
int b = atoi(argv[2]);
printf("%d + %d = %d\n", a, b, a + b);
return 0;
}
运行:program.exe 10 20
输出:10 + 20 = 30
六、动态内存管理
6.1 动态内存分配函数
| 函数 | 说明 |
|---|---|
| malloc | 分配指定字节的内存空间 |
| calloc | 分配并初始化为0 |
| realloc | 调整已分配内存的大小 |
| free | 释放动态分配的内存 |
6.2 malloc与free
#include <stdio.h>
#include <stdlib.h>
int main() {
int n = 5;
int *arr = (int *)malloc(n * sizeof(int));
if (arr == NULL) {
printf("内存分配失败\n");
return 1;
}
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
free(arr);
arr = NULL;
return 0;
}
6.3 calloc
#include <stdio.h>
#include <stdlib.h>
int main() {
int n = 5;
int *arr = (int *)calloc(n, sizeof(int));
if (arr == NULL) {
printf("内存分配失败\n");
return 1;
}
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]); // 输出0(已初始化为0)
}
printf("\n");
free(arr);
arr = NULL;
return 0;
}
6.4 realloc
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr = (int *)malloc(5 * sizeof(int));
for (int i = 0; i < 5; i++) {
arr[i] = i + 1;
}
arr = (int *)realloc(arr, 10 * sizeof(int));
for (int i = 5; i < 10; i++) {
arr[i] = (i + 1) * 10;
}
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]);
}
printf("\n");
free(arr);
arr = NULL;
return 0;
}
6.5 常见的内存错误
错误1:内存泄漏
void leak() {
int *p = (int *)malloc(sizeof(int));
*p = 100;
// 忘记free(p)
}
正确做法:配对使用malloc和free
错误2:野指针
int *p = (int *)malloc(sizeof(int));
free(p);
*p = 100; // 错误:p已成为野指针
正确做法:释放后立即置为NULL
int *p = (int *)malloc(sizeof(int));
free(p);
p = NULL;
错误3:重复释放
int *p = (int *)malloc(sizeof(int));
free(p);
free(p); // 错误:重复释放
七、void*指针
7.1 void*指针的特点
void*是一种"通用指针",可以指向任意类型void*不能直接解引用,需要先强制转换void*不能进行指针运算
#include <stdio.h>
#include <stdlib.h>
int main() {
int a = 10;
float b = 3.14;
char c = 'A';
void *p;
p = &a;
printf("%d\n", *(int *)p);
p = &b;
printf("%.2f\n", *(float *)p);
p = &c;
printf("%c\n", *(char *)p);
return 0;
}
7.2 void*的使用场景
场景1:memcpy函数
#include <stdio.h>
#include <string.h>
int main() {
int src[] = {1, 2, 3, 4, 5};
int dest[5];
memcpy(dest, src, sizeof(src));
for (int i = 0; i < 5; i++) {
printf("%d ", dest[i]);
}
printf("\n");
return 0;
}
八、复杂指针声明
8.1 阅读技巧
从变量名开始,按优先级依次向外解读。
int *p[5]; // p是一个数组,元素是int*类型(指针数组)
int (*p)[5]; // p是一个指针,指向int[5]类型(数组指针)
int **p; // p是一个指针,指向int*类型(指向指针的指针)
int *(*p)[5]; // p是一个指针,指向int*[5]类型
8.2 函数指针声明解读
int (*p)(int, int); // p是指向函数的指针,参数为(int, int),返回int
int (*p[3])(int); // p是数组,元素是指向int(int)的函数指针
int *(*p[3])(int); // p是数组,元素是指向int*(int)的函数指针
九、指针与结构体
9.1 结构体指针的使用
#include <stdio.h>
struct Student {
char name[20];
int age;
float score;
};
int main() {
struct Student stu = {"Tom", 20, 95.5};
struct Student *p = &stu;
printf("姓名:%s\n", (*p).name);
printf("姓名:%s\n", p->name); // 两种方式等价
printf("年龄:%d\n", p->age);
printf("成绩:%.1f\n", p->score);
p->age = 21;
printf("修改后年龄:%d\n", stu.age);
return 0;
}
9.2 结构体指针数组
#include <stdio.h>
struct Student {
char name[20];
int age;
};
int main() {
struct Student students[] = {
{"Alice", 20},
{"Bob", 22},
{"Charlie", 21}
};
struct Student *p = students;
int len = sizeof(students) / sizeof(students[0]);
for (int i = 0; i < len; i++) {
printf("%s %d\n", (p + i)->name, (p + i)->age);
}
return 0;
}
十、常见面试题
10.1 题目:使用指针将字符串逆序
#include <stdio.h>
void reverse_string(char *str) {
char *start = str;
char *end = str;
while (*end != '\0') {
end++;
}
end--;
while (start < end) {
char temp = *start;
*start = *end;
*end = temp;
start++;
end--;
}
}
int main() {
char str[] = "Hello World";
reverse_string(str);
printf("%s\n", str);
return 0;
}
10.2 题目:使用指针实现strstr函数
#include <stdio.h>
char *my_strstr(const char *haystack, const char *needle) {
if (*needle == '\0') return (char *)haystack;
while (*haystack != '\0') {
const char *h = haystack;
const char *n = needle;
while (*h && *n && (*h == *n)) {
h++;
n++;
}
if (*n == '\0') {
return (char *)haystack;
}
haystack++;
}
return NULL;
}
int main() {
char str[] = "Hello World";
char *result = my_strstr(str, "World");
if (result) {
printf("找到:%s\n", result);
} else {
printf("未找到\n");
}
return 0;
}
十一、sizeof 与 strlen 的核心差异(面试必考)
sizeof和strlen是 C 语言极易混淆的两个标识符,二者的核心差异是笔试面试的基础必考题。
11.1 核心差异对比表
| 特性 | sizeof | strlen |
|---|---|---|
| 本质 | C 语言关键字 / 操作符 | C 语言标准库函数(需包含 <string.h>) |
| 计算内容 | 变量 / 类型占用的内存字节数 | 字符串有效字符个数(不含结束符\0) |
| 结束标志 | 无需结束标志,直接计算内存总大小 | 必须以\0为结束标志,遇到\0立即停止 |
| 操作对象 | 任意类型(变量、数组、指针、类型名) | 仅能操作字符串(char*/ 字符数组) |
| 计算时机 | 编译期计算,结果为常量 | 运行期计算,结果为变量 |
| 返回值类型 | size_t(无符号整数) | size_t(无符号整数) |
11.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的使用,也是面试的压轴考点。
12.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;
}
运行结果:
数组中只出现一次的两个数字是:5 和 6
核心思路:
- 利用异或特性:相同数字异或结果为 0,0 异或任意数字结果为该数字本身,将数组中所有数字异或后,结果为两个目标数字的异或值;
- 找到异或结果中第一个为 1 的二进制位:该位表示两个目标数字在这一位上不同,以此为依据将数组分为两组;
- 分组异或:将两组数字分别异或,最终得到的两个结果就是数组中只出现一次的两个数字;
- 指针操作:通过指针遍历数组,
p++的步长由指针类型决定(此处为int*,步长 4 字节),精准访问每个数组元素。
十三、核心总结
- 复杂指针类型的区分:核心是优先级规则:
()>[]>*,先结合的标识符决定其本质(先结合()是指针,先结合[]是数组); - **void*是通用指针**:可接收任意类型地址,是实现通用编程的核心,需强转为
char*后才能做地址运算; - 回调函数是解耦关键:标准库
qsort的通用性正是基于void*通用指针 + 用户自定义回调函数实现; - 二维数组传参:列号不可省略,形参本质是数组指针,这是与一维数组传参的核心区别;
- sizeof与strlen:
sizeof是编译期计算内存大小的操作符,strlen是运行期计算字符串长度的库函数,二者无关联,切勿混淆; - 指针运算规则:仅支持 ± 整数、指针 - 指针、关系运算 ,指针 + 指针、乘除运算均为非法操作,指针 ± 整数的步长由指针类型决定。
十四、指针上下篇整体衔接
指针基础篇围绕指针的本质(内存地址)、基础操作、一维数组与指针的关联展开,是理解指针的基础;进阶篇则聚焦复杂指针类型、通用编程思想、面试高频考点,是对指针知识的综合应用和深化。二者结合构成了 C 语言指针的完整知识体系,而贯穿始终的核心底层逻辑不变:指针的本质是内存地址,指针的类型决定了解引用的权限和运算的步长。
所有示例代码均可直接复制到编译器运行,建议动手敲一遍、修改参数尝试不同的效果,只有通过实际操作,才能真正把指针的知识点吃透,应对考试和面试也会更得心应手。