C++学习:六个月从基础到就业——C++基础语法回顾:数组与字符串
本文是我C++学习之旅系列的第五篇技术文章,主要回顾C++中的数组与字符串,包括传统C风格数组和字符串以及现代C++中的容器和字符串类。查看完整系列目录了解更多内容。
引言
数组和字符串是几乎所有编程语言中最基础的数据结构,它们用于存储和操作线性数据集合。在C++中,有两种主要的方式处理数组和字符串:一种是传统的C风格方法,另一种是现代C++提供的标准库容器和类。本文将详细介绍这两种方法,包括它们的语法、特性、使用场景以及最佳实践。
C风格数组
基本概念
C风格数组是一种固定大小的连续内存区域,用于存储相同类型的元素。它是C++从C语言继承的遗产之一。
数组声明与初始化
// 声明一个包含5个整数的数组
int numbers[5];
// 声明并初始化
int numbers[5] = {1, 2, 3, 4, 5};
// 省略数组大小(由初始化列表确定)
int numbers[] = {1, 2, 3, 4, 5};
// 部分初始化(未显式初始化的元素被设为0)
int numbers[5] = {1, 2, 3}; // 等价于 {1, 2, 3, 0, 0}
// 特定元素初始化(C++11)
int numbers[5] = {}; // 所有元素初始化为0
int numbers[5] = {0}; // 所有元素初始化为0
// 指定初始化器(C++20)
int numbers[5] = {[0]=1, [2]=3, [4]=5}; // 等价于 {1, 0, 3, 0, 5}
数组访问
通过索引访问数组元素,索引从0开始:
int numbers[5] = {10, 20, 30, 40, 50};
// 读取第一个元素(索引为0)
int first = numbers[0]; // first = 10
// 修改第三个元素(索引为2)
numbers[2] = 35; // 数组变为 {10, 20, 35, 40, 50}
注意:C++不会自动进行数组边界检查。访问超出数组范围的元素会导致未定义行为:
int numbers[5] = {1, 2, 3, 4, 5};
int value = numbers[10]; // 未定义行为!可能导致程序崩溃或返回垃圾值
多维数组
C++支持多维数组,它们实际上是"数组的数组":
// 2x3的二维数组
int matrix[2][3] = {
{1, 2, 3}, // 第一行
{4, 5, 6} // 第二行
};
// 访问元素
int element = matrix[1][2]; // 获取第二行第三列的元素,值为6
// 3D数组
int cube[2][3][4]; // 2页,每页3行,每行4列
数组大小
由于C风格数组的大小是固定的,可以在编译时确定其大小:
int numbers[5];
size_t size = sizeof(numbers) / sizeof(numbers[0]); // size = 5
但是,当数组作为函数参数传递时,它会退化为指针,因此上述方法不再有效。
数组与指针
在C和C++中,数组名在多数情况下会退化为指向其第一个元素的指针:
int numbers[5] = {1, 2, 3, 4, 5};
int* ptr = numbers; // ptr指向numbers[0]
// 通过指针访问数组元素
int first = *ptr; // 等价于 numbers[0]
int second = *(ptr + 1); // 等价于 numbers[1]
int third = ptr[2]; // 等价于 numbers[2],指针也可以使用[]运算符
这种数组-指针关系导致一些重要的结果:
- 函数参数中的数组实际上是指针:
void processArray(int arr[], int size) {
// arr是一个指针,而不是数组
std::cout << sizeof(arr) << std::endl; // 输出指针的大小,而非数组大小
}
void processArray(int* arr, int size) { // 与上面的函数等价
// ...
}
- 无法通过值传递数组(只能传递指针):
void function(int arr[5]) { // 实际上是 void function(int* arr)
// 无法知道数组的大小
}
- 因此必须显式传递数组大小:
void processArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
// 处理arr[i]
}
}
C风格数组的限制
虽然C风格数组简单且内存效率高,但它们有许多限制:
- 固定大小:必须在编译时确定大小,不能动态调整。
- 无边界检查:访问越界不会被检测,可能导致严重的安全问题和bug。
- 功能有限:没有内置的大小信息、搜索、排序等功能。
- 传递困难:作为函数参数时退化为指针,丢失大小信息。
- 不能直接赋值:不支持直接用一个数组赋值给另一个数组。
由于这些限制,现代C++开发通常推荐使用标准库容器而非原始数组。
C风格字符串
基本概念
C风格字符串是以空字符(‘\0’)结尾的字符数组。空字符标记字符串的结束。
字符串声明与初始化
// 字符数组
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
// 字符串字面量(自动添加结尾的空字符)
char greeting[] = "Hello"; // 等价于 {'H', 'e', 'l', 'l', 'o', '\0'}
// 注意数组大小需要包含结尾的空字符
char name[10] = "John"; // 数组大小为10,实际使用5(包括'\0')
字符串字面量
字符串字面量(用双引号括起来的字符序列)是常量,尝试修改它们会导致未定义行为:
const char* str = "Hello"; // 推荐:明确指出str指向的内容是常量
// str[0] = 'h'; // 错误:尝试修改字符串字面量
char mutable_str[] = "Hello"; // 数组内容可以修改
mutable_str[0] = 'h'; // 正确:修改了数组内容
字符串操作
C标准库(在C++中通过<cstring>
头文件提供)包含许多用于处理C风格字符串的函数:
#include <cstring>
// 计算字符串长度(不包括结尾的空字符)
char str[] = "Hello";
size_t length = strlen(str); // length = 5
// 字符串复制
char source[] = "Hello";
char destination[10];
strcpy(destination, source); // destination现在包含"Hello"
// 安全的字符串复制(指定最大长度)
char dest[10];
strncpy(dest, source, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0'; // 确保以空字符结尾
// 字符串连接
char str1[20] = "Hello, ";
char str2[] = "World!";
strcat(str1, str2); // str1现在包含"Hello, World!"
// 字符串比较
int result = strcmp("apple", "banana"); // 返回负值(apple在banana之前)
注意:这些函数可能导致缓冲区溢出,如果目标缓冲区不够大:
char small_buffer[5];
strcpy(small_buffer, "Too long string"); // 缓冲区溢出!
C风格字符串的限制
C风格字符串有几个重要的限制:
- 手动内存管理:必须确保分配足够的内存并处理内存释放。
- 容易出错:缓冲区溢出、遗漏空字符等问题很常见。
- 功能有限:基本操作需要使用库函数,不支持现代字符串功能。
- 安全风险:不安全的函数如
strcpy
、strcat
可能导致安全漏洞。
现代C++数组替代品
std::array
std::array
(在C++11中引入)是原始数组的包装器,提供了与STL容器类似的接口,同时保持了大小固定和内存连续的特性:
#include <array>
// 声明一个包含5个整数的std::array
std::array<int, 5> numbers = {1, 2, 3, 4, 5};
// 访问元素
int first = numbers[0]; // 使用下标运算符(无边界检查)
int second = numbers.at(1); // 使用at()方法(有边界检查)
// 迭代
for (const auto& num : numbers) {
std::cout << num << ' ';
}
// 获取大小
size_t size = numbers.size(); // size = 5
// 检查是否为空
bool is_empty = numbers.empty(); // is_empty = false
// 获取原始数组
int* raw_array = numbers.data();
std::array
的优势:
- 知道自己的大小:
size()
方法返回元素数量。 - 可以整体复制:支持赋值运算符。
- 边界检查:
at()
方法在访问越界时抛出异常。 - 兼容STL算法:可以与标准库算法一起使用。
- 不会退化为指针:作为函数参数时保留大小信息。
std::array
应该是新代码中使用固定大小数组的首选。
std::vector
std::vector
是一个动态数组,可以在运行时调整大小:
#include <vector>
// 创建一个空的vector
std::vector<int> numbers;
// 添加元素
numbers.push_back(1);
numbers.push_back(2);
numbers.push_back(3); // numbers现在包含 {1, 2, 3}
// 初始化方式
std::vector<int> primes = {2, 3, 5, 7, 11}; // 使用初始化列表
std::vector<int> zeros(5, 0); // 5个0
std::vector<int> sequence(10); // 10个默认初始化的int
std::vector<std::string> words(3, "hello"); // 3个"hello"字符串
// 访问元素
int first = numbers[0]; // 使用下标运算符(无边界检查)
int second = numbers.at(1); // 使用at()方法(有边界检查)
// 修改元素
numbers[0] = 10; // 修改第一个元素
// 获取大小
size_t size = numbers.size(); // size = 3
// 调整大小
numbers.resize(5); // 添加2个默认构造的元素,numbers现在包含 {10, 2, 3, 0, 0}
numbers.resize(2); // 移除最后3个元素,numbers现在包含 {10, 2}
// 迭代
for (const auto& num : numbers) {
std::cout << num << ' ';
}
// 检查是否为空
bool is_empty = numbers.empty(); // is_empty = false
// 插入与删除
numbers.insert(numbers.begin() + 1, 42); // 在第二个位置插入42,numbers变为 {10, 42, 2}
numbers.erase(numbers.begin()); // 移除第一个元素,numbers变为 {42, 2}
std::vector
的优势:
- 动态大小:可以根据需要增长或缩小。
- 自动内存管理:处理内存分配和释放。
- 丰富的接口:提供多种访问、修改和操作元素的方法。
- 高效的添加操作:
push_back()
通常是常数时间复杂度(摊销)。 - 兼容STL算法:可以与标准库算法无缝配合。
std::vector
应该是新代码中使用可变大小数组的首选。
std::string
std::string
是C++标准库中用于处理字符串的类,它提供了丰富的功能,使字符串操作更加安全和方便。
基本用法
#include <string>
// 创建字符串
std::string greeting = "Hello";
std::string empty_string; // 创建空字符串
// 连接字符串
std::string name = "John";
std::string message = greeting + ", " + name + "!"; // message = "Hello, John!"
// 获取长度
size_t length = message.length(); // 也可以使用 message.size()
// 访问字符
char first = message[0]; // 无边界检查
char second = message.at(1); // 有边界检查(越界时抛出异常)
// 子字符串
std::string sub = message.substr(7, 4); // sub = "John"(从索引7开始,长度4)
// 查找
size_t pos = message.find("John"); // pos = 7
size_t not_found = message.find("Alice"); // not_found = std::string::npos
// 替换
message.replace(7, 4, "Alice"); // message = "Hello, Alice!"
// 插入
message.insert(7, "dear "); // message = "Hello, dear Alice!"
// 删除
message.erase(7, 5); // message = "Hello, Alice!"(删除"dear ")
// 比较
bool equals = (greeting == "Hello"); // true
bool comes_before = (greeting < "Zebra"); // true(字典序比较)
// 转换为C风格字符串
const char* c_str = message.c_str(); // 返回C风格字符串(以'\0'结尾)
字符串修改
std::string str = "Hello";
// 添加内容
str.push_back('!'); // str = "Hello!"
str.append(" World"); // str = "Hello! World"
// 清空
str.clear(); // str = ""
// 重新赋值
str = "New content";
// 调整大小
str.resize(5); // str = "New c"
str.resize(10, '.'); // str = "New c....."
字符串转换
#include <string>
// 数值转字符串
int number = 42;
std::string str = std::to_string(number); // str = "42"
// 字符串转数值
std::string number_str = "42";
int num = std::stoi(number_str); // num = 42
double num_double = std::stod("3.14"); // num_double = 3.14
// 注意:转换函数在失败时会抛出异常
try {
int invalid = std::stoi("not a number");
} catch (const std::invalid_argument& e) {
std::cout << "转换失败:无效的参数" << std::endl;
} catch (const std::out_of_range& e) {
std::cout << "转换失败:结果超出范围" << std::endl;
}
std::string vs C风格字符串
std::string
对比C风格字符串的优势:
- 自动内存管理:不需要担心缓冲区大小和内存释放。
- 安全性:提供边界检查,防止缓冲区溢出。
- 丰富的功能:内置大量字符串操作函数。
- 方便的操作符:支持
+
、==
、<
等直观的运算符。 - 与STL兼容:可以与标准库算法和容器一起使用。
常见的字符串处理操作
字符串分割
C++标准库没有内置的分割函数,但可以实现一个:
#include <string>
#include <vector>
#include <sstream>
std::vector<std::string> split(const std::string& s, char delimiter) {
std::vector<std::string> tokens;
std::string token;
std::istringstream token_stream(s);
while (std::getline(token_stream, token, delimiter)) {
tokens.push_back(token);
}
return tokens;
}
// 使用示例
std::string csv = "apple,banana,cherry";
std::vector<std::string> fruits = split(csv, ',');
// fruits = {"apple", "banana", "cherry"}
字符串修剪(去除首尾空白)
#include <string>
#include <algorithm>
std::string trim(const std::string& str) {
auto start = std::find_if_not(str.begin(), str.end(), [](unsigned char c) {
return std::isspace(c);
});
auto end = std::find_if_not(str.rbegin(), str.rend(), [](unsigned char c) {
return std::isspace(c);
}).base();
return (start < end) ? std::string(start, end) : std::string();
}
// 使用示例
std::string padded = " hello world ";
std::string trimmed = trim(padded); // trimmed = "hello world"
大小写转换
#include <string>
#include <algorithm>
#include <cctype>
std::string to_lower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::tolower(c); });
return s;
}
std::string to_upper(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::toupper(c); });
return s;
}
// 使用示例
std::string mixed = "Hello World";
std::string lower = to_lower(mixed); // lower = "hello world"
std::string upper = to_upper(mixed); // upper = "HELLO WORLD"
字符串字面量
传统字符串字面量
C++中的传统字符串字面量是以双引号括起来的字符序列:
const char* str = "Hello, World!";
这种字面量是由字符组成的数组,以空字符结尾,类型为const char[N]
,但通常会退化为const char*
。
C++11的原始字符串字面量
C++11引入了原始字符串字面量,使用R"delimiter(content)delimiter"
语法,可以包含未转义的特殊字符:
// 传统字符串需要转义反斜杠和引号
std::string path = "C:\\Program Files\\App\\config.ini";
std::string json = "{\"name\": \"John\", \"age\": 30}";
// 原始字符串不需要转义
std::string path_raw = R"(C:\Program Files\App\config.ini)";
std::string json_raw = R"({"name": "John", "age": 30})";
// 自定义分隔符处理包含 )" 的字符串
std::string complicated = R"***("This string contains ) and " characters")***";
原始字符串字面量特别适合表示正则表达式、文件路径、JSON和XML等包含大量特殊字符的字符串。
C++14的字符串字面量后缀
C++14引入了用户定义的字面量,包括字符串字面量的后缀:
#include <string>
using namespace std::string_literals;
// 使用 s 后缀创建std::string
auto str1 = "Hello"s; // 类型为std::string,而非const char*
// 使用标准字符串字面量
auto str2 = "Hello"; // 类型为const char*
C++17的字符串视图
C++17引入了std::string_view
,一个非拥有的字符串引用,提高了字符串处理的效率:
#include <string_view>
using namespace std::string_view_literals;
// 使用 sv 后缀创建std::string_view
auto view = "Hello"sv; // 类型为std::string_view
// string_view的基本使用
std::string_view sv = "Hello, World!";
std::cout << sv.substr(0, 5) << std::endl; // 输出"Hello",无复制
std::string_view
适合作为函数参数,避免不必要的字符串复制:
void process(std::string_view sv) {
// 使用sv,但不修改它
}
// 可以接受多种字符串类型,无需复制
process("literal"); // const char*
process(std::string("string")); // std::string
多维数据结构
二维数组
C++中有多种方式表示二维数组:
使用C风格二维数组
// 静态分配的二维数组
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 访问元素
int element = matrix[1][2]; // 获取第2行第3列的元素,值为7
使用std::array的二维数组
#include <array>
// 使用std::array定义二维数组
std::array<std::array<int, 4>, 3> matrix = {{
{{1, 2, 3, 4}},
{{5, 6, 7, 8}},
{{9, 10, 11, 12}}
}};
// 访问元素
int element = matrix[1][2]; // 获取第2行第3列的元素,值为7
使用std::vector的二维数组
#include <vector>
// 创建3x4的二维vector
std::vector<std::vector<int>> matrix(3, std::vector<int>(4));
// 初始化
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
matrix[i][j] = i * 4 + j + 1;
}
}
// 或者直接初始化
std::vector<std::vector<int>> matrix = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 访问元素
int element = matrix[1][2]; // 获取第2行第3列的元素,值为7
// 动态增加行
matrix.push_back({13, 14, 15, 16}); // 添加第4行
使用一维数组模拟二维数组
有时,使用一维数组模拟二维数组可以提高性能(内存连续性更好):
#include <vector>
int rows = 3, cols = 4;
std::vector<int> matrix(rows * cols);
// 设置元素
void set(std::vector<int>& m, int row, int col, int value, int cols) {
m[row * cols + col] = value;
}
// 获取元素
int get(const std::vector<int>& m, int row, int col, int cols) {
return m[row * cols + col];
}
// 使用示例
set(matrix, 1, 2, 7, cols); // 设置第2行第3列为7
int element = get(matrix, 1, 2, cols); // 获取第2行第3列的元素,值为7
字符串数组
字符串数组是包含多个字符串的数组:
使用C风格字符串数组
// C风格字符串数组
const char* fruits[] = {"apple", "banana", "cherry"};
// 访问元素
const char* first_fruit = fruits[0]; // "apple"
使用std::string数组
#include <string>
// 使用std::string的数组
std::string fruits[] = {"apple", "banana", "cherry"};
// 使用std::vector<std::string>
std::vector<std::string> fruits_vec = {"apple", "banana", "cherry"};
fruits_vec.push_back("date"); // 添加新元素
// 访问与修改
std::string first = fruits_vec[0]; // "apple"
fruits_vec[0] = "avocado"; // 修改第一个元素
实际应用案例
案例1:文本处理
以下是一个简单的文本处理程序,用于统计文件中每个单词的出现频率:
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
#include <sstream>
#include <algorithm>
#include <cctype>
// 转换为小写并移除标点
std::string normalize(const std::string& word) {
std::string result;
for (char c : word) {
if (std::isalpha(c)) {
result.push_back(std::tolower(c));
}
}
return result;
}
// 分割字符串
std::vector<std::string> split(const std::string& text, char delimiter = ' ') {
std::vector<std::string> tokens;
std::istringstream stream(text);
std::string token;
while (std::getline(stream, token, delimiter)) {
if (!token.empty()) {
tokens.push_back(token);
}
}
return tokens;
}
int main() {
std::ifstream file("sample.txt");
if (!file) {
std::cerr << "无法打开文件" << std::endl;
return 1;
}
std::map<std::string, int> word_count;
std::string line;
// 逐行读取文件
while (std::getline(file, line)) {
auto words = split(line);
for (const auto& word : words) {
std::string normalized = normalize(word);
if (!normalized.empty()) {
word_count[normalized]++;
}
}
}
// 转换为vector以便排序
std::vector<std::pair<std::string, int>> word_freq(
word_count.begin(), word_count.end()
);
// 按频率排序
std::sort(word_freq.begin(), word_freq.end(),
[](const auto& a, const auto& b) {
return a.second > b.second; // 频率降序
});
// 打印前10个高频词
std::cout << "最常见的10个单词:" << std::endl;
for (size_t i = 0; i < std::min(word_freq.size(), size_t(10)); ++i) {
std::cout << word_freq[i].first << ": "
<< word_freq[i].second << std::endl;
}
return 0;
}
案例2:矩阵操作
以下是一个简单的矩阵类,演示如何使用二维数组表示和操作矩阵:
#include <iostream>
#include <vector>
#include <stdexcept>
class Matrix {
private:
std::vector<std::vector<double>> data;
size_t rows;
size_t cols;
public:
// 构造函数
Matrix(size_t r, size_t c, double initial_value = 0.0)
: rows(r), cols(c), data(r, std::vector<double>(c, initial_value)) {}
// 访问元素
double& at(size_t r, size_t c) {
if (r >= rows || c >= cols) {
throw std::out_of_range("Matrix indices out of range");
}
return data[r][c];
}
const double& at(size_t r, size_t c) const {
if (r >= rows || c >= cols) {
throw std::out_of_range("Matrix indices out of range");
}
return data[r][c];
}
// 矩阵加法
Matrix operator+(const Matrix& other) const {
if (rows != other.rows || cols != other.cols) {
throw std::invalid_argument("Matrix dimensions must match for addition");
}
Matrix result(rows, cols);
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
result.data[i][j] = data[i][j] + other.data[i][j];
}
}
return result;
}
// 矩阵乘法
Matrix operator*(const Matrix& other) const {
if (cols != other.rows) {
throw std::invalid_argument("Matrix dimensions must match for multiplication");
}
Matrix result(rows, other.cols, 0.0);
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < other.cols; ++j) {
for (size_t k = 0; k < cols; ++k) {
result.data[i][j] += data[i][k] * other.data[k][j];
}
}
}
return result;
}
// 打印矩阵
void print() const {
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
std::cout << data[i][j] << " ";
}
std::cout << std::endl;
}
}
};
int main() {
// 创建两个矩阵
Matrix A(2, 3);
Matrix B(3, 2);
// 初始化矩阵A
A.at(0, 0) = 1; A.at(0, 1) = 2; A.at(0, 2) = 3;
A.at(1, 0) = 4; A.at(1, 1) = 5; A.at(1, 2) = 6;
// 初始化矩阵B
B.at(0, 0) = 7; B.at(0, 1) = 8;
B.at(1, 0) = 9; B.at(1, 1) = 10;
B.at(2, 0) = 11; B.at(2, 1) = 12;
std::cout << "矩阵A:" << std::endl;
A.print();
std::cout << "矩阵B:" << std::endl;
B.print();
// 计算矩阵乘积
Matrix C = A * B;
std::cout << "矩阵A * B:" << std::endl;
C.print();
return 0;
}
案例3:CSV解析器
以下是一个简单的CSV文件解析器,演示如何处理字符串和数组:
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <sstream>
class CSVParser {
private:
char delimiter;
bool has_header;
std::vector<std::string> headers;
std::vector<std::vector<std::string>> data;
// 分割CSV行
std::vector<std::string> splitLine(const std::string& line) {
std::vector<std::string> fields;
std::string field;
bool in_quotes = false;
std::ostringstream current_field;
for (char c : line) {
if (c == '"') {
in_quotes = !in_quotes;
} else if (c == delimiter && !in_quotes) {
fields.push_back(current_field.str());
current_field.str("");
} else {
current_field << c;
}
}
// 添加最后一个字段
fields.push_back(current_field.str());
return fields;
}
public:
CSVParser(char delim = ',', bool header = true)
: delimiter(delim), has_header(header) {}
// 从文件加载数据
bool loadFromFile(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
return false;
}
std::string line;
bool first_line = true;
while (std::getline(file, line)) {
auto fields = splitLine(line);
if (first_line && has_header) {
headers = fields;
first_line = false;
} else {
data.push_back(fields);
first_line = false;
}
}
return true;
}
// 获取表头
const std::vector<std::string>& getHeaders() const {
return headers;
}
// 获取所有数据
const std::vector<std::vector<std::string>>& getData() const {
return data;
}
// 获取特定行
const std::vector<std::string>& getRow(size_t index) const {
if (index >= data.size()) {
throw std::out_of_range("Row index out of range");
}
return data[index];
}
// 获取特定列
std::vector<std::string> getColumn(size_t index) const {
if (data.empty() || index >= data[0].size()) {
throw std::out_of_range("Column index out of range");
}
std::vector<std::string> column;
for (const auto& row : data) {
column.push_back(row[index]);
}
return column;
}
// 获取特定列(按列名)
std::vector<std::string> getColumn(const std::string& column_name) const {
if (!has_header) {
throw std::runtime_error("No header information available");
}
auto it = std::find(headers.begin(), headers.end(), column_name);
if (it == headers.end()) {
throw std::runtime_error("Column name not found");
}
return getColumn(std::distance(headers.begin(), it));
}
// 打印数据
void print() const {
if (has_header) {
for (size_t i = 0; i < headers.size(); ++i) {
std::cout << headers[i];
if (i < headers.size() - 1) std::cout << "\t";
}
std::cout << std::endl;
// 打印分隔线
for (size_t i = 0; i < headers.size(); ++i) {
std::cout << std::string(headers[i].length(), '-');
if (i < headers.size() - 1) std::cout << "\t";
}
std::cout << std::endl;
}
for (const auto& row : data) {
for (size_t i = 0; i < row.size(); ++i) {
std::cout << row[i];
if (i < row.size() - 1) std::cout << "\t";
}
std::cout << std::endl;
}
}
};
int main() {
CSVParser parser;
if (parser.loadFromFile("data.csv")) {
std::cout << "CSV数据:" << std::endl;
parser.print();
try {
// 获取特定列数据
auto ages = parser.getColumn("Age");
std::cout << "\n年龄数据:" << std::endl;
for (const auto& age : ages) {
std::cout << age << " ";
}
std::cout << std::endl;
} catch (const std::exception& e) {
std::cerr << "错误:" << e.what() << std::endl;
}
} else {
std::cerr << "无法打开CSV文件" << std::endl;
}
return 0;
}
最佳实践与建议
-
优先使用标准库容器:
- 使用
std::vector
代替动态分配的C风格数组 - 使用
std::array
代替固定大小的C风格数组 - 使用
std::string
代替C风格字符串
- 使用
-
避免C风格数组的缺陷:
- 避免使用
new[]
/delete[]
手动管理数组内存 - 避免数组作为参数时的指针退化问题
- 避免使用
-
字符串处理:
- 对于只读字符串引用,考虑使用
std::string_view
(C++17) - 使用
std::string
的成员函数代替C风格字符串函数 - 使用
s
字面量后缀创建std::string
(using namespace std::string_literals;
)
- 对于只读字符串引用,考虑使用
-
安全性:
- 总是使用边界检查的方法(如
at()
)而不是未检查的下标运算符,除非对性能要求极高 - 避免C风格字符串函数(如
strcpy
、strcat
)可能导致的缓冲区溢出
- 总是使用边界检查的方法(如
-
性能考虑:
- 使用引用避免不必要的复制,尤其是对大型字符串和容器
- 考虑使用
reserve()
预分配std::vector
和std::string
的容量 - 对于固定大小的多维数组,使用一维数组+索引计算可能提供更好的缓存局部性
-
现代C++特性:
- 使用范围for循环遍历容器和数组
- 使用
auto
简化代码,特别是处理复杂的STL类型时 - 利用移动语义和右值引用减少不必要的复制
- 使用原始字符串字面量(
R"()"
)处理包含转义字符的字符串
总结
数组和字符串是C++编程的基础构建块,掌握它们的用法对于编写高效、安全的代码至关重要。本文回顾了C++中处理数组和字符串的各种方法,从传统的C风格方法到现代C++提供的标准库容器和类。
虽然C风格数组和字符串因其简单性和性能优势仍然在某些情况下有用,但现代C++开发应该优先考虑std::vector
、std::array
和std::string
等安全且功能丰富的替代品。这些现代工具提供了自动内存管理、边界检查和丰富的操作接口,可以帮助我们编写更加安全、可维护的代码。
在下一篇文章中,我们将探讨C++中的指针与引用,这是C++区别于许多其他编程语言的重要特性。
参考资料
- Bjarne Stroustrup. The C++ Programming Language (4th Edition)
- Scott Meyers. Effective Modern C++
- cppreference.com - Arrays
- cppreference.com - std::string
- cppreference.com - std::vector
- cppreference.com - std::array
- C++ Core Guidelines - Arrays and Pointers
这是我C++学习之旅系列的第五篇技术文章。查看完整系列目录了解更多内容。