Skip to content

Latest commit

 

History

History
421 lines (310 loc) · 23 KB

01.md

File metadata and controls

421 lines (310 loc) · 23 KB

一、类型

基本类型

C++ 包含您从 C# 中识别出的相同熟悉的关键字(例如,int)。考虑到两者都是类似 C 的语言,这并不奇怪。然而,有一个潜在的地雷会给你带来麻烦。而 C# 明确定义了基本类型的大小(a short是 16 位整数,int是 32 位整数,long是 64 位整数,double是 64 位双精度 IEEE 754 浮点数,等等。),C++ 不做这样的保证。


履行

像许多编程语言一样,有来自不同公司和组织的多个 C++ 实现。两种最流行的 C++ 实现是 Visual C++ 和 GCC。C++ 标准将一些细节留给实现者来定义。如果您正在编写跨平台代码,您需要记住这些实现定义的细节,以免您发现一个程序在一个操作系统上运行良好,而在另一个操作系统上以奇怪的方式失败。


C++ 中最小的基本单位是char,它只需要至少足够大就可以容纳 C++ 标准指定的 96 个基本字符,加上实现的基本字符集中的任何其他字符。理论上,C++ 的一些实现可以将char定义为 7 位或 16 位……几乎任何事情都是可能的。但实际上,你不需要太担心char是除了 8 位以外的任何东西(相当于 C# 中的bytesbyte类型),这是它在 Visual C++ 中的大小。

在 C++ 中,char, signed char,unsigned char是三种不同的类型。这三个都需要占用相同的内存。所以实际上一个char要么是有符号的,要么是无符号的。它是有符号的还是无符号的是实现定义的(参见侧栏)。在 Visual C++ 中,char类型默认情况下是带符号的。但是您可以使用编译器开关将其视为无符号的。在 GCC 中,它是有符号的还是无符号的,取决于你针对的是哪种 CPU 架构。

有符号整数类型按大小从最小到最大的顺序为:

  1. signed char
  2. short int
  3. int
  4. long int
  5. long long int

这些整数类型大小的唯一保证是每个类型至少与下一个最小的整数类型一样大。在 Visual C++ 中,intlong int都是 32 位整数。只有long long int是 64 位整数。

| | 注:可以简单写longlong long;不需要分别写long int或者long long intshort int也是如此(也就是你可以直接写short))。short类型是 Visual C++ 中的 16 位有符号整数。 |

每个整数类型都有相应的无符号整数类型。你只要把关键字unsigned放在前面就可以得到未签名版本(除了signed char,你改成unsigned char)。

如果您需要确保您使用的是特定的大小,您可以包含 C++ 标准库头文件CST int(例如,#include <cstdint>),其中定义了类型:

  • int8_t
  • int16_t
  • int32_t
  • int64_t
  • uint8_t
  • uint16_t
  • uint32_t
  • uint64_t

这些类型都有它们的用途,但是你会发现大部分 API 都不使用它们;相反,他们直接使用基本类型。这可能会使您的编程变得混乱,因为您经常需要检查底层的基本类型,以确保不会以意外的截断或扩展而告终。

这些类型可能会被更多地使用,所以我建议经常检查它们在主要库和 API 中的使用情况,如果它们被广泛采用,就相应地调整您的代码。当然,如果您绝对需要一个变量,例如,一个无符号的 32 位整数,您当然应该利用uint32_t,并根据需要对 API 调用和可移植性进行任何调整。

就大小顺序规则而言,浮点数是相同的。他们从floatdouble再到long double。在 Visual C++ 中,float是 32 位浮点数,doublelong double都是 64 位浮点数(long double不大于double,换句话说)。

C++ 没有任何可与 C# 的decimal类型相媲美的本机类型。然而,C++ 的一个优点是,通常有大量免费或廉价的库可以获得许可。例如,有 decNumber 库英特尔十进制浮点数学库GNU 多精度算术库。没有一个与 C# 的十进制类型完全兼容,但是如果您只为 Windows 系统编写,那么如果需要,您可以使用十进制数据类型以及十进制算术函数数据类型转换函数来获得这种兼容性。

还有一个布尔类型,bool,可以是true也可以是false。在 Visual C++ 中,一个bool占用一个字节。与 C# 不同的是,bool可以转换成整数类型。当为假时,它的整数值为 0,当为真时,它的整数值为 1。所以语句bool result = true == 1;会编译,当语句执行完毕后result会评估为true

然后是wchar_t类型,持有宽字符。宽字符的大小因平台而异。在 Windows 平台上,它是 16 位字符。它相当于 C# 的char类型。它经常被用来构造字符串。我们将在另一章讨论字符串,因为字符串可以使用许多变体。

最后,还有void类型,它的使用方式与 C# 相同。还有一个std::nullptr_t类型,解释起来很乱,但基本上是要有nullptr文字的类型,这是你应该用它来代替NULL或者文字0(零)来检查空值。

枚举

枚举在 C++ 和 C# 中非常相似。C++ 有两种类型的枚举:作用域和非作用域。

限定范围的枚举定义为enum classenum struct。两者没有区别。未限定范围的枚举被定义为普通的enum。让我们看一个例子:

示例:EnumSample\EnumSample.cpp

    #include <iostream>
    #include <ostream>
    #include <string>
    #include "../pchar.h"

    enum class Color
    {
          Red,
          Orange,
          Yellow,
          Blue,
          Indigo,
          Violet
    };

    // You can specify any underlying integral type you want, provided it fits.
    enum Flavor : unsigned short int
    {
          Vanilla,
          Chocolate,
          Strawberry,
          Mint,
    };

    int _pmain(int /*argc*/, _pchar* /*argv*/[])
    {
          Flavor f = Vanilla;
          f = Mint; // This is legal since the Flavor enum is an un-scoped enum.

          Color c = Color::Orange;
          //c = Orange; // This is illegal since the Color enum is a scoped enum.

          std::wstring flavor;
          std::wstring color;

          switch (c)
          {
          case Color::Red:
                color = L"Red";
                break;
          case Color::Orange:
                color = L"Orange";
                break;
          case Color::Yellow:
                color = L"Yellow";
                break;
          case Color::Blue:
                color = L"Blue";
                break;
          case Color::Indigo:
                color = L"Indigo";
                break;
          case Color::Violet:
                color = L"Violet";
                break;
          default:
                color = L"Unknown";
                break;
          }

          switch (f)
          {
          case Vanilla:
                flavor = L"Vanilla";
                break;
          case Chocolate:
                flavor = L"Chocolate";
                break;
          case Strawberry:
                flavor = L"Strawberry";
                break;
          case Mint:
                flavor = L"Mint";
                break;
          default:
                break;
          }

          std::wcout << L"Flavor is " << flavor.c_str() << L" (" << f <<
                L"). Color is " << color.c_str() << L" (" <<
                static_cast<int>(c) << L")." << std::endl <<
                L"The size of Flavor is " << sizeof(Flavor) <<
                L"." << std::endl <<
                L"The size of Color is " << sizeof(Color) <<
                L"." << std::endl;

          return 0;
    }

| | 注意:在 C++ 中,作用域解析运算符是::。我们稍后会更详细地讨论这个问题。就目前而言,只需将其视为与 C# 中的.运算符服务于许多相同的目的。 |

该代码将给出以下输出:

    Flavor is Mint (3). Color is Orange (1).
    The size of Flavor is 2.
    The size of Color is 4.

如您在示例中所见,作用域Color枚举要求您以与 C# 相同的方式访问其成员,方法是在枚举成员前面加上枚举的名称和作用域解析运算符。相比之下,非作用域Flavor枚举允许您简单地指定没有任何前缀的成员。出于这个原因,我认为更好的做法是更喜欢限定范围的枚举:这样可以最小化命名冲突的风险,减少命名空间污染。

请注意,作用域枚举还有另一个区别:当我们想要输出作用域Color枚举的数值时,我们必须使用static_cast运算符将其转换为int,而我们不需要对非作用域Flavor枚举进行任何转换。

对于Flavor枚举,我们将基础类型指定为无符号的短int。您也可以指定作用域枚举的基础类型。指定基础类型是可选的,但是如果希望对非作用域枚举使用正向声明,则必须指定基础类型。正向声明是一种加快程序编译时间的方法,它只告诉编译器它需要知道的关于一个类型的信息,而不是强迫它编译定义该类型的整个头文件。

我们将在稍后讨论这个问题。现在,请记住,非作用域枚举必须显式指定其基础类型,以便使用它的正向声明;范围枚举不需要指定其基础类型来使用其正向声明(如果没有指定,基础类型将为int)。

就向成员显式赋值以及创建标志枚举而言,在 C++ 中可以像在 C# 中一样对枚举执行同样的操作。除了不需要应用像 C++ 中的FlagAttribute这样的任何东西来创建标志枚举之外,你都可以用同样的方法来做;您只需分配正确的值,然后从那里开始。

std::wcout``std::wcerr``std::wcin

std::wcout << L”Flavor...代码将宽字符数据输出到标准输出流。在这种控制台程序的情况下,标准输出是控制台窗口。还有一个std::wcerr输出流,将宽字符数据输出到标准错误输出流。这也是控制台窗口,但是您可以将std::wcout输出重定向到一个文件,将std::wcerr输出重定向到另一个文件。还有一个std::wcin用于从控制台输入数据。我们不会探究这个,也不会探究它们的字节对应物:std::coutstd::cerrstd::cin

为了让您看到输入的样子,这里有一个例子。

示例:consolessample \ consolessample . CPP

    #include <iostream>
    #include <ostream>
    #include <string>
    #include "../pchar.h"

    struct Color
    {
          float ARGB[4];

          void A(float value) { ARGB[0] = value; }
          float A(void) const { return ARGB[0]; }
          void R(float value) { ARGB[1] = value; }
          float R(void) const { return ARGB[1]; }
          void G(float value) { ARGB[2] = value; }
          float G(void) const { return ARGB[2]; }
          void B(float value) { ARGB[3] = value; }
          float B(void) const { return ARGB[3]; }
    };

    // This is a stand-alone function, which happens to be a binary
    // operator for the << operator when used with a wostream on
    // the left and a Color instance on the right.
    std::wostream& operator<<(std::wostream& stream, const Color& c)
    {
          stream << L"ARGB:{ " << c.A() << L"f, " << c.R() << L"f, " <<
                c.G() << L"f, " << c.B() << L"f }";
          return stream;
    }

    int _pmain(int /*argc*/, _pchar* /*argv*/[])
    {
          std::wcout << L"Please input an integer and then press Enter: ";
          int a;

          std::wcin >> a;

          std::wcout << L"You entered '" << a << L"'." << std::endl;

          std::wcout << std::endl <<
                L"Please enter a noun (one word, no spaces) " <<
                L"and then press Enter: ";

          std::wstring noun;

          // wcin breaks up input using white space, so if you include a space or
          // a tab, then it would just put the first word into noun and there
          // would still be a second word waiting in the input buffer.
          std::wcin >> noun;

          std::wcerr << L"The " << noun << L" is on fire! Oh no!" << std::endl;

          Color c = { { 100.0f/255.0f, 149.0f/255.0f, 237.0f/255.0f, 1.0f } };

          // This uses our custom operator from above. Come back to this sample
          // later when we've covered operator overloading and this should make
          // much more sense.
          std::wcout << std::endl <<
                L"Cornflower Blue is " << c << L"." << std::endl;

          return 0;
    }

前面的代码是一个相当简单的演示。例如,它没有错误检查。所以,如果你输入一个不正确的整数值,它会一直运行到最后std::wcin立即返回,没有任何数据(除非你解决错误,否则它会这样做)。

如果您对 IOs stream 编程感兴趣,包括使用std::wofstream将数据输出到文件和std::wifstream从文件中读取数据(它们的工作方式与std::wcoutstd::wcin相同,只是增加了处理文件的功能),请参见MSDN IOs stream 编程页面。学习溪流的所有细节可以轻松地独自填满一本书。

不过,还有最后一件事。毫无疑问,您已经注意到流功能在移位操作符<<>>上看起来有点奇怪。那是因为这些操作符已经过载了。虽然您会期望位移位运算符以某种方式作用于整数,但是对于它们分别应用于输出流或输入流时应该如何工作,您可能没有任何具体的期望。因此,C++ 标准库流增选了这些操作符,使用它们向流输入和输出数据。当我们希望能够读入或写出我们已经创建的自定义类型(例如前面的Color结构)时,我们只需要创建一个适当的运算符重载。我们将在本书的后面部分了解更多关于运算符重载的信息,所以如果现在有点混乱,请不要担心。

类和结构

C++ 中类和结构的区别很简单,结构的成员默认为公共的,而类的成员默认为私有的。就这样。它们在其他方面是相同的。C# 中没有值类型和引用类型的区别。

也就是说,通常你会看到程序员将类用于复杂的类型(数据和函数的组合),将结构用于简单的纯数据类型。通常,这是一种风格选择,代表了 C 语言中结构的非面向对象起源,通过查看它是结构还是类,可以很容易地快速区分简单数据容器和完整对象。我建议遵循这种风格。

| | 注意:这种风格的一个例外是,程序员编写的代码既适用于 C,也适用于 C++。由于 C 没有类类型,结构类型的使用方式可能与 C++ 中使用类的方式类似。我不打算在这本书里讨论写 C 兼容的 C++ 了。为此,您需要熟悉 C 语言以及它和 C++ 之间的区别。相反,我们专注于编写干净、现代的 C++ 代码。 |

在 Windows Runtime(“WinRT”)编程中,公共结构只能有数据成员(没有属性或函数)。这些数据成员只能由基本数据类型和其他公共结构组成,这些数据类型和公共结构当然具有相同的仅限数据、基本和仅限公共结构的限制。如果您正在使用 C++ 为 Windows 8 开发任何地铁风格的应用程序,请记住这一点。

您有时会看到类定义中使用的friend关键字。它后面是类名或函数声明。这个代码构造所做的是让该类或函数访问该类的非公共成员数据和函数。通常,您会希望避免这种情况,因为您的类通常应该通过其公共接口公开您想要公开的所有内容。但是在极少数情况下,您不希望公开某些数据成员或成员函数,但希望一个或多个类或函数能够访问它,您可以使用friend关键字来实现这一点。

由于类是 C++ 编程中非常重要的一部分,我们将在本书后面更详细地探讨它们。

工会

union型有点奇怪,但是有它的用处。你会时不时遇到它。一个union是一个数据结构,看起来包含许多数据成员,但是只允许你在任何时候使用它的一个数据成员。最终的结果是一个数据结构,在不浪费内存的情况下为您提供了许多可能的用途。联盟的规模要求足够大,只能容纳联盟中最大的成员。实际上,这意味着数据成员在内存中相互重叠(因此,一次只能使用一个)。这也意味着你没有办法知道一个工会的活跃成员是什么,除非你以某种方式跟踪它。有很多方法可以做到这一点,但是在一个结构中放置一个联合和一个枚举是一个好的、简单的、整洁的方法。这里有一个例子。

示例:UnionSample\UnionSample.cpp

    #include <iostream>
    #include <ostream>
    #include "../pchar.h"

    enum class SomeValueDataType
    {
          Int = 0,
          Float = 1,
          Double = 2
    };

    struct SomeData
    {
          SomeValueDataType Type;
          union
          {
                int iData;
                float fData;
                double dData;
          } Value;

          SomeData(void)
          {
                SomeData(0);
          }

          SomeData(int i)
          {
                Type = SomeValueDataType::Int;
                Value.iData = i;
          }

          SomeData(float f)
          {
                Type = SomeValueDataType::Float;
                Value.fData = f;
          }

          SomeData(double d)
          {
                Type = SomeValueDataType::Double;
                Value.dData = d;
          }
    };

    int _pmain(int /*argc*/, _pchar* /*argv*/[])
    {
          SomeData data = SomeData(2.3F);
          std::wcout << L"Size of SomeData::Value is " << sizeof(data.Value) <<
                L" bytes." << std::endl;

          switch (data.Type)
          {
          case SomeValueDataType::Int:
                std::wcout << L"Int data is " << data.Value.iData << L"."
                      << std::endl;
                break;
          case SomeValueDataType::Float:
                std::wcout << L"Float data is " << data.Value.fData << L"F."
                      << std::endl;
                break;
          case SomeValueDataType::Double:
                std::wcout << L"Double data is " << data.Value.dData << L"."
                      << std::endl;
                break;
          default:
                std::wcout << L"Data type is unknown." << std::endl;
                break;
          }
          return 0;
    }

如您所见,我们定义了一个枚举,其成员代表联合的每种成员类型。然后,我们定义一个结构,该结构既包括该枚举类型的变量,又包括匿名联合。这为我们提供了确定联合当前在一个封装的包中持有哪种类型所需的所有信息。

如果您希望联合可以在多个结构中使用,您可以在结构之外声明它并给它一个名称(例如,union SomeValue { ... };))。然后,您可以在结构中使用它,例如,SomeValue Value;。不过,保持匿名通常更好,因为除了在定义它的结构内,您不需要担心进行更改的副作用。

联合可以有构造器、析构器和成员函数。但是由于它们只能有一个活动的数据成员,所以为联合编写成员函数几乎没有任何意义。你将很少见到他们,也许永远不会。

typedef

关于typedef首先要理解的是,尽管它的名字有含义,typedef并不创造新的类型。这是一种混叠机制,可以用于许多事情。

它在实现 C++ 标准库和其他基于模板的代码中被大量使用。可以说,这是它最重要的用途。我们将在模板一章中进一步探讨。

它可以让你省去大量的打字工作(尽管这个参数随着在 C++ 11 中重新使用auto关键字进行类型推导而失去了一些效力)。如果你有一个特别复杂的数据类型,为它创建一个typedef意味着你只需要键入一次。如果你的复杂数据类型的目的不明确,用typedef给它取一个语义上更有意义的名字可以帮助你的程序更容易理解。

它有时被开发人员用作一种抽象,以方便地更改支持类型(例如,从std::vectorstd::list)或参数类型(例如,从intlong)。对于您自己的仅供内部使用的代码来说,这是不应该的。如果你正在开发其他人将要使用的代码,比如一个库,你不应该试图用这种方式使用typedef。如果你改变一个typedef,你所做的就是减少对你的应用编程接口的中断改变的可发现性。当然,使用它们来添加语义上下文,但不要使用它们来更改其他人依赖的代码中的基础类型。

如果您需要更改某个东西的类型,请记住,对函数参数的任何更改都是一个破坏性的更改,就像返回类型的更改或默认参数的添加一样。处理未来类型变化可能性的正确方法是使用抽象类或模板(哪个更合适,或者哪个更好,如果两者都适用的话)。这样,代码的公共接口不会改变,只有实现会改变。皮条客习语是保持稳定的 API 同时保留更改实现细节的自由的另一个好方法。我们将在后面的章节中探讨皮条客这个成语,它是“实现指针”的缩写。

这里有一个小代码块,说明了typedef的语法。

    class ExistingType;

    typedef ExistingType AliasForExistingType;

以下是显示如何使用typedef的简单示例。此示例的目的是说明typedef的简化但真实的用法。实际上,像这样的typedef会进入一个名称空间,然后包含在一个头文件中。由于我们还没有涉及到这些,这个例子故意保持简单。

范例:typedefsample \ typedefsample . CPP

    #include <iostream>
    #include <ostream>
    #include <vector>
    #include <algorithm>
    #include "../pchar.h"

    // This makes WidgetIdVector an alias for std::vector<int>, which has
    // more meaning than std::vector<int> would have, since now we know that
    // anything using this alias expects a vector of widget IDs
    // rather than a vector of integers.
    typedef std::vector<int> WidgetIdVector;

    bool ContainsWidgetId(WidgetIdVector idVector, int id)
    {
          return (std::end(idVector) !=
                std::find(std::begin(idVector), std::end(idVector), id)
                );
    }

    int _pmain(int /*argc*/, _pchar* /*argv*/[])
    {
          WidgetIdVector idVector;

          // Add some id numbers to the vector.
          idVector.push_back(5);
          idVector.push_back(8);

          // Output a result letting us know if the id is in the
          // WidgetIdVector.
          std::wcout << L"Contains 8: " <<
                (ContainsWidgetId(idVector, 8) ? L"true." : L"false.") <<
                std::endl;

          return 0;
    }