最近思想的总结,本人菜鸟,有错指出

变量之所以为变量,意义是它可以改变,不是常量,可为什么能够改变呢?普通的变量作为一个左值的存在,是引用了相应的内存的,而且跟C++的引用很相似:定义时就绑定,不能改变。于是定义了一个变量,它也就是引用了内存,在没有const修饰符的前提下,它可以被赋值操作符修改。

于是,在函数体内定义的变量,就引用了该函数函数栈内的内存,对变量名的赋值,也就是对该引用内存的值的修改。

int a=3;

首先定义了一个数组,名字为a,即变量名为a。那么遵循上述的说法,a应该也引用到了相应的内存,那么假若我们使用了sizeof操作符,可以清楚的看到了结果为12。对于这个复合类型取元素的方法,当然就是[]操作符了,通过这个操作符,我们可以访问到其中的各个元素。

结构体:

typedef struct tag_a

{int _a;char _b}A;



A b;

在这里我们定义了一个结构体变量,即变量名为a。和上述一样,a引用了相应的内存,得到的大小(不考虑对齐)应该为4+1。对于这个复合类型取元素的方法,当然是 . 了,通过这个操作符,我们可以访问到其中的各个元素。

类:

与结构体相似。

于是我们得到了数组变量的变量a,结构体变量的变量b。它们有一个相同的性质,即复合变量,通过相应的操作符,我们可以访问到元素。特殊的地方来了,直接写变量的名字,出了了传参和返回值的时候使用,其它地方是没有意义的,然而在这些地方,就产生了差异了,假如我们直接传入了数组变量a。

void func(int _a[]);

当我们修改了_a[]的值的时候,发现传入的变量a的元素的值也发生了改变。然而假如有相同的函数,我们通过传入b这个结构体来调用,却发现形参不影响实参,这就是特殊点。

当然我们大家可能先入为主了,一开始就知道了数组名传入的时候会退化成指针,这是大家默认遵循的规则了,但是这是一个很特殊的地方,结构体,数组本质是没有区别的,都是通过首地址+元素偏移寻址,a[0]与b._a都是取了它们的首地址解引用而引用到的内存,在理论上说它们都是相等的。

a的类型为int[3] b为A ,均为复合类型,int[3]和A在本质上是没有区别的。然而,在一个地方看到了一些资料,说是ANSI为了c的效率问题,当把数组传入的时候,会退化成指针,于是这个特殊情况就来了。在上述的函数中,int[3]—->>> int*,发生了这种转变,复合变量变成了一个指针。

可能在这里用这个比较好理解我的想法:

int a[3];

func(&a);






void func(int(*p)[3]);

这才是复合类型最通用的以指针方式传入的实现,而用这个方式来实现,也会保存了这个指针相应的信息:sizeof,在函数体内只要这样访问(*p)[0]就能访问到相应的元素了。

void func(int (*p) [3])
{
	printf("%d\n",sizeof(*p));
	(*p)[2]=999;  //*p即为int[3]的复合类型引用的内存
}
 
int main()
{
	int a[3]={0};
	func(&a);	//取复合结构的地址
	printf("%d",a[2]);
	return 0;
}

这才是对应于真正的复合类型的访问规则!其实取了地址,还是a这个名字引用的内存地址,在退化后,其实还是这个地址,因为二者的意义是相同的。

可能有人说数组名就是一个指针常量,是不能更改的,可是别忘了,结构体名也是一个常量,它引用的内存也是不可能更改的。于是为了将结构体以类似的方法传入函数体,就要取一次地址。于是结构体名不能退化成结构体的首地址,而数组名可以退化成数组元素的首地址,基于这个事实,我们不能将结构体名和数组名同等看待,但是它们的存储与寻址实质是一样的。因为数组名的访问元素的操作符和指针的解引用太像了。

指针的解引用

大家都明白对于指针的解引用,主要有*操作符和[]操作符,它们的功能都是一样的,*代表了将后面的变量直接解引用,而[]则是加上偏移解引用。但是其实还有一种解引用符号,就是->,当然对于地址是可以计算出来的变量,可以直接用 . 来解引用。

->大家已经很熟悉了,对于一个结构体指针来访问其元素,可是能够访问其元素,必定是一个左值,换句话说,就是已经引用到了相应的内存了,用汇编来说就是已经在[]中有地址了。->通过访问左操作数的值,根据结构体的头文件算出偏移,两者相加,再解引用,可以看出这样的寻址必定要访问指针变量的值,存在一次寻址的过程。

相应的对于“对象名”的访问元素的操作符,就是”.“,它的原理是一样的,但是有一个区别,即这个操作符的左操作数的地址是已知的,或者是可以算出来的,这句话怎么说呢,举个例子,在函数体内定义了一个变量

A b;

b._a=3;

这个.与->究竟有什么本质上的不同。在这里,变量名b所引用的内存已经确定了,可以通过函数体内声明变量的顺序得知。于是我们将b解引用,就成了[ADDR],而ADDR的值则是已经可以算出来的b的地址+根据头文件算出来的偏移地址,这样访问一次结构体元素,只需要一次解引用而已。而->,由于事先不能得知左操作数的值,必定要将其值取出,放入寄存器,再根据偏移执行自增指令,从而根据这个值来作解引用操作。

我们再看一下更加复杂的情况,我们有一个函数

struct B

{int _c;char _d;A _e;};

void func(A* c);

在这个函数体内我们访问了

c->_e._a;

这样这里的”.“和”->“操作符有什么不同:

依旧是上述的观点,c变量的地址是已知的,但是存的值是未知的,所以必须要访问其值并作偏移运算才能得到A的地址,在执行了c->_e的时候,_e的地址是已知的,其实只写一个结构体名是没什么意义的,只有对其的元素进行访问才有意义,虽然在C语法上来看,我们已经“引用”到了结构体所在的内存。我们通过”.“来访问其元素,_e元素地址已经得出来了,在寄存器中,再一次访问的时候,它的地址是已知的,于是直接加上偏移即可引用到_a的内存。

所以”.“表面上看是对于一个结构体名作访问操作,而在C语法中,变量名也可以看做这块内存,于是有了直接在内存中引用元素的感觉,其实还是通过已知的变量地址加偏移来访问。而”->“则是先访问一次指针的值,而结构体地址是未知的,再偏移引用,这里的消耗比较大,但是比较灵活。

其实C抽象了底层实现,对于使用还是有很大帮助的,学习其底层实现仅仅是为了更好的理解。

共 0 条回复
暂时没有人回复哦,赶紧抢沙发
发表新回复

作者

sryan
today is a good day