为什么我们有后缀增量?


55

免责声明:我完全清楚前缀和后缀增量的语义。因此,请不要向我解释它们的工作原理。

阅读有关堆栈溢出的问题,我不禁注意到程序员一遍又一遍地被后缀增量运算符弄糊涂了。由此产生以下问题:是否存在用例后缀增量在代码质量方面真正受益的用例?

让我用一个例子来澄清我的问题。这是以下内容的超级简洁实现strcpy

while (*dst++ = *src++);

但这并不是我书中最能自我记录的代码(它会在理智的编译器上产生两个烦人的警告)。那么以下替代方案有什么问题呢?

while (*dst = *src)
{
    ++src;
    ++dst;
}

然后,我们可以摆脱条件中令人困惑的分配,并获得完全无警告的代码:

while (*src != '\0')
{
    *dst = *src;
    ++src;
    ++dst;
}
*dst = '\0';

(是的,我知道,src并且dst在这些替代解决方案中将具有不同的结束值,但是由于strcpy在循环之后立即返回,因此在这种情况下无关紧要。)

后缀增加的目的似乎是使代码尽可能简洁。我只是看不到我们应该如何努力。如果这最初是关于性能的,那么今天它仍然有意义吗?


7
postfix变体是最常用的变体,因此如果要对它进行辩护,则最好是一个非常有力的案例。您的第一个示例有点像个稻草人,因为我怀疑很少有人会用strcpy这种方式实际编码该方法(出于您已经提到的原因)。
罗伯特·哈维,

4
为什么两者都没有?
James McNellis 2011年

31
如果我们从语言中删除所有可能使程序员感到困惑的东西,那么我们将没有太多功能。某些内容对您无用或仅很少有用的事实并不意味着应将其删除。如果不再相关,请勿使用。结束。
Cody Gray

4
是的,但是那你永远做不到int c = 0; c++;
Biff MaGriff

4
@Cody:因为有些事情既混乱又有用。他不对后增量本身感到困惑,对它的有用性感到困惑。我们不应该拥有无用的,混乱的或没有混乱的东西。
GManNickG'2

Answers:


23

尽管它确实曾经对性能产生一些影响,但我认为真正的原因是要清晰表达您的意图。真正的问题是,事物是否while (*d++=*s++);清楚地表达了意图。IMO确实如此,我发现您提供的替代方案不太清楚-但这(很容易)可能是由于数十年来习惯了如何完成工作的结果。从K&R学过C(因为当时几乎没有关于C的其他书籍)可能也会有所帮助。

在一定程度上,在较早的代码中,简洁的价值更高。我个人认为这在很大程度上是一件好事-了解几行代码通常是相当琐碎的;困难的是理解大量代码。测试和研究反复表明,一次将所有代码都显示在屏幕上是理解代码的主要因素。随着屏幕的扩展,这似乎仍然是正确的,因此保持代码(合理)简洁仍然很有价值。

当然有可能过分,但是我认为不是。具体来说,当理解一行代码变得极其困难或耗时时,我认为这太过分了-具体地说,当理解更少的代码行比理解更多的行时要花费更多的精力时。这在Lisp和APL中很常见,但是(至少在我看来)不是这种情况。

我不太担心编译器警告-根据我的经验,许多编译器会定期发出非常荒谬的警告。虽然我当然认为人们应该理解他们的代码(以及它可能产生的任何警告),但是恰巧在某些编译器中触发警告的体面代码不一定是错误的。诚然,初学者并不总是知道自己可以安全地忽略什么,但是我们不会永远留在初学者身边,也不需要像我们一样编码。


12
+1指出简短的版本对我们中的某些人更容易阅读。:-)

1
如果您不喜欢某些编译器警告(并且我有很多其他需要做的-认真地说,我想输入if(x = y),我发誓!),您应该调整编译器的警告选项,以免您无意间错过了警告你不要喜欢。

4
@Brooks摩西:我是很多不太热衷那个。我不喜欢污染我的代码以弥补编译器的缺点。
杰里·科芬

1
@ Jerry,@ Chris:此注释用于您认为警告通常是一件好事的情况,但您不希望它应用于发生异常情况的特定行。如果您从不想要警告并认为它有缺点,请使用-Wno-X,但是如果您只想对它做一个例外,并希望将警告应用到其他地方,则比使用克里斯所指的奥秘跳环要容易得多。 。

1
@Brooks:双括号和使(void)x未使用的警告静音是众所周知的习惯用法,用于使本来有用的警告静音。注释看起来有点像pragmas,充其量是无法移植的:/
Matthieu M.

32

以前是硬件的东西


您会发现有趣的事情。Postfix增量可能有很多完全好的原因,但是像C中的许多东西一样,它的流行可以追溯到语言的起源。

尽管C是在各种早期且功能不足的机器上开发的,但C和Unix最早是在PDP-11的内存管理模型中引起了相对的关注。这些计算机在当时是相对重要的计算机,而Unix 比-11可用的其他7种简陋的操作系统好得多(指数级要好)。

而且碰巧在PDP-11上,

*--p

*p++

... 在硬件中作为寻址模式实现。(也*p,但没有其他组合。)在所有低于0.001 GHz的早期机器上,将一条指令或两条指令保存在一个循环中几乎必须等待一秒钟或一分钟或等待一段时间。 -午餐差异。这并不是专门针对postincrement,而是带有指针postincrement的循环可能比那时的索引要好得多。

结果,设计模式因为C习语变成了C思维宏。

这就像在{... 之后立即声明变量一样……不是因为C89是当前的要求,而是现在是一种代码模式。

更新:显然,主要原因*p++是使用该语言,因为这正是人们经常想要做的。代码模式的流行是通过流行的硬件来增强的,这些硬件与PDP-11即将问世之前设计的一种语言的模式相匹配。

如今,使用哪种模式或使用索引都无济于事,而且无论如何我们通常都在较高级别上进行编程,但是在那些0.001GHz机器上,它一定起了很大的作用,并且使用了非*--x*x++将意味着您使用了其他任何东西没有“获得” PDP-11,您可能会有人来找您,说:“您知道吗……” :-) :-)


25
维基百科说:“一个(假)民间神话是,PDP-11指令集架构的影响惯用使用C语言编程的PDP-11的递增和递减寻址模式对应,−−ii++构建在C,如果ij都是寄存器变量,一个表达式*(−−i) = *(j++)可以编译成一条机器指令。[...]丹尼斯·里奇(Dennis Ritchie)明确地与这个民间神话相抵触。[C语言的发展 ]“
fredoverflow 2011年

3
呵呵(来自FredOverflow注释中的链接):“人们经常猜测它们是使用DEC PDP-11提供的自动递增和自动递减地址模式创建的,这在C和Unix上最先流行。这是不可能的,因为在开发B时没有PDP-11,但是PDP-7确实有一些“自动增量”存储单元,其特性是通过它们进行的间接存储引用增加了该单元。向汤普森(Thompson)建议了这样的运算符;使它们成为前缀和后缀的概括是他自己的。”

3
DMR只说PDP-11并不是添加到C中的原因。我也是这么说的。我说的是,正是由于这种原因,PDP-11才被利用,并成为一种流行的设计模式。如果Wikipedia 认为这是错误的话,那么我不会那样看。在该W页上措辞不佳。
DigitalRoss 2011年

3
@FredOverflow。我认为您误解了“民间神话”的确切含义。神话是C旨在利用这11个操作,不是因为它们不存在,也不是因为每个人都知道他们映射到11个操作井,所以代码模式并未流行。万一还不清楚:PDP-11实际上确实在几乎每条指令的硬件中都实现了这两种模式,并将其作为寻址模式,它确实是C上运行的第一台重要机器。
DigitalRoss 2011年

2
“在那些0.001GHz的机器上,这一定很重要”。嗯,我讨厌这样说,因为它要追溯到我,但我有适用于我的CPU的Motorola和Intel手册,它们查找指令的大小以及执行了多少周期。找到最小的代码,以便能够为打印后台处理程序添加更多功能,并节省几个周期,这意味着串行端口可以跟上某些协议,例如新发明的MIDI。C减轻了一些挑剔的麻烦,特别是随着编译器的改进,但是了解什么是最有效的代码编写方法仍然很重要。
Tin Man

14

Ken Thompson在B语言(C的前身)中引入了前缀,后缀--++运算符-否,它们并没有受到当时不存在的PDP-11的启发。

引用丹尼斯·里奇的“ C语言的发展”

汤普森(Thompson)通过发明++--运算符,递增或递减;它们的前缀或后缀位置确定更改是在记录操作数的值之前还是之后进行的。它们不是B的最早版本,而是一路出现。人们经常猜测他们是使用DEC PDP-11提供的自动递增和自动递减地址模式创建的。从历史上讲这是不可能的,因为在开发B时没有PDP-11。但是,PDP-7确实具有一些“自动递增”存储单元,其特性是通过它们进行的间接存储引用增加了该单元。汤普森(Thompson)可能建议使用此运算符。使它们都带有前缀和后缀的概括是他自己的。确实,++x比的小x=x+1


8
不,我和汤普森没有关系。
基思·汤普森

感谢您提供非常有趣的参考!这证实了我来自原始K&R的报价,该报价提出了紧凑性而非技术优化的目标。我不知道但是这些运营商已经在存在B.
克里斯托夫

@BenPen据我所知,如果在另一个 SE网站上存在重复项,则没有关闭问题的政策,只要这两个问题都适合各自的站点即可。
Ordous

有趣的是……程序员当然不是一个很接近的问题。
BenPen

@BenPen:关于堆栈溢出问题的公认答案已经引用了我所做的相同消息(尽管不是很多)。
基思·汤普森

6

postincrement运算符存在的明显原因是,您不必在各处编写像(++x,x-1)或这样的表达式,也不必将(x+=1,x-1)琐碎的琐碎语句(具有易于理解的副作用)分离成多个语句。这里有些例子:

while (*s) x+=*s++-'0';

if (x) *s++='.';

每当向前读或写字符串时,后递增而不是预递增几乎总是自然的操作。我几乎从未遇到过现实中用于预增量的用途,而我发现用于预减量的唯一主要用途是将数字转换为字符串(因为人类书写系统会向后写入数字)。

编辑:其实不会那么难看;代替(++x,x-1)你当然可以使用++x-1(x+=1)-1。还是x++更具可读性。


您必须小心那些表达式,因为您正在修改和读取两个序列点之间的变量。不保证这些表达式的求值顺序。

@Snowman:哪一个?我在回答中没有看到此类问题。
R..

如果您有类似(++x,x-1)(函数调用,也许?)之类的东西

@Snowman:这不是函数调用。它是C逗号运算符,它是一个序列点,用括号括起来以控制优先级。结果与x++大多数情况下相同(一个例外:x等级为的无符号类型,int其初始值为x该类型的最大值)。
R ..

啊,棘手的逗号运算符...当逗号不是逗号时。逗号运算符的括号和稀有性使我失望。没关系!

4

PDP-11在指令集中提供了后递增和递减运算。现在这些不是指令。它们是指令修饰符,使您可以指定要在递增或递减之后的寄存器值。

这是机器语言中的单词复制步骤:

movw (r1)++,(r2)++

将一个单词从一个位置移到另一个位置,并由寄存器指向,寄存器将准备好再次执行该操作。

随着C建立在PDP-11上并用于PDP-11,许多有用的概念进入了C。C旨在替代汇编语言。为对称起见,增加了前增加和后减少。


那是出色的修改器...这让我想学习PDP-11组装。顺便说一句,您有关于“对称性”的参考吗?这是有道理的,但这是历史,有时不是关键。大声笑
BenPen

2
也称为寻址模式。PDP-11提供了递增后和递减前的功能,它们可以很好地配对以用于堆栈操作。
埃里克·艾德

1
PDP-8提供了一种后递增存储间接寻址,但没有相应的前递减操作。使用磁芯内存时,读取内存中的单词将其擦除(!),因此处理器使用回写功能来恢复刚读取的单词。在回写之前应用增量很自然,并且更有效的块移动成为可能。
埃里克·艾德

3
抱歉,PDP-11并没有激发这些运营商。当肯·汤普森(Ken Thompson)发明它们时,它还不存在。看我的回答
基思·汤普森

3

我怀疑这是否真的必要。据我所知,在大多数平台上,它没有比循环中使用预增量更紧凑的编译方式。只是在编写时,简洁的代码比清晰的代码更为重要。

这并不是说代码的清晰度不重要,也不是不重要,但是当您在低波特率的调制解调器上键入信号时(每次测量波特率都很慢),每次击键都必须使之大型机,然后一次用一个位作为奇偶校验来回送一个字节,您不需要输入太多。

这有点像&和| 优先级低于==的运算符

它的设计是出于最佳意图(&&和||的非短路版本),但是现在它每天都会使程序员感到困惑,并且它可能永远不会改变。

无论如何,这只是一个答案,因为我认为您的问题没有一个好的答案,但是比我更精通的编码器可能会证明我是错误的。

- 编辑 -

我要指出,找到同时具有非常有用的,我只是指出它不会改变任何人是否喜欢还是不喜欢。


事实并非如此。“简洁的代码”绝比清晰度更重要
Cody Gray

好吧,我认为这更多的是关注点。你说的很对,虽然,它肯定是从来没有比代码的清晰度重要......对于大多数人。

6
好吧,“简洁”的定义是“平滑优雅:优美”或“用几个词:没有多余的东西”,这两种东西在编写代码时通常都是一件好事。许多人认为,“ Terse”并不意味着“晦涩,难以阅读和混淆”。
James McNellis 2011年

@James:没错,我认为大多数人在编程环境中使用“简洁”的第二个定义是:“用很少的词;没有多余的东西”。在这种定义下,想像一下某些特定代码的简洁性和表达性之间存在折衷的情况当然更容易。显然,编写代码时要做的正确的事情与讲话时是相同的:使事情尽可能短,但不要短。重视简洁而不是表现力可能会导致代码看上去晦涩难懂,但事实并非如此。
Cody Gray

1
倒带40年,想象一下您必须将程序安装在只能容纳约1000个字符的鼓上,或者编写一个编译器只能使用4000字节的ram。有时候,简洁与清晰甚至都不是问题。您必须简洁,此星球上不存在可以存储,运行或编译非简洁程序的计算机。

3

在处理指针(无论是否取消引用它们)时,我喜欢后缀开孔器,因为

p++

比同等读法更自然地读为“移至下一个位置”

p += 1

这肯定会使初学者在第一次看到它与sizeof(* p)!= 1。

您确定要正确说明混淆问题吗?Postfix ++运算符的优先级高于取消引用*运算符的优先级,这不是使初学者感到困惑的事情,从而

*p++

解析为

*(p++)

并不是

(*p)++

如某些人所料?

(您认为这很糟糕吗?请参阅阅读C声明。)


2

良好编程的微妙元素包括本地化极简主义

  • 将变量放在最小的使用范围内
  • 当不需要写访问权限时使用const
  • 等等

本着同样的精神,这x++可以看作是定位对xwhile 的当前值的引用的一种方式,同时立即表明您(至少现在)已经使用该值完成并想要移至下一个(有效值xint还是指针或迭代器)。稍加想象,您可以将其比作让旧值/位置x不再需要时立即“超出范围”,然后转移到新值。 后缀突出显示了可以进行过渡的精确点++。暗示这可能是“旧”字句的最终用法,这可能是x对该算法的宝贵见解,有助于程序员在扫描周围的代码时对其进行帮助。当然,后缀++ 可能会让程序员注意新值的使用,这取决于实际何时需要新值,这可能是好事,也可能不是好事,因此,确定更多内容是编程“艺术”或“工艺”的一个方面在这种情况下很有帮助。

尽管许多新手可能会对某个功能感到困惑,但值得将其与长期利益进行权衡:新手不会长期呆在新手身上。


2

哇,很多答案都不是很重要的(如果我可能这么大胆的话),如果我指出了明显的答案,请原谅我-特别是鉴于您的评论未指出语义,而是明显的(我想从Stroustrup的角度来看)似乎尚未发布!:)

Postfix x++生成一个临时表达式,该临时表达式在表达式中被“向上”传递,而变量x随后递增。

前缀++x不会产生临时对象,但是会增加'x'并将结果传递给表达式。

对于那些了解操作员的人来说,这就是便利。

例如:

int x = 1;
foo(x++); // == foo(1)
// x == 2: true

x = 1;
foo(++x); // == foo(2)
// x == 2: true

当然,该示例的结果可以从其他等效(也许不太倾斜)的代码中生成。

那么为什么要有后缀运算符呢?我想这是一个持久的习语,尽管它显然造成了混乱。这是一个曾经被认为具有价值的遗留构造,尽管我不确定感知的价值对性能和便利性有多大作用。便利性并没有丧失,但我认为对可读性的了解有所增加,从而引起了对操作者目的的质疑。

我们是否需要后缀运算符?可能不会。其结果令人困惑,并为理解提供了障碍。某些优秀的编码人员肯定会立即知道在哪里可以找到它的用处,通常是在它具有“ PERL式”美感的地方。这种美的代价是可读性。

我同意显式代码在简洁性,可读性,可理解性和可维护性方面具有优势。好的设计师和管理者都希望这样。

但是,有些程序员认识到有某种美感,鼓励他们纯粹为了美观简单而使用某些东西来生成代码(例如postfix运算符),否则他们将不会想到这些代码,或者发现它们具有智力上的刺激性。尽管有一点点诚意,但有一点可以使它更加美丽,甚至更令人希望。

换句话说,有些人发现while (*dst++ = *src++);这只是一个更漂亮的解决方案,它的简单性让您叹为观止,就好像它是画布上的画笔一样。您被迫理解语言来欣赏美只会增加它的壮丽。


3
+1“有人发现while(* dst ++ = * src ++);只是成为一个更漂亮的解决方案”。绝对是 不懂汇编语言的人并不能真正理解C语言的优雅程度,但是特定的代码语句却是一颗璀璨的新星。
Tin Man'2

“我们是否需要后缀运算符?可能不需要。” -我不禁想到这里的图灵机。我们是否曾经需要过自动变量,for语句,甚至while(我们goto毕竟需要)?

@伯恩德:哈!想象一下关闭!关闭!太丑了!大声笑
Brian M. Hunt

关闭!真可笑 请给我录音带!认真地说,我的意思是我认为/功能不应该是主要的决策标准(除非您要设计,例如说Lisp)。IME我几乎专门使用(不,需要)后缀运算符,而很少需要前缀前缀。

2

让我们看一下Kernighan&Ritchie的原始理由(原始K&R第42和43页):

不寻常的方面是++和-可用作前缀或后缀。(...)在不需要任何值的情况下(..)根据喜好选择前缀或后缀。但是,在某些情况下,特别需要其中一种。

本文继续使用在索引中使用增量的一些示例,其明确目标是编写“ 更紧凑的 ”代码。因此,这些运算符背后的原因是更紧凑的代码的便利性。

这三个实施例中给出(squeeze()getline()strcat())只使用使用索引表达式中后缀。作者将代码与不使用嵌入式增量的较长版本进行了比较。这证实了重点是紧凑性。

K&R在第102页上突出显示,这些运算符与指针解引用(例如*--p*p--)结合使用。没有给出进一步的示例,但是它们再次表明好处是紧凑。

例如,当减少从末尾开始的索引或指针时,通常会使用前缀。

 int i=0; 
 while (i<10) 
     doit (i++);  // calls function for 0 to 9  
                  // i is 10 at this stage
 while (--i >=0) 
     doit (i);    // calls function from 9 to 0 

1

我将不同意这样的前提,即++pp++难以理解或不清楚。一种表示“先递增p然后读取p”,另一种表示“先读取p然后递增p”。在两种情况下,代码中的运算符正好位于代码说明中的位置,因此,如果您知道++含义,就知道结果代码的含义。

您可以使用混淆代码的任何语法,但我没有在这里看到的情况是p++/ ++p固有的混淆。


1

这是从电气工程师的观点来看的:

许多处理器具有内置的后递增和递减运算符,目的是维护后进先出(LIFO)堆栈。

它可能像:

 float stack[4096];
 int stack_pointer = 0;

 ... 

 #define push_stack(arg) stack[stack_pointer++] = arg;
 #define pop_stack(arg) arg = stack[--stack_pointer];

 ...

我不知道,但这就是我希望看到前缀和后缀增量运算符的原因。


1

为了强调Christophe关于紧凑性的观点,我想向大家展示V6中的所有代码。这是原始C编译器的一部分,来自http://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/source/c/c00.c

/*
 * Look up the identifier in symbuf in the symbol table.
 * If it hashes to the same spot as a keyword, try the keyword table
 * first.  An initial "." is ignored in the hash.
 * Return is a ptr to the symbol table entry.
 */
lookup()
{
    int ihash;
    register struct hshtab *rp;
    register char *sp, *np;

    ihash = 0;
    sp = symbuf;
    if (*sp=='.')
        sp++;
    while (sp<symbuf+ncps)
        ihash =+ *sp++;
    rp = &hshtab[ihash%hshsiz];
    if (rp->hflag&FKEYW)
        if (findkw())
            return(KEYW);
    while (*(np = rp->name)) {
        for (sp=symbuf; sp<symbuf+ncps;)
            if (*np++ != *sp++)
                goto no;
        csym = rp;
        return(NAME);
    no:
        if (++rp >= &hshtab[hshsiz])
            rp = hshtab;
    }
    if(++hshused >= hshsiz) {
        error("Symbol table overflow");
        exit(1);
    }
    rp->hclass = 0;
    rp->htype = 0;
    rp->hoffset = 0;
    rp->dimp = 0;
    rp->hflag =| xdflg;
    sp = symbuf;
    for (np=rp->name; sp<symbuf+ncps;)
        *np++ = *sp++;
    csym = rp;
    return(NAME);
}

这是在samizdat中作为一种良好的教育方式传递的代码,但是我们今天永远不会这样写。查看ifwhile条件中包装了多少副作用,反之,两个for循环如何没有增量表达式,因为这是在循环主体中完成的。看一下在绝对必要时如何仅将块包裹在括号中。

当然,部分原因是我们希望编译器今天为我们做的那种手动微优化,但是我还要提出一个假设,即当您与计算机的整个接口是单个80x25玻璃tty时,您需要代码尽可能地密集,这样您就可以同时看到更多内容。

(对所有内容使用全局缓冲区可能是不肯使用汇编语言的宿醉。)


请小心处理:“ ihash = + * sp ++;” 在某些时候,C不仅具有+ =运算符,还具有= +运算符。今天,= +是赋值运算符,后跟一元加号,它不执行任何操作,因此等效于“ ihash = * sp; sp + = 1;”。
gnasher729

@ gnasher729是的,后来有了rp->hflag =| xdflg,我相信这将是现代编译器的语法错误。恰好“ V6”恰好在发生。转换+=形式发生在V7中。(如果我正确阅读此代码,则V6编译器接受=+-参见同一文件中的symbol()。)
zwol

-1

也许您也应该考虑化妆品。

while( i++ )

可以说比“看起来更好”

while( i+=1 )

由于某种原因,后增量运算符看起来非常吸引人。它简短,甜美,并且使所有内容增加1。将其与前缀进行比较:

while( ++i )

“向后看”不是吗?您永远不会在数学上看到加号“ FIRST”,除非有人愚蠢地指出某项是肯定的(+0或类似的东西)。我们习惯于看到对象+某些东西

与评分有关吗?A,A +,A ++?我可以告诉您,起初我很被Python人operator++从他们的语言中删除了!


1
您的示例执行的操作不同。 i++返回原来的价值i,并i+=1++i返回的原始值i由于您使用的返回值来控制数据流,这个问题加1。
David Thornley

你是对的。但是我在这里只看可读性。
bobobobo

-2

从9倒数到0(含):

for (unsigned int i=10; i-- > 0;)  {
    cout << i << " ";
}

或等效的while循环。


1
怎么for (unsigned i = 9; i <= 9; --i)
fredoverflow'2
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.