C++ 基础知识 四 认识STL 下篇
一、函数对象 与 仿函数
在C++标准模板库(STL)中,函数对象(Function Object)是一种能够被调用的对象,它们像普通函数一样可以完成特殊的处理。函数对象其实就是一个重载了()运算符的类对象,因此它们也被称作仿函数。
1. 函数对象的概念和使用
函数对象是一种可调用的对象,即重载了()运算符的类对象。在STL中函数对象被广泛用于实现容器的算法操作。比如在排序算法sort中可以传入一个函数对象,用于排序时的元素比较。
下面是使用函数对象的一个例子:
- 定义函数对象greater_than,用于比较数字是否大于某个特定的值(在构造函数中初始化)
- 用find_if算法查找vector容器中第一个满足大于某个值的元素,输出其值并判断是否找到
- 注意: 在函数对象的操作符()中,参数是vector容器中的元素
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
// 定义一个函数对象greater_than,用于比较两个数字的大小
class greater_than {
public:
greater_than(int x) : val(x) {}
bool operator()(int x) { return x > val; }
private:
int val;
};
int main() {
vector<int> nums = { 2, 5, 1, 7, 9 };
greater_than gt5(5);
auto it1 = find_if(nums.begin(), nums.end(), gt5);
if (it1 != nums.end())
cout << "找到 > 5 的第一个数: " << *it1 << endl;
greater_than gt10(10);
auto it2 = find_if(nums.begin(), nums.end(), gt10);
if (it2 != nums.end())
cout << "找到 > 10 的第一个数: " << *it2 << endl;
else
cout << "没有找到 > 10 的数" << endl;
}
2. 分类说明 ( 算数 比较 逻辑 )
2.1 算术仿函数
算术仿函数用于执行一些算术运算
示例 加、减、乘、除、 取模 :
template<class T> struct plus; // 加法仿函数,返回lhs + rhs
template<class T> struct minus; // 减法仿函数,返回lhs - rhs
template<class T> struct multiplies; // 乘法仿函数,返回lhs * rhs
template<class T> struct divides; // 除法仿函数,返回lhs / rhs
template<class T> struct modulus; // 取模仿函数,返回lhs % rhs
2.2 比较仿函数
比较仿函数用于比较元素的大小,以便我们可以在容器算法中使用
示例 :
template<class T> struct equal_to; // 等于仿函数,返回lhs == rhs
template<class T> struct not_equal_to; // 不等于仿函数,返回lhs != rhs
template<class T> struct greater; // 大于仿函数,返回lhs >rhs
template<class T> struct less; // 小于仿函数,返回lhs < rhs
template<class T> struct greater_equal; // 大于等于仿函数,返回lhs >= rhs
template<class T> struct less_equal; // 小于等于仿函数,返回lhs <= rhs
2.3 逻辑仿函数
逻辑仿函数用于组合两个或多个条件,产生一个逻辑上的而或非结果
示例 :
template<class T> struct logical_and; // 逻辑与仿函数,返回lhs && rhs
template<class T> struct logical_or; // 逻辑或仿函数,返回lhs || rhs
template<class T> struct logical_not; // 逻辑非仿函数,返回!x
2.4 其他仿函数
还有一些其他常用的仿函数:
template<class T> struct negate; // 取反仿函数,返回-T
template<class T> struct identity; // 身份仿函数,返回x
template<class T> struct select1st; // 返回pair的第一个元素
template<class T> struct select2nd; // 返回pair的第二个元素
3. 高级技巧
基于函数对象的高级技巧通常使用函数适配器(Function Adaptor)来实现
函数适配器可以修改函数对象的调用方式,以适应STL算法中的某些实参要求
下面介绍一些常用的基于函数对象的高级技巧:
3.1 bind1st与bind2nd函数适配器
bind1st和bind2nd函数适配器可以将二元运算符转换为一元运算符或函数对象
template<class OP> class binder1st; // 将二元操作转换为一元操作,对应于op(lhs, x)
template<class OP> class binder2nd; // 将二元操作转换为一元操作,对应于op(x, rhs)
使用bind2nd函数来查找大于5的数字 示例:
vector<int> nums = { 2, 5, 1, 7, 9 };
auto it = find_if(nums.begin(), nums.end(), bind2nd(greater<int>(), 5)); // 查找大于5的数字
3.2 not1与not2函数适配器
not1和not2函数适配器可以将一元或二元谓词取反
template <class Operation> struct unary_negate; // 将一元谓词取反
template <class Operation> struct binary_negate; // 将二元谓词取反
使用not1函数适配器将谓词取反 示例:
// 找到小于5的数字
auto it = find_if(nums.begin(), nums.end(), not1(bind2nd(greater<int>(), 5)));
3.3 ptr_fun函数适配器
ptr_fun函数适配器可以将函数指针转换为函数对象。
template <typename R, typename A>
std::function<R(A)> ptr_fun(R(*pf)(A));
使用ptr_fun函数将tolower函数指针转为函数对象 示例:
// 字符串全转为小写
string str("Hello, STL!");
transform(str.begin(), str.end(), str.begin(), ptr_fun(::tolower));
3.4 谓词合成
谓词合成(Predicate Composition)把多个谓词组合成一个谓词
STL中提供了logical_and和logical_or函数对象来实现谓词的逻辑和与逻辑或运算
下面是谓词合成的一个例子:
如果一个数字既不能被3整除也不能被5整除,则输出它的值
vector<int> nums = { 2, 5, 15, 7, 10 };
copy_if(nums.begin(), nums.end(),
ostream_iterator<int>(cout, " "),
not1(logical_or<greater_equal<int>>(bind2nd(modulus<int>(), 3), bind2nd(modulus<int>(), 5))));
代码中使用了not1和logical_or函数对象,用于将两个谓词(是否能被3整除或能被5整除)取反并组合。记得使用not1来取反谓词,否则我们要用logical_and取反并组合。
另外,如果需要组合的谓词过多,可以使用STL算法accumulate来实现谓词的累加。
4 小结
函数对象和仿函数经常用于实现容器的算法操作,包括排序、查找、过滤和变换等。STL提供了许多常用的函数对象和仿函数,包括算术仿函数、比较仿函数、逻辑仿函数和其他仿函数。
二、内存管理和分配器
在C++ STL中,内存分配器是用于管理容器和其他数据结构的内存分配和释放的工具。本文将介绍STL中的内存分配器、内存分配器的实现原理和优化方法,以及基于内存分配器的高级容器技巧。
1. 内存分配器
STL中提供了一个默认的内存分配器,也允许开发人员使用自定义的内存分配器,用于管理容器和其他数据结构的内存分配和释放。
以下是一个简单的内存分配器的例子:
定义一个模板类my_allocator,其中value_type类型别名用于指定要分配的类型
这个类实现了内存分配函数和内存释放函数
template <typename T>
class my_allocator {
public:
typedef T value_type;
T* allocate(std::size_t n) { // 内存分配函数
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t n) noexcept { // 内存释放函数
::operator delete(p);
}
};
2. 实现 与 优化
2.1 内存分配器的实现原理
在STL中内存分配器通常由两个函数实现:allocate 与 deallocate
allocate用于分配一定数量的内存
deallocate用于释放内存
定义一个模板类my_allocator,其中value_type类型别名用于指定要分配的类型。这个类实现了默认构造函数、拷贝构造函数、内存分配函数和内存释放函数。
template <typename T>
class my_allocator {
public:
typedef T value_type;
my_allocator() = default; // 1. 默认构造函数
template<typename U>
my_allocator(const my_allocator<U>& other) noexcept {/*...*/} // 2. 拷贝构造函数
T* allocate(std::size_t n) { // 3. 内存分配函数
/*...*/
}
void deallocate(T* p, std::size_t n) noexcept {/*...*/} // 4. 内存释放函数
};
2.2 内存分配器的优化方法
2.2.1 池式内存分配器
在使用STL容器时频繁的内存分配和释放会导致内存碎片的产生影响程序性能。一种优化方法是使用池式内存分配器,即预分配一块连续的内存池,每次分配内存时从内存池中取用,避免反复申请释放内存。
简单的内存池分配器示例:
为每个对象添加了一个指向下一个对象的指针,以便在释放内存时建立内存池。如果需要分配的内存大小不为1,则直接调用操作符new分配内存。如果需要分配内存的大小为1,则我们从内存池中取出内存,并在释放内存时将内存块加入内存池。
template <typename T>
class my_memory_pool {
public:
typedef T value_type;
my_memory_pool() = default;
T* allocate(std::size_t n) { // 内存分配函数
/*...*/
}
void deallocate(T* p, std::size_t n) noexcept { // 内存释放函数
/*...*/
}
private:
/*...*/
};
2.2.2 小型对象的内存分配器
对于小型对象的内存分配,内存池的效果比较有限,使用不同的内存算法来管理小型对象的内存分配和释放可以更好地提高内存分配和释放的效率。
简单的小型对象的内存分配器的实现:
template<typename T>
class my_small_object_allocator {
public:
typedef T value_type;
my_small_object_allocator() = default;
template <typename U>
my_small_object_allocator(const my_small_object_allocator<U>& other) noexcept {}
T* allocate(std::size_t n) {/*...*/}
void deallocate(T* p, std::size_t n) noexcept {/*...*/}
private:
/*...*/
};
3. 基于内存分配器的容器技巧
在使用STL容器时可以使用自定义的内存分配器,以优化内存分配和释放的性能。
以下是基于自定义内存分配器的高级容器技巧的示例:
定义一个使用自定义内存分配器my_allocator的vector。
使用reserve函数预先分配内存,随后使用push_back函数将元素插入到vector中,并使用for循环遍历vector并输出其中的元素。可以看到使用自定义的内存分配器可以显著提高程序的内存分配和释放效率。
#include <iostream>
#include <vector>
int main() {
std::vector<int, my_allocator<int>> v; // 1. 自定义内存分配器
v.reserve(10);
for (int i = 0; i < 10; ++i) {
v.push_back(i);
}
for (auto x : v) {
std::cout << x << " ";
}
std::cout << std::endl; // 输出0 1 2 3 4 5 6 7 8 9
return 0;
}
三、扩展与应用
STL提供了一些扩展和应用,下面将详细介绍其中的TR1中的STL扩展和应用,C++11中的STL扩展和应用,以及STL在实际开发中的应用场景和案例
1. TR1中的STL扩展和应用
TR1(Technical Report 1)是C++标准化委员会发布的技术报告,提供了一些STL的扩展。
以下是常见的TR1扩展和应用示例:
1.1 tuple
tuple是C++ TR1中的新增类型,可以将多个变量组合为一个 tuple 对象,在C++11中已成为标准库的一部分。
代码示例:
创建了一个tuple对象t,其中包含一个整数、一个字符串和一个浮点数。
使用std::get函数可以从tuple中获取对应位置的元素,还可以使用std::get函数修改元素的值。
#include <iostream>
#include <string>
#include <tuple>
int main() {
std::tuple<int, std::string, double> t(1, "hello", 3.14);
std::cout << std::get<0>(t) << std::endl; // 输出1
std::cout << std::get<1>(t) << std::endl; // 输出hello
std::cout << std::get<2>(t) << std::endl; // 输出3.14
std::get<1>(t) = "world";
std::cout << std::get<1>(t) << std::endl; // 输出world
return 0;
}
1.2 smart_ptr
smart_ptr是C++ TR1中的智能指针类型,可以自动释放对象的内存。
以下是一个使用smart_ptr的例子:
使用shared_ptr、unique_ptr、weak_ptr三种智能指针类型。
shared_ptr可以用于多个智能指针共享一个对象,并且会计数对象的引用次数,当引用次数为0时自动释放对象内存。
unique_ptr是独占式的智能指针,只能有一个智能指针指向一个对象。
weak_ptr是共享式的智能指针,不会增加对象的引用计数,不具有所有权,主要用于解决循环引用。
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> p(new int(1));
std::unique_ptr<int> q(new int(2));
std::weak_ptr<int> r(p);
std::cout << *p << std::endl; // 输出1
std::cout << *q << std::endl; // 输出2
std::cout << r.use_count() << std::endl; // 输出1
p.reset();
std::cout << r.use_count() << std::endl; // 输出0
return 0;
}
2. C++11中的STL扩展和应用
C++11为STL引入了大量的新特性和类型
以下是一些常见的C++11中的STL扩展和应用:
2.1 lambda表达式
lambda表达式是C++11中新增的一种函数对象,可以用于传递函数或函数对象。
以下是一个使用lambda表达式的例子:
定义一个lambda表达式func,用于判断一个数是否为偶数。
使用std::find_if函数从一个vector中查找符合条件的元素,并将找到的元素输出。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v = { 1, 2, 3, 4, 5 };
auto func = [](int x) { return x % 2 == 0; };
auto it = std::find_if(v.begin(), v.end(), func);
if (it != v.end()) {
std::cout << *it << std::endl; // 输出2
}
else {
std::cout << "not found" << std::endl;
}
return 0;
}
2.2 constexpr函数
constexpr函数是C++11中新增的一种特殊函数,可以在编译期间计算值。
以下是一个使用constexpr函数的例子:
定义一个constexpr函数factorial,用于计算一个数的阶乘。在main函数中使用constexpr修饰变量n和函数result,表示它们都是在编译期就能计算出来的。
#include <iostream>
constexpr int factorial(int n) {
return n == 0 ? 1 : n * factorial(n - 1);
}
int main() {
constexpr int n = 5;
constexpr int result = factorial(n);
std::cout << result << std::endl; // 输出120
return 0;
}
3. 实际开发中的应用案例
3.1 数据结构
STL中的容器适用于存储和管理各种类型的数据,例如vector、list、map、set等。在实际应用中,可以使用vector存储数据、list存储中间结果、map和set存储关联数据。
3.2 算法
STL中的算法可以用于处理各种类型的数据,例如排序、查找、合并、去重等。在实际应用中,可以使用std::sort对数据进行排序,使用std::accumulate计算总和,使用std::merge将两个有序集合合并等。
3.3 并发编程
STL中的atomic、thread和mutex等类型可以用于实现并发编程。在实际应用中,可以使用std::thread创建线程、使用std::atomic保证原子性操作、使用std::mutex和std::condition_variable保证线程同步。
四、最佳实践
1. 使用技巧和注意事项
1.1 使用合适的容器
在使用STL作为程序的一部分时,选择正确的数据结构可以带来很大的性能提升。
下面列出了一些容器类型及其适用场景:
- vector: 随机访问和快速遍历
- list: 插入和删除的频繁操作
- set: 无重复数据元素的自动排序
- map: 键值对的管理和查找
1.2 使用正确的算法
与数据结构选择类似,正确的算法选择也对性能有很大的影响。STL库包含了大量的算法,如排序算法、查找算法、拷贝算法、逆置算法以及统计算法。举个例子,在进行排序操作时,C++中使用sort函数,而不是qsort函数,因为STL中的sort函数不仅使用了快速排序算法,而且对于各种情况它都有很好的性能表现。
1.3 空间和时间的权衡
在STL开发中我们需要考虑是否使用内存容器,因为在动态内存分配时,使用空间的同时会降低程序的运行时间。如果要支持快速检索可以使用vector进行存储。如果要进行大量操作或插入操作使用list等数据结构更加合适。
2. 错误使用和常见陷阱
2.1 迭代器失效
在使用迭代器时,当容器进行插入/删除操作时,迭代器可能失效。因此必须让迭代器得以正确更新,否则在程序运行时可能会出现访问越界等问题。
#include <vector>
#include <algorithm>
// 必须使用迭代器更新,在进行插入或删除操作时
void update_iterator(std::vector<int>& vec, int& value) {
bool found = false;
for (auto it = vec.begin(); it != vec.end();) {
if (*it == value) {
vec.erase(it); // 在此删除时必须使用返回值更新迭代器,否则迭代器可能失效
found = true;
} else {
++it;
}
}
if (!found) vec.push_back(value); // vector不会让一个值出现多次,如果没有找到,则插入此值
std::sort(vec.begin(), vec.end());
}
int main() {
std::vector<int> vec = {1, 2, 4, 5, 6};
int value = 3;
update_iterator(vec, value);
return 0;
}
2.2 动态内存分配
使用C++ STL时通常会面临动态内存分配的问题。
注意如下问题:
- 在使用智能指针时,不必手动进行内存释放
- 在STL中使用动态内存时一定要记得释放内存,因为STL不会随着对象的销毁释放内存。
2.3 对象的管理方式
在使用STL时,如何管理变量和对象也会影响程序的正确性。可能会出现一些常见的陷阱,
例如有时会忘记定义和使用程序中所需的变量和对象,忘记异常处理等。
#include <vector>
class User {};
void get_user_info(std::vector<User>& users) {
// 忘记做数组越界判断
for (int i = 0; i <= users.size(); ++i) {
// 这里会访问所以的User对象,包括无效的。
users[i];
}
// 这里代码不会抛出异常,但是在访问一个无效的User对象时,很有可能导致程序错误,采用try-catch可以追踪并处理问题。
std::vector<User> new_users(10);
int idx = 10;
try {
new_users.at(idx); // 采用at()函数可以抛出out_of_range异常
} catch (std::exception e) {
// 处理
}
}
int main() {
std::vector<User> users;
get_user_info(users);
return 0;
}
3. 代码组织和风格约定
为了提高代码可维护性和开发效率,我们需要一些约定来帮助组织和布置STL代码。
3.1 编写头文件保护
为了避免头文件被多次引入导致的重复定义,C++要求每个头文件保护都应该带有独特的标识符。
#ifndef MY_HEADER_H_
#define MY_HEADER_H_
// ...
#endif // MY_HEADER_H_
3.2 排列正确的头文件
正确包含头文件可以避免一切不必要的麻烦。通常的顺序是,首先包含C和C++标准库头文件,然后是其他优先级的头文件(STL中的头文件有支持的容器和算法等),最后是本项目所需要包含的源文件头文件。
3.3 支持命名空间
STL库中每个算法和对象都在命名空间std内定义。这个名字空间是STL的总命名空间,应该把它们放到std里,好处之一是只需要定义命名空间一次,命名空间中的每个STL文件和库都可以使用该命名空间。
#include <vector>
#include <algorithm>
// 在使用C++ STL时,大多数使用都在std名字空间中。
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::sort(vec.begin(), vec.end());
return 0;
}
3.4 避免重复编写代码
避免写与STL库重复的代码,应该使用STL库提供的函数来代替对应的操作。例如在进行排序操作时,应使用std::sort来代替自己编写的排序算法。
#include <vector>
#include <algorithm>
// STL中封装了常用操作的api。
int main() {
std::vector<int> vec = {5, 3, 1, 4, 2};
// 使用STL带的sort函数。
std::sort(vec.begin(), vec.end());
return 0;
}
五、小结回顾
本文主要整理了STL的三个主题:函数对象和仿函数、内存管理和分配器、以及扩展与应用
在函数对象和仿函数方面,我们了解了函数对象的概念、使用方法和常见应用,以及如何自定义仿函数来满足特定需求。
在内存管理和分配器方面,我们探讨了STL内置的分配器和自定义分配器的使用方法和优劣,以及如何管理STL容器中的内存。
在扩展与应用方面,我们讨论了STL的一些扩展和应用,例如STL算法、迭代器和容器,以及如何在实际开发中使用STL解决问题。
通过学习这些内容可以深入了解C++ STL的内部机制,掌握其基本使用方法和高级技巧,以及在实际开发中如何充分利用STL来提高效率和质量。