文章目录
🚩🚩前言
欢迎来到C语言指针讲解部分,指针?它是C语言中一个很重要的内容,正确理解和使用指针,是很关键的。使用指针可以更好地利用内存资源,描述复杂的数据结构,灵活地处理字符串和数组,从而设计出简洁、高效的C程序。
🌈🌈指针与内存关系
🚀🚀内存
内存是计算机中用于存储数据的存储器,以一个字节作为存储单元,然而为了正确地访问内存单元,必须为每一个内存单元编号,这个编号就叫作该单元的地址。比如:一个旅店看作内存,那么旅店的房间就是内存单元,房间号码就是该单元的地址。
在C语言中,指针就是被用来表示内存单元的地址。若把这个地址用一个变量保存起来,则这个变量就被称为指针变量。当然,指针变量也分为不同的类型,用来保存不同类型变量的地址。严格地说,指针和指针变量是不同的,为了描述方便,就简说指针变量为指针了。
补充:
其实在计算机内部也是把整个内存划分为一个一个的内存单元的,每一个内存单元的大小是1个字节,即1Byte。那么在这里会用到一下计算机中常见的单位:【bit 、Byte、KB、MB、GB、TB、PB等】 这些之间的进制为:
1Byte=8bit、1KB=1024Byte、1MB=1024KB、1GB=1024MB、1TB=1024GB等
图解指针:
可以把每一个单元划分为一个8人间宿舍,这整个宿舍相当于一个内存单元,即1Byte。里面的8个人相当于8bit,所以1Byte=8bit。然而,在这宿舍门口的门牌号就是宿舍的位置所在,用计算机话语描述就是这个宿舍的地址。因此,我们在计算机中把内存单元称为地址,而在C语言中,给地址取了一个名字——指针。
总的说,可以把他们联系起来理解就是,内存单元的编号 = = 地址 = = 指针,三者等价的。
🌈🌈指针变量
🚀🚀地址
我们了解了地址的概念,以及指针的由来,接下来我们需要知道这指针可以用来干什么呢?👇👇👇👇
- 在C语言中,我们创建变量的实质是像内存申请空间,必须有位置放这个变量才可以。我们可以从下面的创建变量的内存中看到:创建a变量以后,通过调试可以看到在内存当中存下了a的值,用16进制表示的:00 00 00 14,在这前面的就是a所在内存当中的地址编号。
#include<stdio.h>
int main()
{
int a = 20;//创建变量
return 0;
}
🚀🚀指针变量的定义
- 定义形式:
[存储类型] 数据类型 *指针变量名[=初始值];//存储类型在开始接触的时候一般没怎么写。默认为auto
1、存储类型:register型、static型、extern型、auto型4种。
2、数据类型是该指针变量所指向的数据类型。
3、*表示为后面的变量是指针变量。
4、初始值通常初始为某个变量名的地址或者NULL。
我们看到了a变量的地址,那么它可以像变量一样,把他们的值打印出来呢?是可以的,需要用到下面的操作符。
🚀🚀指针变量的使用—‘&’
在定义指针变量后,必须将其与某个变量的地址关联起来在可以,关联的方式有2种:
1、赋值方式。将变量的地址赋值给指针变量
<指针变量名>=<普通变量名>;
//比如;
int i,*p;
p=&i;
2、定义时赋初始值。定义指针变量的时候直接指向变量的地址。
//比如:
int i,*p=&i;
无论采用上面哪一种方式,都是把指针 p 指向了变量 i 的地址。也可以将指针初始化为NULL,表示p不指向任何存储单元。
int *p=NULL;
- 例如下面的代码:
#include<stdio.h>
int main()
{
int a = 20;
int* ptr = &a;//把a的地址取出来放到指针变量ptr中去
return 0;
}
🚀🚀如何理解指针变量类型
用上面的例子:
int a = 20;
int* ptr = &a;
对上面进行解释:
ptr 的左边是int*
这里的 * 说明 ptr 是指针变量,前面的 int 说明该指针变量指向的是整型类型的对象。
- 上面说的是int类型的指针变量,那么我想存储其他类型变量的地址,该怎样写?
char ch = 'A';
char* ptr = &ch;
//ptr 的类型就是看你存储那个变量的类型,前面是什么类型,后面就是什么类型。
我们拿到了变量的地址,后续该怎样使用呢?👇👇👇👇 |
🚀🚀指针变量的解引用— ‘*’
好比我们通过地址找到了所在哪个房间,但是怎样才能进去,就需要一把钥匙。在这也是一样的,既然取出了变量的地址,通过地址能找到那个变量,但是找到了不一定愿意给你看里面的东西。所以就需要用到专用的🔑—— ‘ * ’ 解引用操作符
#include<stdio.h>
int main()
{
int a = 20;
int* ptr = &a;
*ptr = 10;//通过解引用,改变a里面原来的值
printf("a=%d\n",a);
printf("ptr=%d\n",*ptr);//通过解引用打印出来
return 0;
}
我们上面代码就是通过解引用操作符来改变原来变量的值,有些看到这会想为什么呢?干嘛这样麻烦,不把一开始的变量初始化我们想要的值,何必通过指针变量再来改一次?
其实,既然有这个东西的存在,必然有用武之地。俗话说:“万物有,必有其义!”
为什么能改变原来变量的值,可以这样想,它既然把地址交给指针变量ptr,而它通过这个地址找到了a的位置,并通过解引用操作符(“钥匙”),发现里面的数据,那么a和ptr就是一家人了。而为何通过指针变量来改变原来的值,就好比《狂飙》里面,强哥不好出面解决的事情,就会找到老默帮忙解决,说:”老默我想吃鱼🐬🐬🐬了!“🧨🧨🧨🧨
🚀🚀指针变量的大小
相信各位对计算机都有所知道,安装操作系统的有32位和64位的,对于32位的机器相当于有32根地址线,组成地址总线。每一根地址线接收的电信号会转换为数字信号,即0或1。那么32根地址线产生的二进制序列当作一个地址,则一个地址就有32个bit位,然而内存单元是用字节来描述,1Byte(字节) = 8 bit(位),所以地址有32bit,就需要4 Byte(字节)才能存储这个地址。
而指针变量是用来存储地址的,那么指针变量的大小就应该是4个字节的空间大小。同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变量的⼤⼩就是8个字节。
#include<stdio.h>
//指针变量的大小是取决于地址大小的,不是有指针变量类型决定的。
//地址大小是有多少位操作系统决定的
//32位平台下:有32个bit位,即4个字节大小
//64位平台下:有64个bit位,即8个字节大小
//sizeof() 操作符是计算所占内存空间大小的
int main()
{
printf("%zd\n",sizeof(int *));
printf("%zd\n",sizeof(short *));
printf("%zd\n",sizeof(char *));
printf("%zd\n",sizeof(float *));
printf("%zd\n",sizeof(double *));
return 0;
}
- 结果如下:
【注意:指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。】
🌈🌈指针变量类型存在的意义
在这里有人会想,既然上面通过计算,每种类型的指针变量在同一个平台下都是一样的大小,那么指针变量类型存在的意义是什么呢?接下来就讨论一下指针变量类型存在的意义。✍✍
🚀🚀指针的解引用操作
- 通过对比下面两个代码:
用整型指针变量存储的时候:
#include<stdio.h>
int main()
{
int a = 0X11223344;
int* ptr1 = &a;//整型指针变量
*ptr1 = 0;
return 0;
}
用字符型指针变量存储的时候:
#include<stdio.h>
int main()
{
int a = 0X11223344;
char* ptr1 = &a;//字符型指针变量
*ptr1 = 0;
return 0;
}
在这总结:通过上面的调试对比,不同的指针变量类型,在改变原来变量的值得时候,是有不同得变化的。在上面例子中,用整型指针变量存储整型变量地址的时候,把指针变量解引用修改为0后,原来整型变量里面的4个字节全部改为0了;但是,当用字符指针变量存储的时候,修改后 ,原来变量里面只有1个字节变为了0。
因此:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。
🚀🚀指针变量类型对指针运算的影响
我们通过代码来展示指针的运算【指针±整数】:
#include<stdio.h>
int main()
{
int n = 20;
char* ptr1 = (char*) & n;//此处做强制类型转化
int* ptr2 = &n;
printf("%p\n\n",&n);
printf("%p\n",ptr1);
printf("%p\n\n",ptr1+1);
printf("%p\n",ptr2);
printf("%p\n",ptr2+1);
return 0;
}
通过指针的运算,可以看出char* 的指针变量加1,是跳过1个字节,而char类型本身就是占1个字节;而int* 的指针变量加1,则是跳过4个字节,和它自身所占的字节一样,都是4个字节。因此,可以说是,指针+1,是跳过这个指针所指向的元素;同理,-1 也是如此。
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。
🌈🌈void* 指针
对于void* 指针,我们可以理解为无具体类型的指针,也可以叫做—— 泛型指针。这种类型的指针可以接受来自各种类型的变量的地址。但是,也有局限性,void* 指针不能直接进行指针的加减整数和解引用的运算。
#include<stdio.h>
int main()
{
int a = 10;
int* ptr1 = &a;
char* ptr2 = &a;
return 0;
上面将int 类型变量的地址赋值给一个char* 类型的指针变量,会出现下面的警告,因为类型不兼容。但是,使用void* 类型的指针变量就不会出现这种警告。
#include<stdio.h>
int main()
{
int a = 10;
int* ptr1 = &a;
void* ptr2 = &a;
*ptr1 = 20;
//*ptr2 = 30; //会出现报错,不能进行运算
return 0;
}
上面的例子可以看出 void* 类型指针的局限性,但是它也有真正的用处,用于函数参数的部分,我会在后面更新回调函数来说 void* 类型的指针变量。
🌈🌈const 修饰指针
🚀🚀const 修饰变量
我们通过上面看到,一个变量的地址赋值给一个指针变量,那么我们可以通过指针变量来改变这个变量的值。但是,我们希望指针不可以改变变量原来本身的值,该怎么办呢?在这 const 就起到了限制的作用。
#include<stdio.h>
int main()
{
int number = 20;
number = 30;//number的值是可以修改的
const int n = 10;
//n = 20;//n是不能修改的,被const修饰了
return 0;
}
上述代码中,看到 number、n 都是变量,number 的值是可以修改的,而 n 被 const 修饰后就不可以修改了。既然,直接不能修改n的值,何不找一下后门,通过n的地址来修改 n 的原来的值。代码如下:👇👇👇👇
#include<stdio.h>
int main()
{
int number = 20;
number = 30;//number的值是可以修改的
const n = 10;
//n = 20;//n是不能修改的,被const修饰了
//通过n的地址来修改
int* ptr = &n;
*ptr = 20;
printf("n=%d\n",*ptr);
return 0;
}
我们通过地址确实修改了n的值,那么我们想一下,为了不让变量不被修改,在变量名前面加上 cosnt 修饰,确实不能直接改,但是可以通过地址来修改,这样还不是打破了const的作用。所以,为了根治这修改的行为,必须让通过地址来修改变量的值的这种方法也得到控制,只有两种方法都不可以修改才达到效果,那么我们该如何来控制地址修改变量值的行为呢?请看下面:👇👇👇👇
🚀🚀const修饰指针变量
我们首先会想到指针变量创建的格式:
int* ptr=NULL;
那么const是放在 * 号的左边还是右边呢?其实,左右都可以,但是到达的效果不一样。用代码来展示:
int const * ptr1=NULL;
//放在* 号左边
int* const ptr2=NULL;
//放在 * 号右边
#include<stdio.h>
//无const 修饰
void test()
{
int number1 = 20;
int number2 = 40;
int* ptr1 = &number1;//无const修饰
*ptr1 = 10;
ptr1 = &number2;
printf("number1=%d\n",*ptr1);
}
//const在*号右边
void test1()
{
int number1 = 20;
int number2 = 30;
int* const ptr1 = &number1;
*ptr1 = 50;
//ptr1 = &number2;//出现报错是因为const 修饰的是指针变量本身,保证指针变量的内容不能修改,但是指针指向的内容是可以修改的。
printf("number1=%d\n",*ptr1);
}
//const在*号左边
void test2()
{
int number1 = 20;
int number2 = 30;
const int* ptr1 = &number1;
//*ptr1 = 60;//修饰的是指针指向的内容不能修改,但是指针变量本身是可以变的。
ptr1 = &number2;
}
void test3()
{
int number1 = 20;
int number2 = 30;
int const* const ptr1 = &number1;
//两边都是修饰的,都不能改。
//*ptr = 50;
//ptr1 = &number2;
}
int main()
{
test();
test1();
test2();
test3();
return 0;
}
总结:const 如果放在 * 的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。
• const如果放在 * 的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
“左定值,右定向”
🌈🌈指针运算
指针的三种基本运算:
1、指针 + - 整数
//指针 + - 整数 <==> 日期 + - 天数
2、指针 - 指针
//为什么没有指针 + 指针呢?这个和日期一样的,用日期+日期是没有意义的,所以指针也是。日期 - 日期是中间有多少天,所以指针-指针就是中间间隔多少个元素。
3、指针的关系运算
🚀🚀指针 + - 整数
我们先来说说数组,数组它是在内存空间中连续存储的,我们只要知道数组的第一个元素的地址,就可以很好的找到后面的地址了,我们下面先用最常用的下标方式来打印数组:
#include<stdio.h>
int main()
{
int array[] = { 1,2,3,4,5,6,7,8,9,10 };
//计算数组大小
int len = sizeof(array) / sizeof(array[0]);
for (int i = 0; i < len; i++)
{
printf("%d ",array[i]);
}
printf("\n");
return 0;
}
- 输出结果:
通过下标来访问当然可以,但我们学了指针,就得把指针用起来,通过地址来访问数组的内容,下面代码展示:
#include<stdio.h>
int main()
{
int array[] = { 1,2,3,4,5,6,7,8,9,10 };
//计算数组大小
int len = sizeof(array) / sizeof(array[0]);
int* p = &array[0];
for (int i = 0; i < len; i++)
{
printf("%d ",*p);
p++;//用到指针运算p=p+1;跳过4个字节
}
return 0;
}
通过上面指针运算来打印数组,可以看到是可以的,但是上面的运算会改变指针变量的地址是发生改变的,而下面的写法可以防止指针变量地址的变化:
所以防止指针变量地址的变化,可以改写为下面形式:可以看到打印初和打印结束,p的地址没变,始终是数组首地址。
从上面可以得出:(指针1) + 1 == (指针2);表示指针跳过1个元素,但始终是指针,所以我们可以交换一下,(指针2) - (指针1) = = 1;表示指针之间的元素个数吧。所以通过下面来了解指针 - 指针吧。👇👇👇👇
🚀🚀指针 - 指针
前提是:必须只想同一块内存空间。
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//数组大小
int sz = sizeof(arr) / sizeof(arr[0]);
printf("sz=%d\n",sz);
int sz1 = &arr[9] - &arr[0];
printf("sz1=%d\n", sz1);
return 0;
}
✨✨指针±整数和指针 - 指针的示例
求字符串长度的库函数strlen(),和自己实现该库函数my_strlen()。
看代码:👀👀
指针 + 整数运算
用自己的函数my_strlen()
看代码:
指针 - 指针运算
🚀🚀指针关系运算
用指针比较大小,来打印数组,下面代码表示,arr为数组首元素地址,加上整个数组的内存大小,得到数组末尾的地址也就是元素10后面的地址,用指针变量先存储数组首元素地址,然后和数组末尾的地址相比较,如果小于末尾的地址,则让指针变量自增,依次打印出数组元素。:
🌈🌈野指针
🚀🚀野指针概念
什么是野指针呢?野指针是说指针指向的位置是不明确的,没有明确限制或不正确的。就好比大街上流浪的野狗,没有主人或家。那么这野指针又是怎样形成的呢?
🚀🚀野指针的成因
①指针未初始化。
//1、指针未初始化造成野指针
#include<stdio.h>
int main()
{
//整形指针变量
int a = 10;
int* ptr;//作为局部变量,不初始化是随机值
*ptr = 20;
printf("%d\n",*ptr);
return 0;
}
②指针越界访问
//指针越界访问造成野指针
#include<stdio.h>
int main()
{
int arr[5] = { 0 };
int* ptr = &arr[0];//把数组首元素地址赋给指针变量ptr
for (int i = 0; i <= 6; i++)
{
*(ptr++) = i;//此处当指针指向的范围超出数组大小的时候,ptr就是野指针。
}
return 0;
}
③指针指向的空间被释放
//指针指向的空间被释放也会造成野指针
#include<stdio.h>
int* test()
{
int number = 100;//局部变量,在调用过后会立马销毁
return &number;
}
int main()
{
int* ptr = test();
printf("ptr=%d\n",*ptr);
return 0;
}
- 会出现警告:
就上面3种情况,出现野指针后,对于计算机内存来说是很危险的,会造成内存泄漏,就像没有被拴着的野狗会很危险的。所以我们在写代码的时候如何避免野指针的出现呢?
🌈🌈对野指针说拜拜
我们对照着问题找措施:
①指针变量创建的时候记得初始化。当我们创建了指针变量后不知道指向哪里,我们可以赋给它NULL。NULL是C语言中定义的一个标识符常量,值为0。
②小心指针越界访问,向内存申请多大空间,就只能访问多大的空间。
③指针变量不再使用的时候,记得及时置为NULL,指针变量使用之前检查指针的有效性。
④避免返回局部变量的地址。
🌈🌈assert断言
assert.h 头文件定义了 assert() 宏,它是用在检验程序是否符合指定条件的,如果不符合就会报错,终止运行。这个宏就称之为断言
🌈🌈传值调用和传址调用
我们学习了指针,目的是使用指针,哪什么时候,必须用指针呢?
✨✨写一个函数,实现两个整数的交换
//编写一个函数,实现两个整数的交换。
#include<stdio.h>
void swap1(int x,int y)
{
int temp = x;
x = y;
y = temp;
}
int main()
{
int number1 = 0, number2 = 0;
printf("请输入两个整数:\n");
scanf_s("%d%d",&number1,&number2);
printf("\n交换前:\n");
printf("number1=%d,number2=%d\n", number1, number2);
swap1(number1,number2);
printf("\n交换后:\n");
printf("number1=%d,number2=%d\n", number1, number2);
return 0;
}
我们看结果会发现,耶???怎么没得行呢?哪出问题了。别慌,我们来调试一哈。
从下面可以看到,number1和number2在输入两个值以后就被初始化为2和5了,并且一开始内存分配了两个地址给这两个数,接下来调试交换函数:
调试函数以后,初始化为0,并且分配了内存空间,但是和原来的两个变量的内存空间不一样,可以看出创建了新的内存空间来存储这两个整数了,接下来我们继续:
下面可以看出x和y确实拿到这两个整数,但是x和y的地址与原来的number1和number2的地址不一样,说明这两个形参是独立的两个内存空间,和原来的互不影响,所以不可以交换。
总结:上面的方式就是传值调用,实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。
- 既然上面的方法行不通,就得支招了,我们在交换的时候就应该用main函数里面变量自身的地址,这样才可以实现交换,所以我们就得把两个变量的地址传过去,既然传的是地址,那么函数形参就必须用指针来接收,接下来看代码:
//编写一个函数,实现两个整数的交换。
#include<stdio.h>
//void swap1(int x,int y)
//{
// int temp = x;
// x = y;
// y = temp;
//
//}
void swap2(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main()
{
int number1 = 0, number2 = 0;
printf("请输入两个整数:\n");
scanf_s("%d%d",&number1,&number2);
printf("交换前:number1=%d,number2=%d\n\n",number1, number2);
//swap1(number1,number2);//传值交换是没有用的
swap2(&number1, &number2);//传地址就可以
printf("交换后:number1=%d,number2=%d\n", number1, number2);
return 0;
}
用swap2()函数就是传地址调用的,最终实现了交换,这就叫做传址调用。
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;
所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。
指针基础部分就描述完了,谢谢🤞🌹