C++ Primer 学习笔记(第三章:字符串、向量和数组)

##C++ Primer 学习笔记(第三章:字符串、向量和数组)git


[TOC]express


###3.1 命名空间的using声明数组

  1. using声明语句能够一行放多条。安全

  2. 位于头文件的代码,通常来讲不该该使用using声明。由于其内容会拷贝到每一个使用该头文件的文件中,可能会产生名字冲突。函数


###3.2 标准库类型string性能

  1. 使用string类型必须首先包含string头文件,做为标准库的一部分,string定义在命名空间std中。学习

  2. 定义和初始化string对象spa

string s1;//默认初始化,空串
string s2(s1);//s2是s1副本
string s2 = s1;//和上一条等价
string s3("value");//s3是字面值副本,除了字面值最后那个空字符外,直接初始化
string s3 = "value";//与上一条相同,拷贝初始化
string s4(n,'c');//把s4初始化为由连续n个字符c组成的串,直接初始化
  1. string上的操做
os<<s;//输出流
is>>s;//读取字符赋给s,字符串以空白分隔,返回is(自动忽略空白)
getline(is,s);//从is中读取一行赋给s,返回is
s.empty();//为空返回true
s.size();//字符个数
s[n];//返回第n个字符的引用
s1 + s2;//链接
s1 = s2;//用s2的副本代替s1
s1 == s2;s1 != s2;//相等性判断,对字母大小写敏感

<, <=, >, >+;字典序比较,对大小写敏感指针

  1. getline(is,s) 参数是一个输入流和一个string对象,从给定的输入流读入,直到遇到换行符为止,并把所读内容(不含换行符)存入到string对象中。code

  2. string::size_type类型 size函数返回的就是一个string::size_type类型的值,是一个无符号类型 可使用auto或者decltype来推断变量类型:

auto len = line.size();

因为无符号数和带符号数混用会产生其余问题,若是一个表达式中有size就不要再用int了。

  1. 比较string对象 依照字典顺序: (1)若是公共部份内容相同,则较短的小于较长的。 (2)若是存在不一致,则以第一对相异字符比较的结果为准。

  2. 在使用相加运算符时,必须确保每一个加法运算符(+)的两侧运算对象至少一个是string

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

切记:字符字面值('\n')和字符串字面值("word"string是不一样类型,能够用来初始化或相加的缘由是标准库容许把字符字面值和字符串字面值转换成string对象。

  1. 处理string对象中的字符,在cctype头文件中定义了一组标准库函数。
isalnum(c);//c是字母或数字
isalpha(c);//c是字母
iscntrl(c);//c是控制字符
isdigit(c);//c是数字
isgraph(c);//c不是空格但可打印
islower(c);//c为小写字母
isprint(c);//c是可打印字符(即c是空格或c具备可视形式)
ispunct(c);//c是标点符号(即c不是控制字符、数字、字母、可打印空白中的一种)
isspace(c);//c是空白(即c是空格、横向制表符、纵向制表符、回车符、换行符、进纸符中的一种)
isupper(c);//c是大写
isxdigit(c);//c是十六进制数字
tolower(c);//把c变小写
toupper(c);//把c变大写
  1. C++标准库兼容了C的标准库,将name.h命名为cname,并去掉了后缀。即cctypectype.h的内容是同样的。 在名为cname的头文件中定义的名字从属于命名空间std,而定义在.h头文件中则否则。 通常来讲C++程序应该使用cname的头文件而不使用.h形式,标准库中的名字总能在std命名空间中找到。

  2. 遍历string中的每一个字符使用范围for语句(C++11):

for (declaration : expression)//这种遍历只能处理元素,不能改变内容
	 statement

expression:一个对象,用于表示一个序列 declaration:定义一个变量,用于访问序列中的基础元素,每次迭代会被初始化为expression部分的下一个元素值 example1:

string str("some string");
for(auto c : str)
	cout << c << endl;

example2:

string s("Hello World!!!");
decltype(s.size()) punct_cnt = 0;
for(auto c : s)
	 if(ispunct(c))
		 ++punct_cnt;
cout << punct_cnt << endl;
  1. 若是想改变对象中的元素,使用范围for必须把循环变量定义成引用类型。
string s("Hello World!!!");
for (auto &c : s)
	 c = toupper(c);
cout << s << endl;
  1. 访问string对象中的单个字符有两种方式:下标和迭代器。 下标运算符[]接受的输入参数是string::size_type类型值,返回该位置上字符的引用。 s[s.size() - 1]是最后一个字符 在访问指定字符时必定要首先检查字符串对象是否为空。(s.empty()) 只要字符串不是常量,就能为下标运算符返回的字符赋新值。 例子:(第一个单词大写)
for(decltype(s.size()) index = 0;
	 index != s.size() && !isspace(s[index]);
	 ++index)
	 s[index] = toupper(s[index]);

###3.3 标准库类型vectorvector表示对象的集合,也被称为容器)

  1. 模板自己不是类或函数,相反能够讲模板当作编译器生成类或函数编写的一份说明。编译器根据模板建立类或函数的过程称为实例化。

  2. vector能容纳绝大多数类型的对象做为其元素,可是由于引用不是对象,因此不存在包含引用的vector

  3. 定义和初始化:

vector<T> v1;//默认初始化,即空
vector<T> v2(v1);//v2包含v1全部副本,这必须保证T一致
vector<T> v2 = v1;//与(2)等价
vector<T> v3(n, val);//n个重复元素val
vector<T> v4(n);//n个重复执行初始化的对象,初始值由元素类型决定(类型T必须支持默认初始化)
vector<T> v5{a, b, c...};//就是这些元素构成的向量,列表初始化(必须是大括号{})
vector<T> v5 = {a, b, c...};//等价于上面
vector<int> v1(10);vector<int> v2{10};
vector<int> v3(10, 1);vector<int> v4{10, 1};

圆括号是构造对象,花括号是列表初始化。 直接初始化只适用于:初始值已知且数量不多、初始值是另外一个vector的副本、全部元素初始值都同样。

  1. 若是循环体内包含有向vector对象添加元素的语句,则不能使用范围for循环(第五章)。

  2. vector的操做:

v.empty();//为空
v.size();//返回v中元素的个数,类型为vector<T>::size_type
v.push_back(t);//添加元素
v[n];//返回位置上的引用,下标类型是vector<T>::size_type
v1 = v2;//拷贝替换
v1 = {a, b, c...};//列表拷贝替换
v1 ==(!=) v2;//判断相等
<, <=, >, >=;//字典序比较。若是容量不一样,可是相同部分一致,则元素少的较小;其他以第一对相异元素比较结果为准。(元素的类型必须支持比较,不然不能比较)
  1. vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。试图用下标方式访问一个不存在的元素会引起错误,不过这种错误不会被编译器发现。 而确保下标合法的一种有效手段就是尽量使用范围for语句。

  2. 能够巧妙地使用退格键:

if(!v.empty())
	 cout<<"\b";

来覆盖多余的输出。(好比cout << a[i] << ",";,到了最后一个元素能够覆盖掉)


###3.4 迭代器介绍

  1. 全部标准库容器(vector)均可以使用迭代器,可是其中只有少数几种才同时支持下标运算符。严格来讲string对象不属于容器类型,可是string支持不少与容器相似的操做,例如和vector同样都支持下标和迭代器。

  2. 迭代器相似指针类型,都提供了对象的间接访问,也有有效和无效之分。有效地迭代器或者指向某个元素,或者指向容器中尾元素的下一位置,其余全部状况都属于无效。

  3. 有迭代器的类型都有返回迭代器的成员:beginendbegin返回指向第一个元素(或字符)的迭代器;end返回指向容器(或string对象)尾元素的下一位置(也被称做尾后迭代器)。 若是容器为空,则它俩返回同一个迭代器,都是尾后迭代器。(因此s.begin() != s.end()能够用来判断容器或字符串是否非空) 一般用auto来定义迭代器类型:auto b = v.begin();

  4. 迭代器运算符:

*iter;//返回所指元素引用(解引用),注意尾后迭代器没法解引用
iter->mem;//解引用并获取mem成员((*iter).mem)
++iter;//下一元素
--iter;//上一元素
iter1 == iter2;iter1 != iter2//判断两个迭代器相不相等,只有指示的是同一元素或同一容器的尾后迭代器才相等
  1. end返回的迭代器并不实际指示某个元素,因此不能对其进行递增或解引用操做。

  2. 循环中:迭代器用!=,下标用<。 标准库容器所有定义了==!=,可是大多数都没有定义<或者不支持下标运算符。所以要养成使用迭代器和!=的习惯。

  3. 和下标类型(::size_type)相对应,标准库的迭代器类型为iteratorconst_iteratoriterator可读可写,const_iterator相似常量指针,只读。 若是string或者vector对象是一个常量,只能使用const_iterator

  4. 若是对象是常量,那么此时的.begin().end()也是const_iterator类型;若是不是常量返回iterator。 若是对象只需读操做无须写操做最好使用常量类型(const_iterator),这时可以使用cbegincend。(C++11)不管对象是不是常量都将返回const_iterator

  5. C++的箭头运算符把解引用和成员访问两个操做结合一块儿。 即it->mem(*it).mem相同

  6. 一些限制: 不能在范围for循环向vector对象添加元素。 任何一种可能改变vector对象容量的操做(push_back),都会使该vector对象的迭代器失效。即但凡是使用了迭代器的循环体都不要向迭代器所属的容器添加元素。

  7. vectorstring迭代器支持的运算:

iter + n;//仍获得一个迭代器,位置向前移动n个元素
iter - n;//仍获得一个迭代器,位置向后移动n个元素
iter += n; iter -= n;
iter1 - iter2;//两个迭代器之间的距离。参与运算的两个迭代器必须指向同一容器中的元素或者下一元素。

这个获得的类型为difference_type的带符号整型,由于它可正可负。(注意迭代器之间的加法是没有意义的)

>, >=, <, <=;//前面小,后面大。参与运算的两个迭代器必须指向同一容器中的元素或者下一元素。

  1. 利用迭代器进行二分查找:
// text必须有序,查找的内容为sought
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg) / 2;
while(mid != end && *mid != sought){
	 if(sought < *mid)
		 end = mid;
	 else
		 beg = mid + 1;
	 mid = beg + (end - beg) / 2;//这里不用(beg + end) / 2的缘由是没有定义迭代器加法
}

###3.5 数组

1. 数组与`vector`:
类似:存放类型相同对象的容器,对象自己没有名字,须要经过位置访问。
不一样:数组大小肯定不变,不能随意添加元素。运行时性能较好,但牺牲了灵活性。

2. 数组是复合类型,其维度也属于数组类型的一部分,编译的时候必须已知。
也就是说维度必须是一个常量表达式。(`const`、`constexpr`)

```cpp
unsigned cnt = 42;
string bad[cnt];//错误
string strs[get_size()];//当get_size()是constexpr时正确,不然错误
  1. 数组定义时必须指定数组类型,不容许用auto由初始值列表推断。另外,和vector同样,数组的元素应为对象,不存在引用的数组。

  2. 对数组的元素进行列表初始化。 容许忽略数组维度,编译器会根据初始值数量计算并推测出维度; 若是指明了维度,而初始值总数量少于维度,则剩下元素将被默认初始化。

int a1[] = {1, 2, 3};维度是3
int a2[5] = {1, 2, 3};//1,2,3,0,0
  1. 须要注意字符数组能够用字符串字面值来初始化,此时尾部有一个空字符'\0'要计算在内。
char a1[] = {'C', '+', '+'};
char a2[] = {'C', '+', '+', '\0'};
const char a3[4] = "C++";
  1. 数组不容许拷贝和赋值。
int a1[] = {0, 1, 2};
int a2[] = a1;//错误
a2 = a1;//错误
  1. 复杂的数组声明:数组与指针引用。
int *ptrs[10];//ptrs是含有10个整型指针的数组(从右向左依次绑定,首先是一个大小为10的数组,名字是ptrs,数组存放的类型为int*)
int &refs[10] = /*?*/;//错误,不存在引用的数组
int (*ptrs)[10] = &arr;//ptrs指向一个含有10个整型的数组(从内向外绑定,ptrs是一个指针,它指向一个大小为10的数组,数组存放的类型是整型)
int (&refs)[10] = arr;//refs引用一个含有10个整型的数组(也是从内向外)
int *(&arry)[10] = ptrs;//arry是一个数组的引用,该数组有10个指针
  1. 使用数组下标时,一般将其定义为size_t类型,在cstddef头文件中定义。

  2. stringvector同样,当须要遍历数组全部元素时,最好的办法也是使用范围for

for(auto i : scores)
	 cout << i << " ";
cout << endl;

范围for最大的好处是能够减轻人为控制遍历过程的负担。

  1. 在类内定义一个数组必定要初始化,不然其值是未定义的(undefined):
a[10] = {};
  1. 在使用数组名字的地方,编译器会自动地将其替换为一个指向数组首元素的指针。
int ia[] = {1, 2, 3, 4, 5};
auto ia2(ia);//ia2是一个整型指针,至关于auto ia2(&ia[0])
  1. 必须注意,使用decltype时就不会发生上面的状况:
decltype(ia) ia3 = {0, 1, 2};
ia[2] = 3;
decltype(ia)返回的类型是由10个整数构成的数组
  1. 指针和迭代器同样,指针支持迭代器的所有操做。 首元素:ia; 尾元素下一位置指针:int *e = &ia[5]; 尾后指针不指向具体元素,不能对尾后指针执行解引用或递增等操做。

  2. begin()end()函数,将数组做为参数获取首元素指针和尾后指针。(C++11

int *beg = begin(ia); int *last = end(ia);//定义在<iterator>头文件中
  1. 指针相减的结果类型为ptrdiff_t,也是定义在cstddef头文件中,它是一个带符号类型。

小结: size_t,数组下标类型。 different_type,迭代器之间距离。 ptrdiff_t,指针距离。

  1. 对数组执行下标计算至关于对指向数组元素的指针执行下标计算。 只要指针指向的是数组中的元素,均可以执行下标计算:
int *p = &ia[2];
int j = p[1];//至关于*(p+1)即ia[3]
int k = p[-2];//至关于*(p-2)即ia[0]
  1. C风格字符串(cstring):char[],字符串存放在数组中以空字符('\0')结束。 (1)函数
strlen(p);//返回p的长度,空字符不算在内
strcmp(p1, p2);//比较p1和p2,p1>p2返回正值
strcat(p1, p2);//p2附加到p1后,返回p1
strcpy(p1, p2);//将p2拷贝给p1,返回p1

传入此类函数的指针必须指向空字符做为结束的数组。 (2)比较字符串 不能按照标准库string类型对象直接比较大小的方法,由于使用数组时其实使用的是首元素指针,它们并不是指向同一对象,比较的结果将是未定义的。 应该使用strcmp函数。 (3)字符串拼接 标准库string只须要相加就能够。 cstring须要strcat()

strcpy(largeStr, ca1);
strcat(largeStr, " ");
strcat(largeStr, ca2);

可是这里在largeStr所需空间上不容易估算准确,容易致使安全泄露。

  1. 混用stringC风格字符串 (1)容许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。
char *ch = "I am";
string s = ch;

(2)在string对象的加法运算中容许使用以空字符结束的字符数组做为其中一个运算对象(但不能两个都是)。 (3)可是若是须要一个C风格字符串,没法直接用string对象代替它。解决的方法是使用stringc_str函数。

string s("Hello World");
char *str = s;//Wrong!
const char *str = s.c_str();//注意必须是const,c_str()返回一个常量。

若是执行完c_str()函数后程序想一直保留返回的数组,须要将数组拷贝一份,由于当s改变后,c_str()返回的数组也将改变。(这里c_str()内部的机制?)

  1. 数组(C风格)和标准库Vector 不容许数组之间的直接赋值和初始化,vector也不能给数组初始化。 可是容许数组为vector初始化,方法就是前面的begin()函数和end()函数。 此外,用于初始化vector对象的值也能够仅是数组的一部分:
vector<int> subVec(int_arr + 1, int_arr + 4);//它包括int_arr[1-3]三个元素

总结:现代C++程序应当尽可能使用vector和迭代器,避免使用内置数组和指针;应该尽可能使用string,避免使用C风格的基于数组的字符串。

  1. C++语言中没有多维数组,一般说的多维数组就是数组的数组。

  2. 多维数组的初始化: 容许用花括号括起来的一组值初始化多维数组:

int ia[3][4] = {//从内向外阅读,ia是一个含有3个元素的数组,ia的元素是一个含有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 (&row)[4] = ia[1];//把row绑定到ia的第二个4元素数组上
  1. 范围for在多维数组中的用法: (1)
size_t cnt = 0;
for (auto &row : ia)
	 for(auto &col : row){
		 col = cnt;
		 ++cnt;
}

(2)

for (const auto &row : ia)
	 for (auto col : row)
		 cout << col <<endl;

(1)中使用引用是由于要改变元素的值 (2)中没有改变值却在外层还要使用引用,由于若是不用引用编译器初始化row时会将数组形式的元素转换成一个指针,使row的类型为int*,这样内层的循环就不合法了。 注意:要使用范围for语句处理多维数组,除了最内层的循环外,其余全部循环的控制变量都应该是引用类型。

  1. 注意多维数组中的指针声明:
int *ip[4];//表示整型指针的数组
int (*ip)[4];//表示指向含有4个整数的数组
//遍历二维数组ia:
for (auto p = begin(ia); p != end(ia); ++p){
	 for(auto q = begin(*p); q != end(*p); ++q)
		 cout << *q << " ";
	 cout << endl;
}
  1. 类型别名可简化指针
using int_array = int[4];
typedef int[4] int_array;
相关文章
相关标签/搜索