内置类型

内置/固有/基本数据类型

在某些机器上,sizeof(char) 可以是 2 吗?例如,对于双字节字符呢?

不,sizeof(char) 始终是 1。始终。它从不是 2。从不,从不,从不。

即使你认为“字符”是一个多字节的东西,char 也不是。sizeof(char) 总是恰好是 1。绝无例外,永远如此。

听着,我知道这会让你头疼,所以请, 务必按顺序阅读接下来的几个常见问题,希望痛苦能在下周的某个时候消失。

sizeof 的单位是什么?

字节。

例如,如果 sizeof(Fred) 是 8,那么 Fred 对象数组中两个 Fred 对象之间的距离将是恰好 8 字节

再举一个例子,这意味着 sizeof(char) 是一个 字节。没错:一个字节。一个,一个,一个,恰好一个字节,永远一个字节。从不是两个字节。绝无例外。

哇,但是那些支持多字节字符的机器或编译器呢?你是说“字符”和 char 可能不同吗?!?

是的,没错:通常被称为“字符”的东西可能与 C++ 称之为 char 的东西不同。

如果这让你感到痛苦,我真的很抱歉,但请相信我,一次性解决所有痛苦更好。深吸一口气,跟我重复:“字符和 char 可能不同。” 看,是不是好受多了?没有?那继续读下去——情况会变得更糟。

但是,但是,但是那些 char 有多于 8 位的机器呢?你肯定不会说 C++ 字节可能有多于 8 位,是吗?!?

是的,没错:C++ 字节可能有多于 8 位。

C++ 语言保证一个字节必须总是至少有 8 位。但是,有些 C++ 实现的每个字节有超过 8 位。

好吧,我可以想象一台有 9 位字节的机器。但肯定不是 16 位字节或 32 位字节,对吧?

错了。

我听说过一个 C++ 实现有 64 位“字节”。你没看错:在该实现中,一个字节有 64 位。每个字节 64 位。64。就像 8 乘以 8。

是的,你没看错,结合以上,这意味着在该实现上,一个 char 将有 64 位。

我太混乱了。你能再解释一遍关于字节、char 和字符的规则吗?

以下是规则:

  • C++ 语言给程序员的印象是内存以 C++ 称之为“字节”的序列布局。
  • C++ 语言称之为字节的每个东西至少有 8 位,但可能有多于 8 位。
  • C++ 语言保证 char* (char 指针) 可以寻址单个字节。
  • C++ 语言保证两个字节之间没有位。这意味着内存中的每个位都是一个字节的一部分。如果你通过 char* 遍历内存,你将能够看到每一个位。
  • C++ 语言保证没有位是两个不同字节的一部分。这意味着对一个字节的更改绝不会导致对不同字节的更改。
  • C++ 语言为你提供了一种方法来找出你特定实现中一个字节有多少位:包含头文件 <climits>,然后每个字节的实际位数将由 CHAR_BIT 宏给出。

让我们举一个例子来说明这些规则。PDP-10 有 36 位字,没有硬件设施来寻址这些字中的任何内容。这意味着指针只能指向 36 位边界上的东西:指针不可能指向某个其他指针指向的位置的右边 8 位。

遵守上述所有规则的一种方法是让 PDP-10 C++ 编译器将“字节”定义为 36 位。另一种有效的方法是将“字节”定义为 9 位,并通过两个内存字来模拟 char*:第一个可以指向 36 位字,第二个可以是该字内的位偏移。在这种情况下,C++ 编译器在编译使用 char* 指针的代码时需要添加额外的指令。例如,为 *p = 'x' 生成的代码可能会将字读入寄存器,然后使用位掩码和位移来更改该字内适当的 9 位字节。int* 仍然可以作为单个硬件指针实现,因为 C++ 允许 sizeof(char*) != sizeof(int*)

使用相同的逻辑,也可以将 PDP-10 C++ 的“字节”定义为 12 位或 18 位。然而,上述技术不允许我们将 PDP-10 C++ 的“字节”定义为 8 位,因为 8×4 是 32,这意味着每隔 4 个字节我们将跳过 4 位。对于这 4 位,可以使用更复杂的方法,例如,将九个字节(每个 8 位)打包到两个相邻的 36 位字中。这里重要的是 memcpy() 必须能够看到内存的每一个位:两个相邻字节之间不能有任何位。

注意:PDP-10 上一种流行的非 C/C++ 方法是将 5 个字节(每个 7 位)打包到每个 36 位字中。然而,这在 C 或 C++ 中行不通,因为 5×7 是 35,这意味着使用 char* 遍历内存会每隔五个字节“跳过”一位(也因为 C++ 要求字节至少有 8 位)。

什么是“POD 类型”?

一种仅包含Plain Old Data(普通旧数据)的类型。

POD 类型是 C++ 类型,它在 C 中有等价物,并且使用与 C 相同的规则进行初始化、复制、布局和寻址。

例如,C 语言声明 `struct Fred x;` 不会初始化 `Fred` 变量 `x` 的成员。为了在 C++ 中实现相同的行为,`Fred` 需要*没有*任何构造函数。同样,为了使 C++ 版本的复制与 C 版本相同,C++ 的 `Fred` 必须没有重载赋值运算符。为了确保其他规则匹配,C++ 版本必须没有虚函数、基类、`private` 或 `protected` 的非静态成员,或者析构函数。但是,它可以有静态数据成员、静态成员函数和非静态非虚成员函数。

POD 类型的实际定义是递归的,并且有点棘手。这是一个**稍微**简化的 POD 定义:POD 类型的非静态数据成员必须是 `public` 的,并且可以是以下任何类型:`bool`、任何数字类型(包括各种 `char` 变体)、任何枚举类型、任何数据指针类型(即任何可转换为 `void*` 的类型)、任何函数指针类型,或任何 POD 类型,包括这些类型的数组。注意:数据指针和函数指针可以,但成员指针不行。另请注意,不允许使用引用。此外,POD 类型不能有构造函数、虚函数、基类或重载的赋值运算符。

初始化内置/固有/原始类型的非静态数据成员时,我应该使用“初始化列表”还是赋值?

为了对称性,通常最好在构造函数的“初始化列表”中初始化所有非静态数据成员,即使它们是内置/固有/原始类型。常见问题解答向你展示了原因和方法

初始化内置/固有/原始类型的静态数据成员时,我应该担心“静态初始化顺序问题”吗?

是的,如果您的内置/固有/原始变量是通过编译器不能完全在编译时评估的表达式来初始化的。常见问题解答提供了几种解决此(微妙!)问题的方法

我能否定义一个对内置/固有/原始类型起作用的运算符重载?

不,C++ 语言要求您的运算符重载至少接受一个“类类型”或枚举类型的操作数。C++ 语言不允许您定义一个所有操作数/参数都是原始类型的运算符。

例如,您不能定义一个接受两个 char* 并使用字符串比较的 operator==。这是个好消息,因为如果 s1s2char* 类型,表达式 s1 == s2 已经有一个明确的含义:它比较的是两个*指针*,而不是这两个指针所指向的两个字符串。您不应该使用指针。请使用 std::string 代替 char*

如果 C++ 允许你重新定义内置类型上运算符的含义,你就永远不知道 1 + 1 是什么:它将取决于包含了哪些头文件以及这些头文件是否重新定义了加法,例如,将其定义为减法。

当我 `delete` 一个内置/固有/原始类型的数组时,为什么我不能只说 `delete a` 而不是 `delete[] a`?

因为你不能。

听着,不要给我发邮件问我 C++ 为什么是这样。它就是这样。如果你真的想知道理由,请购买 Bjarne Stroustrup 的优秀著作《C++ 的设计与演化》(Addison-Wesley 出版社)。但是如果你真正的目标是写代码,不要浪费太多时间去弄清楚 C++ 为什么有这些规则,而是遵循它的规则即可。

所以规则是这样的:如果 a 指向一个通过 new T[n] 分配的“东西”数组,那么你必须,必须,必须通过 delete[] adelete 它。即使数组中的元素是内置类型。即使它们是 charintvoid* 类型。即使你不明白为什么。

如何在不循环的情况下判断一个整数是否是 2 的幂?

inline bool isPowerOf2(int i)
{
  return i > 0 && (i & (i - 1)) == 0;
}

函数应该返回什么?

实践中,情况有很多种。以下是一些随意排列的例子:

  • void — 如果你不需要返回值,就不要返回。
  • 按值返回局部变量——这是最简单的,并且稍加注意,NRVO(命名返回值优化)可以最大限度地提高性能。
  • 按指针或引用返回局部变量——不行!请不要这样做。
  • 按值返回数据成员——如果函数是非静态成员函数,并且数据成员可以相对快速地复制,例如 int,则这是极好的选择。如果数据成员复制速度慢,那么如果你在 CPU 密集型应用程序的内循环中调用此成员函数,这将产生性能损失。
  • 按指针返回数据成员——可以,但要确保你不想按引用返回它,并且如果你不希望调用者修改数据成员,请确保使用 const Foo*Foo const*。由于调用者可能会存储指针而不是复制数据成员,因此你应该在成员函数的“契约”中警告调用者,他们不得在 this 对象销毁后使用返回的指针。
  • 按非常量引用返回数据成员——可以,但这允许调用者在你的类“看不到”更改的情况下更改你对象的数据成员。如果你有一个更改此数据成员的“set”方法,请改用常量引用或按值返回。另一点:由于调用者可能会存储引用而不是复制数据成员,因此你应该在成员函数的“契约”中警告调用者,他们不得在 this 对象销毁后使用返回的引用。
  • 按常量引用返回数据成员——可以,但这确实允许你的用户看到你成员变量的数据类型。这意味着,如果你需要更改成员变量的类型,这种更改可能会破坏使用你类的代码,而这正是封装的主要目的之一。你可以通过为该成员变量的类型(以及因此为常量引用返回值的类型)暴露一个 public typedef 来缓解这种风险,并警告你的用户应该使用 typedef 而不是原始的基础类型。另一个现实是,如果调用者捕获此引用,而不是复制对象,那么底层引用对象可能会在“调用者不知情的情况下”发生变化,即使类型是常量引用。由于许多程序员对此感到惊讶,明智的做法是在成员函数的“契约”中警告调用者。你还应该警告调用者一旦 this 对象销毁,就丢弃返回的引用。
  • 指向通过 new 分配的成员的 shared_ptr — 这与按指针或引用返回成员具有非常相似的权衡;请参阅那些要点以了解权衡。优点是调用者可以在 this 对象销毁后合法地持有并使用返回的指针。
  • freestore-allocated 数据副本的局部 unique_ptrshared_ptr。这对于多态对象很有用,因为它允许你实现按值返回的效果,而没有“切片”问题。性能需要根据具体情况进行评估。
  • 其他 — 此列表仅作示例,并非排他性。换句话说,这只是一个起点,而不是终点。

墨菲定律基本保证了你的特定需求将落在最后一个要点上,而不是前面的任何一个要点上 SMILE!