数据类型
# 第 3 章 C++ 数据类型
# 目录介绍
# 3.1 基本数据
# 3.1.1 基本数据类型
C++的基本数据类型包括整型、浮点型、字符型、布尔型等。如下所示:
- int:整数类型,用于表示整数值。
- float:单精度浮点数类型,用于表示小数值。
- double:双精度浮点数类型,用于表示更大范围和更高精度的小数值。
- char:字符类型,用于表示单个字符。
- bool:布尔类型,用于表示真或假的值。
# 3.1.2 固定宽度类型
传统整数类型的问题: 在 C++ 中,int、long 等类型的宽度(即占用的字节数)是由编译器和平台决定的。例如,int 在某些平台上可能是 16 位,而在另一些平台上可能是 32 位。
C++11引入了固定宽度的整数类型,如int8_t, int16_t, int32_t, int64_t等,定义在
为什么要引入这种类型?是为了解决传统整数类型(如 int、long 等)在不同平台上的宽度不一致问题。
# 3.1.3 空类型 (void)
void类型表示“无类型”或“无值”。它主要有三种用途:
- 函数不返回任何值:
void printMessage() {
cout << "This function returns nothing." << endl;
// 无需 return 语句,或使用 return;
}
2
3
4
- 函数无参数(在C++中,空参数列表最好用
void明确写出,但留空也可):
int getValue(void) { // 明确表示该函数不接受任何参数
return 42;
}
2
3
- 指向未指定类型的指针 (
void*):void*是一种通用指针类型,可以指向任何数据类型的数据。在使用前,必须将其强制转换为具体的指针类型。
int num = 100;
void* genericPtr = # // 可以指向int
// cout << *genericPtr << endl; // 错误!void* 不能直接解引用
int* intPtr = static_cast<int*>(genericPtr); // 必须转换
cout << *intPtr << endl; // 正确,输出 100
2
3
4
5
# 3.1.4 综合案例与思考
综合案例:数据类型信息查询工具
#include <iostream>
#include <cstdint>
#include <limits>
using namespace std;
int main() {
// 1. 展示所有基本数据类型的大小和范围
cout << "=== 基本数据类型信息 ===" << endl;
cout << "int: " << sizeof(int) << " 字节, 范围: ["
<< numeric_limits<int>::min() << ", "
<< numeric_limits<int>::max() << "]" << endl;
cout << "float: " << sizeof(float) << " 字节, 精度: "
<< numeric_limits<float>::digits10 << " 位" << endl;
cout << "double: " << sizeof(double) << " 字节, 精度: "
<< numeric_limits<double>::digits10 << " 位" << endl;
cout << "char: " << sizeof(char) << " 字节" << endl;
cout << "bool: " << sizeof(bool) << " 字节" << endl;
// 2. 固定宽度类型确保跨平台一致
cout << "\n=== 固定宽度类型 ===" << endl;
int32_t fixedInt = 100;
uint64_t bigNum = 18446744073709551615ULL;
cout << "int32_t: " << sizeof(fixedInt) << " 字节, 值: " << fixedInt << endl;
cout << "uint64_t: " << sizeof(bigNum) << " 字节, 值: " << bigNum << endl;
// 3. void指针的通用性
cout << "\n=== void指针 ===" << endl;
int a = 42;
double b = 3.14;
void* genericPtr;
genericPtr = &a;
cout << "指向int: " << *static_cast<int*>(genericPtr) << endl;
genericPtr = &b;
cout << "指向double: " << *static_cast<double*>(genericPtr) << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
案例知识融合:这个案例使用sizeof查看各基本类型的内存大小,用numeric_limits获取取值范围和精度,展示了固定宽度类型int32_t/uint64_t的跨平台一致性,以及void*通用指针配合static_cast的用法。涵盖了2.1节的基本数据类型、固定宽度类型和void类型三个知识点。
思考题:
int在不同平台上大小可能不同(16位或32位),这对可移植性有什么影响?什么时候应该使用int32_t替代int?void*指针可以指向任何类型,那为什么不能直接解引用?这种设计背后的安全考量是什么?numeric_limits<float>::digits10返回6-7,这意味着什么?为什么说float"只有6-9位有效数字"?
# 3.2 整数类型
作用:整型变量表示的是==整数类型==的数据
这些整型数据类型可以使用带符号(可以表示正数和负数)或无符号(仅表示非负数)修饰符进行声明。
# 3.2.1 有符号类型
有符号类型可以存储负数、正数和零,其中最高位用于表示符号(0 表示正数,1 表示负数)。
在 C++ 中,默认情况下,整数类型是有符号的,除非显式地使用 unsigned 关键字声明为无符号类型。
C++中能够表示整型的类型有以下几种方式,区别在于所占内存空间不同:
| 数据类型 | 占用空间 | 取值范围 |
|---|---|---|
short(短整型) | 2字节 | (-2^15 ~ 2^15-1) |
int(整型) | 4字节 | (-2^31 ~ 2^31-1) |
long(长整形) | Windows为4字节,Linux为4字节(32位),8字节(64位) | (-2^31 ~ 2^31-1) |
long long(长长整形) | 8字节 | (-2^63 ~ 2^63-1) |
# 3.2.2 无符号类型
无符号类型是指只能表示非负数(包括零)的数据类型。例如,unsigned int表示无符号整数。
此外,C++还提供了其他整型数据类型,如signed、unsigned short、unsigned long等,它们提供了不同的位数和范围,以满足不同的需求。
| 数据类型 | 占用空间(字节) | 取值范围(最小值到最大值) |
|---|---|---|
unsigned char | 1 | 0 到 255 |
unsigned short | 2 | 0 到 65,535 |
unsigned int | 4 | 0 到 4,294,967,295 |
unsigned long | 4 或 8 | 0 到 4,294,967,295(4 字节) |
| 0 到 18,446,744,073,709,551,615(8 字节) | ||
unsigned long long | 8 | 0 到 18,446,744,073,709,551,615 |
无符号整数类型的特点
- 只能表示非负数:无符号整数类型不能表示负数,最小值为 0。
- 更大的正数范围:由于不需要存储符号位,无符号整数类型可以表示比有符号类型更大的正数。
- 溢出行为:如果超出取值范围,会发生溢出,结果会从 0 重新开始(模运算)。
# 3.2.3 固定宽度整数类型
C++11 引入了 固定宽度的整数类型,这些类型定义在 <cstdint> 头文件中。它们提供了明确大小的整数类型,方便跨平台开发时确保数据大小的一致性。
| 数据类型 | 占用空间(字节) | 取值范围(有符号) | 取值范围(无符号) |
|---|---|---|---|
int8_t | 1 | -128 到 127 | - |
uint8_t | 1 | - | 0 到 255 |
int16_t | 2 | -32,768 到 32,767 | - |
uint16_t | 2 | - | 0 到 65,535 |
int32_t | 4 | -2,147,483,648 到 2,147,483,647 | - |
uint32_t | 4 | - | 0 到 4,294,967,295 |
int64_t | 8 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | - |
uint64_t | 8 | - | 0 到 18,446,744,073,709,551,615 |
固定宽度整数类型的特点
- 明确的大小: 固定宽度整数类型的大小是明确的,例如
int32_t始终是 4 字节。 - 跨平台一致性: 使用固定宽度整数类型可以确保代码在不同平台上具有相同的行为。
- 可选性: 如果平台不支持某种固定宽度类型(例如
int8_t),则编译器可能不会定义该类型。
示例代码
#include <iostream>
#include <cstdint> // 包含固定宽度整数类型
using namespace std;
int main() {
// 固定宽度有符号整数类型
int8_t i8 = -128;
int16_t i16 = -32768;
int32_t i32 = -2147483648;
int64_t i64 = -9223372036854775807LL;
// 固定宽度无符号整数类型
uint8_t u8 = 255;
uint16_t u16 = 65535;
uint32_t u32 = 4294967295;
uint64_t u64 = 18446744073709551615ULL;
// 输出
cout << "int8_t: " << sizeof(i8) << " bytes, value: " << (int)i8 << endl;
cout << "int16_t: " << sizeof(i16) << " bytes, value: " << i16 << endl;
cout << "int32_t: " << sizeof(i32) << " bytes, value: " << i32 << endl;
cout << "int64_t: " << sizeof(i64) << " bytes, value: " << i64 << endl;
cout << "uint8_t: " << sizeof(u8) << " bytes, value: " << (int)u8 << endl;
cout << "uint16_t: " << sizeof(u16) << " bytes, value: " << u16 << endl;
cout << "uint32_t: " << sizeof(u32) << " bytes, value: " << u32 << endl;
cout << "uint64_t: " << sizeof(u64) << " bytes, value: " << u64 << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
输出示例
int8_t: 1 bytes, value: -128
int16_t: 2 bytes, value: -32768
int32_t: 4 bytes, value: -2147483648
int64_t: 8 bytes, value: -9223372036854775807
uint8_t: 1 bytes, value: 255
uint16_t: 2 bytes, value: 65535
uint32_t: 4 bytes, value: 4294967295
uint64_t: 8 bytes, value: 18446744073709551615
2
3
4
5
6
7
8
使用场景
- 跨平台开发:确保整数类型在不同平台上具有相同的大小和行为。
- 网络编程:处理网络协议时,通常需要明确的数据大小。
- 文件格式:读写二进制文件时,使用固定宽度类型可以确保数据的一致性。
- 硬件编程:与硬件寄存器交互时,通常需要明确的数据大小。
# 3.2.4 综合案例与思考
综合案例:整数类型的边界与溢出实验
#include <iostream>
#include <cstdint>
#include <limits>
#include <climits>
using namespace std;
int main() {
// 1. 有符号整数的范围
cout << "=== 有符号整数范围 ===" << endl;
cout << "short: [" << SHRT_MIN << ", " << SHRT_MAX << "]" << endl;
cout << "int: [" << INT_MIN << ", " << INT_MAX << "]" << endl;
cout << "long long: [" << LLONG_MIN << ", " << LLONG_MAX << "]" << endl;
// 2. 无符号整数特点:范围更大但只能表示非负数
cout << "\n=== 无符号整数 ===" << endl;
unsigned int ua = 0;
unsigned int ub = ua - 1; // 无符号溢出是定义良好的
cout << "unsigned 0 - 1 = " << ub << " (模2^32运算)" << endl;
cout << "UINT_MAX = " << UINT_MAX << endl;
// 3. 有符号与无符号混合运算的陷阱
cout << "\n=== 混合运算陷阱 ===" << endl;
int signedVal = -1;
unsigned int unsignedVal = 1;
// 比较时signed会被隐式转换为unsigned
if (signedVal < unsignedVal) {
cout << "-1 < 1 (预期)" << endl;
} else {
cout << "-1 >= 1 (意外!因为-1转为unsigned变成了很大的数)" << endl;
}
// 4. 固定宽度类型用于网络编程
cout << "\n=== 固定宽度类型应用 ===" << endl;
// 模拟网络数据包头
struct PacketHeader {
uint16_t type; // 2字节,包类型
uint32_t length; // 4字节,数据长度
uint64_t timestamp; // 8字节,时间戳
};
cout << "PacketHeader大小: " << sizeof(PacketHeader) << " 字节" << endl;
// 5. 安全的整数范围检查
cout << "\n=== 安全范围检查 ===" << endl;
long long bigValue = 3000000000LL; // 30亿
if (bigValue > numeric_limits<int>::max()) {
cout << bigValue << " 超出int范围,不能安全转换" << endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
案例知识融合:这个案例演示了有符号整数的范围限制、无符号整数的溢出行为(模运算)、有符号与无符号混合运算的经典陷阱、固定宽度类型在网络编程中的应用,以及安全的范围检查方法。通过实际的溢出实验,深刻理解整数类型的底层行为。
思考题:
- 为什么C++标准规定有符号整数溢出是"未定义行为",而无符号整数溢出是"定义良好的"(模运算)?
- 在比较
int和unsigned int时,编译器会进行隐式转换导致意外结果。如何避免这类bug? - 为什么网络编程中通常使用
uint32_t而不是int?字节序(大端/小端)又会带来什么问题?
# 3.3 字符类型
C++ 中的字符类型用于存储单个字符或小整数值。字符类型在 C++ 中非常重要,因为它们不仅用于表示字符,还可以用于处理 ASCII 值或进行位操作。
# 3.3.1 字符类型列表
作用:字符型变量用于显示单个字符
| 数据类型 | 占用空间(字节) | 描述 |
|---|---|---|
char | 1 | 默认字符类型,通常用于存储 ASCII 字符。可以是有符号或无符号,取决于平台。 |
signed char | 1 | 明确表示有符号的字符类型,取值范围:-128 到 127。 |
unsigned char | 1 | 明确表示无符号的字符类型,取值范围:0 到 255。 |
wchar_t | 2 或 4 | 宽字符类型,用于存储 Unicode 字符。大小取决于平台(Windows 通常为 2 字节,Linux 通常为 4 字节)。 |
char16_t | 2 | 用于存储 UTF-16 编码的字符(C++11 引入)。 |
char32_t | 4 | 用于存储 UTF-32 编码的字符(C++11 引入)。 |
语法:char ch = 'a';
注意1:在显示字符型变量时,用单引号将字符括起来,不要用双引号
注意2:单引号内只能有一个字符,不可以是字符串
- C和C++中字符型变量只占用==1个字节==。
- 字符型变量并不是把字符本身放到内存中存储,而是将对应的ASCII编码放入到存储单元。
示例:
#include <iostream>
using namespace std;
int main() {
char ch = 'a';
cout << ch << endl;
cout << sizeof(char) << endl;
//ch = "abcde"; //错误,不可以用双引号
//ch = 'abcde'; //错误,单引号内只能引用一个字符
cout << (int) ch << endl; //查看字符a对应的ASCII码
ch = 97; //可以直接用ASCII给字符型变量赋值
cout << ch << endl;
return 0;
}
//a
//1
//97
//a
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3.3.2 字符类型特点
char类型:默认情况下,char可以是有符号或无符号,具体取决于编译器和平台。 通常用于存储 ASCII 字符(0 到 127)。signed char和unsigned char: 明确表示有符号或无符号的字符类型。常用于处理小整数值或进行位操作。- 宽字符类型:
wchar_t、char16_t和char32_t用于存储 Unicode 字符。wchar_t的大小取决于平台,而char16_t和char32_t的大小是固定的。 - 字符字面量:单引号用于表示字符字面量,例如
'A'。宽字符字面量使用前缀L(wchar_t)、u(char16_t)或U(char32_t),例如L'中'、u'中'、U'中'。
# 3.3.3 字符使用场景
- 文本处理:
char类型用于处理 ASCII 字符或字符串。宽字符类型用于处理 Unicode 字符。 - 位操作:
unsigned char常用于位操作,因为它可以表示 0 到 255 的值。 - 小整数存储:
signed char和unsigned char可以用于存储小整数值。 - 国际化支持:
wchar_t、char16_t和char32_t用于支持多语言字符集。
# 3.3.4 字符注意事项
char的符号性:char的符号性取决于平台,如果需要明确的有符号或无符号行为,请使用signed char或unsigned char。- 宽字符类型的大小:
wchar_t的大小因平台而异,而char16_t和char32_t的大小是固定的。 - 字符字面量的前缀:使用宽字符字面量时,确保使用正确的前缀(
L、u或U)。 - 字符与整数的转换:字符类型可以隐式转换为整数类型,但需要注意取值范围。
# 3.3.5 综合案例与思考
综合案例:字符处理工具集
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main() {
// 1. 字符与ASCII码的转换
cout << "=== 字符与ASCII ===" << endl;
for (char c = 'A'; c <= 'Z'; ++c) {
cout << c << "=" << (int)c << " ";
}
cout << endl;
// 2. 大小写转换(利用ASCII差值)
cout << "\n=== 大小写转换 ===" << endl;
char upper = 'H';
char lower = upper + 32; // 大写转小写:+32
cout << upper << " -> " << lower << endl;
// 推荐方式:使用标准库函数
cout << (char)toupper('a') << " " << (char)tolower('Z') << endl;
// 3. 字符分类判断
cout << "\n=== 字符分类 ===" << endl;
string testStr = "Hello 123!";
for (char c : testStr) {
cout << "'" << c << "': ";
if (isalpha(c)) cout << "字母 ";
if (isdigit(c)) cout << "数字 ";
if (isspace(c)) cout << "空格 ";
if (ispunct(c)) cout << "标点 ";
cout << endl;
}
// 4. 宽字符处理中文
cout << "\n=== 宽字符 ===" << endl;
wchar_t wc = L'中';
char16_t c16 = u'国';
char32_t c32 = U'人';
cout << "wchar_t大小: " << sizeof(wc) << " 字节" << endl;
cout << "char16_t大小: " << sizeof(c16) << " 字节" << endl;
cout << "char32_t大小: " << sizeof(c32) << " 字节" << endl;
// 5. unsigned char用于字节操作
cout << "\n=== 字节操作 ===" << endl;
unsigned char byte = 0xFF; // 255
cout << "byte值: " << (int)byte << endl;
cout << "高4位: " << ((byte >> 4) & 0x0F) << endl;
cout << "低4位: " << (byte & 0x0F) << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
案例知识融合:这个案例涵盖了字符类型的核心知识——ASCII编码转换、大小写转换的两种方式(手动计算和toupper/tolower函数)、字符分类判断(isalpha/isdigit等)、宽字符类型处理Unicode(wchar_t/char16_t/char32_t)、以及unsigned char在字节级位操作中的应用。
思考题:
char类型的符号性是"实现定义的",这意味着同一段代码在不同平台上可能有不同行为。如何编写可移植的字符处理代码?- 大小写转换时为什么
'A' + 32 == 'a'?如果ASCII编码表的设计不是这样排列的,会对编程产生什么影响? - C++11引入了
char16_t和char32_t,而之前只有wchar_t。为什么需要三种宽字符类型?它们各自适用什么场景?
# 3.4 浮点类型
C++ 中的浮点类型用于表示实数(即带小数点的数字)。它们基于 IEEE 754 标准,提供了不同精度的浮点数表示。
作用:用于==表示小数==
浮点型变量分为两种:
- 单精度float
- 双精度double
两者的区别在于表示的有效数字范围不同。
# 3.4.1 浮点类型列表
| 数据类型 | 占用空间(字节) | 精度(有效数字) | 取值范围(近似) |
|---|---|---|---|
float | 4 | 6-9 位 | ±1.18e-38 到 ±2.4e38 |
double | 8 | 15-17 位 | ±2.23e-308 到 ±1.8e308 |
long double | 8、12 或 16 | 18-21 位 | 取决于平台,通常与 double 相同或更高 |
# 3.4.2 浮点类型特点
- 精度:
float提供单精度浮点数,适合对精度要求不高的场景。double提供双精度浮点数,适合大多数科学计算和工程应用。long double提供扩展精度,适合对精度要求极高的场景。 - 取值范围: 浮点类型的取值范围非常大,可以表示非常小或非常大的数。
- 存储格式: 浮点数采用 IEEE 754 标准存储,分为符号位、指数位和尾数位。
- 字面量:浮点数字面量默认是
double类型。使用后缀f或F表示float类型,例如3.14f。使用后缀l或L表示long double类型,例如2.14L。
# 3.4.3 使用场景
- 科学计算:
double是科学计算中最常用的浮点类型,提供了足够的精度和范围。 - 图形处理:
float常用于图形处理,因为其精度足够且占用内存较少。 - 金融计算: 对于高精度计算(如货币计算),可以使用
long double或专门的库(如decimal)。 - 物理模拟: 物理模拟中通常使用
double或long double以确保精度。
# 3.4.4 注意事项
- 精度损失: 浮点数在计算时可能会产生精度损失,尤其是在进行大量运算时。
- 比较浮点数: 由于浮点数的精度问题,直接比较两个浮点数是否相等可能会导致错误。通常使用一个很小的误差范围(如
1e-9)进行比较。 - 溢出和下溢: 浮点数可能会溢出(超过最大值)或下溢(接近零),导致结果不准确。
- 平台差异:
long double的大小和精度可能因平台而异。
# 3.4.5 浮点数特殊值
浮点数可以表示一些特殊值:
- 无穷大:
INFINITY(正无穷)和-INFINITY(负无穷)。 - 非数字:
NaN(Not a Number),表示无效的浮点数操作结果。
#include <iostream>
#include <cmath> // 包含 INFINITY 和 NaN
using namespace std;
int main() {
double inf = INFINITY;
double nan = NAN;
cout << "Infinity: " << inf << endl;
cout << "NaN: " << nan << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
输出:
Infinity: inf
NaN: nan
2
# 3.4.6 综合案例与思考
综合案例:浮点数精度陷阱与正确比较
#include <iostream>
#include <cmath>
#include <iomanip>
#include <limits>
using namespace std;
int main() {
// 1. 经典精度问题
cout << "=== 精度陷阱 ===" << endl;
double a = 0.1 + 0.2;
cout << fixed << setprecision(20);
cout << "0.1 + 0.2 = " << a << endl; // 不等于0.3!
cout << "0.3 = " << 0.3 << endl;
cout << "相等吗?" << (a == 0.3 ? "是" : "否") << endl; // 否!
// 2. 正确的浮点数比较方式
cout << "\n=== 正确比较方式 ===" << endl;
double epsilon = 1e-9; // 误差范围
if (fabs(a - 0.3) < epsilon) {
cout << "使用epsilon比较:0.1+0.2 ≈ 0.3 (正确)" << endl;
}
// 3. float vs double精度对比
cout << "\n=== 精度对比 ===" << endl;
float fVal = 1.0f / 3.0f;
double dVal = 1.0 / 3.0;
cout << "float 1/3 = " << fVal << endl;
cout << "double 1/3 = " << dVal << endl;
// 4. 大数精度丢失
cout << "\n=== 大数精度丢失 ===" << endl;
float bigFloat = 16777216.0f; // 2^24
cout << "bigFloat = " << bigFloat << endl;
cout << "bigFloat + 1 = " << (bigFloat + 1.0f) << endl; // 仍然等于16777216!
cout << "相等吗?" << (bigFloat == bigFloat + 1.0f ? "是" : "否") << endl;
// 5. 特殊值处理
cout << "\n=== 特殊值 ===" << endl;
double inf = 1.0 / 0.0; // 正无穷
double nan = 0.0 / 0.0; // NaN
cout << "1/0 = " << inf << endl;
cout << "0/0 = " << nan << endl;
cout << "NaN == NaN? " << (nan == nan ? "是" : "否") << endl; // 否!NaN不等于自身
cout << "isnan(nan)? " << (isnan(nan) ? "是" : "否") << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
案例知识融合:这个案例深入演示了浮点数的核心问题——0.1+0.2!=0.3的经典精度问题、使用epsilon的正确比较方法、float和double的精度差异、大数累加的精度丢失(float只有约7位有效数字),以及NaN不等于自身的特殊性质。
思考题:
- 为什么
0.1 + 0.2 != 0.3?这和IEEE 754标准的二进制浮点表示有什么关系? - 金融计算为什么不能直接使用
float或double?实际项目中如何解决货币精度问题? NaN != NaN为true,这看起来很反直觉。这种设计的理由是什么?如何正确判断一个值是否为NaN?
# 3.5 布尔类型
# 3.5.1 布尔类型使用
作用:布尔数据类型代表真或假的值
| 类型 | 大小(字节) | 值 |
|---|---|---|
bool | 1 | true 或 false |
- true --- 真(本质是1)
- false --- 假(本质是0)
示例:
#include <iostream>
using namespace std;
int main() {
bool isCppFun = true;
bool isFishWet = false;
int x = 10, y = 20;
bool isEqual = (x == y); // 比较运算的结果是bool类型
bool isGreater = (x > y);
cout << "isCppFun: " << isCppFun << endl; // 输出 1
cout << "isFishWet: " << isFishWet << endl; // 输出 0
// 使用 boolalpha 操纵符以文本形式输出 true/false
cout << boolalpha;
cout << "isEqual: " << isEqual << endl; // 输出 false
cout << "isGreater: " << isGreater << endl; // 输出 false
// 任何非零值都可以隐式转换为true,零值转换为false
if (x) {
cout << "x is non-zero, which is true." << endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 3.5.2 布尔类型底层
在内存中,bool类型通常占用 1 个字节(这是最常见的情况,C++ 标准只规定它至少能存放 true或 false中的一个值,具体大小依赖于编译器和平台)。
true在底层通常用整数值 1 表示。false在底层通常用整数值 0 表示。
注意: 虽然底层用 1 和 0 表示,但在代码层面应始终使用 true和 false以提高可读性和避免混淆。
类型提升:在需要整数类型的表达式中(如算术运算),bool值会先被提升为 int(true提升为 1, false提升为 0),然后再参与运算。
# 3.5.3 综合案例与思考
综合案例:布尔类型在实际编程中的应用
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 用bool作为函数返回值表示成功/失败
bool login(const string& user, const string& pass) {
return (user == "admin" && pass == "123456");
}
// 用bool控制程序逻辑
bool isEven(int n) { return n % 2 == 0; }
bool isPrime(int n) {
if (n < 2) return false;
for (int i = 2; i * i <= n; ++i) {
if (n % i == 0) return false;
}
return true;
}
int main() {
// 1. bool与条件判断
cout << "=== 登录验证 ===" << endl;
bool success = login("admin", "123456");
cout << boolalpha; // 输出true/false而非1/0
cout << "登录结果: " << success << endl;
// 2. bool的隐式转换
cout << "\n=== 隐式转换 ===" << endl;
int zero = 0, nonZero = 42;
string empty = "", nonEmpty = "hello";
void* nullPtr = nullptr;
cout << "0 -> bool: " << (bool)zero << endl; // false
cout << "42 -> bool: " << (bool)nonZero << endl; // true
cout << "nullptr -> bool: " << (bool)nullPtr << endl; // false
// 3. bool参与算术运算(会提升为int)
cout << "\n=== bool算术运算 ===" << endl;
bool a = true, b = true;
int sum = a + b; // true(1) + true(1) = 2
cout << "true + true = " << sum << endl;
// 4. 统计满足条件的元素
cout << "\n=== 条件统计 ===" << endl;
vector<int> numbers = {2, 3, 4, 5, 6, 7, 8, 9, 10};
int evenCount = 0, primeCount = 0;
for (int n : numbers) {
evenCount += isEven(n); // bool转int:true为1
primeCount += isPrime(n);
}
cout << "偶数个数: " << evenCount << endl;
cout << "质数个数: " << primeCount << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
案例知识融合:这个案例展示了bool类型的实际应用——作为函数返回值表示操作结果、与整数的隐式转换规则(非零为true,零为false)、boolalpha格式化输出、以及利用bool参与算术运算的特性进行条件计数。
思考题:
bool类型在内存中占1个字节,但只需要1个bit就够了。为什么C++标准不让bool只占1bit?- C语言没有原生的
bool类型(C99引入了_Bool),早期C程序是如何模拟布尔逻辑的? if (ptr)和if (ptr != nullptr)效果一样吗?你更推荐哪种写法?为什么?
# 3.6 变量和常量
# 3.6.1 变量
作用:给一段指定的内存空间起名,方便操作这段内存。
语法:数据类型 变量名 = 初始值;
示例:
#include <iostream>
using namespace std;
int main() {
//变量的定义
//语法:数据类型 变量名 = 初始值
int a = 10;
cout << "a = " << a << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
注意:C++在创建变量时,必须给变量一个初始值,否则会报错
# 3.6.2 常量
作用:用于记录程序中不可更改的数据
C++定义常量两种方式
- #define 宏常量:
#define 常量名 常量值
==通常在文件上方定义==,表示一个常量
- const修饰的变量
const 数据类型 常量名 = 常量值
==通常在变量定义前加关键字const==,修饰该变量为常量,不可修改
示例:
#include <iostream>
using namespace std;
//1.宏常量
#define day = 7;
int main() {
//cout << "一周里总共有 %d" << day << " 天" << endl;
//printf("一周里总共有:%d\n",day);
//day = 8; //报错,宏常量不可以修改
//2、const修饰变量
const int month = 12;
cout << "一年里总共有 " << month << " 个月份" << endl;
//month = 24; //报错,常量是不可以修改的
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 3.6.3 综合案例与思考
综合案例:变量和常量在配置管理中的应用
#include <iostream>
#include <string>
using namespace std;
// 宏常量:编译前的文本替换
#define APP_VERSION "1.0.0"
#define MAX_RETRY 3
// const常量:类型安全
const string APP_NAME = "MyApp";
const int DEFAULT_PORT = 8080;
// constexpr常量:编译时计算(C++11)
constexpr int BUFFER_SIZE = 1024 * 4; // 4KB
constexpr double PI = 3.14159265358979;
int main() {
// 1. 变量的声明和初始化
cout << "=== 变量初始化方式 ===" << endl;
int a = 10; // C风格初始化
int b(20); // 构造函数风格
int c{30}; // 统一初始化(C++11,推荐)
int d = {40}; // 赋值+列表初始化
auto e = 50; // 自动类型推导
cout << a << " " << b << " " << c << " " << d << " " << e << endl;
// 2. 常量的使用
cout << "\n=== 常量使用 ===" << endl;
cout << "应用: " << APP_NAME << " v" << APP_VERSION << endl;
cout << "端口: " << DEFAULT_PORT << endl;
cout << "缓冲区: " << BUFFER_SIZE << " 字节" << endl;
// 3. const vs #define 的区别
cout << "\n=== const vs #define ===" << endl;
// const有类型检查
const int maxUsers = 100;
// maxUsers = 200; // 编译错误!不能修改const
cout << "最大用户数: " << maxUsers << endl;
cout << "maxUsers大小: " << sizeof(maxUsers) << " 字节" << endl;
// #define没有类型,sizeof无法直接使用
// 4. 变量的作用域
cout << "\n=== 变量作用域 ===" << endl;
int x = 100; // 外层作用域
{
int x = 200; // 内层作用域(遮蔽外层x)
cout << "内层 x = " << x << endl; // 200
}
cout << "外层 x = " << x << endl; // 100
// 5. constexpr在编译时计算
constexpr int arraySize = BUFFER_SIZE / sizeof(int);
int buffer[arraySize]; // 编译时确定数组大小
cout << "\n数组大小: " << arraySize << " 个int" << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
案例知识融合:这个案例展示了变量的多种初始化方式(C风格、构造函数风格、统一初始化、auto推导)、三种常量定义方式(#define宏常量、const常量、constexpr编译时常量)的区别和使用场景,以及变量作用域的遮蔽规则。
思考题:
#define宏常量和const常量各有什么优缺点?Effective C++为什么建议"尽量使用const替代#define"?- C++11的统一初始化(花括号
{})相比传统初始化有什么优势?它能防止什么类型的错误? const和constexpr都表示"常量",它们的区别是什么?什么时候必须用constexpr?
# 3.7 类型转换
在 C++ 中,类型转换 是将一种数据类型转换为另一种数据类型的过程。C++ 提供了多种类型转换方式,包括隐式转换和显式转换。
显式转换又分为 C 风格转换和 C++ 风格转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)。
# 3.7.1 隐式类型转换
隐式类型转换由编译器自动完成,通常发生在赋值、表达式计算或函数调用时。
示例
int i = 3.14; // double 被隐式转换为 int,i 的值为 3(截断小数部分)
double d = i; // int 被隐式转换为 double,d 的值为 3.0
bool b = d; // double 被隐式转换为 bool,任何非零值为 true
2
3
# 3.7.2 显式类型转换
1.C风格转换,C 风格转换使用 (目标类型) 的语法。
double d = 3.14;
int e = (int)d; // 显式将 double 转换为 int
2
2.C++风格转换,C++ 提供了四种显式转换运算符,更加安全和明确。
static_cast:用于良定义的、低风险的转换(如数值类型转换、基类指针到派生类指针)。
const_cast:用于移除const或volatile属性。
dynamic_cast:主要用于安全地沿继承层次结构进行向下或交叉转换(需要多态类型)。
reinterpret_cast:用于低级的、依赖于实现的重新解释,非常危险(如将指针转换为整数)。
# 3.7.2.1 static_cast
用于基本数据类型之间的转换,以及具有继承关系的类之间的转换。
double f = 3.14;
int g = static_cast<int>(f); // 显式将 double 转换为 int
class Base {};
class Derived : public Base {};
Base* basePtr = new Derived;
Derived* derivedPtr = static_cast<Derived*>(basePtr); // 向下转型
2
3
4
5
6
7
# 3.7.2.2 dynamic_cast
用于具有多态性的类之间的转换(通常用于向下转型),转换失败时返回 nullptr。
class Base { virtual void foo() {} };
class Derived : public Base {};
Base* basePtr = new Derived;
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 向下转型
if (derivedPtr) {
cout << "转换成功" << endl;
} else {
cout << "转换失败" << endl;
}
2
3
4
5
6
7
8
9
10
# 3.7.2.3 const_cast
用于移除或添加 const 或 volatile 修饰符。
const int h = 10;
int* i = const_cast<int*>(&h); // 移除 const 修饰符
*i = 20; // 修改值(未定义行为,不推荐)
2
3
# 3.7.3.4 reinterpret_cast
用于低级别的类型转换,通常用于指针或整数之间的转换。
int j = 42;
int* ptr = &j;
long k = reinterpret_cast<long>(ptr); // 将指针转换为整数
2
3
# 3.7.3 类型转换建议
- 优先使用 C++ 风格转换: C++ 风格转换更加安全和明确,避免使用 C 风格转换。
- 避免不必要的转换: 类型转换可能导致数据丢失或未定义行为,尽量避免。
- 谨慎使用
const_cast: 移除const修饰符可能导致未定义行为,应谨慎使用。 - 使用
dynamic_cast进行安全的向下转型:在具有多态性的类之间转换时,优先使用dynamic_cast。
# 3.7.4 类型转换总结
- 隐式转换由编译器自动完成,显式转换需要程序员明确指定。
- C++ 提供了四种显式转换运算符:
static_cast、dynamic_cast、const_cast、reinterpret_cast。 - 优先使用 C++ 风格转换,避免不必要的转换,谨慎使用
const_cast。
# 3.7.5 综合案例与思考
综合案例:类型转换的实际应用与陷阱
#include <iostream>
#include <string>
using namespace std;
class Animal {
public:
virtual ~Animal() = default;
virtual string speak() const { return "..."; }
};
class Dog : public Animal {
public:
string speak() const override { return "汪汪!"; }
void fetch() const { cout << "捡球!" << endl; }
};
class Cat : public Animal {
public:
string speak() const override { return "喵喵~"; }
};
int main() {
// 1. 隐式转换的数据丢失
cout << "=== 隐式转换陷阱 ===" << endl;
int bigNum = 1000000;
short smallNum = bigNum; // 数据截断!
cout << "int: " << bigNum << " -> short: " << smallNum << endl;
double precise = 3.99;
int truncated = precise; // 小数部分丢失
cout << "double: " << precise << " -> int: " << truncated << endl;
// 2. static_cast:安全的数值转换
cout << "\n=== static_cast ===" << endl;
int total = 17, count = 5;
// 错误做法:整数除法丢失小数
cout << "整数除: " << total / count << endl;
// 正确做法:先转为double再除
cout << "浮点除: " << static_cast<double>(total) / count << endl;
// 3. dynamic_cast:安全的多态转换
cout << "\n=== dynamic_cast ===" << endl;
Animal* animals[] = { new Dog(), new Cat(), new Dog() };
for (int i = 0; i < 3; ++i) {
cout << "动物" << i << " 说: " << animals[i]->speak() << endl;
// 安全地尝试转为Dog
Dog* dog = dynamic_cast<Dog*>(animals[i]);
if (dog) {
cout << " 这是一只狗: ";
dog->fetch();
} else {
cout << " 这不是狗,跳过fetch()" << endl;
}
}
// 4. const_cast:移除const(谨慎使用)
cout << "\n=== const_cast ===" << endl;
const string greeting = "Hello";
// 当需要传给不接受const参数的旧API时
string& ref = const_cast<string&>(greeting);
cout << "const_cast结果: " << ref << endl;
// 注意:修改原本的const对象是未定义行为!
// 释放内存
for (auto* a : animals) delete a;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
案例知识融合:这个案例展示了C++类型转换的四种场景——隐式转换的数据丢失风险、static_cast解决整数除法精度问题、dynamic_cast实现安全的多态向下转型(配合nullptr检查)、以及const_cast移除const属性的谨慎使用。每种转换都有明确的适用场景和注意事项。
思考题:
- C风格的强制转换
(int)x和C++的static_cast<int>(x)有什么本质区别?为什么C++推荐使用后者? dynamic_cast失败时返回nullptr(指针)或抛出异常(引用),它的运行时开销来自哪里?什么时候可以用static_cast替代dynamic_cast?- 如果对一个真正的
const对象使用const_cast去掉const后修改它的值,会发生什么?为什么这是"未定义行为"?
# 3.8 练习题
# 3.8.1 类型转换案例
#include <iostream>
using namespace std;
class Base {
public:
virtual void foo() { cout << "Base::foo" << endl; }
};
class Derived : public Base {
public:
void foo() override { cout << "Derived::foo" << endl; }
};
int main() {
// 隐式转换
int a = 10;
double b = a;
cout << "b = " << b << endl;
// C 风格转换
double c = 3.14;
int d = (int)c;
cout << "d = " << d << endl;
// C++ 风格转换
double e = 3.14;
int f = static_cast<int>(e);
cout << "f = " << f << endl;
// dynamic_cast
Base* basePtr = new Derived;
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr) {
cout << "dynamic_cast 成功" << endl;
} else {
cout << "dynamic_cast 失败" << endl;
}
// const_cast
const int g = 10;
int* h = const_cast<int*>(&g);
*h = 20;
cout << "g = " << g << ", *h = " << *h << endl;
// reinterpret_cast
int i = 42;
int* ptr = &i;
long j = reinterpret_cast<long>(ptr);
cout << "j = " << j << endl;
delete basePtr;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 3.8.2 键盘数据输入
作用:用于从键盘获取数据
关键字:cin
语法: cin >> 变量
示例:
#include <iostream>
using namespace std;
int main(){
//整型输入
int a = 0;
cout << "请输入整型变量:" << endl;
cin >> a;
cout << a << endl;
//浮点型输入
double d = 0;
cout << "请输入浮点型变量:" << endl;
cin >> d;
cout << d << endl;
//字符型输入
char ch = 0;
cout << "请输入字符型变量:" << endl;
cin >> ch;
cout << ch << endl;
//字符串型输入
string str;
cout << "请输入字符串型变量:" << endl;
cin >> str;
cout << str << endl;
//布尔类型输入
bool flag = true;
cout << "请输入布尔型变量:" << endl;
cin >> flag;
cout << flag << endl;
return EXIT_SUCCESS;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 3.8.3 nullptr和NULL
以下是一个完整的示例,展示了 nullptr 和 NULL 的区别、使用场景以及相关问题:
#include <iostream>
using namespace std;
// 演示重载问题:一个函数接受整数,一个函数接受指针
void printValue(int num) {
cout << "整数: " << num << endl;
}
void printValue(int* ptr) {
if (ptr) {
cout << "指针: 指向的值是 " << *ptr << endl;
} else {
cout << "指针: 是空指针" << endl;
}
}
int main() {
cout << "=== nullptr 和 NULL 简单对比 ===" << endl << endl;
int number = 100;
int* ptr1 = NULL; // 传统方式
int* ptr2 = nullptr; // 现代方式
// 1. 基本使用对比
cout << "1. 基本使用:" << endl;
if (ptr1 == NULL) {
cout << "ptr1 是空指针" << endl;
}
if (ptr2 == nullptr) {
cout << "ptr2 是空指针" << endl;
}
// 2. 赋值非空值
cout << endl << "2. 指针赋值:" << endl;
ptr1 = &number;
cout << "ptr1 指向: " << *ptr1 << endl;
// 3. 重置为空
cout << endl << "3. 重置指针:" << endl;
ptr1 = nullptr; // 推荐用 nullptr
if (!ptr1) { // 检查是否为空
cout << "ptr1 已被重置为空" << endl;
}
// 4. 重载问题演示(主要区别!)
cout << endl << "4. 重载问题(主要区别):" << endl;
printValue(0); // 调用整数版本
printValue(&number); // 调用指针版本
// printValue(NULL); // 有歧义!在有些编译器会报错
printValue(nullptr); // 明确调用指针版本
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
这个案例演示的要点:
- 基本使用:
NULL和nullptr都可以表示空指针 - 安全性:
nullptr是类型安全的 - 可读性:
if(ptr1 == nullptr)比if(ptr1 == NULL)更明确 - 重载问题:
printValue(NULL)有歧义,printValue(nullptr)是明确的
关键区别总结
| 特性 | nullptr (C++11+) | NULL (传统C/C++) |
|---|---|---|
| 类型 | std::nullptr_t | 通常是int或long |
| 类型安全 | ✅ 指针类型安全 | ❌ 可能被当作整数 |
| 重载函数 | ✅ 明确调用指针版本 | ⚠️ 可能调用整数版本 |
| 模板推导 | ✅ 推导为nullptr_t | ❌ 推导为整数类型 |
| 可读性 | ✅ 明确表示空指针 | ⚠️ 含义不够明确 |
| 向后兼容 | ✅ 兼容C++11+ | ✅ 完全兼容 |
# 3.9 卷一改造增补:现代 C++ 类型补充(C++11~C++23)
本节为卷一新增。前 8 节讲的是「类 C 时代」的传统类型;本节补齐现代 C++ 中真正常用、几乎每个项目都会用到的类型工具。
# 3.9.1 auto 与 decltype:让编译器替你写类型
// C++03 写法
std::map<std::string, std::vector<int>>::const_iterator it = m.begin();
// C++11 起
auto it = m.begin(); // 类型推导
const auto& ref = expensive_call(); // 必须保留 const/&
decltype(x + y) z = x + y; // 取表达式类型
2
3
4
5
6
7
注意三处坑:
auto默认会丢掉引用和 const,需要时显式加const auto&、auto&&。auto x = {1, 2, 3};推导成std::initializer_list<int>,不是vector。- 函数返回值用
auto时,多个return语句类型必须一致,否则编译失败。
# 3.9.2 std::byte(C++17):取代 unsigned char 表示「裸字节」
#include <cstddef>
std::byte buf[1024]; // 明确:这是字节缓冲区,不是字符串
buf[0] = std::byte{0xAB};
auto v = std::to_integer<int>(buf[0]);
2
3
4
std::byte 只支持位运算、比较、to_integer<T>,不支持算术运算——从类型层面阻止「把字节当数字加减」的常见错误。处理网络协议、二进制文件、内存拷贝时一律用它。
# 3.9.3 std::optional<T>(C++17):表达「可能没有值」
#include <optional>
std::optional<int> parse_int(const std::string& s) {
try { return std::stoi(s); }
catch (...) { return std::nullopt; }
}
if (auto v = parse_int("42"); v.has_value()) {
std::cout << *v; // 解引用拿值
}
int x = parse_int("oops").value_or(-1);
2
3
4
5
6
7
8
9
10
用它替代:用 -1 表示「无效结果」、用空指针表示「没有」、用额外的 bool found 输出参数。
# 3.9.4 std::variant<T...>(C++17):类型安全的联合体
#include <variant>
std::variant<int, double, std::string> v;
v = 3.14;
v = std::string{"hello"};
// 访问方式 1:std::visit + lambda
std::visit([](auto&& arg) { std::cout << arg; }, v);
// 访问方式 2:std::get_if(不抛异常)
if (auto* p = std::get_if<std::string>(&v)) { /* ... */ }
2
3
4
5
6
7
8
9
替代传统 union + enum tag 的「手写代数数据类型」组合。
# 3.9.5 std::any(C++17):装任意类型
#include <any>
std::any a = 42;
a = std::string{"hello"};
auto s = std::any_cast<std::string>(a); // 取错类型会抛 bad_any_cast
2
3
4
用于跨 API 边界传未知类型(如插件系统)。日常业务代码请优先用 variant,因为 any 失去了静态类型检查。
# 3.9.6 std::string_view(C++17):零拷贝字符串视图
#include <string_view>
void log(std::string_view sv) { /* 不拷贝、不分配 */ }
log("literal"); // OK
log(std::string{"hello"}); // OK
log(some_char_array); // OK
2
3
4
5
形参传入「只读字符串」时,统一用 string_view 替代 const std::string&——既能省一次构造,又能兼容 C 字符串字面量。生命周期警告:string_view 不持有数据,不要让它指向已销毁的临时 string。
# 3.9.7 <cstdint> 固定宽度整数:跨平台安全
#include <cstdint>
int32_t count; // 任何平台都恰好 32 位
uint64_t size; // 任何平台都恰好 64 位
int_fast16_t i; // 至少 16 位,本平台最快的整数
2
3
4
写文件格式、网络协议、跨平台代码时,禁止用 int/long,统一使用 int32_t/int64_t 等,避免 Windows/Linux/嵌入式平台位宽不一致导致的诡异 bug。
# 3.10 新手陷阱 Top 5
| # | 陷阱 | 错误示例 | 正确做法 |
|---|---|---|---|
| 1 | 隐式窄化 | int i = 3.14; 默默截断 | C++11 起用 int i{3.14}; 编译报错 |
| 2 | 有符号 / 无符号比较 | if (i < v.size()) 警告 | i 改用 size_t,或用 std::ssize |
| 3 | char 当数字用 | char 在不同平台有/无符号性不同 | 用 int8_t / uint8_t |
| 4 | float 比较相等 | if (a == 0.1) 永假 | std::abs(a-b) < eps 或 <=> |
| 5 | auto 丢掉引用 | auto x = vec[0]; 触发拷贝 | auto& x = vec[0]; |