一、列表初始化
列表初始化就是想数组自定义类型用大括号初始化对象的初始化方式
int arr[] = { 1,2,3,4,5,6,7 };
Date d1 = { 2023,2,3 };
C++11增减了一个initializer_list的类,stl容器都支持列表初始化,C++11会自动识别 {值1,值2,值3…} 为initializer_list 类型,容器内部都增加了initializer_list构造函数,容器列表初始化会调用这个构造函数初始化对象。
//vector容器的initializer_list<T>构造函数
vector(const initializer_list<T>& il)
{
for (auto e : il)
{
push_back(e);
}
}
C98是支持{}构造或者拷贝构造的,所以可以用类表初始化内部嵌套多个构造初始化合并使用;另外C++11的初始化时可以省略对象后面的=。
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
public:
int _year;
int _month;
int _day;
};
//可以省略等号
vector<Date> v2{{2023, 5,26}, { 2023,5,25 }, { 2023,5,24 }};
vector<Date>::iterator it = v2.begin();
cout << (*it)._day << endl;
申明类型的关键字decltype(variable name),可以推测初一个value的类型,并用推测的类型,声明一个变量。
decltype(it) it2;
cout << typeid(it2).name() << endl;
二、左值引用与右值引用
1.左值引用
可以取地址的值就是左值,对左值的引用就是左值引用,左值引用加上const可以引用右值,左值可以被改变,右值有const属性不能be改变,左值可以在左边也可以在右边。
//左值
int a1 = 10;
int* p = &a1;
//左值引用
const int& a2 = 10;
int& aa = a1;
int*& pp = p;
2.右值引用
不能取地址的就是右值,对右值的引用就是右值引用,move()以后的左值才可以被右值引用,move是一个函数将一个形参的值返回,返回值就变成了右值;右值引用具有左值属性,右值可以被编译器是被为右值,而右值引用会被编译器识别为左值,forward()可以保持值的属性;算术表达式的返回值,函数的返回值,常数值都是右值;右值分为纯右值和将亡值,将亡值就马上会被回收的值,比如函数的返回值。
//10是右值,n1是右值引用
int&& n1 = 10;
//1+2的返回值是右值
int&& n2 = 1 + 2;
//函数返回值是右值
int&& ret = add(1, 2);
//右值引用左值
int&& n3 = move(a1);
3.移动构造
C++编译器会自动识别右值引用和左值引用
C++11的stl容器都增加了移动构造的函数,可以节省拷贝构造的成本;容器深拷贝是将形参的空间拷贝一份给一个临时变量,然后交换给被构造的对象,这样就多了一次拷贝构造。传参为右值说明这个值是马上被释放的值,将这个右值的空间直接交换给被拷贝构造的对象空间,可以减少一次拷贝构造。这样的构造叫做移动构造。
容器中成员函数返回类的对象,C98需要先拷贝构造一个临时对象,用这个临时对象来构造另一个对象,编译器会将这两个临时对象优化为成一次拷贝构造。C++11会将返回值看做是将亡值,直接调用移动构造将这个返回值的空间交换给被构造的对象。
有的时候一个值可能会被嵌套传递,但是右值引用第二次传递就会被识别成左值,这符合右值引用的使用逻辑,因为移动构造是要修改右值引用的值,所以右值引用具有左值的属性。比如在自定义list中push_back()中复用insert()insert()中有new一个新的node(data)节点的操作,需要调用node类型的构造将data赋值给node中的_node ,如果node是string或者其他的容器类型,_data的赋值又需要调用string或者其他自定义类型的拷贝构造,在这些环节的传递过程中都需要用forward (value)保持value的属性.
4.移动赋值
移动赋值同拷贝赋值一样,在调用时会被编译器识别为左值还是右值,匹配到右值则调用右值引用,直接交换左值和右值。
以string类为例
string.h
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
#include"string.h"
using namespace std;
int main()
{
kk::string s1;
s1 = kk::to_string(12345);
return 0;
}
5.右值引用主要解决的问题
右值引用与左值引用没有什么区别,都是对值的引用,右值引用并不能解决局部变量出了作用域会被销毁的问题。右值引用主要解决的是stl容器内部接口中,对象的传值返回的深拷贝问题,以及所有将亡值的深拷贝问题,所以移动拷贝和移动赋值只是对深拷贝的类有作用,对于浅拷贝没有意义;
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<list>
#include<string>
#include<assert.h>
using namespace std;
namespace kk
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 拷贝赋值" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// s1 = 将亡值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
//cout << "~string()" << endl;
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string operator+(char ch)
{
string tmp(*this);
tmp += ch;
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
kk::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
kk::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
6. 引用与临时变量
引用可以延长临时变量的声明周期,但是不能改变局部变量出了作用域就被释放的原则;
如果改变to_string的传值返回为传引用返回,用const string& 类型接收返回值会造成变量是随机值的情况
#include"string.h"
using namespace std;
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
/*Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}*/
/*Person& operator=(const Person& p)
{
if(this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}*/
/*~Person()
{}*/
private:
kk::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
在没有达到默认生成移动构造和移动赋值的条件时,也可以强制生成,用default;也可以禁止生成用哪个delete;
#include"string.h"
using namespace std;
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(Person&& p) = default;
Person& operator=(Person&& p) = default;
//Person& operator=(Person&& p) = delete;
Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}
Person& operator=(const Person& p)
{
if(this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}
~Person()
{}
private:
kk::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
三、可变参数模板
在C语言中就有可变参数的概念,表示同类型的任意多个参数,比如常见的printf;
C++11衍生出了可变参数模板,表示可以传参任意多个不同类型的参数。
//Args是模板参数包,args是函数形参参数包
//Args... args是声明一个参数包,这个参数中可以包含任意多个参数
template <class ...Args>
void showList(Args... args);
参数包的遍历方式
方式1:
通过递归推演的方式,编译器编译的时候会推演模板参数类型,匹配对应的参数,根据这一点,用递归调用自己的方式,每次调用就用一个模板参数接收第一个参数,参数包就被分离出去一个参数,后续一次分离,知道参数包为0则调用showList的无参重载函数退出。
void showList()
{
cout << "end"<<endl;
}
template <class T,class ...Args>
void showList(const T& val, Args... args)
{
//打印函数名,和参数包参数个数
cout << __FUNCTION__ << "(" << sizeof...(args) << "): ";
cout << val << endl;
showList(args...);
}
int main()
{
showList(1, "hello ", std::string("world"));
return 0;
}
方式2
arr在列表初始化阶段,编译器会将列表中的函数推演生成实例化函数。
template <class T>
int printArgs(const T& val)
{
cout << typeid(val).name()<<": "<<val << endl;
return 0;
}
template <class ...Args>
//这里的args是单个参数,不是参数包
void showList(Args... args)
{
int arr[] = { printArgs(args)... };
cout << endl;
}
//编译器会推演生成一下代码
//void showList(int a1, const char* s2, string s3);
//{
// int arr[] = { printArgs(a1),printArgs(s2), printArgs(s3)};
// cout << endl;
//}
int main()
{
showList(1, "hello ", std::string("world"));
return 0;
}
在stl容器中的插入接口有一套emplace_back接口,是可变参数模板的插入接口,emplace_back的形参是可变参数包,它用的引用属于万能引用,如果实参是左值就是左引用,如果实参是右值就是右引用。
emplace_back与push_back的最大区别:实参如果是需要隐式类型转换的参数,push_back会进行隐式类型转换,而emplace_back不会,因为它是参数包,参数包在传参的时候不会解析,只会一路传参到最后直接给data赋值;所以emplace_back比push_back少一次移动拷贝。除此之外与push_back没有本质的区别。
#include"string.h"
using namespace std;
int main()
{
list<kk::string> list1;
list1.push_back("hello world");
cout << endl;
list1.emplace_back("hello world");
return 0;
}
四、lambda表达式
1.表达式规则
格式:[捕捉列表](参数列表)->返回值类型{函数体}
捕捉列表可以捕捉表达式以外的所有变量,不仅仅局限于上层定义域.可分为传值捕捉[x,y],全部传值捕捉[=],引用捕捉[&x,&y],全部引用捕捉[&],混合捕捉[&x,y],[&,x],[=,&y] 几种捕捉方式;在传值捕捉的时候默认是捕捉的列表的参数是拷贝的值具有const属性,是不可改变的,如果需要改变,需要在返回值前加mutable(可变的);捕捉列表和函数体不可省略.
参数列表与函数参数列表相同,如果不需要传参可以省略();->返回值,使用时要加->,返回值确定时可连同->一块儿省略;
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main()
{
vector<int> v= { 3,4,9,1,2,23,12,11 };
sort(v.begin(), v.end(), [](int x, int y)->int {return x > y;});
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
return 0;
}
2.表达式应用
Windows的线程和linux的线程库功能相似,Windows将线程封装成了对象,同样需要传参函数例程给thread对象,这个时候用lambda表达式传参一些短小的例程函数比较方便,可读性跟高.
#include<iostream>
#include<thread>
#include<vector>
using namespace std;
void func(int n,int i)
{
while (i--)
{
cout << n<< ": " << i << endl;
}
}
int main()
{
/*thread t1(func, 1, 10);*/
thread t1([](int n,int i)->void{
while (i--)
{
cout << n << ": " << i << endl;
}
},1,10);
t1.join();
return 0;
}
3.表达式底层原理
lambda表达式其实是一个匿名对象,可以当做仿函数使用,lambda表达式即使完全相同,也不能相互赋值操作,因为它们的类型是不同的.在底层汇编函数阶段,编译器给每个lambda表达式都分配了不同的类型名,每个类型名都是lambda+uuid生成了系统中同名概率极低的编码.UUID全称:Universally Unique Identifier,即通用唯一识别码。 UUID是由一组32位数的16进制数字所构成
在底层lambda就是一个仿函数,所以它的大小只有1;
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
struct greater1
{
bool operator()(int x, int y)
{
return x > y;
}
};
int main()
{
greater1()(1, 2);
auto greater2 = [](int x, int y) {return x > y; };
greater2(1, 2);
return 0;
}
五、线程库
1.thread类
接口介绍
#include<iostream>
#include<thread>
#include<chrono>
int main()
{
//默认构造
std::thread t1;
//赋值重载
t1 = std::thread([](int x, int y) {
std::cout << x + y << " " ;
}, 3, 4);
//带参构造
std::thread t2([](int x, int y) {
std::cout << x + y << " " ;
}, 4, 4);
//获取线程id
std::cout << t1.get_id() << " " << t2.get_id() << std::endl;
//线程去连接,线程可自动释放资源
t1.detach();
t2.detach();
//判断线程是否可连接,
//3种情况不可被连接1.线程已被连接或者去连接2.线程是默认构造3.已经被移动
if (t1.joinable())
{
//线程连接
t1.join();
}
if (t2.joinable())
{
//线程连接
t2.join();
}
while (1)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return 0;
}
2.mutex类
接口介绍
#include<iostream>
#include<thread>
#include<chrono>
#include<mutex>
int x = 10000000;
std::mutex mtx;
int main()
{
std::thread t1([&] {
while (true)
{
//加锁
mtx.lock();
if (x > 0)
{
--x;
mtx.unlock();
}
else
{
mtx.unlock();
break;
}
}
});
std::thread t2([&] {
while (true)
{
//加锁
mtx.lock();
//尝试加锁,如果没有加锁成功依然向后执行
/*mtx.try_lock();*/
if (x > 0)
{
--x;
mtx.unlock();
}
else
{
mtx.unlock();
break;
}
//解锁
}
});
t1.join();
t2.join();
std::cout << x << std::endl;
return 0;
}
lock_guard类
当临界区抛异常跳出临界区时会造成死锁,其他线程在访问临界资源会一直被阻塞等待.这个时候需要用到lock_guard类,这个类封装了mutex对象,构造时加锁,析构时解锁,也就是出了作用域就会销毁;封装的mutex对象是引用成员,因为要确保线程使用的是同一把锁,而且mutex不支持拷贝构造,所以要用引用传参构造引用成员mutex对象.
#include<iostream>
#include<thread>
#include<chrono>
#include<mutex>
int x = 0;
std::mutex mtx;
void func(int n)
{
while (n--)
{
std::lock_guard<std::mutex> lk(mtx);
++x;
}
}
int main()
{
std::thread t1;
std::thread t2;
int n = 10000;
t1 = std::thread(func,n);
t2 = std::thread(func,n);
t1.join();
t2.join();
std::cout << x << std::endl;
return 0;
}
3. recursive_mutex类
当使用递归时,函数递归的时机必须在mutex加锁之后执行,不然会造成死锁问题.但是这样做也同样限制了递归逻辑设置.recursive_mutex类可以解决这个问题.
#include<iostream>
#include<thread>
#include<chrono>
#include<mutex>
int x = 0;
std::recursive_mutex mtx;
void func(int n)
{
if (n==0)
{
return;
}
mtx.lock();
++x;
func(n-1);
mtx.unlock();
}
int main()
{
std::thread t1;
int n = 100;
t1 = std::thread(func,n);
t1.join();
std::cout << x << std::endl;
return 0;
}
4.atomic类
不用锁atomic同样可以实现对公共变量的原子操作,其对象可以是实现多种原则逻辑+/-运算或者位运算.
#include<iostream>
#include<thread>
#include<atomic>
using namespace std;
int main()
{
int n = 1000;
atomic<int> x = 0;
//atomic<int> x = {0};
//atomic<int> x{0};
thread t1([&, n]() {
for (int i = 0; i < n; i++)
{
++x;
}
});
thread t2([&, n]() {
for (int i = 0; i < n; i++)
{
++x;
}
});
t1.join();
t2.join();
cout << x << endl;
printf("%d\n", x.load());
return 0;
}
5.condition_variable类
#include<iostream>
#include<thread>
#include<chrono>
#include<mutex>
using namespace std;
int main()
{
mutex mtx;
condition_variable cv;
int n = 100;
int x = 2;
thread t1([&, n]() {
while (1)
{
unique_lock<mutex> lock(mtx);
if (x >= 100)
break;
//if (x % 2 == 0) // 偶数就阻塞
//{
// cv.wait(lock);
//}
cv.wait(lock, [&x]() {return x % 2 != 0; });
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
thread t2([&, n]() {
while (1)
{
unique_lock<mutex> lock(mtx);
if (x > 100)
break;
//返回值不是true就不阻塞,为假就阻塞
cv.wait(lock, [&x](){return x % 2 == 0; });
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}
六、包装器
包装器functional本质上是一个类模板,它可以对仿函数,函数指针,lambda类型进行了封装,相当于是这三种类型的适配器.可以解决一个模板类型使用不同的类型
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
1.给function对象赋值的时候要确保,function的类型的返回值和参数类型是和函数指针或者lambda对象,或者仿函数对象的返回值和参数类型是一致的.
2.赋值对象是类的静态成员函数时,赋值的时候要加访问限定符,但是前面加不加&符号都行,function类型中的参数类型根据静态成员函数的显示参数类型设置即可.
3.赋值对象是类的成员函数时,赋值的时候同样要加访问限定符,并且要加&符号.function的类型中的参数类型要另外加上对象的类型,传参的是要将对象传递过去,当然也可以是对象的指针类型,传参使用的时候传递对象指针也可以,但是如果是对象传参的时候可以使用匿名对象从传参,如果是对象指针类型就不能使用匿名对象了.
#include<iostream>
#include<map>
#include<functional>
using namespace std;
//函数指针
int f(int a, int b)
{
cout << "int f(int a, int b) ";
return a + b;
}
//仿函数
struct Functor
{
public:
int operator() (int a,int b)
{
cout << "int operator() (int a, int b) ";
return a + b;
}
};
class Plus
{
public:
Plus(int rate = 2):_rate(rate){}
static int plusi(int a, int b){return a + b;}
double plusd(double a, double b){ return (a + b) * _rate; }
private:
int _rate = 2;
};
int main()
{
/*函数指针或者仿函数和lambda的返回值和参数类型与function要一致
function<void()> f2 = Functor();*/
function<int(int, int)> f11 = f;
function<int(int, int)> f22 = Functor();
//lambda
function<int(int, int)> f33 = [](int a, int b)
{
cout << "[](int a, int b) {return a + b;} ";
return a + b;
};
cout << f11(1, 2) << endl;
cout << f22(3, 4) << endl;
cout << f33(5, 6) << endl;
map<string, function<int(int, int)>> opFuncMap;
opFuncMap["函数指针"] = f;
opFuncMap["仿函数"] = Functor();
opFuncMap["lambda"] = [](int a, int b) {
cout << "[](int a, int b) {return a + b;} ";
return a + b;
};
cout << opFuncMap["lambda"](1, 2) << endl;
cout << opFuncMap["函数指针"](3, 4) << endl;
cout << opFuncMap["仿函数"](5, 6) << endl;
//赋值对象是静态成员函数
function<int(int, int)> f1 = Plus::plusi;
/*function<int(int, int)> f1 = Plus::plusi;*/
//赋值对象是成员函数
function<double(Plus, double, double)> f2 = &Plus::plusd;
cout << f1(1, 2) << endl;
cout << f2(Plus(), 20, 20) << endl;
//传参方法错误
function<double(Plus*, double, double)> f3 = &Plus::plusd;
cout << f3(&Plus(), 20, 20) << endl;
return 0;
}
波兰表达式应用
class solution ipublic:
int evalRPN( vector<string>& tokens){
stack<int> st;
map<string, function<int(int,int)>> opFuncMap ={
{ "+",[](int a, int b){return a + b;}},
{"-",[](int a, int b){return a - b;}},
{"*",[](int a, int b){return a* b;}},
{"/",[](int a, int b){return a / b;}}
};
for( auto str : tokens){
if(opFuncMap.count(str))
{
int right = st.top();
st.pop();
int left = st.top( );
st.pop();
st.push(opFuncMap[str] ( left,right));
}
else
{
st.push(stoi(str)) ;
}
}
return st.top();
}
};
七、bind
bind是一个函数模板,bind包装函数,接收一个可调用对象,返回在一个新的可以调用对象,通过更改bind的占位符的位置可以调参数顺序,通过绑定一个或者多个参数可以实现更改可调用对象的参数个数(placeholders占位符).
#include<iostream>
#include<functional>
using namespace std;
typedef int(*fun)(int);
int f(int a, int b)
{
cout << "a: "<<a<<" " << "b: " << b << endl;
return 0;
}
int main()
{
//调换参数顺序
auto newcall1 = bind(f, placeholders::_2,placeholders::_1);
newcall1(1, 2);
//更改参数个数
function<int(int)> newcall2 = bind(f, 3, placeholders::_1);
newcall2(2);
return 0;
}