文章目录
- 1.指针存在的意义
- 2.指针变量和地址
- 3.指针变量类型的意义
- 3.1指针解引用
- 3.2指针+ - 整数
- 3.3void*
- 4.关键字const
- 4.1const修饰变量
- 4.2 const修饰指针
- 5.指针运算
- 5.1指针+ -整数
- 5.2指针-指针
- 5.3指针比较大小
- 6. 野指针
- 7.assert断言
- 8. 数组名的理解
- 9.一维数组传参的本质
1.指针存在的意义
说到C语言,你是不是最害怕里面的指针呀。看完下面的内容,你或许对指针就没那么胆怯了。
首先,我们要明白C语言中为什么要有指针。
- 指针是C语言的灵魂,这句话并不夸张。指针是C语言中最基础、最重要的概念之一,它使得C语言成为一门强大的、高效的、灵活的编程语言。
- 指针的存在使得C语言可以进行复杂的内存操作,能够更好地控制程序的行为,同时也能够实现高效的数据结构和算法。
指针是一个变量,它存储了一个内存地址,而这个内存地址指向的是另一个变量或对象的位置
。通过指针,我们可以直接访问或修改这个位置的变量或对象,这为我们提供了很大的灵活性和控制力
在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了新的名字叫:指针。
所以我们可以理解为:内存单元的编号=地址 = 指针
2.指针变量和地址
- 在C语言中,创建变量就是向内存申请空间,如下图:
上述代码就是创建了一个整型变量a,向内存中申请了4个字节的空间,每个字节都有自己的地址。
&a取出的是a所占4个字节中地址较小的字节的地址。,我们知道了它的地址,就可以顺藤摸瓜访问到4个字节的数据。
- 如何拿到变量的地址?
我们通过取地址操作符(&)拿到的地址是⼀个数值,比如:0133FE28,这个数值有时候也是需要存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?答案是:指针变量中。
指针变量的写法:就是在变量前加上一颗 *
指针变量也是⼀种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。指针变量也有自己的地址
,这里pa的地址就是0x00cffdfc。
- 如何通过地址获取变量?
我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这⾥必须学习⼀个操作符叫解引⽤操作符(*)。
- 指针变量的大小
- 指针变量的大小取决于地址的大小,与指针变量的类型无关。
- 32位平台下,地址是32个bit位,即4个字节。
- 64位平台下,地址是64个bit位,即8个字节。
3.指针变量类型的意义
3.1指针解引用
通过调试我们可以看到,第一个会将a的4个字节全部改为0,第二个只是将a的第⼀个字节改为0。
结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)
3.2指针+ - 整数
通过上图我们可以发现 int 类型的指针+1跳过了4个字节,char类型的指针+1跳过1个字节。
结论:指针的类型决定了指针向前或者向后走⼀步有多⼤(距离)。
3.3void*
在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址
。但是也有局限性, void* 类型的指针不能直接进行指针的+ -整数和解引⽤的操作。
- void* pi 可以接受任意类型的地址
- 不能对void 类型的指针进行解引用和加减整数的操作
4.关键字const
4.1const修饰变量
如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。
- const修饰变量,变量的值就不能修改了
4.2 const修饰指针
- const 在 * 号的左边
- const 在 * 号的右边
- const在 * 的两边
总结::左定值,右定向
5.指针运算
5.1指针+ -整数
5.2指针-指针
- 指针-指针的
绝对值
是两个指针之间的元素个数(两个指针必须指向同一块空间)
5.3指针比较大小
- 指针比较大小,就是地址比较大小
6. 野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针是如何造成的呢?
- 指针未初始化
局部变量不初始化,默认是随机值。
- 指针越界访问
- 指针指向的空间被释放
可以打印出来10是因为test函数的栈帧空间还没有被破坏,再次打印的时候,就变成随机值了。
指针虽好,但是不规范使用指针可能会造成意想不到的后果,因此在使用时,应该避免出现野指针。
如何规避野指针呢?
- 指针初始化
- 不要越界访问
- 指针变量不使用时及时置为NULL
- 指针使用前检查是否为NULL
7.assert断言
assert.h 头⽂件定义了宏 assert() ,⽤于在运行时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断言”。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣任何作⽤,程序继续运行。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写⼊⼀条错误信息,显示没有通过的表达式,以及包含这个表达式的⽂件名和⾏号,如下:
#include<stdio.h>
#include<assert.h>
int main()
{
int* p = NULL;
assert(p != NULL);
printf("66666\n");
return 0;
}
使⽤ assert() 有几个好处:它不仅能自动标识⽂件和出问题的行号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断言,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。
⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就行,在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响⽤⼾使⽤时程序的效率。
8. 数组名的理解
- 数组名就是数组首元素的地址,但是有2个例外!
- 例外1
sizeof(数组名),sizeof中单独放数组名,数组名表示整个数组的大小
,单位是字节。
- 例外2
&数组名,取出的是整个数组的地址
(整个数组的地址和数组首元素的地址是有区别的)
这里我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是⾸元素的地址,+1就是跳过⼀个元素。
但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。
到这里大家应该搞清楚数组名的意义了吧。
9.一维数组传参的本质
首先从⼀个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把函数传给⼀个函数后,函数内部求数组的元素个数吗?
我们发现结果不是我们想要的,在func函数内部,sizeof(arr) 的大小不是40了,而是4,这是为什么呢?
- 通过对数组名的学习后我们知道,数组名是数组⾸元素的地址;
- 那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组⾸元素的地址。
- sizeof一个地址,那它的大小肯定是4/8个字节,所以结果才会是1.
- 因此我们可以明白:
一维数组传参,传递的是数组首元素的地址。
当我们接收的参数是数组名的时候,可以写成指针的形式。
void test(int* arr)//参数写成指针形式
{
printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩
}
- 数组元素的访问在编译器处理的时候,也是转换成
⾸元素的地址+偏移量
求出元素的地址,然后解引⽤来访问的。