C++ 类型系统

在 C++ 中,类型的概念非常重要。 每个变量、函数自变量和函数返回值必须具有一个类型以进行编译。 此外,在计算表达式前,编译器会隐式给出每个表达式(包括文本值)的类型。 类型的一些示例包括内置类型,例如用于存储整数值的 int,用于存储浮点值的 double,或用于存储文本的标准库类型,如类 std::basic_string。 可以通过定义 classstruct 创建自己的类型。 该类型指定为变量(或表达式结果)分配的内存量。 该类型还指定可存储的值类型、编译器如何解释这些值中的位模式以及可以对它们执行的操作。 本文包含对 C++ 类型系统的主要功能的非正式概述。

术语

标量类型:包含定义范围的单个值的类型。 标量包括算术类型(整型或浮点值)、枚举类型成员、指针类型、指针到成员类型以及 std::nullptr_t。 基本类型通常是标量类型。

复合类型:不是标量类型的类型。 复合类型包括数组类型、函数类型、类(或结构)类型、联合类型、枚举、引用和指向非静态类成员的指针。

变量:数据量的符号名称。 该名称可用于访问它在整个定义代码范围内引用的数据。 在 C++ 中,变量通常用于指标量数据类型的实例,而其他类型的实例通常称为对象

对象:为了简洁一致,本文使用术语“对象”指代类或结构的任何实例。 在一般意义上使用时,它包括所有类型,甚至标量变量。

POD 类型(纯旧数据):C++ 中的此类非正式数据类型类别是指作为标量(参见基础类型部分)的类型或 POD 类。 POD 类没有不是 POD 的静态数据成员,没有用户定义的构造函数、用户定义的析构函数或用户定义的赋值运算符。 此外,POD 类无虚函数、基类、私有的或受保护的非静态数据成员。 POD 类型通常用于外部数据交换,例如与用 C 语言编写的模块(仅具有 POD 类型)进行的数据交换。

指定变量和函数类型

C++ 既是强类型语言,也是静态类型语言;每个对象都有一个类型,并且该类型永远不会更改。 在代码中声明变量时,你必须显式指定其类型或使用 auto 关键字指示编译器通过初始值设定项推断类型。 在代码中声明函数时,必须指定其返回值的类型以及每个自变量的类型。 如果函数未返回任何值,则使用返回值类型 void。 例外情况是,当使用允许任意类型自变量的函数模板时。

在你首次声明变量后,稍后无法更改其类型。 但是,你可以将变量的值或函数的返回值复制到其他类型的另一个变量中。 此类操作称作“类型转换”,这些操作有时很必要,但也是造成数据丢失或不正确的潜在原因。

在声明 POD 类型的变量时,强烈建议你将其初始化,也就是为其指定初始值。 在初始化某个变量之前,该变量会有一个“垃圾”值,该值包含之前正好位于该内存位置的位数。 这是需要注意的 C++ 的一个重要方面,尤其是当你使用另一种语言来处理初始化时。 如果声明非 POD 类类型的变量,则构造函数会处理初始化。

下面的示例演示了一些简单变量声明,并分别对它们进行了说明。 该示例还演示了编译器如何使用类型信息允许或禁止对变量进行某些后续操作。

int result = 0;              // Declare and initialize an integer.
double coefficient = 10.8;   // Declare and initialize a floating
                             // point value.
auto name = "Lady G.";       // Declare a variable and let compiler
                             // deduce the type.
auto address;                // error. Compiler cannot deduce a type
                             // without an intializing value.
age = 12;                    // error. Variable declaration must
                             // specify a type or use auto!
result = "Kenny G.";         // error. Can't assign text to an int.
string result = "zero";      // error. Can't redefine a variable with
                             // new type.
int maxValue;                // Not recommended! maxValue contains
                             // garbage bits until it is initialized.

基本(内置)类型

不同于某些语言,C++ 中不存在派生所有其他类型的通用基类型。 该语言包括许多基本类型(也称为“内置类型”)。 这些类型包括数值类型,例如 intdoublelongbool,以及分别针对 ASCII 和 UNICODE 字符的 charwchar_t 类型。 大多数基础类型(booldoublewchar_t 和相关类型除外)都具有 unsigned 版本,这些版本修改了变量可存储的值的范围。 例如,int 存储 32 位已签名整数,可表示介于 -2,147,483,648 和 2,147,483,647 之间的值。 unsigned int 也存储为 32 位,它可存储介于 0 和 4,294,967,295 之间的值。 可能的值的总数在每种情况下都相同;仅范围不同。

编译器可识别这些内置类型,并具有内置规则,用于控制可对其执行的操作,以及如何将其转换为其他基本类型。 有关内置类型及其大小和数值限制的完整列表,请参阅内置类型

下图显示了 Microsoft C++ 实现中内置类型的相对大小:

Diagram of the relative size in bytes of several built in types.

下表列出了 Microsoft C++ 实现中最常使用的基本类型及其大小:

类型 大小 注释
int 4 个字节 整数值的默认选择。
double 8 字节 浮点值的默认选择。
bool 1 个字节 表示可为 true 或 false 的值。
char 1 个字节 用于早期 C 样式字符串或 std:: 字符串对象中无需转换为 UNICODE 的 ASCII 字符。
wchar_t 2 个字节 表示可能以 UNICODE 格式进行编码的“宽”字符值(Windows 上为 UTF-16,其他操作系统上可能不同)。 wchar_tstd::wstring 类型字符串中使用的字符类型。
unsigned char 1 个字节 C++ 没有内置字节类型。 使用 unsigned char 来表示字节值。
unsigned int 4 个字节 位标志的默认选项。
long long 8 字节 表示更大的整数值范围。

其他 C++ 实现可能对某些数值类型使用不同的大小。 若要详细了解 C++ 标准所需的大小和大小关系,请参阅内置类型

void 类型

void 类型是一种特殊类型;不能声明 void 类型的变量,但可声明 void * 类型的变量(指向 void 的指针),有时当分配原始(非类型化)内存时这样做很有必要。 但是,指向 void 的指针不是类型安全的指针,建议不要在新式 C++ 中使用它们。 在函数声明中,void 返回值意味着该函数不返回值;将其用作返回类型是 void 的常见且可接受的用法。 尽管 C 语言需要含零个参数的函数以在参数列表中声明 void(例如 fn(void)),但新式 C++ 中建议不要这样做,而是应声明无参数函数 fn()。 有关详细信息,请参阅类型转换和类型安全性

const 类型限定符

任何内置或用户定义的类型都可由 const 关键字限定。 此外,成员函数可受到 const 限定,甚至可重载 constconst 类型的值在初始化后将无法修改。

const double PI = 3.1415;
PI = .75; //Error. Cannot modify const variable.

const 限定符在函数和变量声明中使用广泛,而“常量正确性”是 C++ 中的一个重要概念;它实质上表示使用 const 来确保在编译时不会无意中修改值。 有关详细信息,请参阅 const

const 类型与其非 const 版本截然不同;例如,const int 是与 int 截然不同的类型。 如果发生必须从变量中移除常量性的这类少数情况,可使用 C++ const_cast 运算符。 有关详细信息,请参阅类型转换和类型安全性

字符串类型

严格来说,C++ 语言没有内置的字符串类型;charwchar_t 存储单个字符 - 必须声明这些类型的数组来估计字符串,从而将一个终止 null 值(例如,ASCII '\0')添加到最后一个有效字符数后的数组元素(也称为“C 样式字符串”)。 C 样式字符串需要编写更多的代码或者需要使用外部字符串实用工具库函数。 但是,在新式 C++ 中,我们具有标准库类型 std::string(用于 8 位 char 型字符串)或 std::wstring(用于 16 位 wchar_t 型字符串)。 这些 C++ 标准库容器可被视为本机字符串类型,因为它们是所有兼容 C++ 生成环境包含的标准库的一部分。 使用 #include <string> 指令使这些类型在你的程序中可用。 (如果使用的是 MFC 或 ATL,还可使用 CString 类,但其不符合 C++ 标准。)建议不要在新式 C++ 中使用 null 终止字符数组(前面提到的 C 样式字符串)。

用户定义类型

在定义 classstructunionenum 时,该构造会在代码的其余部分使用,如同它是一个基础类型一样。 它具有内存的已知大小以及一些有关可以如何在程序生命期内将其用于编译时检查和运行时的规则。 基本内置类型和用户定义的类型之间的主要区别如下:

  • 编译器没有用户定义的类型的内置知识。 它在编译过程中首次遇到此定义时就学习了此类型。

  • 通过定义(通过重载)适当的运算符作为类成员或非成员函数,可以指定可对你的类型执行的操作以及你的类型转换为其他类型的方式。 有关详细信息,请参阅函数重载

指针类型

与早期版本的 C 语言一样,C++ 继续通过使用特殊声明符 *(星号)声明指针类型的变量。 指针类型在存储实际数据值的内存中存储位置地址。 在新式 C++ 中,这些指针类型称为原始指针,它们通过特殊运算符在代码中访问:*(星号)或 ->(带大于号的短划线,通常称为箭头)。 此内存访问操作称为取消引用。 所使用的运算符取决于是取消引用指向标量指针的指针,还是取消引用指向对象中成员的指针。

使用指针类型很长时间以来都是 C 和 C++ 程序开发的最具挑战性和最难以理解的方面之一。 本节概述了一些事实和实践,以帮助你在需要时使用原始指针。 但是,在新式 C++ 中,由于智能指针的演变(在本部分末尾进行了更多讨论),因此不再需要(或建议)将原始指针用于对象所有权。 使用原始指针来观察对象仍然是有用和安全的。 但是,如果必须将其用于对象所有权,则需要谨慎操作,并仔细考虑如何创建和销毁其拥有的对象。

首先应知道的是,原始指针变量声明只分配足够的内存来存储地址:指针在取消引用时引用的内存位置。 指针声明不会分配存储数据值所需的内存。 (该内存也称为后备存储。)换言之,通过声明原始指针变量,将创建内存地址变量而非实际数据变量。 如果在确保指针变量包含后备存储的有效地址前取消对其的引用,则将导致程序发生未定义行为(通常为严重错误)。 下面的示例演示了此种错误:

int* pNumber;       // Declare a pointer-to-int variable.
*pNumber = 10;      // error. Although this may compile, it is
                    // a serious error. We are dereferencing an
                    // uninitialized pointer variable with no
                    // allocated memory to point to.

该示例取消引用指针类型,未分配用于存储实际整数数据的任何内存或向其分配有效内存地址。 下面的代码更正这些错误:

    int number = 10;          // Declare and initialize a local integer
                              // variable for data backing store.
    int* pNumber = &number;   // Declare and initialize a local integer
                              // pointer variable to a valid memory
                              // address to that backing store.
...
    *pNumber = 41;            // Dereference and store a new value in
                              // the memory pointed to by
                              // pNumber, the integer variable called
                              // "number". Note "number" was changed, not
                              // "pNumber".

已纠正的代码示例使用本地堆栈内存创建 pNumber 指向的后备存储。 我们使用基本类型,以求简单。 实际上,指针的后备存储通常是用户定义类型,这些类型通过使用 new 关键字表达式(在 C 样式编程中,使用旧的 malloc() C 运行时库函数)动态分配到称为“”(或“可用存储”)的内存区域中。 分配后,这些变量通常指“对象”,尤其是基于类定义这些变量的情况下。 使用 new 分配的内存必须由相应的 delete 语句删除(如果使用 malloc() 函数进行关联,则使用 C 运行时函数 free() 执行删除操作)。

但是,很容易忘记删除动态分配的对象(特别是在复杂代码中),这会导致产生名为“内存泄漏”的资源 Bug。 为此,建议你不要在新式 C++ 中使用原始指针。 在智能指针中包装原始指针几乎总是更好的,该指针在调用其析构函数时自动释放内存。 (也就是说,当代码超出智能指针的范围时。)通过使用智能指针,几乎消除了 C++ 程序中的一系列 bug。 在下面的示例中,假定 MyClass 是具有公共方法 DoSomeWork(); 的用户定义的类型

void someFunction() {
    unique_ptr<MyClass> pMc(new MyClass);
    pMc->DoSomeWork();
}
  // No memory leak. Out-of-scope automatically calls the destructor
  // for the unique_ptr, freeing the resource.

有关智能指针的详细信息,请参阅智能指针

有关指针转换的详细信息,请参阅类型转换和类型安全性

有关指针的一般性详细信息,请参阅指针

Windows 数据类型

在 C 和 C++ 的经典 Win32 编程中,大多数函数使用 Windows 特定的 Typedef 和 #define 宏(在 windef.h 中定义)来指定参数类型和返回值。 这些 Windows 数据类型通常是为 C/C++ 内置类型提供的特殊名称(别名)。 有关这些 typedef 和预处理器定义的完整列表,请参阅 Windows 数据类型。 其中一些 typedef(例如 HRESULTLCID)很有用且具有描述性。 INT 等其他类型没有特殊含义,只是基础 C++ 类型的别名。 其他 Windows 数据类型的名称自 C 编程和 16 位处理器得到保留,并且在现代硬件或操作系统中不具有目的和意义。 还有与 Windows 运行时库相关的特定数据类型,它们在列表中显示为 Windows 运行时基础数据类型。 在新式 C++ 中,一般准则是首选 C++ 基本类型,除非 Windows 类型传达一些有关如何解释值的额外意义。

更多信息

有关 C++ 类型系统的详细信息,请参阅以下文章。

值类型
描述值类型以及与其使用相关的问题。

类型转换和类型安全
描述常见类型转换问题并说明如何避免这些问题出现。

另请参阅

欢迎回到 C++
C++ 语言参考
C++ 标准库