文章目录
- 一、数组
- 1.一维数组
- 2.多维数组
- 二、vector
- 三、string
一、数组
1.一维数组
在C++中,数组用于存储具有相同类型和特定大小的元素集合。数组在内存中是连续存储的,并且支持通过索引快速访问元素。
数组的声明:
数组的声明指定了元素的类型和数组的长度。
// 声明一个整型数组,具有10个元素
int arr[10];
// 声明并初始化整型数组
int arr[] = {1, 2, 3, 4, 5};
注意:不要使用
extern
指针来声明数组,应该extern int array[];
即Unknown Bounded Array 声明错误示例:
`file1.cpp`: int array[3] = {1, 2, 3}; `file2.cpp`://正确写法 extern int array[]; `file2.cpp`://错误写法 extern int* array;
数组初始化:
数组的初始化可以是隐式的,也可以是显式的。
-
缺省初始化
在C++中,如果数组是非静态的局部数组,其元素将包含未定义的值,因为它们没有默认初始化。全局数组和静态数组将会被默认初始化为零。
void function() { int localArray[10]; // 未定义的值 static int staticArray[10]; // 初始化为0 }
-
聚合初始化( aggregate initialization )
//使用大括号{} int arr[] = {1, 2, 3, 4, 5}; //根据初始化列表推导出int[5]
注意事项:
-
不能使用
auto
来声明数组类型auto b = {1,2,3}; //b推导出的类型为std::initializer_list<int> int c[3] = {1,2,3}; auto d = c; //d推导出的类型为int*,类型退化了
-
C++中,数组(内置数组)不能直接复制。因为它们没有像类或结构体那样的拷贝构造函数或赋值运算符
-
赋值操作:对于数组,你不能使用简单的赋值操作(
=
)来复制数组。例如,以下代码是错误的:int arr1[10] = {1, 2, 3}; int arr2[10]; arr2 = arr1; // 错误:数组之间不能使用赋值操作符
-
拷贝构造函数:传统数组没有拷贝构造函数,这意味着你不能通过构造函数来创建一个数组的深拷贝
尽管传统数组不能直接通过赋值或拷贝构造函数来复制,你仍然可以通过其他方法来复制数组的内容。
-
手动复制:使用循环手动逐个复制数组的每个元素
-
std::copy:使用C++标准库中的
<algorithm>
头文件提供的std::copy
函数 -
拷贝构造函数(对于数组类型的对象):如果你有一个包含数组的类或结构体,你可以定义一个拷贝构造函数来复制数组。
struct MyStruct { int arr[10]; MyStruct(const MyStruct& other) { std::copy(other.arr, other.arr + 10, arr); } };
-
C++11标准库容器:如
std::array
或std::vector
提供了拷贝构造函数和赋值运算符来复制其内容。std::array<int, 10> arr1 = {{1, 2, 3}}; std::array<int, 10> arr2 = arr1; // 使用std::array的拷贝构造函数 std::vector<int> vec1 = {1, 2, 3}; std::vector<int> vec2 = vec1; // 使用std::vector的拷贝构造函数
-
-
元素个数必须是常量表达式(编译期可计算的值)
-
字符串数组的特殊性
char str[] = "Hello"; //char[6] char str[] = {'H', 'e', 'l', 'l', 'o'}; //char[5] char str[] = {'H', 'e', 'l', 'l', 'o', '\0'}; //char[6]
使用字符串字面量初始化字符数组:
char str[] = "Hello"; // char[6]
在这行代码中,
"Hello"
是一个字符串字面量,它包含了字符串"Hello"
以及一个隐式的空字符\0
,用作字符串的终止符。因此,当你使用这个字符串字面量来初始化字符数组str
时,数组的大小是6个字符,包括5个可见字符和一个空字符。使用字符字面量初始化字符数组:
char str[] = {'H', 'e', 'l', 'l', 'o'}; // char[5]
在这个例子中,使用花括号
{}
包围的字符字面量列表来初始化字符数组str
。这种方式不会自动添加空字符\0
,因此初始化后的数组str
将包含5个字符,即'H'
,'e'
,'l'
,'l'
, 和'o'
。数组的实际大小是5个字符。注意事项:
- 当使用字符串字面量初始化字符数组时,编译器会自动在末尾添加一个空字符
\0
,所以数组需要有足够的空间来存储这个额外的字符。 - 当使用字符字面量列表初始化字符数组时,需要确保数组有足够的空间来存储所有字符,且不会自动添加空字符
\0
。如果你需要一个以空字符终止的字符串,必须手动添加\0
。 - 字符串字面量和字符字面量列表在初始化字符数组时的行为不同,这可能会影响程序中字符串的处理和字符串函数的使用。
- 当使用字符串字面量初始化字符数组时,编译器会自动在末尾添加一个空字符
数组(数组到指针的隐式转换):
数组名在大多数表达式中会被解释为指向数组首元素的指针。使用数组对象时,通常情况下会产生数组到指针的隐式转换,隐式转换会丢失一部分类型信息,可以通过声明引用来避免隐式转换。
int arr[10];
int* ptr = arr; // ptr是指向数组首元素的指针,数组到指针的隐式转换
auto& b = arr; //声明引用来避免隐式转换
由于数组名可以作为指针使用,所以可以进行指针运算。
#include <iostream>
int main()
{
int arr[10];
for (int* ptr = arr; ptr < arr + 10; ++ptr) {
// 操作数组元素
*ptr = *ptr + 1; //1
}
}
指针数组与数组指针:
-
指针数组
指针数组是一个包含多个指针的数组,每个指针可以指向不同的对象。
语法:指针数组在声明时,指针符号
*
放在数组名的前面。int* arr[3];
-
数组指针
数组指针是指向数组的指针。这种指针指向整个数组,而不是数组中的单个元素。
语法:数组指针在声明时,指针符号
*
放在数组类型后面,使用括号括起来// 创建一个整型数组 int arr[] = {10, 20, 30}; // 创建一个指向数组的指针 int (*ptrToArr)[3] = &arr;
ptrToArr
是一个指向数组的指针,它指向一个包含3个int
的数组。ptrToArr
不是一个指向int
的指针,而是指向整个数组。
注意:
- 当传递数组作为函数参数时,实际上传递的是指向数组首元素的指针,而不是整个数组。
- 数组的生命周期和存储空间必须在数组指针使用期间保持有效。
- 指针数组和数组指针在内存布局和访问方式上有所不同,需要根据具体需求选择合适的类型。
声明数组的引用:(C++中没有引用数组的概念)
声明数组的引用意味着创建一个引用,它指向整个数组。数组引用在函数参数传递、大型数据结构的传递等方面非常有用,因为它们允许函数直接操作传入的数组,而不是数组的副本。
int (&arr)[5] = data; // 假设 'data' 是一个已定义的数组
数组作为函数参数时,由于数组不能直接拷贝,通常会退化为指针。为了避免这种情况,可以使用数组引用作为函数参数。
void printArray(int (&arr)[5]) {
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
int data[5] = {1, 2, 3, 4, 5};
printArray(data);
printArray
函数接受一个数组的引用作为参数。这意味着函数可以直接访问和修改数组 data
中的元素。如果想限制数组中的元素不能修改,则void printArray(const int (&arr)[5])
。
注意:
- 数组引用必须在声明时被初始化。
- 数组引用的大小(数组的长度)必须是已知的,因此它们不能指向未指定大小的数组。
- 数组引用通常用于函数参数,以便安全地传递数组并避免数组退化为指针
数组的元素访问:
-
数组对象是一个左值(如果作为右值使用,类型会发生隐式转换:
int[3] --> int*
)C++中左值与右值:
在C++中,表达式可以根据它们是否可以取得内存地址被分为两类:l-value(左值)和r-value(右值)。
-
l-value:l-value是指具有内存地址的表达式,这个内存地址可以被读取和写入。l-value通常用在赋值表达式的左边
l-value的例子:
- 变量:
int a;
- 数组元素:
int arr[10]; arr[0];
- 函数参数:当函数参数是一个引用类型时,它是一个l-value。
- 位域:当位域是一个对象成员时。
- 变量:
-
r-value:r-value是指那些没有内存地址或者有临时内存地址的表达式,它们不能被赋值,因为它们的存在时间很短暂。r-value通常用在赋值表达式的右边,比如字面量、临时对象、表达式的结果等。
以下是一些r-value的例子:
- 字面量:
10
- 表达式:
a + b
- 函数返回值:除非函数返回一个引用,否则返回的是一个r-value。
int b = 20; // b是一个l-value int c = a + b; // a + b是一个r-value
- 字面量:
-
-
数组访问
使用时数组名通常会转换成相应的指针类型(指向数组首元素的指针),本质上:
arr[index] = *(arr + index)
,实际上是利用指针运算来访问数组元素int arr[5] = {1, 2, 3, 4, 5}; int firstElement = arr[0]; // 获取第一个元素,值为1 int thirdElement = arr[2]; // 获取第三个元素,值为3
-
范围检查
C++不提供自动的数组范围检查,所以访问数组元素时应该确保索引不会超出数组的界限。
int arr[5]; for (int i = 0; i < 5; ++i) { // 安全的访问 arr[i] }
-
越界访问
如果尝试访问超出数组实际大小的索引,将导致未定义行为,这可能引发程序错误,如数组越界错误、野指针引用等
获得指向数组开头与结尾的指针 :
int a[3] = {1,2,3};
获得指向数组开头的指针:a &(a[0]) std::begin(a) std::cbegin(a)
获得指向数组结尾的下一个元素的指针:a+3 &(a[3]) std::end(a) std::cend(a)
std::begin(a)与std::end(a)获得的类型为int*
std::cbegin(a)与std::cend(a)获得的类型为const int*
指针算术运算:
-
增加、减少
指向数组下一位置元素的指针
-
比较
如果参数比较运算的指针是指向一个数组中的两个位置 ,是可以进行比较运算的;如果指向的是不同数组,不建议这样做,可能产生未定义的行为
-
求距离
#include <iostream> int main() { int a[3] = {1, 2, 3}; auto ptr = a; auto ptr2 = a +3; std::cout << ptr2 - ptr << std::endl; //3 std::cout << ptr - ptr2 << std::endl; //-3 }
-
解引用
-
指针索引
指针索引使用方括号
[]
来指定偏移量,从而访问指针指向的内存位置之后的某个位置。
数组的其他操作:
-
求元素个数
以下三种方法都是对数组进行操作(获取数组的定义),而不是指针
sizeof
方法: C语言方式,有危险,不推荐std::size
方法(C++17及以后):推荐的方式std::end() - std::begin()
方法:在运行期进行指针运算,但其实数组信息在编译器就可以看到,增加了运行期负担,不推荐
#include <iostream> int main() { int a[3]; std::cout << sizeof(int) << std::endl; //4 std::cout << sizeof(a) << std::endl; //12 //sizeof()方法求元素个数 std::cout << sizeof(a)/sizeof(int) << std::endl; //3 //std::size() C++17 // std::cout << std::size(a) << std::endl; //end-begin std::cout << std::end(a) - std::begin(a) << std::endl; //3 }
-
元素遍历
- 基于元素个数
- 基于©begin/©end
- 基于
range-based
for循环:语法糖
#include <iostream> int main() { int a[3] = {2, 3, 5}; //基于元素个数--C++17 size_t index = 0; while(index < std::size(a)) { std::cout << a[index] << std::endl; index = index + 1; } //基于(c)begin/(c)end auto ptr = std::cbegin(a); while(ptr != std::cend(a)) { std::cout << *ptr << std::endl; ptr = ptr + 1; } //基于`range-based ` for循环 for (int x : a) { std::cout << x << std::endl; } }
数组–C字符串:
- C 字符串本质上是字符数组
- C 语言提供了额外的函数来支持 C 字符串相关的操作 :
strlen
,strcmp
…,这些操作都需要’\0’,知道C字符串到哪里结束strlen()
:统计长度直到找到’\0’
2.多维数组
多维数组本质上是数组的数组。其内存布局仍是连续存储。
int x[3][4]; //x是一个数组,包含3个元素,x的每个元素是一个int[4]数组。
x[0]; //类型是int(&)[4]
声明多维数组:
多维数组的声明涉及指定每个维度的大小。在C++中,通常使用方括号来声明多维数组。
// 声明一个3x3的二维整型数组
int multiArray[3][3];
// 声明一个5x10的二维整型数组
int anotherArray[5][10];
初始化多维数组:
初始化时,每个维度的元素应该用花括号 {}
包围,维度之间的元素应该用逗号 ,
分隔。
-
缺省初始化
-
聚合初始化
一层大括号与多层大括号
//其余元素默认初始化为0
int x2[3][4] = {1, 2, 3, 4, 5, 6};
//其余元素默认初始化为0
int x3[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
//其余元素默认初始化为0
int x3[3][4] = {{1, 2, 3, 4}, {5, 6, 7}};
// 初始化一个3x3的二维数组
int multiArray[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
// 使用缺省初始化
int defaultArray[3][3] = {};
多维数组只能省略最高位的维度如:
int x[][3]
,但是不建议这么干
多维数组索引与遍历:
-
使用多个中括号来索引,访问多维数组元素
int value = multiArray[1][2]; // 获取第二行第三列的元素,值为6
-
使用多重循环来遍历多维数组
#include <iostream> int main() { int multiArray[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; //基于`range-based` for循环 for (auto& p : multiArray) //auto& 非常重要 { for (auto q : p) { std::cout << q << std::endl; } } }
下面分析
for (auto& p : multiArray)
与for (auto p : multiArray)
的区别
指针与多维数组:
在C++中,多维数组的数组名可以被看作一个指向数组首元素的指针。可以使用指针运算来操作多维数组。
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
int (*ptrToRow)[3] = arr; // ptrToRow是一个指向具有3个整数的数组的指针
// 访问第一行的第二个元素
int value = (*ptrToRow)[1]; // 等于 arr[0][1],值为2
-
多维数组可以隐式转换为指针,但只有最高维会进行转换,其它维度的信息会被保留。
-
使用类型别名来简化多维数组指针的声明
#include <iostream> using A = int[4]; int main() { int x[3][4]; A* ptr = x; }
-
使用指针来遍历多维数组
#include <iostream> int main() { int x2[3][4] = {}; auto ptr = std::begin(x2); while(ptr != std::end(x2)) { auto ptr2 = std::begin(*ptr); while(ptr2 != std::end(*ptr)) { ptr2 = ptr2 + 1; } ptr = ptr + 1; } }
二、vector
C++标准库中的 std::vector
是一个序列容器,是 C++ 标准库中定义的一个类模板。与内建数组相比,std::vector
提供了更多的功能,例如自动调整大小、范围检查和一些有用的成员函数(更侧重易用性)。std::vector
可复制、可在运行期动态改变元素个数(内建数组不可以)。
构造与初始化:
-
缺省初始化
-
聚合初始化
-
其他初始化方式–构造函数
std::vector<T,Allocator>::vector,根据参数调用不同的构造函数
std::vector<int> vec; // 默认构造
std::vector<int> vec(10, 1); // 10个整数初始化为1
std::vector<int> vec = {1, 2, 3}; // 列表初始化
其他方法:
-
获取元素个数、判断元素是否为空、容量
#include <iostream> #include <vector> int main() { std::vector<int> x(3, 1); //获取元素个数--3 std::cout << x.size() <<std::endl; //判断元素是否为空,空返回1,不空返回0 std::cout << x.empty() <<std::endl; //容量--3 std::cout << x.capacity() <<std::endl; }
-
插入元素
vec.push_back(4); // 在末尾添加一个元素 vec.push_back({5, 6, 7}); // 从初始列表添加多个元素
-
删除元素
vec.pop_back(); // 删除最后一个元素 vec.erase(vec.begin() + 1); // 删除第二个元素
-
vector
比较逐个比较两个vector的元素,如果元素相同,则比较下一个元素;如果第一个元素不同,则由第一个元素的大小决定;
-
清空
vec.clear(); // 删除所有元素,保留容量
-
修改容器大小
vec.resize(5); // 改变vec的大小为5,如果缩小则可能删除元素 vec.resize(7, 1); // 如果需要,用1填充额外的空间以改变大小为7
-
元素操作
vec.front(); // 返回第一个元素 vec.back(); // 返回最后一个元素
vector 中元素的索引与遍历:
-
[]
与at
实现vector 中元素的索引std::vector<int> vec(10, 1); // 10个整数初始化为1 int firstElement = vec[0]; // 获取第一个元素 vec[1] = 20; // 设置第二个元素的值 //at方法 vec.at(2) = 30;
-
©begin / ©end 函数 V.S. ©begin / ©end 方法
#include <iostream> #include <vector> int main() { std::vector<int> x1 = {1, 2, 3}; //begin、end函数 auto p = std::begin(x1); while(p != std::end(x1)) { std::cout << *p << std::endl; p = p + 1; } //begin、end方法 auto q = x1.begin(); //返回指向第一个元素的迭代器,迭代器解引用也可以获取元素的值 while(q != x1.end()) { std::cout << *q << std::endl; q = q + 1; } //for for (auto x : x1) { std::cout << x << std::endl; } }
vector
中©begin / ©end 方法返回的是随机访问迭代器迭代器:
-
模拟指针行为
-
包含多种类别,每种类别支持的操作不同
-
vector 对应随机访问迭代器
-
解引用与下标访问
-
移动
-
两个迭代器相减求距离
-
两个迭代器比较
这两个迭代器必须指向相同的vector
-
-
vector相关的其他内容:
-
添加元素可能使迭代器失效
-
多维 vector
如:
std::vector<std::vector<int>>
-
从
.
到->
操作符#include <iostream> #include <vector> int main() { std::vector<int> x = {1, 2, 3}; std::vector<int>* ptr = &x; std::cout << x.size() << std::endl; //3 std::cout << ptr->size() << std::endl; //3 }
-
vector 内部定义的类型
-
size_type
-
iterator
/const_iterator
-
注意事项:
std::vector
会根据需要自动调整大小,但可能会引起性能开销,因为它可能需要重新分配内存和复制元素。- 访问
std::vector
中的元素时,要注意范围检查,避免越界访问。- 与原始数组相比,
std::vector
更安全且易于使用,但可能会占用更多的内存,并且访问速度可能稍慢。
三、string
std::string
是标准库中的一个类模板实例化,它提供了一个可变长度的字符序列,用于内建字符串的代替品。与内建字符串相比,更侧重于易用性。string
可复制、可在运行期动态改变字符个数。
Type | Definition |
---|---|
std::string | std::basic_string |
参考:std::basic_string
构造与初始化:
#include <string>
std::string str; // 默认构造,创建一个空字符串
std::string str("Hello, World!"); // 直接初始化
std::string str(10, 'A'); // 初始化为10个字符'A'
string其他方法:
-
访问元素
char ch = str[0]; // 获取第一个字符,与C风格字符串不同,这是安全的
-
添加和修改字符串
str += "追加的字符串"; // 追加字符串 str.append("追加的字符串"); // 另一种追加方式 str.insert(1, "插入的字符串"); // 在指定位置插入字符串 str.replace(1, 2, "新字符串"); // 替换部分字符串 str.erase(1, 4); // 删除从索引1开始的4个字符 str.back() = '!'; // 修改最后一个字符
-
尺寸相关方法(size/length)
#include <iostream> #include <vector> int main() { std::string str= "Hello World!"; size_t size = str.size(); size_t len = str.length(); // 获取字符串长度 std::cout << size << std::endl; std::cout << len << std::endl; str.resize(10); // 改变字符串长度为10 std::cout << str.size() << std::endl; std::cout << str.length() << std::endl; }
-
查找和比较
size_t pos = str.find("sub"); // 查找子串"sub"的位置 bool isEqual = str.compare("anotherString"); // 比较字符串
-
子字符串
std::string sub = str.substr(1, 4); // 获取从索引1开始的4个字符的子字符串
-
清空
str.clear(); // 删除所有字符,保留容量
-
交换
str.swap(anotherStr); // 交换两个字符串的内容
-
转换为C字符串,返回一个指向’\0’结束字符数组的指针
const char* cstr = str.c_str(); // 获取C风格的字符数组
注意事项:
std::string
是一个类模板的实例化,所以它不是像原始数组那样的低级类型。- 字符串字面量(如
"Hello"
)是const char
类型的数组,它们在C++中被定义为const
,因此不能被修改。- 当需要C风格的字符串时,可以使用
c_str()
成员函数来获取,但要注意,这个操作可能涉及内存分配,因此不应该频繁调用。std::string
提供了范围检查的访问方式,避免了数组越界的问题