C ++中最差的做法,常见错误[关闭]


35

在阅读了Linus Torvalds的这本著名的文章之后,我想知道C ++程序员的真正陷阱是什么。我明确不是在这个问题及其答案中提及错别字或不良的程序流,而是指编译器未检测到的更高级错误,这些错误不会在首次运行时导致明显的错误,完整的设计错误,在C中不可能实现的事情,但是可能由不了解其代码的全部含义的新手在C ++中完成。

我也欢迎回答指出通常不会出现的巨大性能下降。我的一位教授曾经告诉我有关我编写的LR(1)解析器生成器的示例:

您使用了太多不必要的继承和虚拟性实例。继承使设计更加复杂(由于RTTI(运行时类型推断)子系统,因此效率低下),因此仅应在有意义的地方使用它,例如,用于解析表中的操作。因为您大量使用模板,所以实际上不需要继承。”


6
您在C / C ++中可能会犯的一些更讨厌的错误,主要是由于C的继承性……阅读未定义的行为,手动内存管理等。而且,教授的建议似乎是虚假/错误的(对我而言,不是C ++专家)-模板实例化应该产生一个带有vtable的普通类来显示virtual函数,对吗?

8
您要么记错了您的教授所说的话,要么他不知道他在说什么。派生类通常不需要使用RTTI(AKA反射)来查找内容。如果他们使用的是虚拟方法,则代码可能需要对分配进行vtable查找,但这会转换为许多处理器上的一条ASM指令。由于存在缓存问题,它可以使事情放慢一定的速度,但是除了最苛刻的用例之外,您几乎不会注意到开销。有许多避免使用C ++的充分理由,但是vtable查找不是其中之一。
梅森惠勒

5
@FelixDombek:如此笼统地陈述并广泛应用,您的教授的那句话只是显示出大量的无知。当您的设计需要某种类型的运行时多态性时,使用虚拟函数通常是最佳选择。当您不需要它时,请不要使用它:例如,因为您使用派生类,就不需要所有方法都是虚拟的。
弗雷德·纽克

5
@Mason Wheeler:RTTI包含有关类型的信息,足以确定a是否dynamic_cast应该成功,以及其他一些事情,但是反射涉及的内容更多,包括能够检索有关成员属性或函数的信息,而不是目前在C ++中。
DavidRodríguez-dribeas 2011年

5
这位教授的评论颇具欺骗性,因为继承和虚拟函数对性能的影响不大。谨慎使用继承的建议是好的,但更多的是程序结构问题,而不是效率问题。继承,特别是对于受保护的成员,继承与您将要获得的耦合密切相关,如果不需要,则不应使用它。
David Thornley

Answers:


69

托瓦尔兹在这里开玩笑。


好吧,他为什么要开怀大笑:

首先,他的怒吼实际上不是什么怒吼。这里的实际内容很少。它真正出名或什至受到尊重的唯一原因是它是由Linux God制造的。他的主要论点是C ++很烂,他喜欢惹恼C ++人士。当然,完全没有理由对此做出回应,并且任何认为这是合理论点的人无论如何都无法讨论。

关于他最客观的观点可能闪闪发光:

  • STL和Boost完全是<-随便什么。你是一个白痴。
  • STL和Boost导致无限量的疼痛,这简直是荒谬的。显然他故意夸大其词,但是他在这里的真正说法是什么?我不知道。当您在Spirit或其他方面导致编译器呕吐时,要找出问题要比平时困难得多,但是要弄清楚,除了调试由于滥用诸如void *之类的C构造而导致的UB之外,要找到或多或少的困难。
  • C ++鼓励的抽象模型效率低下。<-喜欢什么?他从不扩张,从不提供自己的意思的任何例子,他只是这么说。BFD。由于我无法确定他指的是什么,试图“反驳”该声明毫无意义。这是C偏执狂的普遍口头禅,但并没有使它变得更加可理解或可理解。
  • 正确使用C ++意味着您将自己局限于C方面。<-实际上,那里的WORSE C ++代码可以做到这一点,所以我仍然不知道他在谈论WTF。

基本上,Torvalds是在胡说八道。关于任何事情都没有可理解的论点。期望对这种胡说八道的严重反驳只是愚蠢的。我被告知要“反驳”我希望在他说的地方扩展的东西。如果真的,老实看一下托瓦尔兹所说的话,您会发现他实际上没有说什么。

仅仅因为上帝说这并不意味着它比任何随机博佐说的那样有意义或应该被更认真地对待。说实话,上帝只是另一个随机的博世。


回答实际问题:

可能最糟糕,也是最常见的不良C ++做法是将其像C一样对待。继续使用C API函数,例如printf,gets(在C语言中也被认为是不良的),strtok等……不仅无法利用所提供的功能通过更严格的类型系统,在尝试与“实际” C ++代码进行交互时,它们不可避免地导致进一步的复杂化。因此,基本上,与Torvalds建议的相反。

学会利用STL和Boost来进一步检测错误的编译时间,并通过其他通用方式使您的生活更轻松(例如,boost令牌生成器既是类型安全的又是更好的接口)。确实,您必须学习如何读取模板错误,这虽然一开始就令人生畏,但是(以我的经验)坦率地说,这比尝试调试运行时会产生未定义行为的东西要容易得多,而C api相当容易做到。

并不是说C不够好。我当然更喜欢C ++。C程序员更喜欢C。有权衡取舍和主观喜欢。还有很多错误信息和FUD浮动。我想说的是,关于C ++的信息还有更多的FUD和错误信息,但我对此有偏见。例如,C ++所谓的“膨胀”和“性能”问题在大多数情况下实际上并不是主要问题,并且肯定超出了现实的范围。

至于您的教授所指的问题,这些并不是C ++独有的。在OOP(和通用编程)中,您想要组合而不是继承。继承是所有OO语言中都存在的最强耦合关系。C ++增加了一种更强大的友谊。多态继承应用于表示抽象和“ is-a”关系,决不能用于重用。这是您在C ++中可能犯的第二大错误,这是一个很大的错误,但远非该语言独有。您也可以在C#或Java中创建过于复杂的继承关系,它们将具有完全相同的问题。


1
具有讽刺意味的是,直到2007年以后,git只能运行可移植的Linux版本。好吧,任何与Unix相似的系统。再说一遍,考虑到导致git创建的情况,我当然不反对他。
克里斯·K

9
Linus很难找到想要为他工作的优秀C ++程序员。想知道为什么?我认为这只是鸡和蛋的问题。
Bo Persson

19

我一直以为C ++的危险会因缺乏Classes程序员的C经验而大大夸大。

是的,C ++比Java之类的东西更难掌握,但是如果您使用现代技术编程,则编写健壮的程序非常容易。老实说我用C ++编程的时间没有像使用Java这样的语言时困难得多,而且当我用其他语言进行设计时,我经常发现自己缺少某些C ++抽象性,例如模板和RAII。

就是说,即使使用C ++进行了多年编程,我也会时不时地犯一个真正愚蠢的错误,这在高级语言中是不可能的。C ++中的一个常见陷阱是忽略对象生存期:在Java和C#中,您通常不必关心对象生存期*,因为所有对象都存在于堆中,并且由神奇的垃圾收集器为您管理它们。

现在,在现代C ++中,通常您也不需要太在意对象的生存时间。您拥有析构函数和智能指针,它们可以为您管理对象的生存期。99%的时间,这效果很好。但是,时不时地,您会被一个悬空的指针(或引用)所困扰。例如,最近我有一个对象(让我们称它为Foo),该对象包含另一个对象的内部参考变量(称为它Bar)。有一次,我愚蠢地安排了事情,使之Bar超出了范围Foo,但Foo析构函数最终调用的成员函数Bar。不用说,事情进展得并不顺利。

现在,我不能为此怪罪C ++。这是我自己的糟糕设计,但关键是这种事情不会在更高级别的托管语言中发生。即使使用智能指针等,您有时仍需要了解对象的生存时间。


*如果要管理的资源是内存,那就是。


8
从未真正关心Java和C#中的对象生存期吗?他们的GC负责记忆,但这对我来说只是RAII的一小部分。例如,看看这些语言具有的各种“一次性”界面。
弗雷德·纽克

不必关心对象的生命周期是除了不方便设计的I / O库的Java中罕见。
dan04 2011年

我想解决您悬而未决的参考问题。我在我的博客上开始了有关我将要解决的方向(指针承诺)的讨论。基本上,我认为该语言可以使用更多智能指针。如果您有兴趣,请参加该讨论。没人能做到这一点……但是,如果您希望看到它解决了……我实际上有超过10%的时间遇到​​了这个问题。
爱德华·斯特朗奇

13

与语言相比,代码中的差异通常与程序员更相关。特别是,一个好的C ++程序员和一个C程序员都将得出类似的好(即使是不同的)解决方案。现在,C是一种更简单的语言(作为一种语言),这意味着对代码实际作用的抽象更少,可见性更高。

他的部分言论(他因对C ++的言论而闻名)是基于这样一个事实,即越来越多的人会使用C ++,并且在编写代码时实际上并不了解某些抽象隐藏了什么并做出错误的假设。


3
迭代std::vector<bool>更改每个值的成本是多少?for ( std::vector<bool>::iterator it = v.begin(), end = v.end(); it != end; ++it ) { *it = !*it; }?什么是抽象出来的*it = !*it;
DavidRodríguez-dribeas 2011年

2
尽管选择被广泛批评为错误的特定语言可憎性是不公平的……
Fred Nurk 2011年

2
@Fred Nurk:这std::vector<bool>是一个众所周知的错误,但这是正在讨论的一个很好的例子:抽象是好的,但是您必须注意它们隐藏的内容。用户代码中可能会发生同样的事情。首先,我在C ++和Java中都看到过使用异常来执行流控制的人,并且看起来像嵌套函数调用的代码实际上是纾困异常启动器:void endOperation();实现为throw EndOperation;。一个好的程序员会避免那些令人惊讶的构造,但事实是您可以找到它们。
DavidRodríguez-dribeas 2011年

5
Torvalds的要点之一是:他可以通过选择C而不是C ++来驱赶初学者(似乎有更多的C ++初学者),而C ++越复杂,学习曲线就越陡峭,在遇到极端情况时跳闸的可能性就越大。 。
DavidRodríguez-dribeas 2011年

2
+1,这正是Linus抱怨的内容。他被证明是反C ++的,但事实并非如此。他是唯一的反C ++程序员。
greyfade 2011年

13

try/catch块的过度使用。

File file("some.txt");
try
{
  /**/

  file.close();
}
catch(std::exception const& e)
{
  file.close();
}

这通常源于Java之类的语言,人们会争辩说C ++缺少finalize子句。

但是此代码存在两个问题:

  • 需要在file之前构建try/catch,因为您实际上无法创建close一个不存在的文件catch。这会导致“范围泄漏”,在file关闭后可见。您可以添加一个块,但是...:/
  • 如果有人到来并returntry范围的中间添加a ,则该文件不会关闭(这就是为什么人们对缺少finalize子句感到bit 昧)

但是,在C ++中,我们有以下更有效的方法来处理此问题:

  • Java的 finalize
  • C#的 using
  • 去吧 defer

我们拥有RAII,其真正有趣的属性最好概括为SBRM(范围绑定资源管理)。

通过精心设计类,以便其析构函数清除其拥有的资源,我们不会对每个用户都承担管理资源的责任!

这是功能,我在任何其他语言错过,而且可能是最被遗忘的人。

事实是try/catch,除了顶层以外,几乎不需要在C ++中编写块,以免在不进行日志记录的情况下终止。


1
我不认为它的Java的多,因为它的C.(您可以直接替代的影响力fopenfclose在这里。)RAII是“正确”的方法,在这里做的事情,但它的不方便谁想要使用C库由C人们++ 。
dan04 2011年

对于此类回答,提供正确解决方案的示例将是适当的。
ClausJørgensen2014年

@ClausJørgensen:好吧,不幸的是,该解决方案并不是真正的“花哨”,因为它涉及公正File file("some.txt");,就是这样(不open,不close,不try...)
Matthieu M.

D也有RAII
Demi

@Demetri:我对D不太熟悉,您能解释一下RAII如何与Garbage Collection交互吗?我知道在Python中您可以编写“ deinit”方法,但是文档警告说,在循环引用的情况下,某些对象将看不到它们的deinit方法被调用。
Matthieu M. 2014年

9

符合您的条件的一个常见错误是不了解在处理类中分配的内存时复制构造函数的工作方式。我已经失去了修复崩溃或内存泄漏所花费的时间,因为“菜鸟”将其对象放入映射或向量中,并且未正确编写副本构造函数和析构函数。

不幸的是,C ++充满了这样的“隐藏”陷阱。但是抱怨它就像抱怨您去了法国,不明白人们在说什么。如果您要去那里,请学习语言。


1
我认为C ++的问题是很容易陷入困境。当然,周围有优秀的C ++程序员,有很多用C ++编写的优秀软件。但是要成为一名优秀的C ++开发人员非常困难。Scott Meyers的“ Efficient C ++”系列显示了该语言有多少精妙之处。
Marco Mustapic

我同意。不过,部分问题是,很多(大多数)C ++程序员认为他们清楚地知道自己在做什么,而他们显然不知道自己在做什么。您是说“有效的C ++”吗?
亨利

至少对于C ++ 0x中隐式生成复制/移动操作的新的限制性规则,这种情况会变得更好。在许多违反三种规则的情况下,将不赞成使用隐式生成复制操作,并应发出警告。
sellibitze 2011年

6

C ++ 允许使用多种功能和编程样式,但这并不意味着这些实际上是使用C ++的好方法。实际上,错误地使用C ++非常容易。

必须正确地学习和理解它,只是边做边学(或像使用其他语言一样使用它)会导致效率低下和容易出错的代码。


4

好吧...对于初学者,您可以阅读C ++ FAQ Lite

然后,几个人建立了有关C ++复杂性的书籍,从而建立了自己的职业生涯:

分别是赫伯·萨特斯科特·迈耶斯

至于托瓦尔兹(Torvalds)缺乏实质的怒吼……是对人们的严肃对待:没有其他语言在处理这种语言的细微差别上有那么多的墨迹。您的Python,Ruby和Java书籍都专注于编写应用程序,而C ++书籍则专注于愚蠢的语言功能/技巧/陷阱。


1
嗯... javapuzzlers.comjimbrooks.org/web/ python / #Pitfalls。我想说加速C ++(一个例子)集中多少更多关于如何比这些写代码做...
杰里棺材

1
您已经提出了一些资源示例,这些示例指出了各自语言中的极端情况;看起来很奇怪的东西,您不太确定它们的工作方式(尽管python列表中的内容很接近)... C ++整个行业都在指出看起来完全有效的事物,它们的行为方式超出您的预期。
红尘

3

太多的模板起初可能不会导致错误。但是,随着时间的流逝,人们将需要修改该代码,并且他们将很难理解巨大的模板。那就是错误进入的时候-误解会导致“它编译并运行”注释,这通常会导致几乎但不是很正确的代码。

通常,如果我看到自己在做一个三层的深层通用模板,我会停下来想一想如何将其简化为一个模板。通常,通过提取函数或类来解决问题。


8
面对不断变化的需求而维护复杂的代码总是会导致错误,而无需付出很多努力,那里的模板没有什么特别的。
弗雷德·纽克

2

警告:这几乎不像批评“用户不知道”链接到他的回答中的谈话那么多。

他的第一个主要观点是(据说)“不断变化的标准”。实际上,他给出的所有示例都与标准建立之前的 C ++更改有关。自1998年(第一个C ++标准最终定稿以来)以来,对该语言的更改已微乎其微-实际上,许多人认为真正的问题是应该进行更多更改。我可以肯定地说,所有符合原始C ++标准的代码仍符合当前标准。虽然这有点不太确定,除非有快速(和相当意外)的变化同样会与即将推出的C ++标准几乎也是如此(理论上,所使用的所有代码export会破裂,但实际上不存在;从实际的角度来看,这不是问题。我可以想到其他几种语言,操作系统(或与计算机相关的任何其他事物)也可以提出这样的主张。

然后,他进入“不断变化的风格”。再说一次,他的大部分观点都是胡说八道。他试图将自己for (int i=0; i<n;i++)形容为“老破烂”和for (int i(0); i!=n;++i)“新热点”。现实是,尽管对于某些类型而言,这样的更改是有意义的,但对于而言int,它们没有什么区别-即使您可以获得某些东西,也很少需要编写好的或正确的代码。即使在最好的情况下,他也正在用积雪冲天。

他的下一个主张是C ++正在“朝错误的方向进行优化”-特别是,尽管他承认使用好的库很容易,但他声称C ++“几乎使编写好的库变得不可能”。在这里,我相信这是他最根本的错误之一。实际上,为几乎所有语言编写好的库都是极其困难的。至少,编写一个好的库需要很好地理解某个问题领域,以使您的代码可以在该领域中(或与之相关)的多种可能的应用程序工作。C ++ 真正所做的大部分工作都是“提高标准”-在看到一个库可以有多好的表现之后,人们很少愿意回到编写否则会产生的麻烦。真正好的程序员编写了很多库,然后“其他人”可以使用(很容易,他承认)。这确实是“不是错误,而是功能”的情况。

我不会尝试依次击中每个要点(需要花费页面),而是直接跳到他的结束点。他引用Bjarne的话说:“可以使用整个程序优化来消除未使用的虚函数表和RTTI数据。这种分析特别适用于不使用动态链接的较小程序。”

他通过提出“这是一个非常困难的问题” 的无根据的主张来批评这一观点,甚至将其与停顿问题进行比较。实际上,这没什么大不了的-实际上,Zortech C ++附带的链接程序(几乎是1980年代用于MS-DOS 的第一个 C ++编译器)就是这样做的。确实很难确定已消除了所有可能冗长的数据,但为了做一个相当公平的工作仍然是完全合理的。

但是,无论如何,更重要的一点是,在任何情况下,这对于大多数程序员都是完全不相关的。正如我们中那些已经分解了很多代码的人所知道的那样,除非您完全不编写任何汇编语言而编写汇编语言,否则您的可执行文件几乎肯定包含了相当数量的“东西”(在典型情况下,包括代码和数据)可能甚至不知道,更不用说实际使用了。对于大多数人而言,在大多数情况下,这无关紧要-除非您是为最小的嵌入式系统开发的,否则额外的存储消耗根本就没有关系。

最后,的确,这只蚂蚁确实比Linus的愚蠢多了一些东西,但这确实给了它该死的淡淡的赞美。


1

作为由于不可避免的情况不得不使用C ++进行编码的C程序员,这是我的经验。我很少使用C ++,并且大多数情况下还是坚持使用C。主要原因是因为我不太了解C ++。我/没有一位导师向我展示C ++的复杂性以及如何在其中编写良好的代码。如果没有非常好的C ++代码的指导,用C ++编写好的代码将非常困难。恕我直言,这是C ++的最大缺点,因为很难获得愿意扶持初学者的优秀C ++编码人员。

我见过的一些性能下降通常是由于STL的神奇内存分配(是的,您可以更改分配器,但是当他从C ++开始时,谁会这样做呢?)。您通常会听到C ++专家的论点,即向量和数组提供类似的性能,因为向量在内部使用数组,并且抽象非常有效。我发现对于向量访问和修改现有值在实践中是正确的。但是对于添加向量的新输入,构造和破坏而言并非如此。gprof显示,应用程序的累积时间有25%花费在向量构造函数,析构函数,内存(用于重新放置整个向量以添加新元素)和其他重载向量运算符(例如++)上。

在同一应用程序中,某物的向量用于表示某物。无需随机访问大东西中的小东西。仍然使用向量代替列表。使用向量的原因?因为原始编码人员熟悉数组(例如向量)的语法,而不太熟悉列表所需的迭代器(是的,他来自C背景)。继续证明,正确使用C ++需要专家的大量指导。C提供的很少的基本构造几乎没有任何抽象,因此您可以比C ++容易得多。



0

STL和boost在源代码级别是可移植的。我想Linus谈论的是C ++缺少ABI(应用程序二进制接口)。因此,您需要使用相同的编译器版本和相同的开关来编译与之链接的所有库,否则,您将只能使用dll边界的C ABI。我也发现这很昧,但是除非您制作第3方库,否则您应该能够控制您的构建环境。我发现将自己局限于C ABI是不值得的。能够将字符串,向量和智能指针从一个dll传递到另一个dll的便利,值得在升级编译器或更改编译器开关时重建所有库的麻烦。我遵循的黄金法则是:

-继承重用接口,而不是实现

-优先考虑聚合而不是继承

-在可能的情况下,首选自由函数而不是成员方法

-始终使用RAII惯用语来使代码具有强烈的异常安全性。避免尝试捕获。

-使用智能指针,避免使用裸露(无用)的指针

-优先使用值语义来引用语义

-不要重新发明轮子,使用stl和boost

-使用Pimpl惯用法来隐藏私有和/或提供编译器防火墙


-6

;至少在某些版本的VC中,不要在分句声明的末尾添加最后一个。


4
对于初学者来说,这可能是一个很常见的错误(对于仍在学习基本语法的人来说几乎是什么),但是是否有许多人自称能干却仍然发现此错误值得注意?
弗雷德·纽克

1
编写它的原因仅仅是因为编译器给您一个错误,该错误与缺少分号无关。
Marco Mustapic 2011年

2
是的,完全相同的错误是从C编译器得到的。
Mircea Chirea 2011年
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.