文章目录
计时
clock_t clock(void)
;
简单而言,就是该程序从启动到函数调用占用CPU的时间。这个函数返回从“开启这个程序进程”到“程序中调用clock()函数”时之间的CPU时钟计时单元(clock tick)数,在MSDN中称之为挂钟时间(wal-clock);若挂钟时间不可取,则返回-1。其中clock_t是用来保存时间的数据类型,clock_t实质是头文件中long数据类型类型定义的一个代称。使用计时很简单,只要在程序想计时的地方放入一个clock()函数,另外在结束地方放置一个clock()函数,两个返回值的差即是程序消耗时间。但是该函数计时是以毫秒(ms)为单位的,也就是没过千分之一秒加一,所以需要除以一个常量1000才是以秒计时的,一般使用头文件中声明好的常量CLOCKS_PRE_SEC(即1000)。
#include<iostream>
#include<cstdlib>
#include<ctime>
using namespace std;
clock_t start,end;
int main(){
start=clock(); //程序开始计时
int ans=0;
for(int i=1;i<=1e8;i++)
ans++;
end=clock(); //程序结束用时
double endtime=(double)(end-start)/CLOCKS_PER_SEC;
cout<<"Total time:"<<endtime<<endl; //s为单位
cout<<"Total time:"<<endtime*1000<<"ms"<<endl; //ms为单位
system("pause");
return 0;
}
读取目录下所有的文件
包含头文件#include<dirent.h>
struct dirent *ptr;
DIR *dir;
dir = opendir(road_path_.c_str());
while ((ptr = readdir(dir)) != NULL) {
if (ptr->d_name[0] == '.') continue;
road_filesname_.push_back(ptr->d_name);
}
closedir(dir);
按顺序读取目录内所有的文件
alphasort()
函数:依字母顺序排序目录结构
如果不用过滤器的话,把第三个参数设0,对于返回列表要手动判断是目录还是文件:
#include <iterator>
#include <iostream>
#include <vector>
#include <stdio.h>
#include <dirent.h>
using namespace std;
int fileNameFilter(const struct dirent *cur) {
std::string str(cur->d_name);
if (str.find(".bin") != std::string::npos) {
return 1;
}
return 0;
}
std::vector<std::string> getDirBinsSortedPath(std::string dirPath) {
struct dirent **namelist;
std::vector<std::string> ret;
//alphasort()函数:依字母顺序排序目录结构
int n = scandir(dirPath.c_str(), &namelist, fileNameFilter, alphasort);
if (n < 0) {
return ret;
}
for (int i = 0; i < n; ++i) {
std::string filePath(namelist[i]->d_name);
ret.push_back(filePath);
free(namelist[i]);
};
free(namelist);
return ret;
}
产生NAN数
NaN,是Not a Number的缩写,在IEEE浮点数算术标准(IEEE 754)中定义,表示一些特殊数值(无穷与非数值(NaN)),为许多CPU与浮点运算器所采用。
#include<math.h>
// 产生一个double类型的NAN
typedef std::numeric_limits<double> Info;
double const NAN_d = Info::quiet_NaN();
// 产生一个float类型的NAN
typedef std::numeric_limits<float> Info;
float const NAN_f = Info::quiet_NaN();
一行中输入多个字符串并以空格间格,通过多个string存储
#include<iostream>
#include<string.h>
#include<stdio.h>
using namespace std;
int main(){
string str;
string arr[100];
int index = 0;
while(cin>>str){//string遇到空格会停止
arr[index++] = str;
char ch = getchar();//通过getchar()来判断最后输入回车符结束
if(ch == '\n') break;
}
for(int i=0;i<index;i++)
cout<<arr[i]<<" ";
cout<<endl;
return 0;
}
输入一行以空格分割的数字,存入整型数组
#include<iostream>
using namespace std;
int main()
{
int n;
cin >> n;
int arr[n];
int i = 0;
cin >> arr[i++];
char c;
while((c=getchar()) != '\n'){
cin >> arr[i++];
}
}
保留小数输出
代码是需要头文件#include<iomanip>
cout<<std::fixed<<setprecision(2)<<变量<<endl;
setprecision
函数是指设置输出的精度,例如:
float a=2022.156;
cout<<setprecision(5)<<a;
输出2022.2 (注意:这里四舍五入)
float a=123.666;
cout<<setprecision(2)<<a;
将输出1.2e+002
如果setprecision
里的参数小于整数位数,将以指数形式输出
std::fixed
是指以固定的浮点显示,当std::fixed
和setprecision(n)
两个一起用时就标识保留n为小数输出。这里还需注意,每次输出只设置一次就可以了,因为这两份的作用范围是后续对象,而不是仅对后一个对象起作用。
float a=123.666;
float b=542.332;
cout<< setiosflags(ios::fixed) <<setprecision(2)<<a<<endl;
cout<<setprecision(2)<<b;
输出:
123.67
542.33
截取指定两个字符之间的字符串
例如文件名x-1_y-2.pcd,目的取出x,y对应的值。x = -1 , y = -2
string.substr(start , length) 指从start开始,长度为length
for (int i = 0; i < filesname.size(); i++){
cout << filesname[i] << endl;
//filesname[i] = x-1_y-2.pcd
int Pos_1 = filesname[i].find("x"); // 0
int Pos_2 = filesname[i].find("y"); // 4
int Pos_3 = filesname[i].find("."); // 7
string num1 = filesname[i].substr(Pos_1+1, Pos_2 - Pos_1 - 2);
string num2 = filesname[i].substr(Pos_2+1, Pos_3 - Pos_2 - 1);
cout << "x = " << num1 << "y = " << num2 << endl;
//x = -1 y = -2
}
find用法
1、string类寻找子串
c++中string
类中的find
函数用于寻找字符串中是否包含子串,如果包含,那么函数返回第一个找到的子串的位置,如果不包含,返回-1.eg:
#include<iostream>
#include<string>
using namespace std;
int main()
{
string a="testcodecodecode";
string b="code";
string c="lee";
int a_b=a.find(b);
int a_c=a.find(c);
cout<<a_b<<endl;
cout<<a_c<<endl;
return 0;
}
输出:
4
-1
母字符串.find(子字符串);
返回值的类型为int类型,返回的是字符串的下标。找不到返回-1
#include<iostream>
#include<string>
using namespace std;
int main()
{
string a;
string b;
getline(cin,a);
getline(cin,b);
int post=b.find(a);
cout<<post<<endl;
return 0;
}
st1.find(‘a’,1);后面的数字代表从什么位置开始查找。如果不加,默认从位置0(即第一个字符)开始查找。
如果你要查找的字符不是单个字母,用法和查找单个字母一样,它会返回第一个字符的位置。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string st1("babbabab");
cout << st1.find('a') << endl;//1 由原型知,若省略第2个参数,则默认从位置0(即第1个字符)起开始查找
cout << st1.find('a', 0) << endl;//1
cout << st1.find('a', 1) << endl;//1
cout << st1.find('a', 2) << endl;//4
return 0;
}
2、rfind()
rfind()就是倒着查找
后面的数字代表着就是从倒数第几个开始查找。
在这里说一下,如果计算机没有找到,就会返回npos!!
if(b.find(a)==string::npos)
{
cout<<"no find"<<endl;
}
比如说看这个代码,如果返回值等于npos,就说明在b字符串里面,没有找到a。
3、find_first_of()
在源串中从位置pos起往后查找,只要在源串中遇到一个字符,该字符与目标串中任意一个字符相同,就停止查找,返回该字符在源串中的位置;若匹配失败,返回npos。
//将字符串中所有的元音字母换成*
//代码来自C++ Reference,地址:http://www.cplusplus.com/reference/string/basic_string/find_first_of/
#include<iostream>
#include<string>
using namespace std;
int main()
{
std::string str("PLease, replace the vowels in this sentence by asterisks.");
std::string::size_type found = str.find_first_of("aeiou");
while (found != std::string::npos)
{
str[found] = '*';
found = str.find_first_of("aeiou", found + 1);
}
std::cout << str << '\n';
return 0;
}
//运行结果:
//PL**s* r*pl*c* th* v*w*ls *n th*s s*nt*nc* by *st*r*sks
4、find_last_of()
函数与find_first_of()函数相似,只不过查找顺序是从指定位置向前。
5、find_first_not_of()
在源串中从位置pos开始往后查找,只要在源串遇到一个字符,该字符与目标串中的任意一个字符都不相同,就停止查找,返回该字符在源串中的位置;若遍历完整个源串,都找不到满 足条件的字符,则返回npos。
#include<iostream>
#include<string>
using namespace std;
int main()
{
//测试size_type find_first_not_of (const charT* s, size_type pos = 0) const;
string str("abcdefg");
cout << str.find_first_not_of("kiajbvehfgmlc", 0) << endl;//3 从源串str的位置0(a)开始查找,目标串中有a(匹配),再找b,b匹配,再找c,c匹配,
// 再找d,目标串中没有d(不匹配),停止查找,返回d在str中的位置3
return 0;
}
c++字符串和数字转换
1、字符串转数组
1.1 使用 std::stringstream
#include <iostream>
#include <sstream>
int main(int argc, char *argv[])
{
for(int i = 0; i < 5; i++)
{
std::stringstream ss;
std::string strNum = std::to_string(i);
int num;
ss << strNum;
ss >> num;
std::cout << num * num << " ";
}
std::cout << std::endl;
}
1.2 使用 std 的内置函数
#include <iostream>
#include <sstream>
int main(int argc, char *argv[])
{
for(int i = 0; i < 5; i++)
{
std::stringstream ss;
std::string strNum = std::to_string(i);
int num = std::stoi(strNum);
std::cout << num * num << " ";
}
std::cout << std::endl;
}
2、数字转字符串
2.1 使用 std::stringstream
#include <iostream>
#include <sstream>
int main(int argc, char*argv[])
{
for(int i = 0; i < 5; i++)
{
std::stringstream ss;
std::string str;
ss << i;
ss >> str;
std::cout << str.data() << " ";
}
std::cout << std::endl;
return 0;
}
2.2 使用 std 的内置函数
#include <string>
#include <iostream>
int main(int argc, char*argv[])
{
for(int i = 0; i < 5; i++)
{
std::cout << std::to_string(i).data() << " ";
}
std::cout << std::endl;
return 0;
}
2.3 std 内置的数字转字符串函数列表
std::string to_string(int value);
std::string to_string(long value);
std::string to_string(long long value);
std::string to_string(unsigned value);
std::string to_string(unsigned long value);
std::string to_string(unsigned long long value);
std::string to_string(float value);
std::string to_string(double value);
std::string to_string(long double value);
参考链接:
vector用法
1、删除指定位置元素
iterator erase(iterator it)
:删除向量中迭代器指向元素
iterator erase(iterator first,iterator last)
:删除向量中[first,last)
中元素
#include <iostream>
#include <vector>
using namespace std;
int main()
{
std::vector<int> v = {1, 2, 3, 4, 5, 6};
v.erase(v.begin()+2); //删除第三个位置的元素
for(int i = 0;i<v.size();i++){
cout<<v[i]<<" ";
}
}
得到的结果为:1 2 4 5 6
2、vector中find 的用法
vector
没有自带的find
函数,需要用普通的find函数,使用如下:
vector<string> nameList1;
//给nameList1赋值
string name;
if(find(nameList1.begin(),nameList1.end(),name) == nameList1.end()){
//没有找到
}
else{
//找到了
}
找到一个vector
中有、一个vector
中没有的元素,如何会快速一点。
eg:下面找出nameList2中有的,nameList1中没有的元素:
vector<string> nameList1;
vector<string> nameList2;
vector<string> nameList;
//给nameList1、2赋值 省略
vector<string>::iterator itit1;
vector<string>::iterator itit2;
//for(int i=0;i<nameList2.size();i++){
for(itit2 = nameList2.begin();itit2<nameList2.end();itit2++){
//string name = nameList2.at(i);
string name = *itit2;
itit1 = find(nameList1.begin(),nameList1.end(),name);
if( !(itit1 == nameList1.end())){
//找到了
//nameList2.erase(itit2);注意此处不能erase,不然会导致itit2失效,itit2++时会出错
nameList1.erase(itit1);//每次找出时将nameList1中的去掉,这样可以缩小搜索的范围,提高检索速度,尤其是当数据量很大的情况下。
}else{
//没有找到
nameList.push_back(name);
}
}
3、vector的迭代器遍历
#include <iostream>
#include <vector>
using namespace std;
// vector容器遍历方式2 —— 迭代器遍历
void traverseVector_2(vector<int> v)
{
// 注:如果参数为const vector<int> 需要用const_iterator
vector<int>::iterator it = v.begin();
// vector<int>::const_iterator iter=v.begin();
for(; it != v.end(); ++it)
{
cout<<(*it)<<" ";
}
cout<<endl;
}
map用法
1、map容器大小操作
size()
//返回容器元素大小
empty()
//判读容器是为空
// map容器的构造
map<string, int> m;
// map容器添加元素
m.insert(pair<string, int>("Tom", 18));
m.insert(pair<string, int>("Anthony", 23));
m.insert(pair<string, int>("Bob", 24));
m.insert(pair<string, int>("Sunny", 19));
// 统计容器大小
cout << "size of Map: " << m.size() << endl; //4
// 容器是否为空
cout << "is empty? " << m.empty() << endl;//0
2、map初始化(增)
2.1 直接赋值法
map<string, int> m1;
m1["def"] = 2;
2.2 用insert添加
map<string, int> m2;
m2.insert({ "abc", 1 }); //使用这种就可以了
//其他形式和方式
m2.insert(make_pair(string("def"), 2));
m2.insert(pair<string, int>(string("ghi"), 3));
2.3 列表初始化
注:列表初始化适用于c++11和以上的版本,低于11的版本则无法使用
map<string,int> m3 = {
{"string",1}, {"sec",2}, {"trd",3}
};
map<string,string> m4 = {
{"first","second"}, {"third","fourth"},
{"fifth","sixth"}, {"begin","end"}
};
如果需要插入一个key并且不指定value,可以直接用下面这种写法,看起来不是一个表达式,不过map对“[]”进行了重载,本质上仍然是表达式,其含义是如果存在该变量则直接返回对应value,如果不存在则增加该key值并自动初始化为0
map<string,int> a; //a是string到int的空map
a["new"]; //"new"是新增的key
不过需要注意的是,如果value类型为数型,即使像上面一样没有进行赋值操作,也一般会默认赋值为——0,如果value是string或char类型,则默认为空,没有默认值,因此,string或char可以用列表初始化进行不给value赋值的添加key
string<string,string> m5 = {
{"first",""}, {"second",""}
{"third",""}, {"fourth",""}
};
2.4 多类型嵌套的map赋值
这种多层嵌套的map,也是可以直接进行赋值操作的,如:
map<int, map<string, vector<double>>> testmap;
testmap[3]["three"].push_back(3.75);
3、查
函数原型
iterator find (const key_type& k);
const_iterator find (const key_type& k) const;
函数作用:
在容器中寻找值为k的元素,返回该元素的迭代器。否则,返回map.end()。
std::map<char,int> mymap;
std::map<char,int>::iterator it;
mymap['a']=50;
mymap['b']=100;
mymap['c']=150;
mymap['d']=200;
it = mymap.find('b');
if (it != mymap.end())
mymap.erase (it);
// print content:
std::cout << "elements in mymap:" << '\n';
std::cout << "a => " << mymap.find('a')->second << '\n'; //50
4、遍历
map<int, int> _map;
_map[0] = 1;
_map[1] = 2;
_map[10] = 10;
map<int, int>::iterator iter;
iter = _map.begin();
while(iter != _map.end()) {
cout << iter->first << " : " << iter->second << endl;
iter++;
}
// 也可以使用for循环遍历
for(iter = _map.begin(); iter != _map.end(); iter++) {
cout << iter->first << " : " << iter->second << endl;
}
}
5、删除
map
的成员函数 erase()
可以移除键和参数匹配的元素,然后返回所移除元素的个数,例如:
std::map<std::string, size_t> people {{ "Fred", 45}, {"Joan", 33},{"Jill", 22}};
std::string name{"Joan"};
if(people.erase(name))
std::cout << name << " was removed." << std::endl;
else
std::cout << name << " was not found" << std::endl;
显然,map 容器的返回值只可能是 0 或 1,0 表明元素不在容器中。
也可以用指向删除元素的迭代器作为 erase() 的参数。这种情况下,返回的迭代器指向被删除元素的下一个位置。这个参数必须是容器中的有效迭代器,不能是结束迭代器。如果迭代器参数指向的是容器的最后一个元素,那么会返回结束迭代器。例如:
auto iter = people.erase(std::begin(people));
if(iter == std::end(people))
std::cout << "The last element was removed."<< std::endl;
else
std::cout << "The element preceding " << iter->first << "was removed." << std::endl;
也有更高级版本的 erase(),它可以移除两个迭代器参数所定义范围内的元素,例如:
auto iter = people.erase(++std::begin(people), --std::end(people));//Erase all except 1st & last
返回的迭代器指向这段元素中最后一个被删除的元素。如果想删除容器中的所有元素,可以调用成员函数 clear()。
6、遍历+删除
i++操作主要做三件事情:
1、首先把i备份一下。
2、把i加上1。
2、返回第一步备份的i。
mapData.erase(i++);
在执行erase之前,i已经被加1了。erase会使得以前那个未被加一的i失效,而加了一之后的新的i是有效的。
int main(int argc, char* argv[])
{
map<string, string> mapData;
mapData["a"] = "aaa";
mapData["b"] = "bbb";
mapData["c"] = "ccc";
for (map<string, string>::iterator i=mapData.begin(); i!=mapData.end(); /*i++*/)
{
if (i->first == "b")
{
mapData.erase(i++);
}
else
{
i++;
}
}
return 0;
}
多线程
1、创建线程
通过std::thread t(f, args…)
创建线程,可以给线程函数传递参数。通过join()
函数关联并阻塞线程,等待该线程执行完毕后继续;通过detach()
函数解除关联使线程可以与主线程并发执行,但若主线程执行完毕退出后,detach()
接触关联的线程即便没有执行完毕,也将自动退出
//thread1.cpp 创建线程,并观察线程的并发执行与阻塞等待
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void thread_function(int n)
{
std::thread::id this_id = std::this_thread::get_id(); //获取线程ID
for(int i = 0; i < 5; i++){
cout << "Child function thread " << this_id<< " running : " << i+1 << endl;
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
}
class Thread_functor
{
public:
// functor行为类似函数,C++中的仿函数是通过在类中重载()运算符实现,使你可以像使用函数一样来创建类的对象
void operator()(int n)
{
std::thread::id this_id = std::this_thread::get_id();
for(int i = 0; i < 5; i++){
cout << "Child functor thread " << this_id << " running: " << i+1 << endl;
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
}
};
int main()
{
thread mythread1(thread_function, 1); // 传递初始函数作为线程的参数
if(mythread1.joinable()) //判断是否可以成功使用join()或者detach(),返回true则可以,返回false则不可以
mythread1.join(); // 使用join()函数阻塞主线程直至子线程执行完毕
Thread_functor thread_functor; //函数对象实例化一个对象
thread mythread2(thread_functor, 3); // 传递初始函数作为线程的参数
if(mythread2.joinable())
mythread2.detach(); // 使用detach()函数让子线程和主线程并行运行,主线程也不再等待子线程
auto thread_lambda = [](int n){ //lambda表达式格式:[capture list] (params list) mutable exception-> return type { function body }
std::thread::id this_id = std::this_thread::get_id();
for(int i = 0; i < 5; i++)
{
cout << "Child lambda thread " << this_id << " running: " << i+1 << endl;
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
};
thread mythread3(thread_lambda, 4); // 传递初始函数作为线程的参数
if(mythread3.joinable())
mythread3.join(); // 使用join()函数阻塞主线程直至子线程执行完毕
std::thread::id this_id = std::this_thread::get_id();
for(int i = 0; i < 5; i++){
cout << "Main thread " << this_id << " running: " << i+1 << endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
getchar();
return 0;
}
线程创建的参数是函数对象,函数对象不止是函数指针或成员函数指针,同时还包括函数对象(仿函数)与lambda
表达式。上面的代码分别用三种函数对象创建了三个线程,其中第一个线程mythread1
阻塞等待其执行完后继续往下执行,第二个线程mythread2
不阻塞等待在后台与后面的第三个线程mythread3
并发执行,第三个线程继续阻塞等待其完成后再继续往下执行主线程任务。
上面的代码就是利用了std::this_thread
提供的函数获得当前线程的ID,让当前线程睡眠一段时间(一般需要< chrono >
头文件提供duration
或timepoint
)的功能,代码执行结果如下图所示:
2、线程同步
2.1 lock与unlock保护共享资源
// mutex1.cpp 通过互斥体lock与unlock保护共享全局变量
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream>
std::chrono::milliseconds interval(100);
std::mutex mutex;
int job_shared = 0; //两个线程都能修改'job_shared',mutex将保护此变量
int job_exclusive = 0; //只有一个线程能修改'job_exclusive',不需要保护
//此线程只能修改 'job_shared'
void job_1()
{
mutex.lock();
std::this_thread::sleep_for(5 * interval); //令‘job_1’持锁等待
++job_shared;
std::cout << "job_1 shared (" << job_shared << ")\n";
mutex.unlock();
}
// 此线程能修改'job_shared'和'job_exclusive'
void job_2()
{
while (true) { //无限循环,直到获得锁并修改'job_shared'
if (mutex.try_lock()) { //尝试获得锁成功则修改'job_shared'
++job_shared;
std::cout << "job_2 shared (" << job_shared << ")\n";
mutex.unlock();
return;
} else { //尝试获得锁失败,接着修改'job_exclusive'
++job_exclusive;
std::cout << "job_2 exclusive (" << job_exclusive << ")\n";
std::this_thread::sleep_for(interval);
}
}
}
int main()
{
std::thread thread_1(job_1);
std::thread thread_2(job_2);
thread_1.join();
thread_2.join();
getchar();
return 0;
}
从上面的代码看,创建了两个线程和两个全局变量,其中一个全局变量job_exclusive
是排他的,两线程并不共享,不会产生数据竞争,所以不需要锁保护。另一个全局变量job_shared
是两线程共享的,会引起数据竞争,因此需要锁保护。线程thread_1
持有互斥锁lock
的时间较长,线程thread_2
为免于空闲等待,使用了尝试锁try_lock
,如果获得互斥锁则操作共享变量job_shared
,未获得互斥锁则操作排他变量job_exclusive
,提高多线程效率。
2.2 lock_guard与unique_lock保护共享资源
但lock与unlock必须成对合理配合使用,使用不当可能会造成资源被永远锁住,甚至出现死锁(两个线程在释放它们自己的lock之前彼此等待对方的lock)。是不是想起了C++另一对儿需要配合使用的对象new与delete,若使用不当可能会造成内存泄漏等严重问题,为此C++引入了智能指针shared_ptr与unique_ptr。智能指针借用了RAII技术(Resource Acquisition Is Initialization—使用类来封装资源的分配和初始化,在构造函数中完成资源的分配和初始化,在析构函数中完成资源的清理,可以保证正确的初始化和资源释放)对普通指针进行封装,达到智能管理动态内存释放的效果。同样的,C++也针对lock与unlock引入了智能锁lock_guard与unique_lock,同样使用了RAII技术对普通锁进行封装,达到智能管理互斥锁资源释放的效果。lock_guard与unique_lock的区别如下:
从上面两个支持的操作函数表对比来看,unique_lock
功能丰富灵活得多。如果需要实现更复杂的锁策略可以用unique_lock
,如果只需要基本的锁功能,优先使用更严格高效的lock_guard
。两种锁的简单概述与策略对比见下表:
|类模板| 描述 |
类模板 | 描述 | 概述 |
---|---|---|
std::lock_guard | 严格基于作用域(scope-based)的锁管理类模板,构造时是否加锁是可选的(不加锁时假定当前线程已经获得锁的所有权—使用std::adopt_lock策略),析构时自动释放锁,所有权不可转移,对象生存期内不允许手动加锁和释放锁 | std::adopt_lock |
std::unique_lock | 更加灵活的锁管理类模板,构造时是否加锁是可选的,在对象析构时如果持有锁会自动释放锁,所有权可以转移。对象生命期内允许手动加锁和释放锁 | std::adopt_lock std::defer_lock std::try_to_lock |
如果将上面的普通锁lock/unlock
替换为智能锁lock_guard
,其中job_1
函数代码修改如下:
void job_1()
{
std::lock_guard<std::mutex> lockg(mutex); //获取RAII智能锁,离开作用域会自动析构解锁
std::this_thread::sleep_for(5 * interval); //令‘job_1’持锁等待
++job_shared;
std::cout << "job_1 shared (" << job_shared << ")\n";
}
如果也想将job_2
的尝试锁try_lock
也使用智能锁替代,由于lock_guard
锁策略不支持尝试锁,只好使用unique_lock
来替代,代码修改如下(其余代码和程序执行结果与上面相同):
void job_2()
{
while (true) { //无限循环,直到获得锁并修改'job_shared'
std::unique_lock<std::mutex> ulock(mutex, std::try_to_lock); //以尝试锁策略创建智能锁
//尝试获得锁成功则修改'job_shared'
if (ulock) {
++job_shared;
std::cout << "job_2 shared (" << job_shared << ")\n";
return;
} else { //尝试获得锁失败,接着修改'job_exclusive'
++job_exclusive;
std::cout << "job_2 exclusive (" << job_exclusive << ")\n";
std::this_thread::sleep_for(interval);
}
}
}
2.3 timed_mutex与recursive_mutex提供更强大的锁
前面介绍的互斥量mutex提供了普通锁lock/unlock和智能锁lock_guard/unique_lock,基本能满足我们大多数对共享数据资源的保护需求。但在某些特殊情况下,我们需要更复杂的功能,比如某个线程中函数的嵌套调用可能带来对某共享资源的嵌套锁定需求,mutex在一个线程中却只能锁定一次;再比如我们想获得一个锁,但不想一直阻塞,只想等待特定长度的时间,mutex也没提供可设定时间的锁。针对这些特殊需求,< mutex >库也提供了下面几种功能更丰富的互斥类,它们间的区别见下表:
类模板 | 描述 |
---|---|
std::mutex | 同一时间只可被一个线程锁定。如果它被锁住,任何其他lock()都会阻塞(block),直到这个mutex再次可用,且try_lock()会失败。 |
std::recursive_mutex | 允许在同一时间多次被同一线程获得其lock。其典型应用是:函数捕获一个lock并调用另一函数而后者再次捕获相同的lock。 |
std::timed_mutex | 额外允许你传递一个时间段或时间点,用来定义多长时间内它可以尝试捕获一个lock。为此它提供了try_lock_for(duration)和try_lock_until(timepoint)。 |
std::recursive_timed_mutex | 允许同一线程多次取得其lock,且可指定期限。 |
不同互斥类所支持的互斥锁类型总结如下表:
继续用前面的例子,将mutex
替换为timed_mutex
,将job_2的尝试锁tyr_lock()
替换为带时间的尝试锁try_lock_for(duration)
。由于改变了尝试锁的时间,所以在真正获得锁之前的尝试次数也有变化,该变化体现在尝试锁失败后对排他变量job_exclusive
的最终修改结果或修改次数上。更新后的代码如下所示:
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream>
std::chrono::milliseconds interval(100);
std::timed_mutex tmutex;
int job_shared = 0; //两个线程都能修改'job_shared',mutex将保护此变量
int job_exclusive = 0; //只有一个线程能修改'job_exclusive',不需要保护
//此线程只能修改 'job_shared'
void job_1()
{
std::lock_guard<std::timed_mutex> lockg(tmutex); //获取RAII智能锁,离开作用域会自动析构解锁
std::this_thread::sleep_for(5 * interval); //令‘job_1’持锁等待
++job_shared;
std::cout << "job_1 shared (" << job_shared << ")\n";
}
// 此线程能修改'job_shared'和'job_exclusive'
void job_2()
{
while (true) { //无限循环,直到获得锁并修改'job_shared'
std::unique_lock<std::timed_mutex> ulock(tmutex,std::defer_lock); //创建一个智能锁但先不锁定
//尝试获得锁成功则修改'job_shared'
if (ulock.try_lock_for(3 * interval)) { //在3个interval时间段内尝试获得锁
++job_shared;
std::cout << "job_2 shared (" << job_shared << ")\n";
return;
} else { //尝试获得锁失败,接着修改'job_exclusive'
++job_exclusive;
std::cout << "job_2 exclusive (" << job_exclusive << ")\n";
std::this_thread::sleep_for(interval);
}
}
}
int main()
{
std::thread thread_1(job_1);
std::thread thread_2(job_2);
thread_1.join();
thread_2.join();
getchar();
return 0;
}
thread1.cpp程序运行结果可能会出现某行与其他行交叠错乱的情况,主要是由于不止一个线程并发访问了std::cout显示终端资源导致的,解决方案就是对cout << “somethings” << endl语句加锁,保证多个线程对cout资源的访问同步。为了尽可能降低互斥锁对性能的影响,应使用微粒锁,即只对cout资源访问语句进行加锁保护,cout资源访问完毕尽快解锁以供其他线程访问该资源。添加互斥锁保护后的代码如下:
//thread2.cpp 增加对cout显示终端资源并发访问的互斥锁保护
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std;
std::mutex mutex1;
void thread_function(int n)
{
std::thread::id this_id = std::this_thread::get_id(); //获取线程ID
for(int i = 0; i < 5; i++){
mutex1.lock();
cout << "Child function thread " << this_id<< " running : " << i+1 << endl;
mutex1.unlock();
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
}
class Thread_functor
{
public:
// functor行为类似函数,C++中的仿函数是通过在类中重载()运算符实现,使你可以像使用函数一样来创建类的对象
void operator()(int n)
{
std::thread::id this_id = std::this_thread::get_id();
for(int i = 0; i < 5; i++){
{
std::lock_guard<std::mutex> lockg(mutex1);
cout << "Child functor thread " << this_id << " running: " << i+1 << endl;
}
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
}
};
int main()
{
thread mythread1(thread_function, 1); // 传递初始函数作为线程的参数
if(mythread1.joinable()) //判断是否可以成功使用join()或者detach(),返回true则可以,返回false则不可以
mythread1.join(); // 使用join()函数阻塞主线程直至子线程执行完毕
Thread_functor thread_functor;
thread mythread2(thread_functor, 3); // 传递初始函数作为线程的参数
if(mythread2.joinable())
mythread2.detach(); // 使用detach()函数让子线程和主线程并行运行,主线程也不再等待子线程
auto thread_lambda = [](int n){
std::thread::id this_id = std::this_thread::get_id();
for(int i = 0; i < 5; i++)
{
mutex1.lock();
cout << "Child lambda thread " << this_id << " running: " << i+1 << endl;
mutex1.unlock();
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
};
thread mythread3(thread_lambda, 4); // 传递初始函数作为线程的参数
if(mythread3.joinable())
mythread3.join(); // 使用join()函数阻塞主线程直至子线程执行完毕
unsigned int n = std::thread::hardware_concurrency(); //获取可用的硬件并发核心数
mutex1.lock();
std::cout << n << " concurrent threads are supported." << endl;
mutex1.unlock();
std::thread::id this_id = std::this_thread::get_id();
for(int i = 0; i < 5; i++){
{
std::lock_guard<std::mutex> lockg(mutex1);
cout << "Main thread " << this_id << " running: " << i+1 << endl;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
getchar();
return 0;
}
3、线程同步之条件变量
3.1 何为条件变量
条件变量是线程的另外一种有效同步机制。这些同步对象为线程提供了交互的场所(一个线程给另外的一个或者多个线程发送消息),我们指定在条件变量这个地方发生,一个线程用于修改这个变量使其满足其它线程继续往下执行的条件,其它线程则等待接收条件已经发生改变的信号。当条件变量同互斥锁一起使用时,条件变量允许线程以一种无竞争的方式等待任意条件的发生。
3.2 为何引入条件变量
前一章介绍了多线程并发访问共享数据时遇到的数据竞争问题,我们通过互斥锁保护共享数据,保证多线程对共享数据的访问同步有序。但如果一个线程需要等待一个互斥锁的释放,该线程通常需要轮询该互斥锁是否已被释放,我们也很难找到适当的轮训周期,如果轮询周期太短则太浪费CPU资源,如果轮询周期太长则可能互斥锁已被释放而该线程还在睡眠导致发生延误。
下面给出一个简单的程序示例:一个线程往队列中放入数据,一个线程从队列中提取数据,取数据前需要判断一下队列中确实有数据,由于这个队列是线程间共享的,所以,需要使用互斥锁进行保护,一个线程在往队列添加数据的时候,另一个线程不能取,反之亦然。程序实现代码如下:
//cond_var1.cpp用互斥锁实现一个生产者消费者模型
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
std::deque<int> q; //双端队列标准容器全局变量
std::mutex mu; //互斥锁全局变量
//生产者,往队列放入数据
void function_1() {
int count = 10;
while (count > 0) {
std::unique_lock<std::mutex> locker(mu);
q.push_front(count); //数据入队锁保护
locker.unlock();
std::this_thread::sleep_for(std::chrono::seconds(1)); //延时1秒
count--;
}
}
//消费者,从队列提取数据
void function_2() {
int data = 0;
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu);
if (!q.empty()) { //判断队列是否为空
data = q.back();
q.pop_back(); //数据出队锁保护
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
} else {
locker.unlock();
}
}
}
int main() {
std::thread t1(function_1);
std::thread t2(function_2);
t1.join();
t2.join();
getchar();
return 0;
}
程序执行结果如下:
从代码中不难看出:在生产过程中,因每放入一个数据有1秒延时,所以这个生产的过程是很慢的;在消费过程中,存在着一个while循环,只有在接收到表示结束的数据的时候,才会停止,每次循环内部,都是先加锁,判断队列不空,然后就取出一个数,最后解锁。所以说,在1s内,做了很多无用功!这样的话,CPU占用率会很高,可能达到100%(单核)。如下图示:
既然是由于消费者在while循环内因等待数据做了过多的无用功导致CPU占有率过高,我们可以考虑在消费者发现队列为空时,让消费者小睡一会儿,即增加一个小延时(比如500ms),相当于增大了轮询间隔周期,应该能降低CPU的占用率。按该方案修改后的消费者代码如下:
//消费者,从队列提取数据
void function_2() {
int data = 0;
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu);
if (!q.empty()) { //判断队列是否为空
data = q.back();
q.pop_back(); //数据出队锁保护
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
} else {
locker.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(500)); //延时500毫秒
}
}
}
增大轮询周期后,CPU占有率下降很明显:
但前面也说了,困难之处在于如何确定这个延长时间(即轮询间隔周期),如果间隔太短会过多占用CPU资源,如果间隔太长会因无法及时响应造成延误。
这就引入了条件变量来解决该问题:条件变量使用“通知—唤醒”模型,生产者生产出一个数据后通知消费者使用,消费者在未接到通知前处于休眠状态节约CPU资源;当消费者收到通知后,赶紧从休眠状态被唤醒来处理数据,使用了事件驱动模型,在保证不误事儿的情况下尽可能减少无用功降低对资源的消耗。
3.3 如何使用条件变量
C++标准库在< condition_variable >中提供了条件变量,借由它,一个线程可以唤醒一个或多个其他等待中的线程。原则上,条件变量的运作如下:
- 你必须同时包含
<mutex>
和<condition_variable>
并声明一个mutex
和一个condition_variable
变量; - 那个通知“条件已满足”的线程(或多个线程之一)必须调用
notify_one()
或notify_all()
,以便条件满足时唤醒处于等待中的一个条件变量; - 那个等待"条件被满足"的线程必须调用
wait()
,可以让线程在条件未被满足时陷入休眠状态,当接收到通知时被唤醒去处理相应的任务;
将上面的cond_var1.cpp程序使用条件变量解决轮询间隔难题的示例代码如下:
//cond_var2.cpp用条件变量解决轮询间隔难题
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
std::deque<int> q; //双端队列标准容器全局变量
std::mutex mu; //互斥锁全局变量
std::condition_variable cond; //全局条件变量
//生产者,往队列放入数据
void function_1() {
int count = 10;
while (count > 0) {
std::unique_lock<std::mutex> locker(mu);
q.push_front(count); //数据入队锁保护
locker.unlock();
cond.notify_one(); // 向一个等待线程发出“条件已满足”的通知
std::this_thread::sleep_for(std::chrono::seconds(1)); //延时1秒
count--;
}
}
//消费者,从队列提取数据
void function_2() {
int data = 0;
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu);
while(q.empty()) //判断队列是否为空
cond.wait(locker); // 解锁互斥量并陷入休眠以等待通知被唤醒,被唤醒后加锁以保护共享数据
data = q.back();
q.pop_back(); //数据出队锁保护
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
}
}
int main() {
std::thread t1(function_1);
std::thread t2(function_2);
t1.join();
t2.join();
getchar();
return 0;
}
使用条件变量对CPU的占用率也很低,而且免去了轮询间隔该设多长的难题:
上面的代码有三个注意事项:
- 在function_2中,在判断队列是否为空的时候,使用的是while(q.empty()),而不是if(q.empty()),这是因为wait()从阻塞到返回,不一定就是由于notify_one()函数造成的,还有可能由于系统的不确定原因唤醒(可能和条件变量的实现机制有关),这个的时机和频率都是不确定的,被称作伪唤醒。如果在错误的时候被唤醒了,执行后面的语句就会错误,所以需要再次判断队列是否为空,如果还是为空,就继续wait()阻塞;
- 在管理互斥锁的时候,使用的是std::unique_lock而不是std::lock_guard,而且事实上也不能使用std::lock_guard。这需要先解释下wait()函数所做的事情,可以看到,在wait()函数之前,使用互斥锁保护了,如果wait的时候什么都没做,岂不是一直持有互斥锁?那生产者也会一直卡住,不能够将数据放入队列中了。所以,wait()函数会先调用互斥锁的unlock()函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作。lock_guard没有lock和unlock接口,而unique_lock提供了,这就是必须使用unique_lock的原因;
- 使用细粒度锁,尽量减小锁的范围,在notify_one()的时候,不需要处于互斥锁的保护范围内,所以在唤醒条件变量之前可以将锁unlock()。
还可以将cond.wait(locker)
换一种写法,wait()
的第二个参数可以传入一个函数表示检查条件,这里使用lambda
函数最为简单,如果这个函数返回的是true
,wait()
函数不会阻塞会直接返回,如果这个函数返回的是false
,wait()
函数就会阻塞着等待唤醒,如果被伪唤醒,会继续判断函数返回值。代码示例如下:
//消费者,从队列提取数据
void function_2() {
int data = 0;
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu);
cond.wait(locker, [](){ return !q.empty();}); //如果条件变量被唤醒,检查队列非空条件是否为真,为真则直接返回,为假则继续等待
data = q.back();
q.pop_back(); //数据出队锁保护
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
}
}
下面给出条件变量支持的操作函数表:
值得注意的是:
- 所有通知
(notification)
都会被自动同步化,所以并发调用notify_one()
和notify_all()
不会带来麻烦; - 所有等待某个条件变量
(condition variable)
的线程都必须使用相同的mutex
,当wait()
家族的某个成员被调用时该mutex
必须被unique_lock
锁定,否则会发生不明确的行为; wait()
函数会执行“解锁互斥量–>陷入休眠等待–>被通知唤醒–>再次锁定互斥量–>检查条件判断式是否为真”几个步骤,这意味着传给wait
函数的判断式总是在锁定情况下被调用的,可以安全的处理受互斥量保护的对象;但在"解锁互斥量–>陷入休眠等待"过程之间产生的通知(notification)
会被遗失。
线程同步保证了多个线程对共享数据的有序访问,目前我们了解到的多线程间传递数据主要是通过共享数据(全局变量)实现的,全局共享变量的使用容易增加不同任务或线程间的耦合度,也增加了引入bug的风险,所以全局共享变量应尽可能少用。很多时候我们只需要传递某个线程或任务的执行结果,以便参与后续的运算,但我们又不想阻塞等待该线程或任务执行完毕,而是继续执行暂时不需要该线程或任务执行结果参与的运算,当需要该线程执行结果时直接获得,才能更充分发挥多线程并发的效率优势。
4、类成员函数内使用多线程
class PointcloudPose{
private:
void Process(std::queue<time_pointcloud>& clouds,std::vector<time_pose>&poses);
Eigen::Matrix4d ClosestFramePose(std::vector<time_pose> &poses, int n, double target_time);
Eigen::Matrix4d LinearInterpolation(double t1, double t2, double t, Eigen::Matrix4d pose1, Eigen::Matrix4d pose2);
public:
static std::queue<time_pointcloud>kClouds;
static std::vector<time_pose>kPoses;
void Init(std::unique_ptr<apollo::cyber::Node> &listener_node);
void NewThread();
};
线程将会在当前类的实例上调用 Process
函数,而不是直接调用。
注意:如果你把参数传给 std::thread
时,需要使用std::ref()
来绑定变量的引用
void PointcloudPose::Process(std::queue<time_pointcloud>& clouds,std::vector<time_pose>&poses){
Eigen::Matrix4d pose;
pcl::PointCloud<pcl::PointXYZI> cloud;
while(1){
std::this_thread::sleep_for(std::chrono::seconds(1));
if(!kClouds.empty()){
std::unique_lock<std::mutex> lock(mutex_pointcloud);
double timestamp = kClouds.front().timestamp;
cloud = kClouds.front().cloud;
kClouds.pop();
lock.unlock();
pose = ClosestFramePose(kPoses, kPoses.size(), timestamp);
}
}
}
void PointcloudPose::NewThread(){
std::thread t(&PointcloudPose::Process, this, std::ref(kClouds), std::ref(kPoses));
if(t.joinable()){
t.detach();
}
}
5、原子操作std::atomic
std::atomic<bool>
是 C++11 标准库中提供的一个模板类,用于实现原子类型的布尔变量操作。它可以保证在多线程环境下,对该变量的访问和修改操作是原子的,即不会出现数据竞争的情况,确保多线程并发访问时的数据安全。
std::atomic<bool>
有很多种操作,如 load()
、store()
、exchange()
、compare_exchange_strong()
等,可以用来读取、存储、交换、比较并交换等操作。下面是一些示例用法:
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<bool> flag(false);
void set_flag() {
flag.store(true);
}
void clear_flag() {
flag.store(false);
}
void test_flag() {
while (!flag.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
std::cout << "Flag is set." << std::endl;
}
int main() {
std::thread t1(set_flag);
std::thread t2(test_flag);
t1.join();
t2.join();
clear_flag();
return 0;
}
读写CSV文件
csv文件写入
std::ios::out
是用于打开文件进行写操作的标志。
std::ios::trunc
是用于在打开文件时清除文件内容的标志。
#include <iostream>
#include <fstream>
int main()
{
std::ofstream outFile;
outFile.open("test.csv", std::ios::out | std::ios::trunc);
// 写入标题行
outFile << "name" << ','
<< "income" << ','
<< "expenditure" << ','
<< "addr" << std::endl;
// ********写入两行数据*********
// 写入字符串(数字)
outFile << "zhangsan" << ','
<< "3000" << ','
<< "1200" << ','
<< "中国 北京市" << std::endl;
// 写入浮点数(转为字符串)
outFile << "lisi" << ','
<< std::to_string(2032.1) << ','
<< std::to_string(789.2) << ','
<< "中国 陕西省" << std::endl;
outFile.close();
return 0;
}
读取csv文件
#include <iostream>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <string>
using namespace std;
void main()
{
ifstream inFile("Data.csv", ios::in);
if (!inFile)
{
cout << "打开文件失败!" << endl;
exit(1);
}
int i = 0;
string line;
string field;
while (getline(inFile, line))//getline(inFile, line)表示按行读取CSV文件中的数据
{
string field;
istringstream sin(line); //将整行字符串line读入到字符串流sin中
getline(sin, field, ','); //将字符串流sin中的字符读入到field字符串中,以逗号为分隔符
cout<<atoi(field.c_str())<<" ";//将刚刚读取的字符串转换成int
getline(sin, field, ','); //将字符串流sin中的字符读入到field字符串中,以逗号为分隔符
cout << atoi(field.c_str()) << " ";//将刚刚读取的字符串转换成int
getline(sin, field, ','); //将字符串流sin中的字符读入到field字符串中,以逗号为分隔符
cout << atoi(field.c_str()) << " ";//将刚刚读取的字符串转换成int
getline(sin, field); //将字符串流sin中的字符读入到field字符串中,以逗号为分隔符
cout << field.c_str() << endl;//将刚刚读取的字符串转换成int
i++;
}
inFile.close();
cout << "共读取了:" << i << "行" << endl;
cout << "读取数据完成" << endl;
}
}
list 用法
#include <iostream>
#include <list>
int main() {
std::list<int> l = {1, 2, 3, 4};
// 遍历 list
std::cout << "list elements: ";
for (int i : l) {
std::cout << i << " ";
}
// 在 list 的头部插入元素
l.push_front(0);
std::cout << "list after push_front(0): ";
// 在 list 的尾部插入元素
l.push_back(5);
std::cout << "list after push_back(5): ";
// 在 list 的指定位置插入元素
std::list<int>::iterator it = l.begin();
++it;
l.insert(it, 100);
std::cout << "list after insert(it, 100): ";
for (int i : l) {
std::cout << i << " ";
}
// 删除 list 中的指定元素
it = l.begin();
++it;
l.erase(it);
std::cout << "list after erase(it): ";
for (int i : l) {
std::cout << i << " ";
}
// 通过迭代器访问 list 中的元素
std::list<int>::iterator it = l.begin();
std::advance(it, 2);
std::cout << "element at position 2: " << *it << std::endl;
// 不支持随机访问
// std::cout << l[2] << std::endl; // Error: operator[] not supported for list
return 0;
}
std::function的用法
假设有一个程序需要在某个事件发生时调用一个回调函数。但是这个回调函数的类型可能是不确定的,可能是普通函数、类成员函数、lambda
表达式或者其它可调用对象。这时候可以使用std::function
来存储和调用这个回调函数。
例如,我们定义一个简单的函数,该函数将打印一条消息:
void print_message(const std::string& message) {
std::cout << message << std::endl;
}
然后我们定义一个 std::function
类型的变量 callback
,并将其设置为指向 print_message
函数的指针:
std::function<void(const std::string&)> callback = print_message;
std::bind
std::bind
是一个函数模板,它可以将一个可调用对象和一些参数绑定在一起,形成一个新的可调用对象。它可以用来改变函数的参数顺序,添加默认参数等。
举个例子:这里我想将void LidarLocalizationVerify::PointcloudCallback(const std::shared_ptr<drivers::PointCloud> &message)
打包成一个回调函数操作如下
std::function<void(const std::shared_ptr<drivers::PointCloud> &)>
cloud_callback= std::bind(&LidarLocalizationVerify::PointcloudCallback,
this, std::placeholders::_1);
std::bind
将LidarLocalizationVerify::PointcloudCallback
函数和当前对象this
绑定在一起,生成一个新的函数对象。std::placeholders::_1
占位符,表示在调用PointcloudCallback
函数时,将使用第一个参数作为函数的实际参数。
std::move()函数
std::move
作用主要可以将一个左值转换成右值引用,从而可以调用C++11右值引用的拷贝构造函数
std::move应该是针对你的对象中有在堆上分配内存这种情况而设置的,如下
- 在探讨c++11中的Move函数前,先介绍两个概念(左值和右值)
左值和右值
首先区分左值和右值
左值是表达式结束后依然存在的持久对象(代表一个在内存中占有确定位置的对象)
右值是表达式结束时不再存在的临时对象(不在内存中占有确定位置的表达式
便携方法:对表达式取地址,如果能,则为左值,否则为右值
int val;
val = 4; // 正确 ①
4 = val; // 错误 ②
上述例子中,由于在之前已经对变量val进行了定义,故在栈上会给val分配内存地址,运算符=要求等号左边是可修改的左值,4是临时参与运算的值,一般在寄存器上暂存,运算结束后在寄存器上移除该值,故①是对的,②是错的
remove_reference源码剖析
在分析std::move()
与std::forward()
之前,先看看remove_reference
,下面是remove_reference
的实现:
template<typename _Tp>
struct remove_reference
{ typedef _Tp type; };
// 特化版本
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp type; };
std::forward源码剖析
- 转发左值
template<typename _Tp>
constexpr _Tp&& //&& 表示右值引用,用于绑定到右值表达式上,返回类型为右值引用的 constexpr 函数模板
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
先通过获得类型type,定义_t
为左值引用的左值变量,通过static_cast
进行强制转换。_Tp&&
会发生引用折叠,当_Tp
推导为左值引用,则折叠为_Tp& &&
,即_Tp&
,当推导为右值引用,则为本身_Tp&&
,即forward
返回值与static_cast
处都为_Tp&&
。
- 转发右值
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
不同于转发左值,_t
为右值引用的左值变量,除此之外中间加了一个断言,表示当不是左值的时候,也就是右值,才进行static_cast
转换。
std::move()源码剖析
// FUNCTION TEMPLATE move
template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
std::move
的功能是:
传递的是左值,推导为左值引用,仍旧static_cast
转换为右值引用。
传递的是右值,推导为右值引用,仍旧static_cast
转换为右值引用。
在返回处,直接范围右值引用类型即可。还是通过renive_reference
获得_Tp
类型,然后直接type&&
即可。
所以std::remove_reference<_Tp>::type&&
,就是一个右值引用,我们就知道了std::move
干的事情了。