zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C++ Primer读书笔记——3.字符串、向量和数组

C++数组 字符串 读书笔记 向量 primer
2023-09-14 09:14:58 时间

字符串、向量和数组

C++ 定义了一个丰富的抽象数据类型库。string 和 vector是两种最重要的标准库类型。前者支持可变长字符串,后者支持可变长数组。

一、命名空间的using声明

目前为止,用到的库函数基本上都是属于命名空间std

我们可以通过一个简单的途径来使用命名空间中的成员。其中一个最安全的方法就是使用using声明

1.每一个名字都需要独立的using声明

#include<iostream>
//通过using 声明 我们可以使用标准库中的成员
using std::cin;
using std::cout;
using std::endl;

int main() {
	cout << "enter two numbers:" << endl;
	int a, b;
	cin >> a >> b;
	cout << "The sum of " << a << " and " << b << " is " << a + b << endl;
	system("pause");
	return 0;
}

2.头文件不应该包含using声明

位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会被拷贝到引用它的文件中去,如果头文件中有某个using声明,那么每一个使用了该头文件的文件都会有这个声明,可能会产生一些冲突

二、标准库类型string

标准库类型string表示可变长的字符序列,使用string类型必须先包含string头文件,string定义在命名空间std中。

1.定义和初始化string对象

如何初始化类的对象是由类本身决定的,一个类可以定义很多种初始化对象的方式,只不过这些方式之间有所差别。

	string s1;    //默认初始化 s1是一个空串
	string s2 = s1; //s2 是 s1的副本
	string s3 = "abc"; //s3 是 "abc" 字面值的副本
	string s4(3, 'a'); //s4 的内容是 aaa

1.直接初始化 和 拷贝初始化

如果使用等号(=)初始化一个变量,实际执行的是拷贝初始化,不使用等号执行的是直接初始化。

2.string对象上的操作

一个类除了要规定初始化其对象的方式之外,还要定义对象上所能执行的操作。

操作操作
os<<s将 s 读入输出流os中,返回os
is>>s从is中读取字符串赋值给s,字符串以空白分割,返回is
getline(is,s)从is中读取一行赋值给s,返回is
s.emty()s 为空返回true,否则返回false
s.size()返回s中字符个数
s[n]返回s中第n个字符的引用,位置从0开始
s1+s2返回 s1 和 s2 连接后的结果
s1 = s2用s2 的副本代替 s1
s1 == s2如果s1 和 s2 所含字符完全一样,则相等
<,<=,>,>=按字典序排序

1.读写string对象

可以用 IO 操作读写string对象

	string s1, s2;
	cin >> s1 >> s2;
	cout << s1 << s2 << endl;

2.读取位置数量的string对象

	string word;
	while (cin >> word) {
		cout << word << endl;
	}

3.使用getline 读取一整行

getline 函数的参数是一个输入流 和 一个string对象,函数从给定的输入流中读取一行的内容,直到遇到换行符为止(换行符也被读进来了),然后把读到的内容存入到这个string对象中(不存换行符)

string line;
    while (getline(cin, line)) {
		cout << line << endl;
	}

注意:触发getline 返回的那个换行符被丢弃掉了,得到的string对象并不包含这个换行符

4.string 的 empty 和 size操作

empty 函数根据string对象是否为空返回一个对应的布尔值

size 函数返回string对象的长度(即string 对象中字符个数)

	string a = "";
	string b = "abc";

	bool f1 = a.empty(); // a = true
	bool f2 = b.empty(); // b = false
	int cnt_a = a.size(); // cnt_a = 0
	int cnt_b = b.size(); //cnt_b = 3

5.string::size_type类型

size 函数返回的实际上是一个 size_type 类型

string类及其 其他大多数标准库类型都定义了集中配套的类型。这些配套类型体现了与机器无关的特性,类型size_type 就是其中一种。

有一点可以肯定的是:size_type 是一个无符号类型的值,而且能够存放任何string对象的大小

C++ 11 标准中,允许编译器通过 auto 或 decltype 来推断变量的类型。

	auto len = line.size(); // len 的 类型是 string::size_type

建议:如果一条表达式中已经有了size()函数,就不要再使用 int 了,这样可以避免混用 int 和 unsigned 可能带来的问题

6.比较 string 对象

string 类定了集中用于比较字符串的运算符。

  • 相等性运算符(== 和 !=)分别检验两个 string 对象相等 或 不相等,string 对象相等则意味着它们的长度相同 而且 所包含的字符也全部相同。

  • 关系运算符(<,<=,>,>=) 分别检验一个 string 对象是否小于,小于等于,大于,大于等于另外一个 string 对象。

    string str = "Hello";
	string phrase = "Hello World";
	string slang = "Hiya";

	//phrase > str     slang > phrase

7.为 string 对象赋值

对于string 类而言,允许把一个对象的值赋给另一个对象

	string st1(10, 'c'), st2;
	st1 = st2;   //st1 和 st2 都为空字符串了

8.字面值 和 string对象相加

标准库允许把 字符字面值 和 字符串字面值 转为 string 对象,所以在需要 string 对象的地方就可以使用这两种字面值来替代。

	string s1 = "hello", s2 = "world";
	string s3 = s1 + "," + s2;

当把 string 对象和 字符串字面值 以及 字符字面值 混在一条语句中使用时,必须确保每一个 加号(+)的两侧的运算对象 至少有一个是string对象

	string s4 = s1 + ",";
	string s5 = "hello" + ","; //错误: 两个对象都不是string

	string s6 = s1 + "," + "world";
	string s7 = "hello" + "," + s2;//错误: 不能把字面值直接相加

注意:因为某些历史原因,也为了与c兼容,所以C++中字符串字面值并不是标准库string对象。字符串字面值 与 string是不同类型

3.处理string对象中的字符

在cctype 头文件中定义了一组库函数处理针对字符的操作

函数操作
isalnum ( c )c 是字母时 或 数字时为真
isalpha ( c )c 是字母时为真
iscntrl ( c )c 是控制字符时为真
isdigit ( c )c 是数字时为真
isgraph ( c )c 不是空格 但 可以打印时为真
islower ( c )c 是小写字母时为真
isprint ( c )c 是可打印的字符时为真
ispunct ( c )c 是标点符号时为真
isspace ( c )c 是空白时为真
issupper ( c )c 是大写字母时为真
isxdigit ( c )c 是16进制数时为真
tolower ( c )c 转为小写字母
toupper ( c )c 转为大写字母

1.基于范围的for循环语句

   string str("some string");

	//每行输出str中的一个字符
	for (auto c : str) {
		cout << c << endl;
	}

2.使用范围 for 循环改变字符串的字符

如果想要改变 string 对象中字符的值,必须把循环变量定义为引用类型。

    string s("Hello World!!!");
	//将 s 中的字符转为大写形式
	for (auto& c : s) {
		c = toupper(c);
	}
	cout << s << endl; //结果为: HELLO WORLD!!!

3.处理部分字符

想要访问string对象中单个字符有两种方法:一种是使用下标;另外一种是使用迭代器。

下标运算符 ( [ ] ) 接收的输入参数是 string::size_type 类型的值,返回对应位置的引用。

	//把s 第一个字母变为大写
	string s("some string");
	if (!s.empty()) {
		s[0] = toupper(s[0]);
	}

三、标准库类型vector

标准库类型vector 表示对象的集合,其中所有对象的类型都相同。

想要使用vector,必须包含vector的头文件。

vector 是一个类模板。编译器根据类模板创建类 或 函数的过程称为实例化,。当使用类模板时,需要指出编译器应该把类 或 函数实例成什么类型。

注意:vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,如vector< int >

1.定义和初始化vector对象

vector 模板控制着定义和初始化向量的方法

初始化含义
vector v1v1 是一个空 vector,元素类型是T,执行默认初始化
vector v2(v1)v2中的元素 是 v1元素的拷贝
vector v2 = v1等价于 v2(v1)
vector v3(n,val)v3 包含n个重复元素,每一个元素都是val
vector v4(n)v4 包含 n 个0
vector v5{a,b,c,…}v5 包含了初始值个数的元素,每一个元素被赋予了相应的初始值
vector v5 = {a,b,c,…}等价于 v5{a,b,c,…}
    vector<int> ivec; //初始状态为空

	ivec.push_back(10);

	vector<int> ivec2(ivec); //把ivec 的元素 拷贝给ivec2
	vector<int> ivec3 = ivec;//把 ivec 的元素 拷贝给ivec3
	vector<string> svec(ivec2);//错误: svec的类型是string 不是int

1.列表初始化 vector

C++ 11 标准提供了另一种 为vector对象的元素赋值的方式,即列表初始化。

	vector<string> v1{ "a","an","the" };
	vector<string> v2("a", "an", "the"); //错误: 列表初始化用的是 {}

	vector<int> ivec(10, -1);  //10 个int元素 每一个元素都被初始化为-1
	vector<string> svec(10, "hi"); //10 个 string对象,每一个对象都被初始化为 hi

2.值初始化

如果vector对象的元素是内置类型,如 int,则元素的初始值自动设为0。

如果vector对象中元素的类型不支持默认初始化,我们就必须提供初始的元素值。

	vector<int> ivec(10); //10个int元素都是0
	vector<string> svec(10); //10个string对象 都是 ""

3.列表初始化还是元素数量

在某些情况下,初始化的真正含义依赖于传递初始值时用的是花括号 还是 圆括号。

    vector<int> v1(10);    //v1 有10个元素,每个元素都是0
	vector<int> v2{ 10 };  //v2 有一个元素,该元素的值是10

	vector<int> v3(10, 1);  //v3 有10个元素,每个元素都是1
	vector<int> v4{ 10,1 }; //v4 有2个元素,值分别是10,1

	vector<string> v5{ "hi" };  //列表初始化 , v5有一个元素
	vector<string> v6("hi");    //错误: 不能使用字符串字面值构建vector对象
	vector<string> v7{ 10 };    //v7 有 10 个空串
	vector<string> v8{ 10,"hi" };//v8 有 10个 "hi" 元素

2.向vector中添加元素

创建一个vector对象时,可能会存在并不清楚实际元素的数量,元素的值的情况。

可以使用vector 的成员函数 push_back()向容器末端添加元素。

	vector<int> v;
	for (int i = 0; i < 100; i++) {
		//依次 把 1 - 100 放入v中
		v.push_back(i + 1);
	}

注意:如果循环体内包含向vector对象添加元素的语句,则不能使用for循环。范围for循环不应该改变其所遍历序列的大小

3.其他vector操作

操作含义
v.empty()v 为空返回真,否则返回假
v.size()返回 v 中元素个数
v.push_back(t)向 v 的末端添加元素t
v[n]返回 v 中第 n 个位置上的引用
v1 = v2用v2 中的元素拷贝替换 v1 中的元素
v1 = {a,b,c…}用列表中的元素拷贝替换v中的元素
v1 == v2v1 与 v2 相等仅当它们元素数量相等,对应位置的元素值相等
<,<=,>,>=按字典序比较
   vector<int> v{ 1,2,3,4,5,6,7,8,9 };

	for (auto& i : v) {
		//求元素值的平方
		i *= i;
	}

	for (auto i : v) {
		cout << i << endl; //输出元素
	}

1.计算vector内对象的索引

使用下标运算符能获取到指定位置的元素。

vector对象的下标也是从0开始,下标的类型是相应的size_type类型。

2.不能用下标形式添加元素

注意:vector对象(以及string对象)的下标运算符可用于访问已经存在的元素,而不能用于添加元素。

四、迭代器介绍

除了通过下标运算符来访问string对象的字符 和 vector对象的元素之外,使用迭代器也能达到目的。

所有的标准库容器都可以使用迭代器,但是其中只有少部分才同时支持下标运算符。

类似于指针类型,迭代器也提供了对 对象的间接访问。迭代器有 有效迭代器无效迭代器之分,有效迭代器指向某一个元素,或者指向容器中尾元素的下一个位置;其他的都是无效迭代器。

1.使用迭代器

和指针不一样,获取迭代器不是用取地址符。

begin成员返回指向第一个元素的迭代器,end成员返回指向容器的尾元素的下一位置的迭代器,即尾后迭代器。

若容器为空,begin 和 end 返回的是同一个迭代器,都是尾后迭代器。

	vector<int> v;
	auto b = v.begin(), e = v.end();
	//v 是 空容器 所以b 和 e 相同,都是尾后迭代器
操作含义
*iter返回迭代器 iter 所指元素的引用
iter->mem解引用并获取该元素名为mem的成员,等价于(*iter).mem
v.push_back(t)向 v 的末端添加元素t
++iteriter 的指向往后移动一位
–iteriter 的指向往前移动一位
iter1 == iter2如果两个迭代器指向的是同一个元素 或者 是同一个容器的尾后迭代器,则相等;否则不相等

试图解引用一个非法迭代器 或者 尾后迭代器都是未被定义的。

	string s("Some thing");

	if (s.begin() != s.end()) { //确保s非空
		auto it = s.begin(); //it 指向 s 中第一个字符
		*it = toupper(*it);
	}

	//结果为 Some thing

2.将迭代器从一个元素移动到另一个元素

迭代器使用递增(++)运算符 从一个元素移动到下一个元素。

注意:因为end 返回的迭代器并不实际指向某一个元素,所以不能对其递增 或者 解引用操作。

	//将 string 对象中的第一个单词改为大写形式

	for (auto it = s.begin(); it != s.end() && !isspace(*it); it++) {
		*it = toupper(*it);
	}

3.迭代器类型

实际上,那些拥有迭代器的标准库类型使用 iterator 和 const_iterator 来表示迭代器的类型。

	vector<int>::iterator it;    //it 能读写vector<int> 的元素
	string::iterator it2;        //it2 能读写string 的元素

	vector<int>::const_iterator it3; //it3 只能读元素,不能写元素
	string::const_iterator it4;      //it4 只能读元素,不能写元素

4.begin 和 end运算符

begin 和 end返回的具体类型由对象是否是常量决定。如果是常量,begin 和 end 返回 const_iterator,否则返回 iterator

5.结合解引用 和 成员访问操作

解引用迭代器可获得迭代器所指向的对象,如果该对象的类型恰好是类,就可能希望进一步访问它的成员。

	//C++ 中定义了 箭头运算符(->) 把解引用 和 成员访问两个操作结合在一起
	vector<string> text;
	//输出text 的每一行,直到遇到一个空白字符为止
	for (auto it = text.cbegin(); it != text.cend() && !it->empty(); it++) {
		cout << *it << endl;
	}

6.某些对vector对象的操作会使迭代器失效

不能在范围for 循环中向vector对象添加元素。另外一种限制是任何一种可能改变vector容量的操作,比如push_back(),都会使该vector对象的迭代器失效。

注意:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。

2.迭代器运算

操作含义
iter + n迭代器向后移动 n 个位置
iter - n迭代器向前移动 n 个位置
iter += niter 指向向后移动 n 个位置后的元素
iter -= n
iter1 - iter2两个迭代器之间的距离
>,>=,<,<=如果某迭代器指向的容器位置在另一个迭代器所指位置之前,前者小于后者

1.迭代器的算数运算

	//计算得到 v1 中间元素的迭代器
	auto it = v1.begin() + v1.size() / 2;

2.使用迭代器运算

	//用迭代器完成二分搜索
	auto l = text.begin(), r = text.end();
	auto mid = text.begin() + (r - l) / 2;

	while (mid != r && *mid != target) {
		if (target < *mid) {
			r = mid;
		}
		else {
			l = mid + 1;
		}
		mid = l + (r - l) / 2;
	}

五、数组

1.定义和初始化内置数组

数组是一种复合类型。其维度(数组元素的个数)编译时应该是已知的,即维度必须是一个常量表达式。

	unsigned cnt = 42;    //不是常量表达式
	constexpr unsigned sz = 42;   //常量表达式

	int arr[10];
	int* parr[sz];
	string bad[cnt]; //错误: cnt 不是常量表达式
	string strs[get_size()];  //get_size 是常量表达式时正确,否则错误

和内置数据类型一样,如果函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。

定义数组的时候必须指定数组的类型,不允许用 auto 由初始值的列表推断类型。

1. 显式初始化数组元素

	const unsigned sz = 3;
	int ial[sz] = { 0,1,2 };
	int a2[] = { 0,1,2 }; //维度是3的数组
	int a3[5] = { 0,1,2 }; //等价于 a3[5] = {0,1,2,0,0}
	string a4[3] = { "hi","bye" };   //等价于 a4[3] = {"hi","bye",""}
	int a5[2] = { 0,1,2 };          //错误: 初始值过多

2. 字符数组的特殊性

我们可以使用字符串字面值初始化字符数组。使用这种方式时,需要注意字符串结尾的空字符,这个空字符也会像样本的其他字符一样被拷贝到字符数组中去。

	char a1[] = { 'c','+','+' };   // 列表初始化,没有空字符
	char a2[] = { 'c','+','+','\0' }; // 列表初始化,含有显式的空字符
	char a3[] = "c++";   //自动添加表示字符串结束的空字符
	const char a4[6] = "Daniel"; // 错误: 没有空间来存放空字符

3. 不能允许拷贝的赋值

不能将数组的内容拷贝给其他数组作初始值,也不能用数组为其他数组赋值

	int a[] = { 0,1,2 };
	int a2[] = a;    //错误: 不允许使用一个数组初始化另一个数组
	a2 = a;     //错误: 不能把一个数组直接赋值给另一个数组

4. 理解复杂数组声明

	int* ptrs[10];    //ptrs 是含有十个整型指针的数组
	int& refs[10];    //错误: 不存在引用的数组
	int(*Parray)[10] = &arr;  //Parray 是一个指向含有十个整数的数组
	int(&arrRef)[10] = arr;   //arrRef 引用一个含有十个整数的数组

	int* (&array)[10] = ptrs;  //array 时数组的引用,该数组含有10个指针

要理解数组声明的含义,最好的方法是从数组的名字开始按照由内向外的顺序阅读

2. 访问数组元素

1. 检查下标的值

大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似的数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。

3. 指针和数组

使用数组时编译器一般会把它转为指针。

  • 当使用数组作为一个 auto 变量的初始值时,推断得到的类型时指针而非数组。
	int ia[] = { 0,1,2,3,4,5,6,7,8,9 };
	auto ia2(ia);  //ia2 是一个指针 ,指向ia 的第一个元素

	ia2 = 42;    //错误: ia2 是一个指针,不能用int值 给指针赋值
  • 当使用的是decltype 时不会发生这样的情况,decltype(ia) 返回类型是有 10 个整数构成的数组
	decltype(ia) ia3 = { 0,1,2,3,4,5,6,7,8,9 };

	ia3 = p;     //错误: 不能用整型指针给数组赋值
	ia3[4] = 10; 

1. 指针也是迭代器

	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

	//尾后迭代器 : arr[10] 这个元素是不存在的
	int* e = &arr[10];
	//遍历输出数组的元素
	for (int* b = arr; b != e; b++) {
		cout << *b << endl;
	}

2. 标准库函数begin 和 end

尽管能计算得到尾后指针,但是这种用法很容易出错。C++ 11 引入了 begin 和 end函数。

	int* beg = begin(arr);
	int* last = end(arr);
	for (; beg != last; beg++) {
		cout << *beg << endl;
	}

3. 指针运算

一个指针加上(减去)某一个整数 k ,结果还是指针。新的指针与原来的指针相比前进了(后退了) k 个位置。

	constexpr size_t sz = 5;
	int arr[sz] = { 1,2,3,4,5 };
	int* ip = arr;   //等价于 int* ip = &arr[0];
	int* ip2 = ip + 4;  //ip2 指向arr[4] 这个元素

如果一个指针 p 是空指针,允许给 p 加上 或 减去一个值为0的整型常量表达式。两个空指针也允许彼此相减,结果也为0.

4. 解引用和指针运算的交互

假设结果指针指向了一个元素,则允许解引用该结果指针。

	int ia[] = { 0,2,4,6,8 };
	int last = *(ia + 4);  //last = 8 即 ia[4] 的值

5. 下标和指针

对数组执行下标运算其实是对指向数组的指针进行下标运算。

4. C风格字符串

尽管C++支持C风格字符串,但是在C++程序中最好还是不要使用它们。

1. C标准库string函数

函数含义
strlen( p )返回 p 的长度,不包含空字符
strcmp ( p1 , p2 )比较 怕p1 和 p2.如果 p1 == p2,返回0;p1 > p2,返回正数;否则返回负数
strcat ( p1 , p2 )把 p2 附加到 p1 之后,返回p1
strcpy ( p1 , p2 )将p2 拷贝给 p1 ,返回 p1

5. 与旧代码的接口

1. 混用string对象 和 c 风格字符串

  • 允许使用c 风格字符串来初始化string对象 或 为string对象赋值

  • 在string对象的加法运算中允许使用 c 风格字符串作为其中一个运算对象(不能两个都是

  • 在string对象的复合赋值运算中允许使用 c风格字符串作为右侧的运算对象

string 提供了一个c_str 成员函数 返回一个 c 风格字符串。

	string s("hello world");

	char* str = s;   //错误: 不能用string 对象初始化 char*
	const char* str = s.c_str();

2. 使用数组初始化 vector对象

可以用数组初始化vector对象,反之则不行。

	int arr[] = { 0,1,2,3,4,5 };

	//v 中有6个元素 分别是v中对应元素的副本
	vector<int> v(begin(arr), end(arr));

现代C++程序应该尽量使用 vector 和 迭代器,避免使用内置数组 和 指针;应该尽量使用 string,避免使用 c风格字符串。

六、多维数组

通常所说的多维数组其实是数组的数组

1. 多维数组的初始化

	//三个元素 每一个元素都是大小为4的数组
	int a[3][4] = {
		{0,1,2,3},
		{4,5,6,7},
		{8,9,10,11}
	};

	//与上面等价
	int a[3][4] = { 0,1,2,3,4,5,6,7,8,9,10,11 };

	//显式的初始化每一行的首元素 其他元素默认初始化
	int a[3][4] = { {0},{4},{8} };

	//显式的初始化第一行,其他元素默认初始化
	int x[3][4] = { 0,3,6,9 };

2. 多维数组的下标引用

可以使用下标运算符来访问多维数组的元素,此时数组的每一个维度对应一个下标运算符。

	size_t cnt = 0;
	for (auto& row : a) {
		for (auto& col : row) {
			col = cnt;
			cnt++;
		}
	}

	//外层循环必须用引用变量的原因是,避免数组被自动转换为指针

	for (auto row : a) {
		//row 是 int* 是一个指针 无法循环遍历
		for (auto col : row) {
			cout << col << endl;
		}
	}

要使用范围 for 循环处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。

3. 指针和多维数组

当使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。数组名转换得来的指针实际上是指向第一个内层数组的指针。

	int a[3][4];

	int(*p)[4] = a;   //p指向含有4个整数的数组

	p = &a[2];  //p 指向 a的尾元素

	int* ip[4]; // 整型指针的数组
	int(*ip)[4]; //指向含有4个整数的数组

	//打印数组内的所有值
	for (auto p = begin(a); p != end(a); p++) {
		for (auto q = begin(*p); q != end(*p); q++) {
			cout << *q << endl;
		}
	}