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

news2025/5/31 21:03:34
一、指针入门:从野指针到空指针
 
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[i][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的安全版本,要求:

  1. 不超过目标缓冲区大小
  2. 始终以\0结尾
  3. 返回实际写入的字符数(不包括终止符)

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个

  1. 所有指针必须初始化int *p = NULL;  沃日 被这个坑过很多次!!!!!!!
  2. malloc 后检查返回值if (!p) exit(1);
  3. free 后 NULLSAFE_FREE(p);
  4. 宏定义加括号#define ADD(a,b) ((a)+(b))
  5. 数组传参用行指针void func(int (*arr)[N]);
  6. 字符串操作检查长度strncpy(dest, src, size);
  7. 函数指针用 typedeftypedef void (*Handler)();
  8. 内存操作用 assertassert(p != NULL);
  9. 跨平台代码用 sizeofint len = sizeof(arr)/sizeof(arr[0]);
  10. 复杂声明必用分解法int (*(*func(int))[10])(); 分解为函数指针返回数组指针
  11. 避免函数返回局部变量地址int* bad_func() { int a; return &a; }
  12. 慎用 void * 指针void* p; *p = 10; // 错误,需先转换类型
  13. 结构体成员对齐用 #pragma pack#pragma pack(1) struct { char c; int i; };
  14. 多线程共享指针+同步pthread_mutex_lock(&lock); *p = 10; pthread_mutex_unlock(&lock);
  15. 越界int arr[5]; *(arr+10) = 0; // 段错误

本文代码已整理到 GitHub 个人博客
欢迎留言收藏点赞关注+讨论,一起攻克 指针难关

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2386676.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C#高级:Winform桌面开发中CheckedListBox的详解

一、基础设置 单击触发选择效果&#xff1a;需要选择下面这个为True 二、代码实现 1.设置数据源 /// <summary> /// 为CheckBoxList设置数据源 /// </summary> /// <param name"checkedListBox1"></param> /// <param name"data&…

AI学习笔记二十八:使用ESP32 CAM和YOLOV5实现目标检测

若该文为原创文章&#xff0c;转载请注明原文出处。 最近在研究使用APP如何显示ESP32 CAM的摄像头数据&#xff0c;看到有人实现把ESP32 CAM的数据流上传&#xff0c;通过YOLOV5来检测&#xff0c;实现拉流推理&#xff0c;这里复现一下。 一、环境 arduino配置esp32-cam开发环…

免费分享50本web全栈学习电子书

最近搞到一套非常不错的 Web 全栈电子书合集&#xff0c;整整 50 本&#xff0c;都是epub电子书格式&#xff0c;相当赞&#xff01;作为一个被期末大作业和项目 ddl 追着跑的大学生&#xff0c;这套书真的救我狗命&#xff01; 刚接触 Web 开发的时候&#xff0c;我天天对着空…

【prometheus+Grafana篇】基于Prometheus+Grafana实现MySQL数据库的监控与可视化

&#x1f4ab;《博主主页》&#xff1a; &#x1f50e; CSDN主页 &#x1f50e; IF Club社区主页 &#x1f525;《擅长领域》&#xff1a;擅长阿里云AnalyticDB for MySQL(分布式数据仓库)、Oracle、MySQL、Linux、prometheus监控&#xff1b;并对SQLserver、NoSQL(MongoDB)有了…

全链路解析:影刀RPA+Coze API自动化工作流实战指南

在数字化转型加速的今天&#xff0c;如何通过RPA与API的深度融合实现业务自动化提效&#xff0c;已成为企业降本增效的核心命题。本文以「影刀RPA」与「Coze API」的深度协作为例&#xff0c;系统性拆解从授权配置、数据交互到批量执行的完整技术链路&#xff0c;助你快速掌握跨…

高阶数据结构——哈希表的实现

目录 1.概念引入 2.哈希的概念&#xff1a; 2.1 什么叫映射&#xff1f; 2.2 直接定址法 2.3 哈希冲突&#xff08;哈希碰撞&#xff09; 2.4 负载因子 2.5 哈希函数 2.5.1 除法散列法&#xff08;除留余数法&#xff09; 2.5.2 乘法散列法&#xff08;了解&#xff09…

2025 年网络安全趋势报告

一、引言 自欧洲信息安全协会&#xff08;Infosecurity Europe&#xff09;首次举办活动的 30 年来&#xff0c;网络安全格局发生了翻天覆地的变化。如今&#xff0c;网络安全领导者必须应对众多威胁&#xff0c;维持法规合规性&#xff0c;并与董事会成员合作推进组织的网络安…

uniapp 条件筛选

v3 版本 <template><view class"store flex "><view class"store_view"><view class"store_view_search flex jsb ac"><!-- <view class"store_view_search_select">全部</view> --><v…

pytorch问题汇总

conda环境下 通过torch官网首页 pip安装 成功运行 后面通过conda安装了别的包 似乎因为什么版本问题 就不能用了 packages\torch_init_.py", line 245, in _load_dll_libraries raise err OSError: [WinError 127] 找不到指定的程序。 Error loading ackages\torch\lib\c…

开发过的一个Coding项目

一、文档资料、人员培训&#xff1a; 1、文档资料管理&#xff1a;这个可以使用OnLineHelpDesk。 2、人员培训&#xff1a;可以参考Is an Online Medical Billing and Coding Program Right for You - MedicalBillingandCoding.org。 3、人员招聘、考核&#xff1a;可以在Onli…

数据仓库维度建模详细过程

数据仓库的维度建模&#xff08;Dimensional Modeling&#xff09;是一种以业务用户理解为核心的设计方法&#xff0c;通过维度表和事实表组织数据&#xff0c;支持高效查询和分析。其核心目标是简化复杂业务逻辑&#xff0c;提升查询性能。以下是维度建模的详细过程&#xff1…

python打卡day37

早停策略和模型权重保存 知识点回顾&#xff1a; 过拟合的判断&#xff1a;测试集和训练集同步打印指标模型的保存和加载 仅保存权重保存权重和模型保存全部信息checkpoint&#xff0c;还包含训练状态 早停策略 是否过拟合&#xff0c;可以通过同步打印训练集和测试集的loss曲线…

各个网络协议的依赖关系

网络协议的依赖关系 学习网络协议之间的依赖关系具有多方面重要作用&#xff0c;具体如下&#xff1a; 帮助理解网络工作原理 - 整体流程明晰&#xff1a;网络协议分层且相互依赖&#xff0c;如TCP/IP协议族&#xff0c;应用层协议依赖传输层的TCP或UDP协议来传输数据&#…

OSC协议简介、工作原理、特点、数据的接收和发送

OSC协议简介 Open Sound Control&#xff08;OSC&#xff09; 是一种开放的、独立于传输的基于消息的协议&#xff0c;主要用于计算机、声音合成器和其他多媒体设备之间的通信。它提供了一种灵活且高效的方式来发送和接收参数化消息&#xff0c;特别适用于实时控制应用&#x…

区块链可投会议CCF C--APSEC 2025 截止7.13 附录用率

Conference&#xff1a;32nd Asia-Pacific Software Engineering Conference (APSEC 2025) CCF level&#xff1a;CCF C Categories&#xff1a;软件工程/系统软件/程序设计语言 Year&#xff1a;2025 Conference time&#xff1a;December 2-5, 2025 in Macao SAR, China …

【数字图像处理】_笔记

第一章 概述 1.1 什么是数字图像&#xff1f; 图像分为两大类&#xff1a;模拟图像与数字图像 模拟图像&#xff1a;通过某种物理&#xff08;光、电&#xff09;的强弱变化来记录图像上各个点的亮度信息 连续&#xff1a;从空间上和数值上是不间断的 举例&…

从0开始学习R语言--Day10--时间序列分析数据

在数据分析中&#xff0c;我们经常会看到带有时间属性的数据&#xff0c;比如股价波动&#xff0c;各种商品销售数据&#xff0c;网站的网络用户活跃度等。一般来说&#xff0c;根据需求我们会分为两种&#xff0c;分析历史数据的特点和预测未来时间段的数据。 移动平均 移动平…

基于开源链动2+1模式AI智能名片S2B2C商城小程序的产品驱动型增长策略研究

摘要&#xff1a;在数字化经济时代&#xff0c;产品驱动型增长&#xff08;Product-Led Growth, PLG&#xff09;已成为企业突破流量瓶颈、实现用户裂变的核心战略。本文以“开源链动21模式AI智能名片S2B2C商城小程序”&#xff08;以下简称“链动AI-S2B2C系统”&#xff09;为…

使用 OpenCV 实现“随机镜面墙”——多镜片密铺的哈哈镜效果

1. 引言 “哈哈镜”是一种典型的图像变形效果&#xff0c;通过局部镜面反射产生扭曲的视觉趣味。在计算机视觉和图像处理领域&#xff0c;这类效果不仅有趣&#xff0c;还能用于艺术创作、交互装置、视觉特效等场景。 传统的“哈哈镜”往往是针对整张图像做某种镜像或扭曲变换…

鸿蒙仓颉开发语言实战教程:页面跳转和传参

前两天分别实现了商城应用的首页和商品详情页面&#xff0c;今天要分享新的内容&#xff0c;就是这两个页面之间的相互跳转和传递参数。 首先我们需要两个页面。如果你的项目中还没有第二个页面&#xff0c;可以右键cangjie文件夹新建仓颉文件&#xff1a; 新建的文件里面没什…