3.1 简单变量

在第2章中,我们已经接触了变量的声明和使用。本章将深入探讨C++的基本数据类型,首先从用于存储数字和字符的简单变量开始。简单变量是C++中存储数据的基本单元。

3.1.1 变量名

变量名是赋予内存位置的标识符,用于访问存储在该位置的数据。在C++中,选择有意义的变量名是良好编程实践的一部分。

命名规则:

  1. 字符集: 变量名只能包含字母(大小写)、数字和下划线 _
  2. 首字符: 名称的第一个字符不能是数字。
  3. 区分大小写: C++是大小写敏感的,myVariablemyvariable 是两个不同的变量名。
  4. 不能是关键字: 不能使用C++关键字(如 int, double, return, if, class 等)作为变量名。
  5. 长度限制: C++对名称的长度没有硬性规定,但长名称可能会在某些旧编译器或链接器上遇到问题。通常,有意义且不过于冗长的名称是最好的。
  6. 下划线的使用:
    • 以两个下划线 __ 开头或以下划线和大写字母 _A_Z 开头的名称被保留给编译器及其使用的资源使用。
    • 以下划线 _ 开头的名称被保留用作全局标识符。
    • 虽然在某些情况下可以使用以下划线开头的名称(例如在函数内部),但最好避免这种用法,以防与系统使用的名称冲突。

命名约定 (非强制,但推荐):

  • 有意义: 变量名应反映其存储的数据或用途(例如 numberOfStudents, userName, totalScore)。
  • 驼峰命名法 (Camel Case): 从第二个单词开始,每个单词的首字母大写(例如 myVariableName, studentAge)。这是C++中常见的风格。
  • 下划线分隔 (Snake Case): 使用下划线分隔单词(例如 my_variable_name, student_age)。这也是一种常见的风格。
  • 选择一种风格并保持一致。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

int main() {
// 合法的变量名
int age;
double accountBalance;
char first_initial;
long long populationOfEarth;
int _internal_counter; // 合法,但不推荐在全局使用
int value2;

// 非法的变量名
// int 1stPlace; // 不能以数字开头
// double account Balance; // 不能包含空格
// int return; // 不能是关键字
// char my-char; // 不能包含连字符 '-'

// 区分大小写
int count = 10;
int Count = 20;
std::cout << "count: " << count << std::endl; // 输出 10
std::cout << "Count: " << Count << std::endl; // 输出 20

return 0;
}

3.1.2 整型

整型 (Integer) 是没有小数部分的数字。C++提供了多种整型类型来存储整数,它们的主要区别在于占用的内存空间大小以及能够表示的数值范围。

计算机内存由称为位 (bit) 的单元组成。8个位组成一个**字节 (byte)**。每个位可以表示两种状态(通常是0或1)。字节是内存中最小的可寻址单元,意味着每个字节都有一个唯一的地址。

不同的整型类型使用不同数量的字节来存储值。使用的字节数越多,可以表示的整数范围就越大。

基本整型类型:

  • short
  • int
  • long
  • long long (C++11 新增)

我们将在下一节详细讨论这些类型。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

int main() {
// 声明不同类型的整型变量
short smallNumber;
int standardInteger;
long largeInteger;
long long veryLargeInteger; // 需要 C++11 或更高版本

// 赋值
smallNumber = 10;
standardInteger = 10000;
largeInteger = 1000000;
veryLargeInteger = 10000000000LL; // LL 后缀表示 long long

std::cout << "short: " << smallNumber << std::endl;
std::cout << "int: " << standardInteger << std::endl;
std::cout << "long: " << largeInteger << std::endl;
std::cout << "long long: " << veryLargeInteger << std::endl;

return 0;
}

3.1.3 整型short、int、long和long long

C++标准规定了各种整型类型的最小尺寸(占用的内存位数),但具体尺寸可能因编译器和操作系统而异。

标准规定的最小尺寸:

  • short: 至少16位。
  • int: 至少和 short 一样大,通常是系统处理效率最高的整数长度(例如,在32位系统上通常是32位,64位系统上可能是32位或64位)。
  • long: 至少32位,且至少和 int 一样大。
  • long long: 至少64位,且至少和 long 一样大。

如何查看具体尺寸:

可以使用 sizeof 运算符来查看特定类型在你的系统上占用的字节数。

用法与示例:

1
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
#include <iostream>
#include <climits> // 包含整型限制信息 (如 INT_MAX)

int main() {
// 使用 sizeof 查看各种类型占用的字节数
std::cout << "Size of short: " << sizeof(short) << " bytes" << std::endl;
std::cout << "Size of int: " << sizeof(int) << " bytes" << std::endl;
std::cout << "Size of long: " << sizeof(long) << " bytes" << std::endl;
std::cout << "Size of long long: " << sizeof(long long) << " bytes" << std::endl;
std::cout << std::endl;

// 查看 int 类型的最大值 (需要 <climits>)
std::cout << "Maximum value for int: " << INT_MAX << std::endl;
// 类似地,有 SHRT_MAX, LONG_MAX, LLONG_MAX 等

// 声明和初始化
short s_value = 32767; // 通常是 short 的最大值 (如果 short 是 16 位)
int i_value = 2000000000;
long l_value = 1000000000L; // L 后缀表示 long (可选,但有时有助于清晰)
long long ll_value = 50000000000LL; // LL 后缀表示 long long

std::cout << "short value: " << s_value << std::endl;
std::cout << "int value: " << i_value << std::endl;
std::cout << "long value: " << l_value << std::endl;
std::cout << "long long value: " << ll_value << std::endl;

// 溢出示例 (行为是未定义的或回绕)
short max_short = SHRT_MAX;
std::cout << "Max short: " << max_short << std::endl;
max_short = max_short + 1; // 尝试超出最大值
std::cout << "Max short + 1: " << max_short << std::endl; // 可能变成负数

return 0;
}

注意: 当一个整数值超出了其类型所能表示的最大范围时,会发生溢出 (Overflow)**。对于有符号整数溢出,C++标准规定其行为是未定义的 (Undefined Behavior)**,这意味着任何事情都可能发生(程序崩溃、得到奇怪的结果等)。对于无符号整数溢出,行为是定义好的(通常是回绕,即从0重新开始)。

3.1.4 无符号类型

对于每种整型(short, int, long, long long),都存在一个对应的无符号 (unsigned) 版本。无符号类型只能存储非负整数(0和正数)。

通过在类型名前加上 unsigned 关键字来声明无符号类型。

特点:

  • 范围: 在相同的字节数下,无符号类型可以表示的最大值大约是有符号类型的两倍,因为它们不需要用一位来表示正负号。范围从 0 开始。
  • 用途: 当你知道一个变量永远不会是负数时(例如,计数器、数组索引、人口数量等),使用无符号类型可以增大其可表示的正数范围。
  • 回绕 (Wrap Around): 当无符号整数的值超出其最大范围时,它会从0重新开始(模运算)。例如,如果一个 unsigned short 的最大值是 65535,那么 65535 + 1 会变成 0。同样,0 - 1 会变成 65535

用法与示例:

1
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
#include <iostream>
#include <climits> // 包含 UINT_MAX 等

int main() {
// 声明无符号类型
unsigned short us_value;
unsigned int ui_value;
unsigned long ul_value;
unsigned long long ull_value;

// 查看大小和范围
std::cout << "Size of unsigned int: " << sizeof(unsigned int) << " bytes" << std::endl;
std::cout << "Maximum value for unsigned int: " << UINT_MAX << std::endl;
std::cout << std::endl;

// 赋值
us_value = 65535; // 通常是 unsigned short 的最大值 (如果 short 是 16 位)
ui_value = 4000000000U; // U 后缀表示 unsigned int
ul_value = 8000000000UL; // UL 后缀
ull_value = 18000000000000000000ULL; // ULL 后缀

std::cout << "unsigned short: " << us_value << std::endl;
std::cout << "unsigned int: " << ui_value << std::endl;
std::cout << "unsigned long: " << ul_value << std::endl;
std::cout << "unsigned long long: " << ull_value << std::endl;
std::cout << std::endl;

// 回绕示例
unsigned short test_wrap = USHRT_MAX;
std::cout << "Max unsigned short: " << test_wrap << std::endl;
test_wrap = test_wrap + 1;
std::cout << "Max unsigned short + 1: " << test_wrap << std::endl; // 变为 0

test_wrap = 0;
std::cout << "Unsigned short = 0" << std::endl;
test_wrap = test_wrap - 1;
std::cout << "Unsigned short - 1: " << test_wrap << std::endl; // 变为 USHRT_MAX

return 0;
}

注意: 混合使用有符号和无符号整数进行运算时要特别小心,因为有符号数可能会被隐式转换为无符号数,导致意外的结果,尤其是在比较运算中。

3.1.5 选择整型类型

在选择使用哪种整型类型时,应考虑以下因素:

  1. 数值范围: 确保所选类型能够容纳你程序中需要存储的最大(和最小,如果是有符号)整数值。如果数值可能很大,优先考虑 longlong long。如果数值永远非负,可以考虑 unsigned 版本以获得更大的正数范围。
  2. 内存消耗: 如果内存非常宝贵(例如在嵌入式系统或处理大量数据时),并且确定数值范围较小,可以使用 short 来节省内存。
  3. 性能: int 通常被认为是计算机处理效率最高的整数类型。除非有明确的范围或内存需求,否则 int 是一个不错的默认选择。
  4. 可移植性: int 的大小可能因系统而异。如果需要确保在不同系统上具有固定的大小,可以使用 C++11 引入的固定宽度整数类型(在 <cstdint> 头文件中定义),例如 int16_t, uint32_t, int64_t 等。
  5. 代码清晰度: 选择最能自然表达意图的类型。如果变量代表一个永远不会为负的计数,unsigned int 可能比 int 更能表达这个意图。

一般建议:

  • 如果不需要存储负数,且需要更大的正数范围,或者变量逻辑上就是无符号的(如计数),使用 unsigned
  • 如果数值范围不大,且没有特殊内存或性能要求,int 是最常用的选择。
  • 如果 int 可能不够大,使用 long longlong 在某些系统上可能和 int 大小相同,而 long long 保证至少64位。
  • 只有在内存非常受限且确定数值很小时才使用 short
  • 为了明确性和跨平台兼容性,可以考虑使用 <cstdint> 中的固定宽度类型。

用法与示例 (选择):

1
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
#include <iostream>
#include <cstdint> // 包含固定宽度整数类型

int main() {
// 场景 1: 存储学生年龄 (通常不大,非负)
unsigned int studentAge = 20; // unsigned int 或 int 都可以,unsigned 更明确非负

// 场景 2: 循环计数器 (通常用 int)
for (int i = 0; i < 10; ++i) {
// ...
}

// 场景 3: 存储文件大小 (可能非常大,非负)
unsigned long long fileSize = 5000000000ULL; // 需要大范围,非负

// 场景 4: 需要精确 32 位有符号整数 (跨平台)
int32_t preciseCounter = -123456789;

// 场景 5: 内存受限,存储少量选项 (0-100)
// short optionCode = 5; // 如果 short 足够且内存关键

std::cout << "Age: " << studentAge << std::endl;
std::cout << "File Size: " << fileSize << " bytes" << std::endl;
std::cout << "Precise Counter: " << preciseCounter << std::endl;

return 0;
}

3.1.6 整型字面值

整型字面值 (Integer Literal) 是直接写在代码中的整数常量,例如 10, 0, -5, 42。C++允许以不同的进制(基数)书写整型字面值:

  1. 十进制 (Decimal): 最常见的形式,以非零数字开头(除非是数字0本身)。例如:10, 255, 0, 12345
  2. 八进制 (Octal):0 开头。只包含数字 0-7。例如:012 (等于十进制的 10),077 (等于十进制的 63)。现代C++中应谨慎使用,容易与十进制混淆。
  3. 十六进制 (Hexadecimal):0x0X 开头。包含数字 0-9 和字母 a-f (或 A-F),大小写不敏感。例如:0xA (等于十进制的 10),0xFF (等于十进制的 255),0x1a2b。常用于表示内存地址或位模式。
  4. 二进制 (Binary) (C++14):0b0B 开头。只包含数字 0 和 1。例如:0b1010 (等于十进制的 10),0b11111111 (等于十进制的 255)。

后缀 (Suffixes):

可以通过在字面值后面添加后缀来显式指定其类型:

  • uU: 表示 unsigned 类型 (unsigned int, unsigned long, unsigned long long)。
  • lL: 表示 longunsigned long 类型。
  • llLL: 表示 long longunsigned long long 类型 (C++11)。

后缀可以组合使用,例如 ul, UL, ull, ULL, lu, llu 等(顺序和大小写不重要)。

用法与示例:

1
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
#include <iostream>

int main() {
int decimal_val = 100;
int octal_val = 0144; // 1*64 + 4*8 + 4*1 = 64 + 32 + 4 = 100 (十进制)
int hex_val = 0x64; // 6*16 + 4*1 = 96 + 4 = 100 (十进制)
// int binary_val = 0b1100100; // C++14: 64 + 32 + 4 = 100 (十进制)

std::cout << "Decimal: " << decimal_val << std::endl;
std::cout << "Octal (0144): " << octal_val << std::endl;
std::cout << "Hexadecimal (0x64): " << hex_val << std::endl;
// std::cout << "Binary (0b1100100): " << binary_val << std::endl; // 需要 C++14 编译器

// 使用后缀
unsigned int u_val = 100U;
long l_val = 200000L;
unsigned long ul_val = 300000UL;
long long ll_val = 4000000000LL;
unsigned long long ull_val = 5000000000ULL;

std::cout << "Unsigned int: " << u_val << std::endl;
std::cout << "Long: " << l_val << std::endl;
std::cout << "Unsigned long: " << ul_val << std::endl;
std::cout << "Long long: " << ll_val << std::endl;
std::cout << "Unsigned long long: " << ull_val << std::endl;

return 0;
}

3.1.7 C++如何确定常量的类型

当你写下一个整型字面值(常量)而没有指定后缀时,C++编译器会根据其值来推断其类型。

规则:

  1. 十进制常量: 编译器会选择能容纳该值的最小的有符号类型,依次尝试:int, long, long long (C++11)。例如:
    • 100 会被认为是 int (如果 int 能容纳)。
    • 3000000000 会被认为是 long (如果 int 不能容纳但 long 可以),或者 long long (如果 intlong 都不能容纳但 long long 可以)。
  2. 八进制或十六进制常量: 编译器会选择能容纳该值的最小类型,依次尝试:int, unsigned int, long, unsigned long, long long (C++11), unsigned long long (C++11)。注意这里包含了 unsigned 类型。

为什么要知道这个?

这在涉及类型转换和函数重载时可能很重要。如果你传递一个常量给函数,它的默认类型可能会影响哪个重载版本被调用。

用法与示例:

1
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
#include <iostream>
#include <typeinfo> // 用于 typeid (仅作演示)

void process(int n) {
std::cout << "Processing int: " << n << std::endl;
}

void process(unsigned int n) {
std::cout << "Processing unsigned int: " << n << std::endl;
}

void process(long n) {
std::cout << "Processing long: " << n << std::endl;
}

void process(unsigned long n) {
std::cout << "Processing unsigned long: " << n << std::endl;
}

void process(long long n) { // C++11
std::cout << "Processing long long: " << n << std::endl;
}

void process(unsigned long long n) { // C++11
std::cout << "Processing unsigned long long: " << n << std::endl;
}


int main() {
std::cout << "Type of 100: ";
process(100); // 通常调用 process(int)

// 假设 int 是 32 位,最大值约 21 亿
std::cout << "Type of 3000000000: ";
// 如果 int 是 32 位,这个值超出了 int 范围
// 编译器会尝试 long 或 long long
process(3000000000); // 可能调用 process(long) 或 process(long long)

std::cout << "Type of 0xFFFFFFFF: ";
// 这个十六进制数 (等于 4294967295)
// 如果 int 是 32 位,它超出了有符号 int 的范围
// 编译器会尝试 unsigned int, long, unsigned long...
process(0xFFFFFFFF); // 可能调用 process(unsigned int) 或 process(unsigned long)等

// 使用后缀明确类型
std::cout << "Type of 150L: ";
process(150L); // 明确调用 process(long)

std::cout << "Type of 200U: ";
process(200U); // 明确调用 process(unsigned int)

std::cout << "Type of 5000000000LL: ";
process(5000000000LL); // 明确调用 process(long long)

return 0;
}

关键点: 如果不确定常量会被推断成什么类型,或者想要确保它是特定类型,最好使用后缀。

3.1.8 char类型:字符和小整数

char 类型是另一种整型类型,它被设计用来存储**字符 (character)**,例如字母、数字、标点符号等。

特点:

  • 大小: char 通常占用 1 个字节(8位)的内存。这是C++标准保证的 (sizeof(char) 总是 1)。
  • 字符表示: 计算机内部使用数值编码(如 ASCII 或 Unicode 的子集)来表示字符。char 变量存储的是这些字符对应的整数编码。
  • 字符字面值: 使用单引号 ' ' 括起来表示单个字符字面值,例如 'A', 'a', '5', '?', '\n' (换行符)。
  • 整数类型: char 本质上仍然是一个整数类型。它可以参与算术运算。
  • 有符号 vs 无符号: char 类型具体是有符号 (signed char) 还是无符号 (unsigned char) 取决于编译器实现。如果你需要明确,可以直接使用 signed charunsigned char
    • signed char: 范围通常是 -128 到 127。
    • unsigned char: 范围通常是 0 到 255。

用法与示例:

1
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
#include <iostream>

int main() {
char grade = 'A'; // 存储字符 'A'
char initial = 'J';
char symbol = '$';
char newline = '\n'; // 存储换行符 (转义序列)

std::cout << "Your grade is: " << grade << std::endl;
std::cout << "Initial: " << initial << std::endl;
std::cout << "Symbol: " << symbol << std::endl;
std::cout << "Printing a newline:" << newline; // 输出一个换行
std::cout << "After newline." << std::endl;

// cout 会将 char 类型解释为字符进行打印
// 如果想看到字符对应的整数编码,需要进行类型转换
std::cout << "The integer code for 'A' is: " << int(grade) << std::endl;
std::cout << "The integer code for '$' is: " << static_cast<int>(symbol) << std::endl;

// char 作为小整数使用
char small_num = 65; // 65 是 ASCII 码中 'A' 的值
std::cout << "Character with code 65: " << small_num << std::endl; // 输出 'A'

// 算术运算
char next_char = grade + 1; // 'A' 的编码加 1 得到 'B' 的编码
std::cout << "Character after 'A': " << next_char << std::endl; // 输出 'B'

// 输入字符
char response;
std::cout << "Enter a character: ";
std::cin >> response; // 读取一个字符
std::cout << "You entered: " << response << std::endl;
std::cout << "Its code is: " << int(response) << std::endl;

// 明确使用 signed/unsigned char
signed char sc = -5;
unsigned char uc = 250;
std::cout << "Signed char: " << int(sc) << std::endl; // 转换为 int 打印数值
std::cout << "Unsigned char: " << int(uc) << std::endl;

return 0;
}

转义序列 (Escape Sequence):
以反斜杠 \ 开头的特殊字符序列,用于表示无法直接输入的字符或具有特殊含义的字符。常用转义序列包括:

  • \n: 换行符
  • \t: 水平制表符 (Tab)
  • \r: 回车符
  • \\: 反斜杠本身
  • \': 单引号
  • \": 双引号
  • \?: 问号
  • \0: 空字符 (Null character)
  • \xhh: 用两位十六进制数 hh 表示字符
  • \ooo: 用最多三位八进制数 ooo 表示字符

3.1.9 bool类型

bool 类型是C++中的布尔 (Boolean) 类型,用于表示逻辑值:真 (true) 或 **假 (false)**。

特点:

  • 取值: bool 变量只能存储两个值:truefalse。这两个是C++关键字。
  • 整数转换:
    • 在需要整数的地方,true 会被转换为 1false 会被转换为 0
    • 在需要布尔值的地方,任何非零整数值会被转换为 true,零值会被转换为 false。指针类型也可以转换为 bool(空指针为 false,非空指针为 true)。
  • 大小: bool 类型的大小没有严格规定,但通常是 1 个字节,即使它只需要 1 位来存储信息。
  • 用途: 主要用于存储条件判断(如 if 语句、循环条件)的结果。

用法与示例:

1
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
#include <iostream>

int main() {
bool isReady = true; // 声明并初始化为 true
bool hasError = false; // 声明并初始化为 false
bool isEmpty; // 未初始化 (值不确定)

isEmpty = false; // 赋值

std::cout << "isReady (default output): " << isReady << std::endl; // 通常输出 1
std::cout << "hasError (default output): " << hasError << std::endl; // 通常输出 0
std::cout << "isEmpty (default output): " << isEmpty << std::endl; // 输出 0

// 使用 std::boolalpha 控制符以 "true"/"false" 形式输出
std::cout << std::boolalpha;
std::cout << "isReady (boolalpha): " << isReady << std::endl; // 输出 true
std::cout << "hasError (boolalpha): " << hasError << std::endl; // 输出 false
std::cout << "isEmpty (boolalpha): " << isEmpty << std::endl; // 输出 false
std::cout << std::noboolalpha; // 关闭 boolalpha 格式

// bool 和整数转换
int ready_int = isReady; // ready_int 变为 1
int error_int = hasError; // error_int 变为 0
std::cout << "isReady as int: " << ready_int << std::endl;
std::cout << "hasError as int: " << error_int << std::endl;

bool from_int_non_zero = 100; // 100 (非零) 转换为 true
bool from_int_zero = 0; // 0 转换为 false
std::cout << std::boolalpha;
std::cout << "Bool from 100: " << from_int_non_zero << std::endl; // 输出 true
std::cout << "Bool from 0: " << from_int_zero << std::endl; // 输出 false

// 在条件语句中使用
if (isReady) {
std::cout << "System is ready." << std::endl;
} else {
std::cout << "System is not ready." << std::endl;
}

if (!hasError) { // ! 是逻辑非运算符
std::cout << "No errors detected." << std::endl;
} else {
std::cout << "An error occurred." << std::endl;
}

return 0;
}

3.2 const限定符

现在我们来探讨一种在C++中创建常量 (Constant) 的更可靠的方法:使用 const 限定符。常量是指在程序运行期间其值不能被修改的量。

const 是一个**类型限定符 (Type Qualifier)**,它用于修改变量的声明,使其成为只读。

作用:

  • 创建符号常量: const 允许你为常量值(如圆周率、最大尝试次数等)赋予一个有意义的名称,提高代码的可读性和可维护性。
  • 防止意外修改: 一旦变量被声明为 const 并初始化后,编译器会阻止任何试图修改该变量值的代码。这有助于防止因意外赋值而导致的错误。
  • 类型安全: 与使用 #define 创建宏常量相比,const 常量具有明确的数据类型,编译器可以对其进行类型检查,更加安全。
  • 作用域: const 常量遵循标准的作用域规则(例如,在函数内部定义的 const 常量只在该函数内有效),而 #define 宏是全局替换。

声明和初始化:

声明 const 变量的语法是在类型名前或类型名后加上 const 关键字。关键在于,const 变量必须在声明时进行初始化,因为之后就不能再给它赋值了。

1
2
3
const type variableName = value; 
// 或者
type const variableName = value; // 两种写法等效,第一种更常见

用法与示例:

1
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
#include <iostream>

int main() {
// 声明并初始化 const 变量
const int MONTHS_IN_YEAR = 12; // 常量,表示一年中的月份数
const double PI = 3.14159;
const char* GREETING = "Hello, world!"; // 指向常量字符串的常量指针 (更复杂,后续章节详解)

// 尝试修改 const 变量会导致编译错误
// MONTHS_IN_YEAR = 13; // 错误! 不能给 const 变量赋值

std::cout << "Months in a year: " << MONTHS_IN_YEAR << std::endl;

double radius = 5.0;
double circumference = 2 * PI * radius; // 使用 const 常量 PI
std::cout << "Circumference: " << circumference << std::endl;

// const 常量也遵循作用域规则
if (true) {
const int MAX_TRIES = 3; // 只在 if 块内有效
std::cout << "Max tries inside block: " << MAX_TRIES << std::endl;
}
// std::cout << MAX_TRIES; // 错误! MAX_TRIES 在此作用域外不可见

// 必须在声明时初始化
// const int DAYS_IN_WEEK; // 错误! const 变量需要初始化
// DAYS_IN_WEEK = 7;

return 0;
}

const#define 的比较:

在 C 语言中,通常使用 #define 预处理器指令来创建符号常量:

1
#define MONTHS_IN_YEAR 12 // C 风格宏定义

虽然 #define 也能达到类似目的,但 const 在 C++ 中通常是更好的选择:

  1. 类型安全: const 常量有明确的数据类型,编译器会进行类型检查。#define 只是简单的文本替换,没有类型信息,可能导致意外错误。
  2. 作用域: const 常量遵循 C++ 的作用域规则,可以创建局部常量。#define 宏通常是全局的(从定义点到文件尾),容易造成命名冲突。
  3. 调试: const 常量在调试器中通常可见,有符号名。#define 宏在编译前就被替换掉了,调试时可能只能看到替换后的字面值。
  4. 类作用域: const 可以用于定义类作用域内的常量(类成员),而 #define 不能直接做到这一点。

一般建议: 在 C++ 中,优先使用 const 来定义符号常量,而不是 #define

3.3 浮点数

浮点数是C++中用于表示带小数部分的数字(实数)的类型。它们可以表示非常大或非常小的数值,以及整数无法表示的小数。

3.3.1 书写浮点数

C++允许使用两种方式来书写浮点字面值(常量):

  1. 标准小数点表示法 (Standard Decimal Point Notation):

    • 包含一个小数点。
    • 例如:12.34, 0.0, 99., .5 (等同于 0.5), -1.67
  2. E表示法 (E Notation) 或科学记数法 (Scientific Notation):

    • 用于表示非常大或非常小的数。
    • 格式为:mantissaEexponentmantissaeexponent
    • mantissa (尾数) 是一个数字(可以带小数点)。
    • Ee 表示 “乘以10的…次幂”。
    • exponent (指数) 是一个整数(可以为负)。
    • 例如:
      • 3.45E6 表示 3.45 * 106 (即 3,450,000)。
      • 2.5e-4 表示 2.5 * 10-4 (即 0.00025)。
      • 9E12 表示 9 * 1012
      • -1.23e+3 表示 -1.23 * 103 (即 -1230)。

注意:

  • E表示法中,Ee 前后不能有空格。
  • 指数必须是整数。

用法与示例:

1
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
#include <iostream>

int main() {
// 标准小数点表示法
double price = 99.99;
double temperature = -15.5;
double small_fraction = .25; // 等同于 0.25

// E 表示法
double earth_mass = 5.972E24; // 地球质量 (kg)
double electron_charge = -1.602e-19; // 电子电荷 (库仑)
double large_number = 1.0e9; // 10 亿

std::cout << "Price: " << price << std::endl;
std::cout << "Temperature: " << temperature << std::endl;
std::cout << "Small fraction: " << small_fraction << std::endl;

// 默认情况下,cout 可能对非常大或小的数自动使用科学记数法输出
std::cout << "Earth mass: " << earth_mass << " kg" << std::endl;
std::cout << "Electron charge: " << electron_charge << " C" << std::endl;
std::cout << "Large number: " << large_number << std::endl;

// 可以使用 <iomanip> 控制输出格式 (后续章节会详细介绍)
std::cout.setf(std::ios_base::fixed, std::ios_base::floatfield); // 设置为定点表示法
std::cout << "Large number (fixed): " << large_number << std::endl;

return 0;
}

3.3.2 浮点类型

C++提供了三种浮点类型,它们在精度(有效位数)和存储范围上有所不同:

  1. float: 单精度浮点数。通常占用 4 个字节 (32位)。精度较低,范围较小。
  2. double: 双精度浮点数。通常占用 8 个字节 (64位)。精度和范围比 float 大很多。这是C++中最常用的浮点类型,浮点常量(如 3.14)默认就是 double 类型。
  3. long double: 扩展精度浮点数。通常占用 8、10、12 或 16 个字节,具体取决于编译器和系统。提供最高的精度和最大的范围。

精度和范围:

  • 精度 (Precision): 指的是可以表示的有效数字的位数。
    • float 通常保证至少 6 位有效数字。
    • double 通常保证至少 15 位有效数字。
    • long double 通常提供比 double 更高的精度。
  • 范围 (Range): 指的是可以表示的最小和最大数值。double 的范围远大于 floatlong double 的范围通常更大。

可以使用 <cfloat> (或 C 语言的 <float.h>) 头文件中的常量来查看具体系统的精度和范围限制(例如 FLT_DIG, DBL_DIG, LDBL_DIG 表示有效位数)。

用法与示例:

1
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
#include <iostream>
#include <cfloat> // 包含浮点数限制信息

int main() {
// 查看各种类型占用的字节数
std::cout << "Size of float: " << sizeof(float) << " bytes" << std::endl;
std::cout << "Size of double: " << sizeof(double) << " bytes" << std::endl;
std::cout << "Size of long double: " << sizeof(long double) << " bytes" << std::endl;
std::cout << std::endl;

// 查看精度 (有效位数)
std::cout << "Digits of precision for float: " << FLT_DIG << std::endl;
std::cout << "Digits of precision for double: " << DBL_DIG << std::endl;
std::cout << "Digits of precision for long double: " << LDBL_DIG << std::endl;
std::cout << std::endl;

// 声明和初始化
float f_pi = 3.14159265f; // f 后缀表示 float
double d_pi = 3.141592653589793; // 默认是 double
long double ld_pi = 3.14159265358979323846L; // L 后缀表示 long double

std::cout.precision(20); // 设置 cout 输出精度以便观察差异

std::cout << "float pi: " << f_pi << std::endl;
std::cout << "double pi: " << d_pi << std::endl;
std::cout << "long double pi: " << ld_pi << std::endl;

return 0;
}

选择建议:

  • 除非有特别的内存或性能考虑,并且确定 float 的精度足够,否则**优先使用 double**。它是C++浮点计算的常用类型。
  • 当需要极高的精度或非常大的数值范围时,使用 long double

3.3.3 浮点常量

浮点常量(字面值)就是直接写在代码中的浮点数值,例如 3.14, 1.0, -2.5e8

默认类型:
默认情况下,C++将不带后缀的浮点常量视为 double 类型。

后缀 (Suffixes):
可以通过添加后缀来显式指定浮点常量的类型:

  • fF: 表示 float 类型。
  • lL: 表示 long double 类型。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <typeinfo> // 用于 typeid (仅作演示)

int main() {
// 默认类型是 double
std::cout << "Type of 3.14: " << typeid(3.14).name() << std::endl;
std::cout << "Type of 1.0e-5: " << typeid(1.0e-5).name() << std::endl;

// 使用后缀指定类型
std::cout << "Type of 3.14f: " << typeid(3.14f).name() << std::endl; // float
std::cout << "Type of 3.14F: " << typeid(3.14F).name() << std::endl; // float
std::cout << "Type of 3.14l: " << typeid(3.14l).name() << std::endl; // long double
std::cout << "Type of 3.14L: " << typeid(3.14L).name() << std::endl; // long double

// 赋值时的类型匹配
float price = 99.99f; // 使用 f 后缀避免从 double 到 float 的可能精度损失警告
double weight = 75.5; // 75.5 是 double, 赋值给 double 变量
long double distance = 1.23e10L; // 使用 L 后缀

std::cout << "Price: " << price << std::endl;
std::cout << "Weight: " << weight << std::endl;
std::cout << "Distance: " << distance << std::endl;

return 0;
}

(注意: typeid().name() 的输出可能因编译器而异,但可以大致看出类型)

在将常量赋给 float 变量时,使用 fF 后缀是一个好习惯,可以避免编译器产生关于从 double 转换到 float 可能丢失精度的警告。

3.3.4 浮点数的优缺点

浮点数在表示实数方面非常有用,但也存在一些固有的限制和需要注意的地方。

优点:

  1. 表示范围广: 可以表示比整型大得多或小得多的数值。
  2. 表示小数: 可以表示整数无法表示的小数部分。
  3. 标准化: 大多数现代计算机都遵循 IEEE 754 标准来表示和处理浮点数,这提高了可移植性。

缺点:

  1. 精度限制: 浮点数只能近似地表示大多数实数。由于内部使用二进制表示,某些在十进制下看起来很精确的小数(如 0.1)在二进制浮点表示中可能是无限循环小数,只能存储一个近似值。这会导致微小的**舍入误差 (Rounding Error)**。
  2. 比较困难: 由于精度限制,直接使用 == 来比较两个浮点数是否相等通常是不可靠的。微小的舍入误差可能导致逻辑上应该相等的两个数在内部表示上略有不同。比较浮点数时,通常应该检查它们的差值是否在一个很小的容差 (Tolerance) 范围内。
  3. 运算速度: 浮点运算通常比整型运算慢(尽管现代处理器有专门的浮点单元来优化)。
  4. 特殊值: IEEE 754 标准定义了一些特殊值,如 NaN (Not a Number,例如 0.0/0.0 的结果) 和无穷大 (Infinity,例如 1.0/0.0 的结果),需要在使用时注意处理。

用法与示例 (精度问题和比较):

1
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
#include <iostream>
#include <cmath> // 为了 fabs (计算绝对值)
#include <iomanip> // 为了 setprecision

int main() {
// 精度问题示例
double a = 0.1;
double b = 0.2;
double sum = a + b; // 理论上应该是 0.3

std::cout << std::setprecision(20); // 显示更多小数位
std::cout << "a = " << a << std::endl;
std::cout << "b = " << b << std::endl;
std::cout << "a + b = " << sum << std::endl; // 输出可能不是精确的 0.3

if (sum == 0.3) {
std::cout << "Comparison (==): sum is equal to 0.3" << std::endl;
} else {
std::cout << "Comparison (==): sum is NOT equal to 0.3" << std::endl; // 很可能执行这里
}

// 正确的比较方法:检查差值是否在容差范围内
const double TOLERANCE = 1e-9; // 定义一个很小的容差值
if (std::fabs(sum - 0.3) < TOLERANCE) {
std::cout << "Comparison (tolerance): sum is close enough to 0.3" << std::endl; // 应该执行这里
} else {
std::cout << "Comparison (tolerance): sum is NOT close enough to 0.3" << std::endl;
}

// 另一个例子
float x = 1.0f / 3.0f;
float y = 3.0f * x;
std::cout << "y = 3.0f * (1.0f / 3.0f) = " << y << std::endl; // 可能不是精确的 1.0

return 0;
}

总结: 在使用浮点数时,要意识到它们是近似值,并避免直接进行相等性比较。在需要精确计算(如金融计算)的场合,可能需要使用专门的库或定点数表示法。对于大多数科学和工程计算,double 提供了足够的精度和范围。

3.4 C++算术运算符

C++提供了丰富的运算符来执行算术计算。本节将介绍基本的算术运算符、它们的优先级和结合性、整数除法和求模运算、类型转换以及 C++11 引入的 auto 类型推断。

基本算术运算符:

  • +: 加法 (Addition)
  • -: 减法 (Subtraction)
  • *: 乘法 (Multiplication)
  • /: 除法 (Division)
  • %: 求模 (Modulo) 或求余 (Remainder)

这些运算符可以用于 C++ 的所有数值类型(整型和浮点型),但求模运算符 % 通常只用于整数类型。

用法与示例:

1
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
#include <iostream>

int main() {
double price = 15.50;
double tax_rate = 0.08;
int quantity = 3;
int total_items = 17;
int items_per_box = 5;

// 加法
double total_price = price * quantity + 5.0; // 假设有 5 元附加费
std::cout << "Total price (with fee): " << total_price << std::endl;

// 减法
double price_before_tax = price / (1 + tax_rate); // 假设 price 已含税
std::cout << "Price before tax: " << price_before_tax << std::endl;

// 乘法
double tax_amount = price_before_tax * tax_rate;
std::cout << "Tax amount: " << tax_amount << std::endl;

// 除法 (浮点数)
double average_price = total_price / quantity;
std::cout << "Average price per item: " << average_price << std::endl;

// 除法 (整数) - 见 3.4.2
int full_boxes = total_items / items_per_box;
std::cout << "Full boxes: " << full_boxes << std::endl; // 输出 3

// 求模 - 见 3.4.3
int remaining_items = total_items % items_per_box;
std::cout << "Remaining items: " << remaining_items << std::endl; // 输出 2

// 一元减法 (取负)
double discount = -10.0;
std::cout << "Discount: " << discount << std::endl;

// 一元加法 (通常无效果)
double positive_value = +price;
std::cout << "Positive value: " << positive_value << std::endl;

return 0;
}

3.4.1 运算符优先级和结合性

当一个表达式中包含多个运算符时,优先级 (Precedence)结合性 (Associativity) 决定了运算的执行顺序。

  • 优先级: 哪个运算符先执行。优先级高的运算符先于优先级低的运算符执行。例如,乘法和除法的优先级高于加法和减法。
  • 结合性: 当多个具有相同优先级的运算符连续出现时,它们的执行顺序。
    • 左结合性 (Left-to-Right): 运算从左向右执行。大多数二元算术运算符(+, -, *, /, %)都是左结合的。例如 a - b + c 等价于 (a - b) + c
    • 右结合性 (Right-to-Left): 运算从右向左执行。赋值运算符 = 和一元运算符(如取负 -)是右结合的。例如 a = b = c 等价于 a = (b = c)

常见算术运算符优先级 (由高到低):

  1. 一元运算符: + (正号), - (负号) - (右结合)
  2. 乘法、除法、求模: *, /, % - (左结合)
  3. 加法、减法: +, - - (左结合)
  4. 赋值运算符: = - (右结合)

使用括号: 可以使用圆括号 () 来覆盖默认的优先级和结合性,强制按特定顺序执行运算。括号内的表达式总是最先计算。

用法与示例:

1
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
#include <iostream>

int main() {
int a = 5, b = 8, c = 3, d = 2;

// 优先级: * 高于 +
int result1 = a + b * c; // 等价于 a + (b * c) = 5 + (8 * 3) = 5 + 24 = 29
std::cout << "a + b * c = " << result1 << std::endl;

// 使用括号改变顺序
int result2 = (a + b) * c; // (5 + 8) * 3 = 13 * 3 = 39
std::cout << "(a + b) * c = " << result2 << std::endl;

// 结合性 (左结合): / 和 * 优先级相同
int result3 = b / c * d; // 等价于 (b / c) * d = (8 / 3) * d = 2 * d = 2 * 2 = 4 (整数除法)
std::cout << "b / c * d = " << result3 << std::endl;

// 结合性 (右结合): 赋值
int x, y, z;
x = y = z = 10; // 等价于 x = (y = (z = 10))
std::cout << "x=" << x << ", y=" << y << ", z=" << z << std::endl; // 输出 x=10, y=10, z=10

// 结合性 (右结合): 一元负号
int val = - - 5; // 等价于 -(-5) = 5
std::cout << "- - 5 = " << val << std::endl;

return 0;
}

建议: 当表达式复杂或优先级不明确时,使用括号来明确运算顺序,可以提高代码的可读性并避免错误。

3.4.2 除法分支

C++ 的除法运算符 / 的行为取决于其操作数 (Operand) 的类型:

  1. 浮点数除法: 如果操作数中至少有一个是浮点类型 (float, double, long double),则执行浮点数除法,结果也是浮点类型,包含小数部分。
  2. 整数除法: 如果两个操作数都是整数类型 (int, short, long, char, bool 等),则执行整数除法。结果只保留商的整数部分,小数部分被**截断 (truncated)**(直接丢弃,不是四舍五入)。

用法与示例:

1
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
#include <iostream>

int main() {
// 浮点数除法
double result_f1 = 9.0 / 5.0; // 两个 double
double result_f2 = 9.0 / 5; // 一个 double, 一个 int (int 被提升为 double)
double result_f3 = 9 / 5.0; // 一个 int, 一个 double (int 被提升为 double)
float result_f4 = 9.0f / 5.0f; // 两个 float

std::cout << "9.0 / 5.0 = " << result_f1 << std::endl; // 输出 1.8
std::cout << "9.0 / 5 = " << result_f2 << std::endl; // 输出 1.8
std::cout << "9 / 5.0 = " << result_f3 << std::endl; // 输出 1.8
std::cout << "9.0f / 5.0f = " << result_f4 << std::endl; // 输出 1.8

// 整数除法
int result_i1 = 9 / 5; // 两个 int
int result_i2 = 10 / 3; // 两个 int
int result_i3 = 13 / 4; // 两个 int
int result_i4 = -10 / 3; // 负数整数除法 (结果通常向零截断,为 -3)

std::cout << "9 / 5 = " << result_i1 << std::endl; // 输出 1 (小数部分 0.8 被截断)
std::cout << "10 / 3 = " << result_i2 << std::endl; // 输出 3 (小数部分 0.333... 被截断)
std::cout << "13 / 4 = " << result_i3 << std::endl; // 输出 3 (小数部分 0.25 被截断)
std::cout << "-10 / 3 = " << result_i4 << std::endl; // 输出 -3

// 想要得到浮点结果,需要确保至少一个操作数是浮点类型
// 可以使用类型转换 (见 3.4.4)
double result_mixed = double(9) / 5; // 将 9 转换为 double
std::cout << "double(9) / 5 = " << result_mixed << std::endl; // 输出 1.8

return 0;
}

注意: 进行除法运算时,务必清楚操作数的类型,以确保得到期望的结果(整数截断或浮点小数)。

3.4.3 求模运算符

求模运算符 % 计算整数除法的**余数 (Remainder)**。它要求两个操作数都必须是整数类型(或可以转换为整数的类型,如 char, bool)。

运算规则:
a % b 的结果是 a 除以 b 后的余数。其符号通常与被除数 a 的符号相同(C++11 标准规定如此)。

数学关系:
对于整数 ab (其中 b != 0),以下关系通常成立:
(a / b) * b + (a % b) == a

用途:

  • 判断一个数是否能被另一个数整除(如果 a % b == 0,则 a 能被 b 整除)。
  • 获取一个数的最低位数字(num % 10)。
  • 将数值限制在一个范围内(例如,生成 0 到 N-1 之间的数:value % N)。
  • 周期性操作(例如,每隔 M 个元素执行一次操作:if (count % M == 0))。

用法与示例:

1
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
#include <iostream>

int main() {
int total_seconds = 135;
int seconds_per_minute = 60;

int minutes = total_seconds / seconds_per_minute; // 整数除法得分钟数
int remaining_seconds = total_seconds % seconds_per_minute; // 求模得剩余秒数

std::cout << total_seconds << " seconds is "
<< minutes << " minutes and "
<< remaining_seconds << " seconds." << std::endl; // 输出 135 seconds is 2 minutes and 15 seconds.

// 判断奇偶性
int number = 21;
if (number % 2 == 0) {
std::cout << number << " is even." << std::endl;
} else {
std::cout << number << " is odd." << std::endl; // 输出 21 is odd.
}

// 获取个位数
int value = 123;
int last_digit = value % 10;
std::cout << "The last digit of " << value << " is " << last_digit << std::endl; // 输出 3

// 负数求模 (C++11 及以后,结果符号与被除数一致)
int result1 = 10 % 3; // 1
int result2 = -10 % 3; // -1
int result3 = 10 % -3; // 1
int result4 = -10 % -3; // -1

std::cout << "10 % 3 = " << result1 << std::endl;
std::cout << "-10 % 3 = " << result2 << std::endl;
std::cout << "10 % -3 = " << result3 << std::endl;
std::cout << "-10 % -3 = " << result4 << std::endl;

// 不能用于浮点数
// double remainder = 10.5 % 3.2; // 编译错误!

return 0;
}

3.4.4 类型转换

C++允许在不同数据类型之间进行转换,这称为类型转换 (Type Casting)**。转换可以隐式 (Implicitly)** 发生(由编译器自动完成),也可以显式 (Explicitly) 进行(由程序员通过代码指定)。

隐式类型转换 (Automatic Conversion):

在以下情况下,编译器会自动执行类型转换:

  1. 混合类型表达式: 当一个表达式中包含不同数值类型的操作数时,较小或较低优先级的类型通常会被提升 (Promoted) 为较大或较高优先级的类型,然后进行运算。
    • 整型提升 (Integral Promotion):int 小的整型(bool, char, signed char, unsigned char, short, unsigned short)在表达式中通常会被提升为 int (如果 int 能容纳其所有值) 或 unsigned int
    • 算术转换 (Usual Arithmetic Conversions): 在涉及不同算术类型(整型和浮点型)的运算中,遵循一套规则将操作数转换为共同的类型(通常是两者中“更宽”或精度更高的类型)。例如,intdouble 运算时,int 会被转换为 doublefloatdouble 运算时,float 会被转换为 double
  2. 赋值: 将一个类型的值赋给另一种类型的变量时,右侧的值会被转换为左侧变量的类型。这可能导致精度损失(如 doubleint)或范围问题(如 longshort)。
  3. 函数参数传递: 将参数传递给函数时,如果实参类型与形参类型不匹配,会尝试进行转换。
  4. 函数返回值: 从函数返回一个值时,如果返回值类型与函数声明的返回类型不匹配,会尝试进行转换。

显式类型转换 (Explicit Casting):

当需要强制进行类型转换,或者为了使代码意图更清晰时,可以使用显式类型转换。C++提供了多种转换方式:

  1. C 风格强制类型转换 (C-Style Cast):

    1
    2
    (typeName) expression
    typeName (expression) // 函数式转换

    这种方式简单直接,但在某些情况下不够安全,因为它可能执行多种不同类型的转换(如 static_cast, const_cast, reinterpret_cast 的组合)。

  2. C++ 类型转换运算符 (C++ Cast Operators): (更推荐,更安全,意图更明确)

    • static_cast<typeName>(expression): 用于比较“自然”和安全的转换,如数值类型之间的转换(整数与浮点数互转、整数与整数互转)、指针类型之间的相关转换(如 void* 与其他类型指针互转、基类指针与派生类指针互转,但需要谨慎)。这是最常用的 C++ 转换符。
    • dynamic_cast<typeName>(expression): 主要用于处理类继承层次结构中的指针或引用转换(向下转型),并在运行时进行类型检查。如果转换无效,对于指针会返回 nullptr,对于引用会抛出 std::bad_cast 异常。需要基类是多态的(至少有一个虚函数)。
    • const_cast<typeName>(expression): 用于添加或移除表达式的 constvolatile 限定符。通常用于去除 const,但修改原本是 const 的对象是未定义行为。主要用于处理常量性不匹配的旧 API。
    • reinterpret_cast<typeName>(expression): 用于低级别的、通常与实现相关的、不安全的转换。例如,在整数和指针之间进行转换,或者在不相关的指针类型之间进行转换。应极力避免使用,除非确实理解其底层含义和风险。

用法与示例:

1
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
#include <iostream>

int main() {
// 隐式转换示例
int i_val = 10;
double d_val = 3.14;
short s_val = 5;

// 混合表达式 (算术转换)
double result1 = i_val + d_val; // i_val (int) 提升为 double (10.0), 结果是 double (13.14)
std::cout << "int + double = " << result1 << std::endl;

// 整型提升
char c_val = 'A'; // ASCII 码 65
int result2 = c_val + s_val; // c_val (char) 和 s_val (short) 都提升为 int, 结果是 int (70)
std::cout << "char + short = " << result2 << std::endl;

// 赋值转换 (可能丢失信息)
int i_from_d = d_val; // d_val (3.14) 转换为 int, 小数部分截断, i_from_d 变为 3
std::cout << "int from double = " << i_from_d << std::endl;

short s_from_i = 100000; // 100000 可能超出 short 范围, 结果未定义或回绕
std::cout << "short from large int = " << s_from_i << std::endl;

// 显式转换示例
int total = 19;
int count = 5;

// C 风格转换 (用于浮点除法)
double average1 = (double)total / count;
double average2 = double(total) / count; // 函数式转换
std::cout << "(double)total / count = " << average1 << std::endl; // 输出 3.8
std::cout << "double(total) / count = " << average2 << std::endl; // 输出 3.8

// C++ static_cast (推荐用于数值转换)
double average3 = static_cast<double>(total) / count;
std::cout << "static_cast<double>(total) / count = " << average3 << std::endl; // 输出 3.8

int char_code = static_cast<int>('B'); // char 转 int
std::cout << "Code for 'B': " << char_code << std::endl; // 输出 66

// 演示 reinterpret_cast (通常不推荐)
long addr = 1000;
// int* ptr = reinterpret_cast<int*>(addr); // 将整数视为地址 (危险!)
// std::cout << "Pointer from address: " << ptr << std::endl;

return 0;
}

建议:

  • 尽量避免不必要的类型转换。
  • 优先使用 C++ 的 static_cast 进行明确且相对安全的数值或相关指针转换。
  • 谨慎使用 C 风格转换,因为它隐藏了转换的类型和风险。
  • 仅在绝对必要且理解后果的情况下使用 const_castreinterpret_cast
  • 注意隐式转换可能导致的精度损失或意外行为,尤其是在混合有符号和无符号整数时。

3.4.5 C++11中的auto声明

C++11 引入了 auto 关键字,它允许编译器根据变量的初始化表达式 (Initializer) 自动推断出变量的类型。这可以简化代码,尤其是在处理复杂类型(如 STL 迭代器或模板类型)时。

工作原理:

当你使用 auto 声明变量时,必须提供一个初始化表达式。编译器会查看这个表达式的类型,并将该类型赋予 auto 声明的变量。

1
auto variableName = initializationExpression;

要点:

  • 必须初始化: 使用 auto 声明的变量必须在声明时初始化。
  • 类型推断: 类型是从初始化表达式推断出来的,而不是变量本身的某种默认类型。
  • const 和引用: auto 通常不会自动推断出顶层的 const 或引用。如果需要 const 或引用,需要显式添加。
    • auto x = value; // x 的类型与 value 相同 (const/引用被剥离)
    • const auto cx = value; // cx 是 const 类型
    • auto& rx = value; // rx 是引用类型
    • const auto& crx = value; // crx 是 const 引用类型
  • 列表初始化: 对于 C++11 中的列表初始化 {}auto 的推断规则比较特殊。
    • auto x = {1, 2, 3}; // C++11/14: x 被推断为 std::initializer_list
    • auto y = {1}; // C++11/14: y 被推断为 std::initializer_list
    • auto z{1}; // C++17: z 被推断为 int (注意没有等号)

用法与示例:

1
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
#include <iostream>
#include <vector> // 包含 vector
#include <typeinfo> // 用于 typeid (仅作演示)

int main() {
// 基本类型推断
auto i = 10; // i 被推断为 int
auto d = 3.14; // d 被推断为 double
auto f = 3.14f; // f 被推断为 float
auto c = 'A'; // c 被推断为 char
auto b = true; // b 被推断为 bool
auto ll = 1234567890LL; // ll 被推断为 long long

std::cout << "Type of i: " << typeid(i).name() << std::endl;
std::cout << "Type of d: " << typeid(d).name() << std::endl;
std::cout << "Type of f: " << typeid(f).name() << std::endl;
std::cout << "Type of ll: " << typeid(ll).name() << std::endl;

// 推断表达式结果类型
auto sum = i + d; // i(int) + d(double) -> double, sum 被推断为 double
std::cout << "Type of sum: " << typeid(sum).name() << std::endl;
std::cout << "sum = " << sum << std::endl;

// 推断 const 和引用 (需要显式添加)
int original = 100;
auto copy = original; // copy 是 int (非引用, 非 const)
const auto const_copy = original; // const_copy 是 const int
auto& ref = original; // ref 是 int& (引用)
const auto& const_ref = original; // const_ref 是 const int& (const 引用)

copy = 200; // OK
// const_copy = 200; // 错误! const_copy 是 const
ref = 300; // OK, original 也变为 300
// const_ref = 400; // 错误! const_ref 是 const 引用

std::cout << "original: " << original << std::endl; // 输出 300

// 用于复杂类型 (例如 STL 容器迭代器)
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
auto it = names.begin(); // it 被推断为 std::vector<std::string>::iterator
std::cout << "First name: " << *it << std::endl;
std::cout << "Type of it: " << typeid(it).name() << std::endl;

// C++11 列表初始化
auto list1 = {10, 20, 30}; // list1 是 std::initializer_list<int>
std::cout << "Type of list1: " << typeid(list1).name() << std::endl;

// C++17 列表初始化 (无等号)
// auto val{42}; // C++17: val 是 int
// std::cout << "Type of val: " << typeid(val).name() << std::endl;

return 0;
}

优点:

  • 减少冗余代码,尤其是在类型名称很长时。
  • 提高代码的可维护性,如果初始化表达式的类型改变,auto 变量的类型会自动更新。
  • 有助于泛型编程。

缺点/注意事项:

  • 过度使用可能降低代码的可读性,因为读者需要查看初始化表达式才能确定类型。
  • auto 推断出的类型可能不是你期望的(例如,忘记添加 & 得到副本而不是引用)。
  • 对于代理类(Proxy Classes),auto 可能推断出代理类型而不是期望的值类型,需要小心。

建议: 在类型冗长、明显或无关紧要时使用 auto 可以提高效率。在类型对于理解代码逻辑很重要时,显式写出类型可能更好。

3.5 总结

本章深入探讨了C++用于处理数据的基本内置类型。我们首先学习了简单变量的命名规则和约定,强调了名称的可读性和合法性。

接着,我们详细研究了C++的整型家族,包括 shortintlong 和 C++11 新增的 long long。我们讨论了它们各自的大小、表示范围以及如何选择合适的类型。我们还介绍了 unsigned 类型,它们用于存储非负整数,并具有更大的正数范围。我们学习了如何书写不同进制(十进制、八进制、十六进制,以及C++14的二进制)的整型字面值,以及如何使用后缀(U, L, LL)来指定常量的具体类型,并了解了编译器在没有后缀时如何推断常量类型。

char 类型被介绍为一种特殊的整型,主要用于存储字符,但也可以作为小整数使用。我们学习了字符字面值(使用单引号)和转义序列。bool 类型也被引入,用于表示逻辑真 (true) 和假 (false),以及它与整数(1和0)之间的转换关系。

为了创建不可修改的变量(常量),我们学习了 const 限定符。使用 const 定义的常量必须在声明时初始化,它提供了类型安全和作用域控制,是比 #define 更受推荐的常量定义方式。

然后,我们转向了浮点类型 (float, double, long double),用于表示带小数的数字。我们学习了书写浮点数的两种方式(标准小数点和E表示法),了解了不同浮点类型的精度和范围差异,以及如何使用后缀(f, L)指定浮点常量类型(默认为 double)。我们还讨论了浮点数的优点(范围广、表示小数)和固有的缺点(精度限制、比较困难)。

最后,我们学习了C++的基本算术运算符 (+, -, *, /, %)。我们探讨了运算符的优先级和结合性规则,以及如何使用括号来控制运算顺序。特别地,我们区分了整数除法(结果截断)和浮点数除法,并学习了求模运算符 % 的用法(主要用于整数求余)。

类型转换是本章的另一个重点,包括编译器自动执行的隐式转换(如整型提升和算术转换)和程序员指定的显式转换(C风格转换和更安全的C++转换符 static_castdynamic_castconst_castreinterpret_cast)。我们强调了转换中可能出现的信息丢失问题。

C++11 引入的 auto 关键字也被介绍,它允许编译器根据初始化表达式自动推断变量类型,简化了代码,尤其是在处理复杂类型时。

通过本章的学习,我们掌握了C++的基本数据类型及其用法,为后续更复杂的数据结构和算法打下了坚实的基础。

评论