这两个概念紧密相关,但有着本质的区别。简单来说:

  • 字面量 (Literal):是源代码中直接表示一个固定值的符号。它是“值”本身。例如 123, 3.14, 'A', "hello"
  • 常量 (Constant):是一个有名字的、其值在初始化后不能被改变的变量。它像一个贴了标签的、内容不可更改的盒子。例如 const int MAX_SIZE = 100;

下面我们分两大部分来详细剖析。


第一部分:字面量 (Literals)

字面量是 C++ 语言中最基本的元素之一,它们是值的直接表示形式,不需要计算。

1. 整型字面量 (Integer Literals)

表示整数值。

  • 十进制 (Decimal):最常见的形式,由 0-9 组成,不能以 0 开头(除非这个数就是 0)。
int a = 10;
int b = -123;
  • 八进制 (Octal):以 0 开头。
int c = 077; // 7*8^1 + 7*8^0 = 56 + 7 = 63 (十进制)
  • 十六进制 (Hexadecimal):以 0x0X 开头。
int d = 0xFF;  // 15*16^1 + 15*16^0 = 240 + 15 = 255 (十进制)
int e = 0xabc;
  • 二进制 (Binary) (C++14+):以 0b0B 开头。
int f = 0b1010; // 1*2^3 + 0*2^2 + 1*2^1 + 0*2^0 = 8 + 2 = 10 (十进制)

类型后缀 (Type Suffixes):可以为整型字面量添加后缀来明确其类型。

  • uUunsigned (无符号)
  • lLlong (长整型)
  • llLLlong long (长长整型, C++11)

这些后缀可以组合使用,例如 100UL 表示一个 unsigned long 类型的字面量。

auto val1 = 123;   // int
auto val2 = 123U;  // unsigned int
auto val3 = 123L;  // long
auto val4 = 123ULL; // unsigned long long

2. 浮点型字面量 (Floating-Point Literals)

表示小数值。

  • 标准表示法3.14159, -0.001
  • 科学计数法6.022e23 (表示 6.022 x 10²³), 1.6e-19

默认情况下,浮点字面量是 double 类型。

类型后缀 (Type Suffixes)

  • fFfloat (单精度)
  • lLlong double (长双精度)
auto f1 = 3.14;   // double
auto f2 = 3.14f;  // float
auto f3 = 3.14L;  // long double

3. 字符字面量 (Character Literals)

表示单个字符,用单引号 ' ' 括起来。

  • 普通字符'a', 'B', '7'

  • 转义序列:用于表示特殊字符。

  • '\n' (换行), '\t' (水平制表), '\'' (单引号), '\"' (双引号), '\\' (反斜杠)

  • '\xHH' (用两位十六进制表示字符,如 '\x41' 代表 ‘A’)

  • 宽字符和多字节字符前缀 (用于国际化):

  • L'A'wchar_t 类型

  • u8'A'char 类型 (UTF-8, C++17)

  • u'A'char16_t 类型 (UTF-16, C++11)

  • U'A'char32_t 类型 (UTF-32, C++11)

4. 字符串字面量 (String Literals)

表示一个字符序列,用双引号 " " 括起来。

"Hello, World!"
  • 它在内存中是一个 const char 数组,并以一个空字符 '\0' 结尾。所以 "hello" 的实际长度是 6。
  • 相邻的字符串字面量会自动拼接:"hello" " world" 等同于 "hello world"
  • 同样支持字符字面量的转义序列和前缀(L"", u8"", u"", U"")。
  • 原始字符串字面量 (Raw String Literals, C++11)
// 普通字符串,需要转义
std::string path1 = "C:\\Users\\Guest\\Documents";
// 原始字符串,无需转义
std::string path2 = R"(C:\Users\Guest\Documents)";
  • 语法:R"(...)"
  • 用于避免转义反斜杠 \,在写正则表达式或 Windows 文件路径时非常有用。

5. 布尔字面量 (Boolean Literals)

只有两个:truefalse

bool is_ready = true;
bool is_finished = false;

6. 指针字面量 (Pointer Literal)

C++11 引入了 nullptr,它是一个表示空指针的字面量。

int* ptr = nullptr;

nullptr 是类型安全的,优于旧式的 NULL0


第二部分:常量 (Constants)

常量是具有名称的标识符,其值在定义后不能修改。使用常量可以增强代码的可读性和可维护性,并提供类型安全。

C++ 中定义常量主要有以下几种方式:

1. const 关键字

这是定义常量最常用、最基本的方式。

const double PI = 3.14159;
const int MAX_USERS = 100;
// PI = 3.14; // 编译错误!不能修改 const 变量

const 的优点

  • 类型安全:编译器知道它的类型,会进行类型检查。
  • 有作用域const 变量遵循普通变量的作用域规则。
  • 可调试:在调试器中可以看到它的名字和值。

const 与指针:这是一个常见的难点,关键是看 const 修饰的是什么。

  • 指向常量的指针 (Pointer to const):指针指向的值不能被修改,但指针本身可以指向别处。
const int val = 10;
const int* ptr = &val; // ptr 指向一个常量
// *ptr = 20; // 错误!不能通过 ptr 修改 val
int another_val = 20;
ptr = &another_val;   // 正确,指针可以指向另一个(常量或非常量)地址

int const* ptrconst int* ptr 是等价的。

  • 常量指针 (const Pointer):指针本身的值(即它存储的地址)不能被修改,但它指向的数据可以被修改(如果数据本身不是 const 的话)。
int val = 10;
int* const ptr = &val; // ptr 是一个常量指针,必须在声明时初始化
*ptr = 20;            // 正确,可以修改所指向的值
int another_val = 30;
// ptr = &another_val; // 错误!不能修改 ptr 使其指向别处
  • 指向常量的常量指针 (const Pointer to const):指针本身和它指向的值都不能被修改。
const int val = 10;
const int* const ptr = &val;
// *ptr = 20;          // 错误
// ptr = &another_val; // 错误

2. constexpr 关键字 (C++11 及以后)

constexpr (Constant Expression, 常量表达式) 是 const 的加强版。它不仅表示“只读”,还强调其值必须在编译时就能确定

constexpr double PI = 3.14159;
constexpr int MAX_SIZE = 10 * 10;
 
// constexpr 还可以用于函数,表示函数在编译时可以被求值
constexpr int get_array_size() {
    return 5;
}
 
int my_array[get_array_size() + 1]; // 正确,数组大小必须是编译时常量

const vs constexpr

  • const 变量的值可能在运行时才确定。
int n;
std::cin >> n;
const int SIZE = n; // SIZE 的值在运行时确定
  • constexpr 变量的值必须在编译时就确定。
int n;
std::cin >> n;
// constexpr int SIZE = n; // 编译错误!n 的值在编译时未知

最佳实践:如果一个常量的值在编译时就可以确定,优先使用 constexpr。这能给编译器更多优化的机会,并能用在更多需要编译时常量的场景(如数组大小、模板参数等)。

3. 枚举 (enum)

用于定义一组相关的整型常量。

  • 传统 enum
enum Color { RED, GREEN, BLUE }; // RED=0, GREEN=1, BLUE=2
int c = RED; // 隐式转换为 int

缺点:枚举成员会污染所在的作用域,且会隐式转换为整型,不够类型安全。

  • 作用域枚举 (enum class, C++11)
enum class Color { RED, GREEN, BLUE };
enum class Status { OK, FAILED };
 
Color c = Color::RED; // 必须使用作用域解析符
// int status_code = Status::OK; // 错误!不能隐式转换为 int
if (c == Color::RED) {
    // ...
}

强烈推荐使用 enum class,因为它解决了传统 enum 的所有缺点,更加类型安全和模块化。

4. #define 预处理器指令

这是 C 语言遗留下来的方式,在 C++ 中强烈不推荐用它来定义常量。

#define PI 3.14159 // 不推荐!

#define 的缺点

  • 无类型安全:它只是简单的文本替换,在预处理阶段完成,编译器根本不知道 PI 是什么类型。
  • 无作用域:一旦定义,在整个文件中(直到 #undef)都有效,容易引起命名冲突。
  • 难以调试:预处理后,代码中的 PI 已经变成了 3.14159,调试时看不到 PI 这个符号。
  • 可能违反封装:如果定义在头文件中,会污染所有包含该头文件的代码。

总结与最佳实践

特性字面量 (Literal)常量 (Constant)
本质值的直接表示存储值的命名标识符
形式123, 3.14f, "hi", nullptrconst int a = 10;, constexpr ...
是否有名字
是否有类型有(如 123int, 3.14ffloat有,且由编译器强制检查
是否占内存通常不独立占内存(嵌入指令或数据段)通常有独立的内存地址(除非被优化掉)

现代 C++ 最佳实践

  1. 优先使用 const constexpr 而不是 #define 来定义常量。 这是 C++ 编程的基本准则。
  2. 如果常量的值在编译时可知,优先使用 constexpr 这能带来更好的性能和更广泛的用途。
  3. 对于一组相关的整型常量,优先使用 enum class 它提供了类型安全和作用域。
  4. 对于空指针,总是使用 nullptr,而不是 0 NULL
  5. 在函数和方法中贯彻 const 正确性const 参数、const 成员函数等),这能让接口更清晰,代码更健壮。
  6. 使用字面量后缀(如 f, L, U)和前缀(如 R"")来明确表达你的意图,避免不必要的类型转换和错误。