C语言学习第008课——内存和指针

内存含义

  • 存储器:计算机的组成中,用来存储程序和数据,辅助CPU进行运算处理的重要部分
  • 内存:内部存储器,暂存程序/数据——掉电丢失,SRAM DRAM DDR DDR1 DDR2 DDR3
  • 外存:外部存储器,长时间保存程序/数据——掉电不丢失,ROM ERRROM FLASH(NAND,NOR) 硬盘 光盘
    内存是沟通CPU与硬盘的桥梁,暂时存放CPU中的运算数据,暂存与硬盘等外部存储器交换的数据

    扩展:电脑的系统属性里面,显示的安装内存8G 这部分就可以分成8*1024*1024*1024个Byte,每个Byte都有自己的编号,
    32位的系统,内存编号表示为无符号整形 unsigned int 占4个字节 最大代表2的32次方
    64位的系统,内存编号表示为__int64 占8个字节 最大代表2的64次方
    所以32位的系统如果安装上太大的内存条也没有特别大的提升,因为内存虽然大,但是可以编号的最大值卡死在了2的32次方上,剩余的内存无法编号便无法使用

物理存储器

就是实际存在的具体存储器芯片,比如主板上装插的内存条、显卡上的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 = 10int* 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 int4printf("%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也可以修饰指针变量,有三种形式

1const int* p = &a; 
const修饰指针类型int*,可以修改指针变量的值,不可以修改指针指向内存空间的值*p

2int* const p = &a; 
const修饰指针变量p,可以修改指针指向内存空间的值*p,不可以修改指针变量的值p

3const 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
()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值