与C ++相比,C#是否能给您“更少的绳索来吊死自己”?[关闭]


14

乔尔·斯波斯基(Joel Spolsky)将C ++定义为“足以使自己吊死的绳索”。实际上,他在总结Scott Meyers的“ Effective C ++”:

这是一本书,基本上说,C ++足以将自己吊死,然后再延伸几英里,然后再变成一些伪装成M&M的自杀药。

我没有这本书的副本,但是有迹象表明,这本书的大部分内容与管理内存的陷阱有关,这似乎在C#中显得毫无意义,因为运行时会为您管理这些问题。

这是我的问题:

  1. C#是否可以避免仅通过精心编程才能避免的C ++陷阱?如果是这样,在何种程度上以及如何避免?
  2. 新C#程序员应该注意C#中是否存在新的,不同的陷阱?如果是这样,为什么不能通过C#设计来避免它们?

10
常见问题Your questions should be reasonably scoped. If you can imagine an entire book that answers your question, you’re asking too much.。我相信这符合这样一个问题……
Oded

@Oded您是指字符受限的标题问题吗?还是我的帖子正文中3个或更精确的问题?
alx9r 2012年

3
坦率地说-标题和每个 “更精确的问题”。
2012年

3
我已经开始对此问题进行元讨论
2012年

1
关于您现在删除的第三个问题,比尔·瓦格纳的有效C#系列(现在为3本书)教会了我关于C#编程的更多知识,这比我在该主题上阅读的任何内容都多。Martins对EC#的评论是正确的,因为它永远不能直接替代有效C ++,但是他认为应该是错误的。一旦您不再需要担心容易犯的错误,就必须继续处理更困难的错误。
Mark Booth

Answers:


33

C ++和C#之间的根本区别在于未定义的行为

它与执行手动内存管理无关在两种情况下,这都是一个已解决的问题。

C / C ++:

在C ++中,当您犯错时,结果是不确定的。
或者,如果您尝试对系统进行某些类型的假设(例如,有符号整数溢出),则可能是程序未定义。

也许阅读有关不确定行为的这三部分系列文章。

就是C ++如此之快的原因-编译器不必担心出现问题时会发生什么,因此可以避免检查正确性。

C#,Java等

在C#中,可以确保许多错误会作为异常而冒出来,并且可以确保您对底层系统有更多的了解。
这是一个根本性的障碍,使得C#一样快,C ++,但它也是一个根本性的障碍使得C ++安全,它使C#更易于使用和调试。

其他一切都只是肉汁。


所有未定义的内容实际上都是由实现定义的,因此,如果您在Visual Studio中溢出了一个无符号整数,那么如果打开了正确的编译器标志,则会出现异常。现在我知道这就是您在说的,但这不是未定义的行为,只是人们通常不会检查它。(与像operator ++这样的真正未定义的行为相同,每个编译器均定义良好)。您可以对C#说同样的话,只有一种实现-如果在Mono中运行,则有很多“未定义的行为”-例如。bugzilla.xamarin.com/show_bug.cgi?id=310
gbjbaanb 2012年

1
它是真正定义的还是仅由Windows当前版本上的当前.net实现定义的?如果将g ++定义为c ++,则即使c ++未定义的行为也已完全定义。
马丁·贝克特

6
溢出的无符号整数根本不是UB。它溢出 UB的有符号整数。
DeadMG 2012年

6
@gbjbaanb:就像DeadMG所说的那样-有符号整数溢出不确定的。它不是实现定义的。这些短语在C ++标准中具有特定的含义,并且不是同一回事。不要犯那个错误。
user541686

1
@CharlesSalvia:嗯,“ C ++使C#变得比C#更容易利用它吗?” C ++可以提供什么样的控件来控制您在C#中无法拥有的内存?
user541686

12

C#是否可以避免仅通过精心编程才能避免的C ++陷阱?如果是这样,在何种程度上以及如何避免?

大多数情况下,有些却没有。当然,它带来了一些新的东西。

  1. 未定义的行为 -C ++的最大缺陷是,有很多未定义的语言。当您执行这些操作时,编译器实际上可以使Universe崩溃,这没关系。自然,这是罕见的,但是您的程序在一台机器上正常工作,而没有充分的理由不在另一台机器上正常工作,这很普遍的。或更糟的是,巧妙地表现出不同。C#在其规范中有一些未定义行为的案例,但是这种情况很少见,并且在不常使用的语言领域中也很少见。每次声明时,C ++都有可能发生未定义的行为。

  2. 内存泄漏 -对于现代C ++而言,这并不是什么大问题,但对于初学者而言,在其生命周期的大约一半时间内,C ++使其非常容易泄漏内存。有效的C ++正是围绕实践的发展而消除了这种担忧。也就是说,C#仍然可以泄漏内存。人们遇到的最常见情况是事件捕获。如果您有一个对象,并将其方法之一作为事件的处理程序,则该事件的所有者需要进行GC处理,以使该对象死亡。大多数初学者都没有意识到事件处理程序可作为参考。不处理可能泄漏内存的一次性资源还存在一些问题,但是这些问题不像Pre-Effective C ++中的指针那样普遍。

  3. 编译 -C ++具有延迟的编译模型。这导致了许多技巧,可以很好地使用它,并缩短编译时间。

  4. 字符串 -现代C ++对此有所改善,但char*在2000年之前,它承担了约95%的所有安全漏洞。对于有经验的程序员,他们将专注于std::string,但这仍然是要避免的问题,并且是较旧/较差的库中的问题。 。那就祈祷您不需要unicode支持。

确实,这就是冰山一角。主要问题是C ++对于初学者来说是一种非常糟糕的语言。这是相当不一致的,并且通过更改惯用语已经解决了许多古老的,非常非常糟糕的陷阱。问题在于,初学者然后需要从有效C ++之类的语言中学习习语。C#完全消除了许多这些问题,并使得其余的问题不再困扰您,直到您进一步学习为止。

新C#程序员应该注意C#中是否存在新的,不同的陷阱?如果是这样,为什么C#的设计无法避免它们?

我提到了事件“内存泄漏”的问题。这不是语言问题,而是程序员期望语言无法做到的。

另一个是技术上不能保证C#对象的终结器可以由运行时运行。通常这并不重要,但是确实会导致某些事情的设计与您预期的有所不同。

我见过程序员遇到的另一个半陷阱是匿名函数的捕获语义。捕获变量时,将捕获变量。例:

List<Action> actions = new List<Action>();
for(int x = 0; x < 10; ++x ){
    actions.Add(() => Console.WriteLine(x));
}

foreach(var action in actions){
    action();
}

不做天真的想法。这将打印1010次​​。

我敢肯定还有很多其他的我会忘记,但是主要的问题是它们的普及程度较低。


4
内存泄漏已成为历史,而且也是如此char*。更不用说您仍然可以使用C#泄漏内存。
DeadMG

2
将模板称为“规范化字符串粘贴”有点多。模板确实是C ++的最佳功能之一。
查尔斯·索尔维亚

2
@CharlesSalvia当然,它们是C ++ 真正特色。是的,这可能是编译影响的过度简化。但是它们确实会不成比例地影响编译时间和输出大小,尤其是如果您不小心的话。
Telastyn 2012年

2
@deadMG当然可以,尽管我会认为C ++中使用/需要的许多模板元编程技巧都可以通过不同的机制更好地实现。
Telastyn 2012年

2
@Telastyn type_traits的全部要点是在编译时获取类型信息,因此您可以使用此信息以特定的方式使用以下方法来做一些事情,例如专门化模板或重载函数enable_if
Charles Salvia 2012年

10

在我看来,C ++的危险被夸大了。

根本的危险是:尽管C#允许您使用unsafe关键字执行“不安全”的指针操作,但C ++(主要是C的超集)将使您可以在需要时使用指针。除了使用指针(与C相同)固有的常见危险(例如内存泄漏,缓冲区溢出,指针悬空等)外,C ++还为您提供了新的方法来严重破坏问题。

可以这么说,乔尔·斯波斯基(Joel Spolsky)所说的 “额外的绳索” 基本上可以归结为一件事:编写内部管理自己的内存的类,也称为“ 3规则 ”(现在可以称为“规则”) C ++ 11中的4或5的规则)。这意味着,如果您想编写一个内部管理自己的内存分配的类,则必须知道您在做什么,否则程序可能会崩溃。您必须仔细创建一个构造函数,复制构造函数,析构函数和赋值运算符,这很容易出错,通常会在运行时导致异常崩溃。

但是,在实际的日常C ++编程中,编写一个管理自己的内存的类确实非常罕见,因此说C ++程序员总是需要“小心”以避免这些陷阱是一种误导。通常,您只会做更多类似的事情:

class Foo
{
    public:

    Foo(const std::string& s) 
        : m_first_name(s)
    { }

    private:

    std::string m_first_name;
};

该类看起来非常类似于您在Java或C#中所做的事情-它不需要显式的内存管理(因为该库类std::string会自动处理所有事务),并且自默认以来根本不需要“ 3规则”的东西复制构造函数和赋值运算符就可以了。

只有当您尝试执行以下操作时:

class Foo
{
    public:

    Foo(const char* s)
    { 
        std::size_t len = std::strlen(s);
        m_name = new char[len + 1];
        std::strcpy(m_name, s);
    }

    Foo(const Foo& f); // must implement proper copy constructor

    Foo& operator = (const Foo& f); // must implement proper assignment operator

    ~Foo(); // must free resource in destructor

    private:

    char* m_name;
};

在这种情况下,对于新手来说,正确地分配分配,析构函数和复制构造函数可能很棘手。但是在大多数情况下,没有理由这样做。C ++通过使用像std::string和这样的库类,很容易避免99%的时间进行手动内存管理std::vector

另一个相关的问题是以不考虑引发异常的可能性的方式手动管理内存。喜欢:

char* s = new char[100];
some_function_which_may_throw();
/* ... */
delete[] s;

如果some_function_which_may_throw()确实确实引发异常,s则将导致内存泄漏,因为分配给它的内存将永远无法回收。但是,实际上,由于“ 3规则”不再是一个太大的问题,在实践中这不再是一个问题。用原始指针实际管理自己的内存非常罕见(通常是不必要的)。为避免上述问题,您所需要做的就是使用std::stringstd::vector,并且在抛出异常之后在堆栈展开期间会自动调用析构函数。

因此,这里的总主题是许多不是从C继承的C ++功能,例如自动初始化/销毁,复制构造函数和异常,迫使程序员在C ++中进行手动内存管理时要格外小心。但是同样,这只是一个问题,如果您打算首先进行手动内存管理,那么在拥有标准容器和智能指针的情况下,几乎不再需要手动进行内存管理。

因此,我认为,尽管C ++为您提供了很多额外的好处,但几乎没有必要使用它来吊死自己,而Joel所谈论的陷阱在现代C ++中很容易避免。


现在是C ++ 03中的三个规则,现在是C ++ 11中的四个规则。
DeadMG

1
对于复制构造函数,移动构造函数,复制分配,移动分配和析构函数,可以将其称为“ 5的规则”。但是,移动语义并不总是仅对于适当的资源管理而言是必需的。
查尔斯·

您不需要单独的移动和复制分配。复制和交换习惯可以将两个运算符合而为一。
DeadMG

2
它回答了这个问题Does C# avoid pitfalls that are avoided in C++ only by careful programming?。答案是“不是真的,因为很容易避免Joel在现代C ++中谈论的陷阱”
Charles Salvia

1
IMO,虽然像C#或Java这样的高级语言为您提供了内存管理和其他本应为您提供帮助的内容,但它并不总是按预期进行。您仍然需要照顾好代码设计,因此不会留下内存泄漏(这与在C ++中调用的不完全相同)。根据我的经验,我发现用C ++管理内存甚至更容易,因为您知道析构函数将被调用,并且在大多数情况下它们会进行清理。毕竟,C ++在设计不允许有效内存管理的情况下具有智能指针。C ++很棒,但不是傻瓜。
Pijusn 2012年

3

我真的不会同意。可能比1985年存在的C ++的陷阱少。

C#是否可以避免仅通过精心编程才能避免的C ++陷阱?如果是这样,在何种程度上以及如何避免?

并不是的。由于C ++ 11 并被标准化unique_ptrshared_ptr因此诸如“三项规则”之类的规则在C ++ 11中失去了巨大的意义。以隐约明智的方式使用Standard类不是“仔细的编码”,而是“基本的编码”。另外,仍然足够愚蠢,无知或两者兼而为之,无法执行诸如手动内存管理之类的工作的C ++人群的比例比以前要低得多。现实情况是,希望演示此类规则的讲师不得不花费数周的时间来查找仍适用的示例,因为标准类实际上涵盖了所有可以想象的用例。许多有效的C ++技术已采用相同的方式-渡渡鸟的方式。许多其他工具并不是真的特定于C ++。让我看看。跳过第一个项目,接下来的十个项目是:

  1. 不要像C那样编写C ++。这实际上只是常识。
  2. 限制您的接口并使用封装。面向对象
  3. 两阶段初始化代码编写器应该被淘汰。面向对象
  4. 了解什么是值语义。这真的是C ++特有的吗?
  5. 再次以稍微不同的方式限制您的界面。面向对象
  6. 虚拟析构函数。是的 这可能仍然有效-有点。finaloverride帮助改善了这款特殊游戏。创建您的析构函数,override并且如果您从没有成为其析构函数的人那里继承,则可以保证编译器会产生不错的错误virtualfinal没有虚拟析构函数的情况下,使您的类没有任何不良的清理可能会意外地从中继承。
  7. 如果清除功能失败,则会发生不好的事情。这并不是真正针对C ++的-您可以在Java和C#上看到相同的建议-以及几乎每种语言。具有可能会失败的清理功能显然是很糟糕的,并且与C ++甚至OOP无关。
  8. 注意构造函数的顺序如何影响虚函数。可笑的是,在Java中(无论是现在的还是过去的),它都只会错误地调用Derived类的函数,这甚至比C ++的行为还差。无论如何,此问题并非特定于C ++。
  9. 操作员重载应符合人们的预期。不太具体。地狱,几乎没有操作员重载,这同样适用于任何函数-不要给它起一个名字,而是让它做一些完全不直观的事情。
  10. 现在,实际上这被认为是不好的做法。所有严格例外安全的赋值运算符都可以很好地处理自赋值,并且自赋值实际上是一个逻辑程序错误,而检查自赋值只是不值得的性能成本。

显然,我不会遍历每一个有效的C ++项目,但是其中大多数只是将基本概念应用于C ++。您会在任何值类型的面向对象的重载运算符语言中找到相同的建议。虚拟析构函数是唯一一个C ++陷阱,并且仍然有效-尽管可以说,final对于C ++ 11类,它并不像以前那样有效。请记住,当应用OOP和C ++的特定功能的想法还很新时,就写了Effective C ++。这些项目几乎与C ++的陷阱无关,而与如何应对C的变化以及如何正确使用OOP有关。

编辑:C ++的陷阱不包括的陷阱malloc。我的意思是,首先,您可以在C代码中找到的每个陷阱都可以在不安全的C#代码中找到,因此并不是特别相关;其次,仅因为标准将其定义为互操作,并不意味着将其视为C ++码。该标准也进行了定义goto,但是如果您要使用它编写一大堆意大利面条,我会认为这是您的问题,而不是语言的问题。“仔细编码”和“遵循语言的基本习语”之间有很大的区别。

新C#程序员应该注意C#中是否存在新的,不同的陷阱?如果是这样,为什么C#的设计无法避免它们?

using很烂。确实如此。而且我不知道为什么没有做更好的事情。而且,Base[] = Derived[]几乎所有对Object的使用都是存在的,因为最初的设计人员没有注意到模板在C ++中的巨大成功,并决定“让所有东西都继承自所有东西,而失去所有类型安全性”是更明智的选择。 。我还相信,在与代表的比赛条件等方面,您可以找到一些令人讨厌的惊喜,以及其他类似的乐趣。然后还有其他一般性的东西,例如与模板相比,泛型如何令人讨厌地吸吮,将所有东西真正强制地强制放置在中class,等等。


5
受过良好教育的用户群或新构建并没有真正减少绳索。它们只是变通办法,因此更少的人最终陷入饥饿。尽管这都是对有效C ++及其在语言演变中的上下文的很好的评论。
Telastyn 2012年

2
不。这是关于有效C ++中的许多项目如何等同地应用于任何值类型的面向对象的语言的概念的。教育用户群来编写实际的C ++而不是C语言,肯定会减少C ++给您的印象。另外,我希望新的语言结构逐渐消失。关于C ++标准定义的malloc含义并不意味着您应该这样做,不仅仅因为您可以goto像母狗一样who 子,还可以挂在自己身上。
DeadMG 2012年

2
使用C ++的C部分与使用unsafeC#编写所有代码没有什么不同,这同样糟糕。如果您愿意,我也可以列出像C一样编码C#的所有陷阱。
DeadMG,2012年

@DeadMG:所以实际上问题应该是“只要他是C程序员,C ++程序员就有足够的绳子来吊死自己”
gbjbaanb 2012年

“此外,仍然足够愚蠢,不了解信息或两者都不能做手动内存管理之类的C ++人群的比例比以前要低得多。” 需要引用。
2012年

3

C#是否可以避免仅通过精心编程才能避免的C ++陷阱?如果是这样,在何种程度上以及如何避免?

C#具有以下优点:

  • 不与C向后兼容,因此避免了语法上很方便但现在被认为是不良样式的“邪恶”语言功能(例如原始指针)的长长列表。
  • 具有引用语义而不是值语义,这使得至少10个有效C ++项无意义(但引入了新的陷阱)。
  • 与C ++相比,实现定义的行为更少。
    • 特别地,在C ++的字符的编码charstring等是实现定义。Windows的Unicode方法(wchar_t对于UTF-16,char对于过时的“代码页”而言)与* nix的方法(UTF-8)之间的分裂引起了跨平台代码的巨大困难。C#OTOH保证a string为UTF-16。

新C#程序员应该注意C#中是否存在新的,不同的陷阱?

是: IDisposable

是否有等效于C#的“有效C ++”书?

有本书称为有效C#,其结构与有效C ++相似。


0

不,C#(和Java)不如C ++安全

C ++是本地可验证的。我可以检查C ++中的单个类,并假定所有引用的类都是正确的,然后确定该类不会泄漏内存或其他资源。在Java或C#中,有必要检查每个引用的类以确定是否需要某种形式的终结处理。

C ++:

{
   some_resource r(...);  // resource initialized
   ...
}  // resource destructor called, no leaks here

C#:

{
   SomeResource r = new SomeResource(...); // resource initialized
   ...
} // did I need to finalize that?  May I should have used 'using' 
  // (or in Java, a grotesque try/finally construct)?  No way to tell
  // without checking the documentation for SomeResource

C ++:

{
    auto_ptr<SomeInterface> i = SomeFactory.create(...);
    i->f(...);
} // automatic finalization and memory release.  A new implementation of
  // SomeInterface can allocate and free resources with no impact
  // on existing code

C#:

{
   SomeInterface i = SomeFactory.create(...);
   i.f(...);
   ...
} // Sure hope someone didn't create an implementation of SomeInterface
  // that requires finalization.  In C# and Java it is necessary to decide whether
  // any implementation could require finalization when the interface is defined.
  // If the initial decision is 'no finalization', then no future implementation  
  // can acquire any resource without creating potential leaks in existing code.

3
...在现代IDE中,确定是否有某些东西是从IDisposable继承来的,这很简单。主要的问题是您需要知道使用方法auto_ptr(或几种类似方法)。那就是谚语。
Telastyn 2012年

2
@Telastyn不,关键是您始终使用一个智能指针,除非您真的知道自己不需要一个。在C#中,using语句就像您要引用的绳索一样。(即,在C ++中,您必须记住使用智能指针,即使您必须始终使用using语句,为什么C#并没有那么糟糕)
gbjbaanb 2012年

1
@gbjbaanb因为什么?最多C#类中有5%是一次性的?而且您知道,如果它们是一次性的,则需要处理它们。在C ++中,每个对象都是一次性的。而且您不知道是否需要处理您的特定实例。对于非工厂返回的指针会发生什么情况?清理它们是您的责任吗?它应该,但有时是。再说一次,仅仅因为您应该始终使用智能指针并不意味着不存在的选择就不复存在了。特别是对于初学者,这是一个很大的陷阱。
Telastyn 2012年

2
@Telastyn:知道使用auto_ptr就像知道使用IEnumerable或知道使用接口一样简单,或者不使用浮点数表示货币等。这是DRY的基本应用。谁不知道如何编程的基础的人都会犯该错误。不像using。问题using在于您必须为每个类都知道它是否是Disposable(并且我希望永远都不会改变),如果它不是Disposable,则您会自动禁止所有可能必须是Disposable的派生类。
DeadMG 2012年

2
凯文:嗯,你的答案没有道理。您做错了不是C#的错。你不要依赖于正确编写C#代码终结。如果您的字段具有Dispose方法,则必须实现IDisposable(“正确”方式)。如果您的类做到了这一点(相当于在C ++中为您的类实现RAII),并且您使用了using它(就像C ++中的智能指针一样),那么一切都将完美地工作。终结器主要是为了防止意外发生- Dispose对正确性负责,如果您不使用它,那是您的错,而不是C#。
user541686

0

是100%是,因为我认为释放内存并在C#中使用它是不可能的(假定它处于托管状态,并且您不会进入不安全模式)。

但是,如果您知道如何用C ++编程,那么很多人都不会。很好 就像Charles Salvia类一样,它们并不是真正地管理他们的记忆,因为所有这些都是在预先存在的STL类中处理的。我很少使用指针。事实上,我去项目时没有使用任何指针。(C ++ 11使这更容易)。

至于拼写错误,愚蠢的错误等(例如:if (i=0)bc键很快就被卡住了),编译器抱怨这很好,因为它可以提高代码质量。其他示例忘记break了switch语句,并且不允许您在函数中声明静态变量(我有时不喜欢但imo是个好主意)。


4
Java和C#通过使用引用相等和引入值相等使=/ ==问题更加严重。可怜的程序员现在必须跟踪变量是“ double”还是“ Double”,并确保调用正确的变量。==.equals
凯文·克莱恩

@kevincline +1,但是在C#中struct您可以做到==这一点,因为大部分时间里只有字符串,整数和浮点数(即只有struct成员)才能很好地工作。在我自己的代码中,除非我想比较数组,否则我永远不会遇到这个问题。我认为我从未比较过列表或非结构类型(字符串,整数,浮点数,DateTime,KeyValuePair等)

2
Python通过使用==值相等和is引用相等来正确地做到这一点。
dan04'8

@ dan04-您认为C#有几种平等类型?观看ACCU精彩的闪电演讲:一些物体比其他物体更平等
Mark Booth 2012年
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.