我一直在开发一个项目,其中必须部分使用C++。我需要开发一个包装器,并将一些C++功能暴露给我的C#应用程序。自.NET刚开始以来,我一直是一名C#工程师,对C++几乎没有太多经验。当尝试理解语法时,它仍然对我来说非常陌生。
有什么东西会让我无法入手,阻止我去学习C++吗?
C++有很多问题,我无法全部列举。可以搜索"C# vs C++"。以下是一些基本知识:
垃圾回收!
记住每次新建一个对象时,您必须负责调用delete
来销毁它。
有很多不同之处,但我能想到程序员从Java/C#转来时总是犯错的最大问题,而他们从未意识到自己犯了错误,那就是C ++的值语义。
在C#中,您习惯于在想创建对象时使用new
。每当我们谈论类实例时,我们真正指的是“对类实例的引用”。Foo x = y
并不会复制对象y
,它只是创建了另一个对象y
引用的引用。
在C ++中,本地对象(无需使用new
分配)(Foo f
或Foo 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
};
这个类现在可以作为一个本地变量被创建,它会内部执行必要的动态分配。当它超出范围时,它将自动删除已分配的数组。
假设我们需要一个长度为 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.
}
你可以这样做:
void foo(int x){
IntArrayWrapper arr(x);
... use the array ...
// no delete necessary
}
当然,使用局部变量而不是指针或引用会导致对象经常被复制:
Bar Foo(){
Bar bar;
... do something with bar ...
return bar;
}
在上面的代码中,我们返回的是bar
对象的一个副本。虽然我们可以返回一个指针或者引用,但是因为在函数内部创建的实例会在函数返回时被销毁并且超出了作用域,所以我们无法指向它。我们可以使用new
来分配一个长于函数生存期的实例,并返回对该实例的引用 —— 然后我们就需要管理所有内存的释放问题,需要弄清楚责任应该由谁承担以及何时进行删除。这不是一个好主意。Bar
类应该设计成只需复制即可完成我们所需的操作。也许它应该在内部调用new
来分配一个可以在需要时生存的对象。然后我们可以通过复制或赋值“偷走”那个指针。或者我们可以实现某种引用计数方案,在复制对象时只需增加引用计数并复制指针 —— 这样应该在最后一个对象被销毁并且引用计数达到0时而非单个对象被销毁时删除指针。Socket
类),(b)它是标准库中已经建立的模式,以及(c)它将在下一个版本中获得语言支持。swap
来完成,该函数交换两个相同类型的对象的内容。当一个对象超出作用域时,它被销毁,但是如果你先将它的内容交换到引用参数中,那么内容就会逃脱销毁。因此,你不一定需要全程使用引用计数智能指针来允许从函数返回复杂的对象。问题在于你无法真正返回它们——你必须将它们交换到一个引用参数中(与C#中的ref
参数类似)。但是,在下一个版本的C++中的语言支持将解决这个问题。几个月前,我试图为您这种情况的人编写一系列博客文章:
第一部分
第二部分
第三部分
虽然我对它们的效果不是完全满意,但您仍然可能会发现它们有用。
当您感到永远无法掌握指针时,这篇文章可能会有所帮助。
swap
——这是一种笨拙但有效的实现移动语义的方法,比完全采用引用计数简单得多。在函数内部构建一个大对象,然后通过将其与非const引用参数交换来“返回”它。 - Daniel Earwickerswap
是使 C++ 的值语义正常工作的重要部分。 - jalf无运行时检查
C++ 中的一个陷阱是当你尝试做一些可能无效的事情时,但只有在运行时才能检查 - 例如,解引用可能为空的指针,或使用可能超出范围的索引访问数组。
C# 的哲学强调正确性; 所有的行为都应该是明确定义的,在这种情况下,它会对前提条件进行运行时检查,如果失败则抛出明确定义的异常。
C++ 的哲学强调效率和你不应该为任何可能不需要的东西付出代价。在这种情况下,没有任何内容会为您进行检查,因此您必须自己检查前提条件或设计逻辑使其成立。否则,代码将具有未定义的行为,这意味着它可能会(或多或少)做您想要的事情,它可能会崩溃,或者它可能会破坏完全不相关的数据并导致难以追踪的错误。
还有一些其他的东西,其他答案没有提到:
const:C#对常量的概念比较有限。在C++中,“常量正确性”很重要。不修改其引用参数的方法应采用const引用。例如:
void func(const MyClass& x)
{
// x cannot be modified, and you can't call non-const methods on x
}
不修改对象的成员函数应标记为const,即:
int MyClass::GetSomething() const // <-- here
{
// Doesn't modify the instance of the class
return some_member;
}
std::string str = std::string("hello") + " world" + "!";
void func(const MyClass& x)
你可以使用临时的 MyClass 显式地调用它:
func(MyClass()); // create temporary MyClass - NOT the same as 'new MyClass()'!
创建一个MyClass实例,在栈上,func2访问它,然后临时的MyClass在func返回后自动被销毁。这既方便又通常非常快速,因为不涉及堆。请注意,“new”返回指针 - 而不是引用 - 并且需要相应的“delete”。您还可以直接将临时变量赋值给const引用:
const int& blah = 5; // 5 is a temporary
const MyClass& myClass = MyClass(); // creating temporary MyClass instance
// The temporary MyClass is destroyed when the const reference goes out of scope
常量引用和临时变量在良好的C++风格中很常见,它们的工作方式与C#非常不同。
RAII,异常安全和确定性析构函数。这实际上是C ++的一个有用特性,可能甚至是优于C#的优势,值得阅读,因为它也是好的C ++风格。我不会在这里详细介绍。
最后,我只想提醒this是一个指针,而不是引用 :)
头文件!你会发现自己会问,“为什么每次都需要写两遍方法声明呢?”
operator=
。指针和内存分配
...我也是一个C#程序员,仍在努力理解C/C++中正确的内存使用方法。
;
结束一个类定义时最令人烦恼...我的意思是,为什么呢?}
不是已经说明了一切吗? - Noldorin;
即使它在那里是多余的。还有,使用public: private: protected:
作为伪标签而不是对各个成员进行修改。并且将私有继承作为默认选项(尽管我认为 CLR 的类型系统不允许这样做)。 - Daniel Earwicker