it-swarm.cn

C ++最差的做法,常见错误

在读完 Linus Torvalds的这个著名的咆哮 之后,我想知道C++程序员的真正陷阱是什么。我明确不是指 此问题及其答案 中所处理的错别字或错误的程序流,而是指编译器未检测到的更高级错误,并且不会导致明显的错误。第一次运行时,会出现完整的设计错误,这在C语言中是不可能的,但是可能由不了解其代码的全部含义的新手在C++中完成。

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

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

35
Felix Dombek

托瓦尔兹在这里开玩笑。


好吧,为什么他这么说傻话:

首先,他的怒吼实际上不是什么怒吼。这里的实际内容很少。它真正出名或什至受到尊重的唯一原因是它是由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=)创建过于复杂的继承关系,它们也会遇到完全相同的问题。

69
Edward Strange

我一直以为C++的危险因没有经验的C类程序员而被大大夸大了。

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

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

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

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


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

19
Charles Salvia

_try/catch_块的过度使用。

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

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

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

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

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

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

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

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

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

这是我在任何其他语言中都缺少的the功能,可能是最被遗忘的功能。

事实是,几乎不需要在C++中甚至在顶层编写_try/catch_块,以免在不进行日志记录的情况下终止。

13
Matthieu M.

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

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

13

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

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

9
Henry

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

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

6
Dario

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

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

Herb SutterScott Meyers 即.

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

4
red-dirt

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

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

3
Michael K

警告:这几乎不是批评“用户未知”链接到他的回答中的谈话的答案。

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

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

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

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

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

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

最后,的确,这只蚂蚁确实比Linus的愚蠢多了一些东西-但这恰恰给了它以应有的淡淡赞美。

2
Jerry Coffin

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

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

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

1
aufather

尽管我喜欢莱纳斯·索瓦尔兹(Linus Thorvalds),但是这只蚂蚁没有实质意义-只是一只蚂蚁。

如果您想看一眼真实的声音,那就是一个:“为什么C++对环境有害,会导致全球变暖并杀死幼犬” http://chaosradio.ccc.de/camp2007_m4v_1951.html 其他资料: http://www.fefe.de/c++/

有趣的演讲,恕我直言

0
user unknown

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

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

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

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

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

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

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

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

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

0
user16642