SC词法分析笔记1

文章目录

  • 提出问题
  • 错误原因
  • realloc()函数的含义
  • 规范代码
  • 细细讲解

        本次博客主要是为了讲解我在这本书中因为一次修改而引发的关于指针的思考。如果你是从别的地方看到篇博客的话,我强调一点的就是:下面我说的书本是《自己动手写编译器、链接器》,作者是:王博俊、张宇。阅读本文需要一点汇编的基础和对指针的了解。当然,没有也没有关系,我觉得没有的同学,之后就会自己主动去学习这方面的知识了。

提出问题

        因为并不是所有的人都看过那本书,所以我先抽象出来这个问题。问题就是:想要做一个动态数组。不仅仅空间大小是动态分配的,由于之后的数据插入的数量也是不确定的,这个动态数组还得是实现动态扩容的处理。最后,我就写出来这样的代码:

#include
#include

typedef struct DynArray
{
	int count;
	int capacity;
	void **data;
}DynArray;

void dynarray_init(DynArray *parr, int initsize)
{
	if (parr != NULL)
	{
		parr->data = (void**)malloc(sizeof(void*)*initsize);
		parr->count = 0;
		parr->capacity = initsize;
	}
}


void dynarray_realloc(DynArray *parr, int new_size)
{
	int capacity = 0;
	void *data;
	capacity = parr->capacity;
	while (capacity < new_size)
	{
		capacity = capacity * 2;
	}
	data = realloc(parr->data, capacity);//位置 1
	if (!data)
	{
		printf("内存分配失败\n");
	}
	parr->capacity = capacity;
	parr->data = &data;//位置 2
}
void dynarray_add(DynArray *parr, void *data)
{
	int count = 0;
	count = parr->count + 1;
	if (count > parr->capacity)
	{
		dynarray_realloc(parr, count);
	}
	parr->data[count - 1] = data;
	parr->count = count;
}

int main()
{
	DynArray *dp = (DynArray*)malloc(sizeof(DynArray));
	dynarray_init(dp, 2);
	int p1[] = { 1,2,0 };
	int p2[] = { 3,4,0 };
	int p3[] = { 4,5,6,0 };
	dynarray_add(dp, (void*)p1);
	dynarray_add(dp, (void*)p2);
	dynarray_add(dp, (void*)p3);
	int **p = (int**)dp->data;
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; p[i][j]!= 0; j++)
		{
			printf("%d ", p[i][j]);
		}
	}
	return 0;
}

        上面的代码就是我根据书本上的代码修改之后得到的。因为书本上的代码直接写到vs2017上面是会报错的。所以,我就修改了书本上的代码。最后,我发现这样子就出现了大问题。发现好多关于指针的理论就无法解释了。大家如果是第一次接触自己用C语言写动态数组,我建议就是先将我上面的代码跑一遍,然后看看问题在哪里。如果你能很快的发现我的问题,并知道原因,那么我觉得下面的内容应该对你没有什么帮助了。但是如果你并不是特别清楚的话,下面的内容就是为了准备的。

        书本上在位置2处的代码是这样的:

	parr->data = data;

但是由于data是void*,但是parr->data是void**。所以编译器就会立即报错了。我一开始以为是书本印刷错误,并以为自己对于指针的理解已经很透彻了,所以我做出了大胆的修改。

        因为按照我之前对于指针的理解,就是一级指针是用来存放普通变量的地址的。例如:

int a=10;
int *p=&a;

然后二级指针是用来存放指针的地址的。例如:

int a=10;
int *p=&a;
int **q=&p;

而且c语言的语法应该也是这样设计的。也就是说:如果你要把 &a赋值給q,也就是写出下面的语句:

int a=10;
int **q=&a;

编译器就会报错。编译器之所以报错,就是因为C语言语法就是这样规定的。但是,我们到底会不会遇到将一个“一级指针的值”赋值給“二级指针”的时候呢?说实话,在弄清楚这个问题之后,我觉得对于指针的理解又精进一步了。(当然,也可能这篇文章中还是有些地方是错误的,希望大家指正!!!)。现在我可以告诉你的是,是有这样的情况发生的。例如这样的代码:

int maxn=10;
int **arr=(int**)malloc(sizeof(int*)*10);

大家都知道malloc()函数的返回值是void*,但是我这里却是强制转化为了int**。而且我相信比较喜欢写C语言代码的人肯定写过这样的代码,因为这个可以动态的申请一个二维数组。

错误原因

        我为什么要将书本上parr->data=data;改成parr->data=&data;就是因为编译器报错,所以我按照我之前对于指针的理解做出了改变。(为什么作者当初没有报错,而是直接写成这样子了。我估计就是因为当时作者使用的是vc6.0吧,可能但是在vc6.0上面就是一个Warning,但是现在在vc2017上面就是一个error。所以我不得不对代码进行修改。)

        错误的原因在于我没有理解指针的真正含义和malloc()函数的返回值的含义。我相信大家都知道:

指针也是变量,只不过这个变量的值是一个地址的值。

我们能够使用*p就找到了p所保存地址的变量的值,就是因为编译器将其翻译为汇编的时候就是采用的间接寻址(后面我会贴上几个我在知网找到的关于这方面文章,大家可以细看)。那么,这样说来,給int *p赋值,到底是不是一个地址,或者说是一个怎么的地址,你程序员决定就好了,反正C语言的规定就是如果你采用的 *p的话,那么我就把p的值当作寻址地址,然后用这个地址去找到对应变量的值。下面的一段是我推测的。举例说明:

int **p;
int *data=realloc(parr->data,new_size);
p=data;

        但是为了方便程序员写代码,编译器的产商就决定说:如果你定义的是int *p的话,那么你在使用的使用就只用一个 * 就可以了,而p作为左值的时候,仅仅只能接收一个地址值(我觉得编译器肯定可以判断出来),作为右值的时候就只能赋值給一级指针变量。但是,既然不是标准的定义,也就是说C语言的标准没有要求我们不能将二级指针的值赋值給一级指针,但是你这样做又会导致编译器报错。但是作为程序员,我很清楚data的值作为一个地址的时候,我能将其二次间接寻址并且能够正确访问。也就是说我可以对data采用 **data的时候,依然可以到一个正确的值。C语言的灵活之处就在这里,我程序员很清楚data代表的意思,所以我可以对其采用 **data操作。但是并不是所有的C语言的程序员都能在写代码的时候这么清楚,不能保证任何人在任何时候都这么清楚。所以,编译器厂商就说:“既然程序员不靠谱,那么就让编译器来帮助程序员来保证不要错误的访问内存。”因为下面这段代码:

int a=10;
int *p=&a;
int b=**p;

*p就是得到了10,然后就是"以10作为内存地址去找。"显然这里编译器就要报错,因为10肯定不是你能访问的内存。所以,编译器厂商就是认为:“你既然都是定义的int *p;那么采用**p肯定是错误的。”的确,在99%的应该都是这样的情况,但是我觉得那1%的情况,我应该碰到了。

realloc()函数的含义

void *realloc( void *ptr, size_t new_size );
重新分配给定的内存区域。它必须是之前为 malloc() 、 calloc() 或 realloc() 所分配,并且仍未被 free 或 realloc 的调用所释放。否则,结果未定义。
重新分配按以下二者之一执行:
a) 可能的话,扩张或收缩 ptr 所指向的已存在内存。内容在新旧大小中的较小者范围内保持不变。若扩张范围,则数组新增部分的内容是未定义的。
b) 分配一个大小为 new_size 字节的新内存块,并复制大小等于新旧大小中较小者的内存区域,然后释放旧内存块。
若无足够内存,则不释放旧内存块,并返回空指针。
若 ptr 是 NULL ,则行为与调用 malloc(new_size) 相同。
若 new_size 为零,则行为是实现定义的(可返回空指针,此情况下可能或可能不释放旧内存,或返回不会用于访问存储的非空指针)。

情况a):扩张ptr所指向的内存,然后返回的也是ptr原来值。举个例子就是:假如int *ptr的值是0x00dd55f0,操作系统給它分配了4个int的内存空间。然后可以让程序员正确访问的内存空间就是:0x00dd55f0~0x00dd55ff。但是现在你调用了realloc()函数:

ptr=(int*)realloc(ptr,8*sizieof(int));//其实这个是不太严谨的写法,后面会提到这点

假如分配成功,并且是情况a)的话,ptr的值还是0x00dd55f0,但是可以访问的内存空间变大了:0x00dd55f0~0x00dd560f。注意:这里的返回值是和你传入的值是一样的。那么,如果你一开始传入的就是int**呢?有趣的事情就发生了,按照情况a)的说法(只要你的内存在你程序运行的时候还有较多的空间,而且你要重新分配的内存不是也特别大。这几点操作系统自然就会判断。调用realloc()函数之后都是返回原来的值),返回值就是一个void **。但是因为函数定义就是返回一个void *,导致最后编译器要程序员采用一个void *p的变量来接收这个值。所以你对p采用 **操作在编译器看来就错误的,因为p的类型是int *。但是你程序员很清楚,这里的p的值是一个可以二次间接寻址的值。所以你应该做的事情是:

 parr->data=(int**)data;

而不是按照“二级指针存放的是指针的地址”这样的思想来写出这样的代码:

parr->data=&data;

(二级指针存放的是指针的地址在某些时候是正确的,而且对于初学者也比较好理解。但是我个人认为如果想要深入理解指针,必须得按照这样的理解:指针就是一个间接寻址。指针的值我程序员很清楚应该是几重间接寻址。)

规范代码

        这个问题的部分源代码就是这样子:

void dynarray_realloc(DynArray *parr, int new_size)
{
	int capacity = 0;
	void *data;
	capacity = parr->capacity;
	while (capacity < new_size)
	{
		capacity = capacity * 2;
	}
	data = realloc(parr->data, capacity*sizeof(void*));
	if (!data)
	{
		printf("内存分配失败\n");
	}
	parr->capacity = capacity;
	parr->data = (int**)data;
}

为什么不直接调用realloc()函数的时候直接赋值給parr->data:

parr->data =(int**)realloc(parr->data, capacity);

就是因为realloc()函数可能会分配内存失败,那么此时返回的值是NULL。但是你原来的parr->datd的值也丢失了,所以这一块内存就无法回收了,只有等你程序结束之后才能被操作系统回收。这个就是和你malloc()不记得free()一样。

        如果写成这样子就是:

void dynarray_realloc(DynArray *parr, int new_size)
{
	int capacity = 0;
	void *data;
	capacity = parr->capacity;
	while (capacity < new_size)
	{
		capacity = capacity * 2;
	}
	data = realloc(parr->data, capacity);
	if (!data)
	{
		printf("内存分配失败\n");
	}
	parr->capacity = capacity;
	parr->data = &data;//改变了这里
}

那么parr->data的值就是data变量的地址值。但是我想要的就是realloc()函数返回值。这里要理解的话就一定要先深刻的理解realloc()函数的意义和它的返回值。

        在文章最上面的代码中,位置一的也是有问题的。实际正确的代码是这样的:

	data = realloc(parr->data, capacity*sizeof(void*));

因为realloc()函数后面那个参数是字节大小,所以这里传入的一定是字节。如果你仅仅传入capacity的话,就会导致分配内存还比之前的小一些了。像我之前就是出现了这样的问题,导致我的程序P2数组莫名的消失了。最后还是我大哥(手动@大哥)帮我调试出来的。这里大家如果有兴趣的,可以把这样的代码跑一遍,就会发现问题了。

细细讲解

        我觉得作为一个初学者的话就一定要画图,这样就比较好理解。下面的图是文章最上面函数经过修正之后,执行到dynarray_add(dp, (void*)p3);语句之前,我某一次调试的结果:
SC词法分析笔记1_第1张图片
很明显,现在如果还要插入这样一个P3,就会要待用realloc()函数。因为我们一卡是分配給dp.data的内存空间只有sizeof(void*)*2字节个。现在已经使用了完了,所以在插入P3的时候就会调用realloc()函数。在我的程序上面,调用了dynarray_realloc()函数的时候,capacity=4,data=0x705510.有没有觉得这个数字很熟悉。对,你没有想错,就是一开始給dp->data分配的指向的地址空间。为什么还是这个值呢?这一点我在前面说过了,这里就不细说了,没有理解话,就去上面看看。就是说,我之后依然是采用0x705510这个具有“二重地址性质”的值。但是你要知道此时&data的肯定不是0x705510这个值。如果你将其赋值給parr->data的话,此后采用

int **p=(int**)parr->data;
printf("%d",**p);

得到的**p就不是数组里面的数据了。
SC词法分析笔记1_第2张图片
如果parr->data=&data;那么

int **p=(int**)parr->data;
printf("%d",**p);

*p的意思就是说:“以0x00f91c的值作为内存地址,去找到这个内存中的数据”,我们可以从上面的图中可以看到,这个值就是0x00705510。
那么**p的的意思就是:“以0x00705510的值为内存地址,找到内存中这个地址所存放的数据。”,显然,这个地址存放的数据不是数组的元素。所以,一切就真相大白了。至于

parr->data=(int**)data;

之后就可以访问正确,大家也可以仿照我的方法去试试看。相信大家通过这个学习可以对指针的理解加深一步。

你可能感兴趣的:(《自己动手写编译器,链接器》读书笔记,c语言常见问题和解决方法)