1. 模板
1.1 模板的概念

1.2 函数模板
(1)模板的格式

仅仅声明了一个模板,想要使用还需具体实例化
(2)隐式实例化
隐式实例化:调用函数时才会进行函数实例化
#include<iostream>
using namespace std;
template<typename T, typename M>
auto maxval(T a, M b) {
return a > b ? a : b;
}
int main() {
cout << maxval(2, 3.3) << endl;
}
当main函数中调用maxval时,才会实例化出一个函数
double maxval(int, double){return a > b?a:b;}
(3)显示实例化
在调用函数前已经声明并定义了实例化函数,已经定义了具体的函数参数类型
#include<iostream>
#include<string>
#include<vector>
using namespace std;
template<class T, class M> auto max(T a, M b);
// 显式实例化
template<class T, class M>
auto max(int a, double b) {
std::cout << "template:";
return a > b ? a : b;
}
auto max(float a, float b) {
cout << "reload:";
return a > b ? a : b;
}
int main() {
cout << max<int,double>(3, 4) << endl;
return 0;
}
函数max在调用前已经声明并定义了,参数的类型由模板类型替换为具体类型
(4)特化
对于某种类有特别的处理,需要进行特化
特化格式:

实例:对于int,int型特化
#include<iostream>
#include<string>
#include<vector>
using namespace std;
template<class T, class M> auto max(T a, M b){
cout << "tempalte:";
return a > b ? a : b;
}
template<> //对于int,int型有特定的处理,使用特化
auto max(int a, int b) {
cout << "special:";
return a > b ? a : b;
}
int main() {
cout << max(3, 4) << endl;
return 0;
}
输出:

注:特化也是显式实例化的一种,因此不能又显式实例化又特化
(4)函数重载
#include<iostream>
#include<string>
#include<vector>
using namespace std;
template<class T, class M> auto max(T a, M b){
cout << "tempalte:";
return a > b ? a : b;
}
auto max(int a, int b) {
cout << "reload:";
return a > b ? a : b;
}
int main() {
cout << max(3, 4) << endl;
return 0;
}
当函数调用既满足模板,又满足重载时,优先重载
(5)总结:

1.3 类模板
(1)实例化
#include<iostream>
#include<string>
using namespace std;
template<typename T>
class Array {
private:
int _size;
T* _ptr;
public:
Array(T arr[], int s) ;
void show();
};
template<typename T> // 实现模板类方法前都需声明模板
Array<T>::Array(T arr[], int s) {
_size = s;
_ptr = new T[s];
for (int i = 0; i < s; i++) {
_ptr[i] = arr[i];
}
}
template<typename T>
void Array<T>::show() {
// 打印
for (int i = 0; i < _size; i++) {
cout << *(_ptr + i) << " ";
}
cout << endl;
}
int main() {
string name[] = { "zs", "李四", "w5" };
Array<string> namelist(name, 3);
namelist.show();
return 0;
}
实现模板类方法前都需声明模板
(2)特化
和函数模板的特化类似,将模板对应的某种特定类型实例化

类模板的特化不能继承成员和成员函数,所有成员都需要重新定义
(3)部分特化
#include<iostream>
#include<string>
#include<vector>
using namespace std;
template<class T1, class T2>
class mypair {
private:
T1 first;
T2 second;
public:
mypair(T1 a, T2 b):first(a),second(b){}
void print_base() {
cout << "模板" << endl;
}
};
// 部分特化
template<class T2>
class mypair<int, T2> { // 必须加尖括号里的内容 <int, T2>
private:
int first;
T2 second;
public:
mypair(int a, T2 b):first(a),second(b){}
void print() {
cout << "部分特化" << endl;
}
};
int main() {
mypair<int, float> pair(1, 1.5f);
pair.print();
return 0;
}
将参数1给特化为int,参数2仍为模板
(4)类模板参数为模板

实例:

#include<iostream>
#include<string>
#include<vector>
#include<set>
using namespace std;
// 创建一个模板类,其中一个参数为模板
template<template <typename...> class Container, typename T>
class Wrapper {
private:
Container<T> m_values;
public:
Wrapper(const Container<T>& o):m_values(o){}
void print() {
for (auto v : m_values)
cout << v << endl;
}
};
int main() {
vector<int> myvector{ 1,3,5,7,9 };
set<int> myset{ 1,1,3,9,6,3 };
// vector是传入两个参数的模板,在定义时需定义多参数模板
Wrapper<vector, int> mywrapper(myvector);
mywrapper.print();
Wrapper<set, int> mywrapper2(myset);
mywrapper2.print();
return 0;
}
1.4 STL标准模板库
2. 运算符重载
运算符重载函数必须为全局函数或类成员函数
当为类成员函数时,操作数会隐藏左操作数为this,只需传入其他操作数
而当为全局函数,则需要传入全部的操作数
2.1 重载移位运算符
原型:

全局函数,需传入全部的两个操作数
实例:使用cout << 打印vector
// 重载移位运算符
ostream& operator<< (ostream& o, vector<int> &num) {
o << "[";
int n = num.size();
for (int i = 0; i < n - 1; i++) {
o << num[i] << ",";
}
o << num[n - 1] << "]";
return o;
}
int main() {
vector<int> num{1, 3, 7, 5, 8, 6};
cout << num << endl;
return 0;
}
原移位运算符没有接收vector参数,需手动重载该运算符
2.2 重载加法运算符
实例:实现类的加法,将私有变量r,i对应相加
// 重载加法运算符--作为类成员变量函数
class Complex {
private:
float r;
float i;
public:
Complex(float real, float imaginary):r(real), i(imaginary){}
void show() {
cout << "r = " << r << "------i = " << i << endl;
}
// 重载+, 加法运算符第一个操作数是隐含的 this 指针,只需传一个参数
// 第二个const保证不会修改当前对象,即this指向的对象
Complex operator+(const Complex& other) const{
return Complex(this->r + other.r, this->i + other.i);
}
};
int main() {
Complex a(1.0, 0.3);
Complex b(2.1, 1.6);
Complex c = a + b;
c.show();
return 0;
}
加法本身需要两个参数,但这里是类的成员函数,会隐藏左操作数为this,只需要传入又操作数。
运行结果:

2.3 可以重载的运算符
(1)一元运算符

增量++或减量–的运算符重载有两种方式

后缀方式中的int,只起到标识作用
实例:

(2)二元运算符

2.4 只能作为成员函数重载的函数

2.5 函数调用运算符

参数数量可以改变,重载运算符中唯一可以的
实例:重载()运算符,实现一个斜线,输入x可求出y值
class LinearFunction {
private:
double k;
double b;
public:
LinearFunction(double m, double n):k(m),b(n){}
// 重载()运算符
double operator()(double x)const {
return k * x + b;
}
};
int main() {
LinearFunction l(2, 3); // 设置斜率为2,截距为3
cout << l(2) << endl;
cout << l(3.5) << endl;
return 0;
}
2.6 间接引用运算符

typedef struct complex { // 复值结构
float r, i;
}complex;
class localPtr {
private:
complex* m_ptr;
public:
localPtr(complex* ptr):m_ptr(ptr){}
// 重载->运算符
complex* operator->() const{
return m_ptr;
}
};
int main() {
localPtr mylocal(new complex({ 1.0,3.5 }));
mylocal->i++;
return 0;
}
2.7 下标运算符
重载后可以像数组一样访问元素

实例:

2.8 不能重载的运算符


3. 宏定义
3.1 常使用的宏

3.2 “#”
例:

将#后面的内容,字符串的该内容,例如本例中,#a = " a *2 + 3 "
3.3 “##”
例:

将两个表达式连接
3.4 可变参的宏函数
例:

4. 异常
4.1 try
检查其后面大括号中代码块所产生的异常

4.2 catch
获和处理上述代码块产生的异常

一个try后面,可以使用多个catch捕获
catch+…捕获未知异常

一般不处理位置异常,而是直接抛出
4.3 throw
用于抛出异常

4.4 异常实例

catch括号里捕获的类型为 throw抛出的类型
4.5 RAII—资源获取即初始化

问:new创建的对象可能会因为异常的抛出而提前结束,资源不会被释放
解决方法:智能指针
利用RAII的思想,在对象的构造函数中开辟空间,在析构函数中释放资源。如果直接new创建对象,则该指针会指向堆区,仍然会有问题。因此使用智能指针创建,指针指向栈区,在对象内存存在另一个指针指向堆区。当异常时,系统会清理并销毁栈区上的内容。清除栈区上的智能指针,会调用析构函数,然后清除掉堆区开辟的内存
例:
#include <iostream>
#include <memory> // for std::unique_ptr
void someFunctionThatMightThrow() {
throw std::runtime_error("Something went wrong!");
}
void process() {
// 1. 在栈上创建一个 unique_ptr,它管理着一块在堆上分配的 int 内存。
// 这符合RAII:资源(堆内存)在构造函数中被获取。
std::unique_ptr<int> smartPtr(new int(42));
// 2. 假设这里进行一些操作...
std::cout << "Value: " << *smartPtr << std::endl;
// 3. 一个可能抛出异常的函数调用!
someFunctionThatMightThrow(); // <-- 异常在这里抛出!
// 4. 如果异常抛出,这里的代码不会被执行
std::cout << "This line won't be reached." << std::endl;
} // 正常情况下,smartPtr 在这里离开作用域,析构函数被调用,内存被释放。
int main() {
try {
process();
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
std::cout << "Program continued safely." << std::endl;
return 0;
}

5. 显示转换

5.1 动态转换
在编译和运行阶段都会检查。只能用于指针或引用的转换,一般用于基类转换为派生类

- 基类必须是多态类,存在虚函数
- CBase指针指向的对象必须为CDerived
5.2 静态转换
只在编译阶段检查。不要求基类为多态类,只要求两个类兼容。可用于父类转换为子类,或子类转换为父类

实例:隐式转换和static_cast多数情况可以替换
#include<iostream>
using namespace std;
class A {
};
class B :public A {
public:
class B(const A& a){}
};
int main() {
// static_cast
A a1;
B b1 = static_cast<B>(a1);
cout << "static_cast:" << typeid(b1).name() << endl;
// 隐式转换
A a2;
B b2 = a2;
cout << "隐式转换:" << typeid(b2).name() << endl;
return 0;
}
在B的构造函数中创建了一个转换构造函数,传入的参数为A
静态转换和隐式转换中,当类型为B的对象接收到A时,会去检查B的构造函数中是否存在类型为A的构造函数,存在则会转换为B(A),创建一个对象赋值
运行结果:

5.3 重解释转换
可以将一个类指针转换为另一个类指针,不会检查是否有效
实例:

转换为long会保存,因为容纳范围不够,longlong可以
5.4 常量转换
用于常量类型和非常量类型的转换

6. 函数封装和绑定
6.1 std::function
(1)定义

(2)实例1:封装普通函数

(3)实例2:封装类成员函数

函数模板的arg第一个参数必须为类名的引用
(4)类型擦除模式

#include<iostream>
#include<map>
#include<functional>
using namespace std;
float add(float a, float b) {
return a + b;
}
int sub(int a, int b) {
return b - a;
}
int main() {
map<char, function<double(double, double)>> calculate{
{'+',add},
{'-',sub},
{'*', [](double a, double b)->double {return a * b; }}
};
cout << calculate['+'](1.0, 2.0) << endl;
cout << calculate['-'](3.1, 4.2) << endl;
cout << calculate['*'](4.6, 7.1) << endl;
return 0;
}
所有函数的参数类型全部转换为double
将静态多态(模板)的动态类型转换为一种统一的、类型无关的接口的强大技术
6.2 ste::mem_fn

用于封装类的成员
实例:

6.3 std::bind

实例:

绑定函数和函数的参数。调用绑定的bind等同于调用 func(arg)
实例2:使用占位符,实际调用时再传入参数

实例3:绑定变量作为参数

使用cref(n),绑定的为n的引用。当修改n时,则调用函数中的参数也会修改
若不使用cref,由于bind为值传递,则修改n不影响函数调用的值
7. 智能指针

7.1 unique_ptr
只允许一个unique_ptr管理某个对象,因此在底层删除了拷贝构造函数和赋值构造函数
一般用于代替普通指针,实现RAII
(1)原型

(2)常用函数

(3)使用方法

实例:

虽然只允许一个unique_ptr管理一个对象,但可以使用move转移某个对象的管理权
7.2 shared_ptr

当引用计数器ref_count==0时,释放该对象
(1)常用函数

(2)专用函数

(3)内存泄漏

循环引用,每个智能指针的计数器均为2。当删除智能指针时,计数器减为1,对象不会被释放,出现内存泄漏
7.3 weak_ptr
需结合shared_ptr使用,只会对shared_ptr的计数器起观测作用,不会改变计数器的数值
(1)实例:结合weak_ptr优化循环引用

此时每个指针的计数器均为1,weak_ptr不会改变计数器。当shared_ptr销毁时,就会销毁对象。
(2)实例:weak_ptr查看shared_ptr

使用weak_ptr.lock()得到智能指针
(3)底层

当use_count == 0 && weak_count == 0, 控制块被删除
8. 右值引用和移动语义
8.1 左值和右值
(1)左值—能够代表一块区域的表达式
能有获得这个表达式的引用,或取地址的表达式,即为左值

a可以由被引用获取,因此为左值
c可以取地址,因此为左值
4为字面量,不能取地址,因此为右值
(2)图示

glvalue:泛左值; prvalue:纯右值
xlvalue:expiring value 即将消亡的值
(3)lvalue

指针,数组,数组的元素,引用都为左值

类的数据成员也为左值

返回引用的函数表达式也为左值,因此 refNumber()为左值,n会被赋值为5
引用的初始化必须为左值初始化,但赋值可以用右值赋值
(4)prvalue—纯右值

将计算结果存储在临时变量中的值为纯右值
类型转换的值也为prvalue

未命名的类对象 和 返回未命名类对象的函数表达式 也为右值

(5)xvalue—即将消亡的值

函数返回的右值引用(move函数转换),左值到右值的静态转换都是xvalue

未命名的右值对象获得其非静态成员的表达式为xvalue
8.2 右值引用

右值引用使用两个&&, 并且右值引用的对象不能为左值
(1)实例:重载函数区分右值引用和左值引用
#include<iostream>
#include<map>
#include<functional>
using namespace std;
void func(int& a) {
cout << "左值引用:" << a << endl;
return;
}
void func(int&& a) {
cout << "右值引用:" << a << endl;
return;
}
int main() {
int a = 5;
func(a); // 传入左值
func(5); // 传入右值
return 0;
}
运行结果:

(2)右值引用绑定move(左值)
当右值引用绑定move(左值)时,由于右值引用本质也是引用,因此相当于绑定了move中的左值
#include<iostream>
#include<thread>
#include<vector>
using namespace std;
int main() {
int a = 10;
int &&b = move(a);
a++;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
运行结果:

b相当于a的引用,a修改,b也修改
8.3 move函数
将左值转换为右值引用
当move的对象为内置类型(int等)时,移动等于拷贝。而当对象为类类型(string等)时,移动会将类中的资源转移到新的对象上,原对象会变空,但仍存在
原因:
- 移动语义的目的是 避免昂贵的深拷贝。
如果一个类型里有 堆资源 / 文件句柄 / 线程 ID / socket 等独占资源,那拷贝就很重,甚至不合法。所以类类型会实现“移动构造函数”,在移动时把内部资源的“所有权”转移给新对象。
class MyString {
char* data;
size_t len;
public:
// 移动构造:转移指针,避免内存复制
MyString(MyString&& other) noexcept
: data(other.data), len(other.len) {
other.data = nullptr; // 清空旧对象
other.len = 0;
}
};
- 内置类型没有“资源”
对于 int、double、指针(本身作为值,不是指向的内存),这些 就是值本身,没有额外的资源。
所以 移动和拷贝是一样的,因为“转移资源”这个概念对它们没有意义
3.类类型的资源可以“偷”
std::string s = "hello";
std::string t = std::move(s);
s 内部有一个指针指向 “hello” 的堆内存。移动时,t 把这个指针直接接管了(O(1) 操作)。s 被清空,保证析构时不会二次释放。
这样避免了重新分配内存和拷贝字符的开销。
例:线程无拷贝构造函数和拷贝赋值,因此需传入右值引用,使用移动构造函数
#include <thread>
#include <vector>
int main() {
std::vector<std::thread> threads;
// emplace_back 直接在 vector 内构造一个 thread
threads.emplace_back([] { /* work */ });
// 先创建一个 thread
std::thread t([] { /* work */ });
// push_back 需要传入右值,触发移动构造函数
threads.push_back(std::move(t)); // ✅ 触发 thread(thread&&)
}
empalce_back传入一个lambda表达式为什么能将一个线程添加进vector?
原因:
emplace_back会创建一个模板对象(这里为thread),然后将传入的参数(lambda表达式)传给刚创建的对象。lambda表达式为右值,会被构造函数(万能模板)接收。
push_back传入的参数为什么需要先move?
原因:
push_back的原理为拷贝构造,但thread删除了拷贝构造函数。因此,使用move,将t变为右值引用,会调用移动构造函数。在移动构造函数内部,会将t的资源移动到vector的某个元素中,实现添加到线程vector
9. 完美转发
9.1 forward

type的定义:
template <typename T>
struct remove_reference {
using type = T; // 定义类型成员 type
};
// 特化版本
template <typename T>
struct remove_reference<T&> {
using type = T;
};
template <typename T>
struct remove_reference<T&&> {
using type = T;
};

实例:使用forward解决语义问题

若不使用forward转换,则都会调用左值字符串。因为在函数中s为局部变量,是左值。传入f时,均为左值。
9.2 万能引用

传入左值的引用,T会被推导为左值的引用
实例:万能引用的使用----完美转发

- 当传入的参数为右值引用时,例如string,T会被解析为string。函数g传入右值引用。根据9.1的forward函数,forword< string >(v)
根据9.3的引用折叠规则,forward格式为:
string&& forward<string&> (string &&t) noexcept{
return static_cast< string &&>(t);
返回右值引用
- 当传入的参数为左值引用时,例如string,T会解析为string&。函数g传入左值引用。forward<string&>(v)
forward格式为:
string& forward<string&> (string &t) noexcept{
return static_cast< string &>(t);
9.3 引用折叠规则

10. 常量表达式和constexpr
10.1 常量表达式
常量表达式是一个表达式的值,是个固定值
由常量,字面量,函数,运算符组成的表达式,const定义由非常量赋值的常量不被包括

在编译阶段就会直接替换结果
10.2 constexpr

会在编译时进行计算并替换
(1)const和constexpr的范围:

const和constexpr在整型的定义上是相同的
其他类型的都是不同的
(2)constexpr修饰函数:
当使用constexpr修饰的函数,其参数也为常量时,该函数表达式也为常量表达式,在编译时进行替换

c为常量表达式,会在编译时替换;d,f不会
(3)常量表达式函数的要求

内部不能调用非constexpr函数,因此,只有cmath头文件中的函数可用,iostream和thread的函数都不可用
10.3 自定义类的常量表达式

在构造函数前加constexpr
11. 类的特殊成员函数

均可以使用default关键字让编译器自动生成,也可以使用delete关键字删除
11.1 移动构造函数/移动赋值运算符
当类中没有自定义的默认,拷贝,移动构造函数和拷贝赋值运算符时,编译器会隐式声明并创建移动构造函数和移动构造运算符,参数均为右值引用


11.2 平凡可复制类
(1)定义
可以通过直接复制其对象在内存中的二进制位(例如使用 memcpy 或 memmove)来产生一个完全相同的、有效的副本。

平凡指在这里指的是编译器自动生成的默认版本
拷贝,移动,拷贝赋值,移动赋值要不默认生成,要不显示定义default(不关心默认构造函数,因为有上述几个函数就可以平凡复制了)
(2)例:A为平凡可复制类

(3)例:判断类是否平凡可复制


判断类型的构造,赋值,移动构造函数是否可平凡,即函数为编译器自动生成
(4)例:拥有const或引用成员的类,不为平凡可复制类
- 对const成员,对其进行拷贝赋值,会使类中的const成员被赋值,语义上不允许,因此不会创建默认的拷贝赋值运算符
- 对于引用成员,对其进行拷贝赋值,会给类中的引用对象重新绑定一个对象,这是不被允许的,也不会创建默认的拷贝赋值运算符
总结:不创建默认拷贝赋值运算符的类即为非平凡可复制类
11.3 标准布局类型
(1)定义
- 所有标量类型都是标准布局类
标量类型代表一个单一的值,而非标量类型代表值的集合或一个逻辑上的聚合体。不包含数组、类/结构体、联合体、函数类型(但包含函数指针!)、void
-
对于非继承类,不存在虚函数,对于非静态成员都有相同的访问控制属性,并且都是标准布局类型,即为标准布局类。
-
对于继承类,保证该类及其父类拥有静态数据成员,且也满足 (2)的定义

base, Derived2为标准布局类, Derived1有成员,不为标准布局类
(2)查看是否为标准布局类

12. 类型特征—type traits
头文件下 #include < type_traits > 下有一系列模板
12.1 常用模板

12.2 使用方法
例:is_integral的使用,判断是否为整型

value的值与T有关,当T为整型时,value=true;反之,则为false
12.3 原理

前提:
my_is_integral继承false_type / true_type,内部存在一个静态成员变量,const int value分别为false / true
{} 表示没有额外的成员变量或函数
- 实现类模板,继承自false_type,内部成员value默认为false
- 实现模板的特化,将int,bool,char等类型特化,继承自true_type,value为true
- 因此,通过value即可知道,is_intergral是否为true
缺陷:无法实现const int等存在修饰符的判断
实例:只实现了int的特化
#include<iostream>
#include<type_traits>
using namespace std;
template <typename T>
struct my_is_integral:false_type{};
template <>
struct my_is_integral<int>:true_type{};
int main() {
cout << my_is_integral<int>::value << endl;
cout << my_is_integral<double>::value << endl;
return 0;
}
简化:直接获取成员变量

template <typename T>
struct my_is_integral :false_type {};
template <>
struct my_is_integral<int> :true_type {};
template<class T> // 定义一个常量表达式获取内部成员
// 由于value是一个constexpr,因此函数表达式可以用constexpr修饰
constexpr bool my_is_integral_v{ my_is_integral<T>::value };
int main() {
cout << my_is_integral_v<int> << endl;
cout << my_is_integral_v<double> << endl;
return 0;
}
12.4 优化—可识别带修饰符的类型

- remove_cv< T>::type 中保存 T去除const 和volatile修饰符后的类型
- my_is_integral_helper封装去除修饰符后得到的value结果
12.5 if constexpr — C++17及以后可用
- 使用if constexpr,则当编译时会先判断条件是否满足,若满足,该段代码才会被编译。反之,则不会编译。
- if constexpr的条件必须为常量表达式
例:某些类型存在length,某些类型不存在length

在本例中,当传入不存在length成员的类型时,虽然这句分支不会执行,但仍然会编译这句代码。由于t中不存在length成员,因此会报错。使用if constexpr判断,先判断再编译,来解决。
13. Algorithm—算法库
头文件< algorithm >中存在一系列算法
14. 并行编程
同一时间有多个任务同时执行
14.1 实现并行的方法

14.2 openMP
(1)使用前需查看并打开openMP
查看:
int main() {
// 检查OpenMP版本(如果未启用OpenMP,_OPENMP宏不会被定义)
#ifdef _OPENMP
cout << "OpenMP is enabled! Version: " << _OPENMP << endl;
#else
cout << "OpenMP is NOT enabled!" << endl;
return 1; // 直接退出
#endif
omp_set_num_threads(4);
#pragma omp parallel
{
int thread_id = omp_get_thread_num();
int total_threads = omp_get_num_threads();
cout << "hello from thread:" << thread_id << " of " << total_threads << endl;
}
cout << "all threads finish" << endl;
return 0;
}
打开:

(2) 基本使用
OpenMP 指令的格式通常是 #pragma omp …。
该语句后的 {}里的内容为并行区域
例:
#include<iostream>
#include<omp.h> // 包含omp_get_thread_num等函数
using namespace std;
int main() {
// 这句话后的{}内的内容为并行区域
#pragma omp parallel num_threads(8)
{
// 这块代码会被多个线程执行
int thread_id = omp_get_thread_num(); // 获取线程id(从0开始)
int total_threads = omp_get_num_threads(); // 获取线程总数
cout << "hello from thread:" << thread_id << " of " << total_threads << endl;
} // 等待所有线程执行完,才会执行main函数的后续代码
cout << "all threads finish" << endl;
return 0;
}
注:线程总数是存在不同的优先级设置而定,优先级从高到低:
- 在 parallel 指令中直接指定
#pragma omp parallel num_threads(8)- 在main函数中设置后续默认线程数,除非遇到num_threads
omp_set_num_threads(4);- 环境变量
- 系统默认值,通常为cpu核数
(3)并行循环
使用 #pragma omp parallel for
例:
#include<iostream>
#include<omp.h> // 包含omp_get_thread_num等函数
using namespace std;
int main() {
omp_set_num_threads(4); // 设置线程数
#pragma omp parallel for // 后面必须为for循环
for (int i = 0; i < 10; i++) {
cout << i * i + 1 << endl;
}
cout << "all threads finish" << endl;
return 0;
}
#pragma omp parallel for 指令做了两件事:
- parallel:创建一组线程。
- for:将紧随其后的 for 循环的迭代自动划分给这些线程执行。
并行for循环的必要条件:循环必须是可并行化的,即迭代之间没有数据依赖(一次迭代 i 的计算不依赖于另一次迭代 j 的结果)。
(4)数据共享属性 (shared, private, firstprivate, reduction)

例:reduction的使用
#include<iostream>
#include<omp.h> // 包含omp_get_thread_num等函数
#include<thread>
#include<chrono>
using namespace std;
int main() {
int sum = 0;
omp_set_num_threads(4); // 设置线程数
// 错误的写法,直接使用共享变量sum可能会造成数据错误
/*
#pragma omp parallel for // 后面必须为for循环
for (int i = 0; i < 10; i++) {
int temp = sum; // 读取当前sum值
// 添加一些延迟,放大数据竞争的可能性
this_thread::sleep_for(chrono::milliseconds(10));
sum = temp + i; // 写入新的sum值
}
*/
// 正确的写法,reduction,在每个线程中创建sum的副本,然后最后op该副本到原数据
#pragma omp parallel for reduction(+:sum) // 共享数据sum,最后使用+和
for (int i = 0; i < 10; i++) {
int temp = sum; // 读取当前sum值
// 添加一些延迟,放大数据竞争的可能性
this_thread::sleep_for(chrono::milliseconds(10));
sum = temp + i; // 写入新的sum值
}
cout << "sum = " << sum << endl;
return 0;
}
在定义pragma时确定共享数据sum为reduction类型,最后将数据相加
(5)同步结构 (critical, atomic, barrier)



默认在{}结束出,存在一个隐式的barrier。必须所有线程到达花括号尾部才向下执行
(6)输出乱序问题解决
#include<iostream>
#include<omp.h> // 包含omp_get_thread_num等函数
using namespace std;
int main() {
// 这句话后的{}内的内容为并行区域
omp_set_num_threads(4);
#pragma omp parallel
{
// 这块代码会被多个线程执行
int thread_id = omp_get_thread_num(); // 获取线程id(从0开始)
int total_threads = omp_get_num_threads(); // 获取线程总数
cout << "hello from thread:" << thread_id << " of " << total_threads << endl;
}
cout << "all threads finish" << endl;
return 0;
}
输出:存在乱序

这是因为输出都是在对标准输出这个共享资源进行操作。当一个线程输出还未结束时,可能会被os打断,将标准输出让给其他线程输出。等待os调度,再输出剩余的内容。
解决办法:使用critical临界区

使用critical,保证后续代码为原子操作
(7)总结

14.3 STL并行算法库—c++17
vs2019及之后对并行算法支持好
(1)基本使用

三种策略分别为,顺序策略,并行策略和并行向量化策略
seq,par, par_unseq分别为不同策略对应的实例。policy接收一个策略实例



2万+

被折叠的 条评论
为什么被折叠?



