内存
物理存储器和存储地址空间
物理存储器:实际存在的具体存储器芯片。比如:内存条、RAM芯片、ROM芯片。
存储地址空间:对存储器编码的范围。
- 编码:对每个物理存储单元(一个字节)分配一个号码
- 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写
内存地址
- 将内存抽象成一个很大的一维字符数组。
- 编码就是对内存的每一个字节分配一个32位或64位的编号(与32位或者64位处理器相关)。
- 这个内存编号我们称之为内存地址。
内存中的每一个数据都会分配相应的地址: - char:占一个字节分配一个地址
- int: 占四个字节分配四个地址
- float、struct、函数、数组等
指针和指针变量
-
指针就是地址,地址就是指针
int a; int b[]; int* p;
上述代码中
&a
、和b
和p
都是地址,也都是指针。 -
指针变量是存放地址的变量
-
通常我们叙述时会把指针变量简称为指针,实际他们含义并不一样
指针的基础知识
指针变量的定义和使用
定义指针的方式如下:
数据类型* 变量名
其中*
表示该变量为指针变量,数据类型表示该指针变量保存的是哪一种数据类型的地址,也就是指针变量指向哪一种数据类型。
访问指针变量指向的内存空间的数据:
*指针变量
此处的*
为取值运算符。
指针变量保存的是内存地址,*指针变量
就表示访问该地址对应的内存空间。对*指针变量
操作,其实就是读写该内存空间的数据。
#include<stdio.h>
int main() {
int a = 10;
int* p;
p = &a;
printf("a = %d\n", a);
printf("p指向的数据=%d\n", *p);
printf("变量a的地址:%p\n", &a);
printf("指针变量p存的地址:%p\n", p);
return 0;
}
运行上面代码,结果如下:
注意:&
可以取得一个变量在内存中的地址。但是,不能取寄存器变量,因为寄存器变量不在内存里,而在CPU里面,所以是没有地址的。
使用指针访问数据的原理
不同类型的数据在内存中占用不同的字节数,指针变量保存的是数据首个字节的地址。系统根据指针变量保存的地址找到首个字节后,再根据指针指向的数据类型,顺位读取不同的字节长度,从而访问到完整的数据。
比如,一个int
型变量a
占4字节,指针变量int* p
只保存了a
在内存中的首个字节的地址。当操作*p
时,系统会先根据该地址找到首个字节,然后再顺位读取3个字节,总共读取了4个字节,从而取出变量a
的值。
通过指针间接修改变量的值
#include<stdio.h>
int main() {
int a = 10;
int* p;
p = &a;
printf("a = %d\n", a);
printf("p指向的数据=%d\n", *p);
printf("变量a的地址:%p\n", &a);
printf("指针变量p存的地址:%p\n", p);
*p = *p + 1;
printf("p指向的数据加1后,a = %d\n", a);
printf("p指向的数据加1后,p指向的数据=%d\n", *p);
return 0;
}
运行上面的代码,结果如下:
在定义指针类型时一定要和指向的变量类型是一样的才行。
#include<stdio.h>
int main() {
char a = 97;
int* p = &a;
printf("a的值=%d\n", a);
printf("*p的值=%d\n", *p);
return 0;
}
运行上面的代码,结果如下:
上面代码中,虽然指针变量p
保存的是字符变量a
的地址,但是p
是指向int型数据的,所以在找到a
的地址后还会顺位读取3个字节的数据。
指针大小
windows中数据存储采用小端对齐的方式。也就是,数据的低位放在地址小的内存中。
指针也是一种数据类型,所以可以使用sizeof()
测量指针的大小,得到的总是:4或8。
这是因为指针存储的是内存地址。在32位处理器中内存地址是一个32位的编号,在64位处理器中内存地址是一个64位的编号。
所以在32位处理器中sizeof(指针)
得到的总是4,在64位处理器中sizeof(指针)
得到的总是8。
在visio studio中可以在下图中选择基于32位或64位的处理器编译,从而使sizeof(指针)
得到不同的结果。
int main() {
int* p1;
int** p2;
char* p3;
char** p4;
printf("sizeof(p1) = %d\n", sizeof(p1));
printf("sizeof(p2) = %d\n", sizeof(p2));
printf("sizeof(p3) = %d\n", sizeof(p3));
printf("sizeof(p4) = %d\n", sizeof(p4));
printf("sizeof(double *) = %d\n", sizeof(double*));
return 0;
}
上述代码基于32位处理器编译运行后结果如下:
上述代码基于64位处理器编译运行后结果如下:
野指针和空指针
野指针
任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统可能不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针指向的内存区域可能会出问题。
int main() {
int* p = 100;
/*
操作系统将0~255的内存空间作为系统占用,
不允许被访问操作
*/
printf("%d\n", *p);
return 0;
}
空指针
为了标志此指针变量没有指向任何变量(空闲可用),C语言中,可以把NULL
赋值给此指针,这样就标志此指针为空指针。
int *p = NULL;
NULL
是一个值为0的宏常量:
#define NULL ((void *)0)
空指针可用于条件判断
int* p = NULL;
if (p == NULL) {
printf("我是一个空指针\n");
}
else {
printf("我不是一个空指针\n");
}
万能指针void *
万能指针可以接收任意数据类型的地址。但不能直接访问万能指针指向的内存空间,因为系统找到数据的首个字节后,不知道接下来要顺位读取多少个字节。
必须先将万能指针强转为某个具体类型的指针变量后才可以访问。
#include<stdio.h>
int main() {
int a = 10;
void* p = &a;
printf("变量a的值:%d\n", a);
//printf("万能指针p指向的值:%d\n", *p);//不能直接访问万能指针指向的内存空间
printf("万能指针p指向的值:%d\n", *(int*)p);
*(int*)p = *(int*)p + 1;
printf("加1后,变量a的值:%d\n", a);
printf("加1后,万能指针p指向的值:%d\n", *(int*)p);
return 0;
}
执行上面代码,结果如下:
const修饰的指针变量
const 数据类型* 变量名
:const
修饰数据类型,说明该指针变量指向的内存空间可读不可写。
数据类型* const 变量名
:const
修饰变量名,说明该指针变量保存的内存地址可读不可写。
const 数据类型* const 变量名
:const
既修饰数据类型又修饰变量名,说明该指针变量指向的内存空间可读不可写,保存的内存地址也是可读不可写的。
#include<stdio.h>
int main() {
const int a = 10;
//a = 20;//err
int b = 20;
const int* p = &a;
printf("指针变量p指向的值:%d\n",*p);
//*p = 30;//err
p = &b;//ok
printf("指针变量p指向的值:%d\n", *p);
int* const p2 = &a;
printf("指针变量p2指向的值:%d\n", *p2);
//p2 = &b;//err
*p2 = 30;//ok
printf("指针变量p2指向的值:%d\n", *p2);
printf("a的值:%d\n", a);
const int* const p3 = &b;
//*p3 = 40;//err
//p3 = &a;//err
printf("指针变量p3指向的值:%d\n", *p3);
b = 40;
printf("指针变量p3指向的值:%d\n", *p3);
return 0;
}
运行上面代码,结果如下: