泛型编程
所谓泛型编程,就是指独立于某些特定类型的编程,使得同一份代码适用于多种数据类型,从而提高代码重用性和灵活性。
在C++中,使用模板来实现泛型编程。
模板 template
模板用于描述那些通用的行为或概念。
根据描述的对象不同,模板可分为:
函数模板
类模板
函数模板
函数模板不是函数,而是用来生成函数的一种模板(机制)
一般格式:
template<typename T1, ..., typename Tn>
返回类型 函数名(参数列表)
{
函数体
}
例:
template<typename T>
void swap(T&a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
int a = 1;
int b = 2;
swap(a, b);=> 根据实参的值,推导出模板参数的类型为int, 生成具体的函数 void swap(int&,int&), 再调用这个具体生成的函数
说明:
1、模板声明中的 typename 用于说明其后的标识符,是一个类型的名字,也可以使用关键字 class
2、一般情况下,编译器能根据实参自动推导模板参数的具体类型
但,如果编译器无法根据实参推导出所有的模板参数,或推导出来的模板参数与期望的不一致
此时,需要显示的指定模板函数类型,如下:
函数模板名<模板参数列表>(实参列表)
例:
template<class T>
T max(T a, T b)
{
return a > b ?a : b;
}
max(a, b) // 自动推导
max<int>(a,b)// 显示指定
3、模板的声明与定义如果分开,则在声明与定义时都需要添加模板声明
且,模板的声明与定义必须出现在同一个编译单元中。
所以,一般的做法是把模板写在头文件中,且声明与定义一般不分开。
4、模板特化
模板特化是 通用模板的一种特殊情况, 一般如下:
template<>
返回类型 函数名(参数列表)
{
函数体
}
模板的特化版本,必须出现在通过版本的后面。
例:
template <typename T> // 通过版本
int mycompare(T a, T b)
{
if (b < a)
return 1;
if (a < b)
return -1;
return 0;
}
template <> // 特别版本
int mycompare(const char *a, const char *b)
{
if (strcmp(a, b) > 0)
return 1;
if (strcmp(a, b) < 0)
return -1;
return 0;
}
类模板
类模板不是类,而是用来生成类的一种机制
一般格式:
template<typename T1, ..., typename Tn>
class 类名
{
public:
公有方法
private:
私有数据/方法
};
说明:
使用类模板时,必须显示的指定模板参数
当用类模板实例化对象时,是编译器自动使用模板生成类型,再用类型实例化对象
模板类名<模板参数列表> 对象名;
类模板的成员函数声明与定义如果分开,则它们必须出现在同一个编译单元中,且在类外定义成员函数时,必需要添加模板声明。
例:
template<typename T>
class SeqStack
{
public:
SeqStack(int n);
~SeqStack();
void push(T data);
void pop();
T top() const;
bool empty() const;
private:
T* _data;
int _top;
int _max;
};
SeqStack<int> stack1(10);
stack1.push(1);
stack1.push(2);
stack1.push(3);
while (!stack1.empty())
{
cout << stack1.top() << endl;
stack1.pop();
}
SeqStack<std::string> stack2(5);
标准模板库 Standard Template Library - STL
STL是C++标准库的重要组成部分
STL由以下几大组件组成:
容器、算法、迭代器、适配器、分配器、函数对象
核心组件:容器、算法、迭代器
容器 Container
容器是一些通用的类模板与算法的汇集,允许程序员简单地实现如队列、链表和栈这样的常见数据结构。有两 (C++11 前)三 (C++11 起)类容器:
顺序容器
关联容器
无序关联容器(C++11 起)
它们被设计为各自支持一组不同的操作。
容器管理着为其元素分配的存储空间,并提供成员函数来直接访问或通过迭代器(具有类似于指针的属性的对象)访问它们。
顺序容器
序列容器实现能按顺序访问的数据结构。
array (C++11)固定大小的原位连续数组(类模板)
vector 动态的连续数组(类模板)
deque 双端队列(类模板)
forward_list (C++11 起)单链表(类模板)
list 双链表(类模板)
关联容器
关联容器实现能快速查找(O(log n) 复杂度)的有序数据结构。
set 唯一键的集合,按照键排序(类模板)
map 键值对的集合,按照键排序,键是唯一的(类模板)
multiset 键的集合,按照键排序(类模板)
multimap 键值对的集合,按照键排序(类模板)
无序关联容器 (C++11 起)
无序关联容器提供能快速查找(平均 O(1),最坏情况 O(n) 的复杂度)的无序(散列)数据结构。
unordered_set (C++11 起)唯一键的集合,按照键生成散列(类模板)
unordered_map (C++11 起)键值对的集合,按照键生成散列,键是唯一的(类模板)
unordered_multiset (C++11 起)键的集合,按照键生成散列(类模板)
unordered_multimap (C++11 起)键值对的集合,按照键生成散列(类模板)
容器适配器
容器适配器为顺序容器提供了不同的接口。
stack 适配一个容器以提供栈(LIFO 数据结构)(类模板)
queue 适配一个容器以提供队列(FIFO 数据结构)(类模板)
priority_queue 适配一个容器以提供优先级队列(类模板)
智能指针
unique_ptr(C++11) 拥有独有对象所有权语义的智能指针(类模板)
独占式智能指针,同一时刻只允许一个指针指向它所管理的对象
当unique_ptr对象销毁时,自动释放它所管理的 对象。
unique_ptr 不可复制,但可移动。
使用示例:
std::unique_ptr<Demo> p2 = std::make_unique<Demo>(); // C++14
p2->print();
shared_ptr(C++11) 拥有共享对象所有权语义的智能指针(类模板)
共享式智能指针,允许多个共享式智能指针同时访问它们所管理的对象
shared_ptr 可复制,也可移动。
shared_ptr 内部有一个引用计数器,当拷贝 shared_ptr时,引用计数+1,当 shared_ptr 销毁时,引用计数-1
当引用计数减到0时,自动释放它所管理的对象。
使用示例:
std::shared_ptr<Demo> p2 = std::make_shared<Demo>();
p2->print();
weak_ptr(C++11) 到 std::shared_ptr 所管理对象的弱引用(类模板)
std::weak_ptr 是一种智能指针,它对被 std::shared_ptr 管理的对象存在非拥有性(「弱」)引用。
在访问所引用的对象前必须先转换为 std::shared_ptr。
std::weak_ptr 用来表达临时所有权的概念:
当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用 std::weak_ptr 来跟踪该对象。
需要获得临时所有权时,则将其转换为 std::shared_ptr,此时如果原来的 std::shared_ptr 被销毁,
则该对象的生命期将被延长至这个临时的 std::shared_ptr 同样被销毁为止。
std::weak_ptr 的另一用法是打断 std::shared_ptr 所管理的对象组成的环状引用。
若这种环被孤立(例如无指向环中的外部共享指针),则 shared_ptr 引用计数无法抵达零,而内存被泄露。
能令环中的指针之一为弱指针以避免此情况。
auto_ptr(C++17 中移除) 拥有严格对象所有权语义的智能指针(类模板)
不建议使用
线程 thread
类 thread 表示单个执行线程。线程允许多个函数同时执行。
线程在构造关联的线程对象时立即开始执行(等待任何OS调度延迟),从提供给作为构造函数参数的顶层函数开始。
顶层函数的返回值将被忽略,而且若它以抛异常终止,则调用 std::terminate 。
顶层函数可以通过 std::promise 或通过修改共享变量(可能需要同步,见 std::mutex 与 std::atomic )将其返回值或异常传递给调用方。
std::thread 对象也可能处于不表示任何线程的状态(默认构造、被移动、 detach 或 join 后),并且执行线程可能与任何 thread 对象无关( detach 后)。
没有两个 std::thread 对象会表示同一执行线程;
std::thread 不是可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 的,
尽管它可移动构造 (MoveConstructible) 且可移动赋值 (MoveAssignable) 。
线程创建示例:
std::thread t1(task1); // 线程函数task1是全局函数,且没有参数
std::thread t2(task2, n); // 线程函数task2是全局函数, 参数为值传递
std::thread t3(task3, std::ref(n)); // 线程函数task3是全局函数,参数为引用传递
std::thread t4(&Demo::task4, &d, n); // 线程函数task4是Demo类的非静态成员函数, d是Demo类的对象, 参数为值传递
线程操作:
join() // 阻塞等待线程结束
detach() // 使线程分离,不阻塞
注: 线程创建后, 必须以上函数二选一执行.
线程管理:
std中有一个嵌套的命名空间 this_thread, 其中定义了组全局函数, 可用于管理线程
get_id() // 用于获取当前线程的ID
sleep_for() // 线程休眠
注:
编译链接 带线程的程序时,可能 需要添加编译选项 -pthread
互斥锁 mutex
mutex 类是能用于保护共享数据免受从多个线程同时访问的同步原语。
mutex 提供排他性非递归所有权语义:
调用方线程从它成功调用 lock 或 try_lock 开始,到它调用 unlock 为止占有 mutex 。
线程占有 mutex 时,所有其他线程若试图要求 mutex 的所有权,则将阻塞(对于 lock 的调用)或收到 false 返回值(对于 try_lock ).
调用方线程在调用 lock 或 try_lock 前必须不占有 mutex 。
若 mutex 在仍为任何线程所占有时即被销毁,或在占有 mutex 时线程终止,则行为未定义。
std::mutex 既不可复制亦不可移动
说明:
虽然互斥锁可单独使用, 但一般会使用互斥锁包装器,进行包装, 常用包装器如下:
lock_guard
lock_guard 是一种基于作用域的锁包装器, 当创建 lock_guard 对象时, 自动上锁, lock_guard 对象销毁时,自动解锁.
典型使用方式如下:
std::mutex m; // 创建互斥锁对象
std::lock_guard<std::mutex> lg(m); // 进行包装,得到包装器对象 lg, 构造函数中上锁, 析构函数中解锁
unique_lock
unique_lock 具有 lock_guard的功能, 但功能更强大, 允许锁包装器对象存活时解锁与上锁.
一般配合 条件变量 使用.
条件变量 condition_variable
condition_variable 类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable 。
有意修改变量的线程必须
获得 std::mutex (常通过 std::lock_guard )
在保有锁时进行修改
在 std::condition_variable 上执行 notify_one 或 notify_all (不需要为通知保有锁)
即使共享变量是原子的,也必须在互斥下修改它,以正确地发布修改到等待的线程。
任何有意在 std::condition_variable 上等待的线程必须
在与用于保护共享变量者相同的互斥上获得 std::unique_lock<std::mutex>
执行下列之一:
检查条件,是否为已更新或提醒它的情况
执行 wait 、 wait_for 或 wait_until ,等待操作自动释放互斥,并悬挂线程的执行。
condition_variable 被通知时,时限消失或虚假唤醒发生,线程被唤醒,且自动重获得互斥。之后线程应检查条件,若唤醒是虚假的,则继续等待。