C++深入字符串、向量和数组
字符串(String)
C++ 中的字符串表示
在 C++ 中,字符串的表示主要有两种方式:C 风格字符串和 C++ 标准库中的 std::string
。
C 风格字符串
C 风格字符串本质上是一个以空字符 '\0'
结尾的字符数组。例如:
char greeting[] = "Hello, World!";
这里 greeting
是一个字符数组,它包含了字符串中的所有字符以及结尾的空字符。可以通过以下方式访问数组中的字符:
#include <iostream>
int main() {
char greeting[] = "Hello, World!";
for (int i = 0; greeting[i] != '\0'; ++i) {
std::cout << greeting[i];
}
std::cout << std::endl;
return 0;
}
这种方式虽然简单直接,但在使用过程中有一些局限性。例如,手动管理内存很容易导致缓冲区溢出错误。假设我们想要复制一个 C 风格字符串到另一个数组中:
#include <iostream>
#include <cstring>
int main() {
char source[] = "Long string that might cause problems";
char destination[10]; // 这个数组大小太小
std::strcpy(destination, source); // 可能导致缓冲区溢出
std::cout << destination << std::endl;
return 0;
}
在上述代码中,destination
数组的大小不足以容纳 source
字符串,调用 std::strcpy
会导致未定义行为。
std::string
std::string
是 C++ 标准库提供的字符串类,它极大地简化了字符串的操作。std::string
会自动管理内存,避免了很多常见的错误。
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, World!";
std::cout << str << std::endl;
return 0;
}
std::string
提供了丰富的成员函数,用于字符串的操作。比如字符串拼接:
#include <iostream>
#include <string>
int main() {
std::string str1 = "Hello";
std::string str2 = " World";
std::string result = str1 + str2;
std::cout << result << std::endl;
str1.append(str2);
std::cout << str1 << std::endl;
return 0;
}
上述代码展示了两种字符串拼接的方式,一种是使用 +
运算符,另一种是使用 append
成员函数。
字符串的内存管理
C 风格字符串的内存管理
C 风格字符串如果是在栈上定义,其内存空间在函数结束时会自动释放。例如:
void someFunction() {
char str[] = "Local string";
// 函数结束时,str 的内存会自动释放
}
如果是在堆上分配的 C 风格字符串,就需要手动释放内存,否则会导致内存泄漏。例如:
#include <iostream>
#include <cstring>
int main() {
char *dynamicStr = new char[20];
std::strcpy(dynamicStr, "Dynamic string");
std::cout << dynamicStr << std::endl;
delete[] dynamicStr; // 手动释放内存
return 0;
}
忘记调用 delete[]
会导致 dynamicStr
所占用的内存无法被回收。
std::string
的内存管理
std::string
内部使用动态内存分配来存储字符串内容。当 std::string
对象被销毁时,其析构函数会自动释放所占用的内存。例如:
#include <iostream>
#include <string>
void someFunction() {
std::string localStr = "Local std::string";
// 函数结束时,localStr 的析构函数会自动释放内存
}
int main() {
someFunction();
return 0;
}
std::string
还会根据字符串的长度动态调整其内部缓冲区的大小。当字符串长度增加时,如果当前缓冲区不足以容纳新的内容,std::string
会重新分配一块更大的内存,并将原内容复制过去。
字符串操作函数
C 风格字符串函数
C 标准库提供了一系列用于操作 C 风格字符串的函数,定义在 <cstring>
头文件中。
strcpy
:用于将一个字符串复制到另一个字符串。其原型为char* strcpy(char* destination, const char* source);
。如前文所述,使用时需要确保目标缓冲区足够大。strcat
:用于将一个字符串追加到另一个字符串的末尾。原型为char* strcat(char* destination, const char* source);
。同样要注意目标缓冲区的大小。strcmp
:用于比较两个字符串。原型为int strcmp(const char* str1, const char* str2);
。如果str1
小于str2
,返回负整数;如果相等,返回 0;如果str1
大于str2
,返回正整数。
std::string
成员函数
std::string
类提供了大量功能丰富的成员函数。
length
:返回字符串的长度(不包括结尾的空字符)。例如:
#include <iostream>
#include <string>
int main() {
std::string str = "Hello";
std::cout << "Length of the string: " << str.length() << std::endl;
return 0;
}
substr
:用于提取子字符串。原型为string substr(size_t pos = 0, size_t len = npos) const;
,pos
是起始位置,len
是子字符串的长度。例如:
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, World!";
std::string sub = str.substr(7, 5); // 从位置 7 开始,长度为 5
std::cout << sub << std::endl;
return 0;
}
find
:用于查找子字符串在字符串中的位置。原型为size_t find(const string& str, size_t pos = 0) const;
,pos
是开始查找的位置。例如:
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, World!";
size_t pos = str.find("World");
if (pos != std::string::npos) {
std::cout << "Found at position: " << pos << std::endl;
} else {
std::cout << "Not found" << std::endl;
}
return 0;
}
向量(Vector)
向量的基本概念
std::vector
是 C++ 标准库中的动态数组容器。与普通数组不同,std::vector
可以动态调整大小,在运行时根据需要分配和释放内存。
向量的创建和初始化
默认初始化
可以创建一个空的 std::vector
:
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers;
std::cout << "Size of the vector: " << numbers.size() << std::endl;
return 0;
}
这里创建了一个 std::vector<int>
类型的 numbers
向量,初始大小为 0。
初始化列表初始化
C++11 引入了初始化列表,可以方便地初始化 std::vector
:
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
上述代码通过初始化列表为 numbers
向量赋予了初始值。
拷贝初始化
可以通过拷贝另一个 std::vector
来创建新的向量:
#include <iostream>
#include <vector>
int main() {
std::vector<int> original = {1, 2, 3};
std::vector<int> copy = original;
for (int num : copy) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
这里 copy
向量是 original
向量的一个副本。
向量的内存管理
std::vector
内部使用连续的内存空间来存储元素。当向量的大小增加时,如果当前分配的内存不足以容纳新的元素,std::vector
会重新分配一块更大的内存,将原有的元素复制到新的内存位置,然后释放旧的内存。
向量有两个重要的属性:size
和 capacity
。size
表示当前向量中实际存储的元素个数,而 capacity
表示向量在不重新分配内存的情况下能够容纳的最大元素个数。例如:
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers;
std::cout << "Initial size: " << numbers.size() << ", Initial capacity: " << numbers.capacity() << std::endl;
for (int i = 0; i < 10; ++i) {
numbers.push_back(i);
std::cout << "Size: " << numbers.size() << ", Capacity: " << numbers.capacity() << std::endl;
}
return 0;
}
在上述代码中,可以观察到随着元素的不断添加,size
不断增加,而 capacity
会在特定的时机(通常是 size
达到 capacity
时)翻倍。
向量的操作函数
元素访问
可以通过下标运算符 []
或 at
成员函数来访问向量中的元素。[]
运算符不进行边界检查,如果访问越界会导致未定义行为,而 at
成员函数会在访问越界时抛出 std::out_of_range
异常。例如:
#include <iostream>
#include <vector>
#include <exception>
int main() {
std::vector<int> numbers = {1, 2, 3};
try {
std::cout << "Element at index 1 using []: " << numbers[1] << std::endl;
std::cout << "Element at index 1 using at(): " << numbers.at(1) << std::endl;
std::cout << "Element at index 3 using at(): " << numbers.at(3) << std::endl; // 这会抛出异常
} catch (const std::out_of_range& e) {
std::cerr << "Out of range error: " << e.what() << std::endl;
}
return 0;
}
添加和删除元素
push_back
:在向量的末尾添加一个元素。例如:
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers;
numbers.push_back(1);
numbers.push_back(2);
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
pop_back
:删除向量末尾的元素。例如:
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3};
numbers.pop_back();
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
erase
:删除指定位置或指定范围的元素。例如:
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
numbers.erase(numbers.begin() + 2); // 删除索引为 2 的元素
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
numbers.erase(numbers.begin(), numbers.begin() + 2); // 删除前两个元素
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
数组(Array)
数组的基本概念
C++ 中的数组是一种固定大小的连续内存区域,用于存储相同类型的多个元素。数组的大小在编译时就已经确定,不能在运行时动态改变。
数组的声明和初始化
静态数组声明
可以声明一个静态数组,例如:
int numbers[5];
这里声明了一个名为 numbers
的整数数组,大小为 5。数组的元素默认是未初始化的,如果需要初始化,可以使用以下方式:
int numbers[5] = {1, 2, 3, 4, 5};
也可以部分初始化,未初始化的元素会被初始化为 0(对于整数类型):
int numbers[5] = {1, 2};
此时 numbers[0]
为 1,numbers[1]
为 2,numbers[2]
、numbers[3]
和 numbers[4]
为 0。
动态数组声明
使用 new
关键字可以在堆上分配动态数组:
int *dynamicNumbers = new int[5];
这里 dynamicNumbers
是一个指向堆上分配的整数数组的指针。动态数组在使用完毕后需要手动释放内存,使用 delete[]
操作符:
delete[] dynamicNumbers;
数组的内存布局
数组在内存中是连续存储的,这使得对数组元素的访问非常高效。例如,对于一个 int
类型的数组:
#include <iostream>
int main() {
int numbers[3] = {1, 2, 3};
std::cout << "Address of numbers[0]: " << &numbers[0] << std::endl;
std::cout << "Address of numbers[1]: " << &numbers[1] << std::endl;
std::cout << "Address of numbers[2]: " << &numbers[2] << std::endl;
return 0;
}
可以看到数组元素的地址是连续的,相邻元素的地址相差 sizeof(int)
字节。
数组与指针的关系
在 C++ 中,数组名在大多数情况下会隐式转换为指向数组首元素的指针。例如:
#include <iostream>
int main() {
int numbers[3] = {1, 2, 3};
int *ptr = numbers;
std::cout << "Value at ptr: " << *ptr << std::endl;
std::cout << "Value at numbers[0]: " << numbers[0] << std::endl;
return 0;
}
这里 ptr
和 numbers
指向相同的内存位置。通过指针可以像访问数组元素一样访问内存中的数据,例如 *(ptr + 1)
等价于 numbers[1]
。
多维数组
二维数组
二维数组可以看作是数组的数组。例如,声明一个二维整数数组:
int matrix[3][4];
这里 matrix
是一个 3 行 4 列的二维数组。可以这样初始化:
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
访问二维数组的元素使用两个下标,例如 matrix[1][2]
表示第二行第三列的元素。
多维数组的内存布局
多维数组在内存中也是连续存储的。以二维数组为例,先存储第一行的所有元素,然后是第二行,以此类推。例如对于上述的 matrix
数组,内存布局如下:
1 2 3 4 5 6 7 8 9 10 11 12
数组的局限与 std::vector
的优势
数组的主要局限在于其大小固定,在运行时不能动态调整。这在很多情况下会带来不便,例如在处理未知大小的数据集合时。而 std::vector
可以动态调整大小,并且提供了丰富的成员函数来操作数据,如添加、删除元素等。同时,std::vector
还负责自动管理内存,避免了手动内存管理可能出现的错误,如内存泄漏和缓冲区溢出。
例如,假设要从用户输入中读取一些整数并存储起来,如果使用数组,需要预先知道最大可能的输入数量:
#include <iostream>
int main() {
int numbers[100]; // 假设最多输入 100 个整数
int count = 0;
int num;
while (std::cin >> num) {
if (count >= 100) {
std::cerr << "Array is full" << std::endl;
break;
}
numbers[count++] = num;
}
return 0;
}
而使用 std::vector
就可以避免这种限制:
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers;
int num;
while (std::cin >> num) {
numbers.push_back(num);
}
return 0;
}
这样 std::vector
可以根据实际输入动态调整大小,更加灵活和安全。
综上所述,在 C++ 编程中,根据具体的需求选择合适的数据结构非常重要。字符串、向量和数组各有其特点和适用场景,深入理解它们的本质和操作方式,有助于编写出高效、健壮的代码。