一个程序只处理一个值时,类型选择通常不难。真正容易出错的是“一组值”:用户输入的一行文字、不断到来的温度读数、固定三个通道的颜色,表面上都能用下标访问,生命周期和长度规则却完全不同。
如果把动态数据塞进固定数组,程序迟早要自己处理长度;如果把固定协议字段放进可随意增长的容器,代码又没有表达“长度不变”这个约束。更麻烦的是,初始化出错误的元素个数或用错文本输入方式,往往都能通过编译。
这篇文章围绕四种基础工具展开:
std::string:一段文本。std::vector<T>:数量会变化的一组同类型元素。std::array<T, N>:长度在编译期固定、但仍有容器接口的一组元素。T[N]:直接表达固定连续存储,常见于底层接口。目标不只是记住成员函数,而是能判断数据模型、写对初始化、读入完整文本,并用合适的方式增长和遍历序列。
假设一个日志分析程序要保存四类数据:原始消息、消息中的数字、固定的 RGB 三通道值,以及交给旧式 C 接口的缓冲区。它们都像“序列”,但应该使用不同类型。
一个实用的判断顺序是:
std::string,而不是 std::vector<char>。std::vector<T>。std::array<T, N>。这几种类型都把元素连续放置,因此能快速下标访问,也能与需要指针的接口交互。区别主要在谁记录长度、谁负责增长,以及是否具备完整的值语义:string、vector 和 array 都能整体复制,内置数组不能。

默认规则可以很简单:文本用 std::string,普通动态序列用 std::vector<T>,固定序列用 std::array<T, N>。内置数组不是“更专业”的选项,它只是离语言底层更近,也把更多边界责任交给调用者。
分别包含 <string>、<vector> 和 <array>,并保留清楚的 std:: 前缀。
初始化不仅决定初值,也决定元素个数。std::vector 的圆括号和花括号尤其容易混淆。
std::vector<int> a(3); // 3 个元素:0, 0, 0
std::vector<int> b{3}; // 1 个元素:3
std::vector<int> c(3, 7); // 3 个元素:7, 7, 7
std::vector<int> d{3, 7}; // 2 个元素:3, 7圆括号形式通常调用“数量”构造:(count) 或 (count, value)。花括号形式优先表示元素列表。两种写法都合法,但含义不同。
下面的程序把几种初始化放在一起观察。
#include <array>
#include <iostream>
#include <string>
#include <vector>
template <class Range>
void print(const Range& values) {
std::cout << '[';
bool first = true;
for (const auto& value : values) {
if (!first) std
输出:
empty_text.size = 0
bars = ====
[0, 0, 0]
[3]
[0, 0, 0, 0]
[8, 9, 0, 0]std::array<int, 4> partial{8, 9} 只显式提供两个值,剩余元素会被值初始化为 0。空花括号 {} 是把固定数组全部清零的简洁写法。
内置数组也有类似规则,但局部内置数组有一个危险差异:
void work() {
int unknown[4]; // 元素值不确定,读取它们是错误
int zeroed[4]{}; // 4 个 0
int partial[4]{8, 9}; // 8, 9, 0, 0
}不要依赖调试环境“碰巧把内存清零”。需要初值就写出初始化器。
不要为了统一风格而把所有初始化都机械改成花括号。对 std::vector<int> 来说,{5} 是“一个值为 5 的元素”,(5) 才是“五个元素”。先确认你要表达的是元素列表还是元素数量。
下面的实验室先用具体情境检查类型选择,再把圆括号、花括号和固定数组的初始结果展开成内存格。重点观察“元素个数”和“元素内容”是否同时符合预期。
std::string 的元素类型是 char,但它提供的是文本接口:读取、拼接、比较、查找、截取和删除。只要数据代表名字、命令、路径或一行输入,使用 std::string 通常比字符数组更直接。
operator>> 读取一个以空白分隔的词;std::getline 默认读取到换行符。
int count = 0;
std::cin >> count;
std::string word;
std::cin >> word; // 读取一个词
std::string title;
std::getline(std::cin >> std::ws, title); // 读取后续一整行前一个 >> 可能留下换行;std::ws 会先吃掉所有开头空白。这适合标题不允许以空格开头的规则;若开头空格有意义,就应单独处理遗留换行。

getline 则以换行为边界。混用二者时,要明确当前读取位置以及缓冲区里留下的分隔符。例如,const auto dot = filename.rfind('.') 从右侧找点号;只有 dot != std::string::npos 时,才能安全地用 filename.substr(dot + 1) 取扩展名。
这个版本忽略 ASCII 空白和大小写。传给 <cctype> 函数前先转成 unsigned char,避免某些平台上负 char 带来的未定义行为。
#include <cctype>
#include <iostream>
#include <string>
std::string normalize_ascii(const std::string& input) {
std::string result;
for (unsigned char ch : input) {
if (!std::isspace(ch)) {
result.push_back(static_cast<char>(std::tolower(ch)));
输出:
Never odd or even: yes
C plus plus: no这里把 right 初始化为 size(),再在访问前递减。空字符串时 left < right 一开始就是假,不会计算 size() - 1,因此避开了无符号下溢。
std::string::size() 统计的是 char 代码单元。UTF-8 文本中,一个汉字通常占多个 char,所以它不是“用户看到的字符数”。需要按 Unicode 码点或字素处理时,应使用明确的 Unicode 文本方案。
std::vector<T> 适合“现在不知道最终有多少个”的数据。它保存当前元素数,也负责在空间不足时搬到更大的连续区域。
下面的程序读取任意多个分数,直到输入结束,再计算平均值。它先处理空输入,避免除以 0。
#include <iomanip>
#include <iostream>
#include <vector>
int main() {
std::vector<double> scores;
for (double score = 0.0; std::cin >> score;) {
scores.push_back(score);
}
if (scores.empty()) {
std::cout << "no scores\n";
输入:
80 92 88 100输出:
count: 4
average: 90.0push_back(value) 会在末尾创建一个元素,因此每成功调用一次,size() 就增加 1。通常可以先从空 vector 开始,边读取边追加。入门阶段只需注意:size() 是已有元素数,capacity() 是内部可容纳的空间;reserve() 只预留空间而不创建元素,resize() 才会改变元素数。更完整的容量策略放在后面的顺序容器篇讨论。
std::vector<int> original{10, 20, 30};
std::vector<int> copy = original;
copy[0] = 99;
// original[0] 仍然是 10复制会复制元素,两个 vector 之后可以独立修改。按值返回 vector 也是正常写法,编译器可以直接构造结果或移动内部资源;不要为了躲避复制而返回局部变量的引用。

push_back 都在末尾创建元素并增加 size();右侧的副本拥有独立元素,因此改动 copy[0] 不会改动 original[0]。长度为 n 的序列,有效下标组成半开区间 [0, n)。也就是说,首下标是 0,尾下标是 n - 1;n 本身已经越界。
string、vector 和 std::array 都提供两种下标式访问:
内置数组只有不检查边界的 []。初学时可以这样选择:循环结构已经保证下标有效时用 [];下标来自外部输入时可用 at(),并按程序需求处理异常。
#include <iostream>
#include <stdexcept>
#include <vector>
int main() {
const std::vector<int> values{10, 20, 30};
for (std::size_t index : {std::size_t{1}, std::size_t{8}}) {
try {
const
输出:
values[1] = 20
index 8 is out of range越界使用 operator[] 不保证抛异常,也不保证立即崩溃。程序可能读到无关数据,或悄悄破坏别的对象。能够“跑完”不代表访问合法。
固定长度不是缺点。如果长度本身就是规则,例如 IPv4 的 4 个字节、RGB 的 3 个通道或棋盘的一行 8 格,std::array<T, N> 能把规则写进类型。
#include <array>
#include <iostream>
using Rgb = std::array<int, 3>;
Rgb brighten(Rgb color, int amount) {
for (int& channel : color) {
channel += amount;
if (channel > 255) channel = 255;
}
return
输出:
original: 240 100 20
result: 255 130 50 函数按值接收 Rgb,修改的是副本。original 不变,这和 vector、string 的值语义一致。
![std::array 与内置数组的接口差异 std::array<int, 4> 与内置 int[4] 的固定长度、连续存储和接口差异对比图](https://media.edu-free.com/uploads/ch03_array_vs_builtin_fe7eeae34b.png)
std::array 保留长度与容器接口;内置数组传入普通函数时常退化为首元素指针,所以通常还要单独传递长度。内置数组传给普通函数时,长度信息通常丢失;固定长度接口则可直接接收 std::array:
void clear(int values[], std::size_t count) {
for (std::size_t i = 0; i < count; ++i) {
values[i] = 0;
}
}
void clear(std::array<int, 4>& values) {
std::array<int, 3> 和 std::array<int, 4> 是不同类型。这会让错误长度尽早暴露,但也意味着接收其中一种的函数不能自动接收另一种。
内置数组的名称常会转换为首元素指针,指针本身不知道数组长度。跨函数传递时若仍使用这种接口,应把长度一起传递,并由调用方保证二者一致。
标准库用 [begin, end) 表示从首元素开始、到尾后位置结束的范围;处理完整序列时,范围 for 通常更直接。循环变量是否带引用,决定你处理的是副本还是原元素。
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<int> values{1, 2, 3};
std::cout << "copies: ";
for (int value : values) {
value *= 10; // 只修改局部副本
std::cout << value << ' ';
输出:
copies: 10 20 30
Ada Bjarne
values: 10 20 30 三种写法可按意图选择:
结构性修改可能让先前取得的迭代器、指针或引用失效;入门阶段最稳妥的原则是不要长期保存这些位置标记,修改序列后重新取得。各操作的详细失效规则会在顺序容器篇集中说明。
看到范围 for 时,先问自己:“我要修改原元素吗?”只读较大的对象通常用 const auto&,需要修改用 auto&,刻意处理副本才用 auto。
下面的观察器把下标访问与范围 for 放在同一组序列上。先拖动下标观察 [0, n) 的边界,再切换 auto、auto& 和 const auto&,看循环体处理的是副本、可写引用还是只读引用。
最后把几种类型放进同一个小程序。需求是:逐行读取标签,忽略空行,统一为小写,保存全部有效标签,并统计首字符属于字母、数字还是其他字符。
数据模型很自然:
std::string。std::vector<std::string>。std::array<std::size_t, 3>。#include <array>
#include <cctype>
#include <iostream>
#include <string>
#include <vector>
std::string lower_ascii(std::string text) {
for (char& raw : text) {
const auto ch = static_cast<unsigned char>(raw);
raw = static_cast<char>(std::tolower(ch));
}
输入:
Cpp
2026
#Tips
Vector输出:
tags (4):
- cpp
- 2026
- #tips
- vector
letter=2, digit=1, other=1这段代码先跳过空行,因此 tag[0] 的下标有效;字符分类前转成 unsigned char。counts{} 把三个计数清零,分类函数只返回 0、1、2。输出阶段用 const auto& 读取标签,避免复制字符串。
#、其他”四类。选择序列类型时,可以用三句话做最终检查:数据是不是文本,长度会不会变化,固定长度是否属于业务规则。选定类型后,再检查初始化是否创建了预期数量的元素、文本输入是否保留了需要的空格,以及遍历时究竟需要副本还是引用。