数组是C++中最基础也是最强大的数据结构之一。本质上,数组是一块连续的内存空间,用于存储相同类型的元素集合。这种连续存储的特性带来了两个重要特点:
让我们看一个简单的内存布局示例:
cpp复制int arr[5] = {10, 20, 30, 40, 50};
在内存中的表示可能如下:
code复制地址: 0x1000 0x1004 0x1008 0x100C 0x1010
值: [10] [20] [30] [40] [50]
每个int占4字节(32位系统),所以相邻元素地址相差4。这也是为什么数组下标从0开始——第一个元素的地址就是数组基地址,第n个元素的地址=基地址 + n*元素大小。
C++提供了多种数组初始化的语法形式,各有适用场景:
cpp复制// 1. 声明时不初始化(元素值不确定)
int arr1[5];
// 2. 传统初始化列表
int arr2[5] = {1, 2, 3, 4, 5};
// 3. 部分初始化(剩余元素自动初始化为0)
int arr3[5] = {1, 2}; // [1,2,0,0,0]
// 4. 全零初始化(两种等效写法)
int arr4[5] = {0};
int arr5[5] = {}; // C++11起支持
// 5. 自动推断数组大小
int arr6[] = {1,2,3,4,5}; // 编译器推断大小为5
// 6. C++11统一初始化语法
int arr7[5]{10,20,30,40,50};
注意:在C++中,数组大小必须是编译期常量表达式。C99引入的变长数组(VLA)特性在标准C++中不被支持。
数组越界是C++中最危险的错误之一,因为它不会立即导致程序崩溃,而是会悄无声息地破坏其他内存区域。理解其底层机制非常重要。
cpp复制int arr[3] = {10,20,30};
int important = 999;
// 内存布局可能如下:
// [arr[0]][arr[1]][arr[2]][important]
// 10 20 30 999
arr[3] = 0; // 实际上修改了important的值!
为什么C++不检查数组边界?主要有三个原因:
防御性编程建议:
std::array(C++11)替代原生数组cpp复制constexpr int SIZE = 5;
int arr[SIZE];
// 更安全的遍历方式
for(int i=0; i<SIZE; i++) {
// ...
}
// 或者使用范围for
for(int num : arr) {
// ...
}
遍历数组有多种方式,各有优缺点:
cpp复制int arr[] = {10,20,30,40,50};
int size = sizeof(arr)/sizeof(arr[0]);
// 1. 传统for循环(最灵活)
for(int i=0; i<size; i++) {
cout << arr[i] << " ";
}
// 2. 范围for循环(C++11,最简洁)
for(int num : arr) {
cout << num << " ";
}
// 3. 指针遍历(最接近底层)
for(int *p=arr; p!=arr+size; p++) {
cout << *p << " ";
}
性能考虑:在现代编译器优化下,这三种方式性能几乎相同。范围for循环在可读性上优势明显,但在需要索引时仍需使用传统for循环。
线性查找是最基础的查找方式:
cpp复制int linearSearch(const int arr[], int size, int target) {
for(int i=0; i<size; i++) {
if(arr[i] == target) {
return i;
}
}
return -1; // 未找到
}
对于已排序数组,二分查找效率更高(O(log n)):
cpp复制int binarySearch(const int arr[], int size, int target) {
int left = 0, right = size-1;
while(left <= right) {
int mid = left + (right-left)/2;
if(arr[mid] == target) {
return mid;
} else if(arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
冒泡排序是最简单的排序算法,但效率较低(O(n²)):
cpp复制void bubbleSort(int arr[], int size) {
for(int i=0; i<size-1; i++) {
for(int j=0; j<size-1-i; j++) {
if(arr[j] > arr[j+1]) {
swap(arr[j], arr[j+1]);
}
}
}
}
在实际开发中,应该使用标准库的sort函数,它基于快速排序实现,平均时间复杂度为O(n log n):
cpp复制#include <algorithm>
sort(arr, arr+size); // 升序排序
数组作为函数参数传递时,实际上传递的是指向数组首元素的指针。这意味着:
cpp复制void printArray(const int arr[], int size) {
for(int i=0; i<size; i++) {
cout << arr[i] << " ";
}
}
void modifyArray(int arr[], int size) {
arr[0] = 100; // 会修改原始数组
}
int main() {
int arr[] = {1,2,3,4,5};
printArray(arr, 5);
modifyArray(arr, 5);
// arr现在为[100,2,3,4,5]
}
最佳实践:如果函数不应该修改数组,务必加上const限定符。同时,总是传递数组大小作为额外参数。
二维数组实际上是"数组的数组"。在内存中,它仍然是一块连续的区域,按行优先顺序存储。
cpp复制int matrix[2][3] = {
{1,2,3},
{4,5,6}
};
内存布局:
code复制[1][2][3][4][5][6]
理解这一点对性能优化很重要,因为按行访问比按列访问有更好的缓存局部性。
原生二维数组的大小必须在编译时确定。如果需要运行时确定大小,有几种解决方案:
cpp复制vector<vector<int>> matrix(rows, vector<int>(cols));
cpp复制int *matrix = new int[rows*cols];
// 访问matrix[i][j]等价于matrix[i*cols + j]
delete[] matrix;
cpp复制int **matrix = new int*[rows];
for(int i=0; i<rows; i++) {
matrix[i] = new int[cols];
}
// 释放内存
for(int i=0; i<rows; i++) {
delete[] matrix[i];
}
delete[] matrix;
矩阵转置是常见的二维数组操作:
cpp复制const int ROWS = 3, COLS = 4;
int mat[ROWS][COLS] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
int transposed[COLS][ROWS];
for(int i=0; i<ROWS; i++) {
for(int j=0; j<COLS; j++) {
transposed[j][i] = mat[i][j];
}
}
矩阵乘法是更复杂的操作,时间复杂度为O(n³):
cpp复制void matrixMultiply(const int a[][N], const int b[][M], int result[][M], int rows) {
for(int i=0; i<rows; i++) {
for(int j=0; j<M; j++) {
result[i][j] = 0;
for(int k=0; k<N; k++) {
result[i][j] += a[i][k] * b[k][j];
}
}
}
}
C风格字符串是以空字符('\0')结尾的字符数组。虽然简单,但极易出错:
cpp复制char str1[10] = "hello"; // 正确,自动添加'\0'
char str2[] = {'h','e','l','l','o'}; // 错误,没有'\0'结尾
常见的安全隐患:
strcpy不检查目标缓冲区大小安全函数替代方案:
strncpy代替strcpysnprintf进行格式化输出strncat代替strcatcpp复制char dest[10];
const char *src = "longerstring";
// 不安全
strcpy(dest, src); // 缓冲区溢出!
// 安全方式
strncpy(dest, src, sizeof(dest)-1);
dest[sizeof(dest)-1] = '\0'; // 确保终止
std::string自动管理内存,极大简化了字符串操作:
cpp复制string s1 = "Hello";
string s2("World");
string s3(5, 'A'); // "AAAAA"
// 字符串连接
string s4 = s1 + " " + s2; // "Hello World"
// 查找
size_t pos = s4.find("World");
if(pos != string::npos) {
cout << "Found at position " << pos << endl;
}
// 子字符串
string sub = s4.substr(6, 5); // "World"
性能提示:
reserve避免多次分配C++11引入了更安全的转换函数:
cpp复制// 字符串转数值
string numStr = "123.45";
int i = stoi(numStr); // 123
double d = stod(numStr); // 123.45
// 数值转字符串
string s1 = to_string(123); // "123"
string s2 = to_string(3.14); // "3.140000"
对于格式化输出,可以使用stringstream:
cpp复制#include <sstream>
ostringstream oss;
oss << fixed << setprecision(2) << 3.14159;
string s = oss.str(); // "3.14"
C++11引入了<regex>库,提供强大的模式匹配能力:
cpp复制#include <regex>
string text = "Email: test@example.com, Phone: 123-456-7890";
regex email_pattern(R"(\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b)");
smatch matches;
if(regex_search(text, matches, email_pattern)) {
cout << "Found email: " << matches[0] << endl;
}
正则表达式特别适合:
结合数组和字符串操作,实现简单的文本分析:
cpp复制#include <iostream>
#include <string>
#include <map>
#include <algorithm>
#include <cctype>
using namespace std;
string normalizeWord(const string& word) {
string result;
for(char c : word) {
if(isalpha(c)) {
result += tolower(c);
}
}
return result;
}
void countWordFrequencies(const string& text) {
map<string, int> freqMap;
size_t start = 0, end = 0;
while((end = text.find_first_of(" ,.!?\n", start)) != string::npos) {
if(end != start) {
string word = text.substr(start, end-start);
word = normalizeWord(word);
if(!word.empty()) {
freqMap[word]++;
}
}
start = text.find_first_not_of(" ,.!?\n", end);
}
// 输出结果
for(const auto& pair : freqMap) {
cout << pair.first << ": " << pair.second << endl;
}
}
cpp复制// 好:按行访问
for(int i=0; i<rows; i++) {
for(int j=0; j<cols; j++) {
matrix[i][j] = ...;
}
}
// 差:按列访问
for(int j=0; j<cols; j++) {
for(int i=0; i<rows; i++) {
matrix[i][j] = ...;
}
}
cpp复制// 差:多次分配
string result;
for(int i=0; i<100; i++) {
result += "a";
}
// 好:预分配
string result;
result.reserve(100);
for(int i=0; i<100; i++) {
result += "a";
}
cpp复制string processString(string&& str) {
// 处理字符串...
return move(str); // 明确移动
}
string largeStr = "...";
auto result = processString(move(largeStr));
std::array替代原生数组:cpp复制#include <array>
array<int, 5> arr = {1,2,3,4,5};
// 提供size()、迭代器等成员函数
std::vector作为动态数组:cpp复制vector<int> vec = {1,2,3};
vec.push_back(4); // 动态增长
std::string_view(C++17)避免复制:cpp复制string largeStr = "...";
string_view view(largeStr); // 不复制数据
越界访问:最常见的错误,可能导致:
数组大小错误:
cpp复制int arr[5];
cout << sizeof(arr) / sizeof(int); // 正确:5
int* ptr = arr;
cout << sizeof(ptr) / sizeof(int); // 错误:指针大小而非数组大小
cpp复制void foo(int arr[]) { // 实际是指针
cout << sizeof(arr); // 指针大小,不是数组大小
}
cpp复制char str[5] = {'h','e','l','l','o'}; // 缺少'\0'
cout << str; // 可能输出乱码
cpp复制char buf[10];
strcpy(buf, "This is too long!"); // 溢出!
cpp复制char* str = "literal"; // C++11起已禁止
str[0] = 'L'; // 未定义行为
使用调试器:
防御性编程:
cpp复制#define ASSERT_IN_RANGE(index, size) \
if(index >= size) { \
cerr << "Index " << index << " out of bounds (size=" << size << ")\n"; \
abort(); \
}
template<typename T, size_t N>
T& safe_at(T (&arr)[N], size_t index) {
ASSERT_IN_RANGE(index, N);
return arr[index];
}
理解string类的实现原理有助于更好地使用它。一个简化版的字符串类可能包含:
cpp复制class SimpleString {
char* m_data;
size_t m_length;
public:
SimpleString(const char* str) {
m_length = strlen(str);
m_data = new char[m_length + 1];
strcpy(m_data, str);
}
~SimpleString() {
delete[] m_data;
}
// 拷贝构造函数、赋值运算符等...
};
现代C++提供了对Unicode的支持:
std::wstring:宽字符字符串char16_t/char32_t:UTF-16/UTF-32字符类型<codecvt>:字符编码转换(C++11起,但C++17已弃用)推荐使用第三方库如ICU处理复杂的国际化需求。
对于性能关键的场景,可以自定义分配器:
cpp复制template<typename T>
class CustomAllocator {
// 实现allocate、deallocate等方法
};
vector<int, CustomAllocator<int>> customVector;
这在游戏开发和高频交易等场景中很有用。
数组使用准则:
std::array或std::vector字符串处理准则:
std::stringstrncpy等)性能优化准则:
安全编程准则:
现代C++特性:
std::string_view避免复制std::span(C++20)安全访问数组cpp复制// 现代C++风格示例
vector<int> vec = {5,3,1,4,2};
ranges::sort(vec); // C++20范围算法
string_view sv = "Hello World"; // 不复制数据
auto words = sv | views::split(' '); // C++20范围适配器