这两个概念紧密相关,但有着本质的区别。简单来说:
- 字面量 (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):以
0x或0X开头。
int d = 0xFF; // 15*16^1 + 15*16^0 = 240 + 15 = 255 (十进制)
int e = 0xabc;- 二进制 (Binary) (C++14+):以
0b或0B开头。
int f = 0b1010; // 1*2^3 + 0*2^2 + 1*2^1 + 0*2^0 = 8 + 2 = 10 (十进制)类型后缀 (Type Suffixes):可以为整型字面量添加后缀来明确其类型。
u或U:unsigned(无符号)l或L:long(长整型)ll或LL:long long(长长整型, C++11)
这些后缀可以组合使用,例如 100UL 表示一个 unsigned long 类型的字面量。
auto val1 = 123; // int
auto val2 = 123U; // unsigned int
auto val3 = 123L; // long
auto val4 = 123ULL; // unsigned long long2. 浮点型字面量 (Floating-Point Literals)
表示小数值。
- 标准表示法:
3.14159,-0.001 - 科学计数法:
6.022e23(表示 6.022 x 10²³),1.6e-19
默认情况下,浮点字面量是 double 类型。
类型后缀 (Type Suffixes):
f或F:float(单精度)l或L:long double(长双精度)
auto f1 = 3.14; // double
auto f2 = 3.14f; // float
auto f3 = 3.14L; // long double3. 字符字面量 (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)
只有两个:true 和 false。
bool is_ready = true;
bool is_finished = false;6. 指针字面量 (Pointer Literal)
C++11 引入了 nullptr,它是一个表示空指针的字面量。
int* ptr = nullptr;nullptr 是类型安全的,优于旧式的 NULL 或 0。
第二部分:常量 (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* ptr 和 const int* ptr 是等价的。
- 常量指针 (
constPointer):指针本身的值(即它存储的地址)不能被修改,但它指向的数据可以被修改(如果数据本身不是const的话)。
int val = 10;
int* const ptr = &val; // ptr 是一个常量指针,必须在声明时初始化
*ptr = 20; // 正确,可以修改所指向的值
int another_val = 30;
// ptr = &another_val; // 错误!不能修改 ptr 使其指向别处- 指向常量的常量指针 (
constPointer toconst):指针本身和它指向的值都不能被修改。
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", nullptr | const int a = 10;, constexpr ... |
| 是否有名字 | 无 | 有 |
| 是否有类型 | 有(如 123 是 int, 3.14f 是 float) | 有,且由编译器强制检查 |
| 是否占内存 | 通常不独立占内存(嵌入指令或数据段) | 通常有独立的内存地址(除非被优化掉) |
现代 C++ 最佳实践:
- 优先使用
const和constexpr而不是#define来定义常量。 这是 C++ 编程的基本准则。 - 如果常量的值在编译时可知,优先使用
constexpr。 这能带来更好的性能和更广泛的用途。 - 对于一组相关的整型常量,优先使用
enum class。 它提供了类型安全和作用域。 - 对于空指针,总是使用
nullptr,而不是0或NULL。 - 在函数和方法中贯彻
const正确性(const参数、const成员函数等),这能让接口更清晰,代码更健壮。 - 使用字面量后缀(如
f,L,U)和前缀(如R"")来明确表达你的意图,避免不必要的类型转换和错误。