在初阶中我们学习了数据类型、变量常量、字符串转义字符、选择语句、循环语句、函数、数组、操作符、以及结构体的初阶介绍,在进阶的第一节中我们深入挖掘了一些数据在内存中的存储,每一种数据类型有符号和无符号的区别,各自能取到什么范围?(以char类型为例,signed char能取到的范围是-2^7---2^7-1,unsigned char能取到的范围是0---2^8-1,我们说的这两个范围都是拿出来以正常的方式拿出来,而且是存入的数据只能是这么大,如果太大会发生截断)数据怎样放入我们提前在内存中准备好的空间中?(这里有几个需要特别注意的:我们放的都是数据的补码,截断,例如将整形放入char类型中,大小端,小端的定义是将数据的低位放在低地址中,大端反之)当我们使用数据时如何取出来?(一定要看以什么形式取出的,比如以%d的形式取出char类型的数据就需要整型提升,而整型提升看的是char是否有符号补齐1/0),得到一个结论:一定要将数据放入合适的数据类型变量中,并且使用的时候一定要以合适的方式拿出来否则会发生错误。
指针这一部分我们之前也接触过,首先是地址的形成是由计算机底层的32/64根线形成的,也就是4/8个字节,共有2^32种可能,我们如何存储这个地址,用的就是指针变量,int*p=&a,&将变量的地址拿出来,放到指针变量p种,p前面的*表示它是一个指针变量,int表示它指向的是一个int类型的变量,当我们需要改变指针指向的变量内部的数据时就用*p解引用,这可以直接找到变量本身进行修改,指针的进阶:字符指针、数组指针、指针数组、数组传参和指针传参、函数指针、函数指针数组、指向函数指针数组的指针、回调函数。
字符指针:char*,可以char c=‘w’;char*p=&c,*p=‘c’,可以这样使用,另一种使用方式是char*p=“abcdef”,我们都知道内存分为三部分:栈区、堆区、静态区,常量字符串就放在静态区当中,而我们创建的指针变量p放在栈区中,p中放的是abcdef这个字符串首元素的地址,也就是‘a’的地址,常量字符串是不可以被修改的,因此更安全的写法为const char*p=“abcdef”;(const放在*左边表示指向的内容不可被修改,const放在*右边表示这个指针变量内存放的地址不能被修改,例如char*const p=&a,p=&c这就出错了),但是char arr【】=“abcdef”这就不一样了,这个本质是在栈区中开辟了一段空间用来存放数组,也就是将abcdef放在了栈区中,这完全可以修改。
指针数组:看名字可以看出来,这是一个数组,数组里面存放的全是指针,例如int arr1={1,2,3};int arr2={4,5,6};int arr3={7,8,9};int* p[3]={arr1,arr2,arr3};p[3]表示是一个有3个元素的数组,里面存放的元素是int*类型的,而数组名正好是首元素的地址,我们可以用这个指针数组来模拟一个二维数组,例如我们想找到arr2中的5,也就死*(*(p+1)+1)==p[1][1]。
数组指针:是一个指针,指向的是整个数组,而不是数组的首元素,首先说一下一维数组int arr【3】={1,2,3},arr和&arr的区别,arr是数组名,除了单独放在sizeof或&中代表的都是首元素的地址,而&arr表示的是整个数组的地址,两者数值可能相同,但+1的效果完全不同,前者跳过一个int,后者跳过整个数组,也就是3个int,那我们如何存放这个数组指针?int(*p)[3],这表示的是p是一个指针,指向的类型是int 【3】,数组的类型如何判断,其实任何一个变量,将变量名去掉后剩下的就是类型,int arr【3】去掉arr后剩下int 【3】就是整个数组的类型,回到前面指针变量指向的类型就是数组类型的变量。int(*p)[3]=&arr。在二维数组int arr[3][4]={0}中,数组名arr代表的是第一行的地址&arr表示的是整个二维数组的地址,我们一般将arr传入函数中,我们接受的时候就可以用数组指针来接收,int(*p)[4]=arr,arr表示的是第一行的地址,*arr表示的就是第一行数组中首元素的地址,**arr表示的就是arr[0][0]。
函数指针:函数也是有地址的,例如int Add(int x,int y),&Add完全等价于Add,可以将其放入函数指针中,int (*p)(int,int)这表示的是p是一个指针变量,指向的类型是int(int,int)也就是函数声明去掉了函数名,int (*p)(int,int)=Add,我们在调用函数的时候就可以直接用int a=p(2,3);a最后得到结果为5。有两段有意思的代码:( * ( void (*) () ) 0 ) ( ),这段代码从0开始分析,0前面的( void (*) () )是一个函数指针类型,再强调一遍,去掉了变量名剩下的就是类型,这是将0强制类型转换为一个函数指针类型的数据,也就是函数的地址并进行解引用和函数使用。 void(*signal( int , void(*)(int) ) )(int),signal是一个指针,指向的是一个函数,函数的返回类型是函数指针类型void(*)(int),函数的参数是(int,void(*)(int))类型。
函数指针的应用:一个是函数指针数组中用作转换表(优点是可以应用于大量的switch case语句中,缩短了代码长度);另一个是回调函数(优点是可以避免冗余的发生,因为我们往往是因为需要实现不同功能才写的冗余代码,例如写一个计算器,使用者选择12分别为加和减,我们可以用转换表来实现,也可以用回调函数,写一个计算器函数,函数的参数有一个是函数指针,我们其他功能(读取使用者的选择和传入的两个数字)不变,唯一改变的是实现加法还是减法,我们可以通过计算器函数中的函数指针调用Add或Sub函数来达到在一个函数中实现多个不同功能的目的,再总结一下,函数指针的应用有两种,转换表(可以通过下标来调用函数,将函数放在数组中减少了代码长度),回调函数(可以在同一个函数中实现不同的功能,从而达到减少冗余的目的))。回调函数有一个很经典的例子:我们在实现排序的时候一般采用的是冒泡排序的方式,冒泡排序的本质是数据之间两两比较,我们写出的bubble_sort函数只能用于一种数据类型的排序中,那我们如何实现不管什么数据类型的数组我们都可以对其进行排序?我们需要改变的就是数据两两比较的方式和交换的方式这两部分,qsort函数可以实现,当我们模拟实现qsort函数的时候我们看到,不管什么类型的数组我们都用void类型的指针去接收,但是要传入每个单元的大小和整个数组的元素个数,我们才能实现数据之间两两比较,那么我们如何比较不同的数据呢?我们可以找到这两个数据的地址,但是每一种数据的比较方式都是不同的,整形是看大小,字符是比较ascii码值,因此我们比较方式各不相同,我们就需要在这一个qsort函数中实现不同的功能,就需要函数指针的应用回调函数,我们在使用qsort之前首先要给qsort比较的方式cmp并将其传进去,这样才能比较两个数据的大小,若需要交换我们要进行交换,如何交换?我们能找到两个数据的首字节的地址,也就是用char*一个字节一个字节去找,找到后根据数据元素的大小来进行字节间的交换。