关注小庄 顿顿解馋 (≧∇≦)
引言: 小伙伴们在学习c语言过程中是否因为指针而困扰,指针简直就像是小说女主,它逃咱追,我们插翅难飞…本篇文章让博主为你打理打理指针这个傲娇鬼吧~
本节我们将认识到指针本质,何为指针和她的性质用法,请放心食用!
指针本质及其性质用法
- 一.指针的本质
- 1.🏠 内存与地址
- 2.🏠 指针变量与地址
- 2.1 取址符(&)
- 2.2指针变量
- 2.3解引用操作符(*)
- 2.4指针变量的大小
- 二.指针的性质
- 1.🏠指针类型的意义
- 1.1 不同指针类型的解引用
- 1.2指针+-整数
- 2.🏠 const修饰指针
- 2.1 const修饰变量
- 2.2 const修饰指针变量
- 3.🏠 指针运算
- 3.1 指针+-整数
- 3.2 指针-指针
- 3.3指针的关系运算
- 三.指针的使用
- 1.🏠 野指针
- 3.🏠assert断言
- 4.🏠传值调用和传址调用
- 在这里插入图片描述
一.指针的本质
1.🏠 内存与地址
内存单元:我们在学校寄宿时是几个人为一个集体为宿舍的,为了方便入住以及内务检查学校给每个宿舍设置了门牌号方便辨认。类似地,存储数据的内存划分一个一个内存单元同时给它们编上“号”方便取出数据。
内存单元:内存最小单元,单位是字节也就是8个bit。
地址:正如上言所说,我们给内存单元的编号就是地址
,在c语言中我们叫它指针
总结: 指针 == 地址 == 内存单元编号 他是我们用来取数据的东西
2.🏠 指针变量与地址
数据在内存中的地址就是指针,类比之前我们学习过的数据类型,整形变量是存储整形的变量,那么指针变量就是存指针的变量
我们知道指针变量了先不着急,我们知道指针变量存的是地址,赋值两边要相同类型你知道怎么获得地址吗?
2.1 取址符(&)
取址符顾名思义就是取出地址,打印地址时我们用的是%p
,在我们编译器如何查看地址呢?接下来上图
在vs上进入调试模式->调试->窗口->内存/监视窗口都可查看内存,别忘记加上我们的取址符哦
2.2指针变量
指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址,他的定义如下:
Datatype * name;
int a = 10;
int * pf = &a;
char c = 'c';
char * pc = &c;
理解:这里的*
用来表示这是一个指针变量,前面的Datatype
表示的是指针指向的数据类型,而去掉变量名就是这个指针变量的类型
,比如int* pf表示pf是个指针变量它指向的数据类型是int,存储的是int类型数据的地址,指针类型是int *
类比地,聪明的你应该也会类比其他数据类型了吧~
void*类型
在指针类型中有这么一个存在,它可以可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。
就像一个公共场所。
2.3解引用操作符(*)
有的小伙伴到这里可能要问了既然知道什么是指针变量,那他怎么用啊?快端上来罢!这里我们就要了解我们的解引用操作符了
客人房间(变量) | 房间编号(地址) |
---|---|
变量a | 0x11223344 |
变量b | 0x12332166 |
变量c | 0x12ffaa11 |
如上表所示,每个变量(房间)都对应一个编号(地址),而要进入房间使用它就得用解引用操作符
int a = 1;
printf("%d ",a);
int *p = &a;
*p = 10;
printf("%d",a);
输出结果是1 10
总结: 解引用操作符(*)作用是通过地址,以地址为媒介,找到地址指向的空间对该空间进行操作。
2.4指针变量的大小
我们知道不同的数据类型所占字节大小不同,那不同的指针类型是不是也是大小不同呢?接下来我们先普及一下编址的相关知识
CPU与内存之间是有大量数据交互的。大致过程如下:CPU先通过控制总线下命令->地址总线收到命令找对应地址->通过数据总线传输数据。在这个过程中地址总线不只图中一条哦,作者只是大概画出来。不同机器地址总线总数不同,比如32位机器有32条,64位有64条,每根线只有两态,表⽰0,1【电脉冲有⽆】,那么⼀根线,就能表⽰2种含义,2根线就能表⽰4种含义,依次类推。32根地址线,就能表⽰2^32种含义,每⼀种含义都代表⼀个地址
通过上面的知识普及我们了解到地址总线发出的电信号转换成二进制序列
就是我们的地址,所以不同的机器就会产生不同位的二进制序列当做地址
,32位地址->32个bit->4个字节,64位地址->64个bit->8个字节。
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
x86(32位环境) 输出结果为4 4 4 4
x64(64位环境) 输出结果为8 8 8 8
总结: 指针变量大小与它指向的数据类型无关,而与使用的机器有关
二.指针的性质
1.🏠指针类型的意义
既然指针变量大小与指针类型无关,那为什么还要设置这么多类型的指针变量?有句话叫做存在即合理,设置了它肯定是有它的意义的。
1.1 不同指针类型的解引用
//代码1
int n = 0x11223344;
int *pi = &n;
*pi = 0;
//代码2
int n = 0x11223344;
char* pc = &n;
*pc = 0;
两段代码都是存了n的地址,只不过是不同指针类型,那是否值都变为0了呢?我们用之前的查看内存知识观察下。
通过上面观察我们发现不同指针类型得出的结果也不同,由此我们可以得出结论:不同指针类型解引用权限不同,也就是能操作字节个数不同
,正如上面的例子int解引用能将4个字节都置为0,而char解引用只能将1个字节置为0.
1.2指针±整数
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1)
输出结果:
004FF700
004FF700
004FF701
004FF700
004FF704
我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
这就是指针变量的类型差异带来的变化。
结论:指针类型还决定了指针向前或向后走一步有多大。
注:之前提到的void*虽然能接收各种类型指针,但却不能使用指针加减也不能解引用,可谓是上天为你关了一扇门也为你打开一扇窗啊。
2.🏠 const修饰指针
2.1 const修饰变量
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。
但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//n是不能被修改的
加上const后n不能动了,那我们运用之前的解引用呢?
const int n = 0;
printf("n = %d\n", n);
int*p = &n;
*p = 20;
printf("n = %d\n", n);
我们可以看到这⾥⼀个确实修改了,但是我们还是要思考⼀下,为什么n要被const修饰呢?就是为了
不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以应该让
p拿到n的地址也不能修改n,那接下来怎么做呢?
2.2 const修饰指针变量
我们给上几段代码来测试下
//代码1 无const
int n = 10;
int m = 20;
int *p = &n;
*p = 20;//能否赋值
p = &m; //能否赋值
//代码2 const在*左边
int n = 10;
int m = 20;
const int *p = &n;
*p = 20;//能否赋值
p = &m; //能否赋值
//代码3 const在*右边
int n = 10;
int m = 20;
int *const p = &n;
*p = 20;//能否赋值
p = &m; //能否赋值
//代码4 const在*左边和右边
int n = 10;
int m = 20;
int const *const p = &n;
*p = 20;//能否赋值
p = &m; //能否赋值
代码1 :都能赋值
代码2 : 语句1不能赋值
代码3 :语句2不能赋值
代码4: 两语句都不能赋值
总结:1.const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但指针变量本身的内容可变。2.const如果放在*的右边,修饰的是指针变量本身的内容,保证存储的内容不能修改,但指针指向内容可通过指针修改3.const在*的左边和右边则指针变量本身内容和指针指向内容都不能被修改。
总的来说,const修饰变量具有常量属性,但解引用相当于绕道找后门进入这个变量修改变量的值。此时用const修饰指针相当于把这个后门也关了
3.🏠 指针运算
- 指针±整数
- 指针-指针
- 指针关系运算
3.1 指针±整数
根据我们前面分析,指针±整数表示不同指针向前一步或向后一步的大小。这在数组可以很好运用,我们知道数组在内存中是连续存放的,这样知道其中一个元素地址再运用指针±整数就能得到其他元素地址,我们可以用它来打印数组
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
}
3.2 指针-指针
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("%d",&arr[2]-&arr[0]);
printf("%d",&arr[0]-&arr[2]);
输出结果为2 -2
我们可以得出指针-指针的绝对值得到的是指针与指针之间的元素个数,可以类比日期-日期是天数,而指针-指针、就是元素个数。前面的指针±也可以这样类比,日期加天数还是日期
注意:该运算的前提条件是相同类型的指针变量
3.3指针的关系运算
指针也是可以比较大小的,比较的是在内存位置的高低。
三.指针的使用
接下来我们来了解一些关于指针的使用和注意事项吧
1.🏠 野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的),通常以下三种情况会造成野指针。
- 指针未初始化
- 指针越界访问
- 指针指向空间释放(一般指针调用函数后内存空间被回收)
//原因3
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
这个地方*p接收了返回的&n地址,但是调用函数后n就被释放了,也就是我们常说的空有其表
规避方法:
- 指针初始化为NULL
- 小心越界访问
- 不要返回局部变量地址
- 指针不用了及时置为NULL
3.🏠assert断言
assert.h 头⽂件定义了宏 assert() ,用于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运行
这个宏常常被称为“断⾔”。assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
好处:
1.能⾃动标识⽂件和出问题的⾏号
2.有⽆需更改代码就能开启或关闭 assert() 的机制。(如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏NDEBUG
)
4.🏠传值调用和传址调用
我们知道函数传值调用时形参是实参的一份临时拷贝,改变形参不影响实参,那有什么方法能改变我们的实参呢?答案是我们的传址调用。
oid Swap2(int*px, int*py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
Swap1(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
输出结果
交换前 a=10 b=20
交换后 a=20 b=10
由此可以得出,传址调用本质上也是传值,也是拷贝地址的值传到函数内部,只不过地址指向的空间是唯一的,我们便能通过地址来改变它的空间。值得注意的是,在函数内部创建的px,py跟a,b不是同个变量只不过他们存储的值(地址)一样,存储的值能干的事一样!
本次分享到这里就结束了,希望小伙伴能够学到知识,喜欢的话给小庄来个三连吧!