尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C语言指针学习内容总结

C语言指针学习内容总结 前言指针是 C 语言的灵魂。指针是 C 语言最核心、最具魅力的特性也是贯穿 C 语言编程的灵魂所在它赋予了程序员直接操作内存、灵活管理数据的能力让程序拥有了更高的执行效率和更强的功能拓展性。但它也是很多初学者的 “拦路虎”。在最初接触指针的时候我也被它搞得晕头转向经过一段时间系统性的学习我总算能摸清一点门道对指针的学习也有了初步的成果。下面我就将这些天的学习内容总结如下作为对自己学习成果的梳理。一、内存是怎么回事要理解指针首先得搞懂程序运行时的内存布局。1. 内存的基本单位与寻址内存的最小存储单位是比特bit而基本操作单位是字节Byte。在 x86Win32系统中地址总线是 32 根所以能表示的地址范围是0 ~ 2^32 - 1也就是 4G 的寻址空间。我们定义一个变量char val a;val本身存储的是字符 a 的 ASCII 值 97而val获取的是这个变量在内存中的地址指针的作用就是保存这个地址。2. 程序内存分区程序运行时内存会被划分为几个关键区域不同区域的特性直接影响指针的使用。下面给出我对内存分区的小结表格分区存储内容特点代码区.text编译后的机器指令只读由 CPU 直接执行数据区.data全局变量、静态变量、字符串常量程序运行期间一直存在堆区.heapmalloc/calloc/realloc函数申请的动态内存手动申请、手动释放管理不当会造成内存泄漏栈区.stack函数局部变量、函数参数、函数返回地址由系统自动管理函数执行结束后自动释放Windows 默认 1M 大小Linux 默认 8MB 较常见这里需要注意以下几个细节栈区的地址是从高地址向低地址生长的。所以如果同一个函数里有如下定义int a0; int b0;a会比b更高。不要返回栈区局部变量的地址函数结束后内存失效会变成野指针。字符串常量通常放在只读数据段 (.rodata)而不是常规的.data段对其只可读不可写而.data段存储已初始化且可读写的全局/静态变量。二、大小端存储模式在学习指针访问内存时需要了解多字节数据在内存中的存放顺序这个存放顺序就是大小端存储模式。1. 什么是大小端大端模式数据的高位字节保存在内存的低地址低位字节保存在高地址。这符合人类读写数字的习惯用口诀记忆就是是高低、低高小端模式数据的高位字节保存在高地址低位字节保存在低地址。这是当前主流架构如x86、ARM采用的模式用口诀记忆就是是高高、低低。2. 举例一个例子说明以int a 0x12345678;为例4 字节高字节0x12低字节0x78存储模式内存从上往下低地址 → 高地址大端模式0x120x340x560x78小端模式0x780x560x340x123.大小端存储模式的实际意义大端模式符合人类阅读习惯用于网络传输网络字节序统一为大端。小端模式计算机 CPU 运算效率更高x86/x64 架构电脑默认都是小端模式。4. 用代码判断当前系统存储模式int checkEndian() { int a 1; // 用char*取第一个字节 char* p (char*)a; // *p 1是小端模式 // *p 0是大端模式 return *p; } int main() { if (checkEndian()) { printf(小端模式\n); } else { printf(大端模式\n); } return 0; }三、什么是指针如果用一句话来总结指针就是存储内存地址的变量。指针的类型决定了两件事指针解析数据大小的能力比如char*一次只解析 1 个字节int*一次解析 4 个字节。指针的步长指针 1 时跳过的字节数等于指向类型的大小即int*1 会跳过 4 个字节char*1 只跳过 1 个字节。1. 基础指针的定义与解引用定义int a 0; int *p a;p是指针变量存储的是变量a的地址。int*表示指针p指向的是int类型的数据。解引用*p *p 1;*p就是通过指针里存储的地址去访问对应内存里的数据也就是直接操作变量a的值。2. 多级指针如果一个指针存储的是另一个指针的地址那它就是多级指针最常见的是二级指针int a 10b 20; int *p a; // p是一级指针存储a的地址 int **q p; // q是二级指针存储一级指针p的地址 *q b; // 对q进行一次解引用等价于修改p的值让p指向b **q 1; // 对q进行两次解引用等价于通过p修改a的值把b赋值为1二级指针进行一次解引用可以修改一级指针的指向。如果有更高级的多级指针以此类推。3. 指针 * 与自增 运算符的结合根据C语言的运算符优先级查找表可知优先级*且它们的结合性均为自右向左。由此可知解引用符号与自增符号结合的切入点是先看 在 * 左边还是右边。下面对结合的四种情况作具体分析1*pint arr[] {10,20,30}; int *p arr; int x *p; // x 10p 指向 arr[1]等价于*(p)先取值后指针自增实际执行情况是取当前指针指向的值然后指针向后移动一个单位2(*p)int a 10; int *p a; (*p); // a 11p 仍然指向 a括号优先先取值后值自增实际执行情况是指针不动指向的数据 13*pint arr[] {10,20,30}; int *p arr; int x *p; // p 先指向 arr[1]x 20等价于*(p)指针先自增再取值实际执行情况是指针先向后移动一个单位再取当前指针指向的值4*pint a 10; int *p a; *p; // a 11等价于(*p)先取值再把值自增实际执行情况是指针不动指向的数据 1四、const 关键字与指针1. const 的基本作用const是 C 语言中用于定义只读变量的关键字。被const修饰的变量其值不允许被修改任何试图修改的操作都会在编译阶段报错。使用const可以提高代码的可读性和安全性告诉阅读者这个变量不会变防止意外修改本不该改变的数据让编译器帮助进行更多的优化使用示例const int MAX 100; // MAX 是只读常量后续不能修改 // MAX 200; // 错误表达式必须是可修改的左值 int const MIN 0; // 与 const int 等价同样只读2. const 与普通变量的组合1修饰局部变量void func() { const int LOCAL 10; // LOCAL 20; // 错误 }2修饰全局变量const int GLOBAL 100; // 通常存放在 .rodata 段只读3修饰函数参数void printArray(const int *arr, int len) { arr[0] 10; // 错误*arr 是只读的 for(int i 0; i len; i) { printf(%d , arr[i]); // 只能读取不能修改 } }在此种情况下需注意以下细节对于值传递的参数如void f(const int x)const意义不大因为形参本身就是拷贝。对于指针或引用传递C语言中只有指针const可以有效保护原数据。3. const 与指针const修饰指针时规则是从右往左读离谁近就修饰谁我们分三种情况拆解1const 修饰指针指向的值const double *p PI;从右往左读p指向被const修饰的double类型数据即一个指向常量的指针。不能通过 *p 修改 p 指向的值但 p 本身可以指向其他地址比如p val;是合法的。2const 修饰指针本身double *const q val;从右往左读q是一个被const修饰的指针指向double类型数据是一个常量指针。指针 q 本身不能再指向其他地址q a;会报错但可以通过*q修改指向的值。3const 同时修饰指针本身和指针指向的值const double *const s num;这种情况下既不能修改指针s的指向也不能通过*s修改指向的值即指向常量的常量指针这是最严格的限制。4const修饰指针的关键规则权限只能缩小不能放大const修饰的指针只能给同样带const或权限更小的指针赋值不能把const地址赋值给非const指针否则会绕过 const 的限制修改常量的值。举例说明普通指针 → 可以隐式转换为指向常量的指针增加const限制安全指向常量的指针 →不能隐式转换为普通指针去掉const危险const int n 10; const int *p n; // 合法权限匹配 int *q n; // 非法试图放大权限用非const指针修改const变量五、字符串与指针C 语言没有专门的字符串类型字符串是以\0结尾的 char 数组指针是操作字符串的标准方式。1. 两种字符串写法1字符数组存储在栈 / 全局区可读可写char str[] hello; // 栈上数组内容可修改 str[0] H; // 合法字符串变为 Hello字符数组存放在栈区局部变量或数据区全局变量可读可写以\0结尾大小是字符串个数长度 1。2字符串常量指针存储在数据区只读const char *pstr hello; // 推荐加const字符串常量数据区 (.data)不能修改内容只能读。错误写法char *p hello; p[0] H; // 未定义行为程序崩溃2. 字符串指针的运算const char *p hello; printf(%c, *p); // 输出 hp指向 e printf(%c, *p); // 输出 lp指向第二个 l printf(%c, *p); //错误字符串常量不可修改3. 指针数组存放多个字符串// 指针数组每个元素是指向字符串常量的指针 const char *strs[] { apple, banana, cherry }; // 遍历 int len sizeof(strs) / sizeof(strs[0]); for (int i 0; i len; i) { printf(%s\n, strs[i]); }这种写法相较于字符数组更节省空间。它只存地址不重复拷贝字符串。4. 字符串操作函数简介C 语言提供了一组标准库函数头文件为string.h专门操作字符串底层全部基于指针实现。1strlen —— 求字符串长度函数定义size_t strlen(const char *str);功能计算字符串有效长度不包含结束符\0。原理从指针指向的位置开始遍历直到遇到\0停止。示例char str[] hello; int len strlen(str); // len 5我对此函数的具体实现如下int my_strlen(const char* str) //字符串长度计算函数 { assert(str ! NULL); //形参若是指针用断言函数检查其合法性 const char* p str; while (*p ! \0) { p; } return p - str; //不用计数器仅用指针减法完成对字符串的字符计数 }2strcmp —— 字符串比较函数定义int strcmp(const char *str1, const char *str2);功能按 ASCII 值逐字符比较两个字符串内容。返回值相等 → 返回0s1 s2 → 返回正数s1 s2 → 返回负数注意不能用比较字符串因为字符串本质是字符数组str1和str2都是指向数组首字符的指针。符号比较的是指针存着的地址值在函数运行时可能巧合相同但实际字符并不一致。在实际应用中必须用strcmp()逐字符比较内容。我对此函数的具体实现如下int my_strcmp(const char* stra, const char* strb) //字符串比较函数 { assert(stra ! NULL); assert(strb ! NULL); while (*stra *strb) { //如果两个字符串同时结束说明完全相等 if (*stra \0) return 0; stra; //指针后移 strb; } //如果两个字符串不等则返回不等的两个字符的ASCII差值 return *stra - *strb; }3strcpy —— 字符串拷贝函数定义char *strcpy(char *dest, const char *src);功能把src字符串完整拷贝到dest中包含\0。注意dest必须有足够空间。dest不能是字符串常量。示例char dest[20]; strcpy(dest, hello);我对函数的具体实现如下void my_strcpy(char* dest, const char* src) //字符串赋值函数 { assert(dest ! NULL); assert(src ! NULL); char* p dest; //引入一个p指针将dest赋值给它 //dest的值并未改变。以此可以实现嵌套即连续的字符串复制 //例如my_strcmp(strc,my_strcmp(strb,stra)); /*while (*src ! \0) { *p *src; p; src; }*/ while (*p *src); //等价于上述注释代码段 *dest \0; //不要忘记字符串结束标志\0 }六、指针与数组一对 “双胞胎”在之前我对数组已经进行了学习。数组名本质是首元素地址本质是一个常量指针指针可以完全替代数组下标访问元素。1. 指针与数组的等价关系int arr[] {1,2,3,4,5}; int *p arr; // 等价于int *p arr[0]; // 下标法 ↔ 指针法 完全等价 arr[0] *p; arr[1] *(p1); arr[i] *(pi) *(arri); arr[i] pi arri;2. 指针加减运算指针 ± 整数不是地址 ± 数字而是 “±n × 类型大小”int arr[] {1,2,3,4,5}; int *p arr[2]; // 指向元素3 p[-1] *(p-1); // 访问arr[1] 2 (p2)[1] *(p3); // 访问arr[5]越界慎用这里也解释了数组下标从 0 开始的原因下标直接等于偏移量这样的表示方式使实际的地址计算更加简洁。3. 指针变量的比较同类型的指针指向同一块内存时可比较大小int arr[] {1,2,3}; int *p1 arr[0]; int *p2 arr[1]; if(p1 p2) { ... } // 成立p1地址更低4. 指针数组 vs 数组指针1指针数组本质是数组元素是指针int *parr[3]中[ ]优先级高先和parr结合所以是数组每个元素是int*。int a1,b2,c3; int *parr[] {a, b, c}; // 存放3个int指针 // 字符串指针数组最常用 const char *strs[] {hello, world};2数组指针本质是指针指向整个数组int (*p)[3]中( )强制先和*p结合所以是指针指向int[3]。int arr[3] {1,2,3}; int (*p)[3] arr; // 指向整个arr数组 // p1 跳过整个数组3×412字节arr与arr辨析:arr是首元素地址类型是int*即整型指针。arr是整个数组地址类型是int(*)[3]即数组指针。另外在进行sizeof(arr)运算时返回整个数组占用的总字节数不是指针的大小。这是 C/C 中数组名不退化为指针的少数例外情况之一。3指针数组与数组指针的辨析表运算int *parr[3]int (*p)[3]sizeof(parr)3* sizeof(int*) 324sizeof(p) 8指针大小parr1/p1跳过一个int*8 字节跳过一个int[3]12 字节parr[0]类型int*int[3]退化为int*parr[0][0]类型int需确保parr[0]指向有效 intint4数组指针的典型应用1. 指向二维数组的行int matrix[2][3] {{1,2,3},{4,5,6}}; int (*row)[3] matrix; // matrix 退化为指向第一行的数组指针 printf(%d, row[1][2]); // 打印的结果是62. 函数参数传递二维数组void print_2d(int (*arr)[3], int rows) { for(int i0; irows; i) { for(int j0; j3; j) printf(%d , arr[i][j]); printf(\n); } } // 调用print_2d(matrix, 2);3.数组指针与指针数组的混杂指针数组多维数组int a1[3] {1,2,3}, a2[3] {4,5,6}; int *parr[2] {a1, a2}; // 指针数组每个元素指向一个 int[3] printf(%d, parr[1][2]); // 6 // 数组指针 int (*p)[3] a1; // p 指向 a1 printf(%d, p[0][2]); // 3七、指针与函数值传递、指针传递、指针函数、函数指针1. 函数参数传递值传递 vs 指针传递1值传递形参是实参的拷贝修改形参不影响实参void Swap(int a, int b) { int t a; a b; b t; // 只改形参实参不变 }2指针传递传实参地址解引用可修改实参void Swap(int *pa, int *pb) { int t *pa; *pa *pb; *pb t; } // 在调用时传实参地址即Swap(a, b);指针传递的作用修改实参的值节省内存不拷贝整块数据。2. 指针函数返回值是指针的函数在设计函数返回值时禁止返回栈区局部变量地址// 错误返回局部变量地址函数结束内存失效 int *ErrFun() { int arr[] {1,2}; return arr; } // 正确返回静态变量/全局变量/堆内存/字符串常量地址 int *RightFun() { static int arr[] {1,2}; // 静态变量存数据区全程存在 return arr; } const char *GetStr() { return hello world; // 返回字符串常量地址安全 }3. 函数指针指针指向函数存函数地址函数名本质是函数入口地址函数指针可回调函数、动态调用函数// 定义函数指针指向返回int、参数(int,int)的函数 int (*Add)(int, int); // 赋值 int Sum(int a, int b) { return ab; } Add Sum; // 调用 int res Add(1,2); // 等价于Sum(1,2)八、泛型指针void* 万能指针void*是无类型指针可以存任意类型变量的地址但有严格限制不能解引用不知道类型不知道读多少字节。不能做 ± 运算不知道步长。常用于内存操作函数memset / memcpy / memmove。应用示例int a 10; void *p a; // 合法存任意地址 // *p 20; 报错 // p; 报错内存操作函数基于 void*1memset逐字节初始化内存函数int arr[5]; memset(arr, 0, sizeof(arr)); // 全部清0对此函数进行调用memset(val, 0 , sizeof(int));内存底层如下: 0x00 00 00 00我对此函数的具体实现如下void my_memset(void* str, int val, int size) //内存逐字节初始化函数 { assert(str ! NULL); char* p (char*)str; //将泛型指针强制转换为char类型指针以求对数组进行逐字节赋值 while (size-- 0) { *p (char)val;//只取val的低1字节的数据 } }2memcpy / memmove内存拷贝函数memcpy不可处理内存重叠情况。int arr[] {1,2,3,4,5}; int brr[5]; memcpy(brr, arr, sizeof(arr)); // 拷贝全部数据memmove可以兼容内存重叠更安全。我对此函数的具体实现如下void my_memmove(void* dest, const void* src, int size) { //如果目标在源之前开始则从缓冲区开头向前复制字节 //否则从末尾向后复制 assert(dest ! NULL); assert(src ! NULL); if (size 0) { return; } char* d (char*)dest; char* s (char*)src; if (d s) //目标地址在源地址前面 { while (size--) { *d *s; } } else //目标地址在源地址后面 { //指针跳到最后一个字节 d size - 1; s size - 1; //从后往前拷贝 while (size--) { *d-- *s--; } } }九、三类特殊指针空指针、野指针、失效指针1. 空指针NULL指向地址0表示指针不指向任何有效内存是一个安全的 “无指向” 状态。它经常被用来初始化指针。int *p NULL; // *p 10; 报错访问空指针2. 野指针声明了指针变量但未对其进行初始化指向随机内存地址会导致程序崩溃、数据错乱。在实际编程中一定要避免出现。int *p; // 野指针未赋值 *p 10; // 非法访问崩溃3. 失效指针悬挂指针指针指向的内存已被释放 / 销毁但指针未置空。常见场景函数返回局部变量地址、free 后未置空、字符串指针误用。int *func() { int a 10; return a; } // 函数结束a 被销毁 // 返回的指针就是失效指针int *p (int*)malloc(4); free(p); // 释放内存 // p未置空是失效指针失效指针产生的根本原因是指针指向了栈区的局部变量函数退出后内存被系统回收。对它的解决办法是把局部变量变成静态局部变量static让它存放在数据区生命周期延长至整个程序结束。1static 关键字的使用1. 修饰局部变量静态局部变量void fun() { static int count 0; count; printf(%d , count); }局部变量count存储在数据区 (.data)而不是栈区.stack。它的生命周期是整个程序开始→程序结束函数退出不销毁。它只初始化一次多次调用函数不会重新初始化。它的作用域仍在函数内部即只在函数内部可见局部可见。常被用来解决 “返回局部变量地址导致失效指针”、以及统计函数调用次数。2. 修饰全局变量静态全局变量static int val 100;全局变量val存储在数据区 (.data)。它的生命周期是整个程序开始→程序结束。它的作用域在当前文件即只在当前.c 文件内可见其他文件无法访问。常用来防止全局变量命名冲突3. 修饰函数静态函数static void fun() { ... }它的作用域在当前文件即只能在当前.c 文件内可调用。常用来封装内部工具函数不暴露给外部防止函数名冲突。2static 解决失效指针示例int *GetResult() { // 静态数组数据区函数退出不销毁 static int arr[2] {0}; arr[0] 10 20; arr[1] 10 * 20; return arr; } int main() { int *p GetResult(); printf(%d %d\n, p[0], p[1]); // 安全访问无失效 return 0; }
返回列表