第3章:字符串、向量和数组
- #1.命名空间的using声明
-
#2.标准库类型string
- 2.1 定义和初始化string对象
- 2.2 string对象上的操作
- 2.3 处理string对象中的字符
-
#3.标准库类型vector
- 3.1 定义和初始化vector对象
- 3.2 向vector对象中添加元素
- 3.3 其他vector操作
-
#4.迭代器介绍
- 4.1 使用迭代器
- 4.2 迭代器运算
-
#5.数组
- 5.1 定义和初始化内置数组
- 5.2 访问数组元素
- 5.3 指针和数组
- 5.4 C风格字符串
- 5.5 与旧代码的接口
- #6.多维数组
#1. 命名空间的using声明
using声明的作用是无须专门的前缀(形如命名空间::)也能使用所需的名字。using声明具有如下形式:
using namespace::name;
一旦声明了上述语句,就可以直接访问命名空间中的名字:
#include <iostream>
//using声明,当我们使用名字cin时,从命名空间std中获取它
using std::cin;
int main() {
int i;
cin >> i; //正确:cin和std::cin的含义相同
cout << i; //错误:没有对应的using声明,必须使用完整的名字
std::cout << i; //正确:显示地从std中使用cout
return 0;
}
每个名字都需要独立的using声明
按照规定,每个using声明引入命名空间中的一个成员。
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main() {
cout << "Enter two nnumbers:" << endl;
int v1, v2;
cin >> v1 >> v2;
cout << "The sum of " << v1 << " and " << v2 << " is " << v1 + v2 << endl;
system("pause");
return 0;
}
头文件不应包含using声明
位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去。如果在头文件中有using声明,那么每个使用了该头文件的文件都会有这个声明。可能会造成名字冲突。
#2. 标准库类型string
标准库string表示可变长的字符序列,使用string类型必须首先包含string头文件。作为标准库的一部分,string定义在命名空间std中。
#include <string>
using std::string
2.1 定义和初始化string对象
如何初始化类的对象由类本身决定。一个类可以定义很多种初始化对象的方式,只不过这些方式之间必须有区别:或者是初始值的数量不同,或者是初始值的类型不同。
string s1; //默认初始化,s1是一个空串
string s2(s1); //s2是s1的副本
string s2 = s1; //等价于s2(s1),s2是s1的副本
string s3("value"); //s3是字面值"value"的副本,除了字面值后面的那个空字符外
string s3 = "value"; //等价于s3("value"),s3是字面值"value"的副本
string s4(n,'c'); //把s4初始化为连续n个字符c组成的串
直接初始化和拷贝初始化
C++语言有几种不同的初始化的方式,通过string我们可以清楚地看到这些初始化方式之间的差别和联系。如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始化值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化。
string s5 = "hiya"; //拷贝初始化
string s6("hiya"); //直接初始化
string s7(10,'c'); //直接初始化,s7的内容是cccccccccc
string s8 = string(2,'c');//拷贝初始化,s8的内容是cc
//s8所执行的操作,可以分解为下面两个步骤:
//【1】用数字2和字符c两个参数创建出来一个string对象,【2】然后这个string对象又拷贝给了s8
string tmp(2,'c'); //tmp的内容是cc
string s8 = tmp; //将tmp拷贝给s8
2.2 string对象上的操作
一个类除了要规定初始化其对象的方式之外,还要定义对象上能执行的操作。
读写string对象
#include <iostream>
#include <string>
using namespace std;
int main() {
string s; //空字符串
cin >> s; //将string对象读入s,遇到空白停止
cout << s << endl; //输出s
}
读取未知数量的string对象
#include <iostream>
#include <string>
using namespace std;
int main() {
string word;
while(cin >> word) { //反复读取直到文件末尾
cout << word << endl; //逐个输出单词,每个单词后面跟一个换行。
}
return 0;
}
使用getline读取一整行
有时我们希望能在最终得到的字符串中保留输入时的空白字符,这时应该用getline函数代替原来的>>运算符。getline的参数是一个输入流和一个string对象,函数从给定的输入流中读取内容,值到遇到换行符为止(换行符也被读进来了),然后把所读的内容存入到那个string对象中去(注意不存换行符)。getline只要一遇到换行符就结束读取操作并返回结果。
#include <iostrem>
#include <string>
using namepace std;
int main() {
string line;
//每次读取一整行,直到文件的末尾
while(getline(cin,line)) {
cout << line << endl;
}
return 0;
}
==触发getline函数返回的那个换行符实际上被丢弃掉了,得到的string对象中并不包含该换行符。==
string的empty和size操作
empty函数是根据string对象是否为空返回一个布尔值,size函数返回string对象的长度(即string对象中字符的个数)。
string::size_type类型
string类及其它大多数标准库类型都定义了几种配套的类型,这种配套类型体现了标准库类型与机器无关的特性,类型size_type即是其中一种。
==如果表达式中已经有了size()函数就不要再使用int了,这样可以避免混用int和unsigned可能带来的问题。==
2.3 处理string对象中的字符
我们经常需要处理string对象中的字符,比如检查一个string对象是否包含空白,或者把string对象中的字母改写成小写,再或者查看某个特定的字符是否出现等。
处理每个字符?使用基于范围的for语句
如果想对string对象中的每个字符执行什么操作,目前最好的办法是使用C++11新标准提供的一个语句:范围for语句。
for(declaration : expression) {
statement
}
使用范围for语句把string对象中的字符每行一个个输出:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str("helloworld");
//每行输出str的一个字符
for(auto c: str) { //对于str中的每个字符
cout << c << endl; //输出当前字符,后面紧跟换行符
}
return 0;
}
使用范围for语句改变字符串的内容
如果想要改变string对象中字符的值,必须把循环变量定义成引用类型。如果我们想把字符串改写为大写字母的形式:
int main() {
string s("helloworld!!!");
for(auto &c : s) { //对于s中的每个字符
c = toupper(c); //c是一个引用,因此赋值语句将改变s中的字符值
}
cout << s << endl;
}
只处理一部分字符?
要想访问string对象中的单个字符有两种方式:一种是使用下标,另外一种是使用迭代器。下标运算符([])接收到参数是string::size_type类型的值,这个参数表示要访问的位置;返回值是该位置上字符的引用。
int main() {
string s("some string");
if(!s.empty()) { //确保s[0]的位置确实有字符
s[0] = toupper(s[0]); //将s的首字符设置为大写
}
return 0;
}
使用下标迭代执行
将s的第一个词改写成大写形式:
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main() {
string s("hello world");
for(decltype(s.size()) index = 0;index != s.size()&&!isspace(s[index]);++index) {
s[index] = toupper[s[index]];
}
cout << s << endl;
return 0;
}
#3. 标准库类型vector
标准库类型vector表示对象的集合,其中所有对象的类型都相同。C++语言既有类模板,也有函数模板,其中vector是一个类模板。
模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类和函数的过程称为实例化。
==vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,例如vector<int>。==
3.1 定义和初始化vector对象
和任何类型一样,vector模板控制着定义和初始化向量的方法。
vector<T> v1; //v1是一个空vector,它潜在的元素是T类型的,执行默认初始化。
vector<T> v2(v1); //v2中包含有v1所有元素的副本
vector<T> v3(n,val); //v3包含n个重复的元素,每个元素的值都是val
vector<T> v4(n); //v4包含了n个重复地执行了值初始化的对象
vector<T> v5{a,b,c...}; //v5包含了初始值个数的元素,每个元素被赋予相应的初始值
vector<T> v5 = {a,b,c...}; //等价于v5{a,b,c...}
列表初始化vector对象
C++11标准还提供了另外一种为vector对象的元素赋初值的方法,即列表初始化。
vector<string> svec{"a","b","c"}; //列表初始化
vector<string> svec("a","b","c"); //错误
创建指定数量的元素
还可以用vector对象容纳的元素数量和所有元素统一初始值来初始化vector对象:
vector<int> ivec(10,-1); //10个int类型的元素,每个值被初始化为-1
值初始化
通常情况下,可以只提供vector对象容纳的元素数量而不用略去初始值。此时库会根据元素的类型来创建一个值初始化的元素初值,并把它赋给容器中的所有元素。
vector<int> ivec(10); //10个元素,每个都初始化为0
对这种初始化方式有两个特殊限制:
- 有些类要求必须明确提供初始值,如果vector对象中元素的类型不支持默认初始化,就必须提供初始值。
- 如果只提供元素的数量,而没有设定初始值只能使用直接初始化。
列表初始值还是元素数量?
在某些情况下,初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号。例如,用一个整数来初始化vector<int>时,整数的含义可能是vector对象的容量也可能是元素的值。通过花括号和圆括号可以区分上述含义:
vector<int> v1(10); //v1有10个元素,每个值都是0
vector<int> v2{10}; //v2有1个元素,该元素的值是10
vector<int> v3(10,1); //v3有10个元素,每个的值都是1
vector<int> v4{10,1}; //v4有2个元素,值分别是10和1
如果用的是圆括号(),可以说提供的值是用来构造vector对象的;如果用的是花括号{},表示进行列表初始化该对象。另一方面,如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑这样的值来构造vector对象了。
vector<string> v5{"hi"}; //列表初始化:v5有一个元素
vector<string> v6("hi"); //错误:不能使用字符串字面值构建vector对象
vector<string> v7{10}; //v7有10个默认初始化的元素
vector<string> v8{10,"hi"}; //v8有10个值为"hi"的元素
要想使用列表初始化vector对象,花括号的值必须与元素类型相同。
3.2 向vector对象中添加元素
vector提供了成员函数push_back来向其中添加元素。push_back负责把一个值当成vector对象的尾元素“压到(push)”vector对象的“尾端(back)”。
vector<int> v1; //空vector
for (int i = 0; i != 100; i++) {
v1.push_back(i); //依次把整数值放到v1的尾端
}
//循环结束后v1中有0-99共100个元素
向vector对象添加元素蕴含的编程假定
确保所写的循环正确无误,特别是在循环有可能改变vector对象容量的时候。如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。
==范围for语句体内不应该改变其所遍历序列的大小。==
3.3 其他vector操作
#include <iostream>
#include <vector>
using namspace std;
int main() {
vector<int> v{1,2,3,4,5,6,7,8,9};
for(auto &i : v) { //引用v中的每一个元素
i*=i; //求平方
}
for(auto i: v) { //对于v中的每个元素i
cout << i << " "; //输出i的值
}
cout << endl;
return 0;
}
#4. 迭代器的介绍
我们可以使用下标运算来访问string对象的字符或vector对象的元素,另外还有一种更通用的机制也可以实现同样的目的,使用迭代器。所有的标准库容器都可以使用迭代器,但是其中只有少数几种支持使用下标运算符。
4.1 使用迭代器
和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员:begin和end成员,其中begin成员负责返回指向第一个元素的迭代器,end成员负责返回指向容器“尾元素的下一位置”的迭代器。end成员返回的迭代器常被称作尾后迭代器。
==如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。==
迭代器运算符
*iter //返回迭代器所指元素的引用
iter->mem //解引用iter并获取该元素名为mem的成员,等价于(*iter).mem
++iter //令iter指示容器中的下一个元素
--iter //令iter指示容器中的上一个元素
iter1 == iter2 //判断两个迭代器是否相等,如果两个迭代器指示的是同一个元素或者它们是同一个容器的尾后迭代器,则相等;反之,不相等
iter1 != iter2
#include <iostream>
#include <string>
using namespace std;
int main() {
string s("hello world");
if (s.begin() != s.end()) {
auto it = s.begin();
*it = toupper(*it);
}
std::cout << s << std::endl;
return 0;
}
将迭代器从一个元素移动到令一个元素
迭代器使用递增(++)运算符来从一个元素移动到下一个元素。
==因为end返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用操作。==
迭代器类型
一般来说我们也不知道迭代器的精确类型。而实际上,那些拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器类型:
vector<int>::iterator it1; //it1只能读写vector<int>的元素
string::iterator it2; //it2只能读写string对象中的字符
vector<int>::const_iterator it3; //it3只能读元素,不能写元素
string::const_iterator it4; //it4只能读元素,不能写元素
begin和end运算符
begin和end返回的具体类型由对象是否是常量决定,如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator。
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); //it1的类型是vector<int>::iterator
auto it2 = cv.begin(); //it2的类型是vector<int>::const_iterator
有时候这种默认的行为并非我们所要。如果对象只需要读操作而无须写操作的话最好使用常量类型。为了便于得到const_iterator类型的返回值,C++11新标准引入了两个新函数,分别是cbegin和cend:
auto it3 = v.cbegin(); //it3的类型是vector<int>::const_iterator
结合解引用和成员访问操作
解引用迭代器可获得迭代器所指向的对象,如果该对象的类型恰好是类,就有希望进一步访问它的成员。例如,对于一个由字符串组成的vector对象来说,要检查其元素是否为空,令it是该vector的迭代器:
(*it).empty();
为了简化上述表达式,C++定义了箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起。it->mem和(*it).mem表达的意思相同。
const vector<string> text{"hello","world"};
for (auto it = text.cbegin(); it != text.cend() && !it->empty(); ++it) {
std::cout << *it << std::endl;
}
某些对vector对象的操作会使迭代器失效
已知的一个限制是不能在for循环中向vector对象添加元素。另外一个限制是任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。
==谨记,任何使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。==
4.2 迭代器运算
迭代器的递增运算令迭代器每次移动一个元素,所有的标准库容器都有支持递增运算的迭代器。
String和vector的迭代器提供了更多额外的运算符,一方面可使得迭代器的每次移动可跨过多个元素,另外也支持迭代器进行关系运算。所有的这些运算被称为迭代器运算。
迭代器的算术运算
可以令一个迭代器和一个整数值相加(或相减),其返回值是向前(或向后)移动了若干个位置的迭代器。获得指向vector中间元素的迭代器:
//计算得到最接近vi中间元素的一个迭代器
auto mid = vi.begin() + vi.size()/2;
使用迭代器运算
使用迭代器运算的一个经典算法是二分搜索。
//text必须是有序的
//beg和end表示我们搜索的范围
auto beg = text.begin(),end = text.end();
auto mid = text.begin() + (end - beg)/2;//初始化状态下的中间点
//当还有元素尚未检查并且我们还没有找到sought时执行循环
while(mid != end&&*mid != sought) {
if(sought < *mid) {
end = mid;
}else {
beg = mid + 1;
}
mid = beg + (end - beg)/2;
}
#5. 数组
数组是一种类似于标准库类型vector的数据结构,但是在性能和灵活性的权衡上又与vector有所不同。与vector类似的地方是,数组中存放的是同类型的元素;与vector不同的是,数组大小固定不变。
5.1 定义和初始化内置数组
数组是一种复合类型。数组的声明形式如a[d],其中a是数组名,d是数组的维度。数组的维度必须为常量表达式:
unsigned cnt = 42;
constexpr unsigned sz = 42; //常量表达式
int arr[10]; //含有10个整数的数组
int *parr[sz]; //含有42个指针的数组
string bad[cnt]; //错误:cnt不是常量表达式
string strs[get_size()]; //当get_size是constexpr时正确,否则错误。
显示初始化数组元素
可以对数组元素进行列表初始化,此时允许忽略数组的维度。如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来;相反,如果指明了维度,那么初始值的总数量不应该超出指定的大小。
const unsigned sz = 3;
int ial[sz] = {0,1,2}; //含有3个元素的数组,元素值分别为0,1,2
int a2[] = {0,1,2}; //数组维度为3
string a3[3] = {"hello","c++"}; //等价于a3[] = {"hello","c++",""};
int a4[2] = {0,1,2}; //错误:初始值过多
字符数组的特殊性
字符数组有一种额外的初始化形式,我们可以使用字符串字面值对此类型的数组初始化。当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去:
char c1[] = {'c','+','+'}; //列表初始化,没有空字符
char c2[] = {'c','+','+','\0'}; //列表初始化,含有显示空字符
char c3[] = "c++"; //自动添加表示字符串结束的空字符
char c4[6] = "Daniel"; //错误:没有空间存放空字符
不允许拷贝和赋值
不能将数组的内容拷贝给其他的数组作为初始值,也不能用数组为其他数组赋值:
int a[] = {0,1,2}; //含有3个整数的数组
int a2[] = a; //错误:不允许使用一个数组初始化另一个数组
a2 = a; //错误:不允许把一个数组直接赋值给另外一个数组
理解数组的复杂声明
和vector一样,数组能存放大多数类型的对象。又因为数组本身是对象,所以允许定义数组的指针和引用。
int *ptrs[10]; //ptrs是含有10个整形指针的数组
int &refs[10] = /*?*/; //错误:不存在引用的数组
int (*parray)[10] = &arr; //parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
int *(&arr)[10] = ptrs; //arr是数组的引用,该数组含有10个指针
==要想理解数组声明的含义,最好的办法是从数组的名字开始按照从内向外的顺序阅读。==
5.2 访问数组元素
数组的元素也能使用范围for语句或下标运算符来访问。在使用数组下标时,通常将其定义为size_t类型。
检查下标的值
与vector类似,使用下标访问时,需要检查下标的值。避免出现下标越界。
5.3 指针和数组
在使用数组的时候编译器一般会把它转换成指针。对数组元素使用取地址符就能得到指向该元素的指针:
string nums[] = {"one","two","thress"};
string *p = &nums[2]; //p指向nums的第三个元素
string *p2 = nums; //等价于p2 = &nums[0];
==在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。==
指针也是迭代器
允许使用递增运算将指向数组元素的指针向前移动到下一个位置:
string nums[] = {"one","two","three"};
string *e = &nums[nums->size()]; //指向尾元素的下一位置的指针
std::cout << *e << std::endl;
for (string *s = nums; s != e; s++) {
std::cout << *s << std::endl;
}
std::cout << std::endl;
标准库函数begin和end
C++新标准引入了两个名为begin和end的函数。begin函数返回指向数组首元素的指针,end函数返回指向数组尾元素下一位置的指针。
int iarr[] = {1,2,3,4,5};
int *beg = begin(iarr); //指向iarr首元素的指针
int *end = end(iarr); //指向iarr尾后元素的指针
指针运算
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的尾元素arr[4]
解引用和指针运算的交互
指针加上一个整数所得的结果还是一个指针。
int arr[] = {0,2,4,6,8};
int last = *(arr + 4); //正确,把last初始化成8,即arr[4]的值
下标和指针
int ia[] = {1,2,3,4,5};
int i = ia[2]; //ia转换成指向数组首元素的指针,ia[2]得到(ia+2)所指的元素
int *p = ia; //p指向ia的首元素
i = *(p+2); //等价于i = ia[2]
5.4 C 风格字符串
字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的C风格字符串。
C标准库String函数
strlen(p); //返回p的长度,空字符不计算在内
strcmp(p1,p2); //比较p1和p2的相等性,如果p1==p2,返回0;如果p1>p2,返回一个正值;如果p1<p2,返回一个负值
strcat(p1,p2); //将p2附加到p1之后,返回p1
strcpy(p1,p2); //将p2拷贝给p1,返回p1
5.5 与旧代码接口
混用string对象和C风格字符串
string s("hello world"); //s的内容是hello world
char *str = s; //错误:不能用string对象初始化char*
const char *str = s.c_str(); //正确
==如果执行完c_str()函数后程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。==
使用数组初始化vector对象
不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用vector对象初始化数组。相反的,允许使用数组来初始化vector对象。
int int_arr[] = {0,1,2,3,4,5};
//ivec有6个元素,分别是int_arr中对应元素的副本
vector<int> ivec(begin(int_arr),end(int_arr));
#6. 多维数组
多维数组即数组的数组。当一个数组的元素仍然是数组时,通常使用两个维度来定义它:一个维度表示数组本身的大小,另外一个维度表示其元素大小:
int ia[3][4]; //大小为3的数组,每个元素是含有4个整数的数组
多维数组初始化
允许使用花括号括起来的一组值初始化多维数组:
int ia[3][4] = { //3个元素,每个元素是大小为4的数组
{0,1,2,3}, //第1行初始值
{4,5,6,7}, //第2行初始值
{8,9,10,11} //第3行初始值
};
其中内层嵌套着的花括号并非必需的:
//没有标识每行的花括号,与之前的初始化语句是等价的
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
多维数组的下标引用
可以使用下标运算符来访问多维数组,此时数组的每个维度对应一个下标运算符。
constexpr size_t rowCnt = 3,colCnt = 4;
int ia[rowCnt][colCnt]; //12个未初始化的元素
//对于每一行
for(size_t i = 0;i != rowCnt;++i) {
//对于行内的每一列
for(size_t j = 0;j != colCnt;j++) {
//将元素的位置索引作为它的值
ia[i][j] = i*colCnt + j;
}
}
使用范围for语句处理多维数组
size_t cnt = 0;
for(auto &row:ia) { //对于外层数组的每一个元素
for(auto &col:row) { //对于内存数组的每一个元素
col = cnt; //将下一个值赋值给该元素
++cnt; //将cnt加1
}
}
指针和多维数组
当程序使用多维数组名时,编译器会将其转换成指向数组首元素的指针。
int arr[3][4];
int (*p)[4] = arr; //p指向arr的首元素,首元素为含有四个整数的数组(数组指针)
p = &arr[2]; //p指向ia的尾元素
==在上述声明中,圆括号比不可少:==
int *ip[4]; //整形指针的数组
int (*ip)[4]; //指向含有4个整数的数组的指针
类型别名简化多维数组的指针
using int_arr = int[4];
for(int_arr *p = ia;p != ia + 3; ++p) {
for(int *q = *p;q != *p + 4; ++q) {
cout << *q << '';
}
cout << endl;
}