@Pigmon
2021-11-24T06:45:55.000000Z
字数 11949
阅读 920
C++
Old QQ: Little_Xin_1026
New: LittleXin_1026
基本形式:
std::function<返回类型(参数表)> lamda_name = [scope](参数表){ 函数体 };
目的:可以执行函数功能的封装,又能当做变量来传递。Scope说明见1.2。
class Graphic{public:void draw(){whatToDraw();}std::function<void()> whatToDraw; // (1) 定义一个函数类型变量,可以用Lamda为它赋值。};class ofApp : ofBaseApp{protected:vector<Graphic> graphicElements;};
在cpp中,给不同图形赋予不同的绘制过程,可以通过Lamda简洁的实现。
void ofApp::setup(){Graphic circle;// (1)circle.whatToDraw = [](){ofDrawCircle(50,50,20);};this->graphicElements.push_back(circle);Graphic rectangle;// (1)rectangle.whatToDraw = [](){ofRect(50,50,200,200);};this->graphicElements.push_back(rectangle);}void ofApp::draw(){// 多态性的一种实现for(auto & graphic : this->graphicElements){graphic.draw();}}
用法1,直接把需要传入的变量放入[]
float multiplier = 4.0f;auto multiplyByOurMultiplier = [multiplier](float x){return x * multiplier;};std::cout << multiplyByOurMultiplier(2.0f) << std::endl; // prints 8
用法2,传变量引用
float multiplier = 4.0f;float plus = 2.0f;auto multiplyByOurMultiplier = [&multiplier, plus](float x){multiplier *= 2.0f;return x * multiplier + plus;};std::cout << multiplyByOurMultiplier(2.0f) << std::endl; // prints 18
用法3,[=]可以传入任何可见且使用到的变量
float multiplier = 4.0f;float plus = 2.0f;auto multiplyByOurMultiplier = [=](float x){return x * multiplier + plus;};std::cout << multiplyByOurMultiplier(2.0f) << std::endl; // prints 10
用法4,[&]将使用到的,且可见的变量都以引用的方式传入
float multiplier = 4.0f;float plus = 2.0f;auto multiplyByOurMultiplier = [&](float x){multiplier *= 2.0f;plus += 1.0f;return x * multiplier + plus;};std::cout << multiplyByOurMultiplier(2.0f) << std::endl; // prints 19
用法5,[this]对象作用域内可以传入
// 类定义 Test.h#include <functional>class Test{public:float a, b, c;Test(float _a, float _b, float _c) : a(_a), b(_b), c(_c) {}std::function<float(void)> average = [this]() { return (a + b + c) / 3.0f; };};
// 调用 main.cpp#include <iostream>#include "Test.h"int main(){Test obj(1.0f, 2.0f, 3.0f);std::cout << obj.average() << std::endl; // prints 2return 0;}
‘#’把宏参数直接变成字符串,想不出有什么用处:
#define TO_STRING(x) #xconst char* str = TO_STRING(3.14);
‘##’用于连接左右的内容:
#define TAG(x) ID_##xstd::cout << TO_STRING(TAG(211)) << std::endl; // prints: ID_211。这不就用到了?
__VA_ARGS__关键字用于宏定义中的可变参数列表
#define LOG(...) printf(__VA_ARGS__)#define _LOG_(fmt, ...) printf(fmt, ##__VA_ARGS__)const char* name = "Tom";const int age = 23;LOG("My Name is %s.\n", name); // prints: My Name is Tom._LOG_("I am %d years old.\n", age); // prints: I am 23 years old.
编译时输出宏的内容,用于检查错误
#pragma message(TO_STRING(LOG("My Name is %s.\n", name)))
预定义宏,用于方便输出LOG
| 宏 | 类型 | 含义 |
|---|---|---|
_FILE_ |
const char* | 当前执行文件的绝对路径 |
_LINE_ |
int | 当前行号 |
_FUNCTION_ |
const char* | 当前函数名 |
_DATE_ |
const char* | 日期 |
_TIME_ |
const char* | 时间 |
取消某个宏的定义
#undef MACRO_NAME
减少啰嗦程序,这个不知道为什么用#pragma message打印不出来
#define PROPERTY(Name) \int m_##Name; \int Get##Name() const { \return m_##Name; \} \void Set##Name(int _##Name) { \m_##Name = _##Name; \}class Student{public:PROPERTY(Age);};Student obj;obj.SetAge(23);std::cout << obj.GetAge() << std::endl; // prints: 23
常见的do{}while(0), 目的是防止上下文中不符合编译条件的情况,如在switch语句中不能定义变量,在if else的大括号后面不能有;等。
#define SWAP_INT(x, y) do {int tmp = x; x = y; y = tmp;} while (0)int x = 1, y = 2;if (x < y)SWAP_INT(x, y);elseprintf("It's OK.\n");std::cout << "x = " << x << ", y = " << y << std::endl; // prints: x = 2, y = 1
左值:非临时对象,可以在多个语句中使用的值,比如一般的变量;
右值:临时对象。
如:
int a = 0; // a 是左值,0是右值
右值一般情况下不会被改变,但并非不可以,如:
T().Get().Set(2);
右值引用是以2个&符号标志的。
void process_value(int& _n){std::cout << "Process LValue of " << _n << std::endl;}void process_value(int&& _n){std::cout << "Process RValue of " << _n << std::endl;}void forward_value(int&& _n){process_value();}void main(){int a = 0;process_value(a); // print: Process LValue of 0.process_value(1); // print: Process RValue of 1.forward_value(2); // print: Process LValue of 2. 因为临时对象经过传递会变成左值}
Move 语义相比 Copy 语义,减少临时对象的创建和维护,节省时间和空间。可以在类中定义 Move 语义的 Move 构造函数和 Move 赋值函数,以移动而非深拷贝的方式初始化含有指针成员的类对象,在将右值传递给构造函数或赋值函数时触发自动调用。
Move 语义的构造函数基本形式如下:
T(T&& _obj){// 浅拷贝的方式赋值;// 对_obj的包含指针进行重置,避免资源被释放;}
例如:
MyString(MyString&& _other){_len = _other._len;_data = _other._data;_other._len = 0; // 这句没必要吧?_other._data = NULL;}
Move 赋值操作符重载
MyString& operator=(MyString&& _other){if (this != &_other){_len = _other._len;_data = _other._data;_other._len = 0;_other._data = NULL;}return *this;}
std::move 可以将一个左值引用转换成右值引用来使用,例如:
void process_value(int& _n){std::cout << "Process LValue of " << _n << std::endl;}void process_value(int&& _n){std::cout << "Process RValue of " << _n << std::endl;}void main(){int a = 0;process_value(a); // print: Process LValue of 0process_value(std::move(a)); // print: Process RValue of 0}
std::move 可提高swap操作的效率:
void swap<T>(T& a, T& b){T tmp(std::move(a));a = std::move(b);b = std::move(tmp);}
避免3次拷贝。
指在值的传递过程中,除了值不变以外,属性(左右值 / const 等)也保持不变。通过右值引用传递参数,可以保证属性不变。即T&&的推导规则为:右值实参为右值引用,左值实参仍然为左值引用。
其应用场景例如在需要保持const属性的函数中,如果不使用右值引用,则需要重载:
void process_value(const T& _value);void process_value(T& _value);
这种情况,如果参数很多就需要重载很多的函数。而使用右值引用则不需要进行重载:
void process_value(T&& _value);
64位系统下,unique_ptr 占 8 字节,其余2个 16 字节。
智能指针,用于托管由 new 操作符申请的指针对象。维护引用计数,当计数为0时,自动对被托管指针对象执行 delete 操作。
基本形式:
#include <memory>std::shared_ptr<T> var_name(new T);
多个shared_ptr对同一对象托管,以及reset和计数清0的例子:
#include <iostream>#include <memory>class A{private:int m_i;public:A(int n) : m_i(n) {}~A() { std::cout << m_i << " dstructed.\n"; }void Print() { std::cout << m_i << std::endl; }};int main(){/// sp1, sp2, sp3 都托管 A(2)std::shared_ptr<A> sp1(new A(2));std::shared_ptr<A> sp2(sp1);std::shared_ptr<A> sp3;sp3 = sp2;std::cout << sp1->Print() << ", " << sp2->Print() << ", " << sp3->Prrint() << std::endl;/// get 操作A* ptr = sp3.get();std::cout << ptr->Print() << std::endl;/// reset 会将 share_ptr 的托管对象重置,同时将原托管对象的引用计数 -1sp1.reset(new A(3));sp2.reset(new A(4));sp3.reset(new A(5));/// 这里因为 A(2) 的引用计数已经是0,所以会自动调用 delete 操作。std::cout << sp1->Print() << ", " << sp2->Print() << ", " << sp3->Prrint() << std::endl;std::cout << "end.\n";}
输出:
2, 2, 222 dstructed.3, 4, 5end.5 dstructed.4 dstructed.3 dstructed.
注意事项:
shared_ptr 不要托管非 new 操作申请空间的指针对象,虽然编译可以通过,但执行会产生错误(当自动调用delete)。
不能用下面的方式使得两个 shared_ptr 对象托管同一个指针:
A *ptr = new A(10);std::shared_ptr<A> sp1(ptr), sp2(ptr);
sp1 和 sp2 并不会共享同一个对 p 的托管计数,而是各自将对 p 的托管计数都记为 1(sp2 无法知道 p 已经被 sp1 托管过)。这样,当 sp1 消亡时要析构 p,sp2 消亡时要再次析构 p,这会导致程序崩溃。
补充:
初始化方法:
std::shared_ptr<T> sp1(std::make_shared<T>(...));
参考:http://c.biancheng.net/view/7909.html
特殊的 shared_ptr, 区别是其指向的堆内存无法与其他 unique_ptr 共享。
例子:
std::unique_ptr<int> p4(new int);std::unique_ptr<int> p5(p4);//错误,堆内存不共享std::unique_ptr<int> p5(std::move(p4));//正确,调用移动构造函数/// p5 获得了 p4 所指向的堆内存空间的所有权,p4变成 nullptr。
默认情况下,unique_ptr 指针采用 std::default_delete 方法释放堆内存。当然,我们也可以自定义符合实际场景的释放规则。值得一提的是,和 shared_ptr 指针不同,为 unique_ptr 自定义释放规则,只能采用函数对象的方式。例如:
//自定义的释放规则struct myDel{void operator()(int *p) {delete p;}};std::unique_ptr<int, myDel> p6(new int);
完整例子:
#include <iostream>#include <memory>int main(){std::unique_ptr<int> p5(new int);*p5 = 10;// p 接收 p5 释放的堆内存int *p = p5.release();std::cout << *p << std::endl; // print: 10// p5 是否为空指针?if (p5) /// 重载了 operator bool() 判断是否为空指针std::cout << "p5 is not a nullptr.\n";elsestd::cout << "p5 is a nullptr.\n"; // print here.std::unique_ptr<int> p6(new int);/// p6 托管 pp6.reset(p);std::cout << *p6 << std::endl; // print: 10return 0;}
补充:
c++ 14 开始,提供了 std::make_unique 方式对 unique_ptr 进行初始化。
#include <iostream>#include <memory>int main(){std::unique_ptr<int> sp1 = std::make_unique<int>(101);/// std::unique_ptr<int> sp2(sp1); // 无法编译通过,因为 unique_ptr 的拷贝构造函数 = delete/// std::unique_ptr<int> sp3;/// sp3 = sp1; // 无法编译通过,因为 unique_ptr 的 = 重载赋值函数 = deletestd::unique_ptr<int> sp4(std::move(sp1)); // 可以通过move语义赋值std::cout << *sp4 << std::endl;std::unique_ptr<int> sp5;sp5 = std::move(sp4); // 可以通过move语义赋值std::cout << *sp5 << std::endl;}
移动构造函数和移动赋值=重载的区别就是参数是 && 引用,以及对传入对象进行清理。如:
TClass(TClass&& rhs){this->member = rhs.member;rhs.member = nullptr;}TClass& operator=(TClass&& rhs){this->member = rhs.member;rhs.member = nullptr;return *this;}
此外,unique_ptr 还可以持有一组堆对象(shared_ptr 和 weak_ptr 也可以):
#include <iostream>#include <memory>int main(){// 创建10个int的堆对象// 形式1std::unique_ptr<int[]> sp1(new int[10]);// 形式2std::unique_ptr<int[]> sp2;sp2.reset(new int[10]);// 形式3std::unique_ptr<int[]> sp3(std::make_unique<int[]>(10));for (int i = 0; i < 10; i++){sp1[i] = i;sp2[i] = i;sp3[i] = i;}for (int i = 0; i < 10; i++){std::cout << sp1[i] << ", " << sp2[i] << ", " << sp3[i] << std::endl;}return 0;}
输出:
0, 0, 01, 1, 12, 2, 23, 3, 34, 4, 45, 5, 56, 6, 67, 7, 78, 8, 89, 9, 9
有时候,只是调用 delete 无法完整的析构被管理的对象,形式为:
std::unique_ptr<T, DeletorFunlction>
例如 Socket 对象,需要在析构之前调用 socket.close() 关闭 socket 才行:
#include <iostream>#include <memory>class Socket{public:Socket() {//...}~Socket() {std::cout << "Destructor.\n";}void Close() {std::cout << "Close Function.\n";}};int main(){auto deletor = [](Socket* pSocket){pSocket->Close();delete pSocket;};std::unique_ptr<Socket, void(*)(Socket* pSocket)> spSocket(new Socket(), deletor);return 0;}
参考:http://c.biancheng.net/view/7918.html
作为 shared_ptr 的一个辅助工具,用于查看 shared_ptr 当前的各种状态(如有多少指向相同的 shared_ptr 指针、shared_ptr 指针指向的堆内存是否已经被释放等等)。
用 weak_ptr 指向一个堆内存,不会令其引用计数发生任何改变,也没有重载 * 和 -> ,因为只能访问,不能修改。
补充:
weak_ptr 一般用作读取 shared_ptr 管理的堆内存的状态进行操作或协助其工作,可以由 shared_ptr 或另外一个 weak_ptr 构造。书中例子是 tcp 功能中,读取 Connection 的状态正常后再发送报文.
例子:
#include <iostream>#include <memory>int main(){std::shared_ptr<int> sp1(new int(10));std::shared_ptr<int> sp2(sp1);std::weak_ptr<int> wp(sp2);// 输出和 wp 同指向的 shared_ptr 类型指针的数量std::cout << wp.use_count() << std::endl; // print: 2// 释放sp2sp2.reset();std::cout << wp.use_count() << std::endl; // print: 1std::cout << wp.expired(); // print: false, 判断所指向的堆内存是否被释放// 借助 lock() 函数,返回一个和 wp 同指向的 shared_ptr 类型指针,获取其存储的数据std::cout << *(wp.lock()) << std::endl; // print: 10return 0;}
目的是让自定义类型也可以使用大括号方式来初始化容器列表(数组什么的)。
例子1:
#include <iostream>#include <vector>#include <string>#include <initializer_list>class MyVec{private:std::vector<int> m_vector;public:MyVec(std::initializer_list<int> list){m_vector.clear();m_vector.insert(m_vector.end(), list.begin(), list.end());}~MyVec(){m_vector.clear();}void Append(std::initializer_list<int> list){m_vector.insert(m_vector.end(), list.begin(), list.end());}std::string ToString() const{std::string ret = "[";for (auto number : m_vector){ret += std::to_string(number) + ", ";}ret += "]";return ret;}};int main(){MyVec obj{ {1, 2, 3} };std::cout << obj.ToString() << std::endl;obj.Append( {4, 5, 6} );std::cout << obj.ToString() << std::endl;return 0;}
例子2:
#include <iostream>#include <string>#include <initializer_list>#include <vector>// 省略大部分类型,只用string和doubleenum class JsonType{jsonTypeNull,jsonTypeString,jsonTypeDouble};struct JsonNode{// value只用string存储,因此要有一个单独的变量存储值的类型;// 根据重载的构造函数版本判断值的类型JsonType m_value_type;std::string m_key;std::string m_value;// String 版本的构造函数JsonNode(const char* key, const char* value) :m_value_type(JsonType::jsonTypeString),m_key(key), m_value(value){std::cout << "[JsonNode <String> constructor]\n";}// Double 版本的构造函数JsonNode(const char* key, const double value) :m_value_type(JsonType::jsonTypeDouble),m_key(key), m_value(std::to_string(value)){std::cout << "[JsonNode <Double> constructor]\n";}// 非常简略的打印函数,Debug用std::string ToString() const{std::string ret = "[\'" + m_key + "\' : ";switch (m_value_type){case JsonType::jsonTypeString:ret += "\'" + m_value + "\'], ";break;case JsonType::jsonTypeDouble:ret += m_value + "], ";break;}return ret;}};class Json{private:std::vector<JsonNode> m_nodes;public:// 传入 std::initializer_list<JsonNode> 参数的构造函数;// 可以使用 ObjName {"Key", <T>Value} 的形式初始化Json(std::initializer_list<JsonNode> nodes){m_nodes.clear();m_nodes.insert(m_nodes.end(), nodes.begin(), nodes.end());}Json() = delete;~Json(){m_nodes.clear();}// 非常简略的打印函数,Debug用std::string ToString() const{std::string ret = "{";for (auto node : m_nodes){ret += node.ToString();}ret += "}";return ret;}};int main(){// 大括号初始化方式Json json{ {"Key1", "StringValue1"}, {"Key2", 10.10} };std::cout << json.ToString() << std::endl;return 0;}
输出:
[JsonNode <String> constructor][JsonNode <Double> constructor]{['Key1' : 'StringValue1'], ['Key2' : 10.100000], }
形式:
auto [a, b, c, ...] = expression;
expression 可以是函数调用、{}表达式或者支持结构化绑定的类型变量。
其目的是为了简化代码,并可以赋予绑定结果一些可读的名字。
如:
#include <iostream>#include <map>struct Point{double x;double y;Point() = default;Point(const double _x, const double _y): x(_x), y(_y){}};struct Position{int index;Point pos;Position() = default;Position(const int _idx, const Point _pt): index(_idx), pos(_pt){}};int main(){// 1int my_array[3] = { 1, 2, 3 };auto& [_1st, _2nd, _3rd] = my_array;std::cout << _2nd << std::endl; // 2// 2Position pos1{ 1, {10, 12} };// 觉得应该有 [path_id, [x, y]] 的形式,但没有成功const auto& [path_idx, position] = pos1;std::cout << position.y << std::endl; // 12// 3std::map<std::string, int> city_map;city_map["BJ"] = 10;city_map["JL"] = 432;for (const auto& [city_name, area_code] : city_map){std::cout << area_code << " : " << city_name << std::endl;}// 10 : BJ// 432 : JLreturn 0;}
使用 auto& 可以减少不必要的拷贝,如果不修改被绑定对象的值,可以用 const auto&。
std::thread 对象在线程函数运行期间必须有效。可以用detach方法让线程对象和线程函数分离,但就无法用线程对象去管理线程了。