就是实际存在的具体存储器芯片,比如主板上装插的内存条、显卡上的RAM芯片、各种适配卡上的RAM芯片和ROM芯片
是对存储器编码的范围,我们在软件上常说的内存是指这一层含义
编码:对每一个物理存储单元(一个字节)分配一个号码
寻址:可以根据分配的号码,找到相应的存储单元,完成数据的读写
将内存抽象成一个很大的一维字符数组
编码就是对内存的每一个字节分配一个32位或者64位的编号(与32位或者64位处理器有关)
这个内存编号我们称之为内存地址
内存中的每一个数据都会分配相应的地址
char 占一个字节分配一个地址
int 占4个字节,分配4个地址
float struct 函数 数组等
内存地址打印:
int a = 0xaabbccdd;
printf("%p\n",&a);//打印结果:0x0059FE28
调试,根据内存地址查看值发现0x0059FE28 =》 dd
0x0059FE29 =》 cc
0x0059FE2a =》 bb
0x0059FE2b =》 aa
问题:发现aabbccdd在内存里面存储情况,随着内存地址编号增大,好像数据是倒过来存的,不是先存aa而是先存dd
这是因为windows做数据存储是采用小端对齐,也就是变量a中,aa是高位,dd是低位,所以aa存储在内存地址编号高的地方,dd存储在内存地址编号低的地方,一般情况下,比如做嵌入式开发,从Linux拿出来数据显示在windows中,就需要做大小端数据转换
定义指针变量存储变量地址:
int a = 10;
int* p; * 表示一种新的数据类型,就是指针数据类型
p = &a; 将a的地址赋值给p,p就是指向a的指针变量
printf("%p\n",&a);两个打印的结果是一样的
printf("%p\n",p);
在内存中,变量a被存放在0x0021地址中,当定义int* p,并且将&a赋值给p时,在内存中有另外一块地址(0x0087)存放着p的值,
也就是a的地址值0x0021,所以此处p的值为0x0021,p的地址为0x0087,
扩展:如果此处再定义一个指针变量,用来存放p的地址,则这个指针变量叫做二级指针变量,一级指针存放变量内存地址,二级指针存放指针内存地址
通过指针间接修改变量的值:
int a = 10;
int* p;
p = &a;
*p = 100; 取地址运算符 * 代表从p指向的地址中获取到值
printf("%d\n",a);
printf("%d\n",*p); 两个打印的结果一样
int a = 10;
int* p = &a;
printf("%d\n",sizeof(int*));
printf("%d\n",sizeof(p)); 两个打印结果是一样的
指针类型存储的都是内存地址,32位系统一个指针类型大小占4Byte,64位系统一个指针类型大小占8Byte
一个char类型的数据占内存是1Byte,但是指向他的指针占的内存是4Byte或者8Byte,这跟操作系统是多少位有关系
所以指针类型所占的大小跟是什么数据类型的指针没有关系,而跟系统是多少位的有关系
问题:既然指针变量都是无符号16进制整形数,都占4个字节大小,能不能这样写:
int a = 10;
int p = &a;
不用int*定义指针变量
答案:是可以这样写,而且也不会报错,但是这个时候p就是一个整形变量了,而不是一个指针变量,当用到取值符号取值的时候就该哭(报错)了
但是如果非要这么写也可以,需要这样:
*(int*)p = 100;
先将p强制转换成指针变量,这样p就成了一个指针变量了,再取值进行赋值操作就可以了
问题:既然指针变量都是无符号16进制整形数,都占4个字节大小,能不能这样写:
char ch = 97;
int* p = &ch;
printf("%p\n",&ch);
printf("%p\n",p);
答案:两者打印结果是一样的,都为变量ch的地址,这样写不会报错,但是当通过*p给ch重新赋值的时候就会出错,如下:
*p = 100; 给*p赋值为100
printf("%d\n",*p);
printf("%d\n",ch);
程序要么数据不对,要么会奔溃,这是因为int类型的指针p指向了char类型的变量,当用取值符*取值的时候,他会按照int类型的变量去读数据,读取4个
字节的数据,而我们的数据是char类型的,只占一个字节,所以数据会出错,进而导致奔溃。
所以在定义指针类型指向变量地址的时候,一定要和变量类型对应上
野指针
int* p = 100; int类型的指针变量p,指向内存编号为100的地址,词句并不会报错
printf("%d\n",*p); 打印p所指向地址的值,报错了,因为操作系统将0-255作为系统占用空间,不允许访问操作
以上代码中指针变量p就是一个野指针,野指针表示指针变量指向一个未知的空间,未知表示不是自己申请的空间。
操作野指针对应的内存空间可能会报错,也可能不报错
程序中允许出现野指针
不建议直接将一个变量的值直接赋值给指针
通常写代码,定义一个指针,之后经过一系列操作,这个指针指向了另外一片未知的空间,他就成为了野指针,所以一般情况下写代码,操作完指针之后都需要将其置位NULL
空指针
空指针是指内存地址编号为0的空间
int* p = NULL;
空指针对应的地址不允许读和写,因为0对应的地址也包含在0-255,操作空指针对应的空间一定会报错
空指针可以用于条件判断
万能指针void*
万能指针可以指向任意变量的内存空间
int a = 10;
void* p = &a; void*可以接收任意类型变量的内存空间
*p = 100; 这一行会报错,错误提示:非法的间接寻址,表达式必须是可修改的左值,这是为什么呢?
//打印两行内容看看
printf("%d\n",sizeof(void*)); 打印结果为4 正常,因为地址值都是unsigned int 占4位
printf("%d\n",sizeof(void)); 会报错,编译不通过,提示不允许使用不完整的类型,void是不完整的,
说明都没有办法获取sizeof(void)的大小,更别说去用取值符获取他的值了,连取几个字节的数据都不知道
想要通过万能指针修改变量的值时,需要找到变量对应的指针类型,也就是需要知道这个变量到底是什么数据类型的
*(int*)p = 100; 先把p强转成int类型的指针,然后再用取值符取值并且改变他的值。
万能指针一般用于作为函数的形参,并且同时形参还需要一个,告诉函数该万能指针指向的数据类型占多少个字节
万能指针可以赋值给任意一个指针变量:
int a = 10;
void* p1 = &a;
int* p2 = p1;
但是实际操作修改值的时候,还是要将原来的指针类型匹配上。
const修饰的指针类型
const常常用来修饰常量,其修饰的变量是不能直接被修改的,但是可以使用指针的方式来间接修改他的值
const int a = 10;
int* p = &a;
*p = 100;
printf("%d\n",a);打印结果为100
但是通过#define定义的常量是没办法修改的,这是因为const定义的常量是在栈区存储的,而#define定义的常量是存放在数据区的,
栈区的数据是可以修改的,数据区的数据是不能修改的
当然,const也可以修饰指针变量,有三种形式
1、const int* p = &a;
const修饰指针类型int*,可以修改指针变量的值,不可以修改指针指向内存空间的值*p
2、int* const p = &a;
const修饰指针变量p,可以修改指针指向内存空间的值*p,不可以修改指针变量的值p
3、const int* const p = &a;
const修饰指针类型int* 也修饰指针变量p,不可以修改指针变量的值,也不可以修改指针指向内存空间的值
但是之前说了,普通的常量,可以使用一级指针进行修改,同样的,一级指针常量,可以使用二级指针进行修改
int a = 10;
int b = 20;
const int* const p = &a;
int** pp = &p 定义一个二级指针pp,将指针p的地址赋值给二级指针变量pp
*pp = &b; 使用一个取值符*获取pp的值,也就是降维度成为一级指针,改变他的值为b的地址可以将*p的值改变为20
**pp = 100; 也可以直接使用两个取值符**,降维度成为变量,直接给变量赋值100 也可以
结论:通过二级指针可以修改一级指针的值,也可以修改一级指针指向内存空间的值,同样的道理,三级指针可以修改二级指针的值,也可以修改二级
指针指向内存空间的值。
数组名字是数组的首元素地址,但他是一个常量,所以数组名不能修改
int arr[] = {
1,2,3,4,5,6,7,8,9,10};
int* p = arr; 因为arr本身就是一个地址,所以这样赋值是没有问题的,而且两者的地址也是一样的
printf("%p\n",p);
printf("%p\n",arr);
for(int i = 0;i < 10;i++){
printf("%d\n",arr[i]);
}
可以看到,这里利用数组名arr和一个下标i就可以遍历整个数组的元素,那么p和arr一样,也是个地址,使用p和下标i是不是也能遍历所有的元素呢?
for(int i = 0;i<10;i++){
printf("%d\n",p[i]); 也可以打印出来整个数组的元素
}
既然arr和p都是地址,那么这样写来获取数组的第一个元素也是可以的:
printf("%d\n",*arr);
使用地址arr和角标i就可以遍历整个数组中的元素,arr是个常量不能改变,变得只是i,那么我们可以认为i是一个偏移量,那么这样打印一下
printf("%d\n",*(arr+4)); 发现打印的结果是5
那么:
printf("%d\n",*(arr+1)); 打印的结果为1
也就是说 *(arr+1) 和arr[1]结果是一样的
所以数组中数组名arr和角标i的组合也可以写成地址p加偏移量i再用*取值
那么这里有一个问题,这里int类型的数组一个元素占用的内存空间是4个字节,我为什么地址+4的时候,直接跑到了第5个元素上去,而地址+1的时候却在第二个元素上?
这是因为:
指针变量+1 等同于 内存地址+sizeof(数据类型)
打印+1的地址来证明
int arr[] = {
1,2,3,4,5,6,7,8,9,10};
int* p = arr;
p++;
printf("%p\n",arr);
printf("%p\n",p);
打印结果,p比arr多4
两个指针相减,得到的结果是两个指针的偏移量,步长,所有指针类型,相减的结果都是int类型
指针p和数组名arr的区别:p是变量,可以改变,arr是常量,不能改变
sizeof§和sizeof(arr)不同,p是一个指针,4个字节大小,arr是一个数组,40个字节大小(按照以上代码计算)
练习:写一个函数,参数是数组,打印数组的元素个数
void GetCount(int arr[]){
int len = sizeof(arr)/sizeof(arr[0]);
printf("len = %d\n",len);
}
调用该函数的时候,发现无论arr的元素个数是多少,最后打印的值都是1,
这是因为数组名本身既是一个数组名,也是一个指针,但当他作为函数的参数时,会退化为指针,丢失数组的精度
也就是说,一旦arr被传进这个函数中,无论arr里面元素个数是多少,sizeof(arr) = 4,sizeof(arr[0]) = 4 ,所以结果始终是1
所以一般情况下,数组名作为函数的参数时,同时会将数组元素个数也传进来,要不然进了函数里面没办法计算
学到这里,就应该明白例如冒泡排序的函数:
void BubbleSort(int arr[],int len){
for(int i = 0;i < len; i++){
for(int j = 0; j< len-1-i; j++){
if(arr[j] > arr[j+1]){
int temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
}
这里面执行的arr[j]或者arr{j+1}里面的arr其实都是指针,而不是一个数组名了,我们知道使用arr[j+1]这样的方式也可以获取指针的值
所以上面的代码也可以写成:
void BubbleSort(int arr[],int len){
for(int i = 0;i < len; i++){
for(int j = 0; j< len-1-i; j++){
if(arr[j] > arr[j+1]){
int temp = *(arr+j+1);
*(arr+j+1) = *(arr+j);
*(arr+j) = temp;
}
}
}
}
两份代码原理一样,但是一般不写这种,阅读性不强
指针相加不是简单的整数相加
如果是一个int*,+1的结果是增加一个int的大小
如果是一个char* +1的结果是增加一个char的大小
练习:字符串复制
void my_strcpy(char* dest, char* ch){
int i = 0;
while(ch[i] != '\0'){
这一行判断中也可以写成ch[i] != 0 还可以写成ch[i]
dest[i] = ch[i];
i++;
}
dest[i] = 0;
}
void main(){
char ch[] = "hello world";
char dest[100];
my_strcpy(dest, ch);
return 0;
}
方法二:指针操作,其实原理还是数组原理
void my_strcpy(char* dest,char* ch){
int i = 0;
while(*(ch+i)){
*(dest+i) = *(ch+i);
i++;
}
*(dest+i) = 0;
}
方法三:指针操作,真正意义上的指针操作
void my_strcpy(char* dest,char* ch){
while(*ch){
*dest = *ch;
dest++; 指针+1,相当于指向数组下一个元素,内存地址变化了sizeof(char)大小
ch++;
}
*dest = 0;
}
方法四:终极精简版本,原理和方法三一样,只不过更加简洁而已
void my_strcpy(char* dest,char* ch){
while(*dest++ = *ch++); 这一行代码,包含的操作有:*dest = *ch;
} dest++; ch++;
while(*dest!=0)
例子:
int main(){
int arr[] = {
1,2,3,4,5,6,7,8,9,10};
int* p = &arr[3];
printf("%p\n",arr); 0x0028FF14
printf("%p\n",p); 0x0028FF20
return 0;
}
以上代码运行结果中,arr为数组的首元素的地址,p为下标为3的元素的地址,两者相差12,计算步长的话:
printf("step = %d\n",p - arr); step= 3 因为计算的是int类型指针的步长
如果这样改一下代码:
int main(){
int arr[] = {
1,2,3,4,5,6,7,8,9,10};
int* p = &arr[3];
p--;
p--;
p--;
printf("%p\n",arr); 0x0028FF14
printf("%p\n",p); 0x0028FF14
return 0;
}
打印的两个地址一样了,p–的执行过程就是指针向后退一格指针单位,具体指针单位是多少,和指针类型有关系
例如int类型的指针,自减运算之后指针向后偏移4字节,char类型的指针,自减运算之后指针向后偏移1字节
以上自减运算同样可以写为:指针变量 - 偏移量,例如在指针p指向下标为3的元素时,我想要获取下标为1的元素的值:
int main(){
int arr[] = {
1,2,3,4,5,6,7,8,9,10};
int* p = &arr[3];
printf("%d\n",*(p-2));
return 0;
}
运行结果为2
目前结论为,指针可以进行+运算和-运算,但是仅限于偏移量
注意:指针变量和指针变量进行加法,减法,乘除,取余都是无意义的
指针变量之间可以进行比较大小操作,等于和不等于
定义:他是数组,数组中的每个元素都是指针类型
int main(){
int a = 10;
int b = 20;
int c = 30;
int* arr[3] = {
&a,&b,&c};
return 0;
}
遍历数组中的指针对应的值:
for(int i = 0;i < sizeof(arr)/sizeof(arr[0]);i++){
printf("%d\n",*arr[i]);
}
例子:定义3个int类型的一维数组a,b,c,每个数组3个元素,a b c也就是3个int类型的指针变量,组成一个指针数组arr
int a[] = {
1,2,3};
int b[] = {
4,5,6};
int c[] = {
7,8,9};
int* arr[3] = {
a,b,c};
要求遍历arr中每个指针对应的值?
for(int i = 0;i < 3; i++){
printf("%d\n",*(arr[i]));
}
打印结果为 1 4 7 分别为a[0],b[0],c[0],那么思考怎么给指针加一个偏移量呢?
printf("%p\n",arr[0]); 指针a
printf("%p\n",a); 数组a
printf("%p\n",&a[0]); 数组a的首元素地址
打印这三个值,结果相等
既然arr[0]指针和a相等,那么我要获取到a[1]的值,是不是可以使用arr[0][1]?
for(int i = 0;i < 3; i++){
for(int j = 0;j < 3; j++){
printf("%d ",arr[i][j]);
}
puts("");
}
结果可以打印出所有的元素,可以看出来,该遍历方式是以二维数组的思路去解的,其实指针数组实际上就是一个二维数组的特殊模型
上面说到arr[0]指针和a相等,a也是一个指针,那么可以使用指针加偏移量的方式来获取全部的元素
for(int i = 0;i < 3; i++){
for(int j = 0;j < 3; j++){
printf("%d ",*(arr[i]+j));
}
puts("");
}
结果也可以打印出所有的元素,那么再思考,j可以当做偏移量,那么i应该也可以:
for(int i = 0;i < 3; i++){
for(int j = 0;j < 3; j++){
printf("%d ",*(*(arr+i)+j)); arr是指向数组a的指针,加i偏移量之后取值,得到指针a,继续加偏移量取值获取到a[0]的值
}
puts("");
}
可以看到使用到了二级指针,其实指针数组对应于二级指针
所以这个时候就不能写:
int* p = arr;
这样写虽然没有问题,但是指针层级会有问题,arr是一个二级指针,p是一个一级指针
C语言允许有多级指针存在,在实际的程序中,一级指针最常用,其次是二级指针,
二级指针就是指向一个一级指针的指针
int a = 10;
int* p = &a;
int** pp = &p;
int*** ppp = &pp;
以上代码中,存在如下关系
*ppp == pp == &p
(二级指针) (二级指针) (二级指针)
**ppp == *pp == p == &a
(一级指针)
***ppp == **pp == *p == a
(值)