1.内存和地址
内存其实就相当于我们的宿舍,地址就相当于我们的门牌号,假设我们要就谁谁谁就可以通过门牌号来寻找,C语言中我们就通过地址(C语言中给地址起了一个新名字叫指针)来访问内存。
2.指针变量和地址
我们知道了内存和地址的关系,在C语言中创建变量其实就是向内存申请空间,比如:
上述的代码就是创建了整型变量a,内存中申请4个字节,用于存放整数10,其实每个字节都有地址。
那我们要如何得到a的地址呢?我们就可以通过操作符(&)-取地址操作符。
&a取出的是a所占4个字节中地址较小的字节的地址。
虽然整型变量占用四个字节,我们只要知道了第一个字节的地址,顺藤摸瓜访问到4个字节的数据也是可行的。
2.2指针变量和解引用操作符(*)
2.2.1指针变量
我们通过取地址操作符(&)拿到的地址是一个数值,比如:0x006ffd70,这个数值有时候也是需要储存起来,方便后期的使用,那我们就需要把这样的地址存放在指针变量中。
指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。
2.2.2如何拆解指针类型
我们可以看到pa是int*
这里的pa左边写的是int*,*说明pa是指针变量,而前面的int是在说明pa指向的事整型(int)类型的对象。
那如果有一个char类型的变量ch,ch的地址就要存放在字符类型(char)类型的指针变量中。
2.2.3解引用操作符
我们将地址保存起来,未来是要使用的,那怎么使用呢?这里就要用到解引用操作符(*)。
*pa就是通过pa中存放的地址,找到指向的空间,*pa其实就是a变量;所以*pa=0,这个操作就是把a改成了10.
2.3指针变量的大小
在32位平台下地址是32个bit位,指针变量的大小是4个字节
在64位平台下地址是64个bit位,指针变量的大小是8个字节
指针变量的大小和类型是无关的,只要指针类型的变量在相同的平台下,大小都是相同的。
3.指针变量类型的意义
指针的解引用
1.
2.
调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将第一个字节改为0.
所以我们得出结论:指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字节)。像char*的指针解引用就只能访问一个字节,而int*的指针解引用就能访问4个字节,想一次访问8个字节就可以用double*,一次访问两个字节就可以用short*。
指针+-整数
先看这段代码,调试观察地址的化
我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化。
结论:指针的类型决定了指针向前或者向后走一步有多大。
void* 的指针
在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指 针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进 ⾏指针的+-整数和解引⽤的运算。
例:
这段代码在运行的时候会报下面这种警告:
在上面的代码中,将一个int类型的变量的地址赋给一个char*类型的指针变量。编译器就会给出这个警告,是因为类型不兼容。而使用void*类型就不会有这样的问题。
使用void*类型的指针接收地址:
结果:
这里我们可以看到,void*类型的指针可以接受不同类型的地址,但是无法直接进行指针运算。
那么void*类型的指针到底有什么用呢?
一般void*类型的指针是使用在函数的参数部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得一个函数可以来处理多种类型的数据。
4.const
变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量。但是如果我们希望一个变量加上一些限制,不能被修改就可以使用const修饰。
上述代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我 们在代码中对n就⾏修改,就不符合语法规则,就报错,致使没法直接修改n。
但是我们可以绕过n,使用n的地址去修改,但这样打破了const的修饰是不合理的,所以我们就得用const来修饰指针变量。
结论:const修饰指针变量的时候
const放在*号左边,限制的是*pn,意思是不能通过指着变量pn修改pn所指向的空间的内容,但是pn不受限制
const放在*号右边,限制的是pn变量,也就是pn变量不能修改了,没办法在指向其他变量了但是*pn不受限制,还是可以通过pn来修改pn所指向的对象的内容。
5.指针运算
指针的基本运算有三种,分别是:
指针+-整数
指针-指针
指针的关系运算
指针+-整数
因为数组在内存中是连续存放的,只要知道了第一个元素的地址,就能找到后面的元素。
指针-指针
指针-指针的绝对值是指针和指针之间的元素个数,运算的前提是两个指针都指向同一块空间。
指针的运算关系
指针的运算关系就是指针比较大小(地址比较大小)。数组名就是首元素地址
6.野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针的成因:
指针未初始化
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
指针越界访问
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
指针指向的空间释放
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
如何规避野指针呢?
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。
初始化如下:
小心指针越界
指针变量不再使用时,及时置为NULL,指针使用之前检查其有效性
避免返回局部变量地址
7.assert断言
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报 错终⽌运⾏。这个宏常常被称为“断⾔”。
上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序 继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。
assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和 出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 NDEBUG 。
然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移 除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语 句。
assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。
感谢浏览!