C#到C++'陷阱'

Geo*_*ton 25 c# c++

我一直在开发一个我绝对必须在C++中部分开发的项目.我需要开发一个包装器并将一些C++功能暴露给我的C#app.自.NET即将开始以来,我一直是C#工程师,并且在C++方面经验很少.在尝试理解语法时,它对我来说仍然看起来很陌生.

有什么东西会让我失去理智,这会阻止我只是拿起C++并为之奋斗吗?

Sim*_* T. 28

C++有很多陷阱,我无法全部枚举它们.搜索"C#vs C++".一些基本的事情要知道:在C++中:

  • struct和一个类基本相同(结构的默认可见性是公共的,它对于类来说是私有的).
  • struct和class都可以在堆或堆栈上创建.
  • 你必须自己管理堆.如果使用"new"创建内容,则必须在某个时刻手动删除它.
  • 如果性能不是问题并且您只有很少的数据可以移动,则可以通过在堆栈中使用所有内容并使用引用(&运算符)来避免内存管理问题.
  • 学会处理.h和.cpp.未解决的外部可能是你更糟糕的噩梦.
  • 您不应该从构造函数中调用虚方法.编译器永远不会告诉你我这样做.
  • Switch case不强制执行"break"并默认执行.
  • 没有接口这样的东西.相反,您拥有纯虚方法的类.
  • C++爱好者是生活在洞穴中并在C#/ java程序员的新鲜血液中幸存下来的危险人物.仔细与他们谈谈他们最喜欢的语言.

  • 危险,这是真的,但我们知道如何使用SO标记. (4认同)
  • 我不同意#4.通常,通过避免堆分配可以获得最佳性能.您可以节省过多的新/删除调用的(重要)开销,并获得更好的缓存局部性. (4认同)

acr*_*ron 26

垃圾收集!

请记住,每当您new成为对象时,您必须负责呼叫delete.

  • 或者您必须使用智能指针或其他管理内存的类型.当然,你可能不应该首先使用'new' - 过度使用动态分配是C#和Java程序员在使用C++时遇到的另一个问题. (23认同)
  • @Neil:我会说这是OP应该注意的#1问题.应该是一个答案,真的:)每个C#程序员花了3分钟看C++就知道你必须调用`delete`.他们应该经常避免"新",这更令人惊讶. (3认同)
  • 很好的一点.你需要真正掌握堆栈和堆之间的区别:http://www.learncpp.com/cpp-tutorial/79-the-stack-and-the-heap/ (2认同)

jal*_*alf 18

有很多不同之处,但是我能想到的最大的一个是来自Java/C#的程序员总是出错,他们从来没有意识到他们错了,是C++的价值语义.

在C#中,您习惯于在new任何希望创建对象的时候使用它.每当我们谈论类实例时,我们的意思是"对类实例的引用".Foo x = y不复制对象y,它只是创建对任何对象y引用的另一个引用.

在C++中,有本地对象之间有明显的区别,不分配new(Foo fFoo f(x, y)和动态分配的人(Foo* f = new Foo()Foo* f = new Foo(x, y)).而在C#而言,一切都值类型.Foo x = y实际上创建了一个副本中的Foo对象本身.

如果需要引用语义,可以使用指针或引用:Foo& x = y创建对象的引用y.Foo* x = &y创建指向所在地址的指针y.复制指针就是这样:它创建另一个指针,指向原始指针指向的任何指针.所以这类似于C#的引用语义.

本地对象具有自动存储持续时间 - 也就是说,本地对象在超出范围时会自动销毁.如果它是一个类成员,那么当拥有对象被销毁时它就会被销毁.如果它是函数内部的局部变量,则在执行离开声明它的作用域时会被销毁.

调用之前,不会销毁动态分配的对象delete.

到目前为止,你可能和我在一起.C++的新手很快就会被教授.棘手的部分在于它意味着什么,它如何影响您的编程风格:

在C++中,默认应该是创建本地对象.new除非你绝对必须,否则不要分配.

如果确实需要动态分配数据,请将其作为类的责任.一个(非常)简化的例子:

class IntArrayWrapper {
  explicit IntArrayWrapper(int size) : arr(new int[size]) {} // allocate memory in the constructor, and set arr to point to it
  ~IntArrayWrapper() {delete[] arr; } // deallocate memory in the destructor

  int* arr; // hold the pointer to the dynamically allocated array
};
Run Code Online (Sandbox Code Playgroud)

现在可以将此类创建为局部变量,并在内部执行必要的动态分配.当它超出范围时,它将再次自动删除分配的数组.

所以说我们需要一个x整数数组,而不是这样做:

void foo(int x){
  int* arr = new int[x];
  ... use the array ...
  delete[] arr; // if the middle of the function throws an exception, delete will never be called, so technically, we should add a try/catch as well, and also call delete there. Messy and error-prone.
}
Run Code Online (Sandbox Code Playgroud)

你可以这样做:

void foo(int x){
  IntArrayWrapper arr(x);
  ... use the array ...
  // no delete necessary
}
Run Code Online (Sandbox Code Playgroud)

当然,使用局部变量而不是指针或引用意味着对象被复制了很多:

Bar Foo(){
  Bar bar;
  ... do something with bar ...
  return bar;
}
Run Code Online (Sandbox Code Playgroud)

在上面,我们返回的是一个副本中的bar对象.我们可以返回一个指针或引用,但是当函数内部创建的实例超出范围并在函数返回时被销毁,我们无法指向它.我们可以使用new来分配会超越功能的实例,并返回一个函数, -然后我们得到了所有搞清楚谁负责删除对象的存储管理难题,以及何时应该发生.那不是个好主意.

相反,Bar应该简单地设计类,以便复制它做我们需要的.也许它应该内部调用new分配一个可以存活的对象,只要我们需要它.然后我们可以复制或分配"窃取"指针.或者我们可以实现某种引用计数方案,其中复制对象只是递增引用计数器并复制指针 - 然后应该删除,而不是在单个对象被销毁时,但是当最后一个对象被销毁时和参考计数器达到0.

但通常,我们只能执行深层复制,并完整地克隆对象.如果对象包含动态分配的内存,我们为副本分配更多内存.这可能听起来很昂贵,但C++编译器擅长消除不必要的副本(实际上在大多数情况下允许消除复制操作,即使它们有副作用).

如果你想避免复制更多,并且你准备忍受更多笨重的用法,你可以在你的类中启用"移动语义"以及(或代替)"复制语义".值得养成这种习惯是因为(a)某些对象不容易被复制,但它们可以被移动(例如一个Socket类),(b)它是在标准库中建立的模式和(c)它得到语言支持下一个版本.

使用移动语义,您可以将对象用作一种"可转移"容器.这是移动的内容.在当前的方法中,它是通过调用来完成的swap,它会交换相同类型的两个对象的内容.当一个对象超出范围时,它将被破坏,但是如果您首先将其内容交换为引用参数,则当范围结束时内容转义将被销毁.因此,您不一定需要一直使用引用计数智能指针只是为了允许从函数返回复杂对象.笨拙来自于你无法真正返回它们的事实- 你必须将它们交换为参考参数(有点类似于refC#中的参数).但是下一版C++中的语言支持将解决这个问题.

所以我能想到的最大C#到C++的问题:不要将指针作为默认值.使用值语义,而是定制您的类,以便在复制,创建和销毁时按照您想要的方式运行.

几个月前,我试图为你处境中的人写一系列博客文章:
第1
部分第2
部分第3部分

我对他们的表现并不是百分之百满意,但你仍然觉得它们很有用.

当你觉得自己永远不会抓住指针时,这篇文章可能有所帮助.


Mik*_*our 6

没有运行时检查

当您尝试执行可能无效但可以在运行时检查的内容时,一个C++缺陷是行为 - 例如,取消引用可能为null的指针,或者访问索引可能超出范围的数组.

C#哲学强调正确性; 所有行为都应该是明确定义的,在这种情况下,它会对前置条件执行运行时检查,如果失败则抛出明确定义的异常.

C++哲学强调效率,以及你不应该为你可能不需要的任何东西付费的想法.在这种情况下,不会为您检查任何内容,因此您必须自己检查前提条件或设计逻辑以使它们必须为真.否则,代码将具有未定义的行为,这意味着它可能(或多或少)执行您想要的操作,可能会崩溃,或者它可能会破坏完全不相关的数据并导致极难追踪的错误.