这里写目录标题
一.为什么会有指针
我们把计算机的内存想象成我们大学的一个大型的宿舍楼,但是这个宿舍楼是没有门牌号的,然后我就是宿管,有一天我想找一个人这这个人叫张三,那么如果没有门牌号的的话,我是不是就得一个房间一个房间的找张三这个人,于是我过了好久才找到这个,于是上面的领导又要我找李四这个人,那么我就又得重头开始找李四这个人,等我废了九牛二虎之力找到李四之后,上面的领导又要我找叶超凡这个人,那么这个宿舍管理员是不是就又得从头开始一间宿舍一间宿舍的找叶超凡这个人,那么看到这里想必大家发现了一个问题就是宿舍要是没有门牌号的话,找一个人是不是就很难啊,又费时间又费力,那么我们要是有门牌号呢?比如说我要找张三这个人在401号寝室,那么我是不是就可以直接去那个寝室找这个人,无需再一间一间房间的查找了,那么我们计算机中的内存也是这样我们把每个内存单元添加上对应的编号,等计算机想要访问或者修改这个内存单元中的内容时就可以直接根据这个编号去查找无需一个一个的访问这样是不是大大的增长了我们的计算机的运行速度,那么这个编号又是如何来产生的呢?就拿32位的电脑举个例子,32位的电脑里面也就存在着32根地址线,而这些地址线通电就会产生电脉冲,那么高电压就是1,低电压就是0,那么一共有多少种情况呢?那是不是就是00000…00000(一共32个)到11111…111111(一共32个)这么多个地址,那是不是就是一共有2的32次方个地址编号,那我们再来看这些地址编号可以用来标记一个内存单元,那我们的内存单元的大小又是多少呢?我们先假设一个内存单元的大小是1bit,那我们来看这个内存的大小是多少:2的32次方等于4294967296,那内存也就是4294967296bit=536870912byte=524288kb=512mb=0.5gb那我们看这个假设是不是太小了,那我们再来看,如果一个内存单元是一个字节的话,那我们以此内推的话是不是也就是4gb,那我们这么看的话,我们就发现这个大小就十分的合理。所以我们的一个内存单元的大小就是一个字节。好看到这里想必大家已经有了初步的了解,但是这里出现了一个问题就是我们这里的数字太多了啊我们不可能每次写地址的时候都写32个数字吧,所以这里的地址我们就采用16进制的写法我们的10进制是0,1,2,3,4,5,6,7,8,9所以我们的16进制的是0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g这里的a就代表了10后面就依次类推。所以4个2进制的数字就代表了1个16进制的数字所以我们32个2进制的数字是不是就可以用8个16进制的数字进行代替,那我们的地址的编号的书写是不是就方便了很多比如说0x00000001这就用看起来方便了多,要是用2进制那是不是就很难看啊这里就不演示了大家 注意了这里的0x表示的是16进制.既然我们知道了地址是如何产生的,那么我们创建出来了一个变量是不是就相当于在内存中占据了一定的空间,我们前面也说了我们可以通过取地址操作符( & )来得到这个变量的地址,那么是不是就得将这个地址存起来,我们知道变量是可以存储值的,那么我们把存储地址的变量就称为指针变量,但是我们平时口头上就将其称为指针。好看到这里想必大家应该能够知道现在的一个逻辑就是我们的一个指针就对应了一个内存单元,我们一个内存单元的大小就是一个字节,我们把这个地址存到一个变量里面,那么我们就称这个变量为指针变量,这个指针变量在32位的机器下是4个字节的大小,在64位字节的机器下是8个字节的大小,因为我们4个字节就是32个比特位刚好能够将这个地址全部存下去,而8个字节就是64个比特位也就刚好能将64位的地址存下去,那么这里我们来看一下下面的代码:
#include<stdio.h>
int main()
{
int a = 10;
double b = 10.0;
printf("a的地址为:%p\n", &a);
printf("b的地址为:%p\n", &b);
return 0;
}
我们来看一下运行的结果:
啊我们发现有个很奇怪的现象就是我们这里就打印两个变量的地址,啊这里有小伙伴们要说了啊有啥奇怪的啊,两个变量对应了两个地址没毛病啊,但是这里大家想过一个问题没有就是我们这里int变量的a是占有4个字节的,我们这里的变量b是有八个字节,那么也就是说我们这里的a应该是有4个字节,b是有八个字节才对,那么这里为什么只各有一个字节呢?原因是我们这里的取地址取的是这个这个变量的首地址,啊这个首地址就是较小一点的地址,比如说我们这里的变量a它占有4个字节,那么这4个字节的地址就应该分别是00EFD64,00EFD65,00EFD66,00EFD67,那么我们取地址就对应的就是这四个当中最小的那个00EFD64,原因也很简单我们的内存的使用是从高地址往低地址使用的,那么这里我们就解释清楚了指针是什么这个问题,那么我们接下来就来讲讲其他的概念。
二.指针和指针的类型
我们将不同类型的变量的地址取出来,然后放到指针里面,那么我们这里就会对应有不同的指针类型来接收这地址,比如说下面的例子:
#include<stdio.h>
int main()
{
int a = 10;
double b = 10.0;
char c = '0';
int* pa = &a;
double* pb = &b;
char* pc = &c;
return 0;
}
我们这里创建了三个不同类型的变量,但是我们这里也得创建三个不同类型的指针来接收这些地址这时为啥列,既然都是地址而且大小还是一样的那么我们这里为什么不用同一类型的变量来进行接收呢?那么带着这个问题我们来看看下面的内容。
1.指针加减整数
我们将上面的代码添加一点内容:
#include<stdio.h>
int main()
{
int a = 10;
double b = 10.0;
char c = '0';
int* pa = &a;
double* pb = &b;
char* pc = &c;
printf("pa的值为:%p\n", pa);
printf("pa+1的值为:%p\n", pa+1);
printf("pb的值为:%p\n", pb);
printf("pb+1的值为:%p\n", pb+1);
printf("pc的值为:%p\n", pc);
printf("pc+1的值为:%p\n", pc+1);
return 0;
}
我们来看看这段代码的运行结果:
我们发现我们对不同的类型的指针的值+1产生的效果却完全不一样,而且我们发现对int类型的指针+1它的值会加4,对char类型的指针+1他的值会加1,对double类型的指针+1,那么他的值就会加8,所以我们这里就有一个规律就是对不同类型的指针加1他就会跳过对应类型字节大小的地址,你int类型的指针那么加一就会跳过4个字节,你要是double*的指针那么就会跳过8个字节,其他的类型就以此类推,那么这里我们就总结一下:指针的类型决定了指针向前或者向后走一步的大小是多大。
2.指针的解引用
不同的类型的指针不仅仅是在加减整数的时候会有一些区别,在解引用的时候也会有那么一点点的不同,我们指针的类型并不会决定指针变量的大小,但是他能够决定我们的指针变量解引用时能够访问多少个字节,这句话是什么意思呢?我们通过下面的代码来理解:
#include<stdio.h>
int main()
{
int a = 0x11223344;
char* pa = (char*)&a;
int* p = &a;
*pa = 0;
*p = 0;
return 0;
}
首先我们创建一个变量a,并且将其初始化为十六进制的11223344,然后我们创建一个char类型的指针变量pa并且将a的地址强制类型转换为char*类型赋值给这个pa,然后我们再创建一个int*类型的指针变量并且将a的地址赋值给这个指针变量p,那么接下来我们就将这两个指针变量分别进行解引用将其值赋值为0,会发生什么呢?我们通过内存来看看这中间发生的过程:
我们可以看到首先将a的值赋值为0x11223344,我们内存这里是小段储存所以这里显示的是44332211,那么我们接着往下走将*pa的值赋值为0我们来看看会发生什么?
我们发现原来的44变成了00,但是其他的位不动,这是为什么呢?因为我们这里pa的类型是char 类型而char类型能够解引用的大小是1个字节,所以我们这里通过pa来改变a的值话,是只能改变一个字节的,而这个改变的位置就是我们这里通过&a取出来的位置,那么我们再将a的地址赋值给一个int*类型的指针变量,通过这个变量将其赋值为0又会出现什么样的现象呢?按我们上面说的话就是会将这四个字节的内容全部都初始化为0,那我们接着往下执行看看
啊确实是将这4个字节的内容全部都初始化为0,那么我们这里就可以确定这个结论就是不同类型的指针变量还能够决定我们指针在解引用的时候能够访问多少字节,你是int类型的指针你解引用一下子就能访问int个大小的字节,你是double类型的指针那么你解引用一下子就能访问double个大小的字节,那么这时候有小伙伴们就要问了int类型的大小是4个字节,float的大小也是4个字节,那么我们以后在使用的时候是不是可以将这个int和float进行替换啊,你看你int加一或者减一跳过的四个字节我float也是,你int一次接应用访问的是四个字节,巧啦我float*也是的那么这里到底能不能够进行替换呢?答案是不可以,我们看下面的代码就知道了:
#include<stdio.h>
int main()
{
int a = 100;
printf("%x\n", a);
float b = 100.0f;
int* p = (int*)&b;
printf("%x", *p);
return 0;
}
我们通过监视来看看内存当中分部的情况是什么,我们首先看a
这是我们变量a在内存当中存储的值,因为这里是小端存储,所以正常的显示情况应该是00 00 00 64
因为是16进制所以我们这里将其转化为10进制就值6*161+4*160结果等于100是正常的,那么我们再来看看我们的浮点数类型的b在内存当中对应的情况是怎么样的:
结果是00 00 c8 42,那么这里我们就发现了一个问题就是我们这里两个不同类型的变量存储同一个大小的值结果在内存当中对应着两种不同的情况,这是为什么呢?这是因为我们的浮点数的储存方式与我们整型的储存方式是不同的,这就会造成在内存当中显示的情况不一样,既然我们存储的时候规则就不一样,那我们解引用往外面拿的时候,对应的规则是不是也不一样啊,那么我们再回到之前的那个问题,我们的float和int变量解引用时都会提取四个字节大小的空间,那么他们能相互替换吗?答案肯定是不能的,因为不同类型的指针变量不仅决定着解引用时能够提取的空间的内容是多大,还可以决定着我们以何种方式将这些内容提取出来,那么这里想必大家对指针的解引用有了一定的了解,那么我们接着往下看。
三.野指针
我们首先来看看野指针的概念是什么:野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
①野指针的成因
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;
int i = 0;
for(i=0;i<=11;i++)
{
*(p++) = 0;
}
return 0;
}
我们可以运行一下就可以发现:
程序会报错,因为我们这里的p他越界访问了,当我们的指针指向的范围超过了数组arr范围的时候,我们的指针p就是野指针,所以我们这里就会报错。
3.指针指向的空间释放
啊这个原因如果想理解的更清楚的话,得等到我们说到动态内存管理会更好的理解,那么我们这里的话可以通过下面的代码来进行理解:
#include<stdio.h>
int* chaofan()
{
int a = 10;
return &a;
}
int main()
{
int* p = chaofan();
*p = 10;
printf("%d", *p);
return 0;
}
我们这里调用了一个函数,我们在函数里面创建了一个变量a,然后就将这个变量a的地址作为返回值传回我们的主函数并且我们这里又创建一个指针变量p来进行接收,那么这里我们就存在一个问题,我们这里返回来的地址是我们在函数里面创建变量时的地址,但是这个变量会随着我们函数调用的结束而自动进行销毁,那个地址对应的空间也就会还给我们的操作系统,那么这里我们把这个地址传给我们的主函数并且接收的话,是不是就有点不合理啊,就好比你和你的女朋友分手了你又找到了一个新的女朋友但是你还留着前任的各种照片电话已经家庭住址是不是有点不大合适呢?所以我们就把这种指针指向的空间释放的指针称为野指针,我们可以将这个函数打印出来看看:
我们发现这个指针指向的内容依然为10,那么这时候有小伙伴们就要说了,这有什么有要紧的啊,我们打印出来的结果不还是10嘛,这野不野指针打印出来的效果不是一样的吗?那么这里之所以打印出来的值依然是10的原因是因为我们下面没有执行新的函数,所以我们之前的那片内存空间并没有被新的内容所覆盖,所以他会依然保留之前的函数调用留下来的数据,我们可以在打印这个地址的内容之前随便加上一个函数再来打印的话这个值的结果就会有所改变我们把这个代码进行一下修改:
#include<stdio.h>
int* chaofan()
{
int a = 10;
return &a;
}
int main()
{
int* p = chaofan();
*p = 10;
printf("hehe\n");
printf("%d", *p);
return 0;
}
我们来看看运行的结果:
由之前的10变成了5,所以啊如果有小伙伴们想投机取巧使用这种野指针里面的内容的话你就会发现随着不断的有新函数的调用他的值就会变得越来越五花八门,所以我们要避免执政指向的空间释放。
②如何规避野指针
1.对指针进行初始化
这个很好理解我们创建一个指针的时候可以先将我们创建好的一些变量的地址赋值给我们的指针,这样就可以使得我们的指针指向的位置是固定的不是随机的,那么这时候有小伙伴说啊要是我没有变量的地址来给他进行初始化那该这么办呢?这里我们可以先将这个指针置为空(NULL)缓缓燃眉之急,因为我们的NULL对应的地址是是0地址,这个地址是不允许访问的所以我们只用等之后有合适的变量再来把这个变量的地址赋值给这个指针就可以了,就像这样:
#include<stdio.h>
int main()
{
int a = 0;
int* p = &a;
*p = 10;
int* pb = NULL;
return 0;
}
2.小心指针越界
啊这个就提了很多次了嘛,我们数组的下标是从0到数组的元素的个数减一,不要以为是从0到数组的元素的个数哈。
3.指针指向的空间释放即时置为空
这里得等到学了动态内存开辟能够理解的更加的清楚,就是我们可以一些函数开辟一个动态的内存,我们将这个动态的内存的起始位置赋值给我们的一个指针变量,然而我们这个动态的内存在不用的时候是可以被我们人为的还给我们的操作系统的,但是还给了操作系统内存不属于我们这个程序里,但是我们那个指针变量依然记录了那个之前开辟的那个内存的地址,这就会照成一些麻烦出来,所以我们在释放这个内存之后得将这个指针的内容变成空指针这样就找不到原来开辟的那块空间了这样的话就不会造成一些麻烦,这就好比你找了一个新的女朋友,那个新女朋友怕你想前任直接一棍子把你敲失忆了,这样你就彻彻底底的忘记了她,我们这里的置为空指针就是这样的效果。
4,避免返回局部变量的地址
这个就很好理解了,对吧我们在调用函数的时候前往不要把在函数里面创建的变量的地址作为返回值传回主函数就可以避免遇到野指针。
5.指针使用之前检查有效性
我们上面如果你不知道将这个指针变量初始化为什么值的时候,你可以将这个指针的内容初始化为空指针(NULL),但是这个空指针也是不能访问的,这就好比我知道这野狗喜欢咬人我就将他栓到一棵树旁边避免它到处咬人,但是你非要跑到这颗树旁边撒尿这不是就吃饱了撑着嘛,所以我们这里就可以这么做,在使用这个指针之前检查指针的有效性,那么怎么进行检验呢?我们就可以用if语句看看它是不是空指针,我们来看看下面的代码就知道如何来检验了:
#include<stdio.h>
int main()
{
int* p = NULL;
int a = 0;
p = &a;
if (p != NULL)
{
*p = 20;
}
return 0;
}
我们在使用之前先用先用if语句判断一下这个指针里面的内容是否为空就可以有效的避免我们解引用一个空指针而报错
四.指针的运算
1.指针加减整数
这个内容想必看到这里因该已经非常的熟悉了吧,这里就不多赘述了,我们这里直接通过一个例子来看一下巩固一下:
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
*p++ = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
我们可以通过这个指针++得到每一个数组中的元素,将他依次赋值,所以我们这里的指针加一并不是将在地址的数值上的大小单纯的加一,而是跳过一个整型大小的数据,所以我们这里记住了,我们这里的指针加减整数得到的是跳过整数个该类型大小的数据的地址,具体是多少得取决于数据的类型
2.指针-指针
指针减指针得到的结果是什么呢?我们知道指针是地址,那么这里地址减地址得到的难道是这两个地址大小的差吗?显然不可能对吧这地址差要的有啥用啊对吧,那么我们可以写这么一段代码来看看这指针-指针得到的结果是什么:
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int ret = &arr[9] - arr;
printf("%d", ret);
return 0;
}
我们这里将数组第十个的坐标减去数组中第一个元素的下标得到的结果竟然是9这是为什么呢?因为我们指针减去指针得到的是这两个坐标之间的元素个数,所以我们这里得到的结果就是九,但是这里有小伙伴要说了为啥不是10呢。从0到9的元素应该是10个元素啊,这里为啥是九呢?我们画个图就知道了
来认为是十个元素的小伙伴数数这两个地址中有多少个格子啊,很明显是九个嘛!那么这里大家要注意的一点就是我们这里的指针减去指针得是两个指向同一块空间的指针才能进行相减,不能一个指向字符串数组的一个指针减去一个整型数组的指针,问这个相减得到的结果是什么?啊这是没有意义的哈,大家注意一下 ,那么大家知道了这个内容有没有想过我们之前实现的strlen函数,我们用循环和递归的两种方式实现的了这个函数,那么今天这里是不是就有了第三种方式采用指针减去指针的方式来实现,那么具体的实现的方式如下:
#include<stdio.h>
#include<assert.h>
int my_strlen( char* arr)
{
assert(arr);
char* p = arr;
while (*p != '\0')
{
p++;
}
return p - arr;
}
int main()
{
char arr[20] = "abcdefghijk";
int len = my_strlen(arr);
printf("这个字符数组的长度为:%d", len);
return 0;
}
那么看到这里想必大家对指针-指针应该能够很好的理解,那么我们接着往下看。
3.指针的关系运算
这个具体是啥意思呢?就是我们的指针其实也是可以用来比较大小的,比如说我们创建一个整型的数组里面有10个元素,那么我们第十个元素的地址就比我们第一个元素的地址大,那么我们就可以该性质来进行一些操作比如说我想将一个有10个整型元素的数组的他的内容初始化为0,那我们就可以这么做:
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = NULL;
for (p = &arr[10]; p > &arr[0];)
{
*--p = 0;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d", arr[i]);
}
return 0;
}
当然我们这里还可以这么进行比较:
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = NULL;
for (p = &arr[9]; p >= &arr[0];p--)
{
*p = 0;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d", arr[i]);
}
return 0;
}
这么写的话我们的理解条件就更低一些,但是这也同样会产生另一个问题就是我们这里的最后一次循环我们的p已经指向了第一个元素的地址了,由于我们循环的结束我们就会来到循环的调整部分,而这时的p就会指向数组的前面的一个地址,那么我们再来拿这个地址与&arr[0]来进行比较的话就会出现一个问题就是我们的标准规定允许指向数组元素的指针与指向数组最后一个元素的后面的那个内存位置的指针进行比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。虽然我们的标准是这么规定的,但是上面的那个代码在绝大多数的编译器下都可以使用,但是我以后还是得避免这样写代码,因为标准并没有保证他可行。
五.指针和数组
看到这个内容想必大家对这个已经很熟悉了把,我们数组名就是数组首元素的地址,但是这里有两个除外就是第一个就是取地址数组名( &数组名)那么这个数组名就不再代表首元素的地址,而是表示整个数组的地址,第二个就是sizeof(数组名)这个数组名表示的也不是首元素的地址,而是整个数组。那么既然我们的数组名表示的是数组首元素的地址,那么我们是不是可以将这个数组名赋值给我们的一个指针然后通过这个指针来访问我们数组的元素呢?答案是可以的我们首先创建一个10个元素的数组,将数组的元素初始化为1,2,3…,8, 9, 10,然后再创建一个指针变量来接收这个数组名也就是首元素的地址,再通过for循环对这个指针变量加不同大小的数也就得到数组当中的每个数据最后将其一个一个的打印出来,那么我们的代码如下:
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
通过这个代码我们就发现一个现象就是我们的指针其实是可以与我们的数组进行相互交换的,我们既可以用数组的形式来打印我们数组的每个元素,我们还可以通过指针的形式来打印我们数组中的每个元素,那么我们数组中的下标引用操作符是不是就可以和我们的指针解引用操作符进行相互替换呢?答案是确实是可以的也就是说arr[1]的作用与*(arr+1)是一模一样的我们在写的时候哪个你看的舒服看的直接你就选择用哪种方法。
六.二级指针
什么是二级指针,我们说一级指针是用来储存一个变量的地址的,那二级指针呢?难道是用来储存地址的地址的吗?啊当然不是的哪有地址的地址这个说法啊,我们的二级指针是用来存储一级指针变量的地址的,那么这二级指针变量的类型是什么样的呢?我们可以看一下下面的例子:我们创建一个整型的变量a,将a的地址放到一个整型的指针变量里面近进行储存,虽然是指针变量但是他总归来说还是一个变量,他也有对应的地址,那么我们就再通过取地址操作符将这个指针变量的地址取出来放到一个二级的整型的指针变量里面去,那么这个变量的类型就是int **我们来看一下完整的代码:
#include<stdio.h>
int main()
{
int a = 0;
int* p = &a;
int** pa = &p;
return 0;
}
那么这里因为是二级指针所以有两个*,那么这两个*有着不一样的意思,右边的一个表示这个变量里面装的是地址,而左边的那个则和前面的int一起表示的是这个地址指向的那个空间的内容的类型是int*型,所以我们这里的两个*各代表着不同的意思,大家可以扩展到三级指针来理解理解,那么我们地址都取好了,那么我们该如何来使用这个二级指针呢?既然我们的一级指针得解引用一次,那么我们的二级指针就得解引用两次,我们下面就通过这个二级指针将我们变量a的值改成20,并且打印出来:
#include<stdio.h>
int main()
{
int a = 0;
int* p = &a;
int** pa = &p;
**pa = 20;
printf("%d", **pa);
return 0;
}
其实我们的二级指针还可以以另一种形式来表示我们的二维数组,我们知道二维数组可以表示成一个一维数组但是一维数组中的每个元素却又是一个数组,那么我们这里就可以这么想我们先创建三个一维数组,因为数组名是首元素的地址。那么我们就可以将这三个一维数组的数组名放到一个数组里面去,再构建出一个一维数组,那么这个数组里面装的就是我们的三个数组的三个首元素的地址,那么这个数组名又表示什么呢?还是首元素的地址,而我们的首元素也是一个地址,所以我们这里的数组名就是一个二级指针,那我们先将上面的内容用代码实现:
#include<stdio.h>
int main()
{
int arr1[3] = { 1,2,3 };
int arr2[3] = { 4,5,6 };
int arr3[3] = { 7,8,9 };
int* arr[3] = { arr1,arr2,arr3 };
int** p = arr;
return 0;
}
那么我们如何用二级指针来模仿我们这个二级数组呢?好既然他是一个指针并且还是一个二级指针,这个p指向的是int * arr[ 3 ]的首元素的地址,他的首元素就arr1这个数组,那么我们二级指针p进行解引用就会得到我们这整个arr1数组,也就是我们说的数组名,那么数组名arr1,又是arr1数组里面的首元素的地址,那么我们再对其进行解引用就可以得到这个arr1数组里面的内容,那么我们这里是得到arr1里面的内容,那我们如何得到arr2里面的内容呢?我们这里的数组名表示的是首元素的地址,那么我们这里的将这个地址加一是不是就得到第二个元素的地址了,那么再跟上面的操作一样是不是就可以得到arr2里面的每个元素了,那么我们的代码如下:
#include<stdio.h>
int main()
{
int arr1[3] = { 1,2,3 };
int arr2[3] = { 4,5,6 };
int arr3[3] = { 7,8,9 };
int* arr[3] = { arr1,arr2,arr3 };
int** p = arr;
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 3;j++)
{
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
return 0;
}
七.字符指针
我们先来看看什么是字符指针,我们根据这么形象的名字就知道这个东西是个指针,这个指针指向的内容是字符,比如说下面这样:
#include<stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'h';
printf("%c", ch);
return 0;
}
我们创建了一个字符变量,将他的值赋值为字符’ w ',让后我们再将这个变量的地址取出来放到我们创建好的字符指针变量pc里面去,再通过解引用pc来改变我们的这个变量力面的值,再将我们变量ch里面的内容打印出来就可以发现我们这里的ch的值确实发生了修改,说明当我们一个变量里面就放的是一个字符的话我们就可以通过地址来修改这个字符变量里面的值,但是这里想必大家肯定讲过另外一种情况就是我们一个字符变量里面装了一个字符串,比如说下面这个情况:
#include<stdio.h>
int main()
{
const char* pstr = "hello world";
printf("%s", pstr);
return 0;
}
我们上面这个字符变量里面就只装了一个字符,那这里为什么我一个字符变量里面能够装下一个字符串呢?难道这里pstr变成了一个数组了吗?但是按照逻辑来说这里应该是一个指针才对啊,怎么会变成一个数组了呢?所以这种猜想很明显就是错的,那么这里我们所创建的字符指针变量里面放的是我们这个字符串的首元素吧的地址,并不是整个字符串,因为我们这里的字符串他在内存当中是连续存放的,所以我们就可以根据这里的首元素的地址来找到其他元素的地址从而找到这个地址里面对应的内容,比如说这样:
#include<stdio.h>
int main()
{
const char* pstr = "hello world";
int i = 0;
for (i = 0; i <=10; i++)
{
printf("%c", *(pstr + i));
}
return 0;
}
我们就可以通过指针的方式来一个一个访问我们,那么我们之前说过其实我们的[ ]下标应用操作符在某些情况下其实是可以跟我们的指针相互转换的,那么我们这里试一下看能不能行
#include<stdio.h>
int main()
{
const char* pstr = "hello world";
int i = 0;
for (i = 0; i <= 10; i++)
{
printf("%c", pstr[i]);
}
return 0;
}
我们跑一下就知道,答案是完全是可以的,那么这里可能会有仔细的小伙伴们发现了这里的字符指针前面为什么要加一个const啊?那么这里原因很简单因为我们这里的初始化其实是将常量字符串放到外面的自读数据区里面的,这个区域里面的内容是不允许修改的,只允许读取,所以我们这里就这个指针变量的前面加上const来修饰表示不要想着通过指针的解引用来修改这个字符串里面的内容,所以我们这里也就相当于一个保护机制,那么看到这里我们来看一个面试题:
#include<stdio.h>
int main()
{
char str1[] = "hello world.";
char str2[] = "hello world.";
const char* str3 = "hello world.";
const char* str4 = "hello world.";
if (str1 == str2)
{
printf("str1 and str2 are same\n");
}
else
{
printf("str1 and str2 are not same\n");
}
if (str3 == str4)
{
printf("str3 and str4 are same\n");
}
else
{
printf("str3 and str4 are not same\n");
}
return 0;
}
我们来看看这个打印出来的结果是什么,我们首先看到第一个if…else语句他问我们str1和str2的值是否相等,首先我们要知道的是这里的str1和str2是两个数组的数组吧名,也就是首元素的地址,那么我们这里是创建了两个不同的数组,那么我们这里肯定是不相同的啊,有同学就有疑问了说这里两个数组里面的内容都是一样的,因该地址是不相同的啊,那么这里我就要问同学一句,我们这里的内容目前确实是相同的,但是你能确保以后都相同吗,万一我下面要将这两个数组的内容进行修改的话,那么你觉得这两个地址相同合适吗?肯定不合适啊,就好比警察抓小偷你因为和小偷长的特别像就被警擦抓了起来,你会感到公平吗?显然是不公平的嘛对吧虽然我和小偷长的一样但是我们的性格血型基因等等方面都不一样吗凭什么说我是小偷呢?那么我们这里的数组也是一样的到,我们这里是向操作系统申请了两个不同的空间所以我们的地址不一样,所以就会执行下面的对应的else语句,那么我们这里再来看看下面一个的if语句,这里问我们str3与str4的是否相同,好那么这里我们先搞清楚一件事就是我们这里的str3和str4表示的是什么?是地址吗?不是!是这个str3和str4里面的内容,那么既然我们这里if语句比较的就是这两个变量里面的内容是否相同,内容是什么呢?是地址!那么有小伙伴们看到上面的例子就要说了,啊这里肯定是不相同的啊,我们这里会申请两个空间怎么怎么样所以这里面装的地址是不相同的,如果你要是这么想的话,那就错了我们仔细看看这两个变量里面放的是谁的地址?是我们的字符串的首元素的地址,这个字符串放的地方是哪里呢?是我们的只读数据区,那么这个数据区的特点是什么呢?只能读取数据不能修改数据,所以我们这里两个字符指针放的是内容相同的字符串的首元素的地址,那么你觉得我们这里有必要在只读数据区里面申请两个不同的地址来装这同一个字符串吗?答案是完全没必要所以我们这里虽然是创建了两个不同的字符指针变量,但是这两个变量里面放的值是相同的,那么我们最好来做一个对比,我们上面str1和str2为什么不同是因为我们这里的两个数组里面的内容可能会在后面的代码中可以修改成其他的值,所以这里得申请两个不同地址,而我们下面的str3和str4这里面装的是常量字符串放到了我们的只读数据区当中以后不会修改里面的值,所以我们这里就没必要申请两个不同的空间来装相同的字符串,对吧反正内容不会修改,所以我们这段代码运行的结果就是:
好看到这里我们的字符指针就结束了我们接着往下看。
八.指针数组
看到这个标题首先我想问大家一个问题就是我们这个是指针数组说的是指针还是数组啊?嗯?是不是有点迷惑啊,那么我再问你一个问题:好孩子这个词说的对象是孩子还是好啊,当然是孩子这个对象嘛,那么我们这里也是一样的,我们这里的指针数组说的对象是数组嘛,只不过这个数组里面装的内容是指针而已,那么我们如何来创建一个指针数组呢?我们来看看下面的例子:
#include<stdio.h>
int main()
{
int* arr1[10];
char* arr2[4];
char** arr3[5];
return 0;
}
我们这个代码当中就创建了三个字符指针,那么我们如何来理解这个代码呢?首先我们的变量名会跟[]先结合,这样就便是这个变量名表示的是数组,然后[ ]里面的数字则表示的是这个数组中的元素个数,那么这里数组名前面的就表示的是数组中元素的类型,比如说arr1前面的就表示这个数组中的每个元素的类型是int *型,也就是一个指向整型的指针,那么我们下面的arr2也是同样的道理表示数组的每个元素是一个指向字符的指针,我们的arr3则与上面的两个有点点不同他表示的意思这个数组中的每个元素的类型是个二级指针,这个二级指针指向的对象是一个char类型的指针,这个char类型的指针指向的对象又是一个char类型变量,那么看到这里想必大家应该对这个指针数组十分的了解了,那么我们接下来就来看看数组指针。
九.数组指针
首先我们还是得先搞明白一件事情就是我们这个数组指针是什么?根据上面的内容我们知道这数组指针他是一个指针,这个指针指向的对象是一个数组,那么我们如何来描述这个指针呢?我们来看看下面两个代码:
#include<stdio.h>
int main()
{
int* p1[10];
int (*p2)[10];
return 0;
}
大家觉得哪个是数组指针呢?因为我们上面才学了指针数组,所以我们这里很明显是第二个表示的数组指针,那么有小伙伴就感觉有点疑惑啊,为啥得加个括号呢?这时因为我们[ ]的优先级大于我们的*的优先级,也就是说如果我们不加括号的话这个这个变量名就会先跟这里的[ ]先结合这样的话就变成一个数组了,并不是指针所以我们就把变量名和*放到一个括号里面这样的话我们的变量就会和我们的*先结合这样就说明我们这里的p2是一个指针变量,我们将这里的*和变量名去掉剩下的就是这个指针指向的内容,我们上面的p2就表示这个指针指向的是一个数组,该数组有10个元素,每个元素的类型是int类型,看到这里想必大家应该能够理解数组指针是如何进行定义的了,那么我们接着往下看
十.数组指针的使用
我们首先来看一段代码:
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int(*p)[10] = &arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", *(*p + i));
}
return 0;
}
我们创建了一个整型的数组,然后我们将这个数组的地址放到我们的一个数组指针里面去,然后我们就想通过这个数组指针来打印我们的这个数组中的每个元素,那该怎么做呢?首先我们我们明白的一点就是这个指针指向的对象是一整个数组,那么我们对这个这个指向数组的指针进行解引用的话得到的是什么呢?应该是整个数组对吧,那整个数组又表示的是什么呢?就是我们这里的数组名,换句话说我们这里对数组指针解引用得到的就是数组名,而数组名又是首元素的地址,所以我们要想访问这个数组中的每个元素是不是就得对这个数组指针解引用之后加上常数来得到其他元素的地址,再对其解引用是不是就可以得到这个数组中的每个元素了,所以我们这里的代码形式就是*(*p + i)
希望大家能够理解,但是大家看到这个有没有发现这样写很奇怪啊,我们平时写的形式都是这样的:
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
我们一般都是将数组的首元素的地址放到一个对应类型的指针变量里面再对其进行加减整数解引用这样也可以得到我们数组当中的每个元素,这样做的话我们过程就简便的多,而且上面的那个数组指针有点点多此一举的感觉对吧,那么我们这里我们举了一个错误的做法给大家,实际上我们的数组指针真正的用法应该是在传递二维数组的时候使用,我们之前说我们在传递数组的时候可以使用数组来进行接收,比如说下面的代码我们创建了一个二维数组,那么我们把这个二维数组作为参数往函数里面传的时候,就可以以数组的形式来进行接收就比如说这样:
#include<stdio.h>
void printf_arr1(int arr[3][3], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
}
}
int main()
{
int arr[3][3] = { 1,2,3,4,5,6,7,8,9 };
printf_arr1(arr, 3, 3);
return 0;
}
但是我们还知道一点就是我们这个传的是arr就是我们所谓的数组,数组名是一个地址,所以我们这里不仅仅可以用数组来进行接收还可以使用指针的形式来进行接收,那么这个指针的类型是什么呢?我们知道这个数组名是一个地址,他表示的是首元素的地址,但是我们这里是一个二维数组啊,那么这里的首元素表示的就是我们二维数组当中的第一个数组的地址,也就是一个一维数组的地址,那么这里传过来了一个数组的地址,我们是不是就得用数组指针来进行接收啊,所以我们这里就可以这么写:
#include<stdio.h>
void printf_arr2(int (* arr)[3], int row, int col)
{
}
int main()
{
int arr[3][3] = { 1,2,3,4,5,6,7,8,9 };
printf_arr2(arr, 3, 3);
return 0;
}
既然这里我们再来完成通过这个数组的指针来打印整个数组的内容,首先我们这里的arr表示的是是第一个一维数组的地址,那么我们对这个地址进行加一操作的话,就可以得到第二个一维数组的地址,那么我们再对这个数组进行解引用的话是不是就可以得到所谓一维数组的数组名,也就是首元素的地址,那么这时候再对其进行加一加二之类的操作是不是就可以得到这个一维数组当中的其他的元素的地址了,那么再对其进行解引用就可以得到这个元素的本身,那么我们的代码实现如下:
#include<stdio.h>
void printf_arr2(int (* arr)[3], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ", *(*(arr + i)+j));
}
}
}
int main()
{
int arr[3][3] = { 1,2,3,4,5,6,7,8,9 };
printf_arr2(arr, 3, 3);
return 0;
}
那么看到这里想必大家应该了解了如何使用数组指针,那么这里要提醒大家两个点,第一个就是我们创建一个数组的时候可以不在方括号里面填入数字,但是我们在创建数组指针的时候就必须得加了,不然系统会默认是0.第二点就是p如果是指向一个数组的地址的话,我们对这个指针进行解引用得到的是数组名也就是首元素的地址。那么接下来我们就来看看下面的代码,看看学完这些之后是否能分得清楚这些代码的意思:
#include<stdio.h>
int main()
{
int arr[5];
int* parr[10];
int(*parr2)[10];
int(*parr3[10])[5];
return 0;
}
我们首先看看第一个:arr这个表示的是什么呢?这就很简单了嘛表示这是一个数组,数组里面有5个元素,每个元素的类型为int类型,同样的道理parr也表示的是一个数组因为[ ]的优先级大于所以这里parr表示的就是一个数组,数组里面有10元素,每个元素的类型为int*,那么第三个就有点不一样了,因为和parr2放到了同一个括号里面所以这里的parr2会先跟*结合这样的话parr2就表示的是一个指针,那么我们将*和变量名去掉剩下的就是这个指针指向的对象,那么我们这里这个指针指向的就是一个数组该数组是10个元素,每个元素的类型是int类型,那我们继续看第四个parr3,因为这里将*和变量名和 [ 10 ]放到了同一个括号里面,所以根据优先级我们的变量名会先跟 [ 10 ]先结合这样的话我们这里的parr3表示的就是一个数组,那么我们将数组名和后面[10]去掉剩下的就是数组当中每个元素的类型,那么剩下的结果就是这样:int( * )[5]
因为*放到了括号里面所以会先表示他,而他表示的是一个指针,而我们将被这个表示指针的符号(*)去掉就是这个指针指向的内容,那么这里这个指针指向的内容就是一个数组数组当中有5个元素每个元素的类型为整型,那么这里parr3表示的就是这是一个数组,数组中有10个元素,每个元素的类型是一个指向有5个元素且每个元素为整型的数组指针,那么看到这里想必大家对数组指针的使用有了一定的了解,那么我们接着往下看。
十一.数组参数
1.一维数组传参
那么这里我们就直接来看一个例子:
#include<stdio.h>
void test1(int arr[])
{}
void test2(int arr[10])
{}
void test3(int* arr)
{}
void test4(int *arr[20])
{}
void test5(int** arr)
{}
int main()
{
int arr[10] = { 0 };
int* arr2[10] = { 0 };
test1(arr);
test2(arr);
test3(arr);
test4(arr2);
test5(arr2);
return 0;
}
我们来看看这5个一维数组的传参是否正确首先我们来看前两个,我们说在传递一维数组的时候可以以数组的形式传递过去,然后使用数组的形式进行接收,那么我们这里test2就是这么做的,我们传了一个数组过去在函数里面直接用一个数组进行接收,那么我们还说过在使用数组进行接收的话我们是可以省略掉方括号里面的数字的,所以我们这里test1也是正确的,我们不仅可以使用数组的形式来进行接收我们还可以使用指针的形式来进行接收,因为我们的这里传过来的是arr,是数组名表示的首元素的地址,所以我们就可以使用指针的形式来接收,因为数组arr中的每个元素为int类型所以我们这里就可以用int *的指针来进行接收,所以我们这里的test3的接收形式也是正确的,我们再来看看test4和test5这里传递数组arr2,该数组中的每个元素的类型是int *类型所以我们这里依然可以采用用数组的形式进行接收这样的方法所以我们这里的test4也是正确的,那么我们再来看看test5这里是以指针的形式进行接收,因为我们这里传过来的是数组名arr2,表示的是首元素的地址,而我们这个数组的每个元素又表示的一个整型变量的地址,所以这里的arr2就相当于一个二级指针所以我们这里在进行接收的时候就可以用一个二级指针来进行接收,所以我们这里的test5也是正确的,那么看了五个例子想必大家对一维数组传参这个概念已经非常的明白了我们再来看看二维数组传参又会遇到哪些问题。
2.二维数组传参
我们这里依然用例子来讲解二维数组传参:
#include<stdio.h>
void test1(int arr[3][5])
{}
void test2(int arr[][])
{}
void test3(int* arr[][5])
{}
void test4(int* arr)
{}
void test5(int* arr[5])
{}
void test6(int(* arr)[5])
{}
void test7(int** arr)
{}
int main()
{
int arr[3][5];
test1(arr);
test2(arr);
test3(arr);
test4(arr);
test5(arr);
test6(arr);
test7(arr);
return 0;
}
我们的二维数组和一维数组有一个同样的性质就是我们可以以数组的形式传递也可以以数组的形式进行接收,所以我们这里test1,2,3就是以数组的形式进行接收,我们一维数组在进行接收的时候是可以省略方括号里面的内容的,我们二维数组在进行接收的时候也可以省略,只不过我们的二维数组不能完全省略我们以二维数组的形式进行接收的话是只能省略行的不能省略列所以我们这里的test2是错误的而test1和test3是真确的,我们再来看test4,我们这里传过来的是二维数组的数组名也就是这个二维数组的首元素的地址,所以我们这里是可以采用指针的形式进行接收的,那么我们这里的test4这个接收的指针的形式是正确的吗?当然错了哈因为我们的二维数组的首元素并不是一个简简单单的整型而是一个一维数组,所以我们这里得用数组指针的形式进行接收,那么我们这里的test4他是一个整型指针,所以我们这里的test4就接收错误了,同样的道理est5这里是以指针数组的方式进行接收所以也与传过来的参数类型不同所以test5的接收方式也是错的,我们再来看看test6这个指针的类型就是一个数组指针的类型所以我们的test6这种方式的传递就是正确的,那么我们这里的test7是真确的吗?我们的test7是用二级指针的形式进行接收,但是我们这里传过来的是一个数组指针啊,虽然这个指针指向的是一个数组但是不管怎么说他还是一个一级指针嘛,所以我们这里的传递一级指针用二级指针接收这样肯定是不对的,所以我们的test7的接收方式是错误的,那么看到这里想必大家对二维数组的传参有所了解了,那么我们接着往下看。
十二.函数指针
首先我们来了解一下函数指针是什么?当然是一个指针这个指针指向的是一个函数,那么这里可能会有小伙伴感到疑惑了,啊?我们的指针里面装的是地址啊,既然我们的函数指针指向的是函数的话,那么这里首先函数得有地址啊,那么我们c语言的函数有地址吗?答案是有的,我们通过下面的代码可以得到验证:
#include<stdio.h>
void test()
{
printf("hello world");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
我们将这段代码运行一下就可以发现我们这里的编译器确实会打印出来地址:
那么这里我们是采用了两种方法来打印我们函数的地址,那么这里说明了什么呢?我们既可以通过取地址操作符(&)来取出我们函数的地址,而且我们的函数名本省就表示的是函数的地址,那么我们既然能够取出我们函数的地址,那么我们是不是得创建一个指针来接收这个地址啊,那么我们这里就存在一个问题,我们指针的类型是什么样的呢?那么我们这里就给出两种形式大家来看看认为哪种形式是对的:
#include<stdio.h>
void test()
{
printf("hello world");
}
int main()
{
void (*pfun1)() = test;
void *pfun2() = test;
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
根据我们之前的经验想必大家这里肯定能够回答出来我们这里的pfun1的类型是对的,为什么呢?因为我们的()的优先级是大于我们的*的优先级的,如果你不加括号的话那么这里的pfun1就会先于()相结合这样的话就表示成了一个函数,而这个函数的返回值是void *的类型那么这样的话就不是一个指针了,所以我们就把变量名和*放到同一个括号里面这样的话他们之间就会先结合,这样的话就可以用来表示这是一个指针了,那么我们将这个变量名和*和包围他们的括号去掉剩下的就是这个指针指向的对象,那么这里剩下来的括号就表示这是一个函数,里面的类型就表示这个函数所需要的类型,那么我们这个函数是void不需要类型所以我们这里的括号里面就什么都不用填,括号前面的就是我们这个函数的放回值的类型,因为我们这里就打印了一句话没有返回的值所以我们这里就直接填入一个void,那么我们这里就再换一个例子:
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
int c = Add(a, b);
return 0;
}
那么这里我们要创建一个指针变量将Add的地址放进去,那应该怎么写呢?那么这里外面将这个指针变量的名字称为p,那么我们首先做的第一步是不是就是将这个变量的名字和*放到一个括号里面,那么这个括号里面还得加一个括号用来表示这个指针指向的是一个函数,而这个括号里面就会填入该函数所需要的参数的类型,因为这里有两个参数并且全部都为int类型,所以我们这里就得填入两个int并且用逗号将其隔开,那么我们这里的返回值的类型也为int所以我们就要在最前面加入一个int就可以了,那么这里该函数的指针变量创建的形式就是这样: int (*p)(int, int) = &Add;
那么这里我们知道了如何创建一个函数指针的变量,那么我们该如何使用这个指针来调用函数呢?那么这里我们就可以这样,因为这里是指针说白了就是地址,那么我们这里是不是就可以通过对这个指针接应用来得到这个函数,得到这个函数之后再通过函数调用操作符( ())来实现函数的调用并且传参,那么这样的话我们这里要调用Add函数的形式就是这样:c = (*p)(a, b);
那么这里我们还有一个形式,我们上面演示了一个例子发现了一个现象就是我们的函数名也表示的是函数的地址,并且与&函数名一模一样,那么这里我们的函数指针p里面装的也是函数的地址,并且p也是一个变量名,那么我们是不是也可以通过这个变量 p来调用我们的函数呢?答案是可以的,如果是这样的话我们的形式就和普通函数的调用差不多就长这样:c = p(a, b);
那么这里的话我们就不用在这里加上解引用操作符,但是如果你写了解引用操作符的话你就必须得加上括号跟我们的第一种形式一样,不然的话函数调用操作符会先和我们的指针变量结合这样就会先调用这个函数,然后将这个函数的返回值作为地址进行解引用最好将这个解引用的结果赋值给其他的变量这样的话是错误的写法,那么看到这里想必大家应该能够理解我们的函数指针是如何进行创建的,并且函数指针是如何进行解引用进行调用的,那么我们接下俩就来看看下面两个代码表示的是什么意思:
(*(void(*)())0)();
看到这个代码第一个感觉是啥?是不是感觉这个代码很奇怪啊,那么我们如何来分析这个代码呢?首先我们看到这个代码里面有一个数字0,那么这个0在这个代码中的作用是啥呢?首先我们能够想到的一点就是我们的数字在代码中可以当作参数进行传递,那么你觉得这里会是参数吗?很明显不可能为什么因为我们这里好像没有看到函数,那么何来参数这一说法呢?那么我们接着往下想我们数字还可以作为什么?是不是还可以作为地址啊,地址的话我们再对其进行接应用的话是不是就可以得到这个地址对应的内容呢?那么往这个0的前面看我们就会发现前面确实有两个*,但是有个*在括号里面,而另一个确实在外面,那我们先看在括号里面的这个,我们先来想想我们c语言当中的括号有什么作用?首先我们最熟悉的一点就是我们在操作符中提到的括号可以使得在表达式中可以先算括号中的表达式再算其他的表达式,那么这里很显然这个括号并不是这个作用,那么我们的括号还有一个作用就是函数调用,来讲括号里面的值传给我们的函数进行复制并且使用,那么这里很显然也不是的因为我们并没有看到什么参数,那么我们括号还有一个作用就是强制类型转换,哎如果是强制类型转换的话。那么我们这里就还真有一点想法呢!我们来看看这是什么?void(*)()
是不是非常的眼熟啊,那么我们再来对其进行改变:void(*p)()
是不是突然就能想起来了这不就是函数指针吗?那么我们这里将这个变量的名字去掉剩下的是不是就是一个类型了,那么这个类型是什么?是不是就是函数指针类型,所以我们这里的括号的作用就是强制类型转换,我们将0这个整型的数字强制类型转换为一个函数指针的类型,那么我们这个括号的前面的这个*它就是解引用的意思,那么我们将其连起来是什么意思呢?就是调用以0为地址的函数,那么我们这个代码最后面的那个括号的意思就是传参,如果这里为空的话就说明我们这个以0为地址的函数在调用的时候不需要参数,那么看到这里想必大家应该能够理解这段代码那么我们再来看看下面这个代码:
void(*signal(int, void(*)(int)))(int);
我们有了上面的例子的经验,那么看这个题的时候就会简单一点点,首先我们可以看到signal这个变量名,但是这个变量名的类型是什么我们不知道,但是我们发现的是他会先跟后面的括号进行结合,而且我们还发现括号里面装的是一个类型int和一个void(*)(int),这个我们学过也是一个类型,如果一个变量后面紧跟一个括号,并且括号里面只装的类型的话,这说明了什么,这是不是就是我们的函数的声明啊,我们函数在声明的时候可以只写类型不写变量名的,那么我们这里的意思就是声明一个名为signal的函数该函数需要一个int类型的参数和一个函数指针类型的参数,但是我们还记得函数的声明是要写返回值的啊,那么我们这里有写返回值吗?当然有,我们把函数名和函数调用操作符以及里面的参数去掉剩下的就是我们这个函数的返回值那么我们这里去掉就成这样:void(*)(int);
啊这个我们熟悉这个就是一个函数指针类型嘛,那么我们这里函数调用的返回值就是我们的函数指针类型,那么我们将上面的解析串起来的意思就是,调用一个名为signal函数需要一个int类型的参数和一个函数指针该指针指向的函数需要一个int类型的变量并且返回值得是空,然后signal函数的返回值也得是一个函数指针该指针指向的函数需要一个int类型的变量并且返回值得是空,那么看到这里想必大家能够明白这个代码的意思,那么看到这里想必大家对函数指针的理解应该比较熟悉了,那么我们接着往下看下一个内容。
十三.函数指针数组
看完上面的函数指针这个内容不知道大家有没有一种感觉就是我们这个函数指针没啥用啊,对吧用起来没有啥特殊的效果,反而还麻烦,那么如果你是这样的态度来看待函数指针的话,那么不妨来看看这个内容函数指针数组,看名字我们就知道这是一个数组,那么这个数组里面每个元素的类型是函数指针,我们如何来创建这个函数指针数组呢?就像这样int (*parr1[10])();
那么这里我们的变量名就会先和方括号结合这样就表示成一个数组,去掉我们的数组名和方括号剩下来的就是我们的数组中的每个元素的类型的,就是我们这里说的函数指针,那么这里有一点要提醒大家的就是我们这里的写法,可能之前大家在之前写数组的时候写习惯了,喜欢将类型的放到数组名的前面就成了这样:
int(*)()parr3[10];
如果写成这样的话是编译不过去的哈,只能跟我们上面所述的一样,所以这里大家要注意一下。
那么我们理解了函数指针数组之后我们再来完成一件事来实现计算器的加减乘除,这里就很简单啊我就直接写出来:
#include<stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf("*********************************\n");
printf("*******1:add 2.sub********\n");
printf("*******3.mul 4.div********\n");
printf("*********************************\n");
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret =%d\n", ret);
break;
case 2:
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret =%d\n", ret);
break;
case 3:
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret =%d\n", ret);
break;
case 4:
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret =%d\n", ret);
break;
default:
printf("输入错误请重新输入:");
}
} while (input);
return 0;
}
小伙伴看看这段代码有没有发现一个很明显的问题就是我们这里的switch语句中的分支里面的成分好多都是一样的,除了我们调用的函数不一样其他的操作都是一样,那么这样的话是不是就很影响我们的这代码的整洁啊,明明很简单的内容非要搞得这么复杂,那么这时候有小伙伴们说L那我这里将这些相同的代码放到函数里面是不是就可以做到简洁的作用呢?答案是在switch语句中确实简洁了,但是我们在函数的实现过程就麻烦了啊,何必把一样的代码写好几遍呢?那么我们这里如何来进行简便呢?我们就可以用到函数指针数组和函数指针的知识点:我们先创建一个函数指针数组,将我们这里的各个函数的地址放到这个数组里面,等我们要用到这里面的函数的话就可以直接通过数组的下标进行访问拿到这些函数的地址,那么我们再通过这个地址访问函数就可以了,为了方便我们存储的顺序也和我们选择的顺序一样这样的话我们的代码就会简洁很多,那么我们的代码如下:
#include<stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int (*p[5])(int x, int y) = { 0,add,sub,mul,div };
do
{
printf("*********************************\n");
printf("*******1:add 2.sub********\n");
printf("*******3.mul 4.div********\n");
printf("*********************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = (* p[input])(x, y);
}
else
{
printf("输入错误\n");
}
printf("ret=%d\n", ret);
} while (input);
return 0;
}
我们将代码改成这个样子是不是就感觉简洁多了,那么我们这里是用到了函数指针来调用我们的函数,用函数指针数组来存储我们的函数,那么大家看到这里的话是不是觉得函数指针还是挺有用的它有时候能够简化我们的代码,并且等我们学习的更加的深入我们后面用到的函数指针会越来越多,如果没有函数指针我们上面的代码就得不到简化。
十四.指向函数指针数组的指针
这个这个想必大家就可以类比的推理了,那么这个就是一个指针这个指针指向的就是一个数组,该数组中的每个元素的类型都是一个函数指针,那么这里我们如何来声明一个这样的指针呢?我们就直接上例子:
#include<stdio.h>
int main()
{
void(*(*ppfunarr)[5])(const char*);
return 0;
}
这里就是创建了一个指向函数指针数组的指针,这里我们没有将其初始化,我们就来聊聊这里的创建规律是怎么样的,因为这个在最里面的括号里面所以我们的ppfunarr会先和*进行结合这样就表示这是一个指针,然后我们将这个*和数组名去掉就是这个指针指向的对象,因为这里的方括号的优先级较高所以会先和方括号进行结合这样这个指针就指向的是一个数组了,那么我们再将这个方括号去掉就是我们这里数组中的每个元素的类型了,那么我们这里的每个元素的类型为:void(*)(const char*);
是一个函数指针,那么看到这里想必大家也发现了其中的逻辑,大家可以自行理解一下毕竟这东西就想俄罗斯套娃一样一个套着一个,那么接下来就是我们最后一个内容。
十五.模拟实现qsort函数
我们之前说冒泡函数的时候说我们这个函数的功能就是可以将我们的整型数组中的元素按照升序或者降序的方式进行排序,但是我们这个冒泡函数有一个很明显的特点就是它视乎只能针对我们的整型数组,那如果我们是一个结构体是一个其他的类型的数组是否也能够做到使其内容升序或者降序的功能呢?如果对于冒泡函数来说当然不能,但是在我们c语言的库函数当中就有这么一个函数能够做到那就是我们的qsort函数,那么我们这里先来看看这个函数的参数的定义是怎么样的:
我们发现这个函数具有四个参数,那么这个四个参数分别是什么意思呢?那么我们这里就一个一个的来介绍:第一个void *base
表示第一个参数需要一个地址,那么这里的地址就是我们这里要比较数据的起始地址,第二个size_t num
表示第二个参数需要一个无符号整型的变量,那么这里表示的意思就是需要比较的元素个数,第三个size_t width
表示第三个参数需要传给他每个元素的大小,第四个参数int (__cdecl *compare )(const void *elem1, const void *elem2 )
表示要把这不同元素的比较方法以函数的形式传过去,那么这里我们的qsort函数对这里的用与比较的函数是有要求的,那么他的要求下面写的也有就是:
就是这个比较函数需要两个地址,并且如果第一个地址对应的元素小于第二个地址的元素的话那么就会返回一个小于0的值,相反如果大于的话就会返回一个大于0的值,如果等于的话就会返回0,那么看到这里我们就来举一个例子来带着大家学一下qsort函数如何使用,那么我们这里就创建一个一个学生的结构体的变量,这个结构体的变量包含学生的名字和年龄,然后我们再创建一个结构体的数组,里面初始化三个学生的信息,那么我们上述的代码如下:
#include<stdio.h>
struct student
{
char arr[10];
int age;
};
int main()
{
struct student member[3] = { {"zhansan",60},{"lisi",40},{"wangwu",49} };
return 0;
}
那么我们想将这个数组按照年龄的升序进行排序,那该如何去做呢?那么我们这里就可以用到我们的qsort函数,首先第一个参数得是我们的首元素的地址,那么我们这里就可以直接将我们的数组名传过去,因为我们的数组名就是我们首元素的地址,第二个参数就是这个数组的元素个数,那么我们这里就可以通过下面的代码算出我们这个数组当中元素的个数:
sz=sizeof(arr)/sizeof(arr[0]);
然后每个元素的大小是不是就可以直接用我们的sizeof(arr[0])来实现,那么我们先在唯一麻烦的就是我们这里的比较函数该怎么办,那么我们这里首先得再创建一个函数,然后这个函数的两个参数的类型都得是const void类型,然后就是我们的函数体的实现,这里我们首先得说明一件事情就是我们的额指针如果是void类型的话他是不能被解引用的,所以我们在函数里面使用这两个参数的话我们首先得做的事情就是将这两个参数的类型进行强制类型转换,那么我们这里是结构体所以我们这里就转换为结构体类型的指针,既然是这里是结构体类型的地址的话,那么我们这里是不是就可以直接用操作符来访问这个这个结构体里面的内容,再通过if语句进行判断比较,那么我们的代码如下:
int member_cmp(const void* p1, const void* p2)
{
if ((((struct student*)p1)->age) > ((struct student*)p2)->age)
{
return 1;
}
else if ((((struct student*)p1)->age) > ((struct student*)p2)->age)
{
return -1;
}
else
{
return 0;
}
}
既然我们的比较函数实现了,那么接下来就好办的多我们直接看代码:
#include<stdio.h>
#include<stdlib.h>
struct student
{
char arr[10];
int age;
};
int member_cmp(const void* p1, const void* p2)
{
if ((((struct student*)p1)->age) > (((struct student*)p2)->age))
{
return 1;
}
else if ((((struct student*)p1)->age) > (((struct student*)p2)->age))
{
return -1;
}
else
{
return 0;
}
}
int main()
{
struct student member[3] = { {"zhansan",60},{"lisi",40},{"wangwu",49} };
int sz = sizeof(member) / sizeof(member[0]);
qsort(member, sz, sizeof(member[0]), member_cmp);
printf("%d ", member[0].age);
printf("%d ", member[1].age);
printf("%d ", member[2].age);
return 0;
}
再来看看我们的运行的结果:
确实改变了我们数组中的顺序也确实做到了升序,那么我们这里能不能做到自己来实现一个qsort函数呢?啊当然可以那么这里我们首先可以看到我们这个函数需要的4个参数就具有很明显的特点首先就是要比较的数据的起始地址,那么这个参数我们很好理解因为要排序一组数据首先得知道这个数据在哪对吧,那么第二个和第三个数据就是数据的元素和数据的大小,那么这个我们也比较好理解因为我们这里是为了得到我们一共要比较多少个字节大小的数据,以及多少内存的数据放到一起比较,那么如何比较就得用到我们的传过的比较函数嘛,那么我们一步一步的来实现这个过程,首先我们这里还是采用跟我们的冒泡函数一样的思想来完成这个代码,首先是两个for循环的嵌套,用来做到每一对数据都能够得到比较,然后在最里面的for循环中加入一个if语句用来判断我们这个数据是否应该转换,然后如果需要进行转换的话我们这里还得再写一个函数用来实现我们的转换的功能,那么我们这个函数以及上面比较的函数都是讲两个元素的的地址传上去,那我们如何得到两个元素的地址呢?那么这里就得用到我们元素的个数,以及每个元素的宽度,那么我们的代码实现如下:
#include<stdio.h>
#include<stdlib.h>
struct student
{
char arr[10];
int age;
};
int member_cmp(const void* p1, const void* p2)
{
if ((((struct student*)p1)->age) > (((struct student*)p2)->age))
{
return 1;
}
else if ((((struct student*)p1)->age) > (((struct student*)p2)->age))
{
return -1;
}
else
{
return 0;
}
}
void swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void bubble_sort(void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2))
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int flag = 1;
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (member_cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
flag = 0;
}
}
if (flag == 1)
{
break;
}
}
}
int main()
{
struct student member[3] = { {"zhansan",60},{"lisi",40},{"wangwu",49} };
int sz = sizeof(member) / sizeof(member[0]);
bubble_sort(member, sz, sizeof(member[3]), member_cmp);
printf("%d ", member[0].age);
printf("%d ", member[1].age);
printf("%d ", member[2].age);
return 0;
}
那么这里大家要记住的一点就是我们这里的交换是一个字节一个字节的交换,因为这样我们就能保证不管你传过来什么样的数据都能够做到将其全部的内容都进行交换,那么这里我们的文章就结束了感谢观看。
本章代码如下