类型的概念在 C++ 中非常重要,它不仅是存储布局的标识符,更是开发者与编译器及团队成员之间的契约,用以明确“此对象意在何种用途”与“具备何种能力”。每一个变量、函数形参与返回值都必须拥有静态类型,编译器据此完成类型检查、重载决议、模板推导与各类优化。
但是,类型既能成就优雅的表达,也可能成为误用和混乱的源头。为此,我们在选则类型时应遵循一组明确的规则:
- 语义先行:先明确业务或逻辑层面对象的角色,再选用最能直观映射该角色的类型;
- 最小权限:仅赋予对象或函数完成其职责所需的最小能力,避免额外开放不必要的操作;
- 值语义优先:优先使用可按值拷贝或移动的类型,以简化生命周期管理并提升测试可靠性;
- 界限清晰:计数与索引使用合适宽度的无符号类型;差值运算使用有符号类型;协议与存储需固定宽度整数;
- 显式可空:只有在业务模型确实允许“无对象”状态时,才使用可空类型(如裸指针或
std::optional),否则采用不可空引用或视图; - 可移植可度量:当性能与可移植性至关重要时,明确指定整数宽度、数据对齐与字节序,并优先使用标准库类型以保证一致行为。
在本节中,我对 C++ 类型系统的主要功能进行了非正式的概述,我们先从最基本的标量类型说起,再逐步过渡到复合类型和可调用类型。每一小节都将按照定义、选型原则、示例的结构展开。
1. 标量类型 (Scalar Types)
定义
能直接持有一个值、可按值拷贝/比较/赋值的一类类型集合。包含:
- 算数类型 (arithmetic);
- 枚举类型 (enumeration);
- 指针类型 (pointer);
- 成员指针 (member pointer);
- 空指针类型;
1.1 算术类型 (Arithmetic Types)
1.1.1 整型 (Integral)
C++ 沿襲 C 语言传统,将所有用于表示离散整数的类型归为整型。包括:
-
字符类型:
char、signed char、unsigned char- 虽然
char常用于存放文本字符,但其本质是一个小范围整数(通常 8 位),支持算术运算。
- 虽然
-
布尔类型:
bool- 只有
true/false两值,但底层常以 0/1 存储
- 只有
-
常规整型:
short、int、long、long long(及其无符号变体) -
固定宽度整型(
<cstdint>提供):std::int8_t、std::uint16_t、std::int32_t、std::uint64_t等。保证在所有平台上具有相同位宽和符号属性。一般用于制定跨平台二进制协议、文件格式,可确保数据布局可预测。 -
特殊整型:
std::size_tstd::size_t最早在 C89 的<stddef.h>(C++ 中为<cstddef>)中作为“对象大小类型”(object size type)引入,用于表示任意对象或数组的字节长度;在 C99 中被正式定义为无符号整数,C++ 沿用其语义。其底层类型因平台而异:在 32 位系统上通常对应unsigned int,而在 64 位系统上则可能是unsigned long或unsigned long long。由于以下几点特性,我推荐优先使用std::size_t作为计数和下标类型:- 覆盖范围:它能表示平台上可分配的最大对象大小,不会因索引超过范围而发生溢出
- 标准一致性:标准库容器(如
std::vector::size())即返回std::size_t,使用相同类型可以避免隐式转换
char c = 'A'; // 实则为整数 65 (ASCII)
signed char sc = -1; // 通常范围 -128...127
unsigned char uc = 255; // 范围 0...255
bool flag = true; // 底层与整型兼容
int i = 42;
int L = 1'000'000L;
// 使用固定宽度整型的场景
std::uint32_t packetSize = 1024;
std::int64_t offset = -4096;
// 使用 std::size_t 的场景
std::vector<int> v{1,2,3};
for (std::size_t i = 0; i < v.size(); ++i) {
// ...
}
1.1.2 浮点型 (Floating)
用于近似表示实数,一般用在科学计算、图形坐标、物理模拟等需小数运算的场景。包括:
float通常 32 位double通常 64 位long double
float PI = 3.14f; // 单精度
double x = 2.718; // 双精度
long double y = 1.0L; // 扩展精度
1.2 枚举类型(Enumeration)
- 定义:将一组相关常量赋予命名,使代码更具自解释性。
enum:传统枚举,底层类型由实现决定;enum class:强类型枚举,可指定底层类型;
enum Color { Red, Green, Blue };
enum class Direction : uint8_t { North, South, East, West };
1.3 指针 (Pointer)
指针是 C++ 的核心概念之一,它直接映射到内存地址,为语言提供了对底层资源的精细控制能力。通过指针,我们既能高效地遍历与操作动态分配的内存,也能实现复杂的数据结构(如链表、树、图等)的灵活连接。与此同时,指针的使用也暗含所有权与生命周期管理的挑战:裸指针(T*)虽零开销却不具资源回收语义,智能指针(std::unique_ptr、std::shared_ptr 等)则通过 RAII 模式自动管理内存,而成员指针(T C::*)与空指针类型(std::nullptr_t)则分别承载对类内部成员的访问和对空值的类型安全支持。接下来的部分我将依次探讨这些指针形态的定义、语义、使用场景及最佳实践。
1.3.1 裸指针与智能指针
裸指针 (T*)
- 定义:直接存储对象地址,可为
nullptr。 - 语义:
- 非拥有借用 (non-owning borrow):临时访问已有对象,函数不负责销毁;
- 可空拥有 (owning, nullable):不推荐;若用于所有权,必须手动
delete,极容易出错。
- 风险:
- 空指针访问导致未定义行为;
- 悬垂 (Dangling) 指针易引发崩溃;
- 无法在类型层面表达释放责任,容易遗漏或重复释放。
// 非拥有借用:调用者负责生命周期
void process(Resource *r)
{
if (r) // 必须检查
r->use();
}
若确实需要可空借用,请在接口文档或类型签名中注明;若需要管理资源所有权,请优先使用智能指针。
1.3.2 成员指针
1.3.3 空指针类型
1.4 数字字面量与字节语义
- 数字字面量分隔
- C++14 起支持单引号作为千位分隔符,增强可读性:
auto million = 1'000'000u;
std::byte- 定义于 C++17,仅作原始字节标签,不具算术运算。
- 若需要当作整数使用,需显式转换:
std::byte b{0xFF};
int x = std::to_integer<int>(b);
2. 复合类型 (compound types)
定义
由其它类型”复合“而成,或不直接是一个可持值的标量。包含:
- 数组类型 (
T[N]、T[]); - 函数类型 (
R(Args...),函数不是对象); - 引用类型 (
T&、T&&); - 类类型 (
class/struct,包括所有用户自定义类型); - 联合类型 (
union)
按照语义职责划分,复合类型又可以被划为三类:
- 承载/拥有 (own):以对象语义持有并管理状态或资源(
std::vector、std::array、自定义class/struct、union/variant); - 借用 (borrow):以非拥有语义对现有对象进行别名或切片访问(
T&/const T&、std::span、std::string_view、指针T*); - 调用 (call):以可调用语义描述行为的类型或包装(函数类型
R(Args...)、函数指针、std::function、lambda 的闭包类型)。
理解处理复合类型时的选型原则,可以帮助我们用类型表达语义、将错误前置到编译期。在成本最低、反馈最快的阶段阻止将来最昂贵的错误,是工程实践的首要目标之一。
一、数组
C/C++ 语言中内置了一种数组类型,即写成 T[N]/T[] 的定长、连续内存的序列,我们称之为内建数组。内建数组本身并不难用,甚至在一些必须保持 C ABI 或需要精确布局的场景下是第一选择。但是在大多数表达式里内建数组会自动衰变为 T* 指向首元素,这是 C 时代为了便于指针算数与兼容函数调用的设计,会导致关键语义被丢在了约定里而不是类型里,性能大打折扣。
现代 C++ 通过 std::array/std::span 把长度与借用语义带回类型系统,这两种可见于类型的语义正是为了把最容易、最昂贵的错误(如越界、悬垂、无谓拷贝)前置到编译期。
原则
- 对于定长序列(即编译期已知):
std::array<T, N>- 具有值语义,且可与算法/Ranges 相互操作;
std::array<int, 3> a {1, 2, 3};
auto b = a; // 可直接拷贝
- 对于变长序列(运行期):
std::vector<T>- 自动管理内存,使用
reverse可控制其增长。返回值传递具备移动语义;
- 自动管理内存,使用
std::vector<float> v;
v.reserve(100);
v.push_back(3.14f);
- 对于连续切片借用(不拥有):
std::span<T>/std::span<const T>- 类型中带有长度,不用再额外传递
size_t,更安全,可读性也更高;
- 类型中带有长度,不用再额外传递
void process(std::span<const int> xs);
// 调用:process({arr, arr_size});
- API 中不要写
T a[]或T* p, size_t n,否则数组一到函数就会衰变成指针从而丢失长度信息
// ❌
void f(int *p, std::size_t n);
// ✅
void f(std::span<int> xs);
- 对只读文本使用
std::string_view
void log(std::string_view sv);
建议
- 不要对非平凡类型使用
memcpy/memset,对于std::string或自定义对象,一旦memcpy就会导致未定义行为。推荐使用以下写法:
std::ranges::copy(src, dest.begin());
- 多维数组优先考虑扁平+步长。
vector<vector<int>>的写法会分散内存,影响缓存。推荐写法:
std::vector<T> buf(rows * cols);
auto at = [&](int r, int c){ return buf[r * cols + c]; };
二、函数
函数类型描述可用 R(Args...)。函数本身不是对象,但是