这两个概念紧密相关,但有着本质的区别。简单来说:
- 字面量 (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 long
2. 浮点型字面量 (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 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)
只有两个: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
是等价的。
- 常量指针 (
const
Pointer):指针本身的值(即它存储的地址)不能被修改,但它指向的数据可以被修改(如果数据本身不是const
的话)。
int val = 10;
int* const ptr = &val; // ptr 是一个常量指针,必须在声明时初始化
*ptr = 20; // 正确,可以修改所指向的值
int another_val = 30;
// ptr = &another_val; // 错误!不能修改 ptr 使其指向别处
- 指向常量的常量指针 (
const
Pointer 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""
)来明确表达你的意图,避免不必要的类型转换和错误。