您好,这里是limou3434的一篇博客,感兴趣您可以看看我的其他博文系列。本次我主要给您带来了有关C语言数组和指针的相关知识。
让我们先来厘清左右值的概念。
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”。
因此实际上一个变量应该理解为“变量 = 空间(左值)+内容(右值)”,在使用变量的过程中会根据上下文使用变量的不同部分。(因此“指针<==>指针变量”是发生在使用右值的时候)
但是按照上述理解,在记录地址数据之前,首先要存在地址。即:内存空间必须先标记好各自的地址,这就涉及到对内存空间编址问题。
CPU是计算硬件而(CPU在内存中寻址的基本单位是字节),内存是临时存储硬件,两者之前靠着数据总线来连接(实际上应该是地址总线、数据总线、控制总线连接的,但是根据厂家实现的不同可能会用共用)。而在32位机器下,有32根这样的线(理解为“电线”,但是这些“电线”是内嵌在主板上的),每一根线用来传输电信号(1或0),因此同时可以传输32个bit位,一共有232个地址,因此有232个字节被成功编制,内存空间大小就是4GB。
同理64位操作系统也是一样的。
因此指针的存在就是为了加快CPU的寻址效率。
编制,并不是把每个字节的地址记录下来,而是通过硬件设计来完成的。
* 解引用
*p就是p指向的变量,这样理解解引用,然后解释使用的是左值还是右值会更加方便.
*是操作符的情况下,*p使用的就是p的右值,这是一种间接访问的方式。换句话来说“*”操作实际上也可以直接对一个地址值解引用,这叫直接访问。
直接访问地址的方法对于现代编译器来说已经不可能,这是因为编译器会进行栈随机化
#include
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;
}
NULL、0、'\0’在数字层面上是0,但是类型层面上是不同的
首先强调一点,指针和数组没有任何关系,只不过操作方式很像,不要将两者的概念混淆。数组不是C语言特有的,数组是具有相同数据类型的集合。
元素类型 数组名 [元素个数] = { 元素列表 };//元素列表如果直接填0,就会默认给空间填充0
无论是什么数组其实都是一维数组,实际上不存在所谓“二维数组”和“三维数组”
在x86系统中,根据“栈向下生长”的原理,从上往下定义变量,其地址从上往下递减。(但是在不同环境下,有可能不一样,这取决于具体的系统架构)
而数组是从左到右地址递增,因此实际上不应该认为数组是一个个独立元素在开辟空间,应该是整体开辟空间,整体释放。
在数组开辟好空间后,然后从低地址将空间给与“0~size-1”下标去访问。(但是无论是在哪个环境C语言的数组内存布局都是一样的)
#include
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;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include
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
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;
}
int main()
{
char arr1[3] = { 1, 2, 3 };//初始化可以
char arr2[3];//只定义不初始化
arr = { 1, 2, 3 };//不可以定义后才赋值,C语言不允许将arr2作为左值使用
}
在C语言中,数组名是一个指向数组首元素的常量指针。因此,数组名虽然可以作为右值使用,但不能作为左值使用。这是因为,当我们试图向数组名赋值时,相当于试图修改一个常量指针(const,发生权限放大)的指向,而这是非法的。一个数组名代表的是一个固定的内存地址,它不能被修改。
int arr[] = { 1, 2, 3, 4, 5, 6 };
int arrSize = sizeof(arr) / sizeof(arr[0]); //这里写成0是因为数组一定有一个元素,这样的表达式是绝对不会错的(保险)
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语言中最常用的(面向过程语言),如果不这么设计,在传递数组形参时,就需要程序员不断更改他的习惯(如数组的使用“[]”访问,指针的使用“*”访问),时间长了这样出错概率比较大。因此数组和指针的访问方式是通用的,都可以使用“[]”和“*”,但是其本质概念依旧是没有任何关系的。
#define _CRT_SECURE_NO_WARNINGS 1
#include
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[];//不合法
例如“int* pa1[10]”先找变量名pa1,由于[]优先级高于*,pa1先和“[]”结合,所以pa1是一个数组,因此int*修饰的是数组的内容,即每个元素
例如“int(pa2)[10]”先找变量名pa2,由于“”先和pa2结合,所以pa2是一个指针,int[10]是一个匿名的大小为10的数组,可以看作是一种数组类型
#include
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
//注意可能需要在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;
}
在C语言中是实际上只有一维数组和一维指针
指针变量也是变量,任何变量都有地址,好了没了,就是这么简单。
int a = 10; int pa = &a; int ppa = &pa; int* *pppa = &ppa;
数据类型 数组名 [数组大小]
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
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
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”
#include
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;
}
传参只是拷贝,在C语言中不可能直接使用变量传递给函数使用,只能使用间接的形式!!!(除非你是在C++语言里使用引用符号&)
int* function(char a, double)
{
//code
}
int*(*p)(char, double) = function;
(*)p();//调用函数
p();//调用函数
void(*p[10])()
void(*((*p) [10]))();
本次我给您带来了数组和指针的知识,并且深入的数组和指针的关系:没太大关系。仅仅只是他们的访问方式有些许类似罢了。您一定要明白,数组和指针,是两种概念!
最后,与君共勉。