4.1 数组
数组 (Array) 是一种复合类型 (Compound Type)**,它允许你存储多个相同类型的值。数组中的每个值称为一个元素 (Element),可以通过索引 (Index)** 或下标 (Subscript) 来访问特定元素。
4.1.1 程序说明
想象一下,你需要存储一年中每个月的销售额。你可以声明12个独立的 double
变量:
1 | double salesJan, salesFeb, salesMar, /* ..., */ salesDec; |
这种方法非常繁琐,尤其是在需要处理大量数据时。数组提供了一种更简洁、更强大的方式来处理这种情况。
数组声明:
声明一个数组需要指定:
- 元素类型: 数组中存储的数据类型。
- 数组名称: 变量名。
- 数组大小: 数组可以容纳的元素数量,必须是一个常量表达式(在编译时就能确定其值的表达式,例如字面常量、
const
常量、枚举量或sizeof
表达式的结果),并且必须放在方括号[]
内。
语法:
1 | typeName arrayName[arraySize]; |
示例:
1 | // 声明一个可以存储 12 个 double 类型值的数组,名为 monthlySales |
访问数组元素:
使用数组名和方括号内的索引来访问数组元素。C++数组的索引从 0 开始。对于大小为 N
的数组,有效的索引范围是 0
到 N-1
。
1 | // 访问 monthlySales 数组的第一个元素 (一月) |
重要: C++ 不会自动检查数组索引是否越界。访问 arrayName[N]
或 arrayName[-1]
(对于大小为 N
的数组)是**未定义行为 (Undefined Behavior)**,可能导致程序崩溃或数据损坏。程序员有责任确保使用的索引在有效范围内 (0
到 arraySize - 1
)。
数组大小必须是常量表达式:
1 | int n = 10; |
如果需要在运行时确定数组大小,应该使用动态内存分配(new
)或标准库提供的容器(如 std::vector
),我们将在后续章节学习。
4.1.2 数组的初始化规则
在声明数组时,可以同时对其进行初始化。初始化使用花括号 {}
括起来的**初始化列表 (Initializer List)**。
规则:
- 完整初始化: 提供与数组大小相同数量的初始值。
1
2int scores[5] = {90, 85, 92, 78, 88}; // scores[0]=90, scores[1]=85, ..., scores[4]=88
double lengths[3] = {1.2, 3.4, 0.5}; - 部分初始化: 如果提供的初始值数量少于数组大小,则剩余的元素会被自动初始化为 0(对于数值类型)或相应的零等价值(对于其他类型,如字符数组的空字符
\0
)。1
2
3
4
5
6
7int counts[10] = {1, 2, 3}; // counts[0]=1, counts[1]=2, counts[2]=3
// counts[3] 到 counts[9] 都被初始化为 0
float readings[5] = {9.8f}; // readings[0]=9.8f, 其他元素为 0.0f
// 将数组所有元素初始化为 0 的常用方法
int allZeros[100] = {0}; - 省略数组大小: 如果在声明时提供了初始化列表,可以省略方括号中的数组大小。编译器会根据初始化列表中的元素数量自动推断数组大小。
1
2short values[] = {10, 20, 30, 40}; // 编译器推断数组大小为 4
char message[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 大小为 6 (包括空字符) - 不允许初始化列表元素过多: 初始化列表中的元素数量不能超过数组声明的大小。
1
// int errors[3] = {1, 2, 3, 4}; // 错误! 初始化列表元素过多
- 未初始化数组: 如果在声明数组时没有提供初始化列表(仅适用于非静态局部数组),则数组元素的值是未定义的 (indeterminate)**,它们会包含内存中遗留的垃圾值。使用未初始化的变量是常见的错误来源。** (静态存储持续性的变量,如全局变量、命名空间变量、静态局部变量,会被默认零初始化)
1
2
3
4
5
6
7
8int main() {
int garbage[5]; // 数组元素的值是未定义的 (垃圾值)
// std::cout << garbage[0]; // 错误! 使用未初始化的值
static int staticGarbage[5]; // 静态存储数组会被默认初始化为 0
// std::cout << staticGarbage[0]; // 输出 0
return 0;
}
用法与示例:
1 |
|
4.1.3 C++11数组初始化方法
C++11 引入了更统一的初始化语法,称为列表初始化 (List Initialization) 或**花括号初始化 (Brace Initialization)**,它也可以用于数组。
主要变化:
- 可以省略等号
=
: 在使用初始化列表时,可以省略声明语句中的等号。 - 禁止缩窄转换 (Narrowing Conversion): 列表初始化不允许可能导致数据丢失的“缩窄”转换。例如,不能将浮点数直接初始化给整型数组元素,也不能将超出范围的整数值初始化给较小范围的整型数组元素。
语法:
1 | typeName arrayName[arraySize] {initializer_list}; // C++11 列表初始化 (可省略等号) |
用法与示例:
1 |
|
建议: C++11 的列表初始化提供了更一致、更安全的初始化方式,推荐在支持 C++11 及更高标准的项目中使用。特别是 typeName arrayName[size] {};
这种将所有元素初始化为零值的形式非常方便。
4.2 字符串
字符串是程序中用于表示文本信息的重要数据类型。C++处理字符串有两种主要方式:
- C风格字符串 (C-Style String): 这是继承自C语言的方式,将字符串视为存储在
char
数组中并以空字符 (\0
) 结尾的字符序列。 -
string
类: C++标准库提供了一个强大的string
类,提供了更方便、更安全的字符串操作(将在 4.3 节介绍)。
本节主要关注 C 风格字符串。
字符串字面值 (String Literal) 或字符串常量 (String Constant):
在代码中用双引号 ""
括起来的字符序列,例如 "Hello, world!"
, "C++"
, ""
(空字符串)。它们存储在内存的只读区域。编译器会自动在字符串字面值的末尾添加空字符 \0
。
4.2.1 拼接字符串常量
C++允许将相邻的字符串字面值自动拼接(连接)成一个单独的字符串。这对于将较长的字符串分成多行书写非常有用,可以提高代码的可读性。
用法与示例:
1 |
|
4.2.2 在数组中使用字符串
C风格字符串本质上是 char
类型的数组,其特殊之处在于最后一个字符必须是**空字符 (\0
)**。这个空字符标记了字符串的实际结束位置。
声明和初始化:
可以使用字符串字面值来初始化 char
数组。编译器会自动计算大小(包括末尾的 \0
)并将其复制到数组中。
1 |
|
关键点:
- 存储 C 风格字符串的
char
数组大小必须至少是字符串长度加 1(为\0
留出空间)。 - 字符串字面值初始化会自动添加
\0
。 - 列表初始化需要手动添加
\0
。 -
strlen()
计算的是到\0
为止的字符数。 -
sizeof()
计算的是整个数组的字节大小。
4.2.3 字符串输入
使用 cin
和 >>
运算符读取 C 风格字符串(存储在 char
数组中)时,存在一个主要限制:cin
默认以空白字符(空格、制表符、换行符)作为输入的分隔符。这意味着 cin >>
只会读取到第一个空白字符之前的部分。
用法与示例:
1 |
|
运行示例及问题:
如果用户输入:
1 | Enter your first name: Ada Lovelace |
程序输出将会是:
1 | Hello, Ada! |
原因:
-
cin >> name;
读取到 “Ada” 后遇到空格停止,”Ada” 被存入name
数组(并自动添加\0
)。 - “ Lovelace\nChocolate Cake\n” 仍然留在输入缓冲区中。
-
cin >> dessert;
从缓冲区开始读取,跳过开头的空格,读取到 “Lovelace” 后遇到换行符停止,”Lovelace” 被存入dessert
数组。
这显然不是我们期望的结果。cin >>
不适合读取包含空格的字符串。此外,如果用户输入的单词长度超过了数组的大小(减去 \0
的空间),还会导致**缓冲区溢出 (Buffer Overflow)**,这是严重的安全隐患。
4.2.4 每次读取一行字符串输入
为了解决 cin >>
的问题,iostream
库提供了其他成员函数来读取整行输入,包括其中的空格,直到遇到换行符为止。常用的有两个:getline()
和 get()
。
1. cin.getline(char* buffer, int size, char delimiter = '\n')
-
buffer
: 用于存储输入的char
数组。 -
size
: 缓冲区的大小。getline()
最多读取size - 1
个字符,以确保有空间存放末尾的空字符\0
。 -
delimiter
(可选): 指定读取停止的分隔符,默认为换行符\n
。
行为:
- 读取字符到
buffer
中,直到读取了size - 1
个字符、遇到delimiter
或到达文件末尾。 - 如果遇到
delimiter
,它会读取并丢弃该分隔符(通常是换行符)。 - 总是在读取的字符序列末尾添加空字符
\0
。
2. cin.get(char* buffer, int size, char delimiter = '\n')
- 参数与
getline()
类似。
行为:
- 读取字符到
buffer
中,直到读取了size - 1
个字符、遇到delimiter
或到达文件末尾。 - 与
getline()
不同的是,如果遇到delimiter
,它不会读取该分隔符,而是将其留在输入缓冲区中。 - 总是在读取的字符序列末尾添加空字符
\0
。
用法与示例:
1 |
|
选择 getline()
还是 get()
?
-
getline()
通常更方便,因为它会自动处理掉行尾的换行符,使得连续读取多行输入更简单。 -
get()
提供了更精细的控制,因为它允许你检查下一个字符是否是换行符,但需要你手动处理留在缓冲区的分隔符。
空行和 getline()
: 如果 getline()
遇到空行(即用户直接按 Enter),它会读取这个空行,将一个空字符串(只包含 \0
)存入缓冲区,并丢弃换行符。
4.2.5 混合输入字符串和数字
当程序需要交替读取数字(使用 cin >>
)和整行字符串(使用 cin.getline()
或 cin.get()
)时,经常会遇到一个问题:cin >>
读取数字后,会将数字后面的换行符留在输入缓冲区中。
如果紧接着调用 cin.getline()
或 cin.get()
,它们会立即读到这个残留的换行符,并认为已经到达行尾,导致读取失败或读到空字符串。
问题示例:
1 |
|
解决方法:
在读取数字后、调用 getline()
或 get()
读取整行之前,需要消耗掉输入缓冲区中残留的换行符。
- 使用
cin.ignore()
: 这是常用的方法。cin.ignore(n, delim)
会跳过输入流中的字符,直到跳过了n
个字符,或者遇到了delim
分隔符(并丢弃该分隔符),以先到者为准。通常用于丢弃换行符:1
2
3std::cin.ignore(100, '\n'); // 跳过最多100个字符,直到并包括下一个换行符
// 或者更简单地,如果确定只有一个换行符需要丢弃
// std::cin.ignore(); // 跳过下一个字符 (即换行符) - 使用
(cin >> ws)
: C++11 引入了std::ws
输入流操纵符,它可以读取并丢弃输入流开头的所有空白字符(包括换行符)。1
(std::cin >> std::ws).getline(name, 50);
- 使用
cin.get()
读取单个字符:1
std::cin.get(); // 读取并丢弃换行符
修正后的示例 (使用 cin.ignore()
):
1 |
|
修正后的示例 (使用 ws
):
1 |
|
总结: 混合输入数字和整行字符串时,务必记得在 cin >> number;
之后、调用 getline()
或 get()
之前,清除输入缓冲区中残留的换行符。使用 cin.ignore()
或 (cin >> ws)
是推荐的做法。
4.3 string类简介
虽然 C 风格字符串(字符数组)在 C++ 中仍然可用,但 C++ 标准库提供了一个更强大、更方便、更安全的替代品:std::string
类。
string
类是标准库的一部分,它封装了字符序列的操作,提供了自动内存管理和丰富的成员函数来处理字符串。要使用 string
类,需要包含 <string>
头文件。
基本概念:
- 对象:
string
类型的变量是**对象 (Object)**。对象是类的实例。 - 自动内存管理: 与需要手动管理内存(确保数组足够大,处理空字符)的 C 风格字符串不同,
string
对象会自动处理内存分配和释放。它可以根据需要动态增长或缩小。 - 成员函数:
string
类提供了许多内置的操作(成员函数),如获取长度、拼接、查找、替换等,使得字符串处理更加容易。
基本用法:
1 |
|
4.3.1 C++11字符串初始化
C++11 引入的列表初始化(花括号初始化)也可以用于 string
对象,其行为类似于使用 C 风格字符串字面值进行初始化。
语法:
1 |
|
注意: 直接使用字符列表 { 'a', 'b', 'c' }
来初始化 std::string
在 C++11/14 中通常不会按预期工作,因为它会尝试查找接受 std::initializer_list<char>
的构造函数,而标准 std::string
没有这样的构造函数。它通常会被解释为尝试调用接受 C 风格字符串 ( const char*
) 的构造函数,但这需要列表恰好能形成一个有效的 C 风格字符串(例如,包含 \0
)。
最常用和清晰的初始化方式仍然是使用字符串字面值或另一个 string
对象。
用法与示例:
1 |
|
4.3.2 赋值、拼接和附加
string
类重载了常见的运算符,使得赋值、拼接和附加操作非常直观。
- 赋值 (
=
): 可以将一个string
对象、一个 C 风格字符串字面值或一个char
赋给一个string
对象。 - 拼接 (
+
): 可以使用+
运算符将两个string
对象、string
对象和 C 风格字符串字面值、或者string
对象和char
拼接起来,生成一个新的string
对象。注意:不能直接拼接两个 C 风格字符串字面值,至少有一个操作数需要是string
对象。 - 附加 (
+=
): 可以使用+=
运算符将一个string
对象、一个 C 风格字符串字面值或一个char
附加到现有string
对象的末尾(修改原字符串)。
用法与示例:
1 |
|
4.3.3 string类的其他操作
string
类提供了大量成员函数来执行各种字符串操作。以下是一些常用的操作:
- 获取长度/大小:
-
size()
或length()
: 返回字符串中的字符数(两者功能相同)。
-
- 检查是否为空:
-
empty()
: 如果字符串为空,返回true
,否则返回false
。
-
- 访问字符:
-
[]
运算符: 像数组一样通过索引访问字符(不进行边界检查)。 -
at()
: 通过索引访问字符(进行边界检查,如果越界会抛出std::out_of_range
异常)。
-
- 查找:
-
find()
: 查找子字符串或字符首次出现的位置,返回索引;如果未找到,返回std::string::npos
(一个特殊的静态成员常量)。 -
rfind()
: 从后向前查找。 -
find_first_of()
,find_last_of()
,find_first_not_of()
,find_last_not_of()
: 查找字符集中的任意字符或非任意字符。
-
- 子字符串:
-
substr(pos, count)
: 返回从位置pos
开始,长度为count
的子字符串。
-
- 比较:
-
compare()
: 比较字符串(字典序),返回负数、零或正数。 - 重载的关系运算符 (
==
,!=
,<
,>
,<=
,>=
): 可以直接比较string
对象。
-
- 修改:
-
insert()
: 在指定位置插入字符或字符串。 -
erase()
: 删除指定位置和数量的字符。 -
replace()
: 替换指定范围的字符。 -
clear()
: 清空字符串。 -
append()
: 等同于+=
。
-
用法与示例:
1 |
|
4.3.4 string类I/O
可以使用标准的输入输出流对象 cin
和 cout
来方便地读写 string
对象。
- 输出 (
cout <<
):<<
运算符被重载,可以直接将string
对象输出到cout
。 - 输入 (
cin >>
):>>
运算符被重载,可以从cin
读取一个单词(以空白符——空格、制表符、换行符分隔)到string
对象中。它会自动跳过开头的空白符,然后在遇到下一个空白符时停止读取。 - 读取整行 (
getline()
): 如果需要读取包含空格的整行文本,应该使用getline()
函数(这是一个全局函数,不是string
的成员函数)。-
getline(cin, str)
: 从cin
读取一行(直到遇到换行符\n
),并将内容(不包括换行符)存储到string
对象str
中。 -
getline(cin, str, delimiter)
: 读取直到遇到指定的delimiter
字符为止。
-
用法与示例:
1 |
|
注意: 在混合使用 cin >>
和 getline(cin, ...)
时要特别小心。cin >>
读取单词后,会将换行符留在输入缓冲区中。如果紧接着调用 getline()
,它会立即读到这个换行符并认为读取结束,导致得到一个空字符串。通常需要在 cin >>
之后、getline()
之前清除缓冲区中的换行符,例如使用 std::cin.ignore()
。
4.3.5 其他形式的字符串字面值
C++11 引入了新的字符串字面值形式,提供了对不同字符编码(如 Unicode)的更好支持。
原始字符串字面值 (Raw String Literal):
- 语法:
R"delimiter(raw_characters)delimiter"
-
delimiter
是一个可选的、最多16个字符的序列(不能包含空格、括号、反斜杠)。 -
raw_characters
是字符串内容,其中的反斜杠\
和引号"
等特殊字符不会被转义,按原样解释。 - 主要用于书写包含大量特殊字符的字符串,如正则表达式、文件路径、HTML/XML代码等,避免大量的反斜杠转义。
- 示例:
R"(C:\Program Files\)"
,R"delimiter(String with "quotes" and \backslashes)delimiter"
- 语法:
Unicode 字符串字面值:
-
u8"string"
: UTF-8 编码的字符串 (类型是const char[]
,但应存储在std::string
或处理 UTF-8 的地方)。 -
u"string"
: UTF-16 编码的字符串 (类型是const char16_t[]
)。 -
U"string"
: UTF-32 编码的字符串 (类型是const char32_t[]
)。
-
用法与示例:
1 |
|
原始字符串字面值在处理包含特殊字符的文本时非常方便。Unicode 字符串字面值则为处理国际化文本提供了标准化的基础。
4.4 结构简介
数组允许我们存储多个相同类型的数据。但有时我们需要将不同类型的数据组合成一个单一的、有意义的单元。例如,描述一件商品可能需要商品名称(字符串)、数量(整数)和单价(浮点数)。C++ 的结构 (Structure) 就提供了这种能力。
结构是一种用户定义的复合类型,它允许将多个不同类型的数据项(称为成员 (member) 或**字段 (field)**)捆绑在一起,形成一个新的数据类型。
4.4.1 在程序中使用结构
使用结构通常涉及以下步骤:
- 定义结构: 使用
struct
关键字定义一个新的结构类型,并在花括号{}
内声明其成员。结构定义通常放在main()
函数之前或单独的头文件中。 - 声明结构变量: 使用定义好的结构类型名来声明变量。
- 访问结构成员: 使用成员运算符(点运算符
.
) 来访问结构变量的特定成员。
结构定义语法:
1 | struct StructureName { |
用法与示例:
1 |
|
-
struct Inflatable { ... };
: 定义了一个名为Inflatable
的新类型。 -
Inflatable product1;
: 创建了一个Inflatable
类型的变量(对象)。 -
product1.name = ...;
: 使用点运算符访问product1
的name
成员并赋值。
4.4.2 C++11结构初始化
C++11 引入的列表初始化(花括号初始化)也适用于结构体,提供了更灵活、更安全的初始化方式。
特点:
- 可以省略等号
=
: 与数组类似,可以在初始化时省略等号。 - 可以按成员顺序初始化:
StructType var {value1, value2, ...};
- 可以初始化部分成员 (C++20 designated initializers): C++20 允许通过指定成员名进行初始化,可以不按顺序或只初始化部分成员。但在 C++11/14/17 中,通常需要按顺序提供值。
- 空花括号初始化:
StructType var {};
会将所有成员进行零初始化(数值类型为0,指针为nullptr
,bool
为false
,类类型会调用默认构造函数)。 - 禁止缩窄转换: 与数组一样,列表初始化不允许可能丢失信息的缩窄转换。
用法与示例:
1 |
|
4.4.3 结构可以将string类作为成员吗
是的,绝对可以。 正如在 4.4.1
和 4.4.2
的示例中看到的 (Inflatable
和 Product
结构),std::string
对象可以像 int
、double
或其他任何类型一样作为结构的成员。
这使得结构能够方便地包含文本信息,并利用 string
类提供的所有功能(自动内存管理、拼接、查找等)。
示例回顾:
1 |
|
4.4.4 其他结构属性
结构在 C++ 中具有一些方便的属性:
- 赋值 (Assignment): 可以使用赋值运算符
=
将一个结构变量的值赋给同类型的另一个结构变量。这会执行**成员逐一复制 (memberwise copy)**,即将源结构每个成员的值复制到目标结构对应成员中。1
2
3Student s1 = {"Charlie", 111, 3.9};
Student s2;
s2 = s1; // s2 的 name, studentID, gpa 都被设置为 s1 的值 - 作为函数参数 (Pass by Value): 可以将结构变量按值传递给函数。函数会收到结构的一个副本,对副本成员的修改不会影响原始结构变量。
1
2
3
4
5
6
7void displayStudent(Student s) { // s 是传入结构的一个副本
std::cout << "ID: " << s.studentID << ", Name: " << s.name << std::endl;
s.name = "Changed"; // 只修改副本
}
// ...
displayStudent(s1); // 传递 s1 的副本
std::cout << s1.name; // 输出 "Charlie",未被改变 - 作为函数参数 (Pass by Reference/Pointer): 为了避免复制整个结构的开销,或者需要在函数中修改原始结构,通常按引用或指针传递结构。
1
2
3
4
5
6
7
8
9void updateGPA(Student& s, double newGPA) { // 按引用传递,可以修改原始结构
s.gpa = newGPA;
}
void printID(const Student* sPtr) { // 按指针传递 (const 防止意外修改)
std::cout << "ID: " << sPtr->studentID << std::endl; // 使用 -> 访问指针指向的结构成员
}
// ...
updateGPA(s1, 4.0); // 修改原始 s1
printID(&s1); // 传递 s1 的地址 - 作为函数返回值: 函数可以返回一个结构。
1
2
3
4
5
6
7
8
9Student createStudent(std::string name, int id, double gpa) {
Student temp;
temp.name = name;
temp.studentID = id;
temp.gpa = gpa;
return temp; // 返回一个 Student 结构
}
// ...
Student s3 = createStudent("David", 222, 3.7);
这些特性使得结构成为组织和传递相关数据的强大工具。
4.4.5 结构数组
可以创建**结构数组 (Array of Structures)**,即数组的每个元素都是一个结构变量。这对于处理一组具有相同结构的数据非常有用,例如一个班级的学生信息、一个商店的库存列表等。
声明和初始化:
声明结构数组与声明普通数组类似,只是元素类型是结构类型。初始化可以使用嵌套的花括号。
1 |
|
-
Student classRoster[CLASS_SIZE];
: 声明了一个数组,每个元素都是Student
结构。 -
graduates[0] = {"Alice", 101};
: 初始化数组的第一个元素(一个Student
结构)。 -
freshmen[i].name
: 访问数组freshmen
中索引为i
的元素的name
成员。
4.4.6 结构中的位字段
位字段 (Bit Field) 是一种特殊的结构成员,它允许你指定成员变量占用的**位数 (bits)**。这主要用于需要精确控制内存布局或与硬件寄存器交互的场景。
语法:
在结构定义中,成员名后面跟一个冒号 :
和一个整数常量,表示该成员占用的位数。
1 | struct RegisterFlags { |
特点和注意事项:
- 类型: 位字段的类型通常是
unsigned int
或signed int
(或int
,其符号性取决于实现),也可以是bool
(C++11,等效于: 1
)。 - 内存节省: 当多个标志或小范围数值需要存储时,位字段可以显著节省内存,将它们打包到单个整数或几个字节中。
- 硬件接口: 常用于映射硬件设备寄存器的特定位。
- 访问: 像普通结构成员一样使用点运算符访问,但不能获取位字段的地址(
&
运算符不能用于位字段)。 - 可移植性: 位字段的内存布局(位的排列顺序、跨字节边界的处理)可能因编译器和平台而异,因此在需要跨平台兼容性的代码中应谨慎使用。
- 大小限制: 位数不能超过其基础类型的位数(例如,
unsigned int
的位字段不能超过int
的位数)。 - 匿名位字段: 可以使用未命名的位字段来填充或对齐,例如
unsigned int : 2;
。
用法与示例:
1 |
|
位字段是一种底层工具,适用于特定场景,但在常规应用程序开发中不常用。
4.5 共用体
共用体 (Union) 是一种特殊的数据结构,它也允许在一个结构中存储不同的数据类型,但与结构体 (struct) 不同的是,共用体的所有成员共享同一块内存空间。
核心特点:
- 内存共享: 共用体的大小由其最大的成员的大小决定。所有成员都从相同的内存地址开始存储。
- 同一时间只有一个成员有效: 在任何时刻,你只能有效地存储和使用共用体中的一个成员的值。当你给一个成员赋值时,可能会覆盖掉其他成员的数据。
- 节省内存: 当你需要存储多种类型的数据,但知道在任何时候只需要用到其中一种时,共用体可以节省内存,因为它只需要分配足够容纳最大成员的空间。
定义共用体:
使用 union
关键字定义,语法与 struct
类似。
1 | union UnionName { |
访问成员:
与结构体一样,使用成员运算符(点运算符 .
) 来访问共用体变量的成员。
用法与示例:
1 |
|
重要: 程序员有责任跟踪共用体中当前哪个成员是活动的(有效的)。读取非活动成员的值会导致未定义行为或得到无意义的数据。通常会结合一个枚举类型或整数标志来指示当前存储的数据类型,如 DataPacket
示例所示。
匿名共用体 (Anonymous Union):
共用体可以不带名称直接定义在结构体或类内部(或函数局部作用域)。匿名共用体的成员可以直接通过结构/类变量访问,就像它们是结构/类的直接成员一样。匿名共用体的所有成员仍然共享相同的内存。
1 |
|
使用场景:
- 节省内存: 当数据项有多种可能类型,但一次只使用一种时。
- 类型双关 (Type Punning): 以不同的类型解释同一块内存区域(例如,将一个
float
的位模式解释为一个int
)。这是一种低级技巧,通常不可移植且可能违反 C++ 的严格别名规则 (strict aliasing rules),应谨慎使用或避免。
与结构的比较:
- 内存: 结构的所有成员都有自己独立的内存地址;共用体的所有成员共享起始地址。
- 大小: 结构的大小约等于其所有成员大小之和(加上可能的对齐填充);共用体的大小等于其最大成员的大小。
- 有效性: 结构的所有成员可以同时有效;共用体只有一个成员能同时有效。
C++11 及以后的共用体:
C++11 放宽了对共用体成员类型的限制,允许包含具有非平凡构造函数、析构函数或赋值运算符的类类型成员(如 std::string
)。但是,如果共用体包含这样的成员,编译器不会自动生成默认的构造函数、析构函数或复制/移动操作。程序员必须手动管理这些成员的生命周期(例如,使用 placement new 在共用体内存上构造对象,并在不再需要时显式调用析构函数)。这使得包含复杂类型的共用体使用起来更加复杂和易错。对于只包含 POD (Plain Old Data) 类型(如 int
, float
, 指针, C 风格数组/结构)的共用体,其行为与 C 语言中类似。
4.6 枚举
C++ 的 enum
工具提供了一种创建符号常量 (Symbolic Constant) 的方式,常用于定义一组相关的、具有名称的整数常量。这比使用 const int
或 #define
来定义一组相关常量更方便、更具可读性。
基本概念:
- 枚举类型 (Enumeration Type):
enum
关键字用于创建一个新的用户定义的整数类型。 - 枚举量 (Enumerator): 在枚举类型定义中列出的标识符。它们是具名的常量,代表整数值。
定义枚举:
使用 enum
关键字,后跟枚举类型的名称,然后在花括号 {}
内列出枚举量,用逗号分隔。
1 | enum spectrum {red, orange, yellow, green, blue, violet, indigo, ultraviolet}; |
工作原理:
- 创建新类型: 上述语句创建了一个名为
spectrum
的新类型。 - 定义枚举量:
red
,orange
,yellow
等成为spectrum
类型的符号常量。 - 自动赋值: 默认情况下,编译器将整数值赋给枚举量,从 0 开始,依次递增 1。
-
red
值为 0 -
orange
值为 1 -
yellow
值为 2 - …
-
ultraviolet
值为 7
-
声明和使用枚举变量:
可以像使用其他类型一样声明枚举类型的变量。枚举变量通常只能被赋予该枚举类型中定义的枚举量。
1 |
|
枚举的优点:
- 提高可读性: 使用有意义的名称(如
red
,blue
)代替神秘的数字(0, 4)。 - 类型安全: 枚举创建了新的类型,有助于防止将不相关的整数值赋给枚举变量(虽然可以通过强制转换绕过)。
- 代码维护: 如果需要更改某个常量的值或添加新常量,只需修改枚举定义。
4.6.1 设置枚举量的值
可以显式地为枚举量指定整数值。
规则:
- 使用赋值运算符
=
为枚举量指定值。 - 未被显式赋值的枚举量的值将基于前一个枚举量的值加 1。
- 第一个枚举量如果未显式赋值,默认为 0。
- 不同的枚举量可以具有相同的值。
用法与示例:
1 |
|
4.6.2 枚举的取值范围
虽然枚举量是 int
类型的常量,但枚举类型本身 (spectrum
, BitField
等) 的取值范围并不一定等同于 int
。
C++98/03 标准:
- 底层类型 (Underlying Type): 编译器会选择一种能够容纳所有枚举量值的整型作为该枚举的底层类型。这个类型至少要和
int
一样大,但如果所有枚举量的值可以用更小的类型(如char
或short
)表示,编译器可能会选择更小的类型来节省内存。 - 取值范围: 枚举变量理论上可以存储的值的范围由其底层类型决定。然而,C++ 标准对枚举变量可以合法持有的值有更严格的规定。一个枚举变量可以持有的值,其上限是大于最大枚举量值的最小的 2 的幂减 1,下限类似(如果存在负枚举量值,则为小于最小枚举量值的最大的 2 的幂加 1;如果枚举量都非负,则下限为 0)。
- 例如,对于
enum spectrum {red=0, ..., ultraviolet=7}
,最大枚举量是 7。大于 7 的最小的 2 的幂是 8,所以上限是 8 - 1 = 7。下限是 0。因此,spectrum
变量理论上可以持有 0 到 7 范围内的值。 - 对于
enum BitField {..., BIT_SEVEN=13}
,最大枚举量是 13。大于 13 的最小的 2 的幂是 16,上限是 16 - 1 = 15。下限是 0。BitField
变量理论上可以持有 0 到 15 范围内的值。
- 例如,对于
- 赋值限制: 尽管范围可能比枚举量的值域宽,但 C++ 通常不允许直接将超出枚举量定义范围的整数值赋给枚举变量(即使该整数在理论范围内),需要显式类型转换。
C++11 作用域内枚举 (Scoped Enumeration):
C++11 引入了 enum class
(或 enum struct
),称为作用域内枚举,提供了更强的类型安全和作用域控制:
- 强类型:
enum class
的枚举量不会隐式转换为整数。 - 作用域: 枚举量的名称被限制在枚举类型的作用域内,访问时需要使用
EnumType::Enumerator
。 - 可指定底层类型: 可以显式指定底层整数类型,例如
enum class Color : unsigned char { Red, Green, Blue };
。 - 无隐式转换: 不能将整数直接赋给
enum class
变量,也不能将enum class
变量隐式转换为整数,都需要显式转换 (static_cast
)。
用法与示例 (范围和 C++11):
1 |
|
总结:
传统的 enum
提供了一种创建命名常量的方式,但类型安全较弱,且枚举量会污染所在的作用域。C++11 的 enum class
提供了更强的类型安全和作用域控制,是现代 C++ 中更推荐的选择。在使用传统 enum
时,要注意其取值范围和与整数类型转换的规则。
4.7 指针和自由存储空间
到目前为止,我们创建的变量(包括数组、结构等)在声明时,编译器会为其分配内存。这些变量的内存管理是自动的(自动存储或静态存储)。但是,有时我们需要在程序运行时根据需要动态地分配和释放内存。指针 (Pointer) 和 自由存储空间 (Free Store)**(也常称为堆 Heap**)是实现这一目标的关键。
指针是一种特殊的变量,它存储的是另一个变量的内存地址。通过指针,我们可以间接地访问和修改该内存地址处的数据。
自由存储空间是程序可以动态申请使用的内存区域。与自动变量(函数执行完就销毁)或静态变量(程序整个生命周期都存在)不同,程序员需要手动管理自由存储空间中分配的内存的生命周期。
4.7.1 声明和初始化指针
声明指针:
声明指针需要指定它将指向的数据类型,并在变量名前加上星号 *
(星号可以靠近类型名、变量名或在两者之间)。
1 | typeName * pointerName; |
-
typeName
: 指针将要指向的数据的类型。 -
*
: 表明pointerName
是一个指针。 -
pointerName
: 指针变量的名称。
示例:
1 | int * p_int; // 声明一个指向 int 类型的指针 p_int |
获取地址 (&
运算符):
地址运算符 &
用于获取一个变量的内存地址。
初始化指针:
指针在声明时应被初始化,以避免指向不确定的内存地址。常见的初始化方式:
- 初始化为
nullptr
(C++11 及以后):nullptr
是表示空指针的关键字,表示该指针当前不指向任何有效的内存地址。这是推荐的初始化空指针的方式。 - 初始化为
0
或NULL
: 在 C++11 之前,通常使用0
或宏NULL
(通常定义为 0) 来表示空指针。虽然仍可用,但nullptr
类型更安全。 - 初始化为变量地址: 使用
&
运算符获取一个已存在变量的地址来初始化指针。指针的类型必须与变量的类型匹配(或能隐式转换)。
解引用 (*
运算符):
解引用运算符 *
用于访问指针所指向的内存地址处存储的值。当 *
用于已初始化的有效指针变量前时,它表示“获取指针指向的值”。
用法与示例:
1 |
|
关键点:
-
int updates;
:updates
是一个int
变量。 -
int * p_updates;
:p_updates
是一个指针变量,它存储的是一个int
变量的地址。 -
p_updates
: 存储的地址值。 -
*p_updates
: 存储在该地址处的int
值。
4.7.2 指针的危险
指针非常强大,但也容易出错,是 C++ 中常见的 bug 来源。
- 解引用未初始化的指针: 如果指针没有被初始化,它会包含一个随机的地址(垃圾值)。解引用这种指针(试图访问该随机地址处的值)会导致未定义行为,通常导致程序崩溃。
1
2
3int * p_uninitialized;
// std::cout << *p_uninitialized; // 极度危险! 程序可能崩溃
// *p_uninitialized = 100; // 极度危险! 可能覆盖关键数据或导致崩溃 - 解引用空指针: 解引用
nullptr
(或0
,NULL
) 同样是未定义行为,通常也会导致程序崩溃。在使用指针前,最好检查它是否为空。1
2
3
4
5int * p_null = nullptr;
// std::cout << *p_null; // 危险! 程序可能崩溃
if (p_null != nullptr) { // 检查指针是否有效
std::cout << *p_null;
} - 悬挂指针 (Dangling Pointer): 当指针指向的内存已经被释放或不再有效时,该指针就成为悬挂指针。解引用悬挂指针也是未定义行为。这通常发生在
delete
之后(见 4.7.5)或指向局部变量的指针在其作用域结束后仍然存在时。1
2
3
4
5
6int * p_dangle;
{
int local_var = 10;
p_dangle = &local_var; // p_dangle 指向局部变量
} // local_var 在这里被销毁,内存可能被回收
// std::cout << *p_dangle; // 危险! p_dangle 是悬挂指针 - 内存泄漏 (Memory Leak): 如果使用
new
分配了内存(见 4.7.4),但忘记使用delete
释放,或者丢失了指向该内存的唯一指针,这块内存就无法再被程序访问或释放,造成内存泄漏。程序运行时间越长,泄漏的内存越多,最终可能耗尽系统资源。
安全使用指针的建议:
- 总是初始化指针: 声明指针时立即初始化为
nullptr
或一个有效的地址。 - 在使用前检查: 在解引用指针前,检查它是否为
nullptr
。 - 谨慎处理指针生命周期: 确保指针指向的内存在指针使用期间是有效的。
- 配对
new
和delete
: 动态分配的内存必须手动释放。
4.7.3 指针和数字
虽然指针存储的是内存地址,而地址本质上是数字,但指针类型和整数类型是不同的。不能随意将整数赋给指针(除了 0
/nullptr
),也不能直接将指针当作普通整数进行算术运算(指针算术有特殊规则,见 4.8)。
1 |
|
将指针视为地址,而不是普通的数字,有助于避免类型错误和不安全的操作。
4.7.4 使用new来分配内存
new
运算符用于在程序的自由存储区 (Free Store) 或 堆 (Heap) 上动态分配内存。这允许你在运行时根据需要创建变量或对象,而不是在编译时就确定。
语法:
1 | pointerVariable = new typeName; |
-
new
: 运算符。 -
typeName
: 要分配内存的数据类型。 -
initializer
(可选): 用于初始化新分配内存的值。
工作流程:
-
new
在自由存储区找到一块足够大的、未使用的内存块,以存储typeName
类型的数据。 -
new
返回这块内存的起始地址。 - 这个地址被赋给一个相应类型的指针变量。
如果 new
无法分配所需的内存(例如内存不足),它会抛出一个 std::bad_alloc
异常(除非使用了 new (std::nothrow)
版本,该版本在失败时返回 nullptr
)。
用法与示例:
1 |
|
动态分配的内存不会像自动变量那样在作用域结束时自动释放。程序员必须负责在不再需要时手动释放它。
4.7.5 使用delete释放内存
delete
运算符用于释放由 new
分配的内存,将其归还给自由存储区,以便后续可以重新分配使用。
语法:
1 | delete pointerVariable; |
-
delete
: 运算符。 -
pointerVariable
: 指向由new
(**不是new[]
**)分配的内存的指针。
工作流程:
-
delete
接收一个指针,该指针必须指向由new
分配的内存块的起始地址。 -
delete
释放该指针指向的内存块。 - 指针变量本身的值不会被自动修改(它仍然存储着那个现在无效的地址),成为**悬挂指针 (Dangling Pointer)**。
重要规则:
-
new
和delete
必须配对使用: 每个new
都应该对应一个delete
。 - 不要
delete
同一块内存两次: 对同一块内存执行两次delete
是未定义行为。 - 不要
delete
不是由new
分配的内存: 例如,不要delete
指向自动变量(栈变量)或静态变量的指针。 - 不要
delete
空指针 (nullptr
): 对空指针执行delete
是安全且无效果的。 -
delete
之后将指针设为nullptr
: 释放内存后,最好立即将指针设置为nullptr
,以防止它成为悬挂指针被意外使用。
用法与示例:
1 |
|
忘记 delete
会导致内存泄漏,而错误地使用 delete
则可能导致程序崩溃或数据损坏。正确管理动态内存是 C++ 编程中的一项重要技能。
4.7.6 使用new来创建动态数组
除了分配单个变量的内存,new
也可以用来动态分配数组。这在你需要在运行时确定数组大小时非常有用。
语法:
1 | pointerVariable = new typeName [numberOfElements]; |
-
new
: 运算符。 -
typeName
: 数组元素的数据类型。 -
numberOfElements
: 数组的大小,可以是一个变量或表达式,在运行时计算其值。 -
[]
: 表明要分配的是一个数组。
new[]
会分配一块连续的内存,足以容纳 numberOfElements
个 typeName
类型的元素,并返回指向数组第一个元素的指针。
释放动态数组 (delete[]
):
释放由 new[]
分配的数组内存必须使用 delete[]
运算符,而不是 delete
。
1 | delete [] pointerVariable; |
-
delete[]
: 用于释放数组内存的运算符。 -
pointerVariable
: 指向由new[]
分配的数组内存的指针。
delete[]
和 delete
的区别至关重要:
-
delete[]
知道需要释放的是一个数组,它会正确地调用数组中每个对象(如果是类类型)的析构函数(如果需要),并释放整个数组占用的内存。 - 如果对
new[]
分配的内存使用delete
(没有[]
),行为是未定义的。对于基本类型可能看似正常工作(但仍是错误的),但对于包含对象的数组,很可能只调用第一个对象的析构函数,并可能导致内存损坏或泄漏。
用法与示例:
1 |
|
总结:
- 使用
new typeName[size]
分配动态数组。 - 使用
delete [] pointerVariable
释放动态数组。 - 必须匹配
new[]
和delete[]
,否则行为未定义。 - 动态数组提供了在运行时确定数组大小的灵活性,但需要程序员负责内存管理。
4.8 指针、数组和指针算术
指针和数组在 C++ 中有着非常紧密的联系。理解这种关系以及指针算术对于有效地使用 C++ 处理内存和数据集合至关重要。
4.8.1 程序说明
指针算术允许我们对指针执行一些特殊的算术运算,主要是加法和减法,以便在内存中移动,特别是在数组中。
指针与数组名的关系:
在 C++ 中,数组名在很多情况下会被隐式地当作指向其第一个元素的指针。
1 |
|
指针算术规则:
- 指针加整数
p + n
: 结果是一个指向p
原来指向位置之后第n
个元素的地址。编译器会根据指针指向的类型大小自动计算实际地址偏移量(地址 = p的地址 + n * sizeof(指向的类型)
)。 - 指针减整数
p - n
: 结果是一个指向p
原来指向位置之前第n
个元素的地址。 - 指针减指针
p1 - p2
: 结果是两个指针之间相隔的元素数量(一个整数)。只有当两个指针指向同一个数组(或超出末尾一个位置)中的元素时,这个操作才有意义。结果的类型是std::ptrdiff_t
(在<cstddef>
中定义)。 - 递增/递减:
++p
,p++
,--p
,p--
分别使指针指向下一个或上一个元素。 - 比较: 可以使用关系运算符 (
<
,>
,<=
,>=
) 比较指向同一个数组元素的指针,判断它们的相对位置。也可以使用==
和!=
比较指针是否指向同一个地址(或是否都为空)。
数组下标和指针的关系:
表达式 arrayName[i]
在 C++ 中等价于 *(arrayName + i)
。
同样,如果 p
是一个指向数组元素的指针,p[i]
等价于 *(p + i)
。
这意味着你可以对数组名使用指针算术(概念上),也可以对指针使用数组下标表示法。
4.8.2 指针小结
让我们回顾一下关于指针的关键概念:
- 声明: 使用
typeName * pointerName;
声明一个指向typeName
类型数据的指针。 - 初始化:
- 使用
&
获取变量地址:pointerName = &variableName;
- 初始化为空指针:
pointerName = nullptr;
(C++11) 或pointerName = 0;
- 使用
new
分配动态内存:pointerName = new typeName;
或pointerName = new typeName[size];
- 使用
- 解引用: 使用
*
访问指针指向的值:value = *pointerName;
或*pointerName = newValue;
。 - 指针与数组: 数组名通常可视为指向第一个元素的常量指针。指针算术允许在数组元素间移动。
array[i]
等价于*(array + i)
。 - 动态内存: 使用
new
分配,必须使用delete
(对应new
) 或delete[]
(对应new[]
) 释放。 - 危险: 未初始化指针、空指针解引用、悬挂指针、内存泄漏、错误的
delete
/delete[]
使用。 - 指针本身 vs 指向的值:
pointerName
存储的是地址,*pointerName
是该地址处的值。
4.8.3 指针和字符串
C 风格字符串本质上是 char
类型的数组,以空字符 \0
结尾。因此,指针在处理 C 风格字符串时非常常用。
- 字符串字面值: 字符串字面值(如
"Hello"
) 在内存中存储为const char
数组,并以\0
结尾。字符串字面值本身可以被当作指向其第一个字符的const char*
指针。 -
char
指针: 可以声明char*
或const char*
指针来指向 C 风格字符串。
用法与示例:
1 |
|
注意:
-
cout
对char*
有特殊处理,它会打印从指针指向地址开始直到遇到空字符\0
的所有字符。 - 修改字符串字面值是未定义行为,应使用
const char*
指向它们。 - 处理 C 风格字符串时要特别注意缓冲区溢出问题(例如使用
strcpy
时目标数组不够大),并确保字符串以\0
结尾。std::string
类通常是更安全、更方便的选择。
4.8.4 使用new创建动态结构
就像可以动态分配基本类型和数组一样,也可以使用 new
动态创建结构体(或类)对象。
分配:
1 | StructureName * pointerVariable = new StructureName; |
这会在自由存储区分配足够存储 StructureName
结构所有成员的内存,并调用该结构的构造函数(如果是类或有构造函数的结构),然后返回指向新创建结构的指针。
访问成员:
当通过指针访问结构或类的成员时,不能直接使用点运算符 .
。有两种方式:
- 解引用再用点 (
(*ptr).member
): 先解引用指针*ptr
得到结构本身,然后使用点运算符访问成员。括号是必需的,因为点运算符的优先级高于解引用运算符。 - 箭头运算符 (
ptr->member
): 这是更常用、更简洁的方式。箭头运算符->
专门用于通过指针访问其指向的结构或类的成员。ptr->member
完全等价于(*ptr).member
。
释放:
使用 delete
释放由 new
创建的单个结构对象。delete
会先调用该对象的析构函数(如果需要),然后释放内存。
1 | delete pointerVariable; |
用法与示例:
1 |
|
4.8.5 自动存储、静态存储和动态存储
C++ 程序中的变量和数据根据其内存分配方式和生命周期,可以分为三种主要的存储类别:
自动存储持续性 (Automatic Storage Duration):
- 内存区域: 通常在称为栈 (Stack) 的内存区域分配。
- 分配/释放: 内存的分配和释放在函数(或代码块)进入和退出时自动进行。
- 生命周期: 变量在声明它的函数或代码块执行期间存在,块结束时自动销毁。
- 例子: 函数内部声明的非
static
局部变量(包括函数参数)。 - 特点: 分配和释放速度快,管理简单(自动),但空间有限,生命周期受限于作用域。
静态存储持续性 (Static Storage Duration):
- 内存区域: 在程序的整个生命周期内都存在于内存的某个固定区域(通常是静态/全局数据区)。
- 分配/释放: 内存在程序启动时分配(或首次使用时,对于某些静态变量),在程序结束时释放。
- 生命周期: 从程序开始执行到程序结束。
- 例子: 在函数外部声明的变量(全局变量)、使用
static
关键字在函数内部或类内部声明的变量。 - 特点: 生命周期长,可以跨函数调用保持其值,但全局变量可能导致命名冲突和管理复杂性。
动态存储持续性 (Dynamic Storage Duration):
- 内存区域: 在称为自由存储区 (Free Store) 或 堆 (Heap) 的内存区域分配。
- 分配/释放: 内存由程序员使用
new
(或malloc
等 C 函数) 显式分配,并且必须使用delete
(或free
) 显式释放。 - 生命周期: 从
new
分配成功开始,直到程序员使用delete
释放为止。生命周期与函数或代码块的作用域无关。 - 例子: 使用
new
创建的变量、数组或对象。 - 特点: 提供了最大的灵活性,可以在运行时根据需要分配任意大小的内存,生命周期由程序员控制。但管理复杂,容易出现内存泄漏(忘记
delete
)或悬挂指针(delete
后仍使用指针)等问题。
示例对比:
1 |
|
理解这三种存储方式对于编写健壮、高效且无内存错误的 C++ 程序至关重要。现代 C++ 倾向于使用 RAII (Resource Acquisition Is Initialization) 技术和智能指针(如 std::unique_ptr
, std::shared_ptr
)来自动管理动态内存,以减少手动 new
/delete
带来的风险。
4.9 类型组合
C++ 的强大之处在于其类型系统允许你将基本类型、复合类型(数组、结构、共用体、枚举)和指针以多种方式组合起来,创建更复杂的数据结构来精确地模拟现实世界的问题。
本章我们已经接触了一些组合:
- 结构数组 (Array of Structures): 数组的每个元素都是一个结构体 (见 4.4.5)。
- 结构包含
std::string
成员: 结构体可以包含类类型的成员 (见 4.4.3)。 - 指针指向结构: 可以声明指向结构体对象的指针 (见 4.8.4)。
- 指针指向数组 (或数组名视为指针): 指针可以用来操作数组 (见 4.8.1)。
本节将进一步探讨一些常见的类型组合方式。
1. 结构包含数组成员:
结构体可以包含数组作为其成员。
1 |
|
2. 结构包含指针成员:
结构体可以包含指针作为成员。这常用于指向动态分配的内存或指向其他数据结构。
1 |
|
注意: 当结构包含指针成员指向动态内存时,需要特别注意内存管理(复制、赋值、析构),这通常涉及到类的特殊成员函数(拷贝构造函数、拷贝赋值运算符、析构函数),我们将在后续章节深入学习。
3. 指针数组 (Array of Pointers):
可以创建数组,其每个元素都是一个指针。
1 |
|
指针数组常用于存储 C 风格字符串数组(const char*[]
)或管理一组动态分配的对象。
4. 指向指针的指针 (Pointer to Pointer):
指针本身也是变量,它也有自己的内存地址。因此,可以声明一个指向指针的指针。
1 |
|
指向指针的指针常用于:
- 在函数中修改调用者传入的指针本身(使其指向不同的地址)。
- 处理动态分配的指针数组(例如
char** argv
inmain
)。
总结:
通过组合基本类型、数组、结构、指针等,可以构建出非常灵活和强大的数据结构。理解每种组合方式的内存布局、访问方式以及(特别是涉及指针和动态内存时)生命周期管理规则是编写复杂 C++程序的关键。随着学习的深入,我们将看到更多高级的组合和抽象方式,例如使用类和标准库容器。
4.10 数组的替代品
虽然 C++ 内置的数组(包括动态分配的数组)功能强大,但它们存在一些固有的缺点:数组大小通常需要在编译时确定(对于栈上的数组),或者需要手动进行动态内存管理(对于堆上的数组),并且不提供边界检查等安全特性。
C++ 标准模板库 (STL) 提供了更安全、更灵活的数组替代品:vector
和 array
。
4.10.1 模板类vector
std::vector
是 STL 提供的一个动态数组模板类。它封装了动态大小的数组,可以根据需要自动增长或缩小,并负责管理其元素的内存。
特点:
- 动态大小: 可以在运行时添加或删除元素,
vector
会自动处理内存的重新分配。 - 内存管理: 自动管理元素存储的内存(通常在自由存储区/堆上分配)。
- 随机访问: 像普通数组一样,可以通过索引
[]
快速访问任何元素。 - 边界检查 (可选): 提供
at()
成员函数进行带边界检查的元素访问。 - 丰富的成员函数: 提供
push_back()
,pop_back()
,size()
,empty()
,clear()
,insert()
,erase()
等多种方便的操作。 - 模板类:
vector
是一个模板,需要指定存储的元素类型,例如std::vector<int>
,std::vector<double>
,std::vector<std::string>
。
使用方法:
- 包含头文件:
#include <vector>
- 声明和初始化:
1
2
3
4
5
6
7
8
9
10
11
// 声明
std::vector<int> scores; // 创建一个空的 int vector
std::vector<double> lengths(10); // 创建包含 10 个 double 元素的 vector (默认初始化为 0.0)
std::vector<std::string> names(5, "Unknown"); // 创建包含 5 个 string 元素,都初始化为 "Unknown"
// C++11 列表初始化
std::vector<int> primes {2, 3, 5, 7, 11}; // 创建并初始化
std::vector<char> vowels {'a', 'e', 'i', 'o', 'u'}; - 访问元素:
-
[]
运算符:scores[0]
,names[i]
(不进行边界检查)。 -
at()
函数:scores.at(0)
,names.at(i)
(进行边界检查,越界抛出std::out_of_range
异常)。
-
- 常用操作:
-
push_back(value)
: 在vector
末尾添加一个元素。 -
size()
: 返回vector
中元素的数量。 -
empty()
: 检查vector
是否为空。 -
clear()
: 移除所有元素。 -
pop_back()
: 移除末尾的元素。
-
用法与示例:
1 |
|
std::vector
是 C++ 中替代动态数组的首选方案,因为它更安全、更易于管理。
4.10.2 模板类array(C++11)
C++11 引入了 std::array
模板类,它封装了固定大小的数组。与 C 风格数组类似,其大小在编译时确定,但它提供了更现代的接口和一些 vector
具有的便利性(如 size()
, at()
)。
特点:
- 固定大小: 数组大小必须在编译时指定为模板参数,之后不能改变。
- 内存位置: 通常在栈上分配内存(如果作为局部变量声明),与 C 风格数组类似,除非显式使用
new
创建。 - 类型安全: 大小是类型的一部分 (
std::array<int, 5>
和std::array<int, 10>
是不同的类型)。 - STL 接口: 提供与
vector
类似的接口,如size()
,empty()
,at()
,[]
,front()
,back()
,以及迭代器支持,可以方便地与 STL 算法一起使用。 - 无开销抽象: 通常不会比 C 风格数组带来额外的运行时性能开销。
- 模板类: 需要指定元素类型和大小,例如
std::array<int, 10>
,std::array<double, 3>
。
使用方法:
- 包含头文件:
#include <array>
- 声明和初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 声明
std::array<int, 5> scores; // 创建包含 5 个 int 的 array (元素值未定义,除非是静态存储)
std::array<double, 3> coords {}; // 创建包含 3 个 double 的 array, 零初始化 {0.0, 0.0, 0.0}
// C++11 列表初始化
std::array<int, 4> values {1, 2, 3, 4}; // 创建并初始化
std::array<std::string, 2> names {"Alice", "Bob"};
// 注意: 初始化列表的元素数量不能超过 array 的大小
// std::array<int, 3> errors {1, 2, 3, 4}; // 错误!
// 如果元素数量少于 array 大小,剩余元素会被值初始化 (通常为 0)
std::array<int, 5> partial {10, 20}; // {10, 20, 0, 0, 0} - 访问元素:
-
[]
运算符:scores[0]
,names[i]
(不进行边界检查)。 -
at()
函数:scores.at(0)
,names.at(i)
(进行边界检查,越界抛出std::out_of_range
异常)。
-
- 常用操作:
-
size()
: 返回数组的大小(编译时常量)。 -
empty()
: 检查数组是否为空(对于大小 > 0 的std::array
总是返回false
)。 -
fill(value)
: 将所有元素设置为指定值。 -
front()
: 访问第一个元素。 -
back()
: 访问最后一个元素。
-
用法与示例:
1 |
|
std::array
是替代需要固定大小数组的 C 风格数组的现代 C++ 方案。
4.10.3 比较数组、vector对象和array对象
特性 | C 风格数组 (T[] ) |
std::vector<T> |
std::array<T, N> (C++11) |
---|---|---|---|
大小 | 固定 (编译时确定栈数组,运行时确定堆数组) | 动态 (可运行时改变) | 固定 (编译时确定) |
内存分配 | 栈 (局部) 或 静态区 或 堆 (new[] ) |
通常在堆上 (自由存储区) | 通常在栈上 (除非用 new 创建) |
大小信息 | 无内置方法获取大小 (需单独传递) | size() 成员函数 |
size() 成员函数 (编译时常量) |
边界检查 | 无 (不安全) | at() 提供检查, [] 不提供 |
at() 提供检查, [] 不提供 |
赋值/复制 | 不能直接赋值/复制整个数组 | 可以直接赋值/复制 (深拷贝) | 可以直接赋值/复制 (成员逐一复制) |
作为函数参数 | 通常退化为指针 (丢失大小信息) | 可以按值、引用或指针传递 | 可以按值、引用或指针传递 |
STL 兼容性 | 有限 (需要指针和大小) | 完全兼容 (提供迭代器等) | 完全兼容 (提供迭代器等) |
性能 | 通常最快 (直接内存访问) | 访问速度快,添加/删除可能涉及内存重分配 | 通常与 C 风格数组性能相同 |
头文件 | 无需 | <vector> |
<array> |
选择建议:
- 需要动态大小: 如果数组大小在运行时才能确定,或者需要在程序运行期间改变大小,**
std::vector
是最佳选择**。 - 需要固定大小 (编译时已知): 如果数组大小在编译时就确定且不会改变:
- **优先选择
std::array
(C++11 及以后)**。它提供了与 C 风格数组相同的性能和内存布局(通常在栈上),但具有更安全、更方便的接口(如size()
,at()
, 迭代器)。 - 如果不能使用 C++11 或有特定 C 接口兼容性需求,可以使用 C 风格数组,但要特别注意安全性和大小管理。
- **优先选择
- 性能关键且大小固定:
std::array
和 C 风格数组通常性能最佳。
总的来说,在现代 C++ 中,应优先使用 std::vector
和 std::array
而不是 C 风格数组,以获得更好的类型安全、内存管理和易用性。
4.11 总结
本章介绍了C++的**复合类型 (Compound Types)**,它们允许我们将多个值组合成一个数据单元。
我们首先学习了**数组 (Array)**,它用于存储一系列相同类型的数据。我们了解了如何声明数组、使用索引访问元素(从0开始),以及初始化数组的各种规则,包括C++11引入的更安全的列表初始化方法。我们强调了数组大小必须是常量表达式,并且访问数组时需要注意边界,避免越界访问。
接着,我们探讨了处理文本数据的两种方式。第一种是传统的C风格字符串,即以空字符 \0
结尾的 char
数组。我们学习了如何拼接字符串常量、在数组中使用字符串、以及使用 cin
和 getline
读取字符串输入,并特别注意了混合输入数字和整行字符串时可能遇到的问题。
第二种,也是C++中更推荐的方式,是使用标准库提供的 string
类。string
类提供了自动内存管理、方便的赋值、拼接 (+
) 和附加 (+=
) 操作,以及大量用于查找、修改、比较和访问字符的成员函数。我们还学习了如何使用 cin
, cout
, 和 getline
对 string
对象进行输入输出,并了解了C++11引入的原始字符串字面值和Unicode字符串字面值。
结构 (Structure) 被引入作为一种创建自定义复合类型的方式,允许将不同类型的数据项(成员)组合在一起。我们学习了如何定义结构、声明结构变量、使用点运算符 (.
) 访问成员,以及C++11的列表初始化。我们还看到结构体可以包含 string
对象或数组作为成员,结构变量可以相互赋值,可以作为函数参数(按值、按引用、按指针)和返回值。结构数组允许我们管理一组结构对象,而位字段则提供了一种在结构内精确控制成员占用位数的方式。
共用体 (Union) 作为另一种复合类型被介绍,其特点是所有成员共享同一块内存空间,主要用于节省内存或进行类型双关(需谨慎)。我们了解了如何定义和访问共用体,以及使用匿名共用体。
枚举 (Enum) 提供了一种创建具名整数常量的方式,提高了代码的可读性和类型安全。我们学习了如何定义枚举、显式设置枚举量的值、枚举的取值范围,并简要介绍了C++11引入的更安全的**作用域内枚举 (enum class
)**。
本章的一个核心内容是指针 (Pointer) 和**自由存储空间 (Free Store / Heap)**。指针是存储内存地址的变量。我们学习了如何声明和初始化指针(包括使用 &
获取地址和初始化为 nullptr
),如何使用解引用运算符 *
访问指针指向的值,并强调了使用未初始化指针、空指针或悬挂指针的危险。
我们学习了使用 new
运算符在自由存储区动态分配内存(用于单个变量或对象),以及使用 delete
运算符释放这些内存。同样,我们学习了使用 new[]
动态分配数组,并强调必须使用 delete[]
来释放动态数组内存。正确配对 new
/delete
和 new[]
/delete[]
对于避免内存泄漏和程序崩溃至关重要。
指针与数组的紧密关系以及指针算术也被详细讨论。数组名通常可视为指向第一个元素的指针,指针算术允许在数组元素间移动。我们还看到了如何使用指针处理C风格字符串,以及如何动态创建结构并使用箭头运算符 (->
) 通过指针访问其成员。最后,我们区分了三种主要的存储持续性:自动存储(栈)、静态存储和动态存储(堆)。
我们还探讨了如何组合这些类型,例如创建包含数组或指针成员的结构、指针数组以及指向指针的指针,以构建更复杂的数据表示。
最后,我们介绍了C++标准库提供的内置数组的现代替代品:**std::vector
**(动态大小数组)和 **std::array
**(固定大小数组,C++11)。它们提供了更安全、更方便的接口和自动内存管理(对于 vector
),是现代C++编程中推荐的选择。
通过本章的学习,我们掌握了创建和使用各种复合数据类型以及进行动态内存管理的基本技能。