写于2020.10.9
在2020.11.4修改了一些内容
困惑
数组就是特殊的指针?指针常量和常量指针? 这个指针只能指向自己?但是没有被分配空间?就是数组第一个元素的地址?数组为什么不能用
b=a;
赋值?
那它储存在哪?printf输出字符串的参数表里为什么是个指针?为什么加上*运算符反而出错?
......
相信在初学数组和指针的时候, 对着课本你也会有许许多多类似的问题。而且,网上搜的答案和解释五花八门,有说的一看就是错的,也有太难的,扔出来好几个官方文档,看完反而更加看不懂了的。。
昨天一天我大概花了五个小时,从看《Head First C》到写英文笔记再到在网上查没看懂的地方,终于感觉自己在一个层面上终于“融会贯通”了。所以,趁热打铁,现在就写下总结笔记与大家分享。
注意:本人水平非常有限,如有错误请指正。共同进步。
概念与原理
核心解析
开门见山地说,在我的理解里,a并不存在。(本文默认a是一个数组)
我觉得这是一切问题的根源。尽管这个说法可能让你更加迷惑,但请往下看。
所谓“特殊的指针”,所谓“只能指向自己”,所谓“没有被分配空间”,其实都在说:a只是一个给写程序的人看的【符号】。它在编译的时候,会被【替代】为a[0]的地址,因此a这个东西,可以被用来获取地址。而说到获取地址我们往往联想到指针。
C 为了某种程度上读者使用的方便而遮盖了它真正做的事情,并提供了一个更加简单直观的方法让你使用数组。这很好理解,比如我们调用一个标准库的函数,我们其实也用一个简单的函数名替代了达成这个功能电脑真的做的事情。
真正存在的数组,就是一串连续而有序地储存在内存里的数(字符)。非常普通。
现在让我们再回头看这些让人迷惑的表述:
”数组就是特殊的指针“:一个很不好的表述。指针是真实存在于另一个地址的,它有自己的地址,只不过它的值正好也是个地址;而a这个符号只是一个给写程序的人看的东西。当程序编译,c会用一个数组中第一个元素的地址替代它。一个是【指向】,一个是【替代】。
既然我们已经发现了指针和数组的巨大差别,再在”常量指针”和“指针常量”之间纠结选哪个就没有必要了。不过下文我会简单地向你介绍这两个概念,如果你有兴趣可以阅读一下。
”数组只能指向自己“:同样的,很好理解,因为这就是C编译的时候,看见a这个【符号】会做的事情:使用数组中第一个元素的地址代替它。
“没有被分配空间”:它只是个在IDE里的符号。真正编译后的exe并没有它。
“数组为什么不能用
b=a;
赋值?”:b根本不能修改,不能作左值。
怎么样?现在有没有发现数组的概念简单了很多?
当你声明了一个数组,就普普通通地在脑海里想象一串数字(字符);
而当你在书上读到a(为右值)时,告诉自己:“这是第一个元素的地址”!
辨析
好了,现在让我们看看:
“&a” “&a【0】” “a【0】”是什么?
它们与a的区别是什么?
&a:它表示的是整个数组的首地址。数组的首地址的值就是数组第一个元素的地址,但区别是:他们的数据类型不一样。
数组的地址指向类型是整个数组,数组第一个元素的地址指向类型是第一个元素。如:a+1,偏移的是一个数组元素的大小,而&a+1,偏移的是整个数组的大小
int main() { printf("sizeof int:%d\n", sizeof(int)); int a[5] = { 1,2,3,4,5 }; printf("a:%p\n", a); printf("a+1:%p\n", a + 1); printf("=============\n"); printf("&a;%p\n", &a); printf("&a+1:%p\n", &a + 1); printf("=============\n"); printf("a[0]:%p\n", &a[0]); printf("&a[0]+1:%p\n", &a[0] + 1);}
运行结果为:
&a[0]: 在做右值时没区别,都是首元素的地址
a[0]: 表示一个平平无奇的数
一些你还需要的知识
你需要这些知识,使你面对一些代码的时候在一个层面上理解知道它们运行的原理。
数组的索引是从0开始的。因为当你写出a【3】,实际上程序会理解为:*(a+3)。这就是程序找到a中第四个数的方法:对地址进行运算。
这解释了为什么你需要声明数组的数据类型。除了关系到你能存储什么数据、存储多大的数据,同时,由于每个数据类型占的位数不尽相同,因此程序需要知道在进行对一个数组的一个元素定位的时候,每次移动几位来找到它。
同样的,指针也需要声明数据类型。试试看理解这句话:
值相同的两个指针所指向的变量的值可以不同.
值相同表示它们都找到了同一个地址;但是由于数据类型的不同,它们可能读了不同的位数,从而可能读到了不同的值,也就是上面a和&a的例子。
关于字符串:C本身其实并没有字符串。当你利用了标准库里有关字符串的内容,你声明的string会被换成一个字符数组。
看看这个语句:
char *a = ”hello“;
事实上,这个hello的字符串常量在编译的时候会被放入名为stack的只读区域。因此这个指针a,指向了一个只读的区域。因此,在这之后你并不能利用*运算符改变a指向的内容。这就牵扯到了上文提到的指针常量和常量指针(虽然关系并不大。。):
指针常量:指针常量就是指针的值是常量,只能指向固定的内存地址。但是,内存地址所对应的内容是可以通过该指针改变的。
/*指针常量的例子*/ int a,b; a=0;int * const p; p = &a;//正确 p = &b;//错误 *p = 20;//正确
常量指针:常量指针就是指向常量/变量的指针,但它自身的值可以改变,从而指向另一个常量/变量。注意:常量指针不一定指向常量,它只是表示它会将指向的东西【看成】一个常量,从而阻止程序通过使用*p=x;的方式修改其指向对象的值。
/*常量指针的例子*/ int a,b; a=0;int const *p; //这里写const int *p也没问题。只要别写成int *const p就行p = &a; //正确 printf("%d\n", *p); //输出为0a = 20; //再次赋值,改变变量a的内容printf("%d\n", *p); //输出为20p=&b; //正确*p = 20; //错误
参数传递
当数组名作为函数的实参时,形参列表中也应定义相应的数组(或用指针变量),且定义数组的类型必须与实参数组的类型一致,如果不一致就会出错。系统在编译时也只是检查数组名,并不会检查数组长度。所以数组长度要额外定义一个变量进行传递。对于a这可以称为一种从数组到首元素地址的“退化”。
int AddArray(int array[], int n); int main(void){ int a[] = {1, 2, 3, 4, 5, 6, 7, 8}; int size = sizeof(a) / sizeof(a[0]);//如果需要长度的话。 printf("sum = %d\n", AddArray(a, size)); return 0;}int AddArray(int array[], int n)//也可以用int *array{ int i, sum = 0; for (i=0; i<n; ++i) { sum += array[i]; } return sum;}
恭喜你看完了!怎么样,现在对数组的理解有没有更加深刻了呢?如果发现问题请留言哦。