一、指针入门:从野指针到空指针
1.1 野指针的第一次暴击:沃日 那里来的Segmentation Fault ??????
刚学指针时写过一段让我及其楠甭的代码,我x了xx的,最后才发现是为啥..........
- void wild_pointer_demo() {
- int *p;
- *p = 10; // 第一次运行直接段错误
- }
复制代码
调试时 GDB 提示 "access violation",其时完全不懂为什么。后来才知道指针必须初始化,于是改成:
- void fix_wild_pointer() {
- int *p = NULL; // 初始化指针为NULL
- if (p == NULL) {
- p = (int*)malloc(sizeof(int));
- if (p != NULL) {
- *p = 10;
- printf("*p = %d\n", *p);
- free(p);
- p = NULL; // 释放后立即置空
- }
- }
- }
复制代码
总结:
- 野指针:未初始化 / 释放后未置空 / 越界访问
- 用valgrind检测内存错误,assert(p != NULL)在调试阶段捕获空指针
大厂面试:
- void tricky_wild_pointer() {
- int a = 10;
- int *p = &a;
- {
- int b = 20;
- p = &b;
- } // b离开作用域,p成为野指针
- *p = 30; // 未定义行为
- }
复制代码
剖析:
局部变量b在代码块结束后销毁,指针p仍指向其内存地址,导致野指针。此类题目在多层函数调用中更难排查。
1.2 指针大小的玄学:64 位与 32 位的差异
在不同平台调试时发现:
- void pointer_size_test() {
- printf("64位系统:int*=%zu字节,char*=%zu字节\n",
- sizeof(int*), sizeof(char*)); // 输出8 8
- // 32位系统会输出4 4
- }
复制代码
面试常考题:
- 为什么指针大小与系统位数相关?
答:指针存储的是内存地址,64 位系统地址总线 64 位,故指针占 8 字节
进阶分析:
指针大小与数据类型无关,所有指针类型在同一平台下大小雷同:
- struct ComplexStruct {
- int a[100];
- double b[50];
- char c[20];
- };
- void advanced_pointer_size() {
- printf("struct*=%zu字节,函数指针=%zu字节\n",
- sizeof(struct ComplexStruct*),
- sizeof(void(*)())); // 均输出8(64位)
- }
复制代码
1.3 空指针安全操作
写内存操作的代码都会遵照这个模板:
- void safe_memory_operation() {
- int *p = NULL;
- p = (int*)malloc(sizeof(int));
- if (!p) {
- fprintf(stderr, "内存分配失败\n");
- exit(EXIT_FAILURE);
- }
- *p = 42;
- // 使用p...
- free(p);
- p = NULL; // 关键一步,避免悬垂指针
- }
复制代码
个人技巧:
用#define SAFE_FREE(p) { if(p) free(p); p=NULL; }
宏简化释放操作
大厂面试:
实现一个线程安全的内存释放函数:
- #include <pthread.h>
- static pthread_mutex_t free_mutex = PTHREAD_MUTEX_INITIALIZER;
- void thread_safe_free(void **ptr) {
- if (ptr && *ptr) {
- pthread_mutex_lock(&free_mutex);
- free(*ptr);
- *ptr = NULL;
- pthread_mutex_unlock(&free_mutex);
- }
- }
复制代码
剖析:
使用互斥锁掩护内存释放操作,防止多线程环境下重复释放或释放后使用的题目。void**参数允许直接将指针置空,增强安全性。
二、数组与指针:被括号支配的恐惧(1.5 万字)
2.1 数组指针 vs 指针数组:括号位置的玄学
刚开始分不清这两个声明:
- int (*arr_ptr)[5]; // 数组指针,指向含5个int的数组
- int *ptr_arr[5]; // 指针数组,含5个int*指针
复制代码
画内存图才搞明白:
- 数组指针arr_ptr:
- [0x1000] --> [1,2,3,4,5] // 指针指向整个数组
- 指针数组ptr_arr:
- [0x2000, 0x2008, 0x2010, 0x2018, 0x2020]
- 每个元素指向不同int变量
复制代码
面试陷阱题:
int arr[3][4]; int *p = arr;是否合法?
答:非法。arr类型是int (*)[4],不能直接转int*,会导致指针步长错误
大厂面试变种题:
- int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
- int (*p)[4] = arr;
- printf("%d\n", **(p+1)); // 输出5
- printf("%d\n", *(*p+1)); // 输出2
复制代码
剖析:
- p+1偏移一个数组大小(16 字节),指向第二行
- *p+1偏移一个 int 大小(4 字节),指向第一行第二个元素
2.2 二维数组传参与行指针
写矩阵处理函数时踩过的坑:
- // 错误写法:用二级指针接收二维数组
- void process_matrix(int **mat, int rows, int cols) {
- mat[1][2] = 100; // 运行时错误
- }
- // 正确写法:用行指针
- void correct_process(int (*mat)[4], int rows) {
- mat[1][2] = 100; // 正确
- }
复制代码
关键区别:
- 二维数组在内存中连续,行指针int (*)[4]步长为 16 字节(4*4)
- 二级指针指向离散内存,无法直接用mat[j]访问
高阶技巧:
动态分配二维数组并准确通报:
- int **dynamically_allocate(int rows, int cols) {
- int **mat = (int**)malloc(rows * sizeof(int*));
- for (int i=0; i<rows; i++) {
- mat[i] = (int*)malloc(cols * sizeof(int));
- }
- return mat;
- }
- void process_dynamic(int **mat, int rows, int cols) {
- // 正确,mat是真正的二级指针
- }
复制代码 2.3 数组名退化的真相
调试时发现:
- int arr[5] = {1,2,3,4,5};
- printf("sizeof(arr)=%zu\n", sizeof(arr)); // 20
- printf("sizeof(arr+0)=%zu\n", sizeof(arr+0)); // 8
复制代码
结论:
数组名在表达式中会退化为指针,除了sizeof和&操作
大厂面试深挖题:
- void array_decay_trap(int arr[]) {
- printf("函数内: sizeof(arr)=%zu\n", sizeof(arr)); // 8
- }
- int main() {
- int arr[5];
- printf("函数外: sizeof(arr)=%zu\n", sizeof(arr)); // 20
- array_decay_trap(arr);
- return 0;
- }
复制代码
剖析:
函数参数中的数组声明会退化为指针,因此sizeof(arr)在函数内返回指针大小。这是 C 语言设计的一个容易混淆的点。
三、宏与 typedef:预处理与编译的博弈(1 万字)
3.1 宏定义的副作用:表达式求值的陷阱
写过一个求最大值的宏:
- #define MAX(a,b) a>b?a:b
- // 调用MAX(i++,j)时会导致i被递增两次
复制代码
后来改成安全版本:
- #define MAX(a,b) ((a)>(b)?(a):(b))
复制代码
面试题:
宏与内联函数的区别?
答:宏是文本更换,无类型检查;内联函数有类型安全,可调试
高阶宏技巧:
实现带副作用安全检查的宏:
- #define SAFE_MAX(a,b) ({ \
- __typeof__(a) _a = (a); \
- __typeof__(b) _b = (b); \
- _a > _b ? _a : _b; \
- })
复制代码
剖析:
使用 GCC 扩展的语句表达式,为每个参数创建临时变量,制止多次求值的副作用。
3.2 typedef :复杂类型简化
定义函数指针时体会到 typedef 的魅力:
- // 普通声明
- int (*cmp_func)(const void*, const void*);
- // typedef后
- typedef int CmpFunc(const void*, const void*);
- CmpFunc *cmp;
复制代码
项目实践:
用 typedef 封装结构体指针:
- typedef struct Node {
- int data;
- struct Node *next;
- } Node, *NodePtr;
复制代码
大厂面试题:
使用 typedef 定义一个指向函数的指针,该函数担当两个 int 参数并返回一个函数指针(该返回的函数指针指向担当 int 并返回 int 的函数):
- typedef int (*InnerFunc)(int);
- typedef InnerFunc (*OuterFunc)(int, int);
- // 使用示例
- InnerFunc add_factory(int a, int b) {
- return (InnerFunc)([](int x) { return x + a + b; });
- }
复制代码
剖析:
通过多层 typedef 简化复杂声明,这在事件处理系统和回调机制中很常见。
四、字符串处理:从 strcpy 到安全编程(1.5 万字)
4.1 strncpy 的坑:终止符的缺失之痛
自己实现 strncpy 时忽略了终止符:
- char *my_strncpy(char *dest, const char *src, size_t n) {
- for (size_t i=0; i<n && src[i]; i++) {
- dest[i] = src[i];
- }
- // 忘记添加终止符!我操了踏马的
- return dest;
- }
复制代码
准确版本应该添补剩余空间:
- char *safe_strncpy(char *dest, const char *src, size_t n) {
- size_t i;
- for (i=0; i<n && src[i]; i++) {
- dest[i] = src[i];
- }
- for (; i<n; i++) {
- dest[i] = '\0'; // 关键步骤
- }
- return dest;
- }
复制代码
大厂面试变形题:
实现strncpy的安全版本,要求:
- 不高出目的缓冲区大小
- 始终以\0结尾
- 返回现实写入的字符数(不包罗终止符)
- size_t safe_strncpy(char *dest, const char *src, size_t size) {
- if (!dest || !src || size == 0) return 0;
-
- size_t i = 0;
- while (i < size - 1 && src[i]) {
- dest[i] = src[i];
- i++;
- }
-
- if (i < size) dest[i] = '\0'; // 确保终止符
- return i; // 返回实际复制的字符数
- }
复制代码 4.2 strstr 的实现:暴力匹配与 KMP 算法
最初实现的暴力匹配:
- char *my_strstr(const char *haystack, const char *needle) {
- while (*haystack) {
- const char *h = haystack;
- const char *n = needle;
- while (*h && *n && *h == *n) {
- h++; n++;
- }
- if (*n == '\0') return (char*)haystack;
- haystack++;
- }
- return NULL;
- }
复制代码
后来学习了 KMP 算法,预处理 next 数组将时间复杂度从 O (m*n) 降到 O (m+n)
KMP 算法实现:
- char *kmp_strstr(const char *haystack, const char *needle) {
- if (!*needle) return (char*)haystack;
-
- size_t n = strlen(haystack);
- size_t m = strlen(needle);
-
- // 计算next数组
- int next[m];
- next[0] = -1;
- int i = 0, j = -1;
-
- while (i < m) {
- while (j >= 0 && needle[i] != needle[j]) j = next[j];
- i++; j++;
- next[i] = j;
- }
-
- // KMP匹配
- i = j = 0;
- while (i < n) {
- while (j >= 0 && haystack[i] != needle[j]) j = next[j];
- i++; j++;
- if (j == m) return (char*)(haystack + i - j);
- }
-
- return NULL;
- }
复制代码
五、大厂实战
5.1 指针与数组经典题10 题
题目 1:
- int a[5] = {1,2,3,4,5};
- int *ptr = (int*)(&a + 1);
- printf("%d, %d\n", *(a + 1), *(ptr - 1));
复制代码
输出:2, 5
剖析:
&a类型是int (*)[5],&a + 1跳过整个数组,ptr - 1指向最后一个元素。
题目 2:
实现memcpy函数,要求考虑内存重叠情况。
- void *memmove(void *dest, const void *src, size_t n) {
- char *d = dest;
- const char *s = src;
- if (d < s) {
- // 正向复制
- for (size_t i = 0; i < n; i++) {
- d[i] = s[i];
- }
- } else {
- // 反向复制,避免覆盖
- for (size_t i = n; i > 0; i--) {
- d[i-1] = s[i-1];
- }
- }
- return dest;
- }
复制代码 5.2 字符串处理安全题(新增 8 题)
题目 1:
实现snprintf函数。
- int my_snprintf(char *str, size_t size, const char *format, ...) {
- va_list args;
- va_start(args, format);
- int len = vsnprintf(str, size, format, args);
- va_end(args);
- return len;
- }
复制代码
题目 2:
实现strtok函数的线程安全版本。
c
运行
- char *strtok_r(char *str, const char *delim, char **saveptr) {
- char *token;
- if (str == NULL) {
- str = *saveptr;
- }
-
- // 跳过前导分隔符
- str += strspn(str, delim);
- if (*str == '\0') {
- *saveptr = str;
- return NULL;
- }
-
- // 找到下一个分隔符
- token = str;
- str = strpbrk(token, delim);
- if (str == NULL) {
- // 没有更多分隔符
- *saveptr = token + strlen(token);
- } else {
- // 替换分隔符为'\0'
- *str = '\0';
- *saveptr = str + 1;
- }
-
- return token;
- }
复制代码 5.3 内存管理陷阱题7 题
题目 1:
找出以下代码的内存泄漏:
- void leaky_function() {
- char *p = (char*)malloc(100);
- if (condition()) {
- return; // 未释放p
- }
- free(p);
- }
复制代码
题目 2:
实现一个带引用计数的内存分配器。
- struct RefCount {
- void *ptr;
- int count;
- };
- void* rc_malloc(size_t size) {
- struct RefCount *rc = malloc(sizeof(struct RefCount) + size);
- if (!rc) return NULL;
- rc->ptr = rc + 1; // 数据区起始位置
- rc->count = 1;
- return rc->ptr;
- }
- void rc_free(void *ptr) {
- if (!ptr) return;
- struct RefCount *rc = (struct RefCount*)ptr - 1;
- if (--rc->count == 0) {
- free(rc);
- }
- }
复制代码 六、技巧 --万字
6.1 入门阶段:指针可视化练习
用 Python 写了个指针可视化工具,画内存图理解指针操作:
- # 简化的指针可视化
- def visualize_ptr():
- print("栈内存:")
- print("[p=0x1000] --> 堆内存[0x2000:10]")
- # 复杂示例:二维数组
- def visualize_2d_array():
- print("栈内存:")
- print("[arr=0x1000] --> 堆内存:")
- print(" 0x1000: [1, 2, 3, 4]")
- print(" 0x1010: [5, 6, 7, 8]")
- print(" 0x1020: [9, 10, 11, 12]")
复制代码 6.2 进阶阶段:阅读开源代码
读 libc 源码时发现 strcpy 的优化实现:
- // glibc中的strcpy实现,使用内存对齐优化
- char *strcpy(char *dest, const char *src) {
- char *tmp = dest;
- while ((*dest++ = *src++) != '\0');
- return tmp;
- }
- // 进一步优化:按字长复制
- char *fast_strcpy(char *dest, const char *src) {
- size_t i;
- // 处理未对齐部分
- while (((uintptr_t)dest & (sizeof(long) - 1)) != 0) {
- if (!(*dest++ = *src++)) return dest - 1;
- }
-
- // 按字长复制
- long *ldest = (long*)dest;
- const long *lsrc = (const long*)src;
- for (i = 0; i < strlen(src) / sizeof(long); i++) {
- ldest[i] = lsrc[i];
- }
-
- // 处理剩余部分
- dest = (char*)(ldest + i);
- src = (const char*)(lsrc + i);
- while ((*dest++ = *src++) != '\0');
-
- return dest - 1;
- }
复制代码 七、避坑指南15个
- 所有指针必须初始化:int *p = NULL; 沃日 被这个坑过很多次!!!!!!!
- malloc 后检查返回值:if (!p) exit(1);
- free 后 NULL:SAFE_FREE(p);
- 宏定义加括号:#define ADD(a,b) ((a)+(b))
- 数组传参用行指针:void func(int (*arr)[N]);
- 字符串操作检查长度:strncpy(dest, src, size);
- 函数指针用 typedef:typedef void (*Handler)();
- 内存操作用 assert:assert(p != NULL);
- 跨平台代码用 sizeof:int len = sizeof(arr)/sizeof(arr[0]);
- 复杂声明必用分解法:int (*(*func(int))[10])(); 分解为函数指针返回数组指针
- 制止函数返回局部变量地址:int* bad_func() { int a; return &a; }
- 慎用 void * 指针:void* p; *p = 10; // 错误,需先转换类型
- 结构体成员对齐用 #pragma pack:#pragma pack(1) struct { char c; int i; };
- 多线程共享指针+同步:pthread_mutex_lock(&lock); *p = 10; pthread_mutex_unlock(&lock);
- 越界:int arr[5]; *(arr+10) = 0; // 段错误
本文代码已整理到 GitHub 个人博客
欢迎留言收藏点赞关注+讨论,一起攻克 指针难关
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |