C++ primer plus笔记 --- 第16章、string类和标准模板库

16.1、string类

在C++中,string 是一个非常常用的字符串类,可以方便的进行字符串操作。

string类可以和C语言中的字符串进行交互,例如:

#include <iostream>
#include <string>
#include <cstring>

using namespace std;

int main() {
    // 从C语言字符串创建string对象
    char s[] = "hello, world!";
    string str(s);

    // 从string对象创建C语言字符串
    const char* cstr = str.c_str();
    cout << cstr << endl;

    // 操作string对象
    str.append(" Welcome to C++!");
    cout << str << endl;

    return 0;
}

在使用 string 时,常用的操作函数有:

  • append:在字符串末尾追加一段字符串。
  • replace:替换字符串中的一段子串。
  • insert:在字符串的指定位置插入一段字符。
  • erase:删除字符串中的一段字符。
  • find:查找字符串中的某段子串。
  • substr:截取字符串中的一段子串。
  • size:获取字符串的长度。
  • empty:判断字符串是否为空。
  • clear:清空字符串内容。

例如:

#include <iostream>
#include <string>

using namespace std;

int main() {
    string str = "Hello, World!";
    str.append(" Welcome to C++!");   // 在字符串末尾追加一段字符串
    cout << str << endl;             // 输出:Hello, World! Welcome to C++!
    str.replace(7, 5, "China");      // 将第7个字符开始的5个字符替换为“China”
    cout << str << endl;             // 输出:Hello, China! Welcome to C++!
    str.insert(5, " the People's Republic of");  // 在第5个字符插入一段字符串
    cout << str << endl;             // 输出:Hello, the People's Republic of China! Welcome to C++!
    str.erase(5, 26);                // 删除第5个字符开始的26个字符
    cout << str << endl;             // 输出:Hello!!
    size_t pos = str.find("lo");     // 查找字符串中是否包含“lo”,并返回第一次出现的位置
    cout << "Find \"lo\" in str at position " << pos << endl; // 输出:Find "lo" in str at position 3
    string sub = str.substr(3, 4);   // 截取从第3个字符开始的4个字符
    cout << sub << endl;             // 输出:lo!!
    cout << "size: " << str.size() << endl;  // 获取字符串的长度
    cout << "empty: " << str.empty() << endl; // 判断字符串是否为空
    str.clear();                     // 清空字符串
    cout << "empty: " << str.empty() << endl;

    return 0;
}

需要注意的是,string 本质上是一个类,它封装了字符串的操作。因此,在进行字符串操作时,建议使用 string 类,而不是裸指针或 char 数组,可以避免指针的问题和内存管理的复杂性。

16.1.1、构造字符串

在C++中,可以使用 string 类来构造字符串。

1、直接赋值

string str = "hello, world!";

2、从C语言字符串创建

char cstr[] = "hello, world!";
string str(cstr);

3、复制构造函数

string str1 = "hello, world!";
string str2(str1);

4、移动构造函数

string str1 = "hello, world!";
string str2(std::move(str1));

5、通过重复字符构造

string str(10, 'a');    // 构造一个包含10个'a'字符的字符串

需要注意的是,string 类支持隐式类型转换,因此可以将一个 char 类型的字符或一个 const char* 类型的字符串常量赋值给一个 string 对象。但是,在进行字符串操作时,建议尽量使用 string 类的成员函数,以避免不必要的隐式类型转换和指针操作带来的问题。

16.1.2、string类输入

在 C++ 中,可以使用 std::cin 对象读取控制台输入的字符串,也可以使用 std::getline() 函数读取标准输入流中的一行字符串并存储到 std::string 对象中。

1、使用 std::cin 读取字符串

#include <iostream>
#include <string>

int main()
{
    std::string str;

    std::cout << "Enter a string: ";
    std::cin >> str;

    std::cout << "You entered: " << str << std::endl;

    return 0;
}

在上述示例中,使用 std::cin 读取输入的字符串,然后将其存储到 std::string 对象中,并最后输出字符串。

需要注意的是,使用 std::cin 读取输入字符串时,遇到空格或换行符会停止读取。

2、使用 std::getline() 读取一行字符串

#include <iostream>
#include <string>

int main()
{
    std::string str;

    std::cout << "Enter a string: ";
    std::getline(std::cin, str);

    std::cout << "You entered: " << str << std::endl;

    return 0;
}

在上述示例中,使用 std::getline() 函数读取输入流中的一行字符串并存储到 std::string 对象中,并最后输出字符串。

需要注意的是,在使用 getline() 读取一行字符串时,它会读取输入流中的所有字符,直到遇到换行符 \n 或文件结束符 EOF。在读取过程中,getline() 会将换行符或文件结束符从输入流中移除,但不会将其存储到字符串中。如果要保留换行符或文件结束符,可以使用 std::cin.getline() 函数。

16.1.3、使用字符串

在 C++ 中,可以使用 std::string 类型的对象来操作字符串。 std::string 类型提供了对字符串的各种操作,例如连接(append)、查找(find)、截取(substr)、替换(replace)等等。

下面是一些常见的字符串操作的示例:

#include <iostream>
#include <string>

int main() {
    std::string str1 = "Hello";
    std::string str2 = ", World!";
    std::string str3 = str1 + str2;  // 字符串连接
    std::cout << "str3 = " << str3 << std::endl;

    std::string str4 = "World";
    std::size_t pos = str3.find(str4);  // 查找子串,返回子串在主串中的位置
    if (pos != std::string::npos) {
        std::cout << "Found at position " << pos << std::endl;
    } else {
        std::cout << "Not Found" << std::endl;
    }

    std::string str5 = str3.substr(0, 5);  // 截取字符串,从位置0开始,长度为5
    std::cout << "str5 = " << str5 << std::endl;

    str3.replace(pos, str4.size(), "Kit");  // 替换子串为指定的字符串
    std::cout << "Replaced str3 = " << str3 << std::endl;

    return 0;
}

输出结果为:

str3 = Hello, World!
Found at position 7
str5 = Hello
Replaced str3 = Hello, Kit!

需要注意的是,std::string 中的所有字符都是有符号类型 char,而不是无符号类型 unsigned char 或 char16_tchar32_t。 另外,std::string 中的字符是通过动态分配内存来管理的,因此需要注意字符串长度和内存分配的问题。

16.1.4、string还提供了哪些功能

除了基本的字符串操作以外,std::string 类型还提供了一些其他的功能,例如:

1、字符串比较

std::string 类型提供了各种字符串比较函数,例如 compare(),可以比较两个字符串是否相等、大小等等。示例如下:

#include <iostream>
#include <string>

int main() {
    std::string str1 = "Hello, World!";
    std::string str2 = "Hello, World!";

    if (str1.compare(str2) == 0) {
        std::cout << "str1 and str2 are equal" << std::endl;
    } else {
        std::cout << "str1 and str2 are not equal" << std::endl;
    }

    if (str1 > str2) {
        std::cout << "str1 is greater than str2" << std::endl;
    } else if (str1 < str2) {
        std::cout << "str1 is less than str2" << std::endl;
    } else {
        std::cout << "str1 and str2 are equal" << std::endl;
    }

    return 0;
}

输出结果为:

str1 and str2 are equal
str1 and str2 are equal

2、字符串大小写转换

std::string 类型还提供了字符串大小写转换的函数,例如 tolower()toupper()。示例如下:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";

    for (std::size_t i = 0; i < str.size(); i++) {
        if (std::isupper(str[i])) {  // 判断字符是否为大写字母
            str[i] = std::tolower(str[i]);  // 转换为小写字母
        } else if (std::islower(str[i])) {  // 判断字符是否为小写字母
            str[i] = std::toupper(str[i]);  // 转换为大写字母
        }
    }

    std::cout << "str = " << str << std::endl;

    return 0;
}

输出结果为:

str = hELLO, wORLD!

3、字符串格式化输出

std::string 类型可以通过 sprintf() 函数格式化字符输出。示例如下:

#include <iostream>
#include <string>
#include <cstdio>

int main() {
    int n = 10;
    double d = 3.1415926;
    char buf[100];

    std::sprintf(buf, "n = %d, d = %f", n, d);
    std::string str = buf;

    std::cout << "str = " << str << std::endl;

    return 0;
}

输出结果为:

str = n = 10, d = 3.141593

需要注意的是,使用 sprintf() 函数进行字符串格式化输出时,需要注意输出的缓冲区大小,以防止缓冲区溢出。可以使用 snprintf() 函数指定缓冲区大小。

16.1.5、字符串种类

在 C++ 中,有很多不同的字符串类型可以使用。下面是一些常见的字符串类型。

1、char 数组

使用 char 数组来表示字符串,例如:

char str[] = "Hello, World!";

需要注意的是,char 数组是以空字符(\0)作为字符串结尾的。

2、const char*

使用 const char* 指针类型来表示字符串,例如:

const char* str = "Hello, World!";

需要注意的是,虽然 const char* 指针指向了一个字符串,但是不能直接修改字符串的内容。

3、std::string

使用 std::string 类型来表示字符串,例如:

std::string str = "Hello, World!";

相比于 char 数组和 const char* 指针,std::string 类型提供了更多的字符串操作和更好的内存管理。

4、wchar_t 和 std::wstring

使用 wchar_t 数组和 std::wstring 类型来表示宽字符(wide character)字符串。相比于 char 类型的字符串,宽字符字符串可以表示更多的字符集,例如中文字符等。例如:

wchar_t wstr[] = L"你好,世界!";
std::wstring wstr2 = L"Hello, World!";

需要注意的是,在使用宽字符字符串时,需要使用宽字符类型的字符串操作函数,例如 std::wstring::size()std::wstring::append() 等等。

5、UTF-8 和 Unicode 字符串

UTF-8 和 Unicode 字符串是表示多种字符集的字符串类型。UTF-8 是一种变长的字符编码,可以表示世界上几乎所有的字符。Unicode 是一种字符集,包括了世界上几乎所有的字符。例如:

std::string utf8str = u8"你好,世界!";
std::wstring unicodeStr = L"Hello, World!";

需要注意的是,在使用 UTF-8 和 Unicode 字符串时,需要使用对应的字符串操作函数,例如在使用 UTF-8 字符串时,需要使用 std::string::size()std::string::append() 等等函数。

16.2、智能指针模板类

智能指针是一种 C++ 的普通指针的包装,它的目的是管理指针指向的内存资源,避免内存泄漏等问题。智能指针是 C++11 中引入的新特性之一,其主要包括以下几种类型:

1、std::unique_ptr

std::unique_ptr 是一种独占式的智能指针,它可以管理一个指向动态分配对象的指针。它的特点是不能共享指针的所有权,只有一个 unique_ptr 可以拥有它所指向的对象的所有权,当这个 unique_ptr 被销毁时,它所指向的对象也会被自动销毁。std::unique_ptr 不能进行复制或赋值,但可以进行移动操作。

2、std::shared_ptr

std::shared_ptr 是一种共享式的智能指针,它可以管理一个指向动态分配对象的指针,多个 shared_ptr 可以共享指向同一个对象的所有权。std::shared_ptr 使用引用计数来维护对象的生命周期,当最后一个 shared_ptr 被销毁时,它所指向的对象也会被自动销毁。std::shared_ptr 可以进行复制或赋值,每个 shared_ptr 持有一个指向对象的指针和一个指向引用计数的指针。

3、std::weak_ptr

std::weak_ptr 是一种弱引用的智能指针,它可以解决 std::shared_ptr 的循环引用问题。std::weak_ptr 指向一个由 std::shared_ptr 管理的对象,但是它并不会增加该对象的引用计数。因此,当最后一个指向该对象的 shared_ptr 被销毁时,该对象就会被销毁,即使有 weak_ptr 指向该对象也不会影响该对象的销毁。

智能指针的使用让程序员不用手动管理内存,减少了内存泄漏和程序错误的可能性,并且使代码更加简洁。

16.3、标准模板类

C++ 标准库提供了许多标准模板类,在使用 C++ 标准库时,使用这些模板类可以简化开发工作并提高编程效率。以下是一些常见的标准模板类:

1、std::vector

std::vector 是动态数组容器,它可以存储任意类型的对象,并且可以根据需要动态调整容器的大小。std::vector 内部实现了自动内存管理和内存分配,因此可以避免手动分配和释放内存的繁琐工作。std::vector 可以通过 push_back() 和 pop_back() 操作在末尾添加和删除元素,也可以通过下标操作符 [] 访问容器中的元素。

2、std::list

std::list 是双向链表容器,它可以存储任意类型的对象,并且可以在任意位置快速添加或删除元素。std::list 内部实现了自动内存管理和内存分配,因此可以避免手动分配和释放内存的繁琐工作。std::list 可以通过 push_back()push_front() 和 pop_back()pop_front() 操作在容器的前后添加和删除元素。

3、std::map

std::map 是键值对容器,它可以存储任意类型的对象,并且可以通过键值快速查找和访问容器中的元素。std::map 内部实现了自动内存管理和内存分配,因此可以避免手动分配和释放内存的繁琐工作。std::map 存储的元素是按照键来排序的,因此可以通过 begin()end() 迭代器实现有序遍历。

4、std::set

std::set 是关键字集合容器,它可以存储任意类型的对象,并且可以通过关键字快速查找和访问容器中的元素。std::set 内部实现了自动内存管理和内存分配,因此可以避免手动分配和释放内存的繁琐工作。std::set 存储的元素是按照关键字来排序的,因此可以通过 begin()end() 迭代器实现有序遍历。

5、std::string

std::string 是字符串容器,它可以存储任意长度的字符串,并且支持字符串操作,例如拼接、截取、查找等等。std::string 内部实现了自动内存管理和内存分配,因此可以避免手动分配和释放内存的繁琐工作。std::string 是使用频率非常高的容器类型之一,也是 C++ 标准库中功能非常强大的标准模板类之一。

以上只是一些常见的标准模板类,实际上,C++ 标准库提供了更多的标准模板类,例如 std::dequestd::stackstd::queuestd::priority_queue 等等,每种容器都有其特点和优势,可以根据需要选择和使用。

16.3.1、模板类vector

std::vector 是 C++ 标准库中的一种序列式容器,它是一个动态数组,可以存储任意类型的对象,并且可以根据需要动态调整容器的大小。std::vector 内部实现了自动内存管理和内存分配,因此可以避免手动分配和释放内存的繁琐工作。以下是一些 std::vector 的常见使用方法和特点:

1. 定义及初始化

#include <vector>

std::vector<int> v1; // 默认初始化为一个空的 int 类型 vector
std::vector<int> v2(10); // 初始化为一个包含 10 个元素的 int 类型 vector,每个元素的值都是 0
std::vector<int> v3(10, 1); // 初始化为一个包含 10 个元素的 int 类型 vector,每个元素的值都是 1
std::vector<int> v4 = {1, 2, 3, 4, 5}; // 使用花括号列表初始化 vector
std::vector<int> v5(v4.begin(), v4.end()); // 通过迭代器 v4.begin() 和 v4.end() 初始化 vector

2. 元素的访问

std::vector<int> v = {1, 2, 3, 4, 5};
int i = v[2]; // 访问第三个元素,即 3
int j = v.at(4); // 访问第五个元素,即 5
int k = v.front(); // 访问首元素,即 1
int l = v.back(); // 访问尾元素,即 5

3. 元素的插入和删除

std::vector<int> v = {1, 2, 3, 4, 5};

v.push_back(6); // 在末尾添加一个元素 6
v.pop_back(); // 删除末尾一个元素 5

v.insert(v.begin() + 2, 7); // 在第三个位置插入元素 7
v.erase(v.begin() + 3); // 删除第四个元素 4

v.clear(); // 清空 vector,删除所有元素

4. 容器的大小和容量

std::vector<int> v = {1, 2, 3, 4, 5};

int size = v.size(); // 获取 vector 当前的元素数量,即 5
int capacity = v.capacity(); // 获取 vector 当前的容量,即至少 5

5. 遍历 vector

可以使用迭代器来遍历 vector 元素:

std::vector<int> v = {1, 2, 3, 4, 5};

for (auto it = v.begin(); it != v.end(); ++it) {
  std::cout << *it << " ";
}

也可以使用 C++11 的简洁方式:

std::vector<int> v = {1, 2, 3, 4, 5};

for (int i : v) {
  std::cout << i << " ";
}

6. 注意事项

  1. std::vector 可以通过动态分配内部缓存来管理元素,因此会存在增删元素带来内存重分配的开销,应当注意在频繁的大规模插入和删除操作时,应当事先预分配适当大小的容器以提高插入删除效率。

  2. 同时,需要注意避免空间的浪费,可以使用 shrink_to_fit() 方法进行收缩,释放没用到的内存空间。

16.3.2、可对矢量执行的操作

std::vector 是一个非常常用的容器,支持多种操作,以下是一些可对 std::vector 执行的操作:

  1. 元素访问:可以使用下标运算符 [] 或 at() 方法访问 std::vector 中的元素,也可以使用 front() 方法访问第一个元素,使用 back() 方法访问最后一个元素。

  2. 元素插入和删除:可以使用 push_back() 方法在 std::vector 的末尾插入元素,使用 insert() 方法在指定位置插入元素,使用 emplace_back() 或 emplace() 方法在末尾或指定位置就地构造元素,可以使用 pop_back() 方法删除最后一个元素,使用 erase() 方法删除指定位置或者指定范围的元素,使用 clear() 方法删除所有元素。

  3. 容量操作:可以使用 size() 方法获取 std::vector 中元素的数量,使用 capacity() 方法获取 std::vector 内部存储元素的空间大小,使用 reserve() 方法可以预留内存空间以避免不必要的内存重分配,使用 shrink_to_fit() 方法释放未使用的内存空间。

  4. 迭代器:可以使用 begin() 和 end() 方法获取指向 std::vector 开始和结束的迭代器,使用 rbegin() 和 rend() 方法获取指向 std::vector 末尾和开头的反向迭代器,还可以使用 cbegin()cend()crbegin() 和 crend() 方法获取指向 std::vector 开始和结束(反向)的常量迭代器。

  5. 容器之间的赋值和比较:可以使用 = 或者拷贝构造函数将一个 std::vector 赋值给另一个 std::vector,也可以使用 ==!=<<=> 和 >= 等运算符进行比较。

  6. 元素的重排:可以使用 sort() 方法对 std::vector 中的所有元素进行排序,使用 reverse() 方法将 std::vector 内部的元素反转。

  7. 其他操作:还有一些其他的操作,如 emplace_front()emplace_back()resize()swap() 等方法,可以根据实际需要选择使用。

总之,std::vector 提供了丰富的操作和方法,能够满足各种不同场景下的需求。

16.3.3、对矢量可执行的其他操作

除了 std::vector 内置支持的操作外,还可以使用算法库中的一些函数对 std::vector 进行其他操作。

  1. 查找:可以使用算法库中的 std::find() 函数在 std::vector 中查找指定值是否存在,也可以使用 std::find_if() 函数在 std::vector 中通过自定义的谓词判断是否存在符合条件的元素。

  2. 排序:除了 std::vector 的 sort() 方法外,也可以使用算法库中的 std::sort() 函数对 std::vector 排序。 std::sort() 提供了多个版本,可以通过提供自定义的比较函数来实现不同的排序方式。

  3. 删除/替换/变换元素:可以使用算法库中的 std::remove()std::replace()std::transform() 等函数来对 std::vector 中的元素进行删除、替换、变换等操作。

  4. 合并/拆分:可以使用算法库中的 std::merge()std::inplace_merge()std::partition()std::partition_copy() 等函数来对 std::vector 进行合并、拆分等操作。

  5. 统计:可以使用算法库中的 std::count()std::count_if()std::accumulate() 等函数来对 std::vector 进行统计操作。

  6. 遍历:可以使用 for 循环遍历 std::vector 中的元素,也可以使用算法库中的 std::for_each() 函数来对 std::vector 进行遍历操作。

  7. 匹配:可以使用算法库中的 std::search()std::find_first_of()std::adjacent_find() 等函数来在 std::vector 中查找符合要求的子序列。

除了以上操作之外,还可以使用相应的数据结构来对 std::vector 进行进一步的操作,如使用堆来对 std::vector 进行排序、使用有序哈希表来对 std::vector 进行去重等操作,具体将根据具体情况而定。

16.3.4、基于范围的for循环(C++11)

基于范围的 for 循环(Range-Based for Loop)是 C++11 引入的一种新的循环结构,它可以更方便地遍历一个序列,包括 std::vectorstd::initializer_liststd::array、C 数组等。该循环语法如下:

for (auto& element : range) 
{
    // 循环体
}

其中,element 是 range 中的每一个元素的引用或值(根据 range 的类型以及循环变量的类型而定),auto 关键字会自动判断循环变量的类型。

通过基于范围的 for 循环,可以简化遍历 std::vector 的代码,例如:

#include <iostream>
#include <vector>

int main() 
{
    std::vector<int> v {1, 2, 3, 4, 5};
    for (int i : v) 
    {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    return 0;
}

输出结果将为:

1 2 3 4 5

基于范围的 for 循环相比传统的索引循环和迭代器循环,使用起来更加简洁、易读,并且不需要手动管理循环变量的初始化、判断和自增等过程,提高了代码的可读性和可维护性。

16.4、泛型编程

泛型编程(Generic Programming)是一种编程方法论,强调编写独立于特定数据类型的可复用代码。通过使用模板和类型参数,泛型编程可以实现代码的抽象和通用化,降低代码的重构成本和增加代码的可重用性。C++ 是一种支持泛型编程的面向对象语言,其模板系统是该语言中泛型编程的基础。

泛型编程的核心思想是将数据结构、算法和函数的实现与使用解耦,使得代码的可复用性和通用性更强,下面分别从数据结构、算法和函数三个方面来介绍泛型编程。

1. 数据结构

泛型编程可以实现可复用的数据结构模板,使得开发者能够快速构建出各种需要的数据结构。例如,C++ STL 中的 std::vectorstd::list 和 std::map 等容器,就是使用泛型编程构建的。

以 std::vector 为例,其实现方式是使用一个动态数组实现,可以容纳任意类型的对象,因此,使用 std::vector 时,可以为其指定任意的数据类型,例如:

#include <iostream>
#include <vector>

int main() {
  std::vector<int> v1 {1, 2, 3, 4, 5};
  std::vector<double> v2 {1.1, 2.2, 3.3, 4.4, 5.5};
  std::vector<char> v3 {'a', 'b', 'c', 'd', 'e'};

  for (int i : v1)
    std::cout << i << " ";
  std::cout << std::endl;

  for (double d : v2)
    std::cout << d << " ";
  std::cout << std::endl;

  for (char c : v3)
    std::cout << c << " ";
  std::cout << std::endl;

  return 0;
}

输出结果为:

1 2 3 4 5 
1.1 2.2 3.3 4.4 5.5 
a b c d e 

2. 算法

泛型编程还可以实现可复用的算法模板,使得开发者能够快速构建出各种需要的算法。例如,在 C++ STL 中,提供了大量的算法函数,如排序算法、查找算法、数值算法等,都是使用泛型编程构建的。

以 std::sort 排序函数为例,其实现方式使用了模板和比较函数,因此,可以对任意类型的元素进行排序,例如:

#include <iostream>
#include <algorithm>
#include <vector>

int main() {
  std::vector<int> v {5, 4, 3, 2, 1};

  std::sort(v.begin(), v.end());
  
  for (int i : v)
    std::cout << i << " ";
  std::cout << std::endl;

  return 0;
}

输出结果为:

1 2 3 4 5

3. 函数

泛型编程还可以实现可复用的函数模板,可以减少代码的重复,提高代码的复用性和可维护性。例如,在 C++ STL 中,提供了一些可复用的函数模板,如 std::swapstd::min/maxstd::accumulate 等,它们都是使用泛型编程构建的。

以 std::swap 函数为例,其实现方式使用了模板和引用,因此,可以对任意类型的变量进行交换操作,例如:

#include <iostream>
#include <utility>

int main() {
  int x = 5, y = 10;
  
  std::swap(x, y);

  std::cout << x << " " << y << std::endl;

  return 0;
}

输出结果为:

10 5

总的来说,泛型编程可以适用于各种场景,包括数据结构、算法和函数等,其优点包括提高代码的复用性和可维护性,减少代码的重复以及提高开发效率等。

16.4.1、为何使用迭代器

迭代器(Iterator)是一种使得数据容器能够被遍历的重要工具。在C++中,迭代器是一种泛型编程工具,它的设计方便对各种数据容器进行统一的遍历操作,常用于迭代访问 STL 容器内的元素。使用迭代器可以大大提高程序的代码复用性和灵活性,以下是使用迭代器的几个好处:

  1. 遍历容器元素 :使用迭代器可以遍历整个容器中的元素,而不必关心容器的内部实现。每个容器都有对应的迭代器类型,可以使用迭代器对容器中的元素进行访问和操作。

  2. 简化容器和算法的耦合 :使用迭代器可以将算法与实际容器的实现解耦。容器的实现方式可能会发生变化,但是迭代器作为对容器的抽象访问方式,可以保持不变,因此这种方式可以提高代码的可维护性和可扩展性。

  3. 提供算法的简单性 :使用迭代器,可以很容易地实现算法的实现,例如在 STL 中,提供了很多算法函数,如排序、查找、替换、计数等,这些算法函数的参数都是迭代器,这使得开发人员可以轻松地使用这些算法。

  4. 动态扩展数据类型 :使用迭代器可以提高代码的复用性和灵活性。例如,如果需对以下两个容器中的元素进行比较,使用迭代器可以编写一个通用的算法,而无需针对容器进行特定实现。

std::vector<int> v{1, 2, 3, 4, 5};
std::list<double> l{1.1, 2.2, 3.3, 4.4, 5.5};

所以迭代器是一个非常实用的工具,可以提高程序的灵活性和代码的可维护性,也是 C++ STL 模板库中最为核心和常用的组件之一。

16.4.2、迭代器类型

C++ STL 中内置了多种迭代器类型,不同类型的迭代器在实现上略有差异,下面是常用的迭代器类型:

  1. 输入迭代器(Input Iterator):输入迭代器用于从容器中读取数据,但不允许修改容器中的数据。输入迭代器只支持单遍遍历。

  2. 输出迭代器(Output Iterator):输出迭代器用于向容器中写入数据,但不能读取容器中数据。输出迭代器也只支持单遍遍历。

  3. 前向迭代器(Forward Iterator):前向迭代器支持单向遍历容器中的元素,支持读取和写入。前向迭代器只支持单遍遍历。

  4. 双向迭代器(Bidirectional Iterator):双向迭代器支持双向遍历容器中的元素,除了支持前向迭代器的读取和写入,还支持反向遍历和删除元素等操作。

  5. 随机访问迭代器(Random Access Iterator):随机访问迭代器是最为强大的迭代器类型,支持快速跳转和随机访问,所以可以用来实现各种算法。除了支持双向迭代器的操作,还支持加减运算、比较和数组下标等操作。

  6. 流迭代器(Stream Iterator):流迭代器用于将容器中的数据输出到流(例如标准输出流,文件输出流等)或从流中读取数据,它们实际上是一种输出迭代器或输入迭代器。流迭代器可以用于将数据从容器中打印到屏幕上或写入文件中。

以下是常见容器支持的迭代器类型:

容器迭代器类型
vector随机访问迭代器
deque随机访问迭代器
list双向迭代器
stack不支持迭代器
queue不支持迭代器
priority_queue不支持迭代器
set双向迭代器
map双向迭代器

在使用迭代器遍历 STL 容器时,通常需要根据容器类型选择对应的迭代器类型,并根据需要选择合适的遍历方式。

16.4.3、迭代器层次结构

STL 中的迭代器层次结构可以分为五个等级,分别是:

  1. 输入迭代器(Input Iterator):只能从容器中读取数据,无法修改容器中的数据。适用于单遍遍历,如 istream_iterator。

  2. 输出迭代器(Output Iterator):只能向容器中写入数据,无法读取容器中的数据。适用于单遍遍历,如 ostream_iterator。

  3. 前向迭代器(Forward Iterator):支持从前往后遍历容器中的元素,并且可以读取或者修改容器中的元素,但每个元素只能被遍历一次,适用于单向链表等数据结构,如 forward_list。

  4. 双向迭代器(Bidirectional Iterator):将前向迭代器的功能扩展为可以反向遍历容器中的元素,即支持在双向链表中向前或向后遍历,如 list。

  5. 随机访问迭代器(Random Access Iterator):将双向迭代器的功能扩展为支持随机访问容器中的元素,可以通过下标访问容器中的任意元素,如 vector 和 deque。

在迭代器层次结构中,每一级迭代器都包含前一个级别迭代器的全部功能,并在此基础上增加了新的功能。因此,使用高级别的迭代器可以实现更复杂和高效的操作。

16.4.4、概念、改进和模型

在C++ STL中,除了迭代器层次结构外,还有一些其他概念、改进和模型,包括:

  1. 迭代器对(Iterator Pair):由两个迭代器组成的序列,其中一个迭代器指向序列的起始位置,另一个迭代器指向序列的末尾位置。在STL中经常通过迭代器对来表示待操作区间,如pair< vector<int>::iterator, vector<int>::iterator >。

  2. 分类算法(Partitioning Algorithm):将容器中的元素按照某个谓词分成两个部分,如stable_partition和partition函数。分区算法通常返回迭代器对,表示容器中被分为两个部分的位置。

  3. 堆算法(Heap Algorithm):将容器中的元素抽象成一个堆,并将其中的数据按照堆的大小关系进行排序,如make_heap、push_heap、pop_heap和sort_heap函数。

  4. 评估排序算法(Sort Evaluating Algorithm):对排序算法的时间复杂度进行评估,如 stable_sort、nth_element 函数。这些算法还包括计算两个序列的集合减法的set_difference、set_intersection、set_symmetric_difference、set_union。

  5. 模型(Model):STL中既有抽象数据类型,也有抽象概念。STL中的模型指的是一种非常抽象、通用、高度建模的数据类型,而且通常定义了一些抽象概念的接口。

STL作为C++标准库的一部分,封装了很多常用的数据结构和算法,能够方便地进行复杂的数据处理。在使用STL时,熟悉各种概念、算法和模型可以帮助我们更好地利用STL中的功能。

16.4.5、容器种类

在 C++ STL 中,包含了多种不同种类的容器,每种容器都有其特定的特征和用途。以下是常用的容器种类:

1、顺序容器:

顺序容器是一种线性结构,在容器内部元素之间是连续的,可以随时访问容器中的任意位置。常用的顺序容器有:

  • vector:动态数组,可以快速访问任意元素,但插入和删除操作成本较高;
  • deque:双端队列,支持高效的在两端插入和删除操作,但访问中间元素较慢;
  • list:双向链表,可以在任意位置进行插入和删除,但访问元素只能从头到尾遍历。

2、关联容器:

关联容器是一种非线性结构,内部元素不是连续的,元素之间的位置是由其键值决定的。常用的关联容器有:

  • set 和 multiset:基于二叉搜索树实现的集合类型,自动按照键值排序并且不存在相同键值,multiset 允许重复键值;
  • map 和 multimap:基于二叉搜索树实现的键值对类型,自动按照键值排序并且不存在相同的键,multimap 允许重复的键值对。

3、无序容器:

无序容器是一种按照哈希值存储其元素的容器,内部元素并不是按照其插入顺序或者大小排序。常用的无序容器有:

  • unordered_set 和 unordered_multiset:基于哈希表实现的集合类型,无序存储其元素;
  • unordered_map 和 unordered_multimap:基于哈希表实现的键值对类型,无序存储其元素。

4、容器适配器:

容器适配器是一种特殊的容器,可以修改某种容器的接口,使其具有不同的行为或者功能。常用的容器适配器有:

  • stack:基于 deque 或 list 实现的后进先出的栈结构;
  • queue:基于 deque 或 list 实现的先进先出的队列结构;
  • priority_queue:基于 vector 或 deque 实现的优先队列。

通过不同的容器种类,开发人员可以根据需要选择合适的容器,实现不同的数据结构和算法。

16.4.6、关联容器

关联容器是 C++ STL 中的一种容器,可以用来实现不同的数据结构,如集合、映射等。关联容器的内部元素不是线性存储的,而是按照键值进行排序或者哈希存储的。常见的关联容器有 set、multiset、map 和 multimap。

1、set

set 是一个集合容器,它内部的元素是唯一的,不重复。set 内部元素根据键值自动排序,采用的是红黑树实现,所以在增加、删除、查找操作上都有较好的效率。

下面是 set 的基本用法:

#include <set>
using namespace std;

int main() {
    set<int> myset;
    myset.insert(1);
    myset.insert(2);
    myset.insert(3);
    for (auto it = myset.begin(); it != myset.end(); it++) {
        cout << *it << " ";
    } // 输出 1 2 3
    return 0;
}

2、multiset

multiset 是和 set 类似的集合容器,但是它允许元素重复。multiset 内部元素也会根据键值自动排序,采用的是红黑树实现,所以在增加、删除、查找操作上都有较好的效率。

下面是 multiset 的基本用法:

#include <set>
using namespace std;

int main() {
    multiset<int> myset;
    myset.insert(1);
    myset.insert(2);
    myset.insert(3);
    myset.insert(3);
    for (auto it = myset.begin(); it != myset.end(); it++) {
        cout << *it << " ";
    } // 输出 1 2 3 3
    return 0;
}

3、map

map 是一种键值对容器,每个元素是一个键值对,包含一个键和一个值。map 会根据键值自动排序,采用的是红黑树实现,所以在增加、删除、查找操作上都有较好的效率。

下面是 map 的基本用法:

#include <map>
using namespace std;

int main() {
    map<string, int> mymap;
    mymap["apple"] = 1;
    mymap["banana"] = 2;
    mymap["orange"] = 3;
    cout << mymap["apple"] << endl; // 输出 1
    for (auto it = mymap.begin(); it != mymap.end(); it++) {
        cout << it->first << ": " << it->second << endl;
    } // 输出 apple: 1, banana: 2, orange: 3
    return 0;
}

4、multimap

multimap 是和 map 类似的键值对容器,但是它允许键值重复。multimap 采用的是红黑树实现,所以在增加、删除、查找操作上都有较好的效率。

下面是 multimap 的基本用法:

#include <map>
using namespace std;

int main() {
    multimap<string, int> mymap;
    mymap.insert(make_pair("apple", 1));
    mymap.insert(make_pair("banana", 2));
    mymap.insert(make_pair("orange", 3));
    mymap.insert(make_pair("apple", 4));
    for (auto it = mymap.begin(); it != mymap.end(); it++) {
        cout << it->first << ": " << it->second << endl;
    } // 输出 apple: 1, apple: 4, banana: 2, orange: 3
    return 0;
}

关联容器在一些特定的应用场景中十分有用,可以通过自动排序和搜索优势来处理数据。比如,在实现词频统计的时候,我们可以使用 multimap 来记录每个单词的出现次数。

16.4.7、无序关联容器(C++11)

C++11 引入了一组无序关联容器,即 unordered_set、unordered_multiset、unordered_map 和 unordered_multimap。这些容器内部元素不是按照键值自动排序的,而是采用哈希表的方式存储和访问元素,因此在插入、删除和查找等操作上都有极好的性能。

无序关联容器和关联容器相比,除了不排序外,还有以下的特点:

  1. 插入顺序和元素顺序无关,即元素的顺序不保证按照插入的顺序。

  2. 空间占用相对较大,因此对于需要大量存储数据的场景,使用无序关联容器可以更高效地使用内存。

下面是 unordered_set 和 unordered_map 的基本用法:

#include <unordered_set>
#include <unordered_map>
#include <iostream>
using namespace std;

int main() {
    // unordered_set
    unordered_set<int> myset = {3,1,2,4,2};
    for (auto it = myset.begin(); it != myset.end(); it++) {
        cout << *it << " ";
    } // 输出 4 1 2 3

    // unordered_map
    unordered_map<string, int> mymap = {{"apple",1}, {"banana",2}, {"orange",3}};
    mymap.emplace("pear", 4);
    mymap.emplace("apple", 5);
    for (auto it = mymap.begin(); it != mymap.end(); it++) {
        cout << it->first << ": " << it->second << endl;
    } // 输出 pear: 4, apple: 1, banana: 2, orange: 3
    return 0;
}

注意,无序关联容器中的元素类型必须能够进行哈希操作。因此,在使用无序关联容器时,需要为自定义的结构体类型提供一个哈希函数。可以通过重载 std::hash 模板的 operator() 函数实现哈希函数。

struct person {
    string name;
    int age;
};

namespace std {
    template <>
    struct hash<person> {
        size_t operator()(const person& p) const {
            return hash<string>()(p.name) ^ hash<int>()(p.age);
        }
    };
}

int main() {
    // 使用自定义类型 person 的无序关联容器
    unordered_map<person, int> mymap;
    mymap[{"Allen", 18}] = 1;
    mymap[{"Bob", 20}] = 2;
    for (auto it = mymap.begin(); it != mymap.end(); it++) {
        cout << it->first.name << ", " << it->first.age << ": " << it->second << endl;
    }
    return 0;
}

无序关联容器是 C++11 新增的特性,使用起来非常方便,可以极大地提高代码的运行效率。

16.5、函数对象

函数对象是一种类似于函数的对象,它可以像函数一样被调用。在 C++ 中,函数对象通常是一个类,这个类重载了函数调用运算符 operator(),使得可以像函数一样调用这个类的实例。函数对象常用于 STL 算法中,例如 sort、find_if 等,它们接收一个函数对象作为参数,用于指定比较规则或判断条件。

函数对象有以下几种类型:

1、一元函数对象:重载了 operator(),并且只接受一个参数的函数对象。

class Functor {
public:
    int operator()(int x) const { return x * x; }
};

int main() {
    Functor f;
    cout << f(4) << endl; // 输出 16
    return 0;
}

2、二元函数对象:重载了 operator(),并且接受两个参数的函数对象。

class Compare {
public:
    bool operator()(int x, int y) const { return x < y; }
};

int main() {
    Compare cmp;
    cout << cmp(1, 2) << endl; // 输出 true
    return 0;
}

3、仿函数(函数符):重载了 operator() 的函数对象,它们被称为仿函数(function object)或函数符(functor)。比如 C++ 标准库中的 less、greater 等函数对象就是仿函数。

仿函数有以下几个优点:

  • 它可以带有状态,可以在构造时保存一些内部状态,每次调用 operator() 时使用这些状态。

  • 它可以被重载,因此可以定义多个具有相同名称的函数对象,实现类似于函数重载的效果。

  • 它可以使用模板参数,因此可以自由地定义函数对象的参数类型和返回类型。

class Add {
public:
    Add(int base) : base_(base) {}
    int operator()(int x) const { return x + base_; }
private:
    int base_;
};

template <typename T, typename F>
void transform(T* first, T* last, F f) {
    for (T* p = first; p != last; p++) {
        *p = f(*p);
    }
}

int main() {
    int a[] = {1, 2, 3};
    transform(a, a + 3, Add(10));
    for (int i : a) {
        cout << i << " "; // 输出 11 12 13
    }
    return 0;
}

使用仿函数可以将函数作为一等对象使用,更加灵活地实现函数的调用和传递。同时,在 STL 算法中使用函数对象作为参数,也可以实现更加通用的算法设计,使得算法具有更强的扩展性和适应性。

16.5.1、函数符概念

在C++中,函数符(Functor)是一种类或结构体,它重载了函数调用运算符operator(),使其可以像函数一样被调用。函数符常用于STL算法中,如sort、find_if等,作为这些算法的比较规则或判断条件。函数符与函数指针相比,具有更高的运行效率,因为它可以内联操作,避免了函数调用的开销。同时,函数符还可以带有自己的状态,这意味着可以通过函数符的构造函数保存一些内部状态,在函数符的多次调用中重复使用这些状态,从而提高了算法的效率。函数符的使用还可以方便地实现函数重载、函数适配器等功能,使代码更加灵活和模块化。

16.5.2、预定义的函数符

在C++中,STL库中提供了一些预定义的函数符,可以直接使用,也可以作为其他函数符的基础。这些预定义的函数符包括:

  1. less<T>/greater<T>: 用于比较两个类型为T的值的大小,返回true/false。

  2. plus<T>/minus<T>/multiplies<T>/divides<T>/modulus<T>: 分别表示加、减、乘、除、取模操作,用于支持对应的数值类型T。

  3. logical_and<T>/logical_or<T>/logical_not<T>: 分别表示逻辑与、逻辑或、逻辑非操作,用于支持bool类型的操作。

  4. equal_to<T>/not_equal_to<T>/greater_equal<T>/less_equal<T>: 分别表示等于、不等于、大于等于、小于等于操作,用于支持类型T的比较操作。

  5. bind1st/bind2nd: 分别用于绑定函数符的第一个参数或第二个参数,便于对函数符进行柯里化操作。

预定义的函数符的使用可以大大简化代码,提高效率和可读性。同时,通过继承或模板技术,也可以自定义函数符,从而支持更加灵活和复杂的操作。

16.5.3、自适应函数符和函数适配器

自适应函数符和函数适配器是函数对象的扩展功能,它们可以使函数符的使用更加灵活和通用。自适应函数符可以自动适应不同类型的参数,从而避免了重载多个版本的函数符,提高了代码的可维护性和可复用性。而函数适配器则是一种特殊的函数符,它可以将原有函数符进行适配,使其可以满足不同的调用需求。

1、自适应函数符:

自适应函数符包括两个:identity 和 select1st。identity函数符接受一个参数,并返回该参数本身,适用于任意类型的变量。select1st函数符接受一个pair类型的参数,返回其第一个元素,即适用于pair类型的变量。这两个函数符可以配合STL算法中的transform、copy等函数一起使用,从而使算法自动适配不同的类型。

// 使用identity函数符实现一次STL算法
#include<algorithm>
#include<vector>
#include<iostream>
#include<numeric>	//for accumulate()
#include<functional>  //for identity
using namespace std;

int main()
{
    vector<int> vctValues = {2,3,5,7,11};
    int iSum = accumulate(vctValues.begin(), vctValues.end(), 0, plus<int>());  //plus<int>()表示函数符加法,0表示加法的初始值
    cout << "Sum of values is " << iSum<<endl;
    double dAverage = (double)iSum / vctValues.size();
    cout<< "Average value is "<< dAverage << "\n";
    double dVariance = 0;
    transform(vctValues.begin(), vctValues.end(), vctValues.begin(), vctValues.begin(), 
               std::minus<double>(), std::bind2nd(std::plus<double>(), -dAverage));
               //使用函数适配器std::bind2nd配合plus<double>()函数符,从而减去平均值dAverage
    dVariance = accumulate(vctValues.begin(), vctValues.end(), dVariance, std::plus<double>()); //再次使用plus<double>()函数符,累加所有值
    dVariance /= vctValues.size();
    cout << "Variance is "<< dVariance<<endl;
    return 0;
}

2、函数适配器

函数适配器是对原有函数符进行适配的一种机制,通过绑定函数符的参数或同时对多个函数符进行组合,从而实现原有函数符的功能扩展。具体而言,函数适配器包括下列几种类型:

(1) binder1st 和 binder2nd:函数适配器 binder1st 和 binder2nd 分别用于绑定函数符的第一个参数和第二个参数,并返回一个新的函数符,从而实现了函数的柯里化(Currying)。

#include <iostream>
#include <functional>

int main() {
    std::plus<int> myplus;
    auto addfive = std::bind1st(myplus, 5); // 绑定第一个参数为5
    std::cout << addfive(3) << std::endl; // 输出 8
    return 0;
}

(2) not1 和 not2:函数适配器 not1 用于对一元函数符取反,not2 用于对二元函数符取反。

#include <iostream>
#include <functional>

int main() {
    std::less<int> myless;
    auto greater = std::not2(myless); // 对myless进行取反
    std::cout << greater(3, 4) << std::endl; // 输出 true
    return 0;
}

(3) negate:函数适配器 negate 用于对一元函数符取相反数。

#include <iostream>
#include <functional>

int main() {
    std::negate<int> mynegate;
    std::cout << mynegate(5) << std::endl; // 输出 -5
    return 0;
}

(4) compose1 和 compose2:函数适配器 compose1 和 compose2 分别表示对两个函数符进行组合,返回一个新的函数符。compose2 表示对两个二元函数符进行组合,compose1 表示对一个一元函数符和一个二元函数符进行组合。

#include <iostream>
#include <functional>

int main() {
    std::plus<int> myplus;
    std::multiplies<int> mymultiplies;
    auto mycompose = std::compose2(std::negate<int>(), std::bind2nd(myplus, 10), mymultiplies);
    std::cout << mycompose(5,2) << std::endl; // 输出 -80
    return 0;
}

函数适配器的使用可以大大简化代码,避免一些重复的或繁琐的操作。然而,过度使用函数适配器也可能使代码变得晦涩难懂,因此需要根据具体情况进行选择。

16.6、算法

16.6.1、算法根

算法根(algorithm header)是 C++ 标准库中的一个头文件(header),提供了一些数学函数,例如算术平方根、立方根、指数和对数等;以及三角函数、双曲函数、点积、反正切和误差函数等常用函数。这些函数可以用于统计学局部或全局最优化问题、信号处理、图像处理、自然语言处理等领域。

算法根中常用的函数包括:

1、算术平方根

函数 sqrt() 用于计算一个非负数的算术平方根。

#include <iostream>
#include <cmath>

int main() {
    double x = 4.0;
    std::cout << std::sqrt(x) << std::endl; // 输出 2
    return 0;
}

2、算术立方根

函数 cbrt() 用于计算一个实数的算术立方根。

#include <iostream>
#include <cmath>

int main() {
    double x = 8.0;
    std::cout << std::cbrt(x) << std::endl; // 输出 2
    return 0;
}

3、指数和对数

函数 exp() 用于计算一个实数的指数。

#include <iostream>
#include <cmath>

int main() {
    double x = 1.0;
    std::cout << std::exp(x) << std::endl; // 输出 e
    return 0;
}

函数 log() 用于计算一个实数的自然对数。

#include <iostream>
#include <cmath>

int main() {
    double x = 10.0;
    std::cout << std::log(x) << std::endl; // 输出 ln(10)
    return 0;
}

4、三角函数和双曲函数

标准库提供了基本的三角函数和双曲函数,包括 sin()、cos()、tan()、asin()、acos()、atan()、sinh()、cosh() 和 tanh(),以及它们的反函数 asin(), acos() 和 atanh()。

#include <iostream>
#include <cmath>

int main() {
    double x = 3.14159265358979323846/6; // π/6
    std::cout << std::sin(x) << std::endl; // 输出 0.5
    std::cout << std::asin(0.5) << std::endl; // 输出 π/6
    return 0;
}

5、点积

函数内积,或点积(dot product),可以计算两个向量的相似度。

#include <iostream>
#include <numeric>   // for inner_product
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3};
    std::vector<int> w = {4, 5, 6};
    int res = std::inner_product(v.begin(), v.end(), w.begin(), 0);
    std::cout << res << std::endl; // 输出 32(1*4 + 2*5 + 3*6)
    return 0;
}

6、反正切

函数 atan2() 用于计算两个实数 x 和 y 的反正切值,返回值是一个介于 -π 和 π 之间的实数。

#include <iostream>
#include <cmath>

int main() {
    double x = 0.5, y = 0.5;
    std::cout << std::atan2(y, x) << std::endl; // 输出 π/4
    return 0;
}

7、误差函数

误差函数(error function)是统计学和数学上的一个函数,用于描述随机变量的偏离程度,有助于理解正态分布的特征。

函数 erfc() 计算误差函数的补函数,erf() 则计算误差函数本身。

#include <iostream>
#include <cmath>

int main() {
    double x = 1.0;
    std::cout << std::erf(x) << std::endl; // 输出 0.8427
    std::cout << std::erfc(x) << std::endl; // 输出 0.1573
    return 0;
}

总之,算法根提供了许多数学函数,方便了我们在编写复杂程序时的运算与计算。

16.6.2、算法的通用特征

算法在计算机科学中具有非常重要的地位,其通用特征包括以下几点:

  1. 输入:算法需要有一个或多个输入,输入是算法开始运行的前提条件。

  2. 输出:算法必须有一个确切的输出,输出是算法运行结束的标志。

  3. 有限性:算法必须在有限的步骤内结束,不能无限循环或不能结束。

  4. 确定性:算法必须是可预测的,对于给定相同的输入,结果必须完全相同。

  5. 可行性:算法必须是可执行的,即必须存在计算机程序或其他方式来实现算法。

  6. 无歧义性:算法的每个步骤必须具有唯一的含义。

  7. 分解性:算法可以被分解成更小的子问题,从而更容易实现。

  8. 抽象性:算法必须是通用的,不能针对特定的输入或场景。

这些特征都非常重要,它们定义和划分了算法与其他计算机科学概念之间的区别,例如数据结构和编程语言。同时,这些特征提供了一些衡量算法质量的标准,例如时间复杂度、空间复杂度和正确性等,这些标准被广泛应用于算法设计和优化。理解算法的通用特征对于编写高效、可靠和易于维护的程序非常重要,同时也为理解计算机科学的基本原理做出了贡献。

16.6.3、STL和string类

STL(C++标准模板库)是C++语言的标准库之一,提供了许多常用的容器、算法和功能。STL中的string类是一个非常有用的容器,用于处理字符串,它提供了一系列操作函数,使得字符串的处理变得更加方便和高效。

STL中的string类定义在头文件<string>中,它是一个模板类,可以存储任意类型的字符,包括ASCII字符、Unicode字符和其他扩展字符,使用string类可以避免C语言中字符串的一些问题,例如长度限制和内存泄漏。

string类的基本用法包括以下几个方面:

  1. 创建字符串:可以使用赋值操作符或构造函数来创建字符串。

  2. 操作字符串:可以使用各种函数来操作字符串,例如插入、删除、截取、查找等。

  3. 访问字符串:可以使用下标或迭代器来访问字符串中的字符。

  4. 字符串比较:可以使用比较操作符或函数来比较字符串。

  5. 字符串转换:可以使用各种转换函数将字符串转换为其他类型,例如整数和浮点数。

STL中的string类提供了高效的字符串处理功能,可以方便地处理各种字符串操作,例如搜索、替换、分割和连接等。同时,string类也是一个可拓展的容器,可以方便地添加、删除和修改字符串中的元素,适用于各种字符串处理场景。

16.6.4、函数和容器方法

在C++中,函数和容器方法都是用于处理数据的常见工具。

函数是一段代码序列,它执行特定的任务,接受输入参数,并返回一个值或执行特定操作。在C++中,函数可以定义为独立的,也可以作为类的成员函数。可以使用函数来操作STL容器中的元素,例如数组中的元素或vector中的元素。STL中还提供了许多函数模板,例如算法函数(例如sort,find等)和函数对象(例如greater,less等)。

容器方法是STL容器类中包含的函数,这些函数可以方便地操作容器元素。不同的容器(例如vector,list,map等)具有不同的方法。这些方法可以用于添加,删除和修改容器中的元素,以及对容器中的元素进行排序,搜索和遍历等。

以下是一些常见的容器方法:

  1. push_back:在vector或deque的末尾插入一个元素。

  2. pop_back:从vector或deque的末尾删除一个元素。

  3. insert:插入元素到指定位置。

  4. erase:从容器中删除指定元素或指定范围内的元素。

  5. at:返回指定位置的元素。

  6. size:返回容器的大小。

  7. sort:对容器内的元素进行排序。

  8. find:查找指定元素是否存在于容器中,并返回其位置。

函数和容器方法在C++中都是非常重要的工具,可以大大简化代码编写,提高代码的可读性和可维护性。无论是处理数据还是实现算法,都可以通过函数和容器方法来完成,提高代码的效率。

16.6.5、使用STL

STL是C++标准库中的一个重要组件,提供了一系列容器、迭代器、算法和函数对象等工具,使得C++程序设计变得更加高效和方便。下面介绍如何使用STL。

1、包含头文件

STL中的类和函数都包含在头文件中。例如,包含<vector>头文件可以使用STL中的vector容器,包含<algorithm>头文件可以使用STL中的算法函数。

2、定义容器

可以使用STL中的容器类来定义变量。例如,定义一个int类型的向量可以使用vector<int>。STL中常见的容器类型还包括deque(双端队列)、list(链表)、set(红黑树集合)等。

3、填充容器

可以使用容器对象的方法或迭代器来填充容器。例如,vector对象的push_back()方法可以在向量的末尾添加元素。

#include <vector>
#include <iostream>

int main() {
    std::vector<int> v;
    v.push_back(1); // 添加元素
    v.push_back(2);
    v.push_back(3);

    for(auto i : v) { // 使用迭代器遍历元素
        std::cout << i << " ";
    }
    std::cout << std::endl;
    return 0;
}

4、使用容器

可以使用容器对象的方法或迭代器来操作容器中的元素。例如,在vector对象中查找指定元素,可以使用find()算法函数。

#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3};
    auto it = std::find(v.begin(), v.end(), 2); // 在vector对象中查找元素2
    if(it != v.end()) {
        std::cout << "元素2位于向量v的第" << it - v.begin() << "个位置" << std::endl;
    }
    return 0;
}

5、使用算法函数

STL中包含大量的算法函数,可以用于操作各种类型的容器。例如,使用sort()函数在vector对象中对元素进行排序。

#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    std::vector<int> v = {3, 2, 1};
    std::sort(v.begin(), v.end()); // 在vector对象中对元素进行排序
    for(auto i : v) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    return 0;
}

STL是C++中的一个非常强大的工具,可以大大简化C++程序设计,提高程序效率和可读性。通过使用STL中的容器、迭代器、算法等工具,程序员可以更加专注于实现算法和逻辑,而无需担心低层次的数据结构实现。

16.7、其他库

除了STL以外,C++标准库还包括多个其他的库,下面简单介绍一下这些库。

  1. <iostream>:定义了输入输出流,包括cin、cout、cerr和clog等。

  2. <cstdio>:定义了基本的输入输出函数,例如printf、scanf、fopen和fclose等。

  3. <cmath>:定义了常见的数学函数,例如三角函数、指数函数和对数函数等。

  4. <ctime>:定义了时间函数,例如时间获取和延时等。

  5. <cstdlib>:定义了通用的函数,例如内存管理、字符串转换和伪随机数生成等。

  6. <fstream>:定义了操作文件的流,例如ifstream、ofstream和fstream等。

这些库都提供了C++程序设计中常用的工具和函数,可以方便地进行文件操作、数值计算、时间处理和随机数生成等功能。在C++程序设计中,使用这些库可以更加高效地实现各种功能,提高程序效率和可读性。

同时,在C++程序设计中,还有许多其他的库,例如Boost、OpenCV和SFML等,这些库提供各种各样的功能,例如线性代数、图像处理和游戏开发等。使用这些库可以快速实现各种功能,提高开发效率。

16.7.1、vector、valarray和array

vector、valarray和array是C++标准库中的三种数组容器类型。它们都可以存储一组类型相同的数据,但是在实现方式和使用方法上有所不同。

1、vector

vector是一个动态数组容器,在定义时不需要指定容器大小。它以连续的方式存储元素,可以自动调整容器大小以适应元素数量的变化,支持随机访问元素和在末尾添加元素。

vector的定义方式如下:

#include <vector>
using namespace std;

vector<数据类型> 数组名;

例如,定义一个存储整数的向量可以用下面的语句:

vector<int> vec;

2、valarray

valarray是向量数组容器,可以对整个数组进行算术操作,例如加减乘除、求幂、三角函数和指数函数等。它以连续的方式存储元素,使用数组的方式来处理元素,支持在末尾添加元素。

valarray的定义方式如下:

#include <valarray>
using namespace std;

valarray<数据类型> 数组名;

例如,定义一个存储双精度实数的数组可以用下面的语句:

valarray<double> arr;

3、array

array是一个静态数组容器,定义时需要指定容器大小,且容器大小不可变。它以连续的方式存储元素,支持随机访问元素和在末尾添加元素。

array的定义方式如下:

#include <array>
using namespace std;

array<数据类型, 容器大小> 数组名;

例如,定义一个长度为5的整数数组可以用下面的语句:

array<int, 5> arr;

vector、valarray和array都是C++标准库中的数组容器,可以方便地存储一组类型相同的数据,并提供不同的方法来处理这些数据。在实际编程中,需要根据需求选择合适的容器类型,以实现更加高效和方便的数据处理。

16.7.2、模板initializer_list(C++11)

模板initializer_list是C++11中新增的一种特殊容器类型,用于在函数参数中传递一个列表。它是一个简单的容器类型,可以用于初始化其他容器或执行其他特定任务。

initializer_list的定义方式如下:

#include <initializer_list>

std::initializer_list<数据类型> 数组名;

例如,定义一个存储整数列表的initializer_list可以用下面的语句:

std::initializer_list<int> ilist {1, 2, 3, 4, 5};

同时,可以将initializer_list作为函数的参数类型,使得函数可以接受一个列表,例如:

void print_list(std::initializer_list<int> ilist) {
    for(auto i : ilist) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
}

int main() {
    print_list({1, 2, 3, 4, 5}); // 调用函数print_list
    return 0;
}

在函数调用中,可以使用花括号来创建一个initializer_list类型的列表,并将其传递给函数。

initializer_list可以方便地传递一个列表,适用于各种需要列表作为参数的情况。在实际编程中,使用initializer_list可以提高程序的可读性和效率,同时也可以减少代码量。

16.7.3、使用inittializer_list

使用initializer_list时要注意以下几点:

1、传递给initializer_list的列表元素类型必须与目标容器中的元素类型相同,否则会出现编译错误。

2、initializer_list对象中的元素是只读的,无法通过引用或指针修改元素值。

3、在函数调用中,只能传递花括号括起来的initializer_list列表,不能使用其他方式传递。

4、使用initializer_list作为函数参数时,必须声明可变长度参数列表的格式,例如下面的函数声明:

void func(std::initializer_list<int> list, ...);

上面的函数声明中使用了省略号,表示这是一个可变长度参数列表的函数,可以接受任意数量的参数。

5、使用initializer_list定义数组时,必须使用std::array容器,并指定数组大小,例如:

#include <initializer_list>
#include <array>

int main() {
    std::array<int, 5> arr = {1, 2, 3, 4, 5}; // 使用initializer_list定义数组
    return 0;
}

initializer_list是C++11中非常方便的一种容器类型,它可以方便地传递一个列表,适用于各种需要列表作为参数的情况。在实际编程中,使用initializer_list可以提高程序的可读性和效率,同时也可以减少代码量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值