免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
1234下一页
最近访问板块 发新帖
查看: 11888 | 回复: 38
打印 上一主题 下一主题

C/C++数组名与指针区别深入探索(转) [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-12-17 10:36 |只看该作者 |倒序浏览
作者:宋宝华(对不起忘打名字了,现在给上
引言

  指针是C/C++语言的特色,而数组名与指针有太多的相似,甚至很多时候,数组名可以作为指针使用。于是乎,很多程序设计者就被搞糊涂了。而许多的大学老师,他们在C语言的教学过程中也错误得给学生讲解:"数组名就是指针"。很幸运,我的大学老师就是其中之一。时至今日,我日复一日地进行着C/C++项目的开发,而身边还一直充满这样的程序员,他们保留着"数组名就是指针"的误解。

  想必这种误解的根源在于国内某著名的C程序设计教程。如果这篇文章能够纠正许多中国程序员对数组名和指针的误解,笔者就不甚欣慰了。借此文,笔者站在无数对知识如饥似渴的中国程序员之中,深深寄希望于国内的计算机图书编写者们,能以"深入探索"的思维方式和精益求精的认真态度来对待图书编写工作,但愿市面上多一些融入作者思考结晶的心血之作!

  魔幻数组名

  请看程序(本文程序在WIN32平台下编译):

1. #include <iostream.h>
2. int main(int argc, char* argv[])
3. {
4.  char str[10];
5.  char *pStr = str;
6.  cout << sizeof(str) << endl;
7.  cout << sizeof(pStr) << endl;
8.  return 0;
9. }

  1、数组名不是指针

  我们先来推翻"数组名就是指针"的说法,用反证法。

  证明 数组名不是指针

  假设:数组名是指针;

  则:pStr和str都是指针;

  因为:在WIN32平台下,指针长度为4;

  所以:第6行和第7行的输出都应该为4;

  实际情况是:第6行输出10,第7行输出4;

  所以:假设不成立,数组名不是指针

  2、数组名神似指针

  上面我们已经证明了数组名的确不是指针,但是我们再看看程序的第5行。该行程序将数组名直接赋值给指针,这显得数组名又的确是个指针!

  我们还可以发现数组名显得像指针的例子:

1. #include <string.h>
2. #include <iostream.h>
3. int main(int argc, char* argv[])
4. {
5.  char str1[10] = "I Love U";
6.  char str2[10];
7.  strcpy(str2,str1);
8.  cout << "string array 1: " << str1 << endl;
9.  cout << "string array 2: " << str2 << endl;
10.  return 0;
11. }

  标准C库函数strcpy的函数原形中能接纳的两个参数都为char型指针,而我们在调用中传给它的却是两个数组名!函数输出:

string array 1: I Love U
string array 2: I Love U

  数组名再一次显得像指针!

  既然数组名不是指针,而为什么到处都把数组名当指针用?于是乎,许多程序员得出这样的结论:数组名(主)是(谓)不是指针的指针(宾)。

  整个一魔鬼。

  揭密数组名

  现在到揭露数组名本质的时候了,先给出三个结论:

  (1)数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组;

  (2)数组名的外延在于其可以转换为指向其指代实体的指针,而且是一个指针常量;

  (3)指向数组的指针则是另外一种变量类型(在WIN32平台下,长度为4),仅仅意味着数组的存放地址!

  1、数组名指代一种数据结构:数组

  现在可以解释为什么第1个程序第6行的输出为10的问题,根据结论1,数组名str的内涵为一种数据结构,即一个长度为10的char型数组,所以sizeof(str)的结果为这个数据结构占据的内存大小:10字节。

  再看:

1. int intArray[10];
2. cout << sizeof(intArray) ;

  第2行的输出结果为40(整型数组占据的内存空间大小)。

  如果C/C++程序可以这样写:

1. int[10] intArray;
2. cout << sizeof(intArray) ;

  我们就都明白了,intArray定义为int[10]这种数据结构的一个实例,可惜啊,C/C++目前并不支持这种定义方式。

  2、数组名可作为指针常量

  根据结论2,数组名可以转换为指向其指代实体的指针,所以程序1中的第5行数组名直接赋值给指针,程序2第7行直接将数组名作为指针形参都可成立。

  下面的程序成立吗?

1. int intArray[10];
2. intArray++;

  读者可以编译之,发现编译出错。原因在于,虽然数组名可以转换为指向其指代实体的指针,但是它只能被看作一个指针常量,不能被修改。

  而指针,不管是指向结构体、数组还是基本数据类型的指针,都不包含原始数据结构的内涵,在WIN32平台下,sizeof操作的结果都是4。
顺便纠正一下许多程序员的另一个误解。许多程序员以为sizeof是一个函数,而实际上,它是一个操作符,不过其使用方式看起来的确太像一个函数了。语句 sizeof(int)就可以说明sizeof的确不是一个函数,因为函数接纳形参(一个变量),世界上没有一个C/C++函数接纳一个数据类型(如 int)为"形参"。

  3、数据名可能失去其数据结构内涵

  到这里似乎数组名魔幻问题已经宣告圆满解决,但是平静的湖面上却再次掀起波浪。请看下面一段程序:

1. #include <iostream.h>
2. void arrayTest(char str[])
3. {
4.  cout << sizeof(str) << endl;
5. }
6. int main(int argc, char* argv[])
7. {
8.  char str1[10] = "I Love U";
9.  arrayTest(str1);
10.  return 0;
11. }

  程序的输出结果为4。不可能吧?

  一个可怕的数字,前面已经提到其为指针的长度!

  结论1指出,数据名内涵为数组这种数据结构,在arrayTest函数体内,str是数组名,那为什么sizeof的结果却是指针的长度?这是因为:

  (1)数组名作为函数形参时,在函数体内,其失去了本身的内涵,仅仅只是一个指针;

  (2)很遗憾,在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。

  所以,数据名作为函数形参时,其全面沦落为一个普通指针!它的贵族身份被剥夺,成了一个地地道道的只拥有4个字节的平民。

  以上就是结论4。

  结束语

  最后,笔者再次表达深深的希望,愿我和我的同道中人能够真正以谨慎的研究态度来认真思考开发中的问题,这样才能在我们中间产生大师级的程序员,顶级的开发书籍。每次拿着美国鬼子的开发书籍,我们不免发出这样的感慨:我们落后太远了。

[ 本帖最后由 smallchilli 于 2006-12-17 13:06 编辑 ]

论坛徽章:
0
2 [报告]
发表于 2006-12-17 10:42 |只看该作者
支持一下。

论坛徽章:
0
3 [报告]
发表于 2006-12-17 12:02 |只看该作者
sizeof是不是一个函数?

sizeof(int)难道不能作为一个特例存在吗?

c代码都是经过编译器编译的,,,如果编译器说sizeof(int)作为特定形式而通过编译,,,难道不可以吗?


"每次拿着美国鬼子的开发书籍,我们不免发出这样的感慨:我们落后太远了。"
深有感触。

论坛徽章:
0
4 [报告]
发表于 2006-12-17 12:10 |只看该作者
原帖由 james.liu 于 2006-12-17 12:02 发表
sizeof是不是一个函数?

sizeof(int)难道不能作为一个特例存在吗?

c代码都是经过编译器编译的,,,如果编译器说sizeof(int)作为特定形式而通过编译,,,难道不可以吗?


"每次拿着美国鬼子的开发 ...


sizeof不是函数,是一个运算符。
对于计算类型的长度,必须用加括号的形式,例如sizeof(int).对于变量,可以不加括号,例如
int a;
sizeof a;
但习惯上都加括号。

论坛徽章:
0
5 [报告]
发表于 2006-12-17 12:17 |只看该作者
觉得这文章作者在用C写C++程序
还是C++写C程序呢?呵呵

论坛徽章:
0
6 [报告]
发表于 2006-12-17 12:19 |只看该作者
数组名是一个常数,常量。
指针是一个变量。
常量和变量的区别大家知道吧。
这么简单的一个问题还讨论来讨论去的。
有时间看看我的问题吧。

论坛徽章:
0
7 [报告]
发表于 2006-12-17 12:41 |只看该作者
其实让人迷惑的是sizeof  :/

论坛徽章:
0
8 [报告]
发表于 2006-12-17 12:43 |只看该作者
楼主讲的很清楚了,但篇幅太长,其实总结起来可以很简单的表述:
1、数组名不是指针。数组名代表一个地址,这个地址是数组中第一个元素的地址。
2、指针是一个变量,用来存放地址。
这就是本质的区别。

另外,楼主最后没有说明为什么数组名做为参数传递的时候会变成指针(想当年某高人写的一本xx指南里说数组名做参数时退化成指针,到现在我都还不明白什么叫退化)。
原因其实很简单,在调用一个函数的时候,首先会把传递给该函数的参数压入栈中。用c语言的话来说,就是分配了几个临时变量来存放传递给函数的参数。当参数是个数组名的时候,由于数组名是个地址,什么变量可以存放地址呢?答案当然是指针。所以被调用函数得到的数组名,其实是保存该数组名所代表地址的指针。
下面我举个例子(不熟悉汇编的朋友只要理解上面的文字表述就可以了):

  1. //c程序
  2. void test(int a[])
  3. {
  4.     int *a_prt = a;   
  5. }
  6. int main()
  7. {
  8.     int a[3] = {1,2,3};
  9.     test(a);
  10.          
  11. }
复制代码


部分x86反汇编

  1. 5 test:
  2.       6     pushl   %ebp
  3.       7     movl    %esp, %ebp
  4.       8     subl    $4, %esp
  5.       9     movl    8(%ebp), %eax //从%ebp + 8的位置取出值。这里取出的就是下面mian程序中压入的数组第一个元素的地址
  6.      10     movl    %eax, -4(%ebp)
  7.      11     leave
  8.      12     ret
  9.      13     .size   test, .-test
  10.      14 .globl main
  11.      15     .type   main, @function
  12.      16 main:
  13.      17     pushl   %ebp
  14.      18     movl    %esp, %ebp
  15.      19     subl    $24, %esp
  16.      20     andl    $-16, %esp
  17.      21     movl    $0, %eax
  18.      22     addl    $15, %eax
  19.      23     addl    $15, %eax
  20.      24     shrl    $4, %eax
  21.      25     sall    $4, %eax
  22.      26     subl    %eax, %esp
  23.      27     movl    $1, -24(%ebp) //数组第1元素存在%ebp-24的位置
  24.      28     movl    $2, -20(%ebp) //数组第2素存在%ebp-20位置
  25.      29     movl    $3, -16(%ebp) //数组第3素存在%ebp-16位置
  26.      30     leal    -24(%ebp), %eax //把数组第一个元素的地址(也就是数组名)存入%eax中
  27.      31     pushl   %eax //把%eax中内容压入栈。指针就产生了,压入%eax的这个地址就是test函数参数int a[]的地址,test函数会从这个地址取出数组的地址。
  28.      32     call    test
  29.      33     addl    $4, %esp
  30.      34     leave
  31.      35     ret
复制代码

论坛徽章:
0
9 [报告]
发表于 2006-12-17 12:50 |只看该作者
To smallchilli

转载文章请注明出处。这不仅是对作者的尊重,也是对自己的尊重。

----------------------------------------------------

在网上经常看到类似的文章,一上来先把国人数落一顿(他们很少对外国人这样做),想以此表明自己的叙述是多么的正确。我倒是发现:那些指责别人的人,往往是自己并没有真正理解所提问题的人,往往也在自觉或者不自觉地犯着概念上的错误。

比如,一个对数组的普遍的误解就是:数组变量不能被改变,是因为数组被转换为一个指针常量。上面文章的作者就持这种观点。其实真正的原因是因为数组是一个不可被改变的左值。在下面的帖子中我说明了“数组是常量”这种观点为什么是错误的:
http://bbs.chinaunix.net/viewthr ... p;extra=&page=2

在上面的文章中,作者想竭力证明“数组名不是指针”。其实这个问题是不需要证明的,因为数组和指针毕竟是两种不同的类型,不存在谁替代谁的问题。

想这样做的人其实还不明白一个基本的事实:数组对象本身(一个左值)的类型是数组,数组对象的值(一个右值)是一个指针。因此,数组对象是按照数组类型解释还是指针类型解释是由其在程序中所处的上下文环境决定的。“数组名不是指针”是不错的,但是也不要否认“数组名可以作为指针使用”,毕竟作为指针使用才是在程序中使用数组的主要方式。

数组可以作为两种类型(分别是数组和指针)使用,使数组成为一种特殊的类型,也使得人们对数组概念的理解容易产生偏差,即使是对一些有丰富经验的程序员来说也是这样,因为要真正了解数组,需要对表达式、左值和右值等概念有清醒的认识。所幸的是:即使不了解上面的概念,一般人也能正确地使用数组。

论坛徽章:
0
10 [报告]
发表于 2006-12-17 12:55 |只看该作者
原帖由 zx_wing 于 2006-12-17 12:43 发表
楼主讲的很清楚了,但篇幅太长,其实总结起来可以很简单的表述:
1、数组名不是指针。数组名代表一个地址,这个地址是数组中第一个元素的地址。
2、指针是一个变量,用来存放地址。
这就是本质的区别。

另外 ...


int test(int a[])
用gcc试了下,参数a居然不是常量,呵呵
编译器应该是作了简化,int test(int a[])被简化成int test(int *a),前者的语义其实不存在
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP