020+limou+C语言指针和数组

本文详细介绍了C语言中的指针和数组,包括指针的基础理解、编址、解引用以及与数组的关系。数组被阐述为一种特殊的指针,但二者本质上不同,且在内存管理和寻址机制上有独特之处。文章还涉及了栈随机化对直接访问地址的影响,以及函数参数传递时数组和指针的使用。最后,讨论了函数指针和函数指针数组的概念。
摘要由CSDN通过智能技术生成

0.前言

您好,这里是limou3434的一篇博客,感兴趣您可以看看我的其他博文系列。本次我主要给您带来了有关C语言数组和指针的相关知识。

1.基础指针理解

1.1.指针基础

让我们先来厘清左右值的概念。

int a = 10;//定义并初始化a
//开辟空间给a使用,这叫“定义”
//a的内容从一开始就是10,这叫“初始化”

a = 20;//使用的是a的空间,即“左值”
int b = a;//使用的是a的内容,即“右值”

地址是为了标识空间的所在地,而地址本质是数据,数据就可以存储在变量空间里面。而保存地址数据的变量就叫指针变量,地址数据又可以叫做一个“指针”。

int a = 10;//int类型的整型变量a,存储10
int* pa = &a;//int*类型的指针变量pa,存储a的地址数据/指针

int* p;//变量p 
p = (int*)0x123;//存入地址数据

指针和指针变量的混淆,和“int a = 10;int b;b = a”中说“a赋给b”的说法是一样的,正确的说法应该是“a的内容赋给b”。

因此实际上一个变量应该理解为“变量 = 空间(左值)+内容(右值)”,在使用变量的过程中会根据上下文使用变量的不同部分。(因此“指针<==>指针变量”是发生在使用右值的时候)

1.2.对编址理解

但是按照上述理解,在记录地址数据之前,首先要存在地址。即:内存空间必须先标记好各自的地址,这就涉及到对内存空间编址问题。

CPU是计算硬件而(CPU在内存中寻址的基本单位是字节),内存是临时存储硬件,两者之前靠着数据总线来连接(实际上应该是地址总线、数据总线、控制总线连接的,但是根据厂家实现的不同可能会用共用)。而在32位机器下,有32根这样的线(理解为“电线”,但是这些“电线”是内嵌在主板上的),每一根线用来传输电信号(1或0),因此同时可以传输32个bit位,一共有232个地址,因此有232个字节被成功编制,内存空间大小就是4GB。

同理64位操作系统也是一样的。

因此指针的存在就是为了加快CPU的寻址效率。

编制,并不是把每个字节的地址记录下来,而是通过硬件设计来完成的。

1.3.指针解引用

* 解引用

*p就是p指向的变量,这样理解解引用,然后解释使用的是左值还是右值会更加方便.
*是操作符的情况下,*p使用的就是p的右值,这是一种间接访问的方式。换句话来说“*”操作实际上也可以直接对一个地址值解引用,这叫直接访问。

  • 栈随机

直接访问地址的方法对于现代编译器来说已经不可能,这是因为编译器会进行栈随机化

#include <stdio.h> 
int main() 
{ 
  	int a = 10; 
  	printf("%p", &a);//地址会发生变化,这种现象就是栈随机化 
 	 return 0; 
} 
//这是一种栈保护机制,有兴趣的还可以了解一下“金丝雀技术”
//因此靠指定的地址访问上一次编译的变量基本不可能
//实际上,全局变量的也大概率会有类似的地址随机现象
  • 一段有趣的代码:指针指向自己
int main() 
{ 
  	int* p = NULL;//开辟了一个空间(四个/八个字节)给与p使用,接下来由p来维护这块空间,这块空间被初始化为NULL 
  	p = (int*)&p;//p存储了p自己的地址,p指向自己 
  	*p = 10;//通过p存储的地址,解引用得到的还是自己p,p这次存放了10这个数据 
  	p = 20;//这次p不再存储自己,存储了20这个数据 
	return 0;
}

1.4.*p = NULL

NULL、0、'\0’在数字层面上是0,但是类型层面上是不同的

2.基础数组理解

首先强调一点,指针和数组没有任何关系,只不过操作方式很像,不要将两者的概念混淆。数组不是C语言特有的,数组是具有相同数据类型的集合。

2.1.数组基础

元素类型 数组名 [元素个数] = { 元素列表 };//元素列表如果直接填0,就会默认给空间填充0

无论是什么数组其实都是一维数组,实际上不存在所谓“二维数组”和“三维数组”

2.2.数组的内存分布

在x86系统中,根据“栈向下生长”的原理,从上往下定义变量,其地址从上往下递减。(但是在不同环境下,有可能不一样,这取决于具体的系统架构)

而数组是从左到右地址递增,因此实际上不应该认为数组是一个个独立元素在开辟空间,应该是整体开辟空间,整体释放。

在数组开辟好空间后,然后从低地址将空间给与“0~size-1”下标去访问。(但是无论是在哪个环境C语言的数组内存布局都是一样的)

#include <stdio.h>
int main()
{
	int a = 10;
	int b = 100;
	int c = 50;
	printf("%p\n", &a);
	printf("%p\n", &b);
	printf("%p\n", &c);

	printf("\n");

	int number[4] = { 0 };
	for (int i = 0; i < 4; i++)
	{
		printf("&number[%d] = %p\n", i, &number[i]);
	}
	return 0;
}

2.3.&a[0]和&a的区别

  • a代表数组整体的时候有两种情况&arr或sizeof(arr),其他情况arr都是首元素地址
  • 注意&a[0]和&a的数值上是一样的,但是在数据类型上是不一样的
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void Function(int arr[], int size)//接受的是int*的数据,即指针
{
	printf("%zd\n", sizeof(arr));//32位为4,64位为8
	for (int i = 0; i < size; i++)
	{
		printf("%d ", i);
	}
}
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6 };
	int size = sizeof(arr) / sizeof(arr[0]);
	Function(arr, size);//数组传参传的是其首元素的地址
	return 0;
}//如果不进行指针传参,就会发生硬拷贝,在调用函数的需要多开辟临时空间把数组全部拷贝,这会导致空间的浪费,并且效率底下。而传指针的大小在固定的平台有固定的大小。 
  • 一段有趣的代码
#include <stdio.h>
int main()
{
	int a[5] = { 1, 2, 3, 4, 5 };
	int* ptr = (int*)(&a + 1);//(&a)是整个数组的地址,+1就跳过整个数组,此时指针ptr指向的是最后一个数组元素的后一个int大小的空间,直接访问这个空间是非法的
	printf("%d %d\n", *(a + 1), *(ptr - 1));
	//a+1是a数组的第二个元素,解引用就得到数组的第二个元素2
	//ptr-1得到的是数组的最后一个元素5
	return 0;
}

2.4.数组名a做为左值和右值的区别

int main()
{
	char arr1[3] = { 1, 2, 3 };//初始化可以
  
  	char arr2[3];//只定义不初始化
  	arr = { 1, 2, 3 };//不可以定义后才赋值,C语言不允许将arr2作为左值使用
}

在C语言中,数组名是一个指向数组首元素的常量指针。因此,数组名虽然可以作为右值使用,但不能作为左值使用。这是因为,当我们试图向数组名赋值时,相当于试图修改一个常量指针(const,发生权限放大)的指向,而这是非法的。一个数组名代表的是一个固定的内存地址,它不能被修改。

2.5.求数组大小的标准写法

int arr[] = { 1, 2, 3, 4, 5, 6 };
int arrSize = sizeof(arr) / sizeof(arr[0]); //这里写成0是因为数组一定有一个元素,这样的表达式是绝对不会错的(保险)

3.指针和数组的关系

  • 两者不是一个东西,看下面的代码就可以证明
char* str1 = "hello word";//这个hello word是存储在字符常量区,str的空间是在栈上开辟的,因此如果栈销毁空间,“hello word”也有可能仍旧存在
char str2[] = "hello word";//这个“hello word”是存储在栈上的,如果栈销毁了,则“hello word”也会被销毁 
char* str1 = "hello word";
int len1 = strlen(str1);
for(int i = 0 ; i < len1; i++)
{
	printf("%d", *(str1+i));//从访问角度来看,这是先找到栈里的变量str1,使用str1的右值+i,得到一个新的地址,解引用这个地址,找到字符常量区里“hello word”第i+1个字符
}
char str2[] = "hello word";
int len2 = strlen(str2);
for(int j = 0 ; j < len2; j++) 
{ 
  	printf("%d", *(str2+j));//从访问角度来看,这是先找到栈里的str2指向的字符'h',然后通过+i,访问存储在栈内的有字符构成的字符串
}
int main()
{
	//以下代码会报警
	//int arr[2] = {1, 2};
	//arr[-1];
	//以下代码不会报警
	int a = 10;
	int* arr = &a;
	arr[-1];
	return 0;

	//上述代码也侧面说明了指针和数组只是在访问地址的形式是一样的,但是本质还是有所区别的
}

虽然大家的写法(指使用“[]”和“*”)是一样的,但是寻址方案细节是不一样的(数组的是一种直接访问,没有像指针需要间接访问),这样的话就可以直接证明两者并不是用一个东西。

  • 那为什么C语言要这么设计呢?答案是方便

由于函数是C语言中最常用的(面向过程语言),如果不这么设计,在传递数组形参时,就需要程序员不断更改他的习惯(如数组的使用“[]”访问,指针的使用“*”访问),时间长了这样出错概率比较大。因此数组和指针的访问方式是通用的,都可以使用“[]”和“*”,但是其本质概念依旧是没有任何关系的。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void Function(int arr[], int size)//接受的是int*的数据,即指针
{
	printf("%zd\n", sizeof(arr));//32位为4,64位为8
	for (int i = 0; i < size; i++)
	{
		printf("%d ", *(arr+i));
	}
	printf("\n");
}
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6 };
	int size = sizeof(arr) / sizeof(arr[0]);
	Function(arr, size);//数组传参传的是其首元素的地址
	for (int i = 0; i < size; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}
  • “定义为数组,声明为指针”和“定义为指针,声明为数组”都是行不通过的
//源文件1
//定义为数组
char a[6] = { 1, 2, 3, 4, 5, 6 };
//定义为指针
char* b = "abcdef";
//--------------------------------
//源文件2
//声明为指针
extern char* a;//不合法
//声明为数组
extern char b[];//不合法

4.指针数组和数组指针

4.1.指针数组:是数组

例如“int* pa1[10]”先找变量名pa1,由于[]优先级高于*,pa1先和“[]”结合,所以pa1是一个数组,因此int*修饰的是数组的内容,即每个元素

4.2.数组指针:是指针

例如“int(pa2)[10]”先找变量名pa2,由于“”先和pa2结合,所以pa2是一个指针,int[10]是一个匿名的大小为10的数组,可以看作是一种数组类型

5.强制转化

  • C语言的强制类型转化,只是存在内存里的数据被暂时改变了看待它的方法,而本身的数据没有任何变化。
  • 强制类型转化的作用:1.显式的转化 2.会使代码更加明确(一是让程序员明确这段代码强转是有意为之、二是让编译器明确这段代码不是发生错误的转化,取消警告)
  • 例子一
#include <stdio.h>
int main()
{
	int a = 0x11223344;
	int* p = &a;
	printf("%x\n", *(char*)p);
	return 0;
}
  • 例子二
struct Test
{
	int Num;
	char *pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p = (struct Test*)0x100000;//结构体大小为20字节
int main()
{
	printf("%p\n",p + 0x1);//输出0x100014(加1相当于+20)
	printf("%p\n", (unsigned long)p + 0x1);//输出0x100001(加1就是+1,因为这个强转不是强转为指针类型)
	printf("%p\n", (unsigned int*)p + 0x1);//输出0x100004(加1相当于+4)
}
  • 例子三
#include <stdio.h>
//注意可能需要在x86环境下才能运行下面的代码
int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int* ptr1 = (int*)(&a + 1);//这里指向数组元素4后面的地址
    int* ptr2 = (int*)((int)a + 1);//a本来是首元素地址,被强转为int后+1,因此指向的是小端模式中a数据的第二个字节数据
    printf("%x, %x\n", ptr1[-1], *ptr2);
    //因此这里打印4和2000000
    //之所以是2000000,是因为:
    //后者是从“小端模式中a数据的第二个字节数据”开始读取4个字节,
    //内存里小端模式的数据为“00 00 00 02”,
    //逆向输出字节序就是“20 00 00 00”
    return 0;
}

6.多维指针和多维数组

在C语言中是实际上只有一维数组和一维指针

6.1.多维指针

指针变量也是变量,任何变量都有地址,好了没了,就是这么简单。
int a = 10; int pa = &a; int ppa = &pa; int* *pppa = &ppa;

6.2.多维数组

  • 重新理解一下数组,实际上在C语言中只有一维数组
数据类型 数组名 [数组大小]
  • 一些多维数组理解为一维数组的例子
char a[4][3];//a先和第一个[]结合,而a数组有4个元素,每一个元素都是“int[3]”类型
char b[2][4][3];//b先和第一个[]结合,而b数组有2个元素,每一个元素都是“int[4][3]”类型
  • 而且无论是多少维度的数组在内存里都是连续线性排布的
  • 因此推荐不要理解为多维数组,而是统统理解为一维数组,这样理解在后续的代码中就能更加深入理解代码
int main()
{
	int arr[4][3] = { 0 };//全部初始化为0
  	int(*p)[3] = arr;//arr是数组的首元素地址,首元素的数据类型是int[3]
}
  • 例子一
#include <stdio.h>
int main()
{
	int a[3][4] = { 0 };
	printf("%zd\n", sizeof(a));
	//a代表整个数组,大小为3*4*4=48
	printf("%zd\n", sizeof(a[0][0]));
	//a[0][0]是指a数组的第一个数组元素的第一个元素,类型为int,故大小为4
	printf("%zd\n", sizeof(a[0]));
	//a[0]是指a数组的第一个数组元素,sizeof(数组名)是求出整个数组的大小,数据类型为int[4],故为4*4=16
	printf("%zd\n", sizeof(a[0] + 1));
	//a[0]是a数组的第一个数组元素,也是数组名,受到+1的影响,不符合sizeof(数组名)的规则,所以这里的鹅a[0]代表数组名为a[0]的数组的首元素地址,+1还是地址,所以大小为4或者8
	printf("%zd\n", sizeof(*(a[0] + 1)));
	//根据上一条语句的结论,可以得出a[0]+1得到的是a[0][1]这个元素,元素类型为int,故输出4
	printf("%zd\n", sizeof(a + 1));
	//a受到+1的影响,不再代表整个数组,而是数组名为a的数组的首元素地址,+1后还是地址,故输出4或8
	printf("%zd\n", sizeof(*(a + 1)));
	//a代表以a为名数组的首元素地址,+1后得到a[1]的地址,解引用得到a[1],满足sizeof(数组名),而数组类型为int[4],故输出16
	printf("%zd\n", sizeof(&a[0] + 1));
	//&是取地址,得到地址后+1还是地址,所以输出4或8(实际上取地址得到的数据其数据类型为int(*)[4],再+1得到的就是a[1]的地址)
	printf("%zd\n", sizeof(*(&a[0] + 1)));
	//&a[0]是以a为名数组的第一个元素的地址,+1后就得到第二个元素a[1]的地址,这里解引用就得到a[1],由于a[1]是一个数组名字,满足sizeof(数组名),故输出16
	printf("%zd\n", sizeof(*a));
	//a代表以a为名的数组的首元素地址,解引用得到的就是以a为数组名的数组的首元素,即a[0],满足sizeof(数组名),其类型为int[4],输出16
	printf("%zd\n", sizeof(a[3]));
	//a[3]是一个数组名,也是以a为名数组的第四个元素,满足sizeof(数组名),故输出4*4=16(但是它越界了……但是只是查看这个数据是不会报错的)
  	return 0;
}
  • 例子二
//要深刻理解数组的存储都是连续线性的,才能做好这一题!!!
#include <stdio.h>
int main()
{
	int a[5][5];
	int(*p)[4];
	p = a;
	//a原本代表以a为名数组的首元素地址,即a[0]的地址
	//而现在a被强制转化为指向4个元素数组的指针
	printf("a_ptr=%p, p_ptr=%p\n", &a[4][2], &p[4][2]);
	printf("%p, %d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
	return 0;
}
//%f是无符号十六进制,所以要注意补码的问题,所以这一部分是“0xFFFFFFFC”
//%d部分为“-4”

7.数组传参和函数传参

7.1.数组传参

#include <stdio.h>
void function(int(*parr)[5])//或者写成int[][5]
{
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ", *(*(parr + i) + j));
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { 
		{ 1, 2, 3, 4, 5 }, 
		{ 1, 1, 1, 1, 1 },
		{ 0, 0, 0, 0, 0 }
	};
	function(arr);
	return 0;
}
void function(int arr[][4][3][2][6][7])
{
	printf("hello word\n");
}
int main()
{
	int arr[3][4][3][2][6][7];
	function(arr);
	return 0;
}

7.2.函数传参

传参只是拷贝,在C语言中不可能直接使用变量传递给函数使用,只能使用间接的形式!!!(除非你是在C++语言里使用引用符号&)

8.函数指针和函数指针数组

8.1.函数指针

  • 在C语言中函数也是有数组的,函数名代表函数地址,使用“()”来解引用函数指针使用函数。
  • 磁盘中存储.exe文件,在运行.exe文件的过程中,就会把程序中的数据和代码加载到内存中,函数是代码的一部分,因此也要加载到内存中,以供CPU后续寻址访问,因此函数也有地址
  • 在函数中,“函数名”和“&(函数名)”是等价的
  • 使用函数指针的例子
int* function(char a, double) 
{ 
  	//code 
} 
int*(*p)(char, double) = function; 	
(*)p();//调用函数 
p();//调用函数

8.2.函数指针数组

void(*p[10])()

8.3.函数指针数组指针

void(*((*p) [10]))();

9.总结

本次我给您带来了数组和指针的知识,并且深入的数组和指针的关系:没太大关系。仅仅只是他们的访问方式有些许类似罢了。您一定要明白,数组和指针,是两种概念!

最后,与君共勉。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

limou3434

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值