程序员成长之旅——C语言初识指针和数组

指针

一级指针

指针的定义

在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为"指针"。意思是通过它能找到以它为地址的内存单元。

int *p;

我们都清楚这是一个一级指针,而这个指针的类型是什么呢?

我们可以对比 int p。我们清楚的知道int p在32位机器上它的字节是4,而在同一机器上用sizeof测量发现int *p它的字节也是四,当然它不可能是int 类型,但是他们的字节还是相等的,我们就可以知道它的类型就是int *。

这样我们就可以清楚的知道int* p这是一个int* 类型的指针,这个指针指向一个内存单元,内存单元的数据类型是整型int。

同时,我们用编译器调试发现,无论*前面是什么类型,它在32位操作系统中,它的大小都是4,也就是说和前面类型是无关的。

“ * ”的作用是什么?
先看一个代码
在这里插入图片描述

我们可以清楚地看见,调试之后,&p的值没有变,而*p i 都变化了。所以,我们就知道了 *就相当于访问一个内存中的数据,并且可以改变数据的大小,但是&p是不会改变的,也就是p不会改变。
我们可以形象化的理解 * ,就比如吧,我们回家的时候开锁会用钥匙,那个钥匙我们可以形象化的看成 * ,只有有钥匙才能开锁,也就是只有有 * 我们才能访问内存的数据。

*前面的类型能干什么?
在这里插入图片描述
在这里插入图片描述

通过这个代码我们清楚的可以认识到* 前面的类型是能决定指针可以访问多少个字节的,并且决定了它走一步是多少字节。

二级指针
char **p; 

我们已经知道了定义一个一级指针变量p,p保存的是指向数据的地址,而p的地址我们不知道是谁来保存的,这里我们就可以引入一个二级指针,它保存的就是一级指针变量p的地址。画个图你就明白了:
在这里插入图片描述

p = NULL;
char *p1;
p = &p1;

上面是给一个二级指针初始化以及如何使用它。
我们可以这样理解
首先我们让二级指针变量p存一个一级指针的地址,而我们要取一级指针指向的数据时,
我们就应该先用钥匙开门,也就是得到一个*p的值,然后再次开门,这时候得到的就是那个数据的值,总的来说,二级指针要得到数据就需要开门两次也就是**。

数组

一维数组
int a[6];

这个相信大家并不陌生,它就是一个一维数组。我们会用a[0],a[1],a[2]等来访问这个数组中的元素,但这里着重讲一下,a[0],a[1],a[2]等并不是数组的名字,而是这个数组的元素,元素是没有名字的,这个数组只有一个名字,就是已经定义的a,下面我画一个图,大家就清楚了。
在这里插入图片描述
上面图理解了,那么接下来给大家说一下关于sizeof求大小是多少的问题。(32位)

sizeof(a)
这个说的是整个数组的大小,那不用说是20。
sizeof(a[0])
这个是首元素的大小,那就是4,因为类型是int。
sizeof(a[6])
这个本来是应该取不到,是0,但是我们会发现在编译调试的话,它是4,这是为啥呢,这里跟大家说一下,我们的sizeof并不是一个函数,而是一个关键字,关键字求值的话,它是在编译的过程,也就是说,它根本就访问不到a[6],而是根据它的类型决定大小,那就是4.(切记,只有函数才会访问,也就是运行的时候看大小)。
sizeof(&a[0])
取元素a[0]的首地址,那就是4.
sizeof(&a)
取数组a的首地址,那也是4.

&a[0]和&a 的区别(省政府和市政的区别)

这里它们的值是一样的,但是表示的意义是完全不一样的,一个是首元素首地址,另一个是数组的首地址,形象的记忆的话,这里引入一本书《C语言深度解刨》,它里面是这么说的,湖南的省政府是在长沙,而长沙的市政府也是在长沙,就是这么一个意思。

下面给大家说一下数组名a作为左值和右值的区别。

x = y 简单来说的话,在 = 左边的我们叫左值,在 = 右边的我们叫右值。
左值 :在上下文环境中,编译器认为x的含义是x的地址,而这个地址只有编译器知道,在编译的时候给它一个特定的区域保存这个地址,我们并不需要关心这个地址保存在哪里。
右值 :右值的话编译器会认为y它是内存地址所指的数据,只有在运行的时候,我们才能知道这个数据y是什么。
这时候我们引入一个术语-----可修改的左值,意思就是出现在赋值符左边的地址的数据一定是可以修改的,换句话说,我们只能修改非只读变量。
现在明白了左值和右值的区别,我们在来回归主题,讨论一下,a作为左值和右值的区别。
a作为右值,我们一定要清楚,它代表的是数组首元素的地址,也就是&a[0],而不是数组的首地址,但是我们一定要清楚,它仅仅是代表的是数组首元素的地址,而并没有一个内存来保存它,这是和指针有很大差异的。
a作为右值我们清楚了,那a作为左值呢?
切记-----a不能做左值
a如果作为左值的话,那么编译器会认为访问的是一个首元素的地址,而数组开辟的内存是一整块的,我们不能直接修改这一整块数据,因此,它不能作为左值,我们只能访问它其中的一个元素比如a[1],一次修改一个,我们可以形象的理解,把a看成一个变量,它是分很多小块的,我们只能访问这些小块,从而修改a这个整体,但是不能直接修改a。

二维数组

我们已经了解到了,数组里面可以存任何数据,除了函数,那么现在我们来一起认识一下二维数组吧!

char a[3][4];

假想中的二维数组如下图:
在这里插入图片描述
实际的二维数组如下图:
在这里插入图片描述
从上图可知,二维数组在编译器的内存中是线性存储的,而它的地址,取决于首元素首地址到它的偏移量。
知道了这些,看一道题:

#include <stdio.h>
int main(int argc, char* argv[])
{
	int a[3][2] = { (0,1),(2,3),(4,5) };
	int* p;
	p = a[0];
	printf("%d", p[0]);
}

它的值是多少?
我第一次看,一扫而过是0啊,但是被打脸了,编译器是1。
在这里插入图片描述
这是为啥呢?我们仔细再看一下,发现了没有,{}里面嵌套的是一个(),而()里面有逗号,这里是一个逗号表达式,因此我们实际的赋值是int a[3][2] = {1,3,5},所以我们以后再初始化二维数组的时候一定要小心千万不要把()当成{ }来使用。
了解了这个,我们再来看一道题。

#include<stdio.h>
int main()
{
	int a[5][5];
	int(*p)[4];
	p = a;
	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;
}

在这里插入图片描述
编译之后我们发现是-4,那是为什们呢?我们可以根据一个图来理解
在这里插入图片描述
由图可知确实为-4,还有不理解的建议再看一下《C语言深度解剖》里面的二维数组章节。

指针和数组

首先我们一定要清楚,指针和数组是两个完全不一样的东西。

  • 指针是一个在32位操作系统下内存大小为4的一个保存其它数据地址的一个东西,它可以指向任何数据,但并不是任何数据都可以通过它来访问;
  • 数组就是一个数组,它可以保存除函数外任何类型的数据,它的大小是由元素个数和数据类型决定的,也就是说,它定义时,必须是由元素和数据类型共同定义。

以指针的形式访问和以下标的形式访问

char* p = "abcdef";//A
char a[] = "123456";//B

我们先来看一下A
我们知道它是首先定义了一个指针变量p,p在栈中开辟了一个字节为四的内存,存储的是一个在栈中开辟的字节为7的内存的一个首地址,而这个字节为7的内存也没有名字,所以我们对它的访问是完全匿名的。那接下来我们访问一个d元素,可以怎样访问呢?

  • 以指针的形式访问:*(p+3),假设p存储的首元素的地址是0x0000FF00,而这里向右偏移了三个元素,那就是0x0000FF03,然后解引用,也就是用钥匙打开门,就得到了d这个元素。
  • 以下标的形式访问:p[3],我们首先要清楚编译器它总是会将下标的形式访问解析为指针的形式访问,因此,同理,可得d这个元素。可见,本质中它两是没啥区别的,唯一的区别就是写法有所不同。

接下来我们看B
定义了一个数组a,a拥有7个char类型的元素,其占有7个字节,本身就在栈上面,现在要找5的话,我们首先要根据数组名a知道首元素的地址,然后在通过偏移量,就可以找到5这个元素了,不过和指针区别的是它是具名+匿名的查找

  • 以指针形式访问:*(a+4),我们知道a是数组首元素的地址,然后5的话在首元素地址往右偏移4个元素,然后在打开门,这样就找到元素5了。
  • 以下标形式访问: a[4],同上面,这里就不多说了。

这里我们最后要强调一下:这个偏移量+1是一个元素的偏移,不是一个字节的偏移,一定要牢记,要不然理解就会有误,而上面我们正好是一个字节一个元素,char类型。所以在此强调一下。

a和&a的区别
通过上面的讲解,相信你已经明白了指针和数组的访问方式了。下面我们再看一个东西
先看一个例子:

#include<stdio.h>
int main()
{
	int a[5] = { 1,2,3,4,5 };
	int* ptr = (int*)(&a + 1);
	printf("%d,%d", *(a + 1), *(ptr - 1));
}

这个打印出来是多少呢?
在这里插入图片描述
我们可以看到打印出来的是一个2和5,我相信第一个2很好理解,这里我就说一下5是怎么来的吧。

&a是整个元素的地址,而它刚好等于首元素的地址,给它加一就是偏移整个数组的大小,我们会发现是越界了,假设是a[5],接下来我们给其强转为(int*)类型的地址,然后赋值给ptr,接下来ptr-1就是向左移动一个int型大小的元素,也就是移动到了a[4]的地址,这时候开门得到就是5。

指针数组和数组指针

首先我们应该分清什么是数组指针,什么是指针数组,其实这个很好理解。

  • 数组指针不就是一个存放数组的地址的一个指针吗。
  • 指针数组不就是一个元素都为指针的一个数组吗。

来个图理解一下《C语言深度解刨》
在这里插入图片描述

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

我们再来看这个,上面这个是数组指针,下面的是指针数组。因为[ ]的优先级是高于*而小于()的。

在说个题外话
我们通常定义的时候都是将变量放在后面也就是上面的数组指针这样定义
int ()[10] p;其实我们这么想是完全没错的,因为它的类型确实是int ()[10],但是我们有没有感到这样写有点别扭,因此编译器就把p放在()里面了,也就是我们可以那么理解。
再论a和&a的区别
先看一个代码

int main()
{
char a[5]={'A','B','C','D'};
char (*p3)[5] = &a;
char (*p4)[5] = a;
return 0;
}

在这里插入图片描述

所以我们可以清楚的看到p4是有问题的,它左右类型不同。
在这里插入图片描述
通过调试我发现p3+1和p4+1居然相等,我认为这是编译器的一个bug,它是将a 和 &a都当成了数组的地址,因此跳跃的时候直接跳过一个数组,因此相等,但是,我建议大家还是一定要注意=左右数据类型一定要相等。
地址的强制转换
我们先看一个例子:

struct Test
{
	int Num;
	char *pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p;

假设 p 的值为 0x100000。 如下表表达式的值分别为多少?
p + 0x1 = 0x___ ?
(unsigned long)p + 0x1 = 0x___?
(unsigned int*)p + 0x1 = 0x___?

p + 0x1 的值为 0x100000+sizof(Test)0x1。至于此结构体的大小为 20byte。所以 p + 0x1 的值为:0x100014。
(unsigned long)p + 0x1 的值呢?这里涉及到强制转换,将指针变量 p 保存的值强制转换
成无符号的长整型数。任何数值一旦被强制转换,其类型就改变了。所以这个表达式其实就是一个无符号的长整型数加上另一个整数。所以其值为:0x100001。
(unsigned int
)p + 0x1 的值呢?这里的 p 被强制转换成一个指向无符号整型的指针。所
以其值为:0x100000+sizof(unsigned int)*0x1,等于 0x100004。
再看一个例子

//在 x86 系统下,其值为多少?
int main()
{
int a[4]={1,2,3,4};
int *ptr1=(int *)(&a+1);
int *ptr2=(int *)((int)a+1);
printf("%x,%x",ptr1[-1],*ptr2);
return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
根据这个图我相信大家就可以理解了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

从零出发——

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

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

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

打赏作者

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

抵扣说明:

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

余额充值