C++进一步学习

参考视频:一起来学C++

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标准模板库

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 动态转换

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

在这里插入图片描述

  1. 基类必须是多态类,存在虚函数
  2. 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等)时,移动会将类中的资源转移到新的对象上,原对象会变空,但仍存在

原因:

  1. 移动语义的目的是 避免昂贵的深拷贝。
    如果一个类型里有 堆资源 / 文件句柄 / 线程 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;
    }
};

  1. 内置类型没有“资源”
    对于 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会被推导为左值的引用

实例:万能引用的使用----完美转发
在这里插入图片描述

  1. 当传入的参数为右值引用时,例如string,T会被解析为string。函数g传入右值引用。根据9.1的forward函数,forword< string >(v)
    根据9.3的引用折叠规则,forward格式为:
    string&& forward<string&> (string &&t) noexcept{
    return static_cast< string &&>(t);
    返回右值引用
  1. 当传入的参数为左值引用时,例如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或引用成员的类,不为平凡可复制类

  1. 对const成员,对其进行拷贝赋值,会使类中的const成员被赋值,语义上不允许,因此不会创建默认的拷贝赋值运算符
  2. 对于引用成员,对其进行拷贝赋值,会给类中的引用对象重新绑定一个对象,这是不被允许的,也不会创建默认的拷贝赋值运算符

总结:不创建默认拷贝赋值运算符的类即为非平凡可复制类

11.3 标准布局类型

(1)定义

  1. 所有标量类型都是标准布局类

标量类型代表一个单一的值,而非标量类型代表值的集合或一个逻辑上的聚合体。不包含数组、类/结构体、联合体、函数类型(但包含函数指针!)、void

  1. 对于非继承类,不存在虚函数,对于非静态成员都有相同的访问控制属性,并且都是标准布局类型,即为标准布局类。

  2. 对于继承类,保证该类及其父类拥有静态数据成员,且也满足 (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
{} 表示没有额外的成员变量或函数

  1. 实现类模板,继承自false_type,内部成员value默认为false
  2. 实现模板的特化,将int,bool,char等类型特化,继承自true_type,value为true
  3. 因此,通过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 优化—可识别带修饰符的类型

在这里插入图片描述

  1. remove_cv< T>::type 中保存 T去除const 和volatile修饰符后的类型
  2. my_is_integral_helper封装去除修饰符后得到的value结果

12.5 if constexpr — C++17及以后可用

  1. 使用if constexpr,则当编译时会先判断条件是否满足,若满足,该段代码才会被编译。反之,则不会编译。
  2. 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;
}

打开:
在这里插入图片描述

vs2017配置openMP

(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;
}

注:线程总数是存在不同的优先级设置而定,优先级从高到低:

  1. 在 parallel 指令中直接指定
    #pragma omp parallel num_threads(8)
  2. 在main函数中设置后续默认线程数,除非遇到num_threads
    omp_set_num_threads(4);
  3. 环境变量
    在这里插入图片描述
  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 指令做了两件事:

  1. parallel:创建一组线程。
  2. 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接收一个策略实例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值