C++primer 第3章 字符串,向量和数组
- string支持可变长字符串
- vector支持可变长集合
3.1命名空间的using声明
::
:作用域操作符
头文件不应包含using声明
- 一般来说不应该使用using声明.这是因为头文件的内容会拷贝到所有引用它的头文件中去.
3.2标准库类型string
3.2.1 定义和初始化string对象
string s1;
string s2 = s1;
string s3 = "hiya";
string s4(10,'c');
直接初始化和拷贝初始化
- 使用
=
进行初始化实际上是在执行的是拷贝初始化,反之是直接初始化
string s8 = string(10,'c');
// 拷贝初始化,创建一个临时对象然后进行拷贝初始化
3.2.2 string对象上的操作
读写string对象
string s;
cin>>s;
- 在执行读取操作时,string对象会自动忽略开头的空白并从第一个真正的字符开始读起,知道遇到下一个空白符为止.
读取未知数量的string对象
string word;while (cin >> word) {cout << word << " ---- " << endl;}
使用getline读取一整行
- 有时希望能在最终得到的字符串中保留输入时的空白符,这是就应该使用
getline
来代替原来的>>
运算符. - getline(inputstream,string)
#include"Sales_data.h"
#include<iostream>
#include<string>
int main()
{string word;while (getline(cin,word)) {cout << word << " ---- " << endl;}return 0;
}
string的empty和size操作
string::size_type类型
- size函数返回的是
string::size_type
类型的值,这是string定义的几种配套类型之一,这些配套类型体现了标准库类型与机器无关的特性.他可能是一个unsigned类型,因此切记使用负数和size_type做比较.
如果一条表达式中已经有了size()函数就不要再使用int了,这样就可以避免混用int和unsigned可能带来的问题
比较string对象
为string对象赋值
两个string对象相加
字面值和string对象相加
- 当把string对象和字符字面值集字符串字面值混在一条语句中使用时,必须确保每个
加法运算符的两侧
的运算对象至少有一个是string
string s1 = "1";string s2 = s1 + ',';string s3 = "hello" + ","; //error 两个运算对象都不是stringstring s4 = "hello" + ", " + s2; //erro:不能把字面值直接相加string s5 = s1 + "112" + ","; //正确,每个加法运算符都有一个运算对象是string
为了与C兼容,字符串字面值并不是标准库类型的string对象.即字符串字面值与string是不同的类型
3.2.3 处理对象中的字符
处理每个字符? 使用基于范围的for语句
- 范围for语句:
string str("some string");for(auto c:str)cout<<c<<endl;
练习1:统计字符串中标点符号的个数
string str("some string!!!!");decltype(str.size()) punct_cnt = 0;for(auto c:str){if(ispunct(c)){punct_cnt++;}}cout<<"字符串中标点符号的个数: "<<punct_cnt<<endl;
使用范围for语句改变字符串中的字符
- 如果想改变string对象中字符的值,必须把循环变量定义成引用类型
只处理一部分字符
- 要想访问string对象中的单个字符有两种方式
- 使用下标
- 使用迭代器 - 下标运算符
[]
:接受的输入参数是string::size_type
类型的值,这个参数表示要访问的字符的位置,返回值是该位置上字符的引用 - 第一个字符:s[0]; 最后一个字符:s[s.size()-1]
使用下标执行迭代
使用下标执行随机访问
3.3标准库类型vector
- vector表示对象的集合,其中所有对象的类型都相同
#incldue<vector>
3.3.1 定义和初始化vector
列表初始化vector对象
vector<string> articles = {"za","b"};
初始化的方式
- 使用拷贝初始化时(即使用=),只能提供一个初始值(2.2.1节 39页)
- 如果提供的是一个类内初始值,则只能使用拷贝初始化或者使用花括号的形式初始化(3.2.1 76页)
- 如果提供的是元素值的列表,则只能把初始值放在花括号里进行列表初始化,不能放在圆括号里.
vector<string> v1{"1","2"};
vector<string> v2("1","2"); //error 列表初始化只能使用花括号
创建指定数量的元素
值初始化
- 只提供元素的个数
- 如果vector对象的元素是内置类型,比如int则元素初始值自动设为0.如果元素是某种类类型,比如string,则元素有类默认初始化
vector<int> ivec(10); // 每个元素初始化为0
vector<string> svec(10); //每个元素初始化为空的试听对象
- 对于此种初始化的两种限制:
1. 有些类必须明确提供初始值(2.2.1 40页)
2. 如果只提供了元素的数量没有提供初始值,只能使用直接初始化
vector<int> vi = 10; //error 必须使用直接初始化的形式指定向量大小
列表初始值还是元素数量
- 在某些情况下,初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号
vector<int> v1(10); // v1有10个元素,每个元素的值都是0vector<int> v2{10}; //v2有1个元素,该元素的值是10;vector<int> v3(10,1);//v3有10个元素,每个元素都是1vector<int> v4{10,1};//v4有两个元素,只分别为10,1
- 如果用的是圆括号,可以说提供的值是用来构造vector对象的
- 如果用的是换括号,可以表述成我们想列表初始化噶vector对象
- 但是,如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化,就要考量用这样的值来构造vector对象了
vector<string> v5{"hi"}; //列表初始化vector<string> v6("hi"); //erro,不能使用字符串字面值构建vector对象vector<string> v7{10}; //v7有10个默认初始化的元素vector<string> v8{10,"i"};//v8有10个值为"hi"的元素
3.3.2 向vector对象中添加元素
向vector对象中添加元素蕴含的编程假定
如果循环内部包含有向vector对象添加元素的语句,则不能使用范围for循环,即范围for语句体内不应包含改变其所遍历序列的大小
3.3.3 其他vector操作
要使用size_type,需要首先指定他是由哪种类型定义的.vector对象的类型总是包含着元素的类型.vector<int>::size_type----正确,vector::size_type----错误
- 只有当元素的值可比较时,vector对象才能被比较.例如string确实定义了自己的相等运算符和关系运算符.而一些其他类则没有定义,则不能相互比较
计算vector内对象的索引
不能用下标形式添加元素
vector<string> v5;for(decltype(v5.size()) ix=0; ix!=10;ix++)v5[ix] = ix; //验证错误
通过下标访问不存在的元素会导致缓存区溢出,确保下标合法化的一种有效手段就是尽可能的使用范围for语句
3.4迭代器介绍
- 所有标准库容器都可以使用迭代器,但是其中只有少数几种才能同时支持下标运算符
3.4.1 使用迭代器
- 获取迭代器,有迭代器的类型同时拥有返回迭代器的成员.比如,这些类型都拥有名为begin和end的成员,其中begin成员返回指向第一个元素的迭代器.end返回指向容器尾元素的下一个位置的迭代器,也就是该迭代器返回容器本不存在的尾后元素
- 如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器
迭代器运算符
string s("some string!");if(s.begin() != s.end()){auto it = s.begin();*it = toupper(*it);}cout<<s<<endl;
将迭代器从一个元素移动到另外一个元素
所有的标准库容器的迭代器都定义了==和!=,但是他们中的大多数都没有定义<运算符.因此,只要我们养成了使用迭代器和!=的习惯,就不用太在意用的是哪种容器类型
迭代器类型
- 就像不知道容器的size_type成员一样,一般我们也不知道迭代器的精确类型,而实际上,那些拥有迭代器的标准库类型使用
iterator
和const_iterator
来表示迭代器的类型
vector<int>::iterator it; //可读写vector<int>中的元素string::iterator it2; // 可读写string对象中的字符vector<int>::const_iterator it3; //只能读string::const_iterator it4;
- 如果容器对象是一个常量,只能使用const_iterator;如果容器对象不是常量,那么两种迭代器类型均可以使用
begin和end运算符
- begin和end返回的具体类型有对象是否是常量决定
- 如果对象只需多操作而无需写操作的话最好使用常量类型(const_iterator).为了专门得到const_iterator类型的返回值,C++11标准引入两个新函数,
cbegin
和cend
auto it3 = v.cbegin() //it3的类型是vector<int>::const_iterator
- 无论v是否是常量,cbegin和cend均返回const_iterator
结合解引用和成员访问操作
(*it).empty() // 解引用it,然后调用结果对象的empty成员
*it.empty() //error,试图访问it的名为empty的成员//为了简化上述操作操作,引入->
it->empty()
某些对vector对象的操作会使迭代器失效
虽然vector可以动态增长,但是存在一下两个限制
- 不能在范围for循环忠厚向vector对象添加元素
- 任何一种可能改变vector对象容量的操作比如push_back,都会使该vector对象的迭代器失效
注意:凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素
3.4.2 迭代器运算
迭代器的算数运算
- 假设it和mid是同一个vector对象的两个迭代器,可以使用下面的代码来比较他们所指的位置的术前术后
if(it < mid)//处理vi前半部分的元素
- 所谓两个迭代器之间的距离,其类型名为
difference_type
的带符号整型数.
使用迭代器运算
3.5数组
- 与vector不同的是,数组的大小确定不变.
如果不知道元素的确切个数,使用vector
3.5.1定义和初始化内置数组
- a[n];n表示数组的维度,编译的时候维度应该是已知的,也就是说维度必须是一个常量表达式.
unsigned cnt = 42; //不是常量表达式constexpr unsigned sz = 42; //常量表达式int *parr[sz];string bad[cnt]; //error,cnt不是常量表达式string strsp[get_size()]; //当get_size()是constexpr是正确,否则错误
默认情况下,数组的元素被默认初始化,和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值
- 定义数组必须知道数组的类型,不容许使用auto关键字来由初始值的列表来推断.
- 数组的元素应该为对象,因此不存在引用的数组.
显示初始化数组元素
字符数组的特殊性
- 字符数组有一种额外的初始化的方式,可以使用字符串字面值.当使用这种方式初始化时,一定要记住字符串字面值结尾处还有一个空字符
const char a[6] = "Daniel"; //error,没有空间可以存放空字符
不允许拷贝和赋值
- 不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组作为其他数组赋值
int a[] = {1,2,3};
int a2 = a; //error,不允许1使用一个数组初始化另外一个数组
a2 = a;//不能吧一个数组直接赋值个另外一个数组
理解复杂的数组声明
int arr[10];int *ptrs[10]; // ptrs是含有10个整型指针的数组int &refs[10]; //error,不存在引用的数组int (*Parray)[10] = &arr; //Parray指向一个含有10个整数的数组,用法在下面int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组,用法在下面int a[10] = {1,2,3};int (&ptr)[10] = a; //相当于a的别名是ptrcout<<ptr[0]<<endl;int a[10] = {1,2,3};int (*ptr)[10] = &a;cout<<(*ptr)[0]; // === a[0]int a[2][10] = {1,2,3};int (*ptr)[10] = a;
习题
int arr[10];int *ptrs[10]; // ptrs是含有10个整型指针的数组int &refs[10]; //error,不存在引用的数组int (*Parray)[10] = &arr; //Parray指向一个含有10个整数的数组int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
- 数组与vector的比较
- 数组的维度不可以改变
- 数组的维度通过
sizeof(arr)/sizeof(arr[0])
计算,vector可以通过size()可以直接获得
3.5.2访问数组元素
- 数组的维度定义为:
size_t
检查下标的值
3.5.3指针和数组
- 在一些情况下,数组的实际操作实际上是指针的操作
int ia[] = {1,2,3,4};
auto ia2(ia); //ia2是一个整型指针,指向ia的第一个元素
//本质上等价于: auto ia2(&ia[0]);
- 当使用decltype关键字时上述转换过程不会发生.decltype(ia)返回的类型有10个整数构成的数组
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9}; //ia3是一个含有10个整数的数组即:int[10]
ia3 = p; //error,不能用整型指针给数组赋值
标准库函数begin和end
- 注意整个函数与容器中的两个同名成员功能类似,不过数组毕竟不是类类型,因此两个函数不是成员函数
int iap = {0,1,2,3,4,5,6};
int *beg = begin(ia);
int *last = end(ia);//指向arr尾元素的下一位置的指针
指针也是迭代器
尾后指针不能执行解引用和递增操作
指针运算
auto n = end(arr) - begin(arr); //arr中的元素的数量
- 两个指针相减的结果是名为
ptrdiff_t
的标准库类型因为差值可能为负值,所以ptrdiff_t是一种带符号类型
解引用和指针运算的交互
下标和指针
int *p = &ia[2];
int j = p[1];
int k = p[-2]; //p[-2] 是ia[0]表示的元素
- 虽然标准库可惜string和vector也能执行下标运算,但是它限定使用的下标必须是无符号类型,而内置的下标运算无此要求.
int a[] = {1,2,3};int * p1 = &a[1];int *p2 = &a[2];cout<<"before p1 : "<<*p1<<endl; // 2cout<<"before p2 : "<<*p2<<endl; // 3p1+=p2-p1;cout<<"after p1 : "<<*p1<<endl;//3cout<<"after p2 : "<<*p2<<endl;//3
如果p1和p2指向同一个数组,p1+=p2-p1会导致p1最终指向p2
3.5.4C风格的字符串
最好不要使用C风格的字符串
- C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法,按此习惯书写的字符串存放在数组并以空字符结束.
C标准String函数
使用上述函数的字符串必须指向以空字符作为结束的数组
char ca[] = {'c','2'};
cout<<strlen(ca); // error,ca没有以空字符结束
比较字符串
- 比较标准库string对象的时候,用的是普通的关系运算符和相等性运算符
- 如果将这些运算符用在两个c风格字符串上,实际比较的将是指针而非字符串本身,当使用数组的时候真正使用的是指向数组首元素的指针.
const char c1[] = "1233344";
const char c2[] = "{";
if(c1<c2) // 未定义的:试图比较两个无关地址
目标字符串的大小由调用者指定
3.5.5 与旧代码的接口
混用string对象和C风格字符串
- 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值
- 在string对象的加法运算中运行使用以空字符结束的字符数组作为其中一个运算对象但是不能两个运算对象都是.
- 允许使用以空字符结束的字符数组给string对象赋值
- 但是,不能使用string对象直接初始化指向字符的指针,可以通过
c_str
完成
char *str = s; // error;
const char *str = s.c_str();
c_str()
函数的返回值是一个C风格的字符串,也就是说函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组.结果指针的类型是cosnt char *
- 由于我们无法保证c_str返回的数组一直有效,实际上,如果后续的操作改变了s的值就可能让之前返回的数组失去效用.
如果执行完c_str()函数后后续程序想一直使用返回的数组,最好将该数组重新拷贝一份.
使用数组初始化vector对象
- 不允许使用一个数字为一个内置类型的数组赋初值,也不容许使用vector对象初始化数组.但是相反的,允许数组初始化vector对象.
vector<int> ivec(begin(arr),end(arr));
3.6 多维数组
- 所谓多维数组本质上是数组的数组
int ia[3][4];
//大小为3的数组,每个元素是含有4个整数的数组int arr[10][20][30] = {0};
//大小为10的数组,它的每个元素都是大小为20的数组,这些数组的元素是含有30个整数的数组
多维数组的初始化
//方式一
int ia[3][4] = {
{0,1,2,3},
{4,5,6,7},
{8,9,10,11}
};//方式二:
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
//显示的初始化每行的首元素,其他未列的元素执行默认初始化
int ia[3][4] = {{0},{4},{8}}//显示的初始化第1行,其他元素执行默认初始化
int ix[3][4] = {0,1,2,3}
多维数组的下标引用
int (&row)[4] = ia[1];
//把row绑定到ia的第二个4元素数组上.
使用范围for语句处理多维数组
size_t cnt = 0;
for(auto &row:ia){for(auto &col: row){col = cnt;++cnt;}
}
错误
for(auto row:col){for(auto col:row){}
}
- 上述程序无法通过编译,这是因为,第一个循环变量ia的所有元素,这些元素实际上是大小为4的数组.因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素转换成指向该数组内首元素的指针.这样得到的row的类型是int*,显然内层的循环就不合法了,编译器将试图在一个int*内遍历.
要使用范围for语句处理多维数组,处理内层的循环外,其他所有循环的控制变量都应该是引用类型
- 上面程序的正确的形式为:
for(auto &row:col){for(auto col:row){}
}
指针和多维数组
当程序使用多维数组的名字时,也会将其转换成指向数组首元素的指针.定义指向多维数组的指针时,别忘了这个多维数组实际上是数组的数组
int ia[3][4];
int (*p)[4] = ia; //p指向了含有4个整数的数组
p = &ia[2]; //p指向了ia的尾元素
多维数组的遍历
- 遍历一:
for(auto p = ia;p!=ia+3;++p){//o指向含有4个整数的数组for(auto q =*p;q!=*p+4;q++){cout<<*q;}cout<<endl;
}
- 遍历二:
for(auto p =begin(ia);p!=end(ia);++p){//p指向ia的第一个数组for(auto q=begin(*p);q!=end(*p);++q)
}
类型别名简化多维数组的指针
- 将4个整数组成的数组命名
using int_array = int[4];
typedef int int_array[4];for(int_array *p = ia;p!=ia+3;++p){for(int *q=*p;q!=*p+4;++q)cout<<*q;
}
发布评论