【C语言】第三章:内存与指针(指针上篇)

📅 2026/8/14 14:13:58
【C语言】第三章:内存与指针(指针上篇)
系列说明在学习阶段我就开始写C语言的笔记并发表博客本篇是我对学习阶段笔记的整理希望和大家共同进步。如有遗漏欢迎大家指出。运行环境本系列所有演示代码均在Visual Studio 2022x64环境下编写。目录内存和地址指针变量和地址指针变量类型的意义指针运算const 修饰指针野指针assert 断言传值调用与传址调用一、正文1. 内存和地址计算机的CPU处理数据时需要从内存中读取。内存被划分为一个个内存单元每个内存单元大小取1 个字节。每个内存单元都有一个编号就像宿舍的门牌号——在计算机中这个编号叫地址C语言中给地址起的新名字叫指针。内存单元的编号 地址 指针编址原理内存并不记录每个字节的地址而是通过硬件设计完成。32位机器有32根地址总线每根线有0/1两种状态32根线可以表示2^32种地址64位机器则有2^64种。这就是为什么 32 位平台指针占 4 字节、64 位平台指针占 8 字节。2. 指针变量和地址在C语言中创建变量就是向内存申请空间。是取地址操作符取出变量的地址*是解引用操作符通过地址访问指向的对象。demo1_address.c —— 取地址、指针变量、指针大小// demo1_address.c - 内存与地址取地址、指针变量、指针大小#includestdio.hintmain(){inta10;charchw;// 取地址操作符 取出变量第一个字节的地址int*paa;// 指针变量存放 int 类型变量的地址char*pcch;// 指针变量存放 char 类型变量的地址// 打印地址%pprintf(a %p\n,a);printf(pa %p\n,pa);// 解引用操作符 *通过地址访问指向的对象*pa20;// 等价于 a 20printf(a %d\n,a);// 指针变量的大小与类型无关取决于平台位数printf(sizeof(char*) %zu\n,sizeof(char*));printf(sizeof(int*) %zu\n,sizeof(int*));printf(sizeof(double*) %zu\n,sizeof(double*));return0;}预期终端输出地址值每次运行不同a 000000CE56FCF764 pa 000000CE56FCF764 a 20 sizeof(char*) 8 sizeof(int*) 8 sizeof(double*) 8运行结果a与pa相同*pa 20修改了 a64位平台下所有指针大小都是 8 字节结论指针变量的大小与类型无关只取决于平台位数32位平台为4字节64位平台为8字节。int*中的int表示指针指向的是int类型的对象。3. 指针变量类型的意义既然指针大小与类型无关为什么还要各种指针类型因为类型决定了解引用权限和步长。3.1 解引用的权限int*解引用能访问4 个字节char*解引用只能访问1 个字节。demo2_ptype.c —— 指针类型意义权限与步长// demo2_ptype.c - 指针类型的意义解引用权限与步长#includestdio.hintmain(){intn0x11223344;// 1. 解引用权限int* 改4个字节char* 改1个字节int*pin;// *pi 0; // 若执行n 的4个字节全变为0char*pc(char*)n;// 强制转换成 char**pc0;// 只把第一个字节改为0printf(n 0x%x\n,n);// 结果 0x11223300小端机器低位字节被改// 2. 指针步长1 跳过多少个字节取决于类型intarr[3]{1,2,3};printf(arr %p\n,arr);printf(arr1 %p\n,arr1);// int*跳过4字节printf((char*)arr1 %p\n,(char*)arr1);// char*跳过1字节return0;}预期终端输出地址值每次运行不同n 0x11223300 arr 000000BAD9EFFEE8 arr1 000000BAD9EFFEEC (char*)arr1 000000BAD9EFFEE9结论指针的类型决定了解引用的权限一次操作几个字节。指针的类型决定了指针±1的步长向前/向后走一步有多远。3.2 void* 指针void*是无具体类型的指针泛型指针可以接收任意类型的地址。但void*不能直接进行解引用和指针±整数的运算因为它不知道步长。void*常用于函数参数接收不同类型数据的地址实现泛型编程效果后续 qsort 会用到。4. 指针运算指针的基本运算有三种指针±整数、指针-指针、指针的关系运算。demo3_ptrop.c —— 指针运算// demo3_ptrop.c - 指针运算指针±整数、指针-指针、关系运算#includestdio.h// 用指针-指针求字符串长度\0 之前字符个数intmy_strlen(constchar*s){constchar*ps;while(*p!\0)p;return(int)(p-s);// 两个指针相减得到元素个数}intmain(){intarr[10]{1,2,3,4,5,6,7,8,9,10};intszsizeof(arr)/sizeof(arr[0]);int*parr;// 数组名是首元素地址// 1. 指针整数 遍历数组printf(指针整数遍历: );inti0;for(i0;isz;i){printf(%d ,*(pi));}printf(\n);// 2. 指针-指针计算元素个数int*lastarr[9];printf(arr 的元素个数 %d\n,(int)(last-p)1);// 3. 指针关系运算 指针-指针求字符串长度printf(my_strlen(\abcdef\) %d\n,my_strlen(abcdef));// 4. 指针关系运算遍历数组printf(关系运算遍历: );parr;while(parrsz){printf(%d ,*p);p;}printf(\n);return0;}预期终端输出指针整数遍历: 1 2 3 4 5 6 7 8 9 10 arr 的元素个数 10 my_strlen(abcdef) 6 关系运算遍历: 1 2 3 4 5 6 7 8 9 10运行结果*(pi)访问数组元素指针-指针得到元素个数p arrsz关系运算遍历5. const 修饰指针const用来限制变量的修改。当const修饰指针时放在*的左边还是右边意义不同int*p;// 无 const都可变intconst*p;// const 在 * 左边指向的内容不可改指针本身可变int*constp;// const 在 * 右边指针本身不可改指向内容可通过指针改intconst*constp;// 左右都有都不可改demo4_const_ptr.c —— const 修饰指针四种情形// demo4_const_ptr.c - const 修饰指针的四种情形#includestdio.h// 无 const指针和指向内容都可变voidtest1(){intn10;intm20;int*pn;*p20;// ok可修改指向内容pm;// ok可修改指针本身printf(test1 ok\n);}// const 在 * 左边指向的内容不可通过指针修改指针本身可变voidtest2(){intn10;intm20;constint*pn;// *p 20; // 错误不能修改指向内容pm;// ok指针本身可变printf(test2 ok\n);}// const 在 * 右边指针本身不可变指向的内容可通过指针修改voidtest3(){intn10;intm20;int*constpn;*p20;// ok可修改指向内容// p m; // 错误不能修改指针本身printf(test3 ok, n %d\n,n);}// * 左右都有 const两者都不可变voidtest4(){intn10;constint*constpn;// *p 20; // 错误// p n; // 错误printf(test4 ok\n);}intmain(){test1();test2();test3();test4();return0;}预期终端输出test1 ok test2 ok test3 ok, n 20 test4 ok记忆const在*左边修饰的是指针指向的内容不能通过指针改内容const在*右边修饰的是指针本身指针不能再指向别处。6. 野指针野指针就是指针指向的位置是不可知的随机的、不正确的、没有明确限制的。6.1 野指针成因指针未初始化局部变量指针未初始化默认是随机值。指针越界访问指针指向范围超出数组范围。指针指向的空间释放如返回局部变量的地址函数结束空间已销毁。6.2 如何规避野指针指针初始化明确指向就赋值不知道指向哪里就赋NULLNULL 的值是00地址无法读写。小心指针越界只能访问申请过的空间。不再使用时及时置 NULL使用前检查有效性约定 NULL 指针不访问使用前判断是否为空。避免返回局部变量的地址。demo5_wild_ptr.c —— 规避野指针// demo5_wild_ptr.c - 野指针规避方法演示#includestdio.h#includeassert.h// 错误示范野指针成因之一返回局部变量的地址// 编译器会警告 C4172n 出函数即销毁地址无效。// int* test_bad()// {// int n 100;// return n;// }intmain(){// 1. 指针初始化明确指向就赋值否则置 NULLintnum10;int*p1num;// 明确指向int*p2NULL;// 不知道指向哪里就置 NULL// 2. 使用前检查有效性if(p2!NULL){printf(%d\n,*p2);}else{printf(p2 是空指针不访问\n);}// 3. 数组越界会变成野指针intarr[10]{0};int*parr;inti0;for(i0;i10;i){*(p)i;// 结束后 p 已指向数组外}pNULL;// 不再使用及时置空// 4. assert 断言检查assert(p1!NULL);// 表达式为真则继续否则报错终止printf(p1 %d\n,*p1);return0;}预期终端输出p2 是空指针不访问 p1 107. assert 断言assert.h头文件定义了宏assert()用于在运行时确保程序符合指定条件不符合就报错终止运行。assert(p!NULL);若表达式为真非零assert 不产生作用程序继续。若表达式为假零assert 报错在 stderr 写入错误信息含表达式、文件名、行号。好处自动标识文件和行号可通过定义NDEBUG宏在编译期关闭所有 assert#defineNDEBUG#includeassert.hassert有运行时开销一般 Debug 版本使用、Release 版本通过 NDEBUG 关闭。demo6_assert.c —— assert 断言// demo6_assert.c - assert 断言的使用与 NDEBUG 关闭#includestdio.h#includeassert.h// 用 assert 检查形参有效性intmy_strlen(constchar*str){assert(str!NULL);// 若 str 为 NULL程序报错终止intcount0;while(*str){count;str;}returncount;}intmain(){printf(len %d\n,my_strlen(hello));// 传 NULL 会触发 assert 失败测试时可取消注释// printf(len %d\n, my_strlen(NULL));return0;}预期终端输出len 58. 传值调用与传址调用学习指针的目的是解决问题。以交换两个整型变量的值为例传值调用形参是实参的临时拷贝交换形参不影响实参 →交换失败。传址调用把变量的地址传给函数函数通过地址操作主调函数的变量 →交换成功。demo7_strlen_sim.c —— strlen 模拟 传值/传址对比// demo7_strlen_sim.c - strlen 模拟实现 传值/传址调用对比#includestdio.h#includeassert.h// 计数器法模拟 strlenintmy_strlen(constchar*str){assert(str!NULL);intcount0;while(*str){count;str;}returncount;}// 传值调用无法交换voidswap_value(intx,inty){inttmpx;xy;ytmp;}// 传址调用通过地址交换voidswap_ptr(int*px,int*py){inttmp*px;*px*py;*pytmp;}intmain(){// strlen 模拟printf(my_strlen(\abcdef\) %d\n,my_strlen(abcdef));// 传值 vs 传址inta3;intb5;printf(交换前: a%d b%d\n,a,b);swap_value(a,b);printf(传值后: a%d b%d\n,a,b);swap_ptr(a,b);printf(传址后: a%d b%d\n,a,b);return0;}预期终端输出my_strlen(abcdef) 6 交换前: a3 b5 传值后: a3 b5 传址后: a5 b3运行结果strlen 模拟返回6传值调用交换失败传址调用交换成功结论如果函数只是用主调函数变量的值计算用传值调用如果函数要修改主调函数的变量必须用传址调用。声明本文为个人学习笔记基于笔者学习内容整理代码均在 VS2022 环境验证通过。