正如Joel在Stack Overflow Podcast 34中指出的,在C编程语言(aka:k&r)中,提到了C:a[5] == 5[a]中数组的这个属性。
乔尔说这是因为指针算术,但我还是不明白。为什么是a[5] == 5[a]?
- 像[+]这样的东西也可以像*(A++)或*(++A)那样工作吗?
- @埃贡:这很有创意,但不幸的是,编译器不是这样工作的。编译器将a[1]解释为一系列标记,而不是字符串:*(a operator operator operator
- C语言选择实现数组访问纯粹是作为一种句法糖分。这就是编译器无法检查左边部分是否是指针的原因。然后,不知怎么地,指针算术使得结果程序有效,即使它不是有效的。
- @Eldritchconundrum:我不同意它是无效的。里奇自己说是的。这可能是意外的结果,但我相信它仍然有效。
- 一个有趣的复合变化在不合逻辑的数组访问中得到了说明,其中使用了char bar[]; int foo[];和foo[i][bar]作为表达式。
- @EldritchConundrum,为什么您认为"编译器无法检查左侧部分是否为指针"?是的,可以。确实,对于任何给定的a和b,a[b]=*(a + b),但是对于所有类型,+都是语言设计者的自由选择,可以定义为交换的。没有什么能阻止他们禁止i + p,同时允许p + i。
- @安德烈,他们本可以禁止使用i+p,但破坏交换性会损害直觉。禁止i会更有意义,因为括号在视觉上建议访问数组。
- @Eldritchconundrum,对我来说,在这种情况下,是交换性伤害了直觉。对于指针,+运算符意味着偏移,而不是加法;它的参数性质不同,因此没有对称性。我们不能写i - p,可以吗?
- @andrey one通常期望+是交换的,所以真正的问题可能是选择使指针操作类似于算术,而不是设计一个单独的偏移量运算符。
- @每一个"我们不能写i-p":你是说减法通常是交换的吗?;-)
- 不仅是a[5] == 5[a],甚至是&a[5] == &5[a],也就是说,两者的价值并不相同,它们是非常相同的对象。
- @彼得,你不明白我的意思。不是运算符号是交换的,而是由它们表示的运算。使用+来表示偏移本身是可以的,但是偏移不同于加法,是不可交换的。您可以将7步向北偏移到一棵老橡树上以找到宝藏,但不能将一棵老橡树向北偏移7步。
- @当然可以,这是一个简单的向量加在性质上(你可以先将向量移动到树上,然后移动到偏移量,或者先移动到偏移量,然后移动到相同的向量上;这是完全可交换的),在数学和编程中(如果我们把地址空间看作一维向量的话)。显然,减法不是:本质上不是,数学上不是,编程上也不是。这两种情况都不足为奇。
- 注意:除非你记住/考虑到它的历史,否则试图找出为什么C以某种方式做事情并不总是卓有成效的。C被设置为端口Unix,Unix被设置为运行C——这有助于将Unix扩展到许多平台。因此,该语言主要是围绕使易于实现/端口编译器而设计的。如今,大多数语言语法都是以不同的目标设计的,比如可读性和一致性,或者实现速度,或者减少bug,或者以上所有的目标),所以你不会发现像这样的功能有什么意义。
- 乔尔是谁?***
C标准对[]运算符的定义如下:
a[b] == *(a + b)
因此,a[5]将评估为:
5[a]将评估:
a是指向数组第一个元素的指针。a[5]是距离a远5个元素的值,与*(a + 5)相同,从小学数学我们知道它们是相等的(加法是交换的)。
- 我想知道它是否更像*((5*sizeof(a))+a)。不过,很好的解释。
- 我完全是肛门…所以我无法抗拒。…标题中的作业员也在把我逼疯…但我不会变成那么大的把手。;-)
- 抱歉,"赋值操作符"让你抓狂了,但是我问的是数学等价性,不是表示代码片段,所以等号是正确的。谢谢你的回答!
- 为什么要考虑sizeof()。我认为指向"a"的指针指向数组的开头(即:0元素)。如果这是真的,你只需要*(A+5)。我的理解一定不正确。正确的原因是什么?
- 如果您有一个4字节整数数组,则a[1]-a[0]=4(两个指针之间的4字节diefernce)。
- @迪娜:从C编译器的角度来看,你是对的。不需要sizeof,我提到的那些表达式是相同的。但是,编译器在生成机器代码时会考虑sizeof。如果a是一个int数组,那么a[5]将编译为类似mov eax, [ebx+20]而不是[ebx+5]的类型。
- @迪娜:A是一个地址,比如说0x1230。如果a在32位int数组中,那么a[0]在0x1230,a[1]在0x1234,a[2]在0x1238…a[5]在x1244等。如果我们只将5添加到0x1230,则得到0x1235,这是错误的。
- 杰姆斯:宾果。这就是我需要看到的。我一直在看sizeof()和thinking count(),然后非常困惑。不是我最聪明的时刻。谢谢您!
- @作业员的评论只是对我的肛门问题的一种不客气的评论。;-)…我知道你的意思,我相信其他人也一样。好问题,顺便说一句,我只是在听他们谈论的播客。
- 所以在5[a]的情况下,编译器足够聪明,可以使用"*((5*sizeof(a))+a)"而不是"*(5+(a*sizeof(5))"?注:我想是的。我在海湾合作委员会试过,但效果很好。
- @SR105:这是+运算符的一种特殊情况,其中一个操作数是指针,另一个是整数。标准规定结果将为指针类型。编译器/必须足够聪明。
- 在我的记忆中,评论从来没有浮到顶端。
- 当您向指针添加一个整数时,编译器知道指针指向什么类型(所以如果a是int*,它是4个字节或其他什么类型…),所以可以正确执行算术。基本上,如果您执行"p++",那么应该调整p以指向内存中的下一个对象。"p++"基本上等同于"p=p+1",所以指针加法的定义使所有内容都对齐。还要注意,不能用void*类型的指针进行算术。
- @利特:我理解你的担心,可能会误导别人。然而,我希望保持简单的答案,就像在本文中一样,数组将衰减为指针。我把"做一个指针"改成"做一个指针",希望没问题。谢谢你的评论,顺便说一句。
- freeworld.thc.org/root/phun/unmintain.html提到这是一个很好的模糊策略,给出了一个示例myfunc(6291, 8)[Array];,其中myfunc只是模函数(相当于Array[3])。
- @Mehrdad我认为这篇文章被否决的主要原因比剥削性文章(这篇文章绝对值得放在首位)更多,是因为这篇文章解决了一个相对简单的问题,因此更多的人倾向于理解这一点。对这个漏洞的剖析并不是那么简单,大多数人都会跳过它:)
- "从小学数学我们知道这些是平等的"-我理解你是在简化,但我和那些认为这过于简单化的人在一起。*(10 + (int *)13) != *((int *)10 + 13)并不基本。换句话说,这里发生的事情比小学算术还要多。交换性关键依赖于编译器识别哪个操作数是指针(以及对象的大小)。换一种说法,以东十一〔8〕,以东十一〔9〕。
- @拉什:你说得对。我想说它更类似于(10in + 10cm),而不是苹果和桔子(你可以有意义地将它们转换成另一个)。
- @梅尔达:够公平的。也许一个更好的类比是日期和时间间隔,如(May 1st 2010 + 3 weeks)所示。
- "这是作为指针的数组的直接伪影":不,数组根本不作为指针。
- ‘A’是一个内存地址:不,如果你写了int x;,不超过x是一个内存地址。不过,数组的名称可以衰减为指向该数组第一个元素的指针。
- @托马拉克,我明白。有很多地方与之相关,我们已经讨论过了。然而,尽管这个问题具体地问了为什么它是这样工作的。我无法想象这是5[a]的行为,如果在C的原始实现中,指针不是真正的二进制文件,表示CPU可以直接理解的内存地址。如果我们想变得过于迂腐,那么答案(对这个问题以及更多问题)是:"因为标准定义了int类型的[]运算符在一侧的行为,以及数组或指针类型在另一侧的行为。"
- @吉姆:不,这是因为类型,而不是值是一样的。此外,小学算术不能盲目地应用于算术运算符。以INT_MAX - 5 + 1和INT_MAX + 1 - 5为例。
- 吉姆:很难。在这个问题上,EDOCX1的类型(9)和EDOCX1的类型(10)肯定不同。
- @吉姆:当你为了让我的回答看起来愚蠢而编辑你的评论时,它叫什么?您只需查找一些注释,就可以看到这种类型确实很重要。(10 + (int *)13) != ((int *)10 + 13)已经指出了这一点。
- 此外,我的"小学算术不能盲目地应用于算术运算符"的主张只需要一个例子来证明,进一步考虑而非盲目应用是必要的。我可以举几个例子。这里还有一个类型很重要的例子:T a = 7.0; double x = a / 2.0;很明显,a是int还是double在答案上有很大的不同。
- 由于浮点类型的范围和精度有限,可以使用更多的示例。我最初选择的例子,我之所以选择,是因为它涉及整数加法,与正在讨论的问题相同。
- @本沃伊特,我认为你的例子应该是double x = a / 2;。如果是2.0,不管a是int还是double,结果都是double。
- 在小学算术中,究竟是什么说,完全不同类型的加值必须始终是交换的?
- @仓鼠基因小学数学不谈论类型。我对OP问题的回答是唯一真实的答案:"因为C标准是这样说的。"
- @约翰马辛提尔,即使它不是自动递增的,它不是应该是*((5 * sizeof(*a)) + a)而不是*((5 * sizeof(a)) + a)吗?
- 从小学数学我们知道这些是相等的,我们确实知道加法是交换的,但是在相同类型的值的情况下!因此,添加一个指针和一个整数并不明显是一个交换操作!但这是由标准定义的…这不亚于在地址中添加5不会给出地址+5,而是地址+5*sizeof(type)!所以指针算法就不那么明显了。
- @Jean Baptisteyun是的。这个问题的技术答案是"因为语言规范说*(p+5)等于*(5+p),a[b]等于*(a+b)"。然而,*(p+5)等于*(5+p)的基本原理确实与"小学数学"一致。
- 当然,但在指针算术中,与初等数学的一致性不是一项要求。和是"类型化的",指针的类型,所以它不是"自然的",所以为什么您希望它是交换的?只是因为在程序集中生成的代码没有类型?
- @Jean Baptisteyun&232;s这不是要求。这是C语言设计人员作出的一个设计决定,大概是为了与加法运算符的交换性保持一致。当然,在设计语言时,最严格的意义上不需要任何东西。
- @Jean-BaptisteYun&232;S&;Mehrdad Afshari:也许值得一提的是,在汇编语言中,我们有时使用表的恒定基址和计算的偏移量来选择数组的项,有时我们对动态分配结构的成员具有恒定的偏移量。两种类型的访问,const[var]和var[const]都被转换成相同的CPU指令。可能是C,作为高级语言中的一个非常低级的语言,有意地继承了这种等价性。
- 一点历史可能有助于解释为什么会这样。如这里所指出的:GOTW.CA/CUN/HU3.HTM C和C++起源于BCPL。BCPL使用!作为间接运算符,它有一元和二元两种形式。EDCOX1〔1〕一元与C/C++中的EDCOX1〔2〕具有相同的意义,即一元间接。EDCOX1〔3〕二进制用于阵列查找,等效于EDCOX1〔4〕在C.中,因为二进制EDCOX1〔0〕在BCPL是可交换的,并且具有与EDCOX1〔6〕相同的效果。我非常怀疑这就是为什么阵列间接方向在C/C++中具有相同的交换行为的原因。
- @德格纳夫,哇,谢谢!
- 为什么在语法上允许按标准对整型文字进行索引?我看不出有人会故意写这个。标准可能允许这样做,因为添加一个检查会使编译器解析器/lexer稍微复杂一些。但我认为在当今世界,速度对编译的影响将是最小的,而捕获无意行为是非常有用的。更新版本的gcc甚至警告在交换机中出现故障,这是一种实际的故意使用。所以imho编译器至少应该对此发出警告。一般合同条款第8.2款没有给出警告,即使使用了-Wall。
因为数组访问是用指针定义的。a[i]的定义是指*(a + i),它是交换的。
- 数组不是根据指针定义的,但对它们的访问是。
- 我会加上"所以它等于*(i + a),可以写成i[a]"。
- 我建议您包括来自标准的引用,如下所示:6.5.2.1:2后缀表达式后跟方括号[]中的表达式是数组对象元素的下标指定。下标运算符的定义是e1[e2]与(((e1)+(e2))相同。由于应用于binary+运算符的转换规则,如果e1是数组对象(相当于数组对象的初始元素的指针),e2是整数,e1[e2]指定e1的e2第h元素(从零开始计数)。
- 更正确的做法是:数组在访问时会衰减为指针。
我认为其他答案遗漏了一些东西。
是的,根据定义,p[i]相当于*(p+i),这(因为加法是交换的)相当于*(i+p),这(同样,根据[]运算符的定义)相当于i。
(在array[i]中,数组名隐式转换为指向数组第一个元素的指针。)
但是加法的交换性在这种情况下并不那么明显。
当两个操作数属于同一类型,或者甚至属于提升为通用类型的不同数字类型时,交换性完全有意义:x + y == y + x。
但在本例中,我们专门讨论指针算法,其中一个操作数是指针,另一个是整数。(integer+integer是不同的操作,pointer+pointer是无意义的。)
C标准对+操作员(N1570 6.5.6)的描述如下:
For addition, either both operands shall have arithmetic type, or one
operand shall be a pointer to a complete object type and the other
shall have integer type.
它可以很容易地说:
For addition, either both operands shall have arithmetic type, or the left
operand shall be a pointer to a complete object type and the right operand
shall have integer type.
在这种情况下,i + p和i都是非法的。
在C++术语中,我们确实有两组重载的EDOCX1×7个运算符,它们可以被松散地描述为:
1
| pointer operator+(pointer p, integer i); |
和
1
| pointer operator+(integer i, pointer p); |
其中只有第一个才是真正必要的。
为什么是这样?
C++从C继承了这个定义,它从B中得到(数组索引的交换性在1972个用户对B的引用中明确提到),它从BCPL(手动日期1967)得到,它很可能是从更早的语言得到的(CPL)。Algol?).
所以数组索引是用加法来定义的,这个加法,即使是指针和整数,也是可互换的,可以追溯到几十年前的C语言。
这些语言比现代的C语言强得多。特别是,指针和整数之间的区别经常被忽略。(早期的C程序员在unsigned关键字添加到语言之前,有时会将指针用作无符号整数。)因此,由于操作数的类型不同,因此使加法不可交换的想法可能不会出现在这些语言的设计者身上。如果一个用户想要添加两个"东西",不管这些"东西"是整数、指针还是其他什么东西,都不能用语言来阻止它。
多年来,对该规则的任何更改都会破坏现有的代码(尽管1989年的ANSIC标准可能是一个好机会)。
改变C和/或C++需要把指针放在左边和右边的整数可能会破坏一些现有代码,但是不会有真正的表达能力的损失。
所以现在我们有了arr[3]和3[arr],意思完全一样,尽管后者不应该出现在ioccc之外。
- 对这个属性的精彩描述。从高层来看,我认为3[arr]是一件有趣的艺术品,但很少使用。这个问题(<">stackoverflow.com/q/1390365/356>)的公认答案改变了我对语法的看法。尽管在技术上通常没有正确和错误的方法来做这些事情,但是这些类型的特性会让您以一种与实现细节分离的方式开始思考。这种不同的思考方式有好处,当您专注于实现细节时,这种方式部分地会丢失。
- 加法是交换的。对于C标准来说,如果不这样定义它,那将是很奇怪的。这就是为什么它不能简单地说"加法,要么两个操作数都有算术类型,要么左边的操作数是指向完整对象类型的指针,右边的操作数应该是整数类型。"—这对大多数添加东西的人来说是没有意义的。
- @加法通常是交换的——它通常需要两个相同类型的操作数。指针添加允许添加指针和整数,但不能添加两个指针。imho这已经是一个非常奇怪的特殊情况,要求指针是左操作数并不是一个很大的负担。(有些语言使用"+"进行字符串连接;这当然不是交换的。)
- 在字符串示例中为真!从这个角度来看,这看起来像是一个语言决策,它来自于事物的实现方面,而不是设计。
- @iheanyi:数字的加法是交换的,但这并不意味着加法必须和非数字的东西交换。汇编程序通常要求涉及可重定位符号的每个地址必须是"rel_symbol"、"rel_symbol+number"或"rel_symbol-number"的确切形式,因为链接器希望得到一个修复列表,每个修复列表标识一个"base"符号及其使用位置(预修复代码将保留numb将ER添加到符号中)。
- @iheanyi:我认为从规则的角度来看,加法运算符的第二个操作数必须是数字,结果类型将与第一个操作数匹配,而不是说"至少一个"操作数必须是数字,这是比较清楚的。顺便说一句,如果加法运算符总是返回其左侧操作数的类型,而不是说给定uint32_t x=0;,x-1的值在某些实现上必须产生4294967295,而在其他实现上必须产生-1,那么与无符号类型相关的许多麻烦就可以消除。
- @超级卫星,更糟。这意味着有时候x+1!=1+x。这将完全违反加法的结合性质。
- @iheanyi:我认为您的意思是交换属性;加法已经不是关联的,因为在大多数实现中(1ll+1u)-2!=1LL+(1U-2)。事实上,这种变化会使一些目前不相关的情况发生关联,例如3u+(uint-max-2l)等于(3u+uint-max)-2。不过,最好的办法是,语言为可提升整数和"包装"代数环添加新的不同类型,这样,在拥有65535的ring16_t中添加2,将生成值为1的ring16_t,而不受int大小的影响。
- @超级卫星-谢谢你的回复。这用一个很好的例子澄清了手头的问题:)
当然
1
| ("ABCD"[2] == 2["ABCD"]) && (2["ABCD"] == 'C') && ("ABCD"[2] == 'C') |
这主要是因为在70年代,当C被设计时,计算机没有太多的内存(64KB是很多),所以C编译器没有做太多的语法检查。因此,"X[Y]"被盲目地翻译成"*(X+Y)"。
这也解释了"EDOCX1"(12)和"EDOCX1"(13)语法。"A = B + C"形式的所有内容都具有相同的编译形式。但是,如果B与A是同一个对象,那么就可以进行装配级优化。但是编译器不够聪明,无法识别它,所以开发人员不得不(A += C)。同样,如果C是1,则可以使用不同的汇编级优化,开发人员必须再次明确说明,因为编译器无法识别它。(最近编译器会这样做,所以这些语法现在基本上是不必要的)
- 实际上,它的计算结果为假;第一个术语"abcd"[2]==2["abcd"]的计算结果为真,或1和1!="C":D
- @乔纳森:同样的歧义导致了这篇文章最初标题的编辑。我们是等号,数学等价,代码语法,还是伪代码?我认为数学等价,但既然我们在谈论代码,我们就不能逃避我们是从代码语法的角度来看待一切。
- 这不是神话吗?我的意思是,+=和++操作符是为了简化编译器而创建的?有些代码使用它们会变得更清晰,不管编译器使用它们做什么,使用它们都是很有用的语法。
- +=和++还有另一个显著的好处。如果左侧在计算时更改了某个变量,则更改将只执行一次。A=A+…;会做两次。
- 听说当你写变量名两次而不是三次时,+=减少了出错的几率…
- 带有对象的a=a+通常会导致对象的未优化副本,因为它必须生成a的副本。a+=不需要副本,直接对其进行评估。
- "abcd"[2]是否解析为"cd"?如果你想解决"c",你就必须使用取消引用,即*("ABCD"[2]) == 'C')。
- 否-"abcd"[2]==*("abcd"+2)=*("cd")='c'。取消对字符串的引用将为您提供一个字符,而不是子字符串
- "这样更容易实现"比"在数学上更合理,所以即使它没有任何实际意义,我们还是把它作为一个理性的"添加到语言中。
- 据我回忆,algol68是组合算术和赋值运算符的起源,就像在foo +:= bar中一样,发音为"foo plus,并成为bar"。我认为其基本原理是,这更接近于一开始人们想要做的事情,即"为foo添加条"(尽管我不知道为什么我们没有将bar =:+ foo)排除在逻辑之外)。
- @Thomaspardon McCarthy:从这里开始:"在开发过程中,[Thompson]不断地与内存限制作斗争:每种语言的添加都会使编译器膨胀,使其几乎无法适应,但每种利用功能的重写都会减小其大小。例如,B引入了广义赋值运算符,使用x=+y将y添加到x中……汤普森发明了+和-运算符,更进一步……创新的动力可能是他观察到,++x的翻译比x=x+1的翻译小。"
- @戴夫:是x += 5;而不是x =+ 5;,因为后者会被解析为x = (+5);。
- @Jamescurran我很确定它最初是以LHS =- RHS;开始的,最终被换成使用-=。
- ++经常映射到一条机器指令,而x=x+1可能不止一条。x+=3映射到更少的机器指令,x=x+3,因为我们知道,一个人会拿起x一次,加上3个,然后再把它放回去。寄存器int x=3来自同一个时代,当时编译器并不像现在这么聪明。
- @詹姆斯库伦的一元埃多克斯1〔7〕在C早期并不存在。
- @米勒斯鲁特:也许不是,但是一元减号肯定是这样,导致了同样的问题。
- pdp11微型计算机(pdp用于第一个c和unix操作系统)具有+=-=++-的汇编指令,因此,虽然algol中可能存在先驱者,但在指令集和语言能力之间有一点1对1的映射。
- @梵蒂尼是对的,它是在埃多克斯1〔9〕之前的埃多克斯1〔8〕。B编程语言(我很惊讶看到它仍然被使用),C的祖先,使用=+形式。IIRC,改变它的主要原因是i=-1;模糊不清。对于编译器来说并不含糊,但对于理解这是否应该将i减少1(并因此正确编写)或是否应该将-1分配给i的人类读者来说,这并不含糊(因此代码中存在错误)。免责声明:我可能记错了。
- @约翰伯德引用的一句话开头是"一个更强大的创新动力……"只是循环推理。他不可能在发明它之前就注意到它。事实上,PDP-11既有预递增指令,也有后递减指令,或者可能是相反的,已经37年了。
- 那么,如果++基本上是多余的,那么C++是没有必要的吗?我自己也在为C___等待。
- @迪娜,同意,因此进行了相应的编辑。
有一件事似乎没有人提到迪娜与以东的问题1(18):
只能向指针添加整数,不能将两个指针一起添加。这样,当向一个整数添加一个指针,或向一个指针添加一个整数时,编译器总是知道哪个位的大小需要考虑。
- 在接受答案的评论中有一个相当详尽的讨论。我在编辑时引用了刚才的谈话,但没有直接提到你对sizeof非常有效的关注。不知道如何最好地做到这一点。我应该再编辑一下猎户座吗?问题?
从字面上回答这个问题。江户十一〔二十〕并不总是这样。
1 2 3
| double zero = 0.0;
double a[] = { 0,0,0,0,0, zero/zero}; // NaN
cout << (a[5] == 5[a] ?"true" :"false") << endl; |
印刷品
- 实际上,"南"并不等于它本身:cout << (a[5] == a[5] ?"true" :"false") << endl;就是false。
- @特鲁伊:他确实说过,特别是针对NAN案件(特别是x == x并不总是正确的)。我认为那是他的意图。因此,他在技术上是正确的(而且,正如他们所说,可能是最好的一种正确!).
- 问题是关于C,您的代码不是C代码。在中也有一个NAN,比0.0/0.0好,因为0.0/0.0在没有定义__STDC_IEC_559__时是ub(大多数实现没有定义__STDC_IEC_559__,但在大多数实现上,0.0/0.0仍然有效)。
很好的问题/答案。
只是想指出C指针和数组是不一样的,尽管在这种情况下,差异并不重要。
考虑以下声明:
在a.out中,符号a位于数组开始的地址处,而符号p位于存储指针的地址处,并且该内存位置的指针值是数组的开始。
- 不,从技术上讲,它们是不一样的。如果您将一些b定义为int*const并使其指向一个数组,那么它仍然是一个指针,这意味着在符号表中,b指的是存储地址的内存位置,而该地址又指向数组所在的位置。
- 很有道理。我记得我在一个模块中将全局符号定义为char s[100]时遇到了一个非常严重的错误,在另一个模块中将其声明为extern char*s;。在把它们连接在一起之后,程序的行为非常奇怪。因为使用extern声明的模块使用数组的初始字节作为char的指针。
- 最初,在C的祖辈BCPL中,数组是一个指针。也就是说,当你写(我把它译成c)int a[10]时,你得到的是一个名为"a"的指针,它指向其他地方足够存储10个整数。因此a+i和j+i有相同的形式:添加几个内存位置的内容。实际上,我认为BCPL是无类型的,所以它们是相同的。而且sizeof类型的扩展也不适用,因为bcpl是纯面向字的(在字寻址机器上也是)。
- 我认为理解差异的最好方法是比较int*p = a;和int b = 5;,后者中,"b"和"5"都是整数,但"b"是变量,"5"是固定值。同样,"p"和"a"都是字符的地址,但"a"是固定值。
我只是发现这个丑陋的语法可能是"有用的",或者当你想处理一个索引数组时,至少是非常有趣的,它引用同一个数组中的位置。它可以替换嵌套的方括号,使代码更可读!
1 2 3 4 5 6 7 8 9 10
| int a[] = { 2 , 3 , 3 , 2 , 4 };
int s = sizeof a / sizeof *a; // s == 5
for(int i = 0 ; i < s ; ++i) {
cout << a[a[a[i]]] << endl;
// ... is equivalent to ...
cout << i[a][a][a] << endl; // but I prefer this one, it's easier to increase the level of indirection (without loop)
} |
当然,我很确定在真正的代码中没有这种情况,但是我发现它很有趣:)
- 天哪!!!!怎么会有人说他更喜欢那个符号呢!!!!它伤了我的眼睛!!!!
- 当你看到i[a][a][a]时,你认为我要么是指向数组的指针,要么是指向数组或数组的指针数组…a是一个指数。当您看到a[a[a[i]]]时,您认为a是指向数组或数组的指针,i是索引。
- 真的!使用这个"愚蠢"的功能非常酷。在某些问题的算法竞赛中可能有用)
对于C中的指针,我们有
而且
因此,a[5] == 5[a].是真的
不是答案,只是一些思考的食物。如果类具有重载的index/subscript运算符,则表达式0[x]将不起作用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Sub
{
public:
int operator [](size_t nIndex)
{
return 0;
}
};
int main()
{
Sub s;
s[0];
0[s]; // ERROR
} |
因为我们没有访问int类的权限,所以不能这样做:
1 2 3 4
| class int
{
int operator[](const Sub&);
}; |
- class Sub { public: int operator[](size_t nIndex) const { return 0; } friend int operator[](size_t nIndex, const Sub& This) { return 0; } };
- 你真的试过编译它吗?有一组不能在类外部实现的运算符(即非静态函数)!
- 噢,你说得对。"operator[]应该是一个只有一个参数的非静态成员函数,"我熟悉operator=上的限制,不认为它适用于[]。
- 当然,如果更改[]操作符的定义,它将不再是等价的…如果a[b]等于*(a + b)而你改变它,你将不得不超载int::operator[](const Sub&);和int也不是一个类…
- 这……不是……C。
在C语言的指针和数组教程中,它有很好的解释。Ted Jensen。
泰德·詹森解释说:
In fact, this is true, i.e wherever one writes a[i] it can be
replaced with *(a + i) without any problems. In fact, the compiler
will create the same code in either case. Thus we see that pointer
arithmetic is the same thing as array indexing. Either syntax produces
the same result.
This is NOT saying that pointers and arrays
are the same thing, they are not. We are only saying that to identify
a given element of an array we have the choice of two syntaxes, one
using array indexing and the other using pointer arithmetic, which
yield identical results.
Now, looking at this last
expression, part of it.. (a + i), is a simple addition using the +
operator and the rules of C state that such an expression is
commutative. That is (a + i) is identical to (i + a). Thus we could
write *(i + a) just as easily as *(a + i).
But *(i + a) could have come from i[a] ! From all of this comes the curious
truth that if:
writing
is the same as writing
- A+I不是简单的加法,因为它是指针算术。如果a元素的大小是1(char),那么是的,它就像integer+。但如果它是一个整数,那么它可能等于一个+4*i。
- @Alexbrown是的,它是指针算术,这正是你最后一句话出错的原因,除非你首先把"a"转换成(char*)(假设int是4个字符)。我真的不明白为什么这么多人会对指针算术的实际值结果产生怀疑。指针算法的全部目的是抽象出底层指针值,让程序员考虑被操作的对象,而不是地址值。
我知道问题得到了回答,但我忍不住要分享这个解释。
我记得编译器设计的原则,假设a是int数组,int的大小为2字节,&a的基址为1000。
a[5]将如何工作->
1 2
| Base Address of your Array a + (5*size of(data type for array a))
i.e. 1000 + (5*2) = 1010 |
所以,
同样,当C代码分解为3个地址代码时,5[a]将变为->
1 2
| Base Address of your Array a + (size of(data type for array a)*5)
i.e. 1000 + (2*5) = 1010 |
所以基本上,这两个语句都指向内存中相同的位置,因此,a[5] = 5[a]。
这个解释也是数组中负索引在C中工作的原因。
也就是说,如果我访问a[-5],它会给我
1 2
| Base Address of your Array a + (-5 * size of(data type for array a))
i.e. 1000 + (-5*2) = 990 |
它将返回位置990处的Me对象。
在C数组中,arr[3]和3[arr]是相同的,它们的等效指针符号是*(arr + 3)到*(3 + arr)。但与此相反,[arr]3或[3]arr是不正确的,会导致语法错误,因为(arr + 3)*和(3 + arr)*不是有效的表达式。原因是取消引用运算符应放在表达式生成的地址之前,而不是放在地址之后。
在C编译器中
引用数组中元素的方法不同!(一点也不奇怪)
现在有点历史了。在其他语言中,BCPL对C的早期发展有相当大的影响。如果在bcpl中声明的数组类似于:
这实际上分配了11个字的内存,而不是10个字。通常V是第一个,包含紧接着的单词的地址。因此,与C不同的是,命名V到达了那个位置,并获取了数组零元素的地址。因此,在bcpl中数组间接,表示为
实际上必须执行EDOCX1(使用bcpl语法),因为需要获取v来获取数组的基地址。因此,V!5和5!V是同义词。作为一个轶事观察,wafl(华威函数语言)是用bcpl编写的,据我所知,倾向于使用后一种语法而不是前一种语法来访问用作数据存储的节点。当然这是35到40年前的事了,所以我的记忆有点生疏。:)
后来有了一项创新,即去掉多余的存储字,让编译器在数组命名时插入其基地址。根据C历史论文,这种情况发生在将结构添加到C时。
注意,bcpl中的!同时是一元前缀运算符和二进制中缀运算符,在这两种情况下都执行间接寻址。只是二进制形式在进行间接寻址之前包含了两个操作数的加法。考虑到bcpl(和b)面向单词的特性,这实际上很有意义。在C语言中,当获取数据类型时,需要对"指针和整数"进行限制,于是sizeof就成了一回事。
在C中
1 2 3 4 5 6
| int a []={10,20,30,40,50};
int *p =a ;
printf("%d
",*p ++);//output will be 10
printf("%d
",*a ++);//will give an error |
指针是"变量"
数组名是"助记键"或"同义词"
p++;有效,但a++无效
a[2]等于2〔A〕,因为这两者的内部操作都是
"指针算术"内部计算为
*(a+3)等于*(3+a)。
好吧,这是一个只有语言支持才可能实现的特性。
编译器将a[i]解释为*(a+i),表达式5[a]的计算结果为*(5+a)。因为加法是交换的,所以两个都是相等的。因此,该表达式的计算结果为true。
指针类型
1)指向数据的指针
2)常量指向数据的指针
3)常量指针指向常量数据
数组是我们列表中的(2)种类型当一次定义数组时,该指针中的一个地址将被初始化。正如我们所知道的,我们不能更改或修改程序中的常量值,因为它在编译时抛出了一个错误。
我发现的主要区别是…我们可以用一个地址重新初始化指针,但数组的情况不同。
不等于回到你的问题…A[5]只是*(A+5)你很容易理解一个包含地址(人们称之为基地址)就像我们列表中的指针类型(2)[]-该运算符可以用指针*替换。
所以最后…
1
| a[5] == *(a +5) == *(5 + a) == 5[a] |