C 语言指针之手写内存深度剖析与手写库函数:带你从0开始手撸库 附录1.5 万 ...

打印 上一主题 下一主题

主题 1815|帖子 1815|积分 5445

一、指针入门:从野指针到空指针
 


1.1 野指针的第一次暴击:沃日 那里来的Segmentation Fault ??????

刚学指针时写过一段让我及其楠甭的代码,我x了xx的,最后才发现是为啥..........

  1. void wild_pointer_demo() {
  2.     int *p;
  3.     *p = 10; // 第一次运行直接段错误
  4. }
复制代码

调试时 GDB 提示 "access violation",其时完全不懂为什么。后来才知道指针必须初始化,于是改成:

  1. void fix_wild_pointer() {
  2.     int *p = NULL; // 初始化指针为NULL
  3.     if (p == NULL) {
  4.         p = (int*)malloc(sizeof(int));
  5.         if (p != NULL) {
  6.             *p = 10;
  7.             printf("*p = %d\n", *p);
  8.             free(p);
  9.             p = NULL; // 释放后立即置空
  10.         }
  11.     }
  12. }
复制代码

总结



  • 野指针:未初始化 / 释放后未置空 / 越界访问
  • 用valgrind检测内存错误,assert(p != NULL)在调试阶段捕获空指针

大厂面试

  1. void tricky_wild_pointer() {
  2.     int a = 10;
  3.     int *p = &a;
  4.     {
  5.         int b = 20;
  6.         p = &b;
  7.     } // b离开作用域,p成为野指针
  8.     *p = 30; // 未定义行为
  9. }
复制代码

剖析
局部变量b在代码块结束后销毁,指针p仍指向其内存地址,导致野指针。此类题目在多层函数调用中更难排查。
1.2 指针大小的玄学:64 位与 32 位的差异

在不同平台调试时发现:


  1. void pointer_size_test() {
  2.     printf("64位系统:int*=%zu字节,char*=%zu字节\n",
  3.            sizeof(int*), sizeof(char*)); // 输出8 8
  4.     // 32位系统会输出4 4
  5. }
复制代码


面试常考题



  • 为什么指针大小与系统位数相关?
    答:指针存储的是内存地址,64 位系统地址总线 64 位,故指针占 8 字节

进阶分析
指针大小与数据类型无关,所有指针类型在同一平台下大小雷同:

  1. struct ComplexStruct {
  2.     int a[100];
  3.     double b[50];
  4.     char c[20];
  5. };
  6. void advanced_pointer_size() {
  7.     printf("struct*=%zu字节,函数指针=%zu字节\n",
  8.            sizeof(struct ComplexStruct*),
  9.            sizeof(void(*)())); // 均输出8(64位)
  10. }
复制代码

1.3 空指针安全操作

写内存操作的代码都会遵照这个模板:

  1. void safe_memory_operation() {
  2.     int *p = NULL;
  3.     p = (int*)malloc(sizeof(int));
  4.     if (!p) {
  5.         fprintf(stderr, "内存分配失败\n");
  6.         exit(EXIT_FAILURE);
  7.     }
  8.     *p = 42;
  9.     // 使用p...
  10.     free(p);
  11.     p = NULL; // 关键一步,避免悬垂指针
  12. }
复制代码

个人技巧

用#define SAFE_FREE(p) { if(p) free(p); p=NULL; }
宏简化释放操作

大厂面试

实现一个线程安全的内存释放函数:

  1. #include <pthread.h>
  2. static pthread_mutex_t free_mutex = PTHREAD_MUTEX_INITIALIZER;
  3. void thread_safe_free(void **ptr) {
  4.     if (ptr && *ptr) {
  5.         pthread_mutex_lock(&free_mutex);
  6.         free(*ptr);
  7.         *ptr = NULL;
  8.         pthread_mutex_unlock(&free_mutex);
  9.     }
  10. }
复制代码

剖析
使用互斥锁掩护内存释放操作,防止多线程环境下重复释放或释放后使用的题目。void**参数允许直接将指针置空,增强安全性。



二、数组与指针:被括号支配的恐惧(1.5 万字)

2.1 数组指针 vs 指针数组:括号位置的玄学

刚开始分不清这两个声明:


  1. int (*arr_ptr)[5];  // 数组指针,指向含5个int的数组
  2. int *ptr_arr[5];   // 指针数组,含5个int*指针
复制代码

画内存图才搞明白:

  1. 数组指针arr_ptr:
  2. [0x1000] --> [1,2,3,4,5]  // 指针指向整个数组
  3. 指针数组ptr_arr:
  4. [0x2000, 0x2008, 0x2010, 0x2018, 0x2020]
  5. 每个元素指向不同int变量
复制代码

面试陷阱题
int arr[3][4]; int *p = arr;是否合法?
答:非法。arr类型是int (*)[4],不能直接转int*,会导致指针步长错误

大厂面试变种题

  1. int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
  2. int (*p)[4] = arr;
  3. printf("%d\n", **(p+1)); // 输出5
  4. printf("%d\n", *(*p+1)); // 输出2
复制代码

剖析


  • p+1偏移一个数组大小(16 字节),指向第二行
  • *p+1偏移一个 int 大小(4 字节),指向第一行第二个元素



2.2 二维数组传参与行指针

写矩阵处理函数时踩过的坑:

  1. // 错误写法:用二级指针接收二维数组
  2. void process_matrix(int **mat, int rows, int cols) {
  3.     mat[1][2] = 100; // 运行时错误
  4. }
  5. // 正确写法:用行指针
  6. void correct_process(int (*mat)[4], int rows) {
  7.     mat[1][2] = 100; // 正确
  8. }
复制代码

关键区别



  • 二维数组在内存中连续,行指针int (*)[4]步长为 16 字节(4*4)
  • 二级指针指向离散内存,无法直接用mat[j]访问

高阶技巧
动态分配二维数组并准确通报:

  1. int **dynamically_allocate(int rows, int cols) {
  2.     int **mat = (int**)malloc(rows * sizeof(int*));
  3.     for (int i=0; i<rows; i++) {
  4.         mat[i] = (int*)malloc(cols * sizeof(int));
  5.     }
  6.     return mat;
  7. }
  8. void process_dynamic(int **mat, int rows, int cols) {
  9.     // 正确,mat是真正的二级指针
  10. }
复制代码
2.3 数组名退化的真相

调试时发现:

  1. int arr[5] = {1,2,3,4,5};
  2. printf("sizeof(arr)=%zu\n", sizeof(arr)); // 20
  3. printf("sizeof(arr+0)=%zu\n", sizeof(arr+0)); // 8
复制代码

结论
数组名在表达式中会退化为指针,除了sizeof和&操作

大厂面试深挖题

  1. void array_decay_trap(int arr[]) {
  2.     printf("函数内: sizeof(arr)=%zu\n", sizeof(arr)); // 8
  3. }
  4. int main() {
  5.     int arr[5];
  6.     printf("函数外: sizeof(arr)=%zu\n", sizeof(arr)); // 20
  7.     array_decay_trap(arr);
  8.     return 0;
  9. }
复制代码

剖析
函数参数中的数组声明会退化为指针,因此sizeof(arr)在函数内返回指针大小。这是 C 语言设计的一个容易混淆的点。
三、宏与 typedef:预处理与编译的博弈(1 万字)

3.1 宏定义的副作用:表达式求值的陷阱

写过一个求最大值的宏:

  1. #define MAX(a,b) a>b?a:b
  2. // 调用MAX(i++,j)时会导致i被递增两次
复制代码

后来改成安全版本:


  1. #define MAX(a,b) ((a)>(b)?(a):(b))
复制代码

面试题
宏与内联函数的区别?
答:宏是文本更换,无类型检查;内联函数有类型安全,可调试

高阶宏技巧
实现带副作用安全检查的宏:

  1. #define SAFE_MAX(a,b) ({ \
  2.     __typeof__(a) _a = (a); \
  3.     __typeof__(b) _b = (b); \
  4.     _a > _b ? _a : _b; \
  5. })
复制代码

剖析
使用 GCC 扩展的语句表达式,为每个参数创建临时变量,制止多次求值的副作用。


3.2 typedef :复杂类型简化

定义函数指针时体会到 typedef 的魅力:
  1. // 普通声明
  2. int (*cmp_func)(const void*, const void*);
  3. // typedef后
  4. typedef int CmpFunc(const void*, const void*);
  5. CmpFunc *cmp;
复制代码

项目实践
用 typedef 封装结构体指针:

  1. typedef struct Node {
  2.     int data;
  3.     struct Node *next;
  4. } Node, *NodePtr;
复制代码

大厂面试题
使用 typedef 定义一个指向函数的指针,该函数担当两个 int 参数并返回一个函数指针(该返回的函数指针指向担当 int 并返回 int 的函数):

  1. typedef int (*InnerFunc)(int);
  2. typedef InnerFunc (*OuterFunc)(int, int);
  3. // 使用示例
  4. InnerFunc add_factory(int a, int b) {
  5.     return (InnerFunc)([](int x) { return x + a + b; });
  6. }
复制代码

剖析
通过多层 typedef 简化复杂声明,这在事件处理系统和回调机制中很常见。
四、字符串处理:从 strcpy 到安全编程(1.5 万字)

4.1 strncpy 的坑:终止符的缺失之痛

自己实现 strncpy 时忽略了终止符:

  1. char *my_strncpy(char *dest, const char *src, size_t n) {
  2.     for (size_t i=0; i<n && src[i]; i++) {
  3.         dest[i] = src[i];
  4.     }
  5.     // 忘记添加终止符!我操了踏马的
  6.     return dest;
  7. }
复制代码

准确版本应该添补剩余空间:

  1. char *safe_strncpy(char *dest, const char *src, size_t n) {
  2.     size_t i;
  3.     for (i=0; i<n && src[i]; i++) {
  4.         dest[i] = src[i];
  5.     }
  6.     for (; i<n; i++) {
  7.         dest[i] = '\0'; // 关键步骤
  8.     }
  9.     return dest;
  10. }
复制代码

大厂面试变形题
实现strncpy的安全版本,要求:


  • 不高出目的缓冲区大小
  • 始终以\0结尾
  • 返回现实写入的字符数(不包罗终止符)


  1. size_t safe_strncpy(char *dest, const char *src, size_t size) {
  2.     if (!dest || !src || size == 0) return 0;
  3.    
  4.     size_t i = 0;
  5.     while (i < size - 1 && src[i]) {
  6.         dest[i] = src[i];
  7.         i++;
  8.     }
  9.    
  10.     if (i < size) dest[i] = '\0'; // 确保终止符
  11.     return i; // 返回实际复制的字符数
  12. }
复制代码
4.2 strstr 的实现:暴力匹配与 KMP 算法

最初实现的暴力匹配:


  1. char *my_strstr(const char *haystack, const char *needle) {
  2.     while (*haystack) {
  3.         const char *h = haystack;
  4.         const char *n = needle;
  5.         while (*h && *n && *h == *n) {
  6.             h++; n++;
  7.         }
  8.         if (*n == '\0') return (char*)haystack;
  9.         haystack++;
  10.     }
  11.     return NULL;
  12. }
复制代码

后来学习了 KMP 算法,预处理 next 数组将时间复杂度从 O (m*n) 降到 O (m+n)

KMP 算法实现

  1. char *kmp_strstr(const char *haystack, const char *needle) {
  2.     if (!*needle) return (char*)haystack;
  3.    
  4.     size_t n = strlen(haystack);
  5.     size_t m = strlen(needle);
  6.    
  7.     // 计算next数组
  8.     int next[m];
  9.     next[0] = -1;
  10.     int i = 0, j = -1;
  11.    
  12.     while (i < m) {
  13.         while (j >= 0 && needle[i] != needle[j]) j = next[j];
  14.         i++; j++;
  15.         next[i] = j;
  16.     }
  17.    
  18.     // KMP匹配
  19.     i = j = 0;
  20.     while (i < n) {
  21.         while (j >= 0 && haystack[i] != needle[j]) j = next[j];
  22.         i++; j++;
  23.         if (j == m) return (char*)(haystack + i - j);
  24.     }
  25.    
  26.     return NULL;
  27. }
复制代码

五、大厂实战

5.1 指针与数组经典题10 题

题目 1

  1. int a[5] = {1,2,3,4,5};
  2. int *ptr = (int*)(&a + 1);
  3. printf("%d, %d\n", *(a + 1), *(ptr - 1));
复制代码

输出:2, 5
剖析
&a类型是int (*)[5],&a + 1跳过整个数组,ptr - 1指向最后一个元素。

题目 2
实现memcpy函数,要求考虑内存重叠情况。

  1. void *memmove(void *dest, const void *src, size_t n) {
  2.     char *d = dest;
  3.     const char *s = src;
  4.     if (d < s) {
  5.         // 正向复制
  6.         for (size_t i = 0; i < n; i++) {
  7.             d[i] = s[i];
  8.         }
  9.     } else {
  10.         // 反向复制,避免覆盖
  11.         for (size_t i = n; i > 0; i--) {
  12.             d[i-1] = s[i-1];
  13.         }
  14.     }
  15.     return dest;
  16. }
复制代码
5.2 字符串处理安全题(新增 8 题)

题目 1
实现snprintf函数。


  1. int my_snprintf(char *str, size_t size, const char *format, ...) {
  2.     va_list args;
  3.     va_start(args, format);
  4.     int len = vsnprintf(str, size, format, args);
  5.     va_end(args);
  6.     return len;
  7. }
复制代码

题目 2
实现strtok函数的线程安全版本。

c
运行
  1. char *strtok_r(char *str, const char *delim, char **saveptr) {
  2.     char *token;
  3.     if (str == NULL) {
  4.         str = *saveptr;
  5.     }
  6.    
  7.     // 跳过前导分隔符
  8.     str += strspn(str, delim);
  9.     if (*str == '\0') {
  10.         *saveptr = str;
  11.         return NULL;
  12.     }
  13.    
  14.     // 找到下一个分隔符
  15.     token = str;
  16.     str = strpbrk(token, delim);
  17.     if (str == NULL) {
  18.         // 没有更多分隔符
  19.         *saveptr = token + strlen(token);
  20.     } else {
  21.         // 替换分隔符为'\0'
  22.         *str = '\0';
  23.         *saveptr = str + 1;
  24.     }
  25.    
  26.     return token;
  27. }
复制代码
5.3 内存管理陷阱题7 题

题目 1
找出以下代码的内存泄漏:

  1. void leaky_function() {
  2.     char *p = (char*)malloc(100);
  3.     if (condition()) {
  4.         return; // 未释放p
  5.     }
  6.     free(p);
  7. }
复制代码

题目 2
实现一个带引用计数的内存分配器。

  1. struct RefCount {
  2.     void *ptr;
  3.     int count;
  4. };
  5. void* rc_malloc(size_t size) {
  6.     struct RefCount *rc = malloc(sizeof(struct RefCount) + size);
  7.     if (!rc) return NULL;
  8.     rc->ptr = rc + 1; // 数据区起始位置
  9.     rc->count = 1;
  10.     return rc->ptr;
  11. }
  12. void rc_free(void *ptr) {
  13.     if (!ptr) return;
  14.     struct RefCount *rc = (struct RefCount*)ptr - 1;
  15.     if (--rc->count == 0) {
  16.         free(rc);
  17.     }
  18. }
复制代码
六、技巧   --万字


6.1 入门阶段:指针可视化练习

用 Python 写了个指针可视化工具,画内存图理解指针操作:


  1. # 简化的指针可视化
  2. def visualize_ptr():
  3.     print("栈内存:")
  4.     print("[p=0x1000] --> 堆内存[0x2000:10]")
  5. # 复杂示例:二维数组
  6. def visualize_2d_array():
  7.     print("栈内存:")
  8.     print("[arr=0x1000] --> 堆内存:")
  9.     print("         0x1000: [1, 2, 3, 4]")
  10.     print("         0x1010: [5, 6, 7, 8]")
  11.     print("         0x1020: [9, 10, 11, 12]")
复制代码
6.2 进阶阶段:阅读开源代码

读 libc 源码时发现 strcpy 的优化实现:

  1. // glibc中的strcpy实现,使用内存对齐优化
  2. char *strcpy(char *dest, const char *src) {
  3.     char *tmp = dest;
  4.     while ((*dest++ = *src++) != '\0');
  5.     return tmp;
  6. }
  7. // 进一步优化:按字长复制
  8. char *fast_strcpy(char *dest, const char *src) {
  9.     size_t i;
  10.     // 处理未对齐部分
  11.     while (((uintptr_t)dest & (sizeof(long) - 1)) != 0) {
  12.         if (!(*dest++ = *src++)) return dest - 1;
  13.     }
  14.    
  15.     // 按字长复制
  16.     long *ldest = (long*)dest;
  17.     const long *lsrc = (const long*)src;
  18.     for (i = 0; i < strlen(src) / sizeof(long); i++) {
  19.         ldest[i] = lsrc[i];
  20.     }
  21.    
  22.     // 处理剩余部分
  23.     dest = (char*)(ldest + i);
  24.     src = (const char*)(lsrc + i);
  25.     while ((*dest++ = *src++) != '\0');
  26.    
  27.     return dest - 1;
  28. }
复制代码
七、避坑指南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企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

水军大提督

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表