需要预先了解:


1. 变量

变量就是编程里用来存放数据的盒子。

——豆包

  1. 变量有名字(变量名,是一个标识符),可以用于找到它
  2. 变量里能存储数据(数字、文字、真假值等)
  3. 变量里的数据可以修改

1.1 常见的变量类型

整型变量

修饰符signed和unsigend

  • 默认为有符号(signed),表面变量可以表示正数、负数和0。
  • 用unsigned修饰后,变量只能表示非负整数,即0和正数

常见整型变量空间占用:

数据类型 32位系统(x86) 64位系统(x64)
int 4字节(32位) 8字节(64位)
short/short int 2字节(16位) 2字节(16位)
long/long int 4字节(32位) 8字节(64位)
long long/long long int 8字节(64位) 8字节(64位)
unsigned 类型 和对应的有符号类型相同 和对应的有符号类型相同

浮点型变量

  • float:单精度浮点型,占用4个字节(32位),大约能提供7位有效数字。
  • double:双精度浮点型,占用8个字节(64位),大约能提供15-17位有效数字,精度比float更高。
  • long double:长双精度浮点型,占用的字节数在不同系统中可能不同,通常比 double 提供更高的精度。

字符型变量

char 类型变量用于表示单个字符,占用 1 个字节(8 位)。

字符编码

计算机内部只能存储二进制数。为了能让计算机表示文字,人们建立了 字符编码(Character Encoding)——为每个字符分配一个数字编号,存储时存的是这个编号,显示时再将编号映射回对应的字形。

最常见的编码是 ASCII(American Standard Code for Information Interchange,美国信息交换标准代码),它用 7 个二进制位(0 ~ 127)来表示字符:

十进制 十六进制 字符 说明
0 ~ 31 0x00~0x1F N/A 控制字符(不可见)
32 0x20 空格 空格
48 ~ 57 0x30~0x39 0 ~ 9 数字
65 ~ 90 0x41~0x5A A ~ Z 大写字母
97 ~ 122 0x61~0x7A a ~ z 小写字母

由于 ASCII 只用 7 位,最高位(第 8 位)始终为 0。当需要表示中文、日文、韩文等非英语字符时,一个字节远远不够,于是出现了多种扩展编码方案:

GBK(国标扩展):

  • 专门为中文设计的编码标准,兼容 ASCII(0 ~ 127 与 ASCII 完全一致)。
  • 使用 变长编码:英文和数字占 1 个字节(与 ASCII 相同),中文字符占 2 个字节。
  • 高位字节(第一个字节)的范围是 0x81 ~ 0xFE,低位字节(第二个字节)的范围是 0x40 ~ 0xFE。
  • 优点:在中文环境下兼容性好、历史积累深厚;缺点:存在生僻字支持不全、在国际化场景中不如 UTF-8 通用。

UTF-8(Unicode Transformation Format - 8 bit):

  • Unicode 是国际统一的字符集,为全世界所有语言的每个字符分配唯一编号(码点,code point)。UTF-8 是 Unicode 最通用的实现方式之一。
  • 同样兼容 ASCII(0 ~ 127 与 ASCII 完全一致)。
  • 使用 变长编码,每个字符占用 1 ~ 4 个字节:
码点范围 UTF-8 字节数 字节格式(二进制)
U+0000 ~ U+007F 1 字节 0xxxxxxx
U+0080 ~ U+07FF 2 字节 110xxxxx 10xxxxxx
U+0800 ~ U+FFFF 3 字节 1110xxxx 10xxxxxx 10xxxxxx
U+10000 ~ U+10FFFF 4 字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  • 例如中文字符"编"的 Unicode 码点为 U+7F16,UTF-8 编码结果为 0xE7 0xBC 0x96(3 个字节)。

char 与整型的联系

在 C++ 中,char 本质上就是一个 1 字节的整数类型。每个字符存储的是它在编码表中对应的整数值(编码值):

#include <iostream>

int main()
{
    char ch1 = 'A';          // 存储的是 'A' 的 ASCII 码 65
    char ch2 = 65;           // 直接赋整数值,等价于 ch2 = 'A'

    std::cout << "ch1 = " << ch1 << std::endl;   // 输出: A
    std::cout << "ch2 = " << ch2 << std::endl;   // 输出: A

    return 0;
}

因此 char 和 int 之间可以隐式地相互转换:

#include <iostream>

int main()
{
    char ch = 'A';

    // char → int:字符参与算术运算时自动提升为 int
    int ascii_val = ch;             // ascii_val = 65
    int next = ch + 1;              // next = 66,即 'B' 的 ASCII 码

    std::cout << ascii_val << std::endl;        // 输出: 65
    std::cout << (char)next << std::endl;       // 输出: B

    // 大小写转换(利用 ASCII 编码的规律)
    char lower = 'g';
    char upper = lower - ('a' - 'A');   // 小写转大写
    std::cout << upper << std::endl;     // 输出: G

    // 数字字符转整数
    char digit = '7';
    int num = digit - '0';              // num = 7
    std::cout << num << std::endl;       // 输出: 7

    return 0;
}

signed char、unsigned char 与 char

C++ 标准规定了三种字符类型,它们都占用 1 个字节:

类型 取值范围
signed char -128 ~ 127
unsigned char 0 ~ 255
char 由编译器决定,可能等价于 signed char 或 unsigned char

关键区别:char、signed char、unsigned char 是 三种不同的类型。char 到底是有符号还是无符号,由编译器和平台决定(大多数 x86/x64 平台上 char 等于 signed char,范围是 -128 ~ 127)。

为什么字符类型也要分有符号和无符号?

表面上看,字符只需要 0 ~ 127 的 ASCII 值就够了,似乎没必要引入符号。但实际上,char 在 C/C++ 中承担了 双重角色——它既是字符类型,也是 最小的整数类型。这导致了 signed/unsigned 的区分:

1. char 常被当作"1 字节整数"使用

当 char 参与算术运算时,它的最高位(第 8 位)如何解释,会直接决定运算结果:

unsigned char uc = 200;      // 二进制: 1100 1000,值为 200
signed char   sc = 200;      // 同样二进制,但按有符号解释:值为 -56

int a = uc + 10;             // 200 + 10 = 210
int b = sc + 10;             // -56 + 10 = -46

同样一块二进制数据 11001000,被当作 unsigned char 就是 200,被当作 signed char 就是 -56。是否区分 signed/unsigned,取决于编译器如何解释这 8 个位。

2. 符号扩展(sign extension)问题

当 char 隐式转换为 int(更宽的类型)时,有符号和无符号的行为截然不同:

signed char sc = -1;            // 二进制: 1111 1111
unsigned char uc = 255;         // 二进制: 1111 1111

// 二者内存中的二进制完全相同!

int a = sc;     // 有符号扩展:高位全部填 1 → 0xFFFFFFFF = -1(32位)
int b = uc;     // 无符号扩展:高位全部填 0 → 0x000000FF = 255(32位)

同为 11111111,signed char 扩展为 int 后变成 -1,unsigned char 变成 255。在涉及位运算、网络字节流、二进制文件读写等场景中,这个差异至关重要。

3. 历史原因与平台兼容

早期的 C 语言设计时,不同硬件平台对 1 字节整数的默认符号处理不同。标准将 char 的符号交给编译器决定(implementation-defined),既能保证在不同架构上的灵活性,又给开发者提供了 signed char 和 unsigned char 来精确控制。到了 C++,这一设计被继承下来。

简单总结:如果你只是存储和输出 ASCII 文字(a-z、A-Z、0-9),符号无关紧要——它们都在 0~127 范围内,signed 和 unsigned 的解释完全相同。但一旦把 char 当作整数做运算、存储超过 127 的字节值、或与二进制数据打交道,signed/unsigned 的区别就变得非常重要。

给 char 赋非 ASCII 值的问题

由于 char 通常是有符号的(范围 -128 ~ 127),当赋予超出其表示范围的值时,会发生 溢出(overflow) 或 截断(truncation):

#include <iostream>

int main()
{
    // 情况1:赋大于 127 的值
    char ch1 = 128;             // 128 超出 signed char 范围(-128~127)
    std::cout << "ch1 的值: " << (int)ch1 << std::endl;   // 可能输出: -128

    // 情况2:赋负数
    char ch2 = -1;              // -1 在 signed char 范围内,可以存储
    std::cout << "ch2 的值: " << (int)ch2 << std::endl;   // 输出: -1

    // 情况3:int 截断为 char
    int large = 300;            // 二进制:0b 1 0010 1100
    char ch3 = large;           // 只保留低 8 位:0010 1100 = 44
    std::cout << "ch3 的值: " << (int)ch3 << std::endl;   // 输出: 44 (即 ',')

    // 情况4:赋值给 unsigned char
    unsigned char uch = 128;    // unsigned char 范围 0~255,正常存储
    std::cout << "uch 的值: " << (int)uch << std::endl;   // 输出: 128

    return 0;
}

布尔型变量

bool 类型用于表示逻辑真值,只有两个可能的取值:true 和 false。

#include <iostream>

int main()
{
    bool is_ready = true;
    bool is_empty = false;

    std::cout << "is_ready = " << is_ready << std::endl;   // 默认输出: 1
    std::cout << "is_empty = " << is_empty << std::endl;   // 默认输出: 0

    return 0;
}

内存占用

虽然 bool 只有两种状态(理论上 1 个二进制位就够),但在 C++ 中 bool 类型通常占用 1 个字节(8 位),这是由 C++ 标准规定的最小可寻址单元决定的。

std::cout << sizeof(bool) << std::endl;   // 通常输出: 1

bool 与整型的转换

bool 和整型之间可以隐式转换:

  • 整型 → bool:非零值转换为 true,零转换为 false
  • bool → 整型:true 转换为 1,false 转换为 0
#include <iostream>

int main()
{
    // 整型转 bool
    bool b1 = 42;           // true(非零)
    bool b2 = 0;            // false(零)
    bool b3 = -1;           // true(非零,负值也算)
    bool b4 = 3.14;         // true(非零浮点数)

    // bool 转整型
    int n1 = true;          // n1 = 1
    int n2 = false;         // n2 = 0

    // 算术运算中的 bool——true 被提升为 1,false 被提升为 0
    int sum = true + true + false;      // sum = 1 + 1 + 0 = 2

    std::cout << sum << std::endl;       // 输出: 2

    return 0;
}

使用 std::boolalpha 输出文字

默认情况下,std::cout 输出 bool 变量时显示 1 或 0。使用 std::boolalpha 操纵符,可以让它输出 true 或 false:

#include <iostream>

int main()
{
    bool flag = true;

    std::cout << flag << std::endl;                     // 输出: 1
    std::cout << std::boolalpha << flag << std::endl;   // 输出: true
    std::cout << std::noboolalpha << flag << std::endl;  // 恢复,输出: 1

    return 0;
}

布尔型字面量

C++ 中布尔字面量只有两个:true 和 false。它们是 C++ 的 关键字,不能用作标识符名,类型就是 bool。


1.2 变量的初始化方式

1. 默认初始化:变量不写任何初值,由编译器自动处理。

int global_var;         // 未初始化全局变量被自动置 0

int main()
{
    int a;         // 局部内置变量,垃圾值
    std::string str;       // 类:调用无参构造函数
    int* p = new int;      // 堆内存默认初始化,垃圾值
}

2. 空值初始化:用空括号(()或{}),强制清零/调用默认构造

int a{};
int b();
int* p = new int();
int arr[5]{ };

3. 列表初始化:用花括号{},统一语法,禁止窄化转换

int a{10};
int b = {20};
int arr[]{1, 2, 3};
std::vector<int> v{1, 2, 3, 4};

1) 禁止窄化转换:

int a = 3.14;      // 允许阶段
int b{3.14};       // 编译报错

2) 支持std::initializer_list多参数批量构造:

struct Vec
{
    Vec(std::initializer_list<int> list);
}

4. 直接初始化:用圆括号(),直接调用构造函数

int a(100);
struct Point { int x, y; }
Point p(1, 2);

5. 拷贝初始化:用=赋值,触发拷贝构造/隐式转换

int a = 10;
std::string s = "Hello";
class A;
A a = A();

6. 聚合初始化:针对简单的结构体、数组,用{}批量赋值

struct Position { double x, y; }
Position p = { 2.5, 3.4 };
int arr[4] = { 1, 2 };

2. 常量

常量(constant) 是指在程序运行过程中其值不能改变的量。

在C++中,使用const关键字来定义常量,语法为const 数据类型 常量名 = 常量值;。

const double pi = 3.14159;

2.1 定义常量的细节

1. 常量必须在定义时初始化。

#include <iostream>

int main()
{
    const int a;
    return 0;
}

编译后,得到以下报错:

code.cpp: In function 'int main()':
code.cpp:5:15: error: uninitialized 'const a' [-fpermissive]
    5 |     const int a;
      |               ^

2. 常量的值在程序运行过程中不能被修改,如果尝试修改常量的值,编译器会报错。

#include <iostream>

int main()
{
    const int b = 10;
    b = 20;

    return 0;
}

编译后,得到以下报错:

code.cpp: In function 'int main()':
code.cpp:6:7: error: assignment of read-only variable 'b'
    6 |     b = 20;
      |     ~~^~~~

2.2 字面常量

字面常量

字面常量是在代码中直接书写的固定值,其类型由书写形式决定。

  1. 整数常量:如10、-20、30000等,默认是int类型,如果值过大,可能会是 long 或 long long 类型。
  2. 浮点常量:如3.14、2.71828等,默认是double类型。
  3. 字符常量:用单引号括起来的单个字符,如 'a'、'A'、'0' 等。
  4. 字符串常量:用双引号括起来的零个或多个字符序列,如 "Hello"、"what the hell" 等

字面量后缀

可以用后缀来明确字面量的类型。

1. 整数字面量后缀

  • u或U:表示无符号整数(unsigned)。
  • l 或 L:表示长整数(long)。
  • ll 或 LL:表示长长整数(long long)。
unsigned int num1 = 10u;
unsigned long num2 = 100UL;
long long big_num = 1000000000000000000000LL;

2. 浮点字面量后缀

  • f或F:表示单精度浮点数(float)。
  • l 或 L:表示长双精度浮点数(long double)。
float pi = 3.14f;
long double e = 2.71828182845904523536L; 

2.3 编译时常量/常量表达式(constant expression)

注:constexpr关键字在 C++11 中才引入。

编译时常量和普通常量一样,也必须在定义时初始化,并且在程序运行的过程中不能被修改。

在C++中,使用constexpr关键字来定义编译时常量:

// after C++11
#include <iostream>

int main()
{
    constexpr int a = 10;

    return 0;
}

编译时常量的值在编译阶段就能确定,随后就会被直接替换到代码中,这样在程序运行时就不需要再查找地址、访问内存来获取值了,因此编译时常量的访问效率更高。

编译时就能确定的值:只由字面量和其它编译时能确定的值组成。

const int x = 10;      // 10是字面量,因此x可以在编译时求出结果
const int y = 20  *x;  // x可在编译时求出结果,故y也可在编译时求出结果

普通的常量既可以在编译时就确定值,也可以在运行的时候才确定值,前者可以被用来初始化编译时常量,后者则不能。

#include <iostream>

int main()
{
    int variable = 10;              // variable 是一个变量
    const int a = variable;         // a 是一个常量,值在运行时才确定
    constexpr int b = variable;     // 错误,编译时常量不能被变量初始化
    constexpr int c = a;            // 错误,编译时常量不能被运行时才确定的常量初始化

    return 0;
}