chapter 3 字符串、向量和数组
string类:可变长度的字符串。vector :可变长度的集合。
3.1 命名空间的 using 声明
在程序中显示声明将使用XXX命名空间里的某个值,那么下面的程序会自动识别到这个声明
#include <iostream>
int main()
{
{
using std::cout;//声明下面使用的cout都是命名空间std中定义的;
cout << "hello world"; //cout 已经用using声明了,所以不用显示声明cout是谁的;
cout << std::endl; //endl 没有用using声明,每一次用到都要在程序中用std::显示声明;
}
//============================
{
using namespace std;//用namespace std声明下面用到的都是std库里的函数
cout << "good morning" << endl; //无须显示声明
}
getchar();
}
使用用户自定义的命名空间
#include <iostream>
namespace sxd {
int i = 10;
}
int main()
{
using namespace sxd;
i = 100;
std::cout << "i = " << i << std::endl;
getchar();
}
头文件不应包含using声明,因为如果头文件中包含了using声明,那么引用它的文件也会拷贝这个using声明,则每个引用该头文件的文件都会有这个声明
3.2.1 定义和初始化string对象
string类对象默认初始化为空串;
string s(10,'c');//s内容为“cccccccccc”
用“=”初始化string类型都是拷贝初始化,比如 string s1 = "nihao";
如果不使用“=”,就是直接初始化:例如string s1("hello");
3.2.2 string对象上的操作
- 读写操作 cin >> s; cout << s;//cin将对象读入s中,遇到空白停止
- 使用getline()函数读取一整行字符串:getline(cin, s);//其中cin是输入流,s是字符串,getline函数遇到换行符结束,存取到s中没有换行符
- string的empty()操作,s.empty(); 判断string类s是否为空,返回布尔类型。if(s.empty()) ……
- string的size()操作,s.size();返回string类s的字符串长度。
- string::size_type类型,string的size()函数返回的就是size_type类型,是无符号的,而且与机器无关,体现了标准库与机器无关的特性。
- string类型支持+,+=,操作,是字符串相接,(连加时注意不能使两个字面值常量相加)。
3.2.3 处理string对象中的字符
- 头文件C++与C的区别:C++包含所有C语言的库,只是形式有点改变C:#include <name.h> C++: #include 将后缀.h改为前缀c。
- C++中一般使用cname的头文件,因为这样库中的名字总能在命名空间std中找到。
- 范围for(declaration:expression) statement; 语句。定义declaration对象去迭代访问对象expression,执行statement语句。如下:
/*
*下面的程序使用范围for语句、decltype确定类型函数、以及auto类型
*实现寻找一个字符串中有多少个标点符号
*/
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s("hello world!!!");
decltype(s.size()) punct_num = 0;
for (auto c : s)
if (ispunct(c))
punct_num++;
cout << "s 中的标点符号有:" << punct_num << "个。" << endl;
getchar();
return 0;
}
- 如果想要使用范围for语句改变对象中的值,则循环变量declaration必须是引用类型。如下:
#include <iostream>
#include <string>
using namespace std;
int main()
{
/*
*下面的程序使用范围for语句
*修改字符串里面的值
*/
{
string s("Hello world!!!");
decltype(s.size()) punct_num = 0;
for (char &c : s)
c = toupper(c); //c是一个引用,所以改变的并不是c,而是c代表的值
cout << s << endl;
getchar();
return 0;
}
}
- string类型支持下标运算符[],如string s(“hello”); s[0] = h;且可以通过访问下标赋值,下标不能越界,下标越界将引发未知结果。
- 养成习惯:用下标访问string时,先判空。if(!s.empty()) ……;
3.3 标准库类型vector(模板)
vector——对象的集合(容器),其中的所有的对象的类型都相同;
vector中每个对象都有对应的索引;
vector使用的头文件——#include <vector>
3.3.1 定义和初始化vector对象
(1)默认初始化vector对象,vector对象中不含任何元素
vector v1;//v1为空
(2)列表初始化:vector除了提供这些初始化方式外,提供了一种新的初始化方式:列表初始化:
vector s = {“hello”, “world”, “nihao”};
- 初始化的几种不同方式除以下三种外都可以互相等价:
- 使用拷贝初始化时(即使用=时),只能提供一个初始值;
- 类内初始化:只能使用拷贝初始化(“=”),或使用花括号形式初始化(“{}”);
- 提供的是初始化元素列表时,只能将元素放在花括号里面初始化;
class ChuShiHuaClass
{
private:
int a{10};
int b = 10;
int c(10); //错误,类内初始化只能用以上两种
public:
void Geta();
ChuShiHuaClass();
};
(3)创建指定数量的元素
vector<string> s(10,"hello"); //创建10和hello字符串。
vector<string> s1{10,"hello"}; //创建10和hello字符串,{}在此同();
值初始化:可以只提供vector对象容纳的元素个数而省略元素初始值,这样的话,C++库会自动根据vector的元素类型值初始化里面的元素,如int型的初始化为0。
vector<int> v1(10);//自动创建10个值为0的元素。
vector<string> v2{10};//自动创建10个值为0的string元素。
限制1:有些类不支持默认初始化(定义对象时必须提供初始值),则无法完成初始化工作。
限制2:如果只提供了元素的个数,没有提供初始值,那么不能进行赋值初始化,只能进行直接初始化(构造初始化)
// vector<string> v1 = {10}; //错误,只提供了元素个数,没有提供初始值,不能进行赋值初始化
vector<string> v1{10}; //这样就可以了,因为是直接初始化的
(4)判断是列表初始化还是元素数量
因为,使用{}可以代替()进行构造初始化,但{}又单独可以列表初始化,所以{}里面第一个值既有可能是列表值,又有可能是元素的个数
分辨的方法:编译时先判断{}里面的值是否能直接列表初始化vector,如果不能,则再判断是否可以进行创建指定数量元素的初始化
vector<int> v1{10,20};//列表初始化
vector<string> v0{10,"hello"}; //指定数量的元素初始化
vector<int> v2{10};//1个元素,值为10;
vector<int> v3{10, 1};//2个元素,分别为10,1;
//vector<string> v4("hello"); //错误,不能使用字面值常量构建vector对象(构造初始化)
3.3.2 向vector 对象中添加元素——push_back()函数
push_back()函数负责将一个值当成vector对象的元素压(push)到vector对象的尾端。
vector<int> v1;
int i;
while(cin >> i) {
v1.push_back(i);
}
C++里,vector对象的个数能够高效的增长
使用范围for()语句遍历vector对象的时候,不应该有添加、删除对象的操作:因为范围for()语句不应该改变其所遍历序列的大小。
3.3.3 其他vector操作
- .empty()函数:判空函数,空则返回true,不空则返回false
- .size()函数:返回元素的个数 返回值类型:vector::size_type(而不是vector::size_type)
- [n] 下标运算符,返回第n个位置上的元素的引用
- = 拷贝替换
- ==判等,所有元素都相同且个数相同才相等
注意:
- 不能以下标形式添加对象,只能以。push_back()函数添加,下标运算符只能操作已存在的元素
- 习惯:在使用下标运算符之前必须判空
- 尽量使用范围for()语句可以确保下标合法
- 通过下标去访问不存在的元素将引发错误,这种错误不会被编译器发现,在运行时产生不可预知的值,所谓的缓冲区溢出(buffer overflow)就是指这样的错误
3.4迭代器
所有标准库容器都可以使用迭代器,但只有少数几种支持下标运算符[]
3.4.1迭使用迭代器
- 如何获取迭代器:有迭代器的类型能够返回迭代器成员,这些类型都能返回 .begin() 和 .end() 成员
- end成员返回尾后迭代器——尾元素的下一位置的迭代器
- 尾后迭代器本不存在,没有实际含义,表示已经处理完成了所有元素
- 当容器为空的时候,.begin 和.end成员都指向尾后迭代器
迭代器是一个指针
迭代器运算符
*iter 解引用,返回iter所指向的元素的引用
iter->mem 解引用,iter所指向的成员,等价于(*iter).mem(一定要加括号,否则先运行 . 操作)
++iter iter指向下一个元素
–iter iter指向上一个元素
== / != 判等或不等,只有两个iter指向同一个元素的地址时候才等于,否则不等(所有的迭代器都支持判等,但不一定支持 > < >= <= 等不等式运算符,所以尽量用判等 左判断条件)
使用迭代器和使用下标运算符一样之前要先判空
if(v.begin() != v.end())
.end()成员指向尾后迭代器,不能解引用,也不能递增,所以迭代器指向下一元素时先判断不为尾后迭代器
iter != v.end();
关键概念:泛型编程——是一种编程风格,比如这一段代码到哪里都能用,通用性很强——这一段代码不依赖于具体的数据类型,什么数据类型都会定义这种操作
- 具体来说:养成习惯:尽量使用 != 或 == 在循环中进行判断iter迭代器,而不要使用 <= 、>= 等判断,因为绝大多数类都会定义 判等操作,很少有类会定义比较操作
所以:一段循环代码,vector容器 不同类型(如int、string 之类) 都会支持 == !=运算,但是换种类型可能就不支持 <= 、>= 不等式操作了。 - 运算符优先级
迭代器类型
像容器这样有迭代器的类都定义了迭代器自己的类型:iterator 和 const_iterator类型(常迭代器,迭代器本身可以跳转,但是不能改变其指向的元素)
vector<int> v1;
vector<int>::iterator it1;//不常用
vector<int>::const_iterator it2; //不常用
auto it3 = v1.begin(); //一般常用auto让编译器自己去定义iterator类型
auto自动定义迭代器类型:
vector<int> v1;
auto it3 = v1.begin(); //那么it3的类型是:vector<int>::iterator
const vector<int> v2;
auto it4 = v2.begin(); //那么it4的类型是:vector<int>::const_iterator
- 使用 .cbegin() 和 .cend() 成员函数(cbegin()意思是:const_begin(),同理cend())
我们想用iter迭代器访问vector容器元素但不想改变元素的值(vector本身不是const的),那么就可以使用.cbegin() 和 .cend() 来赋值定义给auto对象,让迭代器编程const_iterator类型的;
auto it5 = v1.cbegin();
vector<int>::const_iterator it6 = v1.begin(); //it5 和 it6的作用相同
改变容器的大小(添加或删除元素)都会让迭代器失效,这同范围for()一样,不能用于改变容器的容量
3.4.2 迭代器运算
iter + n;
iter - n;
iter += n;
iter -= n;
iter1 - iter2;
<、 > 、<=、 >= 比较迭代器之间的前后关系。
注意:没有 iter1 + iter2 不存在这种方法 ; 迭代器中也没有定义这种方法
//++++++++iterator 二分查找++++++++++++++
{
vector<int> v{0,1,2,4,5,8,9,12,13,16,19};
auto beg = v.begin(), end = v.end();
auto mid = beg + (end -beg) / 2;
int i;
cout << "input the number which needs find"<< endl;
cin >> i;
while(mid != end && *mid != i)
{
if(i < *mid)
end = mid;
if(i > *mid)
beg = mid + 1;
mid =beg + (end -beg) / 2;
}
if(mid == end)
cout << "no such a number " << endl;
else
cout << "the num in v's position is " << mid - v.cbegin() << endl;
}
3.5 数组
3.5.1 定义和初始化内置数组
数组是一种复合类型:a[d], 数组的名称a 和数组的维度d(d>0) 在编译时应该都是已知的,所以维度d是一个常量表达式
string s;
//以下三种都是常量表达式,都可以作数组维度
constexpr int i = 32;
const int j = 32;
const int m = sizeof(s.size());
int b[i];
int a[j];
int c[m];
cout << "m = " << m << endl;
cout << "c[m]'s size = " << sizeof(c) << endl;
- 在函数体内定义数组和定义内置类型一样,会默认初始化为未知的值。
- 不允许使用auto定义数组!,定义数组必须指定数组类型
- 数组里装的是对象,不能装引用。
- 指定数量初始化未填充完成,后面自动补0
int a[10] = { 0,1,2,3 };
for (auto i : a)
cout << i << " ";//程序输出结果为 0 1 2 3 0 0 0 0 0 0
字符数组特殊,特殊处在于可以用字符串赋值:
char a[6] = "hell0";//hello占6个字,后面有一个'\0'
数组不允许整体直接拷贝或复制
char a[] = “hello”;
char b[] = a;//错误
b = a;//错误
- 复杂的数组声明(类型饰符是从右向左依次绑定的)
- 数组能存放元素(是个容器,能装元素),所以能装指针,引用不是对象,所以不能存放引用
//数组能存放对象(是个容器,能装元素),所以能装指针
//数组ptr里面存放了10个指针,不能是野指针,所以习惯性初始化!!
int *ptr[10] = { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL };
int a[10] = { 00,01,02,03,04,05,0xD,0xC,0xA,0xB };
for (int i = 0; i != 10; i++)
{
ptr[i] = &a[i];
cout << "*ptr[" << i << "] = " << *ptr[i] << endl;
}
//int &refs[10]; //错误,引用不是对象,不能存放
- 数组本身又是对象,可以被指针指向或者被引用。
//数组本身又是对象,可以被指针指向,或者被引用
int arr[10] = { '0','1','2', '3', '4', '5', '6', '7', '8','9' };
int(*ptr)[10] = &arr; //定义了一个指针,该指针指向一个数组,这个数组必须是拥有10个元素的int数组
int(&refs)[10] = arr; //定义了一个引用,该引用指向一个数组,这个数组必须是拥有10个元素的int数组
//类型修饰符满足从右向左依次绑定的规律
//int *ptr[10] ,首先绑定的是ptr[10],说明ptr是一个数组,然后再将数组ptr[10]绑定*即 *(ptr[10]),是包含指针的数组
//int(*ptr)[10],首先绑定的是(*ptr),说明ptr是指针,然后(*ptr) 与[10]绑定,[10]作为修饰,说明这一个指针指向的 //数组必须只含有[10]个元素。
类型修饰符满足从由内向外(括号内外)、从右向左依次绑定的规律
3.5.2 访问数组元素
使用或定义数组下标时,可以使用size_t类型:(是一个与机器无关的无符号整型,且被设计的足够大),需要包含头文件cstddef(C++)或stddef.h©
注意下标的值不要溢出,判空,判满,范围for( )
3.5.3 指针和数组
C++中,用数组的时候编译器一般会将他转换成指针,很多用到数组名字的地方,编译器会自动将数组名转换成指向首地址的指针:
数组名就是一个指针(这个指针指向数组首元素地址)
int a[] = { 1,2,3,4 };
cout << *a << endl;
getchar();
使用数组名作为auto类型的初始值时,定义的变量将是一个指针
int a[] = { 1,2,3,4 };
auto b(a); // 也就是 auto b(&a[0]);
b = &a[3];
cout << "*b = " << *b << endl;
使用decltype(数组名)的时,返回的是数组,
int a[] = { 1,2,3,4 };
decltype(a)b;
b[0] = 0;
b[1] = 1;
b[2] = 2;
b[3] = 3;
//相当于 int b[4];
数组的指针拥有迭代器的所有功能,其中,迭代器拥有尾后迭代器,数组指针也可以指向数组的尾后地址:(尾后指针不能执行解引用和递增操作)
int a[5] = { 0,1,2,3,4 };
int *p = &a[5];
标准库函数的begin和end:
- 上述的尾后指针是通过计算得到的,很容易出错,一般使用标准库函数的begin()和end()函数(为了模仿容器里的这两个函数)
- 需要头文件#include
//标准库函数begin()和end()
//需要用到 #include <iterator>
{
int a[5] = { 0,1,2,3,4 };
int *pbeg = begin(a);
int *pend = end(a);
for (auto p = pbeg; pbeg != pend && p != pend; p++)
cout << *p << " ";
cout << endl;
}
- 指向同一个数组元素的两个指针可以相减,得到 ptrdiff_t 类型的有符号类型
- 两个指针指向同一个数组的元素(或尾后)就可以进行比较,(两个指针若指向不相关的对象就不能比较)
- (知识点没什么用):空指针和两个指向对象不是数组的指针也可以相减和比较:空指针只能和空指针比较,空指针只能+0 或 -0;指向对象的指针必须指向同一个对象或该对象的下一个位置。
- 指向数组的指针也可以执行下标运算!!,指针的下标还可以为负数!与标准库的类(string、vector)不同。
//指向数组的指针也可以执行下标运算
{
int a[5] = { 0,1,2,3,4 };
int *p = a + 2;
cout << "p[1] = " << p[1] << endl;
cout << "p[-1] = " << p[-1] << endl;
}
3.5.4 C风格字符串
尽量不要用C风格的字符串
3.5.5 与旧码的接口
- 如果C风格的字符数组以‘\0’结束:可以用其对C++ 的 string类型 初始化或者赋值
char a[] = "hello world";
string s(a);
cout << s << endl;
char b[] = "你好";
s = b;
cout << s << endl;
- 如果C风格的字符数组以‘\0’结束:可以用与C++中的字符串做加法运算
( + 是加法运算符, += 是复合赋值运算符)
//衔接上面的代码
s += a;
cout << s << endl;
- C++ string 返回字符数组 c_str()函数,返回string对象的字符数组形式,但返回的是const char * 类型的,必须要用const char * 类型指向。
const char *p = s.c_str();
cout << "p = " << p << endl;
- 将c_str()函数返回的常量字符数组拷贝到普通字符数组
//将string类型的s.c_str() 拷贝到C风格字符数组
{
#define STRLEN 128
string s = "hello world";
const char *p = s.c_str();
int str[STRLEN] = {};
int i = 0;
for ( i = 0; *p != '\0'; p++, i++)
str[i] = *p;
str[i] = '\0';
cout << "str = ";
for (int i = 0;str[i] != '\0' ;i++)
cout << (char)str[i];
cout << endl;
}
- C风格字符串初始化vector对象,可以用前后两个指针初始化vector对象。
//用 字符数组初始化vector对象
{
int a[] = { 0,1,2,3,4,5,6,7,8,9 };
vector<int> v{ begin(a),end(a) };//指针不必是头指针或尾后指针,可以是指向字符数组的任意位置指针
for (auto p : v)
cout << p;
}
- 将整型vector对象赋值到整型数组中
//将整型vector对象赋值到整型数组中
{
#define MAX 128
vector<int> v{ 0,1,2,3,4,5,6 };
int a[MAX] = {};
int i = 0;
for (auto p : v)
a[i] = p;
}
3.6 多维数组
多维数组就是数组的数组,数组里面的每一个值仍然是数组。
//多维数组的初始化
{
int a[2][3] = {
{0,1,2},
{3,4,5}
};
int b[2][3] = { 0,1,2,3,4,5 };
//这两种方式是一样的,b的方法容易混淆
}