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_t
、char32_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::deque
、std::stack
、std::queue
、std::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. 注意事项
-
std::vector
可以通过动态分配内部缓存来管理元素,因此会存在增删元素带来内存重分配的开销,应当注意在频繁的大规模插入和删除操作时,应当事先预分配适当大小的容器以提高插入删除效率。 -
同时,需要注意避免空间的浪费,可以使用
shrink_to_fit()
方法进行收缩,释放没用到的内存空间。
16.3.2、可对矢量执行的操作
std::vector
是一个非常常用的容器,支持多种操作,以下是一些可对 std::vector
执行的操作:
-
元素访问:可以使用下标运算符
[]
或at()
方法访问std::vector
中的元素,也可以使用front()
方法访问第一个元素,使用back()
方法访问最后一个元素。 -
元素插入和删除:可以使用
push_back()
方法在std::vector
的末尾插入元素,使用insert()
方法在指定位置插入元素,使用emplace_back()
或emplace()
方法在末尾或指定位置就地构造元素,可以使用pop_back()
方法删除最后一个元素,使用erase()
方法删除指定位置或者指定范围的元素,使用clear()
方法删除所有元素。 -
容量操作:可以使用
size()
方法获取std::vector
中元素的数量,使用capacity()
方法获取std::vector
内部存储元素的空间大小,使用reserve()
方法可以预留内存空间以避免不必要的内存重分配,使用shrink_to_fit()
方法释放未使用的内存空间。 -
迭代器:可以使用
begin()
和end()
方法获取指向std::vector
开始和结束的迭代器,使用rbegin()
和rend()
方法获取指向std::vector
末尾和开头的反向迭代器,还可以使用cbegin()
、cend()
、crbegin()
和crend()
方法获取指向std::vector
开始和结束(反向)的常量迭代器。 -
容器之间的赋值和比较:可以使用
=
或者拷贝构造函数将一个std::vector
赋值给另一个std::vector
,也可以使用==
、!=
、<
、<=
、>
和>=
等运算符进行比较。 -
元素的重排:可以使用
sort()
方法对std::vector
中的所有元素进行排序,使用reverse()
方法将std::vector
内部的元素反转。 -
其他操作:还有一些其他的操作,如
emplace_front()
、emplace_back()
、resize()
、swap()
等方法,可以根据实际需要选择使用。
总之,std::vector
提供了丰富的操作和方法,能够满足各种不同场景下的需求。
16.3.3、对矢量可执行的其他操作
除了 std::vector
内置支持的操作外,还可以使用算法库中的一些函数对 std::vector
进行其他操作。
-
查找:可以使用算法库中的
std::find()
函数在std::vector
中查找指定值是否存在,也可以使用std::find_if()
函数在std::vector
中通过自定义的谓词判断是否存在符合条件的元素。 -
排序:除了
std::vector
的sort()
方法外,也可以使用算法库中的std::sort()
函数对std::vector
排序。std::sort()
提供了多个版本,可以通过提供自定义的比较函数来实现不同的排序方式。 -
删除/替换/变换元素:可以使用算法库中的
std::remove()
、std::replace()
、std::transform()
等函数来对std::vector
中的元素进行删除、替换、变换等操作。 -
合并/拆分:可以使用算法库中的
std::merge()
、std::inplace_merge()
、std::partition()
、std::partition_copy()
等函数来对std::vector
进行合并、拆分等操作。 -
统计:可以使用算法库中的
std::count()
、std::count_if()
、std::accumulate()
等函数来对std::vector
进行统计操作。 -
遍历:可以使用
for
循环遍历std::vector
中的元素,也可以使用算法库中的std::for_each()
函数来对std::vector
进行遍历操作。 -
匹配:可以使用算法库中的
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::vector
、std::initializer_list
、std::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::vector
、std::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::swap
、std::min/max
、std::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 容器内的元素。使用迭代器可以大大提高程序的代码复用性和灵活性,以下是使用迭代器的几个好处:
-
遍历容器元素 :使用迭代器可以遍历整个容器中的元素,而不必关心容器的内部实现。每个容器都有对应的迭代器类型,可以使用迭代器对容器中的元素进行访问和操作。
-
简化容器和算法的耦合 :使用迭代器可以将算法与实际容器的实现解耦。容器的实现方式可能会发生变化,但是迭代器作为对容器的抽象访问方式,可以保持不变,因此这种方式可以提高代码的可维护性和可扩展性。
-
提供算法的简单性 :使用迭代器,可以很容易地实现算法的实现,例如在 STL 中,提供了很多算法函数,如排序、查找、替换、计数等,这些算法函数的参数都是迭代器,这使得开发人员可以轻松地使用这些算法。
-
动态扩展数据类型 :使用迭代器可以提高代码的复用性和灵活性。例如,如果需对以下两个容器中的元素进行比较,使用迭代器可以编写一个通用的算法,而无需针对容器进行特定实现。
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 中内置了多种迭代器类型,不同类型的迭代器在实现上略有差异,下面是常用的迭代器类型:
-
输入迭代器(Input Iterator):输入迭代器用于从容器中读取数据,但不允许修改容器中的数据。输入迭代器只支持单遍遍历。
-
输出迭代器(Output Iterator):输出迭代器用于向容器中写入数据,但不能读取容器中数据。输出迭代器也只支持单遍遍历。
-
前向迭代器(Forward Iterator):前向迭代器支持单向遍历容器中的元素,支持读取和写入。前向迭代器只支持单遍遍历。
-
双向迭代器(Bidirectional Iterator):双向迭代器支持双向遍历容器中的元素,除了支持前向迭代器的读取和写入,还支持反向遍历和删除元素等操作。
-
随机访问迭代器(Random Access Iterator):随机访问迭代器是最为强大的迭代器类型,支持快速跳转和随机访问,所以可以用来实现各种算法。除了支持双向迭代器的操作,还支持加减运算、比较和数组下标等操作。
-
流迭代器(Stream Iterator):流迭代器用于将容器中的数据输出到流(例如标准输出流,文件输出流等)或从流中读取数据,它们实际上是一种输出迭代器或输入迭代器。流迭代器可以用于将数据从容器中打印到屏幕上或写入文件中。
以下是常见容器支持的迭代器类型:
容器 | 迭代器类型 |
---|---|
vector | 随机访问迭代器 |
deque | 随机访问迭代器 |
list | 双向迭代器 |
stack | 不支持迭代器 |
queue | 不支持迭代器 |
priority_queue | 不支持迭代器 |
set | 双向迭代器 |
map | 双向迭代器 |
在使用迭代器遍历 STL 容器时,通常需要根据容器类型选择对应的迭代器类型,并根据需要选择合适的遍历方式。
16.4.3、迭代器层次结构
STL 中的迭代器层次结构可以分为五个等级,分别是:
-
输入迭代器(Input Iterator):只能从容器中读取数据,无法修改容器中的数据。适用于单遍遍历,如 istream_iterator。
-
输出迭代器(Output Iterator):只能向容器中写入数据,无法读取容器中的数据。适用于单遍遍历,如 ostream_iterator。
-
前向迭代器(Forward Iterator):支持从前往后遍历容器中的元素,并且可以读取或者修改容器中的元素,但每个元素只能被遍历一次,适用于单向链表等数据结构,如 forward_list。
-
双向迭代器(Bidirectional Iterator):将前向迭代器的功能扩展为可以反向遍历容器中的元素,即支持在双向链表中向前或向后遍历,如 list。
-
随机访问迭代器(Random Access Iterator):将双向迭代器的功能扩展为支持随机访问容器中的元素,可以通过下标访问容器中的任意元素,如 vector 和 deque。
在迭代器层次结构中,每一级迭代器都包含前一个级别迭代器的全部功能,并在此基础上增加了新的功能。因此,使用高级别的迭代器可以实现更复杂和高效的操作。
16.4.4、概念、改进和模型
在C++ STL中,除了迭代器层次结构外,还有一些其他概念、改进和模型,包括:
-
迭代器对(Iterator Pair):由两个迭代器组成的序列,其中一个迭代器指向序列的起始位置,另一个迭代器指向序列的末尾位置。在STL中经常通过迭代器对来表示待操作区间,如pair< vector<int>::iterator, vector<int>::iterator >。
-
分类算法(Partitioning Algorithm):将容器中的元素按照某个谓词分成两个部分,如stable_partition和partition函数。分区算法通常返回迭代器对,表示容器中被分为两个部分的位置。
-
堆算法(Heap Algorithm):将容器中的元素抽象成一个堆,并将其中的数据按照堆的大小关系进行排序,如make_heap、push_heap、pop_heap和sort_heap函数。
-
评估排序算法(Sort Evaluating Algorithm):对排序算法的时间复杂度进行评估,如 stable_sort、nth_element 函数。这些算法还包括计算两个序列的集合减法的set_difference、set_intersection、set_symmetric_difference、set_union。
-
模型(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。这些容器内部元素不是按照键值自动排序的,而是采用哈希表的方式存储和访问元素,因此在插入、删除和查找等操作上都有极好的性能。
无序关联容器和关联容器相比,除了不排序外,还有以下的特点:
-
插入顺序和元素顺序无关,即元素的顺序不保证按照插入的顺序。
-
空间占用相对较大,因此对于需要大量存储数据的场景,使用无序关联容器可以更高效地使用内存。
下面是 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库中提供了一些预定义的函数符,可以直接使用,也可以作为其他函数符的基础。这些预定义的函数符包括:
-
less<T>/greater<T>: 用于比较两个类型为T的值的大小,返回true/false。
-
plus<T>/minus<T>/multiplies<T>/divides<T>/modulus<T>: 分别表示加、减、乘、除、取模操作,用于支持对应的数值类型T。
-
logical_and<T>/logical_or<T>/logical_not<T>: 分别表示逻辑与、逻辑或、逻辑非操作,用于支持bool类型的操作。
-
equal_to<T>/not_equal_to<T>/greater_equal<T>/less_equal<T>: 分别表示等于、不等于、大于等于、小于等于操作,用于支持类型T的比较操作。
-
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、算法的通用特征
算法在计算机科学中具有非常重要的地位,其通用特征包括以下几点:
-
输入:算法需要有一个或多个输入,输入是算法开始运行的前提条件。
-
输出:算法必须有一个确切的输出,输出是算法运行结束的标志。
-
有限性:算法必须在有限的步骤内结束,不能无限循环或不能结束。
-
确定性:算法必须是可预测的,对于给定相同的输入,结果必须完全相同。
-
可行性:算法必须是可执行的,即必须存在计算机程序或其他方式来实现算法。
-
无歧义性:算法的每个步骤必须具有唯一的含义。
-
分解性:算法可以被分解成更小的子问题,从而更容易实现。
-
抽象性:算法必须是通用的,不能针对特定的输入或场景。
这些特征都非常重要,它们定义和划分了算法与其他计算机科学概念之间的区别,例如数据结构和编程语言。同时,这些特征提供了一些衡量算法质量的标准,例如时间复杂度、空间复杂度和正确性等,这些标准被广泛应用于算法设计和优化。理解算法的通用特征对于编写高效、可靠和易于维护的程序非常重要,同时也为理解计算机科学的基本原理做出了贡献。
16.6.3、STL和string类
STL(C++标准模板库)是C++语言的标准库之一,提供了许多常用的容器、算法和功能。STL中的string类是一个非常有用的容器,用于处理字符串,它提供了一系列操作函数,使得字符串的处理变得更加方便和高效。
STL中的string类定义在头文件<string>中,它是一个模板类,可以存储任意类型的字符,包括ASCII字符、Unicode字符和其他扩展字符,使用string类可以避免C语言中字符串的一些问题,例如长度限制和内存泄漏。
string类的基本用法包括以下几个方面:
-
创建字符串:可以使用赋值操作符或构造函数来创建字符串。
-
操作字符串:可以使用各种函数来操作字符串,例如插入、删除、截取、查找等。
-
访问字符串:可以使用下标或迭代器来访问字符串中的字符。
-
字符串比较:可以使用比较操作符或函数来比较字符串。
-
字符串转换:可以使用各种转换函数将字符串转换为其他类型,例如整数和浮点数。
STL中的string类提供了高效的字符串处理功能,可以方便地处理各种字符串操作,例如搜索、替换、分割和连接等。同时,string类也是一个可拓展的容器,可以方便地添加、删除和修改字符串中的元素,适用于各种字符串处理场景。
16.6.4、函数和容器方法
在C++中,函数和容器方法都是用于处理数据的常见工具。
函数是一段代码序列,它执行特定的任务,接受输入参数,并返回一个值或执行特定操作。在C++中,函数可以定义为独立的,也可以作为类的成员函数。可以使用函数来操作STL容器中的元素,例如数组中的元素或vector中的元素。STL中还提供了许多函数模板,例如算法函数(例如sort,find等)和函数对象(例如greater,less等)。
容器方法是STL容器类中包含的函数,这些函数可以方便地操作容器元素。不同的容器(例如vector,list,map等)具有不同的方法。这些方法可以用于添加,删除和修改容器中的元素,以及对容器中的元素进行排序,搜索和遍历等。
以下是一些常见的容器方法:
-
push_back:在vector或deque的末尾插入一个元素。
-
pop_back:从vector或deque的末尾删除一个元素。
-
insert:插入元素到指定位置。
-
erase:从容器中删除指定元素或指定范围内的元素。
-
at:返回指定位置的元素。
-
size:返回容器的大小。
-
sort:对容器内的元素进行排序。
-
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++标准库还包括多个其他的库,下面简单介绍一下这些库。
-
<iostream>:定义了输入输出流,包括cin、cout、cerr和clog等。
-
<cstdio>:定义了基本的输入输出函数,例如printf、scanf、fopen和fclose等。
-
<cmath>:定义了常见的数学函数,例如三角函数、指数函数和对数函数等。
-
<ctime>:定义了时间函数,例如时间获取和延时等。
-
<cstdlib>:定义了通用的函数,例如内存管理、字符串转换和伪随机数生成等。
-
<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可以提高程序的可读性和效率,同时也可以减少代码量。