指针学习(上) 九、深入学习指针深入理解指针(一)、1、内存和地址在学习指针前我们需要先了解内存和地址在计算机中CPU是集中处理器当程序运行 起来后CPU会占用内存读写数据这也是为什么我们买电脑的内存加硬盘的模式例如16 G512 G内存的大小决定着电脑能不能运行大的游戏。读写是两个过程分别是读和写读CPU从内存上获取谁写将数据存入内存中缓存CPU是如何从内存这片空间中读取数据的呢首先CPU必须先知道数据在内存的哪里。比如说你的朋友来宿舍楼找你如果他不知道你的宿舍门牌号那么他只能一个一个挨着找但是如果他知道门牌号就可以直接找到。原因就是我们对宿舍楼这片空间分区然后编号同理我们对内存分区编号CPU按这编号就可以直接找到对应的内存位置这就是地址而在C语言中将内存的编号新命名了一个名字——指针我们知道宿舍 楼是一个房间一个房间的编号然后房间里面有床位那内存是如何分区编号的呢在内存中我们存放的是二进制的数值也就是 1 和 0 而能存放一个二进制位的空间就是bite比特每8个这样的bite合起来就是一个Byte字节每1024个Byte合起来就是Kb此后的进制就是210所以地址指针内存单元的编号那么内存是如何编址的呢首先内存创造好后它的每一个内存单元就已经完成编号了是通过硬件手段编写好的读取对于内存只需要CPU把地址告诉给内存然后内存反馈就OK了内存CPU这些在计算机内都是硬件他们直接进行数据交换就需要用到“线”内存和CPU直接简单来说有三组线控制总线数据总线地址总线注意我说的是一组而不是一根模式图如下模拟读写中的读首先控制总线发出读 的信号假如是R然后地址总线开始传入地址假如是0x0012354efc这是一个16进制的数值将它转换为二进制然后在地址总线中一根线传输一位二进制数值最终将地址传给内存然后内存读取到数据再通过数据总线返回给CPU地址总线有多少根就取决于计算机是多少位的32位64位等上面就是CPU和内存直接的数据交互的部分过程这些线是真实存在的可能太小而看起来不像我们日常生活中看到的数据线2、指针变量和地址当我们在编译器里面创建了一个整型变量并初始化这一个过程是怎么样的呢如下图我们创建了变量 a 它的值是 10 变量a这个名字是给程序员看的因为程序员直接看地址太难记住了而对于编译器计算机它们看的是a 的地址。地址又是如何表现的呢首先创建的是int类型变量也就是4个字节的它的值是10转化为二进制就是0000 0000 0000 0000 0000 0000 0000 1010 这是32位一共32/84个字节换算位十六进制就是0 0 0 0 0 0 0 a按字节间隔开就是00 00 00 0a 我们知道应该字节就是一个内存存储单元就对应一个内存编号所以这里应该有四个内存编号也就是四个地址如上图红色框框这四个地址的顺序是挨着一起的就会疑问那我求a的地址应该得到谁呢很简单就是地址最小的那一个然后在此基础上地址依次加一就可以得到其他地址当我们通过a找到a的地址后我们发现地址本质也是一个数值它是一个16进制的数如果我们把地址存给 p 那么p就是指针变量也就是存储地址的变量有变量就有变量类型指针的变量类型就是 int *指针变量是专门存放地址的只要存入指针变量那么我就认为它是地址。换句话就是“在锤子的眼里万物都是钉子”见谁都是锤两下。3、指针变量拆解对于这串代码int main() { int a 10; int* p a; return 0; }指针变量是p而int *是变量类型它的意思其实是说指针p指向的数据的类型是int而 * 才是在 表明p是指针如果指针p指向的数据类型是 char 那么指针p的类型就是 char *通常我们用字母p表示指针原因是应为指针的单词的pointer而不是地址place4、解引用操作符 *与取地址操作符成对的是解引用操作符 * 也叫间接访问操作符作用如下可以通过指针去找到地址里面存放的变量用如下代码解释#includestdio.h int main() { int a 10; int* p a; *p 5; printf(%d\n, a); return 0; }上述代码中*p表示的就是a简单理解就是 * ( a ) a所以最后打印的结果是55、指针变量的大小指针变量也是变量变量就需要存储起来那么需要多大的空间存指针变量那么指针变量的大小就是多大在之前我在知道在32位的机器里CPU通过32根地址线将32个0或者1串传给内存那么内存就需要32个bite来接收这个地址所以地址需要的内存是32个比特也就是4个字节。同理如果计算机是64位的那么指针变量大小就是64位也就是8个字节并且指针就是地址不论它是谁的地址它的大小都由计算机是32位还是64位来决定我们用如下代码验证6、指针类型的意义既然指针大小和指针所指向的元素的类型无关只取决于计算机的环境那么指针类型还有什么用呢当然有用指针变量的类型决定了指针访问内存时的权限。如下代码当我们使用指针修改变量a的值的时候指针需要访问几个字节呢变量a的值是十六进制的11223344每一个十六进制位转换为二进制位也就是bite就是4个bite而这里8个十六进制数刚好就是32位也就是8个字节也就是一个int的大小当我们使用int * 类型的 * pa 指针将a修改为0的时候就是直接修改四个字节内存中的东西如果我们用char * 来存放指针就会只修改一个字节如下图所示。原因就是指针是char类型的char的大小就是一个字节只有访问一个字节的内存的权限所以只能修改第一个字节里面存放的数据7、指针整数观察下面代码的效果当我们对指针加一后产生了不同效果对于int *类型的指针它加一会移动4个字节对于char *类型的指针它加一会移动1个字节结论指针的类型决定了指针向前向后走一步的字节数8、void * 指针void* 类型是只无类型的指针所有类型的变量的地址都可以用它来存储同时由于void* 它自己都不知道自己是什么类型所以它无法进行加一减一这样的操作也不可以使用解引用操作符9、指针运算9.1、指针加减整数如下代码数组中的元素是连续存放的所以我们知道第一个元素的地址然后指针就可以找到所有元素的地址这样就可以打印所有元素#includestdio.h int main() { int arr[10] { 1,2,3,4,5,6,7,8,9,10 }; int* pa arr[0]; int i 0; int sz sizeof(arr) / sizeof(arr[0]); for (i 0; i sz; i) { printf(%d\n, *pa); pa; } return 0; }写法二#includestdio.h int main() { int arr[10] { 1,2,3,4,5,6,7,8,9,10 }; int* pa arr[0]; int i 0; int sz sizeof(arr) / sizeof(arr[0]); for (i 0; i sz; i) { printf(%d\n, *(pai)); } return 0; }当我们将数组元素修改为字符时一样可以打印代码如下#includestdio.h int main() { char arr[] hello world!; char* pa arr[0]; while (*pa ! 0)//while(*pa) { printf(%c , *pa); pa; } return 0; }注意到在while循环的条件判断中判断 *pa是不是等于0因为ASCII值为0的字符刚好就是 \0 而在字符串“hello world!”的后面会自己跟着 \0 所以可以判断另外也可以不用和零比较因为当*pa来到字符末尾的 \0 时此时 *pa 就表示0为假循环结束9.2、指针减指针首先指针减指针的值的绝对值得到的就是元素个数原理就是加减法的转换在 数组中指针-整数 指针其中的整数就是元素个数如图 p -5 start对上表达式移项就可以得到指针减指针的结论实现自己写一个函数来求字符串长度使用指针加整数的方法代码如下。输入字符串存在数组内其末尾自动加上 \0 所以还是用*p来判断while的循环条件//自己写函数使用指针加整数 #includestdio.h size_t my_strlen(char* p) { size_t count 0; while (*p) { count; p; } return count; } int main() { char arr[10] {0}; scanf(%s, arr); size_t ret my_strlen(arr); printf(%zu\n, ret); return 0; }使用指针减去指针的方式代码如下先将arr表示的首地址传给函数中的指针 p然后将指针 p 赋值给伴随变量 start用它锚定首地址然后再将p指针循环向后直到找到 \0 这个字符也就是字符串的结尾并将这个地址存在 p 中然后返回指针减指针就是元素个数//自己写函数使用指针减去指针 #includestdio.h size_t my_strlen(char* p) { char* start p;//存首地址作为参照 while (*p) { p; } return p - start; } int main() { char arr[10] { 0 }; scanf(%s, arr); size_t ret my_strlen(arr); printf(%zu\n, ret); return 0; }9.3、指针的运算关系指针就是地址编号地址实质上是十六进制的数也是可以比较大小的在一个整型数组中我们要打印数组的元素因为不是字符串末尾不是 \0 无法依靠指针去判断循环结束条件否则的话会一直访问下去直到越界为了解决这个问题我们可以限定指针的范围用 指针p首地址元素个数 作为循环结束条件就OK代码如下#includestdio.h int main() { int arr[] { 1,2,3,4,5,6,7,8,9,10 }; int* p arr; int sz sizeof(arr) / sizeof(arr[0]); while (p arr sz) { printf(%d , *p); p; } return 0; }深入理解指针(二)、1、const修饰变量与指针1.1、const修饰变量在C语言中const修饰变量达到的效果就是变量变成常变量如下代码#includestdio.h int main() { int const a 10;//或者const int a 10,这个前后无所谓 a 20; return 0; }在个代码中变量 a 被const修饰了过后是不能直接修改 a 的值在语法层面修改a的值就会报错但是还可以使用指针修改代码如下#includestdio.h int main() { int const a 10; int* p a; *p 20; return 0; }这样的修改是可以运行的1.2、const修饰指针如果我们想要将变量 a 实实在在的固定死那么就需要对它的指针也用const修饰如下代码修饰指针后变量a彻底无法修改了两行企图修改变量的代码都会报错#includestdio.h int main() { int const a 10; const int* p a; a 20; *p 20; return 0; }const在 * 左边如下代码const在*左边修饰的是 *p 也就是修饰指针p指向的元素此时指针p指向的元素无法被修改但是指针p本身是可以被修改的也就是指针指向a或者指向其他的变量这个是可以修改的#includestdio.h int main() { int const a 10; const int* p a; return 0; }const在 * 右边如下代码const在 * 右边时修饰的是指针p本身这个时候*p 并没有被修饰 * p 仍然可以修改但是指针永远都只能指向变量a不能指向其他变量#includestdio.h int main() { int const a 10; int* const p a; return 0; }综合理解吃凉皮的故事如下代码#includestdio.h int main() { int a 10; int b 100; int* p a; return 0; }上述代码中a就是男孩一号b是男孩二号他们分别有10元100元然后p是女孩*pa就表示他们现在是男女朋友关系。有一天女生看到凉皮想吃问男生一号可不可以给女生10元请女生吃凉皮也就是执行代码*p-10如果男生一号愿意那么这个代码可以执行如果男生不愿意男生用const修饰了 *p 女生就不能吃凉皮然后女生生气了凉皮都舍不得于是女生决定换男朋友执行代码p b;这个代码是可以执行的男生一号看到要分手觉得不行于是答应买凉皮但是不能换男朋友于是就将const修饰放在 * 右边也就是代码int* const pa;这样凉皮吃到了但是不能换男朋友了2、野指针野指针指针指向的空间不属于当前程序的指针就是野指针常见的野指针有访问越界指针指向的空间被释放还指针没有初始化如下三段代码都是标明野指针指针没有初始化#includestdio.h int main() { int a 10; int* pa; *pa 20; return 0; }指针越界访问代码如下当访问到第11个元素的时候就越界访问了不知道访问的是谁指针指向的空间被销毁#includestdio.h int* func() { int a 100; //…… return a; } int main() { int * pafunc(); printf(%d\n, *pa); return 0; }变量a是函数内的局部变量当函数结束变量a的生命周期也就结束了所以指针pa指向的空间里面可能是a的值也可能不是这取决于a的地址有没有被覆盖具体请看函数栈帧的创建与销毁3、如何规避野指针当我们明确知道指针指向哪里时要对指针初始化当我们不知道指针指向哪里是将指针初始化为NULL它表示空指针表示指针指向的空间为空当我们不再使用某一个指针的时候将这个指针设置为空当我们使用指针前要对指针验空验证它是不是空的代码如下if(pNULL) { //是空指针 }4、assert 断言在头文件 assert .h 中有assert( 表达式)语句当表达式为真代码直接运行不报错当表达式为假assert会告诉我们错误并且终止程序给出错误行号。如下代码assert报错他会告诉你什么表达式错误错误文件是什么以及错误的行号是多少assert的作用就是用来验证值的对于指针可以用来验空对于变量可以用来验证值是否是预期的值取消assert的方法就是定义一个宏 代码如下定义ndebug这样可以一键关闭所有assert函数否则的话需要手动注释太麻烦了#define NDEBUG;assert会影响运行时间在vs中只能在debug版本使用assertrelease版本会被禁止5、指针的使用和传址调用5.1、strlen 的模拟实现strlen求字符串长度现在我们要写一个my_strlen来模拟实现strlen的功能代码如下在此代码中加入了const修饰变量* str 为的就是只读取字符串长度但是不改变字符串内容加上const就是为了以防万一//模拟实现strlen #includestdio.h #includeassert.h size_t my_strlen(const char* str) { size_t count 0; assert(str ! 0); while (*str ! \0) { count; str; } return count; } int main() { char arr[] abcdef; printf(%zu\n, my_strlen(arr)); return 0; }同理加上assert就是为了检查指针是不是空如果传入空指针会有风险报错只要传入的指针不是空的就可以正常进行程序5.2、传值调用和传址调用写一个函数实现两个整数的交换给出错误代码如下。上图代码中变量a b 的值并没有交换原因就是函数内形参无法修改实参而实质是形参实参的地址不一样导致的并且交换两个变量的值你也不能说返回 X 和 Y 返回值只能有一个所以这样的传值调用是不行的使用传址调用代码如下#includestdio.h void swap(int* pa, int* pb) { int z 0; z *pa; *pa *pb; *pb z; } int main() { int a 10; int b 20; printf(交换前a%d b%d \n,a,b); swap(a, b); printf(交换后a%d b%d \n,a,b); return 0; }将a和b的地址传给形参这样修改形参时就可以改变实参