C++primer笔记——第三章【字符串、向量和数组】

【第三章】 字符串、向量和数组


3.1 命名空间的using声明
1、std::cin的意思就是要使用命名空间std中的名字cin

2、一旦使用了using namespace::name;这个语句,就可以直接访问命名空间中的名字。每个名字都需要独立的using声明,且以分号结束

头文件不应包含using声明:
3、位于头文件中的代码一般来说不应该使用using声明。因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。

3.2 标准库类型 string
1、使用string类型必须首先包含string头文件。作为标准库的一部分,string定义在命名空间std中。
#include<string>
using std::string;

2、初始化string对象的方式
string s1; // 默认初始化,s1是一个空串
string s2(s1); // s2是s1的副本
string s2 = s1; // 等价于上一句
string s3("value"); // s3是字面值"value"的副本,除了字面值最后的那个空字符外
string s3 = "value"; // 等价于上一句
string s4(n,'c'); // 把s4初始化为由连续n个字符c组成的串

直接初始化和拷贝初始化:
3、如果使用等号初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去。如果不使用等号,则执行的是直接初始化。

3.2.2 string对象上的操作
os << s // 将s写到输出流os当中,返回os
is >> s // 从is中读取字符串赋给s,字符串以空白分隔,返回is
getline(is, s) // 从is中读取一行赋给s,返回is
s.empty() // s为空返回true,否则返回false
s.size() // 返回s中字符的个数
s[n] // 返回s中第n个字符的引用,位置n从0开始
s1 + s2 // 返回s1和s2链接后的结果
s1 = s2 // 用s2的副本代替s1中原来的字符
==、!=、<

4、string对象会自动忽略开头的空白(即空格符、换行符、制表符等),并从第一个真正的字符开始读起,直到遇见下一处空白为止

使用getline读取一整行:
5、希望能在最终得到的字符串中保留输入时的空白符,这时就用getline函数代替原来的>>运算符。
getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止,然后把所读的内容存入到那个string对象中去(不存换行符)。
如果输入一开始就是换行符,那么所得的结果是一个空串。最后返回它的流参数。

6、如果一条表达式中已经有了size()函数,就不要再使用int了。假设n是一个具有负值的int,则表达式s.size()<n的判断结果几乎肯定是true。因为负值n会自动转换成一个比较大的无符号值

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

// 练习3.2 编写一段程序从标准输入中一次读入一整行,然后修改该程序使其一次读入一个词。
#include<iostream>
using std::cin;
using std::cout;
using std::endl;
#include<string>
using std::string;


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


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


3.2.3 处理string对象中的字符
8、在cctype头文件中定义了一组标准库函数
1)isalnum(c) 当c是字母或数字时为真
。。。

使用范围for语句来处理每个字符:
9、这种语句遍历给定序列中的每个元素,并对序列中的每个值执行某种操作,语法形式是:
for( declaration : expression )
statement
其中,expression部分是一个对象,用于表示一个序列。declaration部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,declaration部分的变量会被初始化为expression部分的下一个元素值
string str("some string");
for( auto c : str )
cout<<c<<endl;
for循环把变量c和str联系了起来,通过使用auto关键字让编译器来决定变量c的类型。这里c的类型是char,每次迭代,str的下一个字符被拷贝给c
上述的代码可以理解为:对字符串str中的每个字符c,执行某某操作。

使用范围for语句改变字符串中的字符:
10、如果想改变string对象中字符的值,必须把循环变量定义成引用类型。所谓引用只是给定对象的一个别名,因此当使用引用作为循环控制变量时,这个变量实际上被依次绑定到了序列的每个元素上。使用这个引用,就能改变它绑定的字符。
for( auto &c : str )
c = toupper(c);

只处理一部分字符:
11、要想访问string对象中的单个字符有两种方式:一种是使用下标,另一种是使用迭代器。

12、下标运算符接收的输出参数是string::size_type类型的值,这个参数表示要访问的字符的位置,返回值是该位置上字符的引用。
如:s[0]是第一个字符,s[s.size()-1]是最后一个字符。注意,不可以用下标访问空string

13、在访问指定字符之前,首先检查s是否为空。if(!s.empty())

14、使用下标时必须确保其在合理范围内,即下标必须大于等于0而小于字符串的size()的值。方法是总是设下标的类型为string::size_type,因此类型是无符号数,可以确保下标不会小于0.
for(decltype(s.size()) index = 0;
index != s.size() && !isspace(s[index]);
++index)
...

3.3 标准库类型vector
1、标准库类型vector表示对象的集合,其中所有对象的类型都相同。常被称为容器。
#include<vector>
using std::vector;

2、C++既有类模板,也有函数模板。其中vector是一个类模板。

3、模板本身不是类或函数,相反可以将模板看做为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化,当使用模板时,需要指出编译器应把类或函数实例化成何种类型。

4、对于类模板,我们提供一些额外信息来指定模板实例化成什么样的类,提供信息的方式:在模板名字后面跟一对尖括号,在括号内放上信息。

5、因为引用不是对象,所以不存在包含引用的vector。

6、初始化vector对象的方法:
1)vector<T> v1; // v1是一个空vector,它潜在元素是T类型的,执行默认初始化
2)vector<T> v2(v1); // v2中包含有v1所有元素的副本
3)vector<T> v2 = v1; // 等价于上一句,允许把一个vector对象的元素拷贝给另外一个vector对象,注意两个vector对象的类型必须相同
4)vector<T> v3(n, val); // v3包含了n个重复的元素,每个元素的值都是val
5)vector<T> v4(n); // v4包含了n个重复地执行了值初始化的对象
6)vector<T> v5{a,b,c...}; // v5包含了初始值个数的元素,每个元素被赋予相应的初始值
7)vector<T> v5={a,b,c...}; // 等价于上一句

列表初始化vector对象:
7、使用拷贝初始化时(=)只能提供一个初始值。如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号形式初始化。如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里。

3.3.3 vector操作
1)v.empty() // 如果v不含有任何元素,返回真
2)v.size() // 返回v中元素的个数
3)v.push_back(t) // 向v的尾端添加一个值为t的元素
4)v[n] // 返回v中第n个位置上元素的引用
5)v1 = v2 // 用v2中元素的拷贝替换v1中的元素
6)v1 = {a, b, c,...} // 用列表中元素的拷贝替换v1中的元素
7)比较运算

8、只能对确知已存在的元素执行下标操作

3.4 迭代器介绍
9、可以用迭代器机制来访问string对象的字符或者vector对象的元素

10、就迭代器而言,其对象是容器中的元素或者string对象中的字符。使用迭代器可以访问某个元素,也能从一个元素移动到另一个元素。

3.4.1 使用迭代器
11、迭代器拥有begin和end的成员,其中begin成员负责返回指向第一个元素(或第一个字符)的迭代器。而end成员负责返回最后一个元素的下一个位置。于是
auto b = v.begin(), e = v.end(); // b 和e 的类型相同,用auto是因为我们不关心迭代器的准确类型是什么

12、end成员返回的迭代器指示的是容器的一个本不存在的尾后元素,这样的迭代器没有什么实际含义,仅是一个标记,表示已经处理完了容器中的所有元素。

13、end成员返回的迭代器称为尾后迭代器,如果容器为空,则begin和end返回同一个迭代器,都是尾后迭代器。

14、使用迭代器检查容器是否为空 if(s.begin() != s.end())

将迭代器从一个元素移动到另外一个元素
15、因为end返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作

迭代器类型
16、就像不知道string和vector的size_type成员到底是什么类型一样,一般也无需知道迭代器的精确类型。
而实际上,那些拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型。
vector<int>::iterator it; // it能读写vector<int>的元素
vector<int>::const_iterator it1; // it1只能读元素,不能写元素
如果vector对象或者string对象是一个常量,只能使用const_interator

17、迭代器这个名字有三种不同含义:可能是迭代器本身,也可能是指容器定义的迭代器类型,还可能是指某个迭代器对象。

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

19、cbegin和cend专门得到const_iterator类型的返回值,不论vector对象本身是否是常量,返回值都是const_iterator

结合解引用和成员访问操作:
20、解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。
例如,对于一个由字符串组成的vector对象来说,要想检查其元素是否为空,令it是该vector对象的迭代器,只需检查it所指字符串是否为空就可以
(*it).empty()
注意,圆括号必不可少。表示先对it解引用,然后解引用的结果再执行点运算符。如果不加圆括号,点运算符由it来执行,而非it解引用的结果。

21、为简化上述表达式,定义了箭头运算符->,该运算符把解引用和成员访问两个操作结合在一起,即it->mem  等价于(*it).mem

某些对vector对象的操作会使迭代器失效:
1)不能再范围for循环中向vector对象添加元素
2)任何一种可能改变vector对象容量的操作,比如push_back都会使该vector对象的迭代器失效。
注意:凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素

22、迭代器运算
1)iter + n   // 迭代器加上一个整数值仍得一个迭代器,迭代器指示的新位置比原来向前移动了若干个元素
2)iter - n   // 迭代器减去一个整数值仍得一个迭代器,迭代器指示的新位置比原来向后移动了若干个元素
3)iter += n
4)iter1 - iter2 // 两个迭代器相减的结果是它们之间的距离

迭代器的算术运算:
23、计算得到最接近vi中间元素的一个迭代器
auto mid = vi.begin() + vi.size() / 2;
auto mid1 = vi.begin() + (vi.end() - vi.begin()) /2;
if(it < mid)
// 处理前半部分的元素


使用迭代器运算:
24、使用迭代器运算的一个经典算法是二分搜索。二分搜索从有序序列中寻找某个给定的值。
二分搜索从序列中间的位置开始搜索,如果中间位置的元素正好就是要找的元素,搜索完成;
如果不是,假如钙元素小于要找的元素,则在序列的后半部分继续搜索;
假如该元素大于要找的元素,则在序列的前半部分继续搜索。重复之前的过程,直到最终找到目标或者没有元素可供继续搜索
auto b = v.begin(), e = v.end();
auto mid = b + v.size() / 2;
while(mid != end && *mid != value)
{
if(*mid < value)
b = mid + 1;
else
e = mid;
mid = b + (e - b)/2;
}

3.5 数组
3.5.1 定义和初始化数组
1、数组的维度必须是一个常量表达式
unsigned cnt = 42; // 不是常量表达式
constexpr unsigned sz = 42; // 常量表达式
int *parr[sz]; // 含有42个整型指针的数组
string strs[get_size()]; // 当get_size是constexpr时正确,否则错误

2、定义数组的时候必须制定数组的类型,不允许用auto关键字由初始值的列表推断类型。另外和vector一样,数组的元素应为对象,因此不存在引用的数组。

显式初始化数组元素:
const unsigned sz = 3;
int ia1[sz] = {0,1,2};
int a2[] = {0,1,2};
string a4[3] = {"hi","bye"};

字符数组的特殊性:
3、字符数组由一种额外的初始化形式,可用字符串字面值对此类数组初始化。当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去:
char a1[] = {'c', '-', '-'}; // 列表初始化,没有空字符。维度是3
char a2[] = {'c', '-', '-', '\0'} // 列表初始化,含有显式的空字符,维度是4
char a3[] = "c--"; // 自动添加表示字符串结束的空字符
const char a4[3] = "c--"; // 错误,没有空间可存放空字符

4、数组不允许拷贝和赋值

理解复杂的数组声明!!!!
允许定义数组的指针及数组的引用。
int *ptrs[10]; // ptrs是含有10个整型指针的数组(ptrs是一个含有10个元素的数组,每个元素的类型是int *)
int &refs[10]; // 错误,不存在引用的数组,不能引用数组
int (*Parray)[10]; // Parray指向一个含有10个整数的数组
int (&arrRef)[10]; // arrRef引用一个含有10个整数的数组
int *(&arry)[10]; // arry是数组的引用,该数组含有10个指针
默认情况下,类型修饰符从右向左依次绑定。
1)对于ptrs来说,首先直到定义的是一个大小为10的数组,它的名字是ptrs,然后知道数组中存放的是指向int的指针
就数组而言,从内向外阅读比从右向左阅读好。
2)对于Parray来说,*Parray意味着Parray是个指针,观察右边知道Parray是个指向大小为10的数组的指针,最后观察左边,知道数组中的元素是int
这样,就可以理解为Parray是一个指针,它指向一个int数组,数组中包含10个元素
3)同理,(&arrRef)表示arrRef是一个引用,引用的对象是一个大小为10的数组,数组中的元素是int
4)首先直到arry是一个引用,观察右边知道arry引用的对象是一个大小为10的数组,最后观察左边知道数组的元素类型是指向int的指针。
这样,arry就是一个含有10个int型指针的数组的引用。

3.5.2 访问数组元素
5、在使用数组下标的时候,通常将其定义为size_t类型。
constexpr size_t array_size = 10;

3.5.3 指针和数组
6、通常情况下,用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。数组的元素也是对象。
string num[] = {"one", "two", "3"};
string *p = &num[0];
然而,数组还有一个特性:在很多用到数组名字的地方,编译器会自动将其替换为一个指向数组首元素的指针:
string *p2 = nums;  // 等价于上一句

7、当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组:
auto ia2(num); // ia2是一个string型指针指向num的第一个元素
必须指出的是,当使用decltype关键字时上述转换不会发生,decltype(ia)返回的类型是由10个string构成的数组

指针也是迭代器:
int *p = arr; // p指向arr的第一个元素

标准库函数begin和end:
这两个函数与容器中的两个同名成员功能类似,不过数组毕竟不是类类型,因此这两个函数不是成员函数
int ia[] = {0, 1, 2};
int *beg = begin(ia}; // 指向ia首元素的指针
int *end = end(ia); // 指向ia尾元素的下一位置的指针
这两个函数定义在iterator头文件中

3.5.4 C风格字符串
8、字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的C风格字符串

9、cstring头文件定义的函数:
1)strlen(p) // 返回p的长度,空字符不计算在内
2)strcmp(p1, p2) // 相等返回0,p1大于p2返回正值,否则返回负值
3)strcat(p1, p2) // 将p2附加到p1后,返回p1
4)strcpy(p1, p2) // 将p2拷贝给p1,返回p1
传入此类函数的指针必须指向以空字符作为结束的数组:
char ca[] = {'c', '-', '-'};
cout<< strlen(ca) <<endl; // 严重错误,ca没有以空字符结束
ca虽然也是一个字符数组,但它不是以空字符作为结束

比较字符串:
10、string对象可以直接使用<等关系运算符。而如果将其用到C风格字符串,实际比较的将是指针而非字符串本身:
const char ca1[] = "a small pig";
const char ca2[] = "a big pg";
if(ca1<ca2) //  未定义的,试图比较两个无关地址(const char*的值)
要想比较两个c风格字符串需要调用strcmp函数。

3.5.5 与旧代码的接口
混用string对象和C风格字符串:
11、任何出现字符串字面值的地方都可以用以空字符结束的字符数组来代替:
1)允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值
2)在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象
3)在string 的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象
上述性质反过来就不成立了:如果程序某处需要一个C风格字符串,无法直接使用string对象来代替他。
但string专门提供了一个名为c_str的成员函数
char *str = s; // 错误,不能用string对象初始化char*
const char *str = s.c_str(); // 正确
该函数返回一个C风格字符串,也就是说,函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组

使用数组初始化vector对象:
12、要实现这一目的,只要指明拷贝区域的首元素地址和尾后地址就可以:
int int_arr[] = {1,2,3,4,5};
vector<int> ivec(begin(int_arr),int_arr + 3};

3.6 多维数组
1、严格来说,C++没有多维数组,通常所说的多维数组其实是数组的数组。

2、当一个数组的元素仍然是数组时,通常使用两个维度来定义它:
一个维度表示数组本身大小,另一个维度表示其元素大小。
int ia[3][4] = {
{0, 1, 2, 3},
{0, 1, 2, 3},
{0, 1, 2, 3},
};
// 大小为3的数组,每个元素是含有4个整数的数组

int a[10][20][30]; //大小为10的数组,它的每个元素都是大小为20的数组,这些数组的元素都是含有30个整数的数组

多维数组的下标引用:
3、如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果将是给定类型的元素。
反之,如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引处的一个内层数组。
ia[2][3] = a[0][0][0]; // 用a的首元素为ia的最后一行最后一个元素赋值
int (&row)[4] = ia[1]; // row是一个引用,它引用一个含有四个元素的数组,于是把row绑定到ia的第二个4元素数组上。

4、二维数组元素对应的索引:ia[i][j] : i * colCnt + j;

指针和多维数组:
5、当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。
因为多为数组实际上是数组的数组,所以由多维数组名转换得到的指针实际上是指向第一个内层数组的指针:
int ia[3][4];
int (*p)[4] = ia; // p指向含有4个整数的数组
p = &ia[2]; // p指向ia的尾元素

随着C++11新标准提出,通过使用auto或者decltype就能尽可能地避免在数组面前加上一个指针类型了:
for(auto p = ia; p != ia+3; p++){
for(auto q = *p; q != *p+4; q++)
cout<< *q <<' ';
cout<<endl;
}
// 解引用p得到指向内层数组首元素的指针,加上4就得到了终止条件
当然,使用标准库函数begin和end更简洁一些:
for(auto p = begin(ia); p != end(ia); ++p){
for(auto q = begin(*p); q != end(*p); ++q)
cout<< *q<<endl;
}



猜你喜欢

转载自blog.csdn.net/CSDN_dzh/article/details/80952104