C++_Primer03.string_vector

字符串、向量和数组

除了内置的基本数据类型,C++ 还提供了丰富的抽象数据类型库,最重要的两个是 string 和 vector,前者支持可变长字符串,后者支持可变长集合,还有一种标准库类型是迭代器,它是 string 和 vector 的配套类型,常用于访问 string 和 vector 中的元素

命名空间的 using 声明

std::cin 中的 :: 是作用域操作符,其含义是编译器应从操作符左侧名字的作用域中寻找右侧那个名字

使用 using 声明(using declaration),就能更加简单地使用其他命名空间中的对象:

#include <iostream>

using std::cout;
using std::endl;

int main(int argc, char **argv){
    int i = 2;
    cout << "i: " << i << endl;

    return 0;
}

头文件不应包含 using 声明

标准库类型 string

引入string头文件:

#include <string>
using std::string;

初始化

初始化方式 解释
string s1 默认初始化,s1是一个空串
string s2(s1) s2 是 s1 的副本
string s2 = s1 与上等价
string s3("hello") s3 是字面值"hello"的副本(除去字面值最后那个空字符)
string s3 = "hello" 同上
string s4(5, 'c') s4 是5个 c, "ccccc"

直接初始化和拷贝初始化

如果使用等号 = 初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。如果不使用等号,执行的是直接初始化(direct initialization)

当初始值只有一个时,使用直接初始化或拷贝初始化都行,但如果初始化要用到的值有多个,一般来说只能使用直接初始化的方式。

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中原来的字符
s1==s2 判断内容是否相等,大小写敏感
s1!=s2 如上
<, <=, >, >= 利用字符在字典中的顺序进行比较,且对字母的大小写敏感

string读写

#include <iostream>
#include <string>

using std::cin;
using std::cout;
using std::endl;
using std::string;

int main(int argc, char **argv){
    string s;
    cin >>s;
    cout << s << endl;

    return 0;
}

编译执行:

$ g++ --std=c++11 -o stringio stringio.cpp
$ ./stringio
  Hello world!  
Hello

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

读取未知数量的string对象

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

编译运行:

$ g++ --std=c++11 -o stringread stringread.cpp
$ ./stringread
Hello world!
Hello
world!

使用 getline 读取一整行

读一整行字符,包括空白字符,但不包含行尾的换行符:

int main(int argc, char **argv){
    string line;
    while (getline(cin, line)){
        cout << line << endl;
    }
    return 0;
}

string 的 empty 和 size 操作

empty: 判断字符串是否为空
size: 获取字符串的长度

while (getline(cin, line)){
    // 打印非空并且长度超过80的字符串
    if (!line.empty() && line.size() > 80){
        cout << line << endl;
    }
}

size() 函数的返回值类型: string::size_type

无符号整型,足够放下任何 string 对象的大小

C++11 新标准中,允许编译器通过 autodecltype 来推断变量的类型:

auto len = line.size();

切记 size 函数返回的是无符号整数,因此如果在表达式中混用了带符号数和无符号数将可能产生想不到的结果:
假设 n 是一个 int 类型的负数,则 s.size() < n 的结果几乎肯定是 true,
因为负值 n 会自动转换成一个比较大的无符号值

string 对象的比较

  • 如果两个 string 对象长度不同,而且较短 string 对象的每个字符都与较长 string 对象相同,则较短 string 对象小于较长 string 对象
  • 如果两个 string 对象在某些对应位置上不一致,则比较结果是 string 对象中第一对相异字符比较的结果

赋值

string st1(10, 'c'), st2;   // st1 是10个c,st2 是空字符串
st1 = st2;                  // 用 st2 的副本替换 st1 的内容

相加

string s1 = "hello", s2 = "world";
string s3 = s1 + s2;
string s4 = s1 + "";
string s5 = "hello" + ",";      // 错误,字面值不能直接相加
string s6 = s1 + "hello" + ",";
string s7 = "hello" + "," + s1; // 错误

为了与 C 语言兼容,C++ 中的字面值与 string 是不同的类型

处理 string 对象中的字符

cctype 头文件中定义了一组标准库函数:

函数名 说明
isalnum(c) 判断是否是字母或数字
isalpha(c) 判断是否是字母
iscntrl(c) 控制字符
isdigit(c) 数字
isgraph(c) c不是空格但可打印
islower(c) 小写字母
isprint(c) 可打印字符(空格或具有可视形式)
ispunct(c) 标点符号
isspace(c) 空白(空格、制表符、回车、换行、进纸符)
isupper(c) 大写字母
isxdigit(c) 十六进制数字
tolower(c) 输出小写
toupper(c) 输出大写

C++ 标准库兼容了C语言的标准库,其区别在文件名上有所体现,一般头文件命名规则是 name.h,而 C++ 则将这些文件命名为 cname,即去掉 .h 后缀,而在 name 之前添加字母 c,表示这是一个属于 C 语言标准库的头文件。

因此,cctype 头文件和 ctype.h 头文件内容是一样的,只不过命名规范更符合 C++ 语言的要求。另外,名为 cname 的头文件中定义的名字从属于命名空间 std,所以标准库中的名字总能在命名空间 std 中找到。而如果使用 .h 形式的头文件,程序员就不得不时刻牢记哪些是从 C 语言那里继承过来的,哪些是 C++ 独有的。

使用基于范围的 for 语句

string str("hello world!");
// 每行输出 str 中的一个字符
for (auto c: str){
    cout << c << endl;
}

decltype 关键字

获得变量的类型:

// 获得字符串中标点符号的个数
string s("Hello world!!!");
decltype(s.size()) cnt = 0;     // size_type
for (auto c: s){
    if (ispunct(c)){
        ++cnt;
    }
}
cout << cnt << " punctuation characters in Hello world!!!" << endl;

改变字符串中的字符

想要改变 string 对象中字符的值,必须把循环变量定义成引用类型:

// 小写变大写
string s("hello world!!!");
for (auto &c: s){
    c = toupper(c);
}
cout << s << endl;

标准库类型 vector

容器(container),表示对象的集合,其中所有对象的类型都相同

使用 vector:

#include <vector>
using std::vector;

C++ 既有类模板,也有函数模板,vector就是一个类模板,只有对 C++ 相当深入地理解才能写出模板。

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

早期版本的 C++ 标准中(C++11以前),如果 vector 的元素仍然为 vector,则必须在外层 vector 对象的右尖括号和其元素类型之间添加一个空格:

vector<vector<int> >

定义和初始化 vector 对象

初始化形式 说明
vector v1 v1 是一个空 vector,潜在元素是T类型,执行默认初始化
vector v2(v1) v2 中包含有 v1 所有元素的副本
vector v2 = v1 同上
vector v3(n, val) v3 包含了 n 个重复元素,每个元素的值都是 val
vector v4(n) 值初始化,v4 包含了 n 个重复执行了值初始化的对象
vector v5 列表初始化,用 a,b,c... 初始化 v5
vector v6 = 同上

vector 最常见的使用方式是先定义一个空 vector,然后当运行时获取到元素的值后再逐一添加

int 类型集合的构造

vector<int> v1(10);         // v1 有 10 个元素,每个值都为0
vector<int> v1{10};         // v1 有一个元素,值为 10
vector<int> v1(10, 1);      // v1 有10个元素,值都是1
vector<int> v1{10, 1};      // v1 有两个元素,分别是10和1

圆括号中的值是用来构造 vector 对象的;花括号中的值是列表初始化。

string 类型集合的构造

vector<string> v2{"hi"};        // 列表初始化
vector<string> v2("hi");        // 错误,不能用字符串字面值构建 vector 对象
vector<string> v2{10};          // v2 有10个默认初始化的元素
vector<string> v2{10, "hi"};    // v2 有10个值为 "hi" 的元素

如果使用了花括号的形式,而提供的值又不能进行列表初始化,就要考虑值初始化了。

添加元素

push_back(): 负责把一个值当做 vector 对象的尾元素“压到” vector 对象的“尾端”。

对于 vector 对象来说,直接初始化的方式适用于三种情况:初始值一直且数量较少;初始值是另一个 vector 对象的副本;所有元素的初始值都一样。而更常见的情况是,创建一个 vector 对象时并不清楚实际所需的元素个数,元素的值也经常无法确定。

实时读入数据然后将其赋予 vector 对象:

string word;
vector<string> text;
while (cin >> word){
    vector.push_back(word);
}

C++ 标准要求 vector 应该能在运行时高效快速地添加元素。
因此在定义 vector 对象的时候设定初始值就没什么必要了,而事实上这样做性能可能更差。
只有一种情况例外,就是所有元素的值都一样。
一旦元素的值有所不同,更有效的方法是定义一个空的 vector 对象,再在运行时向其中添加具体值。

其他操作

操作 说明
v.empty() 判断v是否为空
v.size() 返回v中的元素个数
v.push_back(t) 向v的尾端添加一个值为t的元素
v[n] 返回v中第n个位置上元素的引用
v1 = v2 用 v2 中的元素拷贝替换 v1 中的元素
v1 = 用列表中的元素拷贝替换 v1 中的元素
v1 == v2 当且仅当元素数量相同且对应位置的元素值都相同时判断为相等

只有元素的值可比较时,vector 对象才能被比较。

迭代器介绍

iterator

所有标准库容器都可以使用迭代器,但只有少数几种才同时支持下标运算符。
string 不是容器,但它和容器都既支持下标运算符,也支持迭代器。

迭代器有有效和无效之分,类似指针。有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一个位置,其他情况都属于无效。

获取和使用迭代器

auto b = v.begin(), e = v.end();

begin() 返回指向第一个元素的迭代器;end() 返回指向容器尾元素的下一个位置的迭代器,又被称作尾后迭代器。
如果容器为空,则 beginend 返回的是同一个迭代器。

迭代器运算符

运算 说明
*iter 返回迭代器所指元素的引用
iter->mem 等价于 (*iter).mem
++iter 令 iter 指向下一个元素
--iter 指向上一个元素
iter1 == iter2 判断两个迭代器是否相等,如果两个迭代器指向同一个元素或他们是同一个容器的尾后迭代器,则相等

试图解引用一个非法迭代器或尾后迭代器都是未定义的行为。

// 将字符串第一个字符转换成大写
string s("some thing");
if (s.begin() != s.end()){
    auto it = s.begin();
    * it = toupper(* it);
}
// 全部转成大写
string s("some thing");
for (auto iter = s.begin(); iter != s.end(); ++iter){
    * iter = toupper(* iter);
}

要习惯使用 != 来作为循环的判断条件,而不是 <
因为并不是所有的对象都定义了 < 运算符

迭代器类型

  • iterator
    • 能读写容器的元素
  • const_iterator
    • 只能读元素,类似常量指针

begin()end() 返回的具体类型由对象是否是常量决定,如果对象是常量,则返回 cont_iterator,否则返回 iterator

C++11 引入两个新函数,方便专门得到 const_iterator 类型的返回值:cbegin()cend()

某些对 vector 对象的操作会使迭代器失效
所以,凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素

迭代器相减的结果是类型为 difference_type 的带符号整型数,代表后面的迭代器向前移动多少位置就能追上左侧的迭代器。string 和 vector 都定义了 difference_type。

二分搜索

// text 是存放字符串的有序容器 vector<string>
// sought 是要查找的元素
auto begin = text.begin(), end = text.end();
auto mid = begin + (end - begin) / 2;
while (begin != end && * mid != sought){
    if (sought < *mid){
        end = mid;
    } else {
        begin = mid + 1;
    }
    mid = begin + (end - begin) / 2;
}

数组

数组是一种类似于标准库类型 vector 的数据结构,但数组大小固定,所以损失了一些灵活性。

初始化数组

数组初始化时需要指定数组大小,维度必须是一个常量表达式

unsigned cnt = 42;
constexpr unsigned sz = 42;
int arr[cnt];               // 错误,cnt不是常量表达式,它的值无法在编译器确定
int *parr[sz];
string text[10];
string strs[get_size()];    // 当 get_size 是 constexpr 时正确,否则错误

定义数组的时候必须指定数组的类型,不允许使用 auto 关键字进行推断;数组的元素应为对象,不存在引用的数组。

显式初始化数组元素

const unsigned sz = 3;
int a1[sz] = {1,2,3};
int a2[] = {1,2,3};
int a3[5] = {1,2,3};            // 等价于 {1,2,3,0,0}
string a4 = {"hello", "hi"};    // 等价于 {"hello", "hi", ""}
int a5[2] = {1,2,3};            // 错误,初始值过多

字符数组

使用字符串字面值初始化时,字符串字面值结尾处还有一个空字符,空字符也会被拷贝到字符数组中去:

char a1[] = {'C', '+', '+'};        // 列表初始化,没有空字符,维度是3
char a2[] = {'C', '+', '+', '\0'};  // 列表初始化,含有显式的空字符,维度是4
char a3[] = "C++";                  // 自动添加表示字符串结束的空字符,维度是4
char a4[3] = "C++";                 // 错误:没有空间可以存放空字符

不允许拷贝和赋值

不能将数组的内容拷贝给其他数组作为初始值,也不能用数组为其他数组赋值:

int a1[] = {0,1,2};
int a2[] = a;           // 错误
a2 = a1;                // 错误

有些编译器支持数组的赋值,即 编译器扩展(compiler extension)
但一般来说,最好避免使用非标准特性,因为含有非标准特性的程序可能在其他编译器上无法工作

复杂数组声明

int *ptrs[10];                  // 一个含有10个指针的数组 (int * [10] ptrs)
int &refs[10] = /* ... */;      // 错误,不存在引用的数组  (int & [10] refs)
int (* parr)[10] = &arr;        // rarr 指向一个含有10个整数的数组  (int[10] (* parr))
int (& rarr)[10] = arr;         // rarr 引用一个含有10个整数的数组  (int[10] (& rarr))

访问数组元素

使用数组下标的时候,通常将其定义为 size_t 类型,它是一种机器相关的无符号类型,它被设计的足够大以便能表示内存中任意对象的大小。在 cstddef 头文件中定义了 size_t 类型,这个文件是 C 标准库 stddef.h 头文件的 C++ 语言版本。

指针和数组

使用数组的时候编译器一般会把它转换成指针;但不能对数组进行赋值。

指针可以看做是数组的迭代器,其行为与迭代器非常相似。

标准库函数 begin 和 end

为了让指针的使用更简单、安全,C++11 新标准引入了两个名为 beginend 的函数:

int ia[] = {0,1,2,3};
int *begin = begin(ia);     // 指向 ia 的首元素
int *last = end(ia);        // 指向 ia 尾元素的下一个位置

使用 begin 和 end 可以很容易地写出一个循环并处理数组中的元素:

// 找出arr中的第一个负数:
int * pbegin = begin(arr), * end = end(arr);
while (pbegin != pend && * pbegin >= 0){
    ++pbegin;
}

指针运算

两个指针相加减时,两指针必须指向同一个数组中的元素。

指针相减的结果类型是一种名为 ptrdiff_t 的标准库类型,定义在 cstddef 头文件中;与机器相关的类型,可以为负值。

如果p是空指针,则允许给p加上或减去一个值为0的整型常量表达式;两个空指针相减的结果是0。

C 风格字符串

C 风格字符串不是一种类型,而是继承自 C 的关于字符串某种约定俗成的写法;在 C 中,一般用字符数组存放和操作字符串,并提供了一些 C 风格字符串的函数:

函数 说明
strlen(p) 返回p的长度,空字符不计算在内
strcmp(p1, p2) 比较相等性,按相同位置的字符依次比较
strcat(p1, p2) 连接两个字符串
strcpy(p1, p2) 把p2拷贝给p1,返回p1

比较字符串时,C 风格字符串不能直接比较,应该使用 strcmp(),而 C++ 的 string 类型的字符串可以直接比较。C 风格字符串若直接比较,实际上是对指针地址的比较:

string s1 = "a string example";
string s2 = "a different string";
cout << (s1<s2) << endl;            // false

const char ca1[] = "a string example";      // 末尾有个空字符
const char ca2[] = "a different string";
cout << (ca1<ca2) << endl;                  // 未定义行为,试图比较两个无关地址

与 C 风格字符串混用

  • 允许使用以空字符结束的字符数组来初始化 string 对象或为 string 对象赋值
  • 在 string 对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象
  • 在 string 对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象

在需要 C 风格字符串而无法使用 string 对象代替的地方,string 专门提供了一个名为 c_str 的成员函数:

const char * str = s.c_str();

注意,c_str() 函数返回的类型是不可变类型,并且 string 无法保证 c_str() 函数返回的数组一直有效,因为后续操作可能会改变字符串的内容。

如果想要 c_str() 函数的返回值一直有效,最好将其重新拷贝一份

使用数组初始化 vector 对象

需要指明要拷贝区域的首元素地址和尾后地址:

int int_arr[] = {0,1,2,3,5,6};
vector<int> ivec(begin(int_arr), end(int_arr)); // 全部拷贝
int * begin = begin(int_arr);
vector<int> ivec1(begin, begin+4);              // 部分拷贝 {0,1,2,3}

现代 C++ 程序应尽量使用 vector 和迭代器,避免使用内置数组和指针;
尽量使用 string,避免使用 C 风格的基于数组的字符串

多维数组

严格来说,C++ 没有多维数组,所谓多维数组其实是数组的数组。

posted @ 2021-06-22 22:17  keep-minding  阅读(102)  评论(0)    收藏  举报