
一、指针1.1 指针的基本概念概念指针是一个变量它存储另一个变量的内存地址。变量存储在内存中每个变量占据一定的内存空间并有唯一的地址。指针就是用来存储这些地址的变量。int x 10; int *p x; //声明指针p指向变量x的地址解释x获取变量x的内存地址。int*p声明了一个指向int类型的指针变量p。p存储了x的地址。用整型变量保存地址时只能打印地址值当作整数不能通过它来访问对应的变量值。1.2 指针的声明与解引用声明指针int *p; //声明一个指向int类型的指针p用于定义指针表示这是一个存储地址的变量。赋值与获取地址int a 5; p a;//将a的地址赋给指针p解引用通过指针访问变量值。int value *p; //通过解引用访问指针p指向的变量a的值示例int a 10; int *p a; printf(a的值:%d\n, a);//打印a的值 printf(p指向的值:%d\n, *p);//打印p指向的值1.3 p与*p的区别表达式含义p 的类型p 的值int p x;将 x 的地址赋给整型变量 pintx 的地址当作整数int *p x;定义指针 p并初始化为指向 xint*x 的地址p x;将 x 的地址赋给已定义的指针 pint*x 的地址*p 2;将 2 赋给 p 所指向的变量int*不变仍为某地址p 存地址指向谁*p 取地址里的内容指向的那个变量的值1.3指针的算术运算1.3.1 指针加法当对指针进行加法运算时指针会移动到它所指向类型的下一个元素位置。加法运算的步长取决于指针所指向数据类型的大小。int arr[5] {1, 2, 3, 4, 5}; int *p arr;// p指向arr[0] //指针加法 p;// p指向arr[1]即arr[1]这里p实际上将p移动了sizeof(int)个字节因为p指向的是int类型的数组元素。1.3.2 指针减法类似地指针减法会将指针向数组的前一个元素移动。p--;// p现在又指向arr[0]1.3.3 指针相减如果两个指针指向同一个数组中的元素则它们的差值表示两个元素之间的距离以元素为单位而不是字节为单位。int arr[5] {1, 2, 3, 4, 5}; int *p1 arr[3];//指向arr[3] int *p2 arr[0];//指向arr[0] int diff p1 - p2; // diff 3表示两个元素之间有3个int的距离指针相减的结果是两个指针之间的元素个数而不是字节差1.3.4 指针与整数相加/相减指针加上一个整数时会将指针移动若干个元素的位置。与加法类似指针减去一个整数时会将指针移动到前面的元素。p p 2;// p移动两个元素 p p - 1;// p向前移动一个元素1.4 指针的比较运算指针可以进行比较运算用来判断两个指针是否指向相同位置或不同位置。指针的比较运算包括判断两个指针是否指向相同位置。!判断两个指针是否指向不同位置。、、、比较两个指针在内存中的相对位置。这些运算仅在指针指向同一个数组时有意义。int arr[5] {1, 2, 3, 4, 5}; int *p1 arr[0]; int*p2 arr[2]; if(p1 p2) { printf(p1在p2之前\n); }在这个例子中p1比p2的内存地址小因为它指向数组中的前一个元素。二、二级指针2.1 基本概念指针的指针简单来说就是一个存储另一个指针地址的变量。假设我们有一个普通的指针它指向某个变量的内存地址。一个指针的指针则指向这个指针的地址从而形成一种多级引用关系。声明方式int a 10;//普通变量 int *p a;//指针p指向变量a的地址 int **pp p;//指针的指针pp指向指针p的地址2.2 应用场景2.2.1 动态内存分配指针的指针常用于动态分配二维数组。在二维数组的分配中我们可以使用指针数组来存储每一行的地址指针的指针可以作为整个二维数组的入口。//动态分配一个指针的指针存储每一行的地址 int **array (int **)malloc(rows * sizeof(int *)); if (array NULL) { printf(内存分配失败\n); return 1; } //为每一行分配内存存储列元素 for (int i 0; i rows; i) { array[i] (int *)malloc(cols * sizeof(int)); if (array[i] NULL) { printf(内存分配失败\n); return 1; } }2.2.2 函数参数传递如果想在函数中修改一个指针的值需要使用指针的指针。例如在函数中修改传入的指针本身的地址而不是指针指向的数据就需要使用指针的指针。void allocateMemory(int **ptr) { *ptr (int *)malloc(sizeof(int));//动态分配内存 if (*ptr ! NULL) { **ptr 100;//给分配的内存赋值 } } int main() { int *p NULL;//调用函数通过指针的指针分配内存 allocateMemory(p); if (p ! NULL) { printf(p指向的值:%d\n, *p);//输出:100 free(p);//释放内存 } return 0; }2.3二级指针的*运算表达式等于什么类型含义p指针的地址int**二级指针本身存的是一级指针的地址*p一级指针int*对 p 解引用一次得到一级指针存的是变量的地址**p变量的值int对 p 解引用两次得到实际存储的数据三、常量指针和指针常量3.1 常量指针数据只读int a 10, b 20; const int *p a; p b; // 可以改指向 *p 15; // 不能改数据3.2 指针常量指向固定int a 10, b 20; int *const p a; *p 15; // 可以改数据 p b; // 不能改指向3.3 两种指针的核心区别指针类型定义格式能否改指向能否改指向的内容常量指针const int *p✅ 可以❌ 不可以指针常量int *const p❌ 不可以✅ 可以3.4 双重限制都不能改int a 10; const int *const p a; // 指向和数据都不能改3.5 函数参数为什么要用 constvoid printStr(const char *str); // 常量指针作参数四个好处安全防止函数内部误改数据兼容可以接收字符串字面量只读数据自文档一看就知道函数不会修改参数优化编译器可做更多优化四、指针数组与数组指针4.1 指针数组定义int *arr[5];→ 数组有5个元素每个元素都是int*指针int a 10, b 20; int *arr[2]; // 指针数组 arr[0] a; // 第一个指针指向a arr[1] b; // 第二个指针指向b printf(%d\n, *arr[0]); // 输出10 printf(%d\n, *arr[1]); // 输出204.2 数组指针定义int (*p)[5];→p是一个指针指向包含5个int元素的数组int array[5] {10, 20, 30, 40, 50}; int (*p)[5] array; // 数组指针指向整个数组 // 访问元素 printf(%d\n, (*p)[0]); // 输出10 printf(%d\n, (*p)[1]); // 输出20特点指针移动时会跳过整个数组5个int4.3 指针区别名称定义格式本质含义普通指针int *p;指针p 是一个指针指向一个 int 变量指针数组int *p[5];数组数组有 5 个元素每个元素都是 int 类型的指针数组指针int (*p)[5];指针p 是一个指针指向一个包含 5 个 int 的数组4.4 数组名退化4.4.1 一维数组场景行为类型示例多数表达式退化成首元素指针int*arr→ 指向arr[0]sizeof运算保持数组类型int[5]数组本身大小取地址保持数组类型int(*)[5]arr→ 指向整个数组的指针案例int arr[5] {10, 20, 30, 40, 50}; printf(%p\n, arr); // 首元素地址 printf(%p\n, arr); // 也是首地址但类型不同 // arr1 和 arr1 的区别 printf(%p\n, arr 1); // 跳过一个int4字节 printf(%p\n, arr 1); // 跳过整个数组20字节4.4.2 二维数组表达式退化后的类型含义matrixint (*)[4]指向整个一维数组行的指针matrix[0]int*指向行内第一个int的指针matrix[0][0]int具体的整数值4.5 二维数组与数组指针int matrix[3][4] { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };数组名matrix的本质matrix不是二级指针而是一个指向一维数组的指针数组指针类型是int (*)[4]。二维数组传参的方式// 正确使用数组指针 void printMatrix(int (*p)[4], int rows) { for (int i 0; i rows; i) { for (int j 0; j 4; j) { printf(%d , p[i][j]); } } } int main() { int matrix[3][4] {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}}; printMatrix(matrix, 3); // matrix退化成 int (*)[4] return 0; }五、指针函数与函数指针5.1 指针函数指针函数是指返回类型为指针的函数换句话说函数返回一个指针。指针函数和普通函数的区别主要在于其返回值是指针。语法int* 函数名(int* a, int* b) { }int* getMax(int* a, int* b) { if(*a *b){ return a;} else{ return b;} } int main() { int x 10, y 20; int *max getMax(x, y);//调用返回指针的函数 printf(Max: %d\n, *max);//解引用指针输出较大的值 return 0; }5.2 函数指针函数指针是指向函数的指针。函数指针可以存储函数的地址从而通过指针来调用该函数。函数指针在实现回调函数、动态选择函数或函数数组时非常有用。int add(int a, intb) { return a b; } int multiply(int a, int b) { return a * b; } int main() {//声明一个函数指针 int (*func_ptr)(int, int);//让函数指针指向add函数 func_ptr add; printf(Add: %d\n, func_ptr(3, 4));//调用add函数输出7 //让函数指针指向multiply函数 func_ptr multiply; printf(Multiply: %d\n, func_ptr(3, 4));//调用multiply函数输出12 return 0; }5.3 指针区别对比项指针函数函数指针定义返回值为指针的函数指向函数的指针变量语法类型 *函数名(参数列表)类型 (*指针名)(参数列表)示例int *max(int *a, int *b)int (*p)(int, int)作用返回内存地址如数组、字符串、动态分配的内存通过指针调用函数实现回调、多态调用方式int *result max(x, y);p 函数名; result p(a, b);5.4 函数指针作为函数参数函数指针可以作为函数的参数这通常用于实现回调函数或策略模式在排序、搜索等操作中也非常常见。//定义两个函数 int add(int a, int b) { return a b; } int subtract(int a, int b) { return a - b; } //定义一个接收函数指针的函数 int compute(int (*operation)(int, int), int x, int y) { return operation(x, y); //通过函数指针调用函数 } int main() { //传递add和subtract函数作为参数 printf(Add: %d\n, compute(add, 10, 5));//输出15 printf(Subtract: %d\n, compute(subtract, 10, 5));//输出5 return 0; }5.5 函数指针数组函数指针可以存储在数组中用于处理多个函数尤其在实现命令表或分支逻辑时非常方便。//定义两个函数 int add(int a, int b) { return a b; } int multiply(int a, int b) { return a * b; } int main() { //定义函数指针数组 int (*operations[2])(int, int); operations[0] add; operations[1] multiply; //调用函数指针数组中的函数 printf(Add: %d\n, operations[0](5, 3));//输出8 printf(Multiply: %d\n, operations[1](5, 3));//输出15 return 0; }六、野指针6.1 概念野指针Dangling Pointer是指向无效内存地址的指针。它的产生通常是由于指针指向的内存已经被释放、未正确初始化或超过其作用域。野指针的使用会导致未定义行为包括程序崩溃、内存泄漏甚至安全漏洞如缓冲区溢出、代码注入等因此在编程中应尽量避免它们。6.2 形成原因6.2.1 指针未初始化当指针声明时未赋予初始值它会包含一个随机的内存地址。如果直接使用这种未初始化的指针就可能访问不合法的内存区域导致程序崩溃或其他未定义行为。int *ptr;//未初始化的指针指向随机地址 *ptr 10;//错误可能导致崩溃6.2.2 指向的内存已被释放当指针指向的内存被释放后该指针依然保留了原来的地址但该地址已经不再有效成为野指针。如果尝试再次访问此地址会导致不可预测的行为。int *ptr (int*)malloc(sizeof(int)); *ptr 10; free(ptr);//释放了指针指向的内存 *ptr 20;//错误ptr现在是野指针6.2.3 超出变量的作用域如果指针指向某个局部变量的地址当该局部变量的作用域结束后指针仍然持有这个变量的地址但此时该变量的内存已经无效。int *getPointer() { int x 10; return x;//错误返回局部变量地址 } int main() { int *ptr getPointer();//此时ptr指向一个已经销毁的局部变量 printf(%d\n, *ptr);//未定义行为 return 0; }6.2.4多次释放同一块内存如果对同一个指针多次调用free()在第二次free()时指针可能已经指向无效内存从而导致野指针。int *ptr (int *)malloc(sizeof(int)); free(ptr); free(ptr);//错误重复释放同一块内存6.3 规避方法6.3.1指针初始化为NULL在声明指针时将其初始化为NULL。NULL是一个特殊的地址它表示指针没有指向任何合法的内存。如果尝试解引用NULL指针程序通常会崩溃容易发现错误。此外在释放指针后也可以将其赋值为NULL。int *ptr NULL;//初始化为NULL if(ptr! NULL) { *ptr 10;//只有指针不为NULL时才操作 }6.3.2 释放指针后置为NULL释放动态内存后立即将指针置为NULL避免误使用已经释放的指针。int *ptr (int*)malloc(sizeof(int)); free(ptr);//释放内存 ptr NULL;//避免成为野指针6.3.3 避免重复释放内存确保每块内存只调用一次free()。如果有复杂的数据结构需要释放可以跟踪内存的状态确保释放的正确性。int*ptr (int*)malloc(sizeof(int)); free(ptr); ptr NULL;//防止重复释放七、动态内存分配7.1 与静态内存变量区别对比项静态内存变量动态内存变量定义编译时分配内存生命周期固定运行时手动分配内存生命周期可控分配方式编译器自动分配程序员用malloc/calloc申请存储位置栈区局部或数据段全局/静态堆区生命周期作用域内自动管理从malloc到free释放方式自动释放必须手动free大小编译时确定运行时确定7.2 内存相关概念7.2.1系统内存图7.2.2 栈与堆的区别对比项栈Stack堆Heap管理方式系统自动管理编译器自动分配和释放程序员手动管理手动申请和释放空间大小较小通常几MB由系统决定溢出会导致栈溢出较大可达几GB受物理内存限制溢出会导致分配失败生长方向向下生长从高地址向低地址向上生长从低地址向高地址分配方式静态分配编译时和动态分配函数调用时只有动态分配运行时通过malloc/calloc/realloc存放内容局部变量、函数参数、返回地址、函数调用上下文程序员手动分配的内存如动态数组、对象、结构体7.3 应用场景1.动态分配可变大小的数组当数组的大小无法在编译时确定时可以使用malloc或calloc来在运行时动态分配内存。例如用户输入数组大小或程序运行时根据数据决定数组大小的情况。2.创建链表链表是一种动态数据结构节点的数量在运行时可能变化。每当需要添加新节点时可以通过malloc动态分配新节点的内存。3.动态分配大型数据结构对于某些大型结构体或复合数据结构直接在栈上分配内存可能会导致栈溢出。因此可以使用malloc或calloc在堆上为这些结构体分配内存。4.实现自定义大小的缓存或缓冲区在某些应用中程序需要临时存储大量数据如文件数据、网络数据、日志数据等。可以使用malloc或calloc动态分配缓存根据需要调整缓存大小。