C#开发者学习C++

63
我是一名.NET开发人员,之前使用过VB6。我对这些环境以及在垃圾回收语言的上下文中工作非常熟悉。然而,现在我希望通过学习C++来增强我的技能,但我有点不知所措。具体来说,以下几个方面让我感到困惑:
  • 引用/使用其他库
  • 公开我的库供他人使用
  • 字符串处理
  • 数据类型转换
  • 良好的项目结构
  • 要使用的数据结构(例如,在C#中,我经常使用List<T>,那我应该使用哪种类似的数据结构?)
对于我来说,似乎这并不像初学者通常会遇到的障碍,因为我觉得我对指针和内存管理有了很好的掌握。但对我来说有点混乱的是:
实际上,这感觉就像根据你使用的集成开发环境(IDE)不同,指南也不同。所以我真的正在寻找一些更普遍的东西。或者最坏的情况下,专注于使用Microsoft的编译器/IDE。同时请注意,我不需要任何关于通用编程实践的内容(如设计模式、Code Complete等),因为我觉得我在这些主题上已经很熟练了。

3
哦,你列出的很多事情正是让我越来越偏向于C#的原因。我喜欢C++并且对工具链有着扎实的理解,但只有一个程序集,整洁地打包在一起成为自包含体,与使用静态和动态运行时的调试版本以及大量头文件和某些常量的静态链接或动态链接相比,这种美丽简直不可同日而语。 - Cygon
6个回答

202

我知道你说你掌握了指针和内存管理的技能,但我仍然想解释一个重要的技巧。

作为一个经验法则,永远不要在用户代码中使用new/delete。

每个资源获取(无论是同步锁、数据库连接、一块内存还是其他必须获取和释放的东西)都应该被封装在一个对象中,以便构造函数执行获取,析构函数释放资源。这种技术被称为RAII,基本上是避免内存泄漏的方法。要习惯它。

C++标准库显然广泛使用了这个技术,所以你可以从那里了解它的工作原理。回答你的问题,List<T>的等价物是std::vector<T>,它使用RAII来管理其内存。你可以像这样使用它:

void foo() {

  // declare a vector *without* using new. We want it allocated on the stack, not
  // the heap. The vector can allocate data on the heap if and when it feels like
  // it internally. We just don't need to see it in our user code
  std::vector<int> v;
  v.push_back(4);
  v.push_back(42); // Add a few numbers to it

  // And that is all. When we leave the scope of this function, the destructors 
  // of all local variables, in this case our vector, are called - regardless of
  // *how* we leave the function. Even if an exception is thrown, v still goes 
  // out of scope, so its destructor is called, and it cleans up nicely. That's 
  // also why C++ doesn't have a finally clause for exception handling, but only 
  // try/catch. Anything that would otherwise go in the finally clause can be put
  // in the destructor of a local object.
} 

如果我必须选择一个C++程序员必须学习和接受的原则,那就是上面的内容。让作用域规则和析构函数为您工作。它们提供了所有您需要编写安全代码的保证。
字符串处理: 在这方面,std::string是您的朋友。在C中,您将使用char数组(或char指针),但它们很糟糕,因为它们不像字符串一样行为。在C++中,您有一个std::string类,它的行为符合您的预期。唯一需要记住的是,“hello world”是char[12]类型而不是std::string(为了与C兼容),因此有时您必须显式转换字符串字面值(括在引号中的内容,如“hello world”)以获得所需的行为:您仍然可以编写。
std::string s = "hello world";

因为C风格的字符串(如字面量,比如“hello world”)可以隐式转换为std::string,但不总是奏效: "hello" + " world"无法编译,因为+运算符未定义为两个指针。然而,“hello worl”+'d'可以编译,但它不会做任何明智的事情。它不会将一个字符附加到一个字符串中,而是将该字符的整数值(升级为int)加入到指针的值中。
相比之下,std::string("hello worl") + “d”可以按预期工作,因为左侧已经是std::string,并且即使右侧是char *或单个字符,也对std::string重载了加法运算符以按预期工作。
有关字符串的最后一点说明: std::string使用char,这是一个单字节数据类型。也就是说,它不适用于Unicode文本。 C ++提供了宽字符类型wchar_t,它是2或4个字节,具体取决于平台,并且通常用于Unicode文本(尽管在任何情况下都不会真正指定C ++标准字符集)。 wchar_t的字符串称为std::wstring。

库:

基本上不存在。 C ++语言没有库概念,这需要一些时间来习惯。它允许您包含另一个文件(通常是扩展名为.h或.hpp的头文件),但这只是一个逐字复制/粘贴。预处理器只需将两个文件组合成一个称为翻译单位的结果即可。多个源文件通常包括相同的头文件,这仅在某些特定情况下起作用,因此这一点是理解C ++编译模型的关键,后者是出了名的古怪。与像C#编译器一样编译一堆单独的模块,并在它们之间交换某种元数据不同,每个翻译单元都是孤立地编译的,然后将生成的对象文件传递给链接器,后者然后尝试将公共部分合并回来(如果多个翻译单元包括相同的头文件,则在翻译单元之间重复了代码,因此链接器将它们合并为单个定义);)
当然,编写库有特定于平台的方法。在Windows上,您可以制作.dll或.lib文件,其中.lib将链接到应用程序中,而.dll是必须与应用程序捆绑的单独文件,就像在.NET中一样。在Linux上,相应的文件类型是.so和.a,在所有情况下,您还必须提供相关的头文件,以便人们能够针对您的库进行开发。

数据类型转换:

我不确定您要寻找什么,但我认为一点很重要的是“传统”转换如下是不好的:
int i = (int)42.0f; 

这样做有几个原因。 首先,它尝试按顺序执行几种不同类型的转换,你可能会惊讶于编译器最终应用了哪种转换。其次,很难在搜索中找到,第三,它不够丑陋。通常最好避免使用转换,在C++中,它们被制作得有点丑陋,以提醒你这一点。

// The most common cast, when the types are known at compile-time. That is, if 
// inheritance isn't involved, this is generally the one to use
static_cast<U>(T); 

// The equivalent for polymorphic types. Does the same as above, but performs a 
// runtime typecheck to ensure that the cast is actually valid
dynamic_cast<U>(T); 

// Is mainly used for converting pointer types. Basically, it says "don't perform
// an actual conversion of the data (like from 42.0f to 42), but simply take the
// same bit pattern and reinterpret it as if it had been something else). It is
// usually not portable, and in fact, guarantees less than I just said.
reinterpret_cast<U>(T); 

// For adding or removing const-ness. You can't call a non-const member function
// of a const object, but with a const-cast you can remove the const-ness from 
// the object. Generally a bad idea, but can be necessary.
const_cast<U>(T);

如您所见,这些转换更具体,这意味着编译器可以在转换无效时给出错误(与传统语法不同,传统语法会尝试上述任何一种转换直到找到有效的),它很大且冗长,使您可以搜索它,并提醒您在可能的情况下避免使用它们。 ;)
标准库:
最后,回到数据结构,花点时间了解标准库。它很小,但功能非常多样化,一旦学会如何使用它,您将处于更好的位置。
标准库由几个相当不同的构建块组成(该库随着时间的推移而逐渐积累。其中一部分是从C移植过来的。I/O流库是从一个地方采用的,容器类及其相关功能则是从另一个完全不同的库中采用的,并且设计明显不同。后者是通常被称为STL(标准模板库)的一部分。严格来说,那是稍作修改后被纳入C++标准库的名称。
STL是理解“现代C ++”的关键。它由三个支柱组成:容器、迭代器和算法。简而言之,容器公开迭代器,算法对迭代器对进行操作。
以下示例将一个int向量的每个元素加1,并将其复制到链表中,仅作为示例:
int add1(int i) { return i+1; } // The function we wish to apply

void foo() {
  std::vector<int> v;
  v.push_back(1);
  v.push_back(2);
  v.push_back(3);
  v.push_back(4);
  v.push_back(5); // Add the numbers 1-5 to the vector

  std::list<int> l;

  // Transform is an algorithm which applies some transformation to every element
  // in an iterator range, and stores the output to a separate iterator
  std::transform ( 
  v.begin(),
  v.end(), // Get an iterator range spanning the entire vector
  // Create a special iterator which, when you move it forward, adds a new 
  // element to the container it points to. The output will be assigned to this
  std::back_inserter(l) 
  add1); // And finally, the function we wish to apply to each element
}

上述样式需要一些适应,但它非常强大和简洁。 由于转换函数是模板化的,它可以接受任何类型的输入,只要它们表现为迭代器。这意味着该函数可用于组合任何类型的容器,甚至流或任何其他可迭代的东西,只要迭代器设计为与STL兼容即可。我们也不必使用begin/end对。除了结束迭代器外,我们还可以传递一个指向第三个元素的迭代器,算法将在那里停止。或者我们可以编写自定义迭代器,跳过每个其他元素或任何其他我们喜欢的元素。 以上是三个支柱的基本示例。我们使用容器来存储数据,但我们用于处理它的算法实际上不必知道容器的情况。它只需要知道它必须工作的迭代器范围。当然,这三个支柱中的每一个都可以通过编写新类来扩展,然后这些类将与STL的其余部分平稳地协同工作。
在某种意义上,这与LINQ非常相似,因此由于您来自.NET,您可能会看到一些类比。 STL的对应物更加灵活,但代价是稍微奇怪的语法。 :) (如评论中所提到的,它也更有效率。通常情况下,STL算法没有任何开销,它们可以像手动编码的循环一样高效。这通常令人惊讶,但是可能性是因为所有相关类型在编译时已知(这是模板工作的要求),并且C ++编译器倾向于积极地内联。)

10
漂亮的回答,我只想补充一点,就是使用std::swap,因为它是现代C++中与构造、析构和赋值一样重要的部分,有助于以异常安全的方式实现赋值,并支持传递大型对象的所有权,例如std::vectors。 - Daniel Earwicker
5
教科书答案。值得一提的是,与LINQ不同,这些算法在没有任何封装相关开销的情况下执行,与汇编级别上的硬编码循环一样高效(至少在具有良好STL实现的优秀编译器中)。这总是令人惊讶的。 - Konrad Rudolph

3
您有一些可用的工具包。例如,STL(标准模板库)和Boost/TR1(STL的扩展)被认为是行业标准(至少STL是)。它们提供列表、映射、集合、共享指针、字符串、流以及各种其他方便的工具。最重要的是,它们在编译器中得到广泛支持。
至于数据转换,您可以使用强制类型转换或创建显式转换函数。
库 - 您可以创建静态库(被吸收到最终可执行文件中)或DLL(您已经熟悉这些了)。MSDN是DLL的一个很棒的资源。静态库取决于您的构建环境。
总的来说,这是我的建议: -非常了解您选择的IDE -购买Herbert Schildt的《C++完全参考》,我认为这是关于C++(包括STL)的一本优秀的书籍
考虑到您的背景,一旦您完成这两个步骤,您应该会很好地设置。

3
我不会重复其他人关于库等的讲述,但如果你认真学习C++,请自己一个忙,拿起Bjarne Stroustrup的《C++程序设计语言》。我在C++领域工作多年后才开始阅读这本书,一旦读了这本书,我花了一个下午拍着自己的额头说“当然!我应该意识到的!等等。”(具有讽刺意味的是,我也曾经有过与K&R的《C程序设计语言》完全相同的经历。总有一天,我会学会第一天就去找“那本书”)。

1

关于“引用/使用其他库”的回应

有关在 Windows 和 Linux 中显式加载 DLL 的信息包括...

Windows:

Windows DLL 教程

函数:LoadLibrary、GetProcAddress、FreeLibrary

Linux:

函数:dlopen、dlsym、dlerror、dlclose

Linux DLL 教程


1

引用和使用其他库,如果你包含源代码,只需将库的头文件 #include 到需要它们的 .cpp 文件中(然后将库的源代码与项目一起编译)。然而,大多数情况下,您可能会使用 .lib(静态库)或 .dll(动态库)。大多数(全部?)DLL 都带有一个 .lib 文件,因此两种类型的过程是相同的:在需要它们的地方包含适当的头文件,然后在链接步骤中添加相关的 .lib 文件(在 Visual Studio 中,我认为您可以将文件添加到项目中)。

我很久没有创建自己的库供他人使用了,所以我会让其他人回答这部分。或者明天我会回来编辑这个答案,因为我明天要为工作创建一个 .lib :)

字符串处理通常使用 std::string 完成。在特殊情况下,您也可以使用旧的 C 样式 sprintf() 函数,但这通常是不鼓励的。

就你所寻找的数据结构而言,可以查看STL(标准模板库)。它包括List、Vector、Map、String等,这些应该对你来说很熟悉。 我不确定你所说的类型转换是什么意思……我假设你知道强制类型转换,所以你肯定指的是更复杂的东西,这可能与你要转换的类型有关。也许其他人可以提供更多信息。

-3

1
链接无效。 - afarazit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接