内置/固有/基本数据类型
在某些机器上,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==
。这是个好消息,因为如果 s1
和 s2
是 char*
类型,表达式 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[] a
来 delete
它。即使数组中的元素是内置类型。即使它们是 char
或 int
或 void*
类型。即使你不明白为什么。
如何在不循环的情况下判断一个整数是否是 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_ptr
或shared_ptr
。这对于多态对象很有用,因为它允许你实现按值返回的效果,而没有“切片”问题。性能需要根据具体情况进行评估。 - 其他 — 此列表仅作示例,并非排他性。换句话说,这只是一个起点,而不是终点。
墨菲定律基本保证了你的特定需求将落在最后一个要点上,而不是前面的任何一个要点上 。