超详细的c进阶教程!手撕c指针
Posted 东条希尔薇
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了超详细的c进阶教程!手撕c指针相关的知识,希望对你有一定的参考价值。
作者的码云地址:https://gitee.com/dongtiao-xiewei
后续作者会更新力扣的每日一题系列,原代码会全部上传码云,推荐关注哦,笔芯~
还像更深入地了解c语言?快来订阅作者的c语言进阶专栏!作者承诺本系列不会TJ!预计更新:指针,字符串处理,内存管理,结构体,预处理等等
文章目录
C指针我相信,对于许多正在新手村发育的程序员来说,都是一道过不去的坎(包括我新手期呜呜呜TAT)。为了解决广大新手玩家在指针关卡卡关的问题,特地推出此攻略~
废话不多说,让我们开始吧!
指针快速入门
指针的概念
到底什么是指针?以下是度娘给出的定义:
指针也就是内存地址,指针变量是用来存放内存地址的变量,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。
指针在c语言如此流行,也是因为指针可以有效的实现例如树,链表等数据结构。
听了度娘这么说,感觉还是一头雾水啊,说了个寂寞啊。。
别急,接下来我用生活中的例子给大家引出指针这一概念吧
我们可以把计算机内存看做一条街道上的一排房子,就像这样,每个房子都有自己的编号,如下图
每个房子都有且仅有一个门牌号与它对应,每个房子都可以容纳一口人家
对应到计算机科学中就是:
计算机中创建的每个变量(人家)都会在内存中占据一定的位置,每个内存位置都由一个特定的地址(门牌号)唯一确定
而指针,它就是一种能存储地址的变量。我们可以通过一些操作,利用指针找到某一个变量,去间接的操作它。
对应到计算机中就是这样
为什么一个格子对应一个字节呢?
指针大小
我们知道,我们使用的操作系统有32位和64位之分(在此电脑上点击右键后再点击属性查看)
操作系统位数的意义是:电脑可以使用多少根地址线
我们拿32位操作系统来举例:
32的机器对应32根地址线,假设每根地址线可以产生一个电信号(正电/负电),对应二进制中用1/0表示
32位就可以产生如下的电信号
注:一根地址线对应一个比特位,每8个比特位对应1个字节
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
....
11111111 11111111 11111111 11111111
我们简单的算一下:每根地址线可以产生2个电信号,32个地址就可以产生232个不同的电信号。
如果我们把每个字节表示成一个地址的话,可以表示232字节内存,按一个单位1024的比例扩大,算出可以操控4GB的空闲空间进行编址
问题来了,那为什么不拿其它单位来标识一个地址呢?
就拿1kb来举例,我们知道一个int变量大小仅有4b,如果我们拿1kb大小来标识一个地址,将会造成大量空间的浪费(本例浪费1020b左右)
按一个字节1b的大小来计算:
- 32位操作系统下的指针变量大小为4b
- 64位操作系统下的指针变量大小为8b
可以简单写个程序来验证一下
int main()
{
printf("%d\\n", sizeof(int*));//int*的指针变量
printf("%d\\n", sizeof(char*));//可以用不同类型的指针变量来验证,是不是指针变量大小是固定的
return 0;
}
VS2019设置系统的方法如下图
这里的x86代表我们使用的是32位平台
输出结果如下
64位平台上输出如下
指针操作符类型
可能又有好奇的小伙伴会问了,既然不同指针类型大小都一样,那为啥还要用这么多的指针类型啊?
哦对了,我们还是先介绍一下指针该怎么用吧~
指针的操作主要靠两个运算符:*—— 解引用操作符和 & ——取地址操作符
比如以下的代码
int a=10;
int* p=&a;
*p=20;
printf("%d\\n",a);
注:定义指针变量int p这里的星号表明p是一个指针变量,而不是解引用的意思*
我们通过画图解释这两个操作符
所以最后的输出结果就应该是20
当然,这里我们把int类型的变量存放在了int指针中,那可不可以把int a放在char* 类型的指针变量中呢?
int main()
{
int a = 10;
char* p = &a;
return 0;
}
程序确实能编过去,但是报了一个警告
虽然这个程序可以编过去,不过我们最好还是把指针存放在相同类型的变量中,不然就可能导致一些意外:
比如还是本节第一个程序,只不过我们换成char*类型的变量,并将a的值修改到足够大
int a=0x11223344;
char* p=&a;
*p=0x55667788;
printf("%x\\n",a);
输出如下图
很显然,输出的值不是我们想要的值,为什么会造成这种错误呢
让我们调试一下,看看内存情况。
所以我们可以得出结论:
指针类型决定了向前或向后走一步或者操作的权限有多大
指针的运算
这一节全是概念性质的知识点,只需要记住就行了
指针±整数
我们通过打印数组的例子来说明这一概念
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
for (int i = 0; i < 10; i++)
{
printf("%d ", *(arr + i));
}
return 0;
}
这个程序可以打印出每一个元素
所以,指针加减整数,可以表示跳过的元素个数
指针-指针
int main()
{
int arr[10] = { 0 };
int* parr1 = (arr + 9);
int* parr2 = &arr[0];
printf("%d\\n", parr2 - parr1);
return 0;
}
输出结果
结论:指针减去指针的值的绝对值是两个指针中间的元素个数
指针的比较
我们知道,其实指针都是有一个十六进制的数字表示的,也算作一个值,所以理所当然的可以比较大小
int main()
{
int arr[10] = { 0 };
int* parr1 = (arr + 9);
int* parr2 = &arr[0];
if (parr1 > parr2)
printf("haha\\n");
else
printf("hehe\\n");
return 0;
}
结论:数组中的元素随着下标从小到大,其地址也由低到高
指针和数组的联系
我们平时在学习c语言的时候或者我们在看一些c语言教材的时候,我们发现作者通常会把指针和数组放在一起谈,这是为啥呢?指针和数组到底有什么关系呢?
用一个代码说明
int main()
{
int arr[10] = { 0 };
int* p1 = &arr[0];
int* p2 = arr;
printf("%p\\n", p1);
printf("%p\\n", p2);
return 0;
}
输出结果如下
这两个表示形式的地址是一模一样的
所以可以得出以下结论:数组名代表的是首元素地址
但是有以下两种特殊情况
- sizeof(arr)(其实arr未参加任何运算),这里的arr代表整个数组
- &arr,这里也代表的是取出整个arr数组的地址
用代码举例吧
int main()
{
int arr[10] = { 0 };
printf("%p\\n", arr);
printf("%p\\n", &arr + 1);//检测跳过了一整个元素还是一个元素
printf("%u\\n", sizeof(arr));//比较是否与首元素地址大小一样
printf("%u\\n", sizeof(&arr[0]));
return 0;
}
输出结果如下
数组的传参
我们知道,数组元素名代表的是首元素地址,所以以下两种传参方式都是可行的。
void test(int* p)//1
void test(int p[])//2
int main()
{
int arr[10];
test(arr);
return 0;
}
数组传参的时候会退化成首元素的地址传给函数体
所以我们可以使用一个指针接受arr
当然,利用第二种数组的方式接受也可以,第二种方式更容易使萌新理解,我们不用管方块里面该填什么,随便你怎么填,要么不填,要么填一个大于原数组的数字,都无所谓
但是,这样传参,我们在有些使用场景下可能会出现一些问题
比如,我们要利用函数打印一个数组
void print_arr(int* arr)
{
int sz = sizeof(arr) / sizeof(arr[0]);//我们必须要知道一个数组有多少元素才能打印,这是通用的计算方法
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
print_arr(arr);
return 0;
}
但是,程序的输出只打印了一个元素
其实是因为函数传进去的是一个指针,指针大小是固定的4或者8个字节,所以在函数体内计算数组大小永远是4/类型大小,结果是1,这个结果显然是错误的
所以,我们必须在函数外先算好数组元素个数,把个数参数传进去,才能达到我们想要的结果
void print_arr(int* arr,int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
print_arr(arr,sz);
return 0;
}
这个结果就是正确的
[]操作符与指针
以下两种程序的效果完全等价
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);//第一种
printf("%d ", *(arr + i));//第二种
}
}
第二种的解释:arr代表首元素的地址,首先进行加法运算跳过某一些元素,最后在对其解引用找到数组中某一个元素的数字
二级或更高级指针
二级指针可以存放比它低一级指针变量的地址
int main()
{
int a = 10;
int b = 20;
int* pa = &a;
int* ppa = &pa;
*ppa = &b;
return 0;
}
字符指针和字符串
我们知道,有一种指针类型叫char*类型,它可以存放字符类型的地址
int main()
{
char ch = 'w';
char* p = &ch;
*p = 'q';
return 0;
}
但是我们知道,c语言不像python,它是没有字符串类型的
但是,我们可以使用字符指针来处理字符串
在数组阶段,我们通常是使用数组来保存字符串的
char arr[]="hello world!";
我们同样可以使用字符指针来保存字符串
char* p="hello world!";
这种保存方式,是把整个字符串地址传进来了吗?
其实不是,我们简单用一个程序验证一下
int main()
{
char* p = "hello world!\\n";
printf("%c\\n", *p);
return 0;
}
输出结果如下
结果是h,所以我们可以初步得出结论
字符指针在储存字符串时,本质还是把首元素地址储存起来,而存放空间又是连续的,所以可以通过这个指针找到整个字符串
指针数组和数组指针
至于为什么把它们放在一起,是因为它们名字里面每个字都一样,但是意义却完全不一样~
前者是数组,后者是指针
指针数组
顾名思义,指针数组就是一个能存放地址的数组,表示方式如下
int* parr[10];
这个程序就代表,一个名为parr的数组,其中包含10个元素,每个元素都是int*指针类型
我们可以这样分析:
把名字去掉,剩下的是int*[10],就是它的类型了~
指针数组的初始化方式举例
int main()
{
int a = 10;
int b = 20;
int c = 30;
int d = 40;
int* parr[4] = { &a,&b,&c,&d };
for (int i = 0; i < 4; i++)
{
printf("%d ", *parr[i]);
}
return 0;
}
可以打印出我们想要的结果
内存图:
总结:初始化只需要传进地址就行了,使用的时候记得解引用操作
数组指针
结论:数组指针是一种指向数组的指针
表示方式:
int(*p2)[10];
注意:由于括号的优先级最高p2不会先与[]结合,而是先与*结合,说明其是一个指针变量,然后指向是一个含10个整型元素的数组
数组指针的应用:二维数组传参
我们知道,二维数组在内存的方式也是连续存放的,一行元素存完后,接着立刻存储下一行
int arr[3][3];
这个二维数组的内存情况如下
我们就可以把二维数组的每一行看做一个一维数组的元素
于是我们就可以这样理解二维数组:看做一个一维数组,每一行的元素都是另外一个一维数组
基于以上的推测,于是我们就可以使用数组指针来传参
void test(int(*p)[3])
{
}
int main()
{
int arr[3][3] = { 0 };
test(arr);
return 0;
}
p就被看作一个指针,该指针指向一个有3个整型元素的数组
解引用方式:
*(*(p+1)+2)//等价于arr[1][2]
函数指针
其实,我们自己写的函数也是会使用特定的地址保存起来的
那函数的地址能不能保存呢?答案是当然可以,用函数指针就行了
函数指针包含以下几个成分:
- 指向的函数的返回值
- 指向的函数的参数类型
以下有两种可能会混淆的定义方式
void(*pfun1)()=test1;
void*pfun2()=test2;
第一种方式是正确的,因为变量必须要保存先与*结合,说明其是一个指针
使用方式
int Add(int x, int y)
{
return x + y;
}
int main()
{
int(*pfun)(int, int) = Add;
//(返回类型)int(指针变量名称)(*pfun)(参数类型)(int, int) = (指向的函数)Add;
int ret = (*pfun)(2, 5);
//也可以进行简化
int ret1 = pfun(2, 6);
printf("%d,%d\\n", ret, ret1);
return 0;
}
函数指针数组
类比指针数组,就是把每个元素换成一个函数指针了
声明方式
void(*parr[10])(int,int);
函数指针应用(回调函数)
定义:就是通过函数指针调用的函数,这个函数的参数通常是一个函数指针
例子:计算器
函数定义部分:
float Add(float x, float y)
{
return x + y;
}
float Sub(float x, float y)
{
return x - y;
}
float Mul(float x, float y)
{
return x * y;
}
float Div(float x, float y)
{
if (y == 0)
{
printf("error\\n");
return 0.0
}
else
{
return x / y;
}
}
不适用函数指针的话,代码将会变得非常冗余,(输入操作数的部分)
int main()
{
int input = 0;
float x = 0.0;
float y = 0.0;
do
{
printf("1. Add 2.Sub\\t");
printf("3. Mul 4.Div\\n");
printf("你要做什么?\\n");
scanf("%d"以上是关于超详细的c进阶教程!手撕c指针的主要内容,如果未能解决你的问题,请参考以下文章