变量、类型与类型安全
学习 C++ 的类型系统 ── 基本类型、初始化形式、auto 推导、const 正确性、类型转换以及 C++ 如何在编译时防止类型错误。
C++ 是静态类型语言
在 C++ 中,每个变量都有一个在编译时确定且永远不会改变的类型。这与 Python 或 JavaScript 不同,在那些语言中变量可以在运行时持有任何类型。C++ 的类型系统是你的第一道防线 ── 编译器在程序运行之前就能捕获许多错误。
基本类型有:int(通常 32 位)、double(64 位浮点数)、char(单个字节/字符)、bool(true/false)。精确的大小取决于平台,但可以使用 中的固定宽度类型如 int32_t、uint64_t 来保证。
初始化形式
C++ 有多种初始化变量的方式。现代 C++ 推荐花括号初始化({})因为它可以防止窄化转换:
#include <iostream>
#include <cstdint> // Fixed-width integer types
#include <limits> // std::numeric_limits
int main() {
// Boolean
bool is_valid = true; // true or false, typically 1 byte
// Character types
char letter = 'A'; // At least 8 bits, holds ASCII
char8_t u8ch = u8'A'; // C++20: exactly 8 bits, UTF-8
char16_t u16ch = u'\u00E9'; // At least 16 bits, UTF-16
char32_t u32ch = U'\U0001F600'; // At least 32 bits, UTF-32
// Integer types (sizes are minimums, typical x86-64 shown)
short s = 32000; // At least 16 bits (16)
int i = 2'000'000; // At least 16 bits (32) — digit separator C++14
long l = 100'000L; // At least 32 bits (64 on Linux, 32 on Windows)
long long ll = 9'000'000'000'000'000'000LL; // At least 64 bits
// Unsigned variants
unsigned int ui = 42u; // Cannot be negative
// Floating-point types
float f = 3.14f; // ~7 decimal digits precision
double d = 3.141592653589793; // ~15 decimal digits precision
long double ld = 3.14159265358979323846L;
// Fixed-width types from <cstdint> — use these when size matters
std::int32_t exact = 42; // Exactly 32 bits, guaranteed
std::uint64_t big = 18'446'744'073'709'551'615ULL; // Exactly 64 bits
std::int_fast32_t fast = 42; // At least 32 bits, fastest available
std::size_t index = 0; // Unsigned, suitable for sizes and indices
std::cout << "int: " << sizeof(int) << " bytes, max = "
<< std::numeric_limits<int>::max() << '\n';
}点击“运行”即可编译并运行这个示例。
auto 与类型推导
auto 让编译器从初始化器推导类型。它减少冗余,防止意外转换,并使代码适应未来的变化。但是 auto 永远不会推导出 const 或引用 ── 你必须显式写出 const auto&。
#include <iostream>
#include <string>
int main() {
// Direct initialization
int a(42); // OK but can be confused with function declaration
// Copy initialization
int b = 42; // OK, familiar from C
// Uniform (brace) initialization — preferred in modern C++
int c{42}; // Direct-list-initialization
int d = {42}; // Copy-list-initialization
// Brace initialization PREVENTS narrowing conversions
// double to int loses precision — the compiler will ERROR:
// int narrow{3.14}; // ERROR: narrowing conversion
int ok = 3.14; // Compiles! Silently truncates to 3 (dangerous)
// Value initialization (zero for built-in types)
int zero{}; // Guaranteed to be 0
double dzero{}; // Guaranteed to be 0.0
bool bfalse{}; // Guaranteed to be false
// auto deduction
auto x = 42; // int
auto y = 3.14; // double
auto z = 'A'; // char
auto s = std::string{"hello"}; // std::string (not const char*!)
// decltype — queries the type of an expression
decltype(x) another = 100; // same type as x → int
decltype(auto) ref = (x); // int& because (x) is an lvalue expression
std::cout << "zero-initialized int: " << zero << '\n';
std::cout << "auto-deduced string: " << s << '\n';
}点击“运行”即可编译并运行这个示例。
const 与 constexpr
const 意味着「初始化后不可修改」。使用 const 既记录了意图又防止了意外修改。constexpr 更进一步:它意味着值在编译时已知。constexpr 变量是隐式 const 的并且必须从编译时表达式初始化。
一般来说:如果可以用 constexpr 就用 constexpr(编译时已知的值),否则使用 const(运行时确定但之后不变的值),只有在变量确实需要改变时才使用非 const。
C++ 中的类型转换
C++ 有四种命名转换运算符。每种都有特定用途。永远不要使用 C 风格转换((int)x)── 它没有任何安全检查:
窄化转换会静默地丢失数据:int x = 3.14; 丢弃了 .14,而不会发出警告(使用圆括号初始化时)。花括号初始化可以防止这种情况 ── int x{3.14}; 是编译错误。
其他危险的隐式转换包括:unsigned 到 signed(大值变为负数)、int 到 float(大整数会丢失精度)、以及 size_t(无符号)到 int(有符号)的混合 ── 当你将 std::vector::size() 的返回值(size_t)与 int 比较时,后者经常出现。
编译时开启 -Wall -Wextra -Wpedantic 标志可以捕获大多数这类问题。
- 花括号初始化(
{})可以防止窄化转换 ── 在新代码中始终使用 - 注意有符号/无符号混合 ──
size_t与int的比较是常见的错误来源 - 编译时使用
-Wall -Wextra -Wpedantic来捕获隐式转换警告 - 永远不要使用 C 风格转换 ── 使用
static_cast、const_cast、reinterpret_cast(谨慎使用)
类型别名提高了可读性,并使日后更改类型更加容易。在现代 C++ 中,优先使用 using 而非 typedef:
- 每个 C++ 表达式都有一个在编译时确定的类型 ── 类型决定了大小、有效值和有效操作
- 当精确大小很重要时,使用
中的固定宽度类型(int32_t、uint64_t) - 优先使用花括号初始化 {} ── 它能防止危险的窄化转换
- 使用 nullptr(而非 NULL 或 0)表示空指针,以避免重载歧义
- auto 从初始化器推导类型;decltype 查询表达式的类型而不求值
- 使用 constexpr 定义编译时常量,使用 'using' 定义类型别名
测验 — 检验你的知识
(15 经验值)1. `int x{3.14};` 和 `int x = 3.14;` 之间有什么区别?
2. 为什么 `auto x = 42;` 推导出 `int` 而不是 `const int`?
3. 什么时候应该使用 `static_cast<int>(x)` 而不是 `(int)x`?