委员长 发表于 2024-12-30 21:08:41

C++Primer学习笔记03-1.字符串、向量和数组

- 命名空间 using 声明
- 标准库 string,支持可变长字符串,由数组实现
- 标准库 vector,支持可变长序列,由数组实现
- 迭代器,string,vector 的迭代标准接口
- 数组
- 多维数组

### 命名空间using

每个 using 声明引入命名空间中的一个成员。例如,可以把要用到的标准库中的名字都以 using 声明的形式表示出来。

```cpp
#include<iostream>
using std::cout;
using std::endl;
int main(){
    cout<<"hello"<<endl; // hello
}
```

<b>每个名字都需要独立的 using 声明</b>

> <b>头文件不应包含 using 声明</b>

一般来说不应该使用 using 声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个 using 声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。

例如:

```cpp
// one.cpp
#include<iostream>
using std::cout;
using std::endl;

// two.cpp
#include"one.cpp"
int main(){
    cout<<"123"<<endl;
}
```

### 标准库string

标准库类型 string 表示可变长的字符序列,使用 string 类型必须首先包含 string 头文件。作为标准库的一部分,string 定义在命名空间 std 中。

```cpp
#include<string>
using std::string;
```

使用 string 的例子

```cpp
#include<iostream>

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

int main() {
    string str = "hello"; // hello
    cout << str << endl;
}
```

#### 定义和初始化string

string 初始化的方式有多种


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

> <b>直接初始化和拷贝初始化</b>

使用 = 的是拷贝初始化,其他的是直接初始化

```cpp
string s = "hello"; // 拷贝初始化
string s("hello"); // 直接初始化
string s(10,'c'); // 直接初始化,s 的内容是 cccccccccc

string ss = string(10,'c'); // 拷贝初始化
// 相当于
string tmp(10,'c');
string ss = tmp; // 将 tmp 拷贝给 ss
```

#### 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         | 返回 s 中第 n 个字符的引用,位置 n 从 0 计算                     |
| s1+s2          | 返回 s1 和 s2 连接后的结果                                       |
| s1=s2          | 用 s2 的副本代替 s1 中原来的字符                                 |
| s1==s2         | 如果 s1 和 s2 中所含的字符完全一样,则它们相等;<br>不忽略大小写哦 |
| s1!=s2         | 判断是否不相当,不忽略大小写                                       |
| <, <=, >, >=   | 比较大小,不忽略大小写                                             |

使用上述的多个操作来完成一系列字符串的操作。从键盘读入两个 string,先判断 string 是否为空,不为空则打印字符的个数,并逐个打印每个字符,然后比较字符的大小,并拼接字符串,大的放在前面。

```cpp
#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::string;

int main(){
    string s1;
    string s2;

    cout << "请输入第一个字符串" << endl;
    getline(cin, s1);
    cout << "请输入第二个字符串" << endl;
    cin >> s2;
    if (!s1.size() || !s2.size())
    {
      cout << "其中一个字符为空,程序结束" << endl;
      return 0;
    }

    // 输出 string 的大小
    cout << "s1.size=" << s1.size() << " s2.size=" << s2.size() << endl;

    // 逐个输出字符
    for(auto single : s1){
      cout<<single;
    }
    cout<<""<<endl;
    for(auto single : s2){
      cout<<single;
    }

    cout<<(s1>s2?s1+s2:s2+s1)<<endl;
}
```

触发 getline 函数返回的那个换行符实际上被丢弃掉了,得到的 string 对象中并不包含该换行符。

读取未知数量的 string 对象。刷题的时候用得到。

```cpp
int main(){
    string word;
    string ans("");
    while (cin>>word){
      ans.append(word);
    }
    cout<<ans<<endl;
    return 0;
}
```

> <b>一些小细节</b>

- string 类的 size 函数返回值的变量,是 string::size_type 类型的。可以用 auto 来接收 size 的返回值。
- string 用 ==,!=,> 这些比较时,是按逐个字符的字典顺序进行比较的。
- string 的 + 操作会得到一个新的 string 对象,应该是对 + 做了重载。

> <b>易错点</b>

字符串的相加

```cpp
void add(){
    string s1 = "hello",s2 = "world";
    string s3 = s1 + ", " + s2 + '\n';
    string s4 = s1 + ",";
    //‘const char ’ and ‘const char ’ to binary ‘operator+’
    string s5 = "hello" + ", "; // 错误
    string s6 = s1 +", "+ "world";
    //‘const char ’ and ‘const char ’ to binary ‘operator+’
    string s7 = "hello" + ", " + s2; // 错误:不能把字面量值直接相加
}
```

#### 处理string中的字符

如检查是否包含空白,字符大小写转换等。


| 函数      | 说明                                                |
| ------------- | ----------------------------------------------------- |
| isalnum(c)| 当 c 是字母或数字是为 true                        |
| isalpha(c)| 当 c 是字母时为 true                              |
| iscntrl(c)| 当 c 是控制字符时为 true                            |
| isdigit(c)| 当 c 是数字时为 true                              |
| isgraph(c)| 当 c 不是空格但可以打印时为 true                  |
| islower(c)| 当 c 是小写字母时为 true                            |
| isprint(c)| 当 c 是克打印字符时为 true                        |
| ispunct(c)| 当 c 是标点符号时为 true                            |
| isspace(c)| 当 c 是空白时为 true                              |
| isupper(c)| 当 c 是大写字母时为 true                            |
| isxdigit(c) | 当 c 是十六进制数字时为 true                        |
| tolower(c)| 当 c 是大写字母,输出对应的小写字母;否则原样输出 c |
| toupper(c)| 当 c 是小写字母,输出对应的大写字母;否则原样输出 c |

使用上述部分函数完成字符串小写转大写,并统计字母的个数。

```cpp
int main(){
    string str("hello world! This is Code!");
    // 小写转大写,并统计字母的个数
    int ans = 0;
    for(auto &single : str){
      single = toupper(single);
      if(isalpha(single)) ans++;
    }
    cout<<str<<endl;
    cout<<"共有:"<<ans<<" 个字母"<<endl;
}
```

string 也可以像数组一样使用 '[]' 访问。

```cpp
void access(){
    string str("hello");
    cout<<str<<endl;
    str = 'H';
    cout<<str<<endl;
}
```

像迭代器一样遍历 string 数组。

```cpp
void access(){
    string str("hello");
    // 拿到头指针
    auto start = begin(str);
    // 拿到尾指针,就是 o 后面的那个地址。
    auto last = end(str);
    while (start!=last){
      cout<<*start<<endl;
      start++;
    }
}
```

### 标准库vector

vector 一个可变长的容器。要想使用vector,必须包含适当的头文件。

```cpp
#include<vecotr>
using std::vector;
```

C++ 语言既有类模板(class template),也有函数模板,其中 vector 是一个类模板。模板的具体意义后面再讨论。现在只需要知道,编译器会根据模板创建类或函数,使用模板时,需要指出编译器应把类或函数实例化成何种类型。

```cpp
vector<int> vec1; // 存储 int 类型数据的容器
vector<vector<string>> vec2; // 存储 vector<string> 类型数据的容器
```

<span style="color:red">vector 是模板而非类型,由 vector 生成的类型必须包含 vector 中元素的类型,例如 vector\<int\>。</span>

vector 存储的是对象,而引用不是对象因此不存在包含引用的 vector。C++11 之前的复合 vector 需要这样写 `vector<vector<int> >`,最后两个 > 之间要有一个空格。

#### 定义和初始化vector对象


| 方式                        | 说明                                                               |
| ----------------------------- | -------------------------------------------------------------------- |
| vector\<T\> v1            | 创建一个元素类型为 T 的空 vector,执行默认初始化,不包含任何元素。 |
| vector\<T\> v2(v1)          | v2 包含 v1 中所有元素的副本                                        |
| vector\<T\> v2 = v1         | 等价于 v2(v1)                                                      |
| vector\<T\> v3(n, val)      | v3 包含 n 个重复的元素 val                                       |
| vector\<T\> v4(n)         | v4 包含了 n 个重复执行了值初始化的对象                           |
| vector\<T\> v5{a, b, c...}| v5 包含了指定值 a, b, c.. 这些元素                                 |
| vector\<T\> v5={a, b, c...} | 等价于 v5={a, b, c...}                                             |

<b>列表初始化 vecotr,C++11 提供</b>

```cpp
vector<string> articles = {"a", "an"};
```

<b>值初始化</b>

```cpp
vector<int> vec(10); // 10 个元素,都初始化为 0
```

<b>列表初始值还是元素数量</b>

```cpp
vector<int> v1(10); // v1 有 10 个元素,每个的值都是 0
vector<int> v2{5}; // v2 有一个元素 5

vector<int> v3(10,1); // v3 有 10 个相同的元素 1
vector<int> v4{10,1}; // v4 有两个元素 10 和 1
```

如果用的是圆括号,可以说提供的值是用来构造(construct)vector 对象的。

如果用的是花括号,可以表述成我们想列表初始化(list initialize)该 vector 对象。也就是说,初始化过程会尽可能地把花括号内的值当成是元素初始值的列表来处理,只有在无法执行列表初始化时才会考虑其他初始化方式。

```cpp
vector<string> v5{"hi"}; // 列表初始化:v5 有一个元素
vector<string> v6("hi"); // 错误,不能用字面量构建 vector 对象
vector<string> v7{10}; // v7 有 10 个默认初始化的元素
vector<string> v8{10, "hi"}; // v8 有 10 个值为 "hi" 的元素
```

注意 `vector<string> v8{10, "hi"};` 被解析成了:v8 有 10 个值为 "hi" 的元素,这也符合前面所说的 <span style="color:red">“花括号内的值当成是元素初始值的列表来处理,只有在无法执行列表初始化时才会考虑其他初始化方式。”</span>

```cpp
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::string;
using std::vector;

void init(){
    vector<vector<int>> vec;
    // vector<string> svec = vec; // error 类型不匹配
    vector<string> svec(10, "null");
    for (auto t : svec){
      cout<<t;
    }
}

int main(){
    init();
    return 0;
}
```

> 练习

下列的 vector 对象包含多少个元素?元素的值分别是多少?

```cpp
void practice(){
    vector<int> v1;   // 0 个
    vector<int> v2(10); // 10 个 0
    vector<int> v3(10, 42); // 10 个 42
    vector<int> v4{10}; // 1 个 10
    vector<int> v5{10, 42}; // 2 个,分别是 10,42
    vector<int> v6{10}; // 1 个 10
    vector<string> v7{10, "hi"};    // 10 个 hi

    // cout<<v1.size()<<":"<<v1<<"\t";
    cout<<v2.size()<<":"<<v2<<"\t";
    cout<<v3.size()<<":"<<v3<<"\t";
    cout<<v4.size()<<":"<<v4<<"\t";
    cout<<v5.size()<<":"<<v5<<"\t";
    cout<<v6.size()<<":"<<v6<<"\t";
    cout<<v7.size()<<":"<<v7<<"\t";
}
```

#### 添加元素

push_back,添加到尾部。向 vector 中添加 10 个数字。

```cpp
void push_back(){
    vector<int> ivec{-1,-2};
    for (size_t i = 0; i < 10; i++){
      ivec.push_back(i);
    }
    // 12
    cout<<ivec.size()<<endl;
}
```

<span style="color:red">注意:如果循环体内部包含有向 vector 对象添加元素的语句,则不能使用范围 for 循环。切记,范围 for 语句体内不应改变其所遍历序列的大小。</span>

> 练习

- 编写一段程序,用 cin 读入一组整数并把它们存入一个 vector 对象。
- 编写一段程序,用 cin 读入一组字符串并把它们存入一个 vector 对象。

```cpp
void add1(){
    int num;
    vector<int> ivec;
    while (cin>>num){
      ivec.push_back(num);
    }
    cout<<ivec.size()<<endl;
}

void add2(){
    string str;
    vector<string>svec;
    while (cin>>str){
      svec.push_back(str);
    }
    cout<<svec.size()<<endl;
}
```

#### 其他操作


| 操作         | 说明                                                            |
| ---------------- | ------------------------------------------------------------------- |
| v.empty()      | 如果 v 不含有任何元素,返回 true,否则 false                      |
| v.size()       | 返回 v 中元素的个数                                             |
| v.push_back(t) | 向 v 的尾部添加一个值为 t 的元素                                  |
| v         | 返回 v 中第 n 个位置上的元素的引用                              |
| v1=v2          | 用 v2 中元素的拷贝替换 v1 中的元素                              |
| v1={a,b,c....} | 用列表中元素的拷贝替换 v1 中的元素                              |
| v1==v2         | 当元素数量相等,且对应位置元素的值都相同返回 true               |
| v1!=v2         |                                                                   |
| <, <=, >, >=   | 按字典顺序比较<br>只有当元素的值可比较时,vector 对象才能被比较。 |

拿到 vector 元素的引用,然后修改它的值

```cpp
void otherOp(){
    vector<string> svec{"hello", "world"};
    string &c = svec;
    c = "111";
    cout<<svec<<endl; // 111
}
```

vector 的 empty 和 size 两个成员与 string 的同名成员功能完全一致:empty 检查 vector 对象是否包含元素然后返回一个布尔值;size 则返回 vector 对象中元素的个数,返回值的类型是由 vector 定义的 size_type 类型。

```cpp
vector<int>::size_type; // 正确
vector::size_type; // 错误
```

vector 的比较,只有当 vector 内部的元素可以比较时才行

```cpp
class D{}

void cmp(){
    vector<D> dvec1{new D,new D,new D};
    vector<D> dvec2{new D,new D,new D};
    cout<<(dvec1>dvec2)<<endl; // 错误,D 没有定义比较规则,不能比较
}
```

> <b>可以使用下标访问 vector 内的元素</b>

```cpp
void otherOp(){
    vector<string> svec{"hello", "world"};
    string &c = svec;
    c = "111";
    cout<<svec<<endl; // 111
}
```

> <b>不能用下标形式添加元素</b>

```cpp
vector<int> ivec{0,0,0};
ivec = 100; // 正确
ivec = 10; // 错误,不能用 [] 添加元素
cout<<ivec<<endl;
```

> 习题

```cpp
void code(){
    vector<int> ivec;
    int count = 10;
    for (size_t i = 0; i < count; i++){
      ivec.push_back(i*i);
    }

    while (!ivec.empty()){
      ivec.pop_back();
    }
    cout<<ivec.size();
}
```

### 迭代器

用统一的方式去遍历容器中的元素,数组中也可以变相使用迭代器,用 begin、end 函数拿到数组的头尾指针即可。

#### 使用迭代器遍历

> 在数组中使用迭代器

```cpp
#include <iostream>
using std::cout;
using std::endl;
using namespace std;

int main(){
    int arr[] = {1, 2, 34, 5, 6, 7, 7, 8};
    auto start = begin(arr);
    auto last = end(arr);
    while (start != last){
      cout << *start << endl;
      start++;
    }
}
```

> 在 string 中使用迭代器

```cpp
#include <iostream>
#include<string>
using std::cout;
using std::endl;
using std::string;
using namespace std;

int main(){
    string str("hello");
    auto start = str.begin();
    auto last = str.end();
    while (start!=last){
      cout<<*start<<"\t";
      start++;
    }
}
```

> 在 vector 中使用迭代器

```cpp
#include <iostream>
#include<string>
#include<vector>
using std::cout;
using std::endl;
using std::string;
using std::vector;

int main(){
    vector<string> svec{2, "hello"};
    auto start = svec.begin();
    auto last = svec.end();
    while (start!=last){
      cout<<*start<<"\t";
      start++;
    }
}
```

<span style="color:red">注意:因为 end 返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作。</span>

#### 迭代器的说明

建议使用迭代器对数组,容器等进行遍历,这样就可以用统一的方式遍历各种元素

```cpp
while(start!=last){
    // some code;
}
```

迭代器类似于指针,可以通过 ++ 移动到下一个元素的位置,使用 \* 来拿到元素。

标准容器迭代器的运算符


| 操作         | 说明                                                            |
| ---------------- | ----------------------------------------------------------------- |
| *iter          | 返回迭代器 iter 所指元素的引用                                  |
| iter->mem      | 解引用 iter 并获取该元素的名为 mem 的成员<br>等价于 (*iter).mem |
| ++iter         | 令 iter 指示容器中的下一个元素                                  |
| --iter         | 令 iter 指示容器中的上一个元素                                  |
| iter1 == iter2 | 判断两个迭代器是否相等,相等则说明指向的同一个元素            |
| iter1 != iter2 | 判断两个迭代器是否不相等                                        |

> <b>迭代器类型</b>

一般来说我们也不知道(其实是无须知道)迭代器的精确类型。而实际上,那些拥有迭代器的标准库类型使用 iterator 和 const_iterator 来表示迭代器的类型。

```cpp
vector<int>::iterator it1; // it1 能读写 vector<int> 的元素
string::iterator it2;           // it2 能读写 string 的元素

vector<int>::const_iterator it3; // it3 只能读元素,不能写元素
string::const_iterator it4;        // it4 只能读元素,不能写元素
```

> <b>begin 和 end 运算符</b>

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

```cpp
vector<int> v;
const vector<int> cv;

auto it1 = v.begin(); // it1 的类型是 vector<int>::iterator
auto it2 = cv.bengin(); // it2 的类型是 vector<int>::const_iterator
```

> <b>C++11 的 cbegin 和 cend</b>

有时候我们只需要读取元素,且不希望给与它被修改的可能,为了便于专门得到 const_iterator 类型的返回值,C++11 新标准引入了两个新函数,分别是 cbegin 和 cend

```cpp
auto it3 = v.cbegin(); // it3 的类型是 vector<int>::const_iterator
```

类似于 begin 和 end,上述两个新函数也分别返回指示容器第一个元素或最后元素下一位置的迭代器。有所不同的是,不论 vector 对象(或 string 对象)本身是否是常量,返回值都是 const_iterator。

> <b>迭代器的运算</b>

解引用,++,--,+n,iter1 == iter2 之类的,和前面说的类似,不再赘述。

```cpp
// 利用迭代器完成二分查找
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; // 新的中间点
}
```
页: [1]
查看完整版本: C++Primer学习笔记03-1.字符串、向量和数组