)
前言本系列篇幅有点长但若能完整的看下来相信你对指针能有一个全新的认识。1. 内存和地址内存的物理意义就是插在电脑主板上的内存条经过cpu的处理映射后程序所能访问到的其实就是一堆的01比特。计算机为了更好的管理内存将8个连续的比特称之为字节(1 Byte 8 bit)。每个字节在内存中都有独立的地址内存空间单元每个内存单元的大小取1个字节。图1.1 常见单位补充其中每个内存单元相当于一个学生宿舍1个字节空间内能放8个比特位每个内存单元都有一个编号好比宿舍门牌号有了这个内存单元的编号CPU就可以快速找到一个内存空间。图1.2 内存单元编号在C中定义变量就是在某个地址开始的连续内存分配给变量。生活中我们把门牌号称为地址计算机中把内存单元的编号也称为地址C语言给地址起了新名字指针。内存单元的编号好比门牌号、宿舍号 地址 指针那么如何理解编址?CPU访问内存中的某个字节空间必须知道字节空间在内存的什么位置而内存中字节很多所以需要给内存进行编址好比给房间设立门牌号。计算机中的编址不是把每个字节的地址记录下来而是通过硬件设计完成。钢琴、吉他上没有写“朵、来、咪、发”这样的信息但演奏者照样能找到琴弦精确的位置。这是因为制造商已经在乐器硬件层面设计好了并且所有演奏者都知道。本质是一种约定好的共识硬件编址也是如此。计算机中有一组线地址总线可以进行硬件单元的协同工作数据传递。简单理解为32位机器有32根地址总线一根线有0、1俩种状态电脉冲有无那么一根线代表俩种含义32根线代表2^32次方种含义每种含义代表一个地址。地址信息被下达给内存在内存上就可以找到该地址对应的数据将数据通过数据总线传入CPU内寄存器。2. 指针变量和地址2.1 取地址操作符()C语言创建变量其实就是向内存申请空间。#includestdio.h int main() { int a 10; return 0; }图2.1 a变量地址空间及其值上述代码创建了整型变量a内存中申请4个字节的内存空间用于存放整数100a为十六进制每个字节都有其地址空间。上图0a 00 00 00四个字节地址分别为0x00DAFC4C0x00DAFC4D0X00DAFC4E0X00DAFC4F那么如何得到a变量的地址?此时就要使用取地址操作符。int main() { int a 10; printf(%p\n,a); return 0; }图2.2打印a变量地址图2.2a变量地址如下0x00DCFDA80X00DCFDA90X00DCFDAA0X00DCFDAB //地址是依次递增的由图可见会打印出0x00DCFDA8,a取出的是a变量空间所占4字节地址中较小字节的地址千万不要认为取出的是整个a变量空间的地址虽然整型变量a占用4个字节的地址空间我们只需要知道第一个字节地址顺藤摸瓜访问到4个字节的数据也是可以的。2.2 指针变量和解引用操作符(*)2.2.1 指针变量我们通过取地址操作符()拿到的地址是一个数值比如0x00DCFDA8这个数值有时候也需要存储起来方便后期再使用那我们把这样的地址值存放在哪里呢?答案是指针变量中。指针变量也是一种变量那么也符合变量的定义创建指针变量时也会向内存申请地址空间这很重要后续的二级、三级指针会用到指针变量就是专门存放地址的存放在指针变量中的值都会理解为地址。int main() { int a 10; int* pa a;//取出a变量的空间地址4个字节地址中较小的字节地址并存储到指针变量pa中 printf(%p\n,pa); printf(%p\n,a); return 0; }一定要牢记我2.1节最后一段的话有助于你更好的理解指针图2.3普通变量a和指针变量pa图2.3普通变量a所占地址如下0x0116FE700x0116FE710x0116FE720x0116FE732.2.2 如何拆解指针类型在2.2.1代码中我们看到pa变量的类型是int*,那么该如何理解指针类型呢?pa变量的类型int*, *是在说明pa是指针变量而前面的int是在说明pa指向的是整型int类型的对象。那如果有一个char类型的变量chch的地址要放在什么类型的指针变量中呢?读者可以思考一下2.2.3 解引用操作符(*)那么我们如何使用保存起来的地址呢?在现实生活中我们是根据门牌号来寻找房间的找到房间后可以在房间中拿取或存放物品。C语言也是一样的我们只要拿到了地址指针变量所申请内存空间中存储的值就可以通过地址找到地址指向的对象此时就用到了解引用操作符*又称间接访问操作符。int main() { int a 100; int* pa a; /* pa指针变量所申请的空间中存放的是a变量的地址*pa就是把a变量地址解引用后找到了 a变量所申请空间,也就是说*pa就是a变量也可以对a变量地址空间中的值进行修改 */ *pa 0;//将100修改为0 return 0; }有些读者可能想哎呀不就是改a变量空间中存放的值吗直接a 0就行了为啥使用指针阿?其实这里是把a变量的修改交给pa来操作这样对a变量的修改就多了一种途径(相当于不用自己动手交给手下员工就行)多了一种方法。写代码就会更加灵活。2.3 指针变量的大小前面我们提到32位机器假设有32根地址总线每根地址线出来的电信号转换成数字信号后为0或1那把32根地址线产生的2进制序列当作一个地址那么一个地址就是32个bit位需要4字节1字节 8比特才能存储。如果指针变量是用来存放地址的那么指针变量的大小就得是4个字节的空间才可以。同理64位机器假设有64根地址线一个地址就是64个二进制位组成的二进制序列存储起来就需要8个字节的空间指着变量的大小就是8字节。/* sizeof()函数是用来计算参数所占字节个数的函数 */ //指针变量的⼤⼩取决于地址的⼤⼩ //32位平台下地址是32个bit位即4个字节 //64位平台下地址是64个bit位即8个字节 int main() { printf(char*:%zd\n, sizeof(char*)); printf(short*:%zd\n, sizeof(short*)); printf(int*:%zd\n, sizeof(int*)); printf(double*:%zd\n, sizeof(double*)); return 0; }图2.4 32位平台下所占字节图2.5 64位平台下所占字节总结32位平台下地址是32个bit位指针变量大小是32位4字节64位平台下地址是64个bit位指针变量大小是64位8字节指针变量的大小所占字节个数和类型是无关的只要指针类型的变量在相同平台下大小都是相同的指针变量所占字节空间必须要放下地址32位平台下是4字节64位平台是8字节3. 指针变量类型的意义指针变量的大小所占字节个数和类型int 、char无关只要是指针变量在同一平台下大小都是一样的为什么还要有各种各样的指针类型(int* 、char*)呢?其实指针类型也是有意义的。3.1 指针的解引用//代码1 int main() { int n 0x11223344;//0x代表该数字是一个16进制数字 int* pi n;//整型指针变量pi *pi 0; return 0; }图3.1 n变量未修改前在内存中的情况图3.2 n变量和pi指针变量未修改前的情况图3.3 n变量被*pi修改后在内存中的情况图3.4 n变量和pi指针变量修改后的情况//代码2 int main() { int n 0x11223344;//0x代表该数字是一个16进制数字 char *pc (char *)n;//字符类型的指针变量pc *pc 0; return 0; }图3.5 n变量未修改前在内存中的情况图3.6 n变量和pc指针变量未修改前情况图3.7 n变量被*pc修改后在内存中情况图3.8 n变量和pc指针变量修改后情况通过调试我们看到代码1会将n变量申请的4个字节空间的值全部修改为0但是代码2只是将n变量申请的4个字节空间中较小的字节空间中的值修改为0。结论指针的类型int*、char*决定了对指针解引用的时候有多大权限一次能操作几个字节。比如char*类型的指针解引用只能访问1个字节而int*的指针解引用就能访问4个字节。3.2 指针-整数int main() { int n 10; char* pc (char*)n; int* pi n; printf(n :%p\n, n); printf(pc :%p\n, pc); printf(pc1 :%p\n, pc 1); printf(pi :%p\n, pi); printf(pi1 :%p\n, pi 1); return 0; }图3.9 pc、pi、pc1、pi1地址我们可以看到char*类型的指针变量pc 1跳过1个字节int*类型的指针变量pi 1跳过4个字节。这就是指针变量的数据类型差异所带来的变化。指针 1就是跳过1个指针指向元素所占的字节个数int类型元素就跳过4字节char类型元素就跳过1字节指针-1和1一个道理。总结指针变量的数据类型决定了指针变量向前或向后走一步有多大跳过几个字节3.3 void*指针在指针类型中有一种特殊的类型是void*类型的可以理解为无具体类型的指针或者泛型指针这种类型的指针可以用来接受任意类型地址但也有局限性。void*类型的指针不能直接进行指针的-整数和解引用运算因为类型不确定嘛。那么void*类型的指针有什么用呢?一般void*类型的指针是使用在函数参数的部分用来接受不同类型数据的地址这样的设计可以实现泛型编程的效果使得函数来处理多种类型的数据。4. const修饰指针4.1 const修饰普通变量变量是可以修改的如果把变量的地址交给一个指针变量通过指针变量也可以修改这个变量。但如果我们希望一个变量加上一些限制不能被修改。这就要用到const了。int main() { int m 0; m 20;//m是可以修改的 const int n 0; n 20;//n是不能被修改的 return 0; }上述代码中n是不能被修改的其实n本质是变量只不过被const修饰后在语法上加了限制。当我们对const修饰的n变量进行修改就会报错没法直接修改n。/* 可以使用指针间接进行修改n变量的值 */ int main() { const int n 0; printf(n %d\n, n); int*p n; *p 20; printf(n %d\n, n); return 0; }虽然通过指针确实修改了n的值但我们还是要思考一下const修饰n就是为了n不能被修改如果p拿到n的地址就能修改n这样就打破了const的限制。是不合理的所以应该让p拿到n的地址也不能修改n那么要怎么做呢?4.2 const修饰指针变量一般来讲const修饰指针变量可以放在*的左边也可以放在*的右边。都有不同的含义。int * p;//没有const修饰 int const * p;//const 放在*的左边做修饰 int * const p;//const 放在*的右边做修饰#include stdio.h //代码1 - 测试⽆const修饰的情况 void test1() { int n 10; int m 20; int *p n; *p 20;//ok? p m; //ok? } //代码2 - 测试const放在*的左边情况 void test2() { int n 10; int m 20; const int* p n; //p指针变量所指向字节空间的内容不能变但p指针变量内容可以变(字节空间地址) *p 20;//ok? p m; //ok? } //代码3 - 测试const放在*的右边情况 void test3() { int n 10; int m 20; int * const p n; //指针变量p的内容(字节空间地址)不能变也就是p指针变量空间中的地址容不能变 *p 20; //ok? p m; //ok? } //代码4 - 测试*的左右两边都有const void test4() { int n 10; int m 20; int const * const p n;//指针变量本身和指针变量所指向的内容都不能修改 *p 20; //ok? p m; //ok? } int main() { //测试⽆const修饰的情况 test1(); //测试const放在*的左边情况 test2(); //测试const放在*的右边情况 test3(); //测试*的左右两边都有const test4(); return 0; }结论const修饰指针变量的时候1const如果放在*左边修饰的是指针指向的内容保证指针指向的内容不能通过指针来改变也就是不能再用指针间接修改变量的值但是指针变量的内容字节空间地址可以变。2const如果放在*右边修饰的是指针变量本身保证了指针变量的内容字节空间地址不能修改但是指针所指向的内容可以通过指针改变。也就是说还可以通过指针间接修改被const修饰的变量5. 指针运算指针的基本运算有三种指针-整数指针-指针指针的关系运算5.1 指针-整数因为数组在内存中是连续存放的只要知道第一个元素的地址顺藤摸瓜就能找到后面的所有元素。#include stdio.h //指针- 整数 int main() { int arr[10] {1,2,3,4,5,6,7,8,9,10}; int *p arr[0];//p存放数组首元素地址 int i 0; int sz sizeof(arr)/sizeof(arr[0]); for(i0; isz; i) { printf(%d , *(pi));//pi 这⾥就是指针整数 } return 0; }5.2 指针-指针//指针-指针 #include stdio.h int my_strlen(char *s) { char *p s; while(*p ! \0 ) p; return p-s; } int main() { printf(%d\n, my_strlen(abc)); //指针-指针其实计算的是俩个指针之间的元素个数 return 0; }6. 野指针野指针就是指针指向的位置是不可知的随机的、不正确的、没有明确限制的6.1 野指针原因1. 指针未初始化#include stdio.h int main() { int *p;//局部变量指针未初始化默认为随机值 *p 20; return 0; }2. 指针越界访问#include stdio.h int main() { int arr[10] {0}; int *p arr[0]; int i 0; for(i0; i11; i) { //当指针指向的范围超出数组arr的范围时p就是野指针 *(p) i; } return 0; }3. 指针指向空间被释放#include stdio.h int* test() { int n 100; return n; } int main() { int*p test();//test函数执行完后函数栈帧被释放p指针无法指向n变量 printf(%d\n, *p); return 0; }6.2 如何避免野指针1指针初始化如果明确知道指针指向哪里就直接赋值地址如果不知道指针应该指向哪里可以给指针赋NULLNULL是C语言中定义的一个标识符常量值是00也是地址这个地址是无法使用的读写该地址会报错。int main() { int num 10; int*p1 num; int*p2 NULL; return 0; }2避免指针越界一个程序向内存申请了哪些空间通过指针也就只能访问哪些空间不能超出范围访问。3指针变量不再使用时及时置NULL指针使用之前检查有效性当指针变量指向一块区域的时候我们可以通过指针访问该区域。后面不再使用这个指针访问空间的时候我们可以把该指针置为NULL。这里有一个规则只要是NULL指针就不去访问同时使用指针之前可以判断指针是否为NULL。我们可以把野指针想象成流浪狗放任不管是非常危险的。所以我们可以把流浪狗拴在树上把狗固定起来就相对安全了给指针变量及时赋值为NULL就好比把流浪狗固定住。把野指针暂时管理起来。不过流浪狗即使被固定住了我们也不要轻易招惹它对于指针也是在使用之前我们也要判断是否为NULL。看是否是“被固定住的流浪狗”如果是那么不能直接使用反之则可以。int main() { int arr[10] {1,2,3,4,5,6,7,8,9,10}; int *p arr[0]; int i 0; for(i0; i10; i) { *(p) i; } //此时p已经越界了可以把p置为NULL p NULL; //下次使⽤的时候判断p不为NULL的时候再使⽤ //... p arr[0];//重新让p获得地址 if(p ! NULL) //判断 { //... } return 0; }4避免返回局部变量的地址比如6.1节中的第三种情况。7. assert断言assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件如果不符合就报错终止运行。这个宏常常被称为断言。assert(p ! NULL);程序运行到上面这一行代码时验证变量p是否等于NULL。如果确实不等于NULL程序继续运行否则终止运行并且给出报错信息提示。assert宏接受一个表达式作为参数。如果该表达式为真返回值非0assert不会产生任何作用程序继续运行。如果该表达式为假返回值为0assert就会报错。在标准错误流stderr中写入一条错误信息显示没有通过的表达式以及包含这恶表达式的文件名和行号。assert的使用对程序员是非常友好的使用assert有几个好处1它不仅能自动标识文件和出问题的行号还有一种无需更改代码就能开启或关闭assert的机制。如果已经确认程序没有问题不需要再做断言就在#includeassert.h前定义一个宏NDEBUG。#define NDEBUG #include assert.h然后重新编译程序编译器就会禁用文件中所有的assert语句。如果程序又出现问题可以移除这条#define NDEBUG再次编译就又重新启用assert语句。assert缺点是因为引用了额外的检查增加了程序的运行时间。