ch03.数组,vector与字符串目录
- 数组
- vector
- 字符串
一.数组
1.1.数组 — 引入
- 将一到多个相同类型的对象串连到一起,所组成的类型
-
int a → int b[10]
#include<iostream> #include<type_traits> int main(){ int a; //int int b[10];// b的类型为 int[10] std::cout<< std::is_same_v<decltype(b), int[10]><<std::endl;// 1 std::cin >> x; int b[x];//非法,x为运行期输入,但b[]是在编译期所确定的 //g++,clang++设置编译选项可以运行,但最好不要这样做, //可移植性很差,不是C++标准,只是某些编译器支持. //variable length array C++ }int a;//int int b[1];//int[1] //注意a,b为不同类型,尺寸一样,允许的操作不同数组长度需要是常量表达式:const expression
constexpr short x= 3;//constexpr:编译期常量表达式 int b[x];//合法 int b[3.14];//非法 int b[-6];//非法 int b[0];// 非法,不是C++ 标准,g++,clang支持,比如柔性数组: struct str{ ... int b[0]; //使用柔性数组构造可缩放的结构体 //Array of length Zero }; -
数组的初始化方式:
- 缺省初始化
int b[3];//函数内,随机初始化赋值;全局域,初始化赋值为0 - 聚合初始化( aggregate initialization )
int b[3] = {1,2,3}; int b[2] = {1,2};//b[2] = 0 int b[3] = {};//全0 int b[] = {1,2,3};//b 为int[3] int b[];//非法
- 缺省初始化
-
- 注意事项
- 不能使用 auto 来声明数组类型
#include<typeinfo> auto b = {1,3,4};//b 不是int[3] std::cout<< std::is_same_v<decltype(b), int[3]><<std::endl;// 0 // b为std::initalizer_list<int>初始化列表 std::cout<< typeid(b).name() << std::endl; 如果: - 数组不能复制
int b[] = {1,2,3}; int a[3] = b;//非法 ---- int b[] = {1,2,3}; int a[3]; a = b;//非法 ---- int b[] = {1,2,3}; auto a = b;// 不会报错但是a的类型不是int[3],a为一个指针int* (auto推导的退化问题) autu& a = b;//auto& 不会退化,此时a为int(&)[3] ==>int[3]的引用 - 元素个数必须是一个常量表达式(编译期可计算的值)
- 字符串数组的特殊性
char str[] = "hello";//str类型为char[6];char[5] = '\0' char str[] = {'h', 'e', 'l', 'l', '0'};//str类型为char[5]
- 不能使用 auto 来声明数组类型
- 数组的复杂声明
- 指针数组与数组的指针
/**指针数组**/ int* a[3];//指针数组,a是一个数组,数组里有3个int*, a的类型为int*[3] std::cout<< is_same_v<decltype(a), int*[3]> <<std::endl;//1 int x1, x2, x3; int* a[3] = {&x1, &x2, &x3}; /**数组指针**/ int (*a)[3];//a是一个指针,指针指向int[3], a的类型为int(*)[3] std::cout<< is_same_v<decltype(a), int(*)[3]> <<std::endl;//1 int b[3]; int (*a)[3] = &b;//对a解引用 *a 为 int[3] 既是b - 声明数组的引用
int b[3]; int (&a)[3] = b;//由内到外,a为引用,a为数组的引用; 此时a就是b的别名 std::cout<< is_same_v<decltype(a), int(&)[3]> <<std::endl;//1int x1,x2,x3; int& a[3] = {x1,x2,x3};//非法,不存在引用的数组
- 指针数组与数组的指针
- 数组中的元素访问
-
数组对象是一个左值
```c int a[3] = {1,2,3}; //l-value, 能够放在等号左边的值; C++ 进行了扩展: locator value (有地址名称?) const int x;//x为l-value,但不能放在等号左边
/* 参考链接:https://zh.cppreference.com/w/cpp/language/decltype a) if the value category of expression is xvalue(广义右值), then decltype yields T&&; b) if the value category of expression is lvalue(广义左值), then decltype yields T&; c) if the value category of expression is prvalue(一般值), then decltype yields T. 请注意,如果对象的名称被括号括起来,则它被视为一个普通的左值表达式, 因此 decltype(x) 和 decltype((x)) 通常是不同的类型。 */ const int x = 3; std::cout« std::is_same_v<decltype(x), const int> «std::ensl;//1 //0 .(x)解析为表达式, 表达式作为l-value,类型应该为const int & std::cout« std::is_same_v<decltype((x)), const int> «std::ensl; std::cout« std::is_same_v<decltype((x)), const int&> «std::ensl;//1
- 使用时通常会转换成相应的指针类型 ```c int a[3] = {1,2,3}; std::cout<< std::is_same_v<decltype((a)), const int(&)[3]> <<std::ensl;//1 //左值可以出现在赋值语句的左侧,右值不能 a = 1;//a为左值,但不能放在等号左边,它不能被修改 auto b = a; //a为左值,此时放在等号右边,可能会发生隐式的转换, //此时b为 int* ,指向a的第一个元素,既是 b = &(a[0]) std::cout<< std::is_same_v<decltype(b), int*> <<std::ensl;//1 std::cout<< *b <<std::endl;//1 std::cout<< b <<std::endl;// 地址A std::cout<< &(a[0]) <<std::endl;// 地址A ---- int a[3] = {1,2,3}; auto b = a; std::cout<< b[1] <<std::endl; // 2 通过[]访问元素不是数组的专属,指针也可以, //就看怎么解引:*(b+1),这里的1即为int型四个字节- x[y] <–> *((x) + (y))
x为首地址,y为解引地址偏移
int a[3] = {1,2,3}; std::cout<< *(a+1) <<std::endl;//2 std::cout<< *(1+a) <<std::endl;//2 std::cout<< 1[a] <<std::endl;//2 牛逼
-
1.2.数组 — 从数组到指针
- 数组到指针的隐式转换
- 使用数组对象时,通常情况下会产生数组到指针的隐式转换
int a[3] = {1,2,3}; auto b = a; // decay 数据类型退化, 此时的b为 int* /* 还是有一些情况不会产生数组到指针的退化: decltype(a);//int[3] sizeof(a);//3*int;如果有退化,应该是指针的大小:4字节 大部分情况都会产生数组到指针的隐式转换 */ - 隐式转换会丢失一部分类型信息
int a[3] = {1,2,3}; std::cout<< a[4] <<std::endl;//数组已经越界,但是此时a已经退化为指针,此句编译不会报错!!! auto b = a; // decay 数据类型退化, 此时的b为 int* ;丢失信息[3] -
可以通过声明引用来避免隐式转换
数组与指针的区别:

int a[3] = {1,2,3}; std::cout<< a[4] <<std::endl;//此句编译不会报错!!! auto b = a; // int* ;丢失信息[3] /**不希望产生这样的隐式转换**/ autu& b = a;// b的类型为 int(&)[3] 引用-->数组的引用 - 注意:不要使用 extern 指针来声明数组
- Unknown Bounded Array 声明
//source.cpp 定义数组 int array[4] = {1,2,3,4}; //main.cpp 声明数组 extern int array[4]; int main(){ std::cout<< array[1] << std::endl;//这样链接就是合法的 } /*这样会存在一个问题: 如果在source.cpp中对数组大小进行了更改, 其他地方的声明也需要做相应的更改,这是不利于维护的!*///改法一: //source.cpp 定义数组 int array[4] = {1,2,3,4}; //在source中打印array:0x55ec34614010 (这个是正确的) std::cout<< array <<std::endl; //main.cpp 声明数组 extern int* array;//将数组退化为指针使用,不关注长度信息 // 段错误 int main(){ // 1 --> 00000001 (十六进制:四字节,8个十六进制数字可表示4字节) // 2 --> 00000002 // 实际系统大小端保存 // 1 --> 01 00 00 00 // 2 --> 02 00 00 00 // array --> 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 // 将array中的数据解析成了内存地址,打印输出以逻辑输出(64位机): 2 00 00 00 01 //在main中打印array:0x200000001 (十六进制数:指针存放的地址) std::cout<< array <<std::endl; } //程序是错误的:运行期错误!!可以编译可以链接,一旦运行就是段错误! //编译时:两个文件单独编译都没问题; /* nm main.cpp.o nm main.cpp.o | c++filt -t */ //链接时:array没有类型信息,可以成功进行链接 //运行时:main中的array (int*类型)会将链接过来的array存储的数值解析为地址,出现错误```c //正确的改法: //source.cpp 定义数组 int array[4] = {1,2,3,4}; //在source中打印array:0x55ec34614010 (这个是正确的) std::cout« array «std::endl; //main.cpp 声明数组 extern int array[];//合法!!!同时易于维护,不关注数组大小
int main(){ std::cout« array[1] « std::endl;//这样链接就是合法的 //在main中打印array:0x55ec34614010 (这个是正确的) std::cout« array «std::endl; std::cout«std::begin(array)«std::endl; //err array为Unknown Bounded Array std::cout«std::end(array)«std::endl; //err array为Unknown Bounded Array }
int x[];//非法,编译错误,数组大小为编译期常量 int x[] = {1,2.3};//合法,聚合初始化 extern int array[];//只是声明,合法==>Unknown Bounded Array 声明 // 还有一个术语称之为incomplete type ==> 不完整类型 // 后续的类的相关知识也会涉及到不完整类型 ```
- Unknown Bounded Array 声明
- 使用数组对象时,通常情况下会产生数组到指针的隐式转换
- 获得指向数组开头与结尾的指针 : std::(c)begin, std::(c)end
int a[3] = {1,2,3}; //获得指向数组开头的指针:&(a[0]) , a, std::begin(a), std::cbegin(a) //获得指向数组结尾的指针:&(a[3]) , a + 3, std::end(a), std::cend(a)begin与cbegin的区别:c为const的意思 std::begin(a): // int* 读写 std::cbegin(a): std::end(a): // const int* 只读 std::cend(a): // 指向数组最后一个元素的下一个元素
int a[3] = {1,2,3}; auto b = a; std::cout<<std::begin(b)<<std::endl;//err b不在是数组 std::cout<<std::end(b)<<std::endl;//err b不在是数组 auto& b = a;// 不会有退化 - 指针算数:
- 增加、减少
- 比较
- 求距离
- 解引用
- 指针索引
int a[3] = {1,2,3}; auto ptr = a;// ptr 为 int* ptr = ptr + 1;//注意 1 的解析,指向想一个元素 auto ptr2 = a + 3; //可以对 ptr 与 ptr2 进行大小比较,一般是比较相等 //可以求距离## 1.3.数组 — 其它操作
- 求元素的个数
- sizeof 方法: 和类型相关联,不太推荐
- std::size 方法:编译期执行,推荐
- (c)end - (c)begin 方法: 运行期的方法,不推荐 ```c int a[3] = {1,2,3}; std::cout« sizeof(a) «std::endl;//12, a不会退化为指针 std::cout« sizeof(int) «std::endl;// 4 std::cout« sizeof(a)/sizeof(int) «std::endl;// 3 std::cout« std::size(a) «std::endl;// 3,推荐 std::cout« std::cend(a) - cbegin(a) «std::endl;// 12
//以下两种情况都无法使用上述三种方法 auto b = a; extern array[]; ```
- 元素遍历
- 基于元素个数
- 基于 (c)begin/(c)end ` for(auto ptr = std::cbegin(a); ptr != std::cend(a); ptr ++)`
- 基于 range-based for 循环
for(int x:a){ std::cout<< x << std::endl; }
1.4.数组 — C 字符串
- C 字符串本质上也是数组
- C 语言提供了额外的函数来支持 C 字符串相关的操作 : strlen, strcmp…
char str[] = "Hello";// str类型为char[6], null-terminated string, 以'\0'为结尾的字符串 auto ptr = str;// ptr 为 char* std::cout<< strlen(str) << std::endl;// 5, #include<cstring> std::cout<< strlen(ptr) << std::endl;// 5, #include<cstring> ---- char str[] = {'H','e','l','l','o'}; std::cout<< strlen(str) << std::endl;//输出有误,strlen是检测'\0'停止 //更改 char str[] = {'H','e','l','l','o','\0'};
1.5.数组 — 多维数组
- 本质:数组的数组
- int a[3][4];
int x1[3]; int x2[3][4];//从里到外看,先看 x2[3], x2[3]有三个元素,每个元素是 int[4] int x3[3][4][5];//从里往外看,x3[2]-->int[4][5] // (int int int int) (int int int int) (int int int int) // 行优先 x2[0];//类型为int[4] std::cout<< sizeof(int) << std::endl;//4 std::cout<< sizeof(x2[0]) << std::endl;//12 //注意x2[0]是一个左值,int(&)[4] (左值,decltype返回的是类型引用) std::is_same_v<decltype(x2[0]), int(&)[4]> <<std::endl;
- int a[3][4];
-
多维数组的 聚合初始化:一层大括号 V.S. 多层大括号
int x2[3][4] = {1,2,3,4,5}; //(int int int int) (int int int int) (int int int int) // 1 2 3 4 5 0 0 0 0 0 0 0 int x2[3][4] = {{1,2,3,4},{5,6,7,8}}; //(int int int int) (int int int int) (int int int int) // 1 2 3 4 5 6 7 8 0 0 0 0 int x3[3][4] = {{1,2,3,4},{5,6,7}}; //(int int int int) (int int int int) (int int int int) // 1 2 3 4 5 6 7 0 0 0 0 0 int x3[3][4] = {{1,2,3},{4,5,6,7}}; //(int int int int) (int int int int) (int int int int) // 1 2 3 0 4 5 6 7 0 0 0 0 int x[][] = {1,2,3,4};//err无法推导 int x[][2] = {1,2,3,4};//合法 int[2][2] int x[][3] = {1,2,3,4};//合法 int[2][3], 只能省略最前面的 - 多维数组的索引与遍历
- 使用多个中括号来索引
- 使用多重循环来遍历 ```c int x2[3][3] = {1,2,3,4}; x[0][3]; x2[1],x2[2]; —- //for(auto p:x2){//如果这里不加&,则p就退化为指针, 内循环就无法对指针进行循环!! for(auto& p:x2){//这里一定要加引用 for(auto q:p){ std::cout« q «‘\n’; } }
//三层同理 intx3[3][4][5] = {1,2,3,4,5,6}; for(auto& p:x3){ for(auto& q:p){ for(auto r:q){ std::cout« r « ‘\n’; } } }
//通过size遍历 size_t index0 = 0; while(index0 < std::size(x2)){// <3, x2是一个数组,包含了三个元素 size_t index1 = 0; while(index1 < std::size(x2[index0])){// <4 std::cout«x2[index0][index2]«‘\n’; index1 ++; } index0 ++; }
//x[0][0-4] x[1][0-4] x[2][0-4] 这种遍历顺序更优,地址的连续 //x[0-2][0] x[0-2][1] x[0-2][2] x[0-2][3] ```
- 指针与多维数组
- 多维数组可以隐式转换为指针,但只有最高维会进行转换,其它维度的信息会被保留
int x2[3][4] = {1,2,3,4}; auto ptr = x2;//ptr类型为 int(*)[4],类型退化 x2[1];// --> *(x2 + 1)// 这里 1 的解析则为 1*4int int x3[3][4][5]; auto ptr = x3;//int(*)[4][5],类型退化 auto ptr2 = ptr[0];//int(*)[5],类型退化 - 使用类型别名来简化多维数组指针的声明
#include<iostream> using A2 = int[4][5]; int main(){ int x2[3][4][5]; A2* ptr = x2;//int(*)[4][5] auto ptr1 = ptr[0]; } ---- #include<iostream> using A = int[4]; int main(){ int x2[3][4]; //还可以这样定义:x2 A x2[3];// x2有三个元素,每个元素为A,既每个元素为int[4] A* ptr = x2;//int(*)[4] auto ptr1 = ptr[0]; } - 使用指针来遍历多维数组
#include<iostream> int main(){ int x2[3][4]; auto ptr = std::begin(x2);// ptr为int(*)[4] while(ptr != std::end(x2)){ auto ptr2 = std::begin(*ptr); //ptr2 为int* while(ptr2 != std::end(*ptr)){ std::cout<< *ptr2 << '\n'; ptr2 ++; } ptr = ptr + 1; } }
- 多维数组可以隐式转换为指针,但只有最高维会进行转换,其它维度的信息会被保留
二.vector
- 是 C++ 标准库中定义的一个类模板
#include<iostream> #include<vector> int main(){ std::vector<int> x; std::vector<float> y; } - 与内建数组相比,更侧重于易用性
- 可复制、可在运行期动态改变元素个数
#include<iostream> #include<vector> int main(){ std::vector<int> x; std::vector<int> y; //数组不支持复制 int a[3]; int b[3] = a;//err x = y;//可以复制 }
- 可复制、可在运行期动态改变元素个数
- 构造与初始化
- 聚合初始化
std::vector<int> x = {1,2,3}; std::vector<int> x(3); //三个int,随机初始化 std::vector<int> x(3,1);//3个1 其他的参考: https://duckduckgo.com/?sites=cppreference.com&q=std%3A%3Avector - 其它的初始化方式
- 聚合初始化
- 其它方法
- 获取元素个数、判断是否为空
std::vector<int> x(3,1); std::cout<< x.size()<<'\n';//size()方法 std::cout<< x.empty()<<'\n'; - 插入、删除元素
std::vector<int> x(3,1); x.push_back(2); x.pop_back();//删除最后一个 - vector 的比较
std::vector<int> x1 = {1,2,3}; std::vector<int> x2 = {1,3,2}; std::cout<< (x1 == x2) <<'\n';//0 字典排序 std::cout<< (x1 > x2) <<'\n';//0
- 获取元素个数、判断是否为空
- vector 中元素的索引与遍历:
- [] V.S. at
int a[3] = {1,2,3}; std::vector<int> x1 = {1,2,3}; std::cout<< x1[2] <<'\n';//容易越界 std::cout<< x1.at(2) <<'\n';//会检测越界行为 - (c)begin / (c)end 函数 V.S. (c)begin / (c)end 方法
int a[3] = {1,2,3}; std::vector<int> x1 = {1,2,3}; auto b = std::begin(x1);//不是指针了, 返回的是迭代器 auto e = std::end(x1);
- [] V.S. at
- 迭代器
- 模拟指针的行为
- 包含多种类别,每种类别支持的操作不同
- vector 对应随机访问迭代器
- 解引用与下标访问
- 移动
- 两个迭代器相减求距离
- 两个迭代器比较
- vector 相关的其它内容
- 添加元素可能使迭代器失效
std::vector<int> x1 = {1,2,3}; auto b = std::begin(x1);//不是指针了, 返回的是迭代器 auto e = std::end(x1); x1.push_back(4); 添加元素后,b, e 可能就失效了 -
多维 vector
```c std::vector<std::vector
> x; x.push_back(std::vector ()); x[0].push_back(1); cout<< x[0][0] <<endl; std::vector<std::vector
> x{{1,2,3},{4,5}};//聚合初始化 - 从 . 到 -> 操作符
c std::vector<int> x1 = {1,2,3}; cout<< x.size()<<endl; std::vector<int>* ptr = &x; cout<< (*ptr).size()<<endl; cout << ptr->size() <<endl;// ptr 为指针 - vector 内部定义的类型
- size_type
- iterator / const_iterator
- 添加元素可能使迭代器失效
三.string
- 是 C++ 标准库中定义的一个类模板特化别名,用于内建字符串的代替品
- 与内建字符串相比,更侧重于易用性
- 可复制、可在运行期动态改变字符个数
- 构造与初始化
- 其它方法
- 尺寸相关方法( size / empty )
- 比较
- 赋值
- 拼接
- 索引
- 转换为 C 字符串
#include<iostream> #include<string> int main(){ std::string x = "Hello World"; std::string y = x; std::string y(x); std::string y("Hello"); y = "New World!" y = y + x; y = y + "Hello"; y = y + '!'; auto ptr = y.c_str();//转换为 C 字符串 返回指针,指向null-terminated字符串 }