1.
① 可变参数... 、__VA_ARGS__与##__VA_ARGS__
结论:##__VA_ARGS__
中##
的作用就是去掉前面多余的,号
,在使用自定义打印的时候,推荐##__VA_ARGS__而不是__VA_ARGS__
C语言##__VA_ARGS__的用法_fengwang0301的博客-CSDN博客
例1 __VA_ARGS__
报错如下:
#include <stdio.h>
#include <iostream>
// ...表示可变参数,__VA_ARGS__就是将...的值复制到这里
#define LOG(...) printf(__VA_ARGS__)
int main(){
std::string str = "BBBBBBBBBB";
int num = 10086;
LOG("AAAAAAAAAAAA\n");
LOG(str); // 错误
return 0;
}
原因是printf()的传参只能是char*类型,我这里给的string类型无法默认转换
修改如下:
#include <stdio.h>
#include <iostream>
// ...表示可变参数,__VA_ARGS__就是将...的值复制到这里
#define LOG(...) printf(__VA_ARGS__)
int main(){
std::string str = "BBBBBBBBBB";
int num = 10086;
LOG("AAAAAAAAAAAA\n");
LOG(str.c_str());
return 0;
}
又如:
#include <stdio.h>
#include <iostream>
// ...表示可变参数,__VA_ARGS__就是将...的值复制到这里
#define LOG(...) printf(__VA_ARGS__)
int main(){
std::string str = "BBBBBBBBBB";
int num = 10086;
LOG("AAAAAAAAAAAA\n");
LOG("this is test: %s, %d", str.c_str(), num);
return 0;
}
例2 ##__VA_ARGS__
#include <stdio.h>
#include <iostream>
// <%s:%s> 对应 __FILE__, __FUNCTION__ , format 对应 ##__VA_ARGS__
#define LOG(format, ...) printf("<%s:%s>:" format, __FILE__, __FUNCTION__, ##__VA_ARGS__)
int main(){
std::string str = "BBBBBBBBBB";
int num = 10086;
LOG("AAAAAAAAAAAA\n");
LOG("this is test: %s, %d\n", str.c_str(), num);
LOG();
return 0;
}
② 智能指针std::shared_ptr
reset()就是把shared_ptr赋空,reset(new Tmp(10))就是把shared_ptr 指向new Tmp(10)。
#include <iostream>
int main(){
int* a;
std::cout << "new int(5)-------:\n";
a = new int(5); // 元素
std::cout << *a << "\n";
std::cout << "new int[5]-------:\n";
a = new int[5]; // 数组(初始未赋值)
std::cout << *a << "\t";
std::cout << *(a+1) << "\n";
a[0] = 2; // 赋值
a[1] = 4;
std::cout << *a << "\t";
std::cout << *(a+1) << "\n";
// a = new int()[5]; // 错,int是类型而不是类
std::cout << "new int[5]()-------:\n";
a = new int[5](); // 数组(初始未赋值,括号里默认所有元素为0)
std::cout << *a << "\t";
std::cout << *(a+1) << "\n";
// a = new int[5](2, 4, 6, 8, 10); // error: array 'new' cannot have initialization arguments
a[0] = 2; // 赋值
a[1] = 4;
std::cout << *a << "\t";
std::cout << *(a+1) << "\n";
std::cout << "std::string b------:\n";
std::string b;
// b = new std::string(); // error: no viable overloaded '='
// b("aaaa"); // error: type 'std::string' (aka 'basic_string<char>') does not provide a call operator
b = "bbbbbb";
std::cout << b << "\n";
std::cout << "std::string c-------:\n";
std::string c("cccccc");
std::cout << c << "\n";
return 0;
}
(1)智能指针std::shared_ptr 与普通指针的转换
std::shared_ptr 和普通指针的转换_指针转shared_ptr_HosannaX的博客-CSDN博客
struct test{
int num;
string name;
};
test* pTest = new test();
std::shared_ptr<test> ptr_test = std::shared_ptr<test>(pTest); //普通指针转shared_ptr
std::shared_ptr<test> ptr_test2 = std::make_shared<test>();
test* pTest2 = ptr_test2.get(); //shared_ptr转普通指针
例1
报错如下:
#include <stdio.h>
#include <iostream>
int main(){
std::string a = "AAAAA";
printf(a);
return 0;
}
修改如下:
#include <stdio.h>
#include <iostream>
int main(){
char* a = "AAAAA";
printf(a);
return 0;
}
例2
#include <stdio.h>
#include <iostream>
int main(){
std::shared_ptr<char> cptr = std::make_shared<char>('A');
printf(cptr.get());
return 0;
}
#include <stdio.h>
#include <iostream>
int main(){
// 或 std::shared_ptr<std::string> cptr = std::make_shared<std::string>(10, 'A');
std::shared_ptr<std::string> cptr = std::make_shared<std::string>("AAAAAAAAAAA");
// 或 std::cout << *cptr.get();
printf(cptr.get()->c_str());
return 0;
}
(2)智能指针std::shared_ptr 指向数组
5种创建指向数组的智能指针shared_ptr/unique_ptr的方法_智能指针创建数组_我不是萧海哇~~~~的博客-CSDN博客
#include <iostream>
int main(){
std::cout << "数:\n";
int* a = new int(10);
std::cout << "a: " << *a << "\n";
std::cout << "数组:\n";
int* b = new int[10];
std::cout << "b: " << *b << "\n";
std::cout << "b[5]: " << b[5] << "\n";
std::cout << "数组赋值:\n";
b[0] = 0;
b[5] = 5;
std::cout << "b: " << *b << "\n";
std::cout << "b[5]: " << b[5] << "\n";
std::cout << "-----------------\n";
std::cout << "数:\n";
std::shared_ptr<int> a1 = std::make_shared<int>(10);
std::cout << "a1: " << *a1 << "\n";
std::cout << "数组:\n";
// std::shared_ptr<int> b1 = std::make_shared<>(new int[10]); // 错误
std::shared_ptr<int> b1(new int[10], std::default_delete<int[]>()); // 创建指向数组的智能指针
std::cout << "b1: " << *b1 << "\n";
// std::cout << "b1[5]: " << b1[5]; // 错误
std::cout << "b1[5]: " << b1.get()[5] << "\n";
std::cout << "数组赋值:\n";
b1.get()[0] = 0;
b1.get()[5] = 5;
std::cout << "b1: " << *b1 << "\n";
std::cout << "b1[5]: " << b1.get()[5] << "\n";
return 0;
}
#include <iostream>
int main(){
std::shared_ptr<char> b1(new char[10], std::default_delete<char[]>()); // 创建指向数组的智能指针
b1.get()[0] = 'h';
b1.get()[1] = 'a';
b1.get()[2] = 'p';
b1.get()[3] = 'p';
b1.get()[4] = 'y';
std::cout << "b1: " << b1.get() << "\n";
return 0;
}
③ int main( ) 里的参数 int argc, char* argv[] 的作用
c++中int main ( int argc , char** argv )_c++ int main_啦啦大侠的博客-CSDN博客
- int main () 是一种对主函数的参数缺省的写法,也是我是在学习C++时主要用到的一种写法,自己也很习惯这种写法。
- int main ( int argc , char** argv ) 和 int main ( int argc , char* argv[] ) 是一样的效果和作用。其中argc是在运行编译的程序时:输入参数的个数+ 1(因为要包括程序名,程序名也算是一个参数)。argv则是指向这些参数的指针数组。
#include <iostream>
int main(int argc, char* argv[]){
printf("argc: %d\n", argc);
for(int i = 0; i < argc; i++){
printf("argv[%d]: %s\n", i, argv[i]);
}
return 0;
}
当然,在不缺省参数下,如果用不到参数的话,不给参数也是可以运行的:
#include <iostream>
int main(int argc, char* argv[]){
printf("test\n");
return 0;
}
④ char*、char[]、string之间的转换
char * 与char []区别总结_char*_bitcarmanlee的博客-CSDN博客
C++中的char,char*,char[]_c++ char*_NeoLy123的博客-CSDN博客
(1)char[]与char*进行转换
报错如下:
#include <iostream>
int main(){
// char*转char[]: 字符拷贝实现,不能进行赋值操作
char* str2 = "def";
char str3[] = str2;
printf("str2:%s\tstr3:%s\n", str2, str3);
}
修改如下:
#include <iostream>
int main(){
// char[]转char*: 直接赋值
char str[] = "abc";
char* str1 = str;
printf("str:%s\tstr1:%s\n", str, str1);
// char*转char[]: 字符拷贝实现,不能进行赋值操作
char* str2 = "def";
char str3[] = "12345";
std::strncpy(str3, str2, strlen(str2) + 1); // 注意加1操作
printf("str2:%s\tstr3:%s\n", str2, str3);
}
(2)char*与string进行转换
#include <iostream>
int main(){
// char*转string
char* str = "hello";
// 赋值转换
std::string str1 = str;
// 构造转换
std::string str2(str, str + strlen(str));
printf("str:%s\tstr1:%s\tstr2:%s\n", str, str1.c_str(), str2.c_str());
// string转char*:赋值转换
std:: string str3 = "abc";
// char* str4 = str3.c_str(); // 错误
char* str4 = const_cast<char*>(str3.c_str());
printf("str3:%s\tstr4:%s\n", str3.c_str(), str4);
}
(3)char[]与string进行转换
#include <iostream>
int main(){
// char[]转string
// 直接赋值
char str[] = "abc";
std::string str1 = str;
// 构造实现
std::string str2(str, str + strlen(str));
printf("str:%s\tstr1:%s\tstr2:%s\n", str, str1.c_str(), str2.c_str());
// string转char[]:构造实现
std::string str3 = "12345";
char str4[] = "qwerty";
strncpy(str4, str3.c_str(), str3.length() + 1);
printf("str3:%s\tstr4:%s\n", str3.c_str(), str4);
}
⑤ #define语句后面是否加分号
#define语句后面加分号(转载)_define后面加分号吗_斗转星移3的博客-CSDN博客
所谓#define语句后面一般没有分号的原因在于,将要替换的字符串还原之后,导致还原位置的语句出现问题,因此才使得后面不能有分号。
也就是说,如果替换之后,语法正常,其实是可以的(注意空格问题)。
例1
例2
⑥ 自定义拷贝构造函数
类名::类名 (const 类名 & 对象名)
{
//拷贝构造函数的函数体
}
我们看到拷贝构造函数的参数有且只有一个:就是同类对象的引用。
这么做的原因有两个:
- 因为调用拷贝构造函数的时候是实参向形参传值,如果传进来的不是引用,那么就是值传递,那么就会在函数里又重新创建一个对象,而重新创建又是通过调用拷贝构造函数,所以如果不是引用的话,就会一直调用下去。
- 调用拷贝构造函数时不需要消耗另外的内存空间。
(1)左值引用T& 与 右值引用T&&
结论:const T& (const不能省略)等价于 T&&
- 左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:变量、函数或数据成员的名字返回左值引用的表达式,如 ++x、x = 1、cout << ’ '字符串字面量如 "hello world"在函数调用时,左值可以绑定到左值引用的参数,如 T&。一个常量只能绑定到常左值引用,如 const T&。
- 反之,纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。最常见的情况有:返回非引用类型的表达式,如 x++、x + 1、make_shared(42)除字符串字面量之外的字面量,如 42、true。
引入右值引用,就是为了移动语义。移动语义就是为了减少拷贝。std::move就是将左值转为右值引用。这样就可以重载到移动构造函数了,移动构造函数将指针赋值一下就好了,不用深拷贝了,提高性能。
例1
报错如下:
#include <iostream>
void process_value(int& i) {
std::cout << "左值引用: " << i << std::endl;
}
int main() {
int a = 0;
process_value(a);
process_value(1); // 报错
}
修改如下:
#include <iostream>
void process_value(const int& i) {
std::cout << "左值引用: " << i << std::endl;
}
int main() {
int a = 0;
process_value(a);
process_value(1);
}
也可修改如下:
#include <iostream>
void process_value(int& i) {
std::cout << "左值引用: " << i << std::endl;
}
void process_value(int&& i) {
std::cout << "右值引用: " << i << std::endl;
}
int main() {
int a = 0;
process_value(a); // 左值引用: 0
process_value(1); // 右值引用: 1
}
例2
报错如下:
CStatus.h
CFuncType.h
CObject.h
修改如下:
CStatus.h
CObject.h
也可修改如下:
CStatus.h
CObject.h
(2)const引发的报错
例1
对象含有与成员 函数 "CGraph::CSTATUS::isOK" 不兼容的类型限定符 -- 对象类型是: const CGraph::CSTATUS
报错如下:
修改如下:
例2
报错如下:
修改如下:
⑦ .inl文件在c++中的意义
.inl
文件从不强制,对编译器没有特别的意义。 这只是一种构build代码的方式,可以为可能读取它的人提供提示。
在两种情况下我使用.inl
文件:
- 有关内联函数的定义。
- 有关function模板的定义。
在这两种情况下,我把函数的声明放在一个头文件中,这个文件包含在其他文件中,然后我在头文件的底部包含.inl
文件。
我喜欢它,因为它将接口从实现中分离出来,并使头文件更容易阅读。 如果你关心实现细节,你可以打开.inl
文件并阅读它。 如果你不这样做,你不需要。
例1
UDistance.h
#ifndef CGRAPH_UDISTANCE_H
#define CGRAPH_UDISTANCE_H
#include "UDistanceObject.h"
CGRAPH_NAMESPACE_BEGIN
/** 传入的类型和计算结果的类型,可能不同。一般默认相同 */
template<typename TSrc, typename TRes = TSrc>
class UDistance : public UDistanceObject {
public:
/**
* 计算距离信息
* @param v1 向量1
* @param v2 向量2
* @param dim1 向量1的维度
* @param dim2 向量2的维度
* @param result 结果信息
* @param ext 可扩展信息
* @return
*/
virtual CStatus calc(const TSrc* v1, const TSrc* v2, CSize dim1, CSize dim2, TRes& result, CVoidPtr ext) = 0;
/**
* 判断入参信息是否符合
* @param v1
* @param v2
* @param dim1
* @param dim2
* @param ext
* @return
*/
virtual CStatus check(const TSrc* v1, const TSrc* v2, CSize dim1, CSize dim2, CVoidPtr ext);
/**
* 将数据归一化
* @param v
* @param dim
* @param ext
* @return
*/
virtual CStatus normalize(TSrc* v, CSize dim, CVoidPtr ext);
};
CGRAPH_NAMESPACE_END
#include "UDistance.inl"
#endif //CGRAPH_UDISTANCE_H
UDistance.inl
#ifndef CGRAPH_UDISTANCE_INL
#define CGRAPH_UDISTANCE_INL
#include <cmath>
#include "UDistance.h"
CGRAPH_NAMESPACE_BEGIN
template<typename TSrc, typename TRes>
CStatus UDistance<TSrc, TRes>::check(const TSrc* v1, const TSrc* v2, CSize dim1, CSize dim2, CVoidPtr ext) {
CGRAPH_FUNCTION_BEGIN
CGRAPH_ASSERT_NOT_NULL(v1)
CGRAPH_ASSERT_NOT_NULL(v2)
if (0 == dim1 * dim2) {
// 理论上不应该传入 dim=0 向量
CGRAPH_RETURN_ERROR_STATUS("input dim error")
}
CGRAPH_FUNCTION_END
}
template<typename TSrc, typename TRes>
CStatus UDistance<TSrc, TRes>::normalize(TSrc* v, CSize dim, CVoidPtr ext) {
CGRAPH_FUNCTION_BEGIN
/** 这里不需要判定v为空的情况,需要的话可以通过开启 needCheck 逻辑来判断 */
TSrc val = 0;
for (CSize i = 0; i < dim; i++) {
val += (v[i] * v[i]);
}
const TSrc& denominator = 1 / std::sqrt(val); // 分母信息
for (CSize i = 0; i < dim; i++) {
v[i] = v[i] * denominator;
}
CGRAPH_FUNCTION_END
}
CGRAPH_NAMESPACE_END
#endif //CGRAPH_UDISTANCE_INL
⑧ final
该关键字是用来标识虚函数不能在子类中被覆盖(override),或一个类不能被继承。
#include <iostream>
struct Base{
virtual void foo();
};
struct A : public Base {
void foo() final; // Base::foo被覆盖 而 A::foo是最终的覆盖函数
void bar1();
// void bar2() final; // error: only virtual member functions can be marked 'final'
};
struct B final : public A {
// void foo() override; // error: declaration of 'foo' overrides a 'final' function
};
// struct C : public B {}; // error: base 'B' is marked 'final'
int main(){
return 0;
}
⑨ 对象初始化 大括号'{ }',等号 '=' ,圆括号 '( )'
使用等号初始化经常会让C++初学者认为会进行一次赋值,但不是那样的。对于内置类型,例如int
,初始化和赋值操作的差别是模糊的。但是对于用户定义的类,区分初始化和赋值操作是很重要的,因为这会导致不同的函数调用:
Widget w1; // 调用默认构造函数
Widget w2 = w1; // 不是赋值操作,调用拷贝构造函数( Widget w2(w1) )
w1 = w2; // 赋值操作(调用operator=函数)
因为初始化的语法很混乱,而且有些情况无法实现,所以C++11提出了统一初始化语法:一种至少在概念上可以用于表达任何值的语法。它的实现基于大括号,所以我称之为大括号初始化。
使用大括号可以更容易的初始化容器列表初始化:std::vector<int> v{1, 3, 5};
大括号也可以用于类内成员的默认初始值,在C++11中,等号”=”也可以实现,但是圆括号 '( )' 则不可以:
class Widget {
...
private:
int x{ 0 }; // x的默认初始值为0
int y = 0; // 同上
int z( 0 ); // 报错
}
另一方面,不可拷贝对象(例如,std::atomic
)可以用大括号和圆括号初始化,但不能用等号:
std::atomic<int> ai1{ 0 }; // 可以
std::atomic<int> ai2( 0 ); // 可以
std::atomic<int> ai3 = 0; // 报错
注意:当大括号初始化用于内置类型的变量时,如果我们初始值存在丢失信息的风险,则编译器将报错:
double ld = 3.14;
int a {ld}; // 报错,存在信息丢失风险
int b (ld); // 正确
大括号初始化的另一个值得注意的特性是它会免疫C++中的最让人头痛的歧义。当开发者想要一个默认构造的对象时,程序会不经意地声明个函数而不是构造对象。
Widget w1(10); // 调用Widget的带参构造函数
但当你尝试用类似的语法调用无参构造时,你声明了个函数,而不是创建对象:
Widget w2();// 最让人头痛的歧义,声明了一个名为w2,不接受任何参数,返回Widget类型的函数!
Widget w2; // 正确:w2是个默认初始化的对象
使用大括号包含参数是无法声明为函数的,所以使用大括号默认构造对象不会出现这个问题:
Widget w2{}; // 无歧义
我们讲了很多大括号初始化的内容,这种语法可以用于多种场景,还可以避免隐式范围窄化转换,又免疫C++的最让人头痛的歧义问题。一举多得,那么为什么这条款不起名为“用大括号初始化语法替代其他”呢?
大括号初始化的缺点是它有时会显现令人惊讶的的行为。这些行为的出现是因为与std::initializer_list
混淆了。在构造函数中,只要形参不带有std::initializer_list
,圆括号和大括号行为一致:
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
...
};
Widget w1(10, true); // 调用第一个构造函数
Widget w2{10, true}; // 调用第一个构造函数
Widget w3(10, 5.0); // 调用第二个构造函数
Widget w4{10, 5.0}; // 调用第二个构造函数
但是,如果构造函数的形参带有std::initializer_list
,调用构造函数时大括号初始化语法会强制使用带std::initializer_list
参数的重载构造函数:
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<long double> il);
...
};
Widget w1(10, true); // 使用圆括号,调用第一个构造函数
Widget w2{10, true}; // 使用大括号,强制调用第三个构造函数,10和true被转换为long double
Widget w3(10, 5.0); // 使用圆括号,调用第二个构造函数
Widget w4{10, 5.0}; // 使用大括号,强制调用第三个构造函数,10和5.0被转换为long double
此时此刻,大括号初始化,std::initializer_list
,构造函数重载之间的复杂关系在你的大脑中冒泡,你可能想要知道这些信息会在多大程度上关系到你的日常编程。可能比你想象中要多,因为std::vector
就是一个被它们直接影响的类。std::vector
中有一个可以指定容器的大小和容器内元素的初始值的不带std::initializer_list
构造函数,但它也有一个可以指定容器中元素值的带std::initializer_list
函数:
// 使用不带std::initializer_list的构造函数
// 创建10个元素的vector,每个元素的初始值为20
std::vector<int> v1(10, 20);
// 使用带std::initializer_list的构造函数
// 创建2个元素的vector,元素值为10和20
std::vector<int> v2 = {10, 20};
⑩ 头文件相互嵌套,提示没有这个类的问题解决方法
这样相互包含的问题,可以用前置声明解决。即:在头文件中声明该类,在实现文件中包含该类。
C++ 前置声明_c++ 前置声明 命名空间_HUSTER593的博客-CSDN博客
报错如下:
A.h
#ifndef A_H
#define A_H
#include <iostream>
#include "B.h"
class A{
public:
static std::string aName;
static std::shared_ptr<B> b;
};
std::string A::aName = "AAAAA";
std::shared_ptr<B> A::b = nullptr;
#endif
B.h
#ifndef B_H
#define B_H
#include <iostream>
#include "A.h"
class B{
public:
static std::string bName;
};
std::string B::bName = A::aName + "BBBBB";
#endif
A.cpp
#include <iostream>
#include "A.h"
int main(){
std::cout << A::b->bName;
return 0;
}
修改如下:
A.h
#ifndef A_H
#define A_H
#include <iostream>
class B;
class A{
public:
static std::string aName;
static std::shared_ptr<B> b;
};
std::string A::aName = "AAAAA";
std::shared_ptr<B> A::b = nullptr;
#endif
B.h
#ifndef B_H
#define B_H
#include <iostream>
#include "A.h"
class B{
public:
static std::string bName;
};
std::string B::bName = A::aName + "BBBBB";
#endif
A.cpp
#include <iostream>
#include "A.h"
#include "B.h"
int main(){
std::cout << A::b->bName;
return 0;
}
2.
① 互斥锁与条件变量的结合使用
互斥锁:在多个线程同时访问同一个变量的情况下,保证在某一个时刻只能有一个线程访问。每个线程在访问共享变量的时候,首先要先获得锁,然后才能访问共享变量,当一个线程成功获得锁时,其他变量都会block在获取锁这一步,这样就达到了保护共享变量的目的。
条件变量:用于多线程之间的线程同步。线程同步是指线程间需要按照预定的先后顺序进行的行为,比如我想要线程1完成了某个步骤之后,才允许线程2开始工作,这个时候就可以使用条件变量来达到目的。
(1)互斥锁需要条件变量的原因
以一个生产者消费者的例子来看,生产者和消费者通过一个队列连接,因为队列属于共享变量,所以在访问队列时需要加锁。生产者向队列中放入消息的时间是不一定的,因为消费者不知道队列什么时候有消息,所以只能不停循环判断或者sleep一段时间,不停循环会浪费cpu资源,如果sleep那么要sleep多久,sleep太短又会浪费资源,sleep太长又会导致消息消费不及时。
#include <iostream>
#include <sys/time.h>
#include <unistd.h>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <random>
uint64_t GetNowUs()
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000000 + tv.tv_usec;
}
struct Message
{
Message(int _id, const std::string& _msg)
: id(_id), msg(_msg)
{
genTime = GetNowUs();
}
// 消费消息时打印消息产生时间和消费时间的间隔
void Consume()
{
uint64_t dura = GetNowUs() - genTime;
std::cout << "ID:" << id << "\tdura:" << dura << "\t" << msg << std::endl;
}
int id;
uint64_t genTime;
std::string msg;
};
std::mutex mtx; // 全局互斥锁.
std::queue<Message> msgQueue;
uint32_t GetSleepTime()
{
static std::random_device rd;
static std::default_random_engine engine(rd());
static std::uniform_int_distribution<uint32_t> dist(1, 10);
return dist(engine);
}
// 生产者每隔一个随机的时间(1~10秒),就会生产一条消息
void DoProduce(int id)
{
while (true)
{
{
std::unique_lock<std::mutex> lock(mtx);
msgQueue.push(Message(id, "new message"));
}
sleep(GetSleepTime());
}
}
// 当队列中的消息被消费完,消费者slepp 3秒
void DoConsume()
{
while (true)
{
{
std::unique_lock<std::mutex> lock(mtx);
while (!msgQueue.empty())
{
msgQueue.front().Consume();
msgQueue.pop();
}
}
sleep(3);
}
}
int main()
{
int num = 4;
std::thread producers[num];
std::thread consumer(DoConsume);
for (int i = 0; i < num; ++i)
{
producers[i] = std::thread(DoProduce, i);
}
for (int i = 0; i < num; ++i)
{
producers[i].join();
}
consumer.join();
return 0;
}
这个时候运行程序的结果可以看出来,有的消息很快就会被消费,有的消息要等3秒才能被消费。这个时候其实我们想要的很简单,就是生产者生产完消息之后,通知一下消费者,这个时候就可以引入我们的条件变量了。
使用条件变量改进后的代码 :
#include <iostream>
#include <sys/time.h>
#include <unistd.h>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <random>
uint64_t GetNowUs()
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000000 + tv.tv_usec;
}
struct Message
{
Message(int _id, const std::string& _msg)
: id(_id), msg(_msg)
{
genTime = GetNowUs();
}
// 消费消息时打印消息产生时间和消费时间的间隔
void Consume()
{
uint64_t dura = GetNowUs() - genTime;
std::cout << "ID:" << id << "\tdura:" << dura << "\t" << msg << std::endl;
}
int id;
uint64_t genTime;
std::string msg;
};
std::mutex mtx; // 全局互斥锁.
std::condition_variable cond; // 全局条件变量.
std::queue<Message> msgQueue;
uint32_t GetSleepTime()
{
static std::random_device rd;
static std::default_random_engine engine(rd());
static std::uniform_int_distribution<uint32_t> dist(1, 10);
return dist(engine);
}
// 生产者每隔一个随机的时间(1~10秒),就会生产一条消息,生产消息之后通知消费者
void DoProduce(int id)
{
while (true)
{
{
std::unique_lock<std::mutex> lock(mtx);
msgQueue.push(Message(id, "new message"));
cond.notify_one();
}
sleep(GetSleepTime());
}
}
// 消费者消费完之后,等待生产者的通知
void DoConsume()
{
while (true)
{
std::unique_lock<std::mutex> lock(mtx);
while (!msgQueue.empty())
{
msgQueue.front().Consume();
msgQueue.pop();
}
cond.wait(lock);
}
}
int main()
{
int num = 4;
std::thread producers[num];
std::thread consumer(DoConsume);
for (int i = 0; i < num; ++i)
{
producers[i] = std::thread(DoProduce, i);
}
for (int i = 0; i < num; ++i)
{
producers[i].join();
}
consumer.join();
return 0;
}
前后对比:
生产者从
到
消费者从
到
通过上面的示例,我们就可以知道,互斥锁只能保证线程之间的互斥,但是不能保证线程之间的执行顺序,而引入条件变量,就是控制线程之间的执行顺序,以生产者消费者为例,就是生产者生产完消息之后,消费者才去消费消息。而不是消费者盲目的去循环或者sleep。
(2)条件变量需要互斥锁的原因
既然条件变量可以线程之间进行同步,那为什么还要互斥锁呢?也就是为什么条件变量一定要和互斥锁一起使用呢?
就拿上面的例子来说,互斥锁是为了保证队列同一时刻只能被一个线程访问。如果队列换成无锁队列,是不是就不需要互斥锁了呢?
为了解释这个问题,我们假设程序使用的是无锁队列。消费者的逻辑 可以简单分为两步:
- 消费消息直至消费完;
- 执行cond.wait(lock)开始等待下一次通知。
如果有互斥锁的情况下,这两步是原子的,就是在这个过程中是不会有新的消息添加到队列中的。那如果没有互斥锁保护,那么这两步就不是原子的了,比如刚执行完步骤1,生产者在队列里添加了一个消息,生产者添加消息并发送通知之后消费者才开始执行步骤2,这个时候就会导致这个新添加的消息无法及时被消费者消费到。
② 多线程并发
以两个线程并发举例:
第1次运行结果:
第2次运行结果:
第3次运行结果:
所以想到给共享资源加锁。
(1)互斥量mutex与原子变量atomic
先说结论:mutex一般不单独使用,而是用模板类std::unique_lock()来管理。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int globalV = 0;
void task(){
for(int i = 0; i < 1000000; i++){
mtx.lock();
globalV++;
globalV--;
mtx.unlock();
}
}
int main(){
std::thread t1(task);
std::thread t2(task);
t1.join();
t2.join();
printf("globalV = %d", globalV);
return 0;
}
如上直接加锁,虽然得到的值一直为0了,但是存在线程不安全的问题,如死锁。
以下是会发生死锁的第一种情况:
结果是一直运行,卡在那里不会结束。
以下是会出现死锁的第二种情况:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1;
std::mutex mtx2;
int globalV = 0;
void task1(){
for(int i = 0; i < 1000000; i++){
mtx1.lock();
mtx2.lock();
globalV++;
globalV--;
mtx1.unlock();
mtx2.unlock();
}
}
void task2(){
for(int i = 0; i < 1000000; i++){
mtx2.lock();
mtx1.lock();
globalV++;
globalV--;
mtx2.unlock();
mtx1.unlock();
}
}
int main(){
std::thread t1(task1);
std::thread t2(task2);
t1.join();
t2.join();
printf("globalV = %d", globalV);
return 0;
}
任务1先申请锁1, 任务2先申请锁2。接着任务1申请锁2,但此时锁2已经任务2被占用着(任务2同理),就出现了死锁。
针对死锁的第一种情况,可以通过模板类std::lock_guard() 来解决(RAII的思想):
解决前:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int globalV = 0;
void task(){
for(int i = 0; i < 10000000; i++){
// std::lock_guard<std::mutex> lock(mtx);
globalV++;
globalV--;
if(i == 1000000){
return;
}
}
}
int main(){
std::thread t1(task);
std::thread t2(task);
t1.join();
t2.join();
printf("globalV = %d", globalV);
return 0;
}
第1次运行结果:
第2次运行结果:
第3次运行结果:
解决后:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int globalV = 0;
void task(){
for(int i = 0; i < 10000000; i++){
std::lock_guard<std::mutex> lock(mtx);
globalV++;
globalV--;
if(i == 1000000){
return;
}
}
}
int main(){
std::thread t1(task);
std::thread t2(task);
t1.join();
t2.join();
printf("globalV = %d", globalV);
return 0;
}
需注意,std::unique_lock() 比 std::lock_guard() 的优势是前者可以提前控制解锁,以达到控制作用域范围的目的,后者则不行,只能等待自行析构释放。
针对死锁的第二种情况,解决方法如下:
任务1与任务2的加锁解锁顺序保持一致
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1;
std::mutex mtx2;
int globalV = 0;
void task1(){
for(int i = 0; i < 10000000; i++){
mtx1.lock();
mtx2.lock();
globalV++;
globalV--;
mtx1.unlock();
mtx2.unlock();
}
}
void task2(){
for(int i = 0; i < 10000000; i++){
mtx1.lock();
mtx2.lock();
globalV++;
globalV--;
mtx1.unlock();
mtx2.unlock();
}
}
int main(){
std::thread t1(task1);
std::thread t2(task2);
t1.join();
t2.join();
printf("globalV = %d", globalV);
return 0;
}
或者通过std::lock()把多把锁都锁在一起:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1;
std::mutex mtx2;
int globalV = 0;
void task1(){
for(int i = 0; i < 10000000; i++){
std::lock(mtx1, mtx2);
globalV++;
globalV--;
mtx1.unlock();
mtx2.unlock();
}
}
void task2(){
for(int i = 0; i < 10000000; i++){
std::lock(mtx1, mtx2);
globalV++;
globalV--;
mtx1.unlock();
mtx2.unlock();
}
}
int main(){
std::thread t1(task1);
std::thread t2(task2);
t1.join();
t2.join();
printf("globalV = %d", globalV);
return 0;
}
此外,也可以直接用原子操作std::atomic() 来代替 mutex:
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> globalV; // 原子操作常用于计数
// std::atomic<int> globalV = 0; // 报错
void task(){
for(int i = 0; i < 10000000; i++){
globalV++;
globalV--;
}
}
int main(){
std::thread t1(task);
std::thread t2(task);
t1.join();
t2.join();
std::cout << globalV;
return 0;
}
(2)条件变量condition_variable
#include <iostream>
#include <thread>
#include <mutex>
#include <deque>
#include <condition_variable>
std::mutex mtx;
std::deque<int> dq;
std::condition_variable cv;
void producer(){
int i = 0;
while (1){
std::unique_lock<std::mutex> lock(mtx);
dq.push_back(i);
cv.notify_one(); // 唤醒
if(i < 999){
i++;
}else{
i = 0;
}
}
}
void consumer1(){
int data = 0;
while (1){
std::unique_lock<std::mutex> lock(mtx);
// 用while来避免虚假唤醒
while (dq.empty()){
cv.wait(lock); // 休眠
}
data = dq.front();
dq.pop_front();
printf("consumer1 get value from deque: %d\n", data);
}
}
void consumer2(){
int data = 0;
while (1){
std::unique_lock<std::mutex> lock(mtx);
while (dq.empty()){
cv.wait(lock);
}
data = dq.front();
dq.pop_front();
printf("consumer2 get value from deque: %d\n", data);
}
}
int main(){
std::thread t1(producer);
std::thread t2(consumer1);
std::thread t3(consumer2);
t1.join();
t2.join();
t3.join();
return 0;
}
(3)future(shared_future)、promise
#include <iostream>
#include <thread>
#include <unistd.h>
void task(int a, int b, int& ret){
ret = a*b;
}
int main(){
int ret_ = 0;
std::thread t(task, 3, 5, std::ref(ret_));
// sleep(4); // do something
// ret_是主线程与子线程的共享变量,在不加锁的时候会有问题
printf("get value: %d\n", ret_); // 若不加延时,至此的子线程还没来得及生效,所以ret_尚未改变
t.join();
printf("get value: %d\n", ret_); // join后,子线程已执行完毕,所以ret_对应子线程的修改结果
return 0;
}
对比如下:
#include <iostream>
#include <thread>
#include <unistd.h>
void task(int a, int b, int& ret){
ret = a*b;
}
int main(){
int ret_ = 0;
std::thread t(task, 3, 5, std::ref(ret_));
sleep(4); // do something
// ret_是主线程与子线程的共享变量,在不加锁的时候会有问题
printf("get value: %d\n", ret_);
t.join();
printf("get value: %d\n", ret_);
return 0;
}
以上的ret_是主线程与子线程的共享变量,所以在不加锁的时候会有问题,现通过 mutex与condition_variable 修改如下:
#include <iostream>
#include <thread>
#include <unistd.h>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
void task(int a, int b, int& ret){
std::unique_lock<std::mutex> lock(mtx);
ret = a*b;
cv.notify_one();
}
int main(){
int ret_ = 0;
std::thread t(task, 3, 5, std::ref(ret_));
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock); // 用条件变量确保子线程完成后,才会往下继续执行
printf("get value: %d\n", ret_);
t.join();
printf("get value: %d\n", ret_);
return 0;
}
以上的实现是正确的,也可以用future+promise来实现:
#include <iostream>
#include <thread>
#include <future>
void task(int a, int b, std::promise<int>& p){
p.set_value(a*b);
}
int main(){
std::promise<int> p_;
std::thread t(task, 3, 5, std::ref(p_));
std::future<int> f = p_.get_future();
printf("get value: %d\n", f.get()); // f.get()只能执行一次
t.join();
return 0;
}
再如:
#include <iostream>
#include <thread>
#include <future>
#include <unistd.h>
void task(int a, std::future<int>& b, std::promise<int>& p){
p.set_value(a*b.get());
}
int main(){
std::promise<int> p_ret, p_in;
std::future<int> f_in = p_in.get_future();
std::thread t(task, 3, std::ref(f_in), std::ref(p_ret));
sleep(4); // do something
p_in.set_value(8); // task()的第二个参数不是立即给出,而是等待一段操作后再传入
std::future<int> f_ret = p_ret.get_future();
printf("get value: %d\n", f_ret.get());
t.join();
return 0;
}
std::shared_future用法:
#include <iostream>
#include <thread>
#include <future>
#include <unistd.h>
void task(int a, std::shared_future<int> b, std::promise<int>& p){
p.set_value(a*b.get());
}
int main(){
std::promise<int> p_in, p_ret1, p_ret2, p_ret3, p_ret4;
std::future<int> f_in = p_in.get_future();
std::shared_future<int> s_f = f_in.share();
std::thread t1(task, 1, s_f, std::ref(p_ret1));
std::thread t2(task, 2, s_f, std::ref(p_ret2));
std::thread t3(task, 3, s_f, std::ref(p_ret3));
std::thread t4(task, 4, s_f, std::ref(p_ret4));
sleep(4); // do something
p_in.set_value(8); // task()的第二个参数不是立即给出,而是等待一段操作后再传入
std::future<int> f_ret1 = p_ret1.get_future();
std::future<int> f_ret2 = p_ret2.get_future();
std::future<int> f_ret3 = p_ret3.get_future();
std::future<int> f_ret4 = p_ret4.get_future();
printf("t1 get value: %d\n", f_ret1.get());
printf("t2 get value: %d\n", f_ret2.get());
printf("t3 get value: %d\n", f_ret3.get());
printf("t4 get value: %d\n", f_ret4.get());
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}
(4)std::async
#include <iostream>
#include <thread>
#include <unistd.h>
#include <future>
#include <chrono>
int task(int a, int& b){
sleep(2);
b = 8;
return a*b;
}
int main(){
auto begin = std::chrono::high_resolution_clock::now();
int ret = 4;
// std::launch::async会另起线程;std::launch::deferred延迟执行,不会另起线程
// std::future<int> f = std::async(std::launch::async, task, 3, std::ref(ret));
std::future<int> f = std::async(std::launch::deferred, task, 3, std::ref(ret));
sleep(4);
printf("ret: %d\n", ret);
printf("f.get(): %d\n", f.get()); // 阻塞至get()才会执行
printf("ret: %d\n", ret);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<float> duration = end - begin;
// printf("测试std::launch::async,总耗时: %fs\n", duration.count());
printf("测试std::launch::deferred,总耗时: %fs\n", duration.count());
return 0;
}
③ 左值与右值 左值引用与右值引用
(1)左值与右值
在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。
举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。
左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;
右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。
一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
(2)左值引用
左值引用就是我们平常使用的“引用”。引用是为对象起的别名,必须被初始化,与变量绑定到一起,且将一直绑定在一起。
我们通过 & 来获得左值引用,
type &引用名 = 左值表达式;
可以把引用绑定到一个左值上,而不能绑定到要求转换的表达式、字面常量或是返回右值的表达式。举个例子:
int i = 42;
int& r = i; //正确,左值引用
int& r1 = i*42; //错误, i*42是一个右值
const int& r2 = i*42; //正确,可以将一个const的引用绑定到一个右值上
(3)右值引用
右值引用是C++11中引入的新特性 , 它实现了转移语义和精确传递。
它的主要目的有两个方面:
消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
能够更简洁明确地定义泛型函数。
右值引用就是必须绑定到右值的引用,他有着与左值引用完全相反的绑定特性,我们通过 && 来获得右值引用。
右值引用的基本语法type &&引用名 = 右值表达式;
右值有一个重要的性质——只能绑定到一个将要销毁的对象上。举个例子:
int&& rr = i; //错误,i是一个变量,变量都是左值
int&& rr1 = i*42; //正确,i*42是一个右值
(4)区别对比
- 左值可以寻址,而右值不可以。
- 左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。
- 左值可变,右值不可变(仅对基础类型适用,用户自定义类型的右值引用可通过成员函数改变)。
④ std::move 移动构造函数 移动赋值运算符函数
使用时机:
std::move的作用是将一个左值变成一个右值,以供移动构造函数使用。
如果一个变量并没有自定义移动构造函数,那么其将调用默认的移动构造函数,其功能与普通的构造函数相同(如果变量内部的成员变量也没定义移动构造函数的话);
因此std::move应该用在自定义了移动构造函数的那些变量中。
这篇博客讲的很好:
Th3.14:对象的移动、移动构造函数、移动赋值运算符函数_移动构造函数和移动赋值函数_Fanfan21ya的博客-CSDN博客
(1)对象的移动
其实就是把该对象所占据的内存空间的访问权限转移(移动)给另一个对象
比如:原来这块内存空间是属于张三的,你现在do了对象转移,则该内存空间就属于李四了!
(2)移动构造函数和移动赋值运算符的概念
我们在前面的章节中提及过,C++11引入右值引用、std::move()函数以及对象移动的概念就是为了提高程序运行的效率!为什么这么说呢?因为我们平时在类中定义的拷贝构造函数以及拷贝赋值运算符重载函数会do大量的拷贝和赋值的操作。这些操作都是非常地耗时的。因此这样你写的代码的效率就会非常低下了!
可能光说文字大家还不是很有体会,那么我举个例子吧:比如vector这个容器,如果你在这个容器中push_back了成千上万甚至更多个对象的话,当你要对vector容器的对象do拷贝操作or赋值操作时,是不是就要挨个地进行拷贝、挨个地进行赋值了呢?是的!!!这样do的代码无疑是非常低效率的!
综上所述:C++11引入了移动构造函数 和 移动赋值运算符重载函数。这两个函数可以帮助我们避免do大量的拷贝和赋值操作,从而大大地提高我们写的代码的执行效率!(也即提高程序的效率了!)
由于移动构造函数以及移动赋值运算符重载函数 与 拷贝构造函数以及拷贝赋值运算符重载函数 非常地类似因此,下面给出5点说明:
(1)如果把 对象A 移动给 对象B后,那么 对象A 就 不能 再使用了
(不论是把对象A整体还是一部分移动给B,A都不能再用了,因为你A已经没有完整的权限用/操作这块内存空间中的东西了!)
(2)这里所谓的“移动”,并不是说把内存中的数据所占据的内存 从一个地址 倒腾 到另一个地址,而只是变更一下所属权而已!
(只是把地址变更一下所有权而已,原本这个房子是属于你的,现在你把房产证的名字换成我,这个房子的地址还是不变的,只是所有者从你变成了我而已!)
(那房产证上写的是我的名字你当然没有权限来住我的房子了对吧?)
(如果说要将地址倒腾掉的话,就是把这个房子的地址都给搬移了,那这样就和拷贝构造函数以及赋值运算符函数没区别了,何来的提升程序的效率呢?对吧?)
(3)这种直接变更内存空间的所有权的构造函数就比单纯的拷贝和赋值的函数的效率要大得多!
(4)那么移动构造函数怎么写呢?(其实和拷贝构造函数写法很类似的!)
(以Time类为例子)
拷贝构造函数:Time::Time(const Time& t){/.../} //const的左值引用&
移动构造函数:Time::Time( Time&& t)noexcept{/.../}// 右值引用&&
(注意这里的右值引用不能是const的,因为你用右值引用do函数参数就算为了让其绑定到一个右值上去的!就是说这个右值引用是一定要变的,但是你一旦加了const就没法改变该右值引用了)
拷贝赋值运算符重载函数:Time& operator=(const Time& t){/.../} //const的左值引用&
移动赋值运算符重载函数:Time& operator=( Time&& t)noexcept{/.../} // 右值引用&&
(介绍到这里相信大家已经明白了我们之前为啥要引入右值引用这个概念了)
(C++11引入右值引用&&类型就是为了写移动构造函数和移动赋值运算符重载函数的!)
(5)移动构造函数 和 移动赋值运算符函数 应该完成什么工作呢?
a)完成必要的内存移动,斩断原对象和其所占据的内存空间的关系。
b)然后,确保 移动后 原对象处于一种“即便被销毁也没有什么问题”的这样一种状态。就比如让对象A移动给对象B,移动后,A对象与它原来所代表的这块内存空间应该没有任何关系了。并且当我销毁该对象A时,不会有任何异常错误出现,且对象B过继自对象A的这块内存空间的数据也不会受到任何影响。此时,我们不应该再使用对象A去对这块内存空间do事情了,而是用对象B来对这块内存do事情!
(3)移动构造函数
移动构造函数格式:
className(className&& tobj) noexcept :initialList {/.../}
注意喔,这里一定要传入一个是右值的对象!不然的话你没法触发(编译器帮你调用)移动构造函数!并且要声明一下noexcept关键字告诉编译器为的移动构造函数是不会触发异常的!为什么要这样干呢?不就是为了防止重复释放同一内存空间的问题嘛。因为你要是在移动构造函数的函数体内实现时没有将原对象所拥有的这块内存空间赋值为nullptr的话,当你释放原对象时就已经把该空间释放了,那你再释放得到这块内存空间的新对象时,编译器就会报错说你重复释放同一内存空间了。
补充:noexcept关键字:C++11引入的关键字,作用:通知标准库我们所定义的这个移动构造函数or移动赋值运算符函数是不抛出任何异常的!(提高编译器的工作效率!)后续学习到该关键字时我会详细总结下来,这里就不做赘述了。
(4)移动赋值运算符
移动构造函数格式:
className& operator=(className&& tobj) noexcept {/.../}
注意喔,这里一定要传入一个是右值的对象!不然的话你没法触发(编译器帮你调用)移动构造函数!
(5)总结
1)在写自定义的类时,尽量给你的类写上对应的移动构造函数以及移动赋值运算符重载函数-以减少大量的关于该类的拷贝和赋值的操作。
(当然,这只是针对复杂的类,或说一些会大量调用其拷贝构造函数or拷贝赋值运算符函数的类;若是比较简单的类or不会大量调用上述两种函数的类就可以不写上移动函数)
2)写移动函数时,一定要在对应的位置上加上noexcept关键字,来通知编译器你写的这个函数并不会抛出异常!
3)当把原对象所代表的内存空间的使用权限过继给新对象后,一定要记得把原对象所占据的内存空间指向空(值 = NULL | 指针 = nullptr)!
4)若没有移动函数,编译器会为你自动调用对应的拷贝函数完成相应的创建对象和给对象赋值的操作(相比用移动函数,这样do你的代码效率就是低的!)。
⑤ std::forward 完美转发
std::forward通常是用于完美转发的,它会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。一个经典的完美转发的场景是:
template <class... Args>
void forward(Args&&... args) {
f(std::forward<Args>(args)...);
}
需要注意的有2点:1、输入参数的类型是Args&&... , &&的作用是引用折叠,其规则是:
&& && -> &&
& && -> &
& & -> &
&& & -> &
由于我们需要保留输入参数的右值属性,因此Args后面需要跟上&&;2、std::forward的模板参数必须是<Args>,而不能是<Args...>,这是由于我们不能对Args进行解包之后传递给std::forward,而解包的过程必须在调用std::forward之后。
例1 C++ 完美转发为什么一定要用forward - 简书
刚看到这时,有一个疑问,好像 T && universal reference 已经可以接受所有类型的参数,并且保持原来参数的类型。为什么还要有forward呢。想明白后发现 T && 转发并不完美,在转发右值的时候会出问题, 有如下例子
#include <iostream>
void foo(int&& foo_arg){
std::cout << foo_arg << "\n";
}
template <typename T>
void fwd(T&& fwd_arg){
foo(fwd_arg); // 转发右值会报错
}
int main(){
fwd(20);
return 0;
}
修改如下:
#include <iostream>
void foo(int&& foo_arg){
std::cout << foo_arg << "\n";
}
template <typename T>
void fwd(T&& fwd_arg){
// foo(fwd_arg); // 转发右值会报错
foo(std::forward<T>(fwd_arg));
}
int main(){
fwd(20);
return 0;
}
总结:
虽然fwd_arg是的类型是右值引用,但fwd_arg本身是左值,当继续给foo传值的时候其实是一个int && 类型的左值。 就会产生编译错误
这个时候就要用,forward了,因为fwd_arg本身是左值,foo的参数是个右值引用,无法绑定到一个左值上。
用forward<T>(fwd_arg)当实参,T推断成int&&,所以返回值是int&& &&,折叠成int&&,可以被绑定到foo参数的右值引用上,就没有错误了
所以,universal reference 转发时并不完美,只完美了一半,当转发目标的的参数是右值引用时,会出现问题。
std::forward 解决当转发目标的的参数是右值引用时的问题。可以保持原始参数的类型,将实参从原来的类型为右值引用的左值,变成了本身就是右值引用
例2
#include <iostream>
void foo(int& foo_arg){
std::cout << "左值" << "\n";
}
void foo(int&& foo_arg){
std::cout << "右值" << "\n";
}
template <typename T>
void fwd(T&& fwd_arg){
foo(std::forward<T>(fwd_arg));
}
int main(){
fwd(22); // 右值
std::cout << "--------------------\n";
int x = 33;
fwd(x); // 左值
return 0;
}
例3 https://www.cnblogs.com/judes/p/15159460.html
#include <iostream>
template<typename T>
void print(T& t){
std::cout << "左值" << std::endl;
}
template<typename T>
void print(T&& t){
std::cout << "右值" << std::endl;
}
template<typename T>
void testForward(T&& v){
print(v);
print(std::forward<T>(v));
print(std::move(v));
}
int main(){
testForward(1);
std::cout << "======================" << std::endl;
int x = 1;
testForward(x);
}
第一组:
传入的1虽然是右值(1作为常量,自然是右值),但经过函数传参之后它变成了左值(在内存中分配了空间);而第二行由于使用了std::forward函数,所以不会改变它的右值属性,因此会调用参数为右值引用的print模板函数;第三行,因为std::move会将传入的参数强制转成右值,所以结果一定是右值。
第二组:
因为x变量是左值,所以第一行一定是左值;第二行使用forward处理,它依然会让其保持左值,所以第二也是左值;最后一行使用move函数,因此一定是右值。
⑥ std::function 类型擦除
C++中的 std::function
为我们提供了对可调用对象的抽象。我们可以使用 std::function
封装可调用对象,从而擦除其类型信息,使用统一的方法对其进行调用。
C++类型擦除与`std::function`性能探索 | 码农网
std::function用法详解_图灵,图灵,图个机灵的博客-CSDN博客
(1)什么是类型擦除
对于 Python 这种动态类型语言来说,是不存在“类型擦除”这个概念的。Python对象的行为并不由接口定义,而是由“当前方法和属性的集合”决定。
所以,以下的代码是完全合法的:
class Foo(object):
def print(self):
print 'foo'
class Bar(object):
def print(self):
print 'bar'
def do_print(obj):
print obj.print()
do_print(Foo()) // print 'foo'
do_print(Bar()) // print 'bar'
但是对于C++这种静态类型语言来讲,我们就无法使用这种语法:
class Foo {
public: void print() { printf("%s\n", "foo"); }
};
class Bar {
public: void print() { printf("%s\n", "bar"); }
};
void do_print(??? obj) { // 不同类型,无法传参
obj.print();
}
do_print(Foo());
do_print(Bar());
因为 Foo
和 Bar
的类型不同,我们不能直接将不同类型的实例传入 do_print(obj)
函数。所以我们需要一种方法擦除类型信息,提供一种类型的抽象。使得实现不依赖于具体类型,而是依赖于类型抽象。
(2)C++中的类型擦除的实现
1)通过虚函数继承
#include <iostream>
class IPrintable{
public:
virtual void print() = 0;
};
class Foo : public IPrintable {
public:
void print(){printf("%s\n", "foo");}
};
class Bar : public IPrintable {
public:
void print(){printf("%s\n", "bar");}
};
void do_print(std::shared_ptr<IPrintable> obj){
obj->print();
}
int main(){
do_print(std::make_shared<Foo>());
do_print(std::make_shared<Bar>());
return 0;
}
在这里,我们使用了一个 IPrintable
基类,擦除了子类 Foo
和 Bar
的具体类型信息。也就是说,只要子类实现了基类的接口,就可以做为参数传入 do_print(obj)
中。这样的好处是我们只需要为继承同样接口的类型完成一套实现,提供了更好的封装与抽象。
但是,这种实现的问题在于虚函数的调用是有额外的开销的。需要进行一次运行时虚表的查找,才可以确定对象需要调用哪一个函数。
2)通过模版template实现
#include <iostream>
class Foo {
public:
void print(){printf("%s\n", "foo");}
};
class Bar {
public:
void print(){printf("%s\n", "bar");}
};
template <typename T>
void do_print(T obj){
obj.print();
}
int main(){
do_print(Foo());
do_print(Bar());
return 0;
}
3)通过std::function实现
#include <iostream>
class Foo {
public:
static void print1(){printf("%s\n", "foo");}
};
class Bar {
public:
static void print2(){printf("%s\n", "bar");}
};
int main(){
// 该例子没啥实际意义,只是用于演示
std::function<void()> f1 = Foo::print1;
f1();
std::function<void()> f2 = Bar::print2;
f2();
return 0;
}
(3)std::function可调用对象
在 C++ 中,存在“可调用对象( Callable Objects)”这么一个概念。准确来说,可调用对象有如下几种定义 :
- 是一个函数指针。
- 是一个具有 operator() 成员函数的类对象(仿函数)。
- 是一个可被转换为函数指针的类对象。
- 是一个类成员(函数)指针。
std::function 是可调用对象的包装器。它是一个类模板,可以容纳除了类成员(函数)指针之外的所有可调用对象。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。
1)容纳所有这一类调用方式的“函数包装器”
#include <iostream>
void func(){
std::cout << __FUNCTION__ << '\n';
}
class Foo{
public:
static int foo_func(int a){
std::cout << __FUNCTION__ << "(" << a << ") ->: ";
return a;
}
};
class Bar{
public:
int operator()(int a){
std::cout << __FUNCTION__ << "(" << a << ") ->: ";
return a;
}
};
int main() {
// 绑定一个普通函数
std::function<void(void)> fr1 = func;
fr1();
// 绑定一个类的静态成员函数
std::function<int(int)> fr2 = Foo::foo_func;
std::cout << fr2(123) << '\n';
// 绑定一个仿函数
Bar bar;
fr2 = bar;
std::cout << fr2(123) << '\n';
return 0;
}
2)std::function 替代函数指针
因为它可以保存函数延迟执行,所以比较适合作为回调函数。
#include <iostream>
class A{
public:
A(std::function<void()> f) : callbackFunc(f){}
void run(){
std::cout << "33333333\n";
callbackFunc(); // 回调到上层
std::cout << "44444444\n";
}
private:
std::function<void()> callbackFunc;
};
class B{
public:
void operator()(){
std::cout << __FUNCTION__ << '\n';
}
};
int main(){
B b;
std::cout << "111111\n";
A a(b);
std::cout << "222222\n";
a.run();
return 0;
}
3)std::function 作为函数入参
#include <iostream>
void call_when_even(int x, std::function<void(int)> f){
f(x);
}
void output(int x){
std::cout << x << '\t';
}
int main() {
for (size_t i = 0; i < 6; i++){
call_when_even(i, output);
}
return 0;
}
⑦ 回调函数
回调函数的实质——什么是回调函数,为什么要使用回调函数_斗趣的博客-CSDN博客
C++回调函数_c++ 回调函数_zzw-111-bit的博客-CSDN博客
(1)定义
首先,回调函数也是函数,就像白马也是马一样。它具有函数的所有特征,它可以有参数和返回值。其实,单独给出一个函数是看不出来它是不是回调函数的。回调函数区别于普通函数在于它的调用方式。只有当某个函数(更确切的说是函数的指针)被作为参数,被另一个函数调用时,它才是回调函数。就像给你一碗饭,你并不能说它是中饭还是晚饭一样,只有当你在某个时候把它吃掉了你才明确它是中饭还是晚饭(这个比喻貌似有点挫。领会精神就好,哈哈)。
那么问题来了,为什么我们要把函数作为参数来调用呢,直接在函数体里面调用不好吗?这个问题问的好。在这个意义上,“把函数做成参数”和“把变量做成参数”目的是一致的,就是以不变应万变。形参是不变的,而实参是变的。
C++ Primer里面举了个例子就是排序算法。为了使排序算法适应不同类型的数据,并且能够按各种要求进行排序,机智的人类把排序算法做成了一个模版(在标准模版库STL里),并且把判断两个数据之间的“大小”(也可以是“字节数”,或者其他某种可以比较的属性)这个任务(即函数)当成一个参数放在排序算法这个函数的参数列表里,而把它的具体实现就交给了使用排序算法的人。这个判断大小的函数就是一个回调函数。比如我们要给某个vector容器里面的单词进行排序,我们就可以声明一个排序算法:
void stable_sort(
vector<string>::iterator iterBegin,
vector<string>::iterator iterEnd,
bool (*isShorter)(const string &, const string &));
其中前面两个是普通参数,即迭代器(用于标记vector容器里面元素的位置),而第三个参数isShorter就是回调函数。根据不同需求isShorter可以有不同的实现,包括函数名。比如:
bool myIsShorter(const string &s1, const string &s2){
return s1.size()<s2.size();
}
stable_sort(words.begin(),words.end(),myIsShorter);
根据需求你也可以换一种方式来实现。注意,在传递myIsShorter这个参数时,只需写函数名,它代表函数指针。后面绝对不能加()和参数!因为那样是调用函数的返回值!两者天壤之别!在stable_sort运行时,当遇到需要比较两个单词的长短时,就会对myIsShorter进行调用,得到一个判断。在调用时,还必须把两个单词传递给isShorter供isShorter调用。所以说stable_sort调用了myIsShorter,而myIsShorter又调用了stable_sort给它的单词。它们相互调用。这就是“回调”这两个字的含义!
虽然说形参不变,实参可变,以不变应万变。但是作为实参有一点还是不能变的,那就是实参的数据类型不能变。比如void foo(int i)这个函数里的参数i可以取1也可以取2,但是它必须是整型的。同样的,回调函数这种参数的类型也不能变。而函数的类型是由函数的参数类型和返回值类型决定的。比如前面提到的排序算法里面,isShorter这个回调函数的参数必须是两个const string类型,返回值必须是bool类型。所以在写回调函数时还是不能太任性,必须要查看一下调用该回调函数的函数的声明。
总之,所谓回调函数就是把函数当作参数使用。目的是使程序更加普适(正如活字印刷,把可能会“变”的字一个个分离开来,这样就可以任意组合,重复利用)。一般情况下,一个人的小规模程序用不着这种普适性,除非你想把它做成工具箱(比如游戏引擎),供他人使用。
其实实现这种普适性还有其他方法,比如对虚函数进行重写(或者用纯虚函数。Objective C里面所有函数都是虚函数,而协议相当于纯虚函数)。这样同一个函数就可以有不同的实现。不同的合作者之间就可以通过这种虚函数“协议”进行合作。
(2)实现
1)入门案例
#include <iostream>
int callback1(int a){ // 回调函数1
printf("callback1, a = %d\n", a);
return 0;
}
int callback2(int a){ // 回调函数2
printf("callback2, a = %d\n", a);
return 0;
}
int callback3(int a){ // 回调函数3
printf("callback3, a = %d\n", a);
return 0;
}
int func(int a, int(*callbackFunc)(int)){ // 注意这里用到的函数指针定义
callbackFunc(a);
}
int main(){
func(1, callback1);
func(2, callback2);
func(3, callback3);
return 0;
}
在这个入门案例中,Callback_1、2、3就是回调函数,handle函数的第二个参数就是函数指针,也就是通过函数指针来调用。纯C语言通过函数指针来进行回调函数的调用,C++则可以通过引用、Lambda等多种方式来进行。
2)函数指针
函数指针也是一种指针,只不过指向的是函数(C语言中没有对象)。然后通过这个指针就可以调用。
#include <iostream>
int Max(int x, int y) {
if (x > y){
return x;
}else{
return y;
}
}
int main(){
int(*p)(int, int); // 定义一个函数指针
p = Max; // 把函数Max赋给指针变量p, 使p指向Max函数
int a = (*p)(1, 2); // 通过函数指针调用Max函数
printf("max = %d\n", a);
return 0;
}
函数名本身就可以表示该函数地址(指针),因此在获取函数指针时,可以直接用函数名,也可以取函数的地址。
p = Max 可以改成 p = &Max;
c = (*p)(a, b) 可以改成 c = p(a, b);
所以函数指针的通常写法是:
函数返回值类型 (* 指针变量名) (函数参数列表);
即也可如下:
#include <iostream>
int Max(int x, int y) {
if (x > y){
return x;
}else{
return y;
}
}
int main(){
int(*p)(int, int); // 定义一个函数指针
p = Max; // 等同于 p = &Max;
int a = (*p)(1, 2); // 等同于 int a = p(1, 2);
printf("max = %d\n", a);
return 0;
}
最后需要注意的是,指向函数的指针变量没有 ++ 和 -- 运算。
3)C++类的静态函数作为回调函数
前面函数指针的方式作为回调函数的一种方式,可以同时用于C和C++,下面介绍另外的一些方式,因为C++引入了对象的概念,可以使用类的成员和静态函数作为回调函数。
#include <iostream>
class A {
public:
static void FunA() { printf("FunA\n"); }
};
class B {
public:
void FunB(void(*callback)()) {
printf("FunB\n");
callback();
}
};
int main() {
B b;
b.FunB(A::FunA);
return 0;
}
在类B中调用类A中的静态函数作为回调函数,从而实现了回调。但这种实现有一个很明显的缺点:static 函数不能访问非static 成员变量或函数,会严重限制回调函数可以实现的功能。
4)类的非静态函数作为回调函数
如果仍然像上面"类的静态函数作为回调函数"的做法,会报错:
正确如下:
#include <iostream>
class A {
public:
void FunA() { printf("FunA\n"); }
};
class B {
public:
void FunB(void(A::*callback)(), void* p) {
printf("FunB\n");
((A*)p->*callback)();
}
};
int main() {
A a;
B b;
b.FunB(&A::FunA, &a); // 此处都要加&
return 0;
}
功能总体与上面一个相同,但是,类的静态函数本身不属于该类,所以和普通函数作为回调函数类似。这种方式存在一些不足,,也就我预先还要知道回调函数所属的类定义,当ProgramB想独立封装时就不好用了。(违背了一些设计模式的原则)
5)Lambda表达式作为回调函数
Lambda本身就是一种匿名函数,是一种函数的简写形式。
#include <iostream>
void func(int a, std::function<void(int)> f){
f(a);
}
int main() {
auto fun1 = [](int a){printf("a = %d\n", a);};
func(3, fun1);
return 0;
}
6)std::funtion和std::bind实现回调
存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等。
报错如下:
修改如下:
#include <iostream>
class A {
public:
void FunA2() { printf("FunA2\n"); }
static void FunA3() { printf("FunA3\n"); }
};
class B {
private:
typedef std::function<void()> CallbackFun;
public:
void FunB(CallbackFun callback) {
printf("FunB\n");
callback();
}
};
void normFun() { printf("normFun\n"); }
int main() {
A a;
B b;
b.FunB(normFun);
b.FunB(A::FunA3);
b.FunB(std::bind(&A::FunA2, &a));
return 0;
}
或
#include <iostream>
class A {
public:
void FunA2() { printf("FunA2\n"); }
static void FunA3() { printf("FunA3\n"); }
void operator()(){ printf("class A的operator()\n"); }
};
class B {
public:
B(std::function<void()> f) : CallbackFun(f){}
void FunB(){
printf("FunB\n");
CallbackFun();
}
private:
std::function<void()> CallbackFun;
};
void normFun() { printf("normFun\n"); }
int main() {
A a;
B b(a); // 回调a对象的operator()函数
b.FunB();
B b1(normFun);
b1.FunB();
B b2(A::FunA3);
b2.FunB();
B b3(std::bind(&A::FunA2, &a));
b3.FunB();
return 0;
}
⑧ [](){} 与 []{}() 对比
【C++】[](){}与[]{}()_c++ [](){}_None072的博客-CSDN博客
[](){}
是标准的 lambda 表达式用法,而[]{}()
则是 lambda 的简写版+调用,即省略了用于参数传入的(),并在构建完匿名函数后直接调用了自己。
例1
⑨