这篇笔记的内容是字符串、向量和数组。
字符串和向量都是抽象数据类型,而数组则相对更基础,个人认为算是介于抽象数据类型和基本内置类型之间吧。
一、命名空间的using声明
在之前(1)的笔记中给出过using跟include区别的链接。个人认为namespace的作用范围要比include大的。如果要进一步了解namespace参考:C++/C++11中命名空间(namespace)的使用_网络资源是无限的-CSDN博客
注意的是:
还有就是我们经常只使用using namespace std直接声明整个std,但是这样还是比较笼统。用类似using std::cin声明会更容易理解cin是std里面的,这个在项目较大的时候应该会有帮助。
关于namespce怎么用举两个例子:
1.namespace跟main函数在一个文件里的话
namespace B
{
int x = 1;
}
using namespace B;//要放在定义的下面
int main()
{
cout << x << endl ;
return 0;
}
这个时候对里面的数据类型没太大要求,应该是这个时候x的作用域就是在该页面。
2.要跨文件的话(namespace跟main不在一个文件)
首先是要新建一个头文件和源文件的,假设为a.h跟a.cpp
//a.h文件
#pragma once
namespace A
{
extern int y;
/*
变量只能为const或者static才可以调用,个人理解是不加修饰的话只能在局部调用.
如果直接加const或者static修饰的话可以不必在a.cpp文件赋值了,
应该是这两个修饰符代表就是全局变量了,否则只能加extern关键字
*/
double max(int size);
}
//a.cpp
int A::y = 11;
double A::max(int size) {
return size;
}
//main.cpp
int main()
{
A::y=11;//这时候的y可以更改,要是在a.h定义为const的话就不能改了
cout << y << endl ;
return 0;
}
二、标准库类型string
string是官方封装的,效率肯定不低,至少比我们自己用基本数据类型随便封装的会高的多。这里主要记住一下要用string要导入的头文件和命名空间
1.定义与初始化string对象
关于直接初始化与拷贝初始化
关于拷贝初始化与直接初始化即看看是否是等号直接赋值。
2.对string对象的操作
可理解为string有哪些函数和变量。
1)读写string对象
这个说明采用cin输入的话是无法给string赋值包含空格的字面值的,那咋样才能让string里面包含空格呢?
还是证明了cin输入string变量的时候是可以以空格作为分隔的。然后试试用while。
int main()
{
string s1;
while (cin >> s1) {
cout << s1 << endl;
if (s1 == "11")break;
}
return 0;
}
直到遇到11的时候才会停止,并且这个是不断覆盖的,最后s1="11"。
2)使用getline读取一整行
用这个办法就可以给字符串赋值包含空格的字符串字面值了,即这个是赋值一整行,而cin是一个词一个词赋值。
3)string的empty和size函数
4)string::size_type类型
大致要知道size()返回的是一个无符号数。
5)string之间的比较
记住两个比较规则。具体实现应该是字符串按字符一位一位比较Ascii码值。
6)字面值与string对象相加
两个原则:a.加的顺序,要按照从左到右的优先级,并且(字面值常量+string变量)与(string变量+字面值常量)最后都是string变量;
b.加号两边一定要有一边是string对象
举例一:
string s1="11";
string s = "1" + s1+"2"+"22"+"111";
这个是对的,因为“1”+s1是string变量,并且接下来从左到右依次叠加都是string变量。
举例二:
string s1="11";
string s = "1" + "121"+s1+"2"+"22"+"111";
这个是错的,因为“1”+"121"加号两端没有string变量。
3.处理string对象中的字符
1)系统中的字符处理函数
2)范围for语句遍历字符串每个字符
也可以把string看成一个字符数组,直接用下标进行访问。
三、标准库类型vector
vector 被称为容器,其是类模板。
1.定义和初始化vector对象
1)默认初始化
2)列表初始化
注:之前string中的初始化是从直接初始化与拷贝初始化的角度去介绍,这里则是从列表初始化与值初始化的角度去介绍
3)值初始化
4)关于列表初始化与值初始化
二者区别在于圆括号与花括号。
2.vector对象添加元素
注意:vector对象值初始化确定元素数量后,还可以使用push_back继续添加元素。如下例子:
vector<int> vec(2, 0);
cout << vec.size() << endl;//输出2
vec.push_back(2);
cout << vec[2] << endl << vec.size() << endl;//输出2和3
3.vector的其他操作
基本上其他操作都跟string类似,具体可参考string的内容。
关于vector的下标:
四、迭代器介绍
使用typeid打印出来的vector<int>的迭代器类型
1.迭代器运算符
迭代器和指针的使用方法类似,基本上可以把它当成一个指针来用。
1)迭代器类型
2)begin与end运算符
即使用cbegin与cend可以使得迭代器为const。
3)迭代器失效
2.迭代器运算
五、数组
数组也是容器。
1.定义和初始化内置数组
即如果想动态控制维度,还是别用是数组了。用vector吧。
1)显式初始化数组元素
需要注意的是自动初始化默认值。
2)字符数组的特殊性
即字符数组的最后一位一定要是空字符。未声明维度时, 字符数组的长度为字符字面值个数+1(即自动补充的空字符)。如果声明了维度,那么字符中字符字面值个数只能为维度减一,最后一位要留来给空字符。
3)不允许拷贝和赋值
一般来说其他语言则是可以的,这个也算是C++比较不人性化的地方。不过可以通过数组指针来拷贝。
4)复杂的数组声明
有指针数组,没有引用数组(着重点在数组元素);而有数组的指针也有数组的引用(着重点在数组)。
关于&符和*符修饰的是数组还是元素还是很容易弄混的,需要背一下。
2.数组元素的访问
其他的访问方式与vector类似。
注意:在使用范围for语句的话,关于参数传递要把它当成函数一样对待,它也是分为值传递、引用传递的。
char a[3] = {'1','2'};
for (auto f : a) {
f = 'b';
cout << f;//依次输出bbb
}
for (auto f : a) {
cout << f;//依次输出12空格
}
for (auto &f : a) {
f = 'c';
cout << f;//依次输出ccc
}
for (auto f : a) {
cout << f;//依次输出ccc
}
所以如果要用范围for语句对数组赋值的话要记得使用引用传递
3.指针与数组
即数组的名字等价于数组第一个元素的指针。
1)指针与迭代器
迭代器能干的活,指针也能干。指针也是迭代器。
2)标准库函数中的begin和end
注意最后一点,尾后指针无法用于解引用与递增操作(因为递增后可能会访问未规划的内存造成数组访问越界的现象,此类错误在C++编译器中不会提示),但是拿来递减后解引用还是可以的。
3)数组与指针的其他骚操作
两个指针相减,最后得出的类型是一种标准库类型。
指针与下标
要注意一下,C++中数组的下标是很随意灵活的,同时也很容易翻车。
char a[3] = {'1','2'};
char *endp = end(a),*startp=begin(a);
cout << *(endp+1) << endl << endp[-2] << endl<< a[-2] << endl;
*(a+4)= 9;
cout << a[3] << endl;
如图所示,这样搞C++都不会报错的,不过在超出数组范围内还要访问的话输出为空。
这个举例也说明了数组跟指针是紧密联系的。
4.C风格字符串
引起 安全漏洞是比较可怕的事。
1)常见的C风格字符函数
注意的是:定义的字符数组最后要加上空白字符,这样才方便让C风格函数调用,否则容易出错。
2)其他注意
关于 C风格字符串可进一步参考:理解c-风格字符串 - 简书
5.数组与标准库类型之间
1)string与字符数组
要把string当成字符数组去用指针访问的话要使用c_str函数。
2)数组初始化vector对象
vector无法初始化数组,但是数组可以初始化vector对象。
六、多维数组
可以把多维数组理解成数组嵌套。
1.多维数组初始化
这些初始化的规律只能是不断重复实践背下来了。
2.多维数组下标
3.范围for语句处理多维数组
关于多维数组在使用范围for语句时,前n-1层的循环要写成引用是有讲究的,否则这些参数将会被识别为指针,然后就会报错的。只有最后一层才能自由声明。(即参考一维数组,范围for语句中的参数的类型是int指针类型的,而前面的n-1层需要是数组不能是指针)这个感觉也没规律,只能硬记了
4.指针和多维数组
关于数组的指针还是指针的数组需要多记一记,要不然容易弄混。
然后举个例子说明多维数组的多级指针思想
int ia[2][3][4] = { 0 };
int(*p)[3][4] = &ia[1];
//在赋值时,ia默认等于&ia[0]
//p是最底层的指针,是一个三级指针,要用三个*号才能将其彻底解引用
int(*p1)[4] = p[0];
//p1是一个二级指针,要用二个*号才能将其彻底解引用
int(*p2) = p1[0];
//p2是一个普通指针,用一个*号能将其解引用
cout << *p2 << endl<< p1[0]<<endl;//最终打印出0还有ia[1][0][0]的地址
可以从多级指针的角度理解多维数组的指针问题,这一块的逻辑有点复杂。
1)使用auto或decltye
使用自动类型推断能降低工作量,更好简化指针的访问逻辑。根据auto推断出来的指针依然符合上一条的多级指针的举例。
2)使用类型别名进行简化
感觉还是没有直接类型推断来的快。
3)多维数组中的地址
为了验证一下多维数组中的元素是不是顺序存储的,
int ia[3][3] = {1,2,3,4};
int* q = &ia[0][0];
for (int k = 0; k < 9; ++k)cout << *(q+k)<<" "<<q+k<<" ";
cout << endl;
for (auto& i : ia)
for (auto& j : i)cout << j << " " << &j<<" ";
结果证明是顺序存储的。所以如果想贪快遍历整个多维数组的话用一个指针,一层循环就够了。
但是要注意,两重的vector则不再是连续的了(一重的话还能是连续的),如以下代码:
vector<vector<int>> vec(2,vector<int>(2));
int* p = &vec[0][0];
for (int i = 0; i < 4; i++)
{
cout << *(p + i) << endl;
}
打印结果: