@Pigmon
2021-11-24T14:45:55.000000Z
字数 11949
阅读 643
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 2
return 0;
}
‘#’把宏参数直接变成字符串,想不出有什么用处:
#define TO_STRING(x) #x
const char* str = TO_STRING(3.14);
‘##’用于连接左右的内容:
#define TAG(x) ID_##x
std::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);
else
printf("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 0
process_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 的托管对象重置,同时将原托管对象的引用计数 -1
sp1.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, 2
2
2 dstructed.
3, 4, 5
end.
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";
else
std::cout << "p5 is a nullptr.\n"; // print here.
std::unique_ptr<int> p6(new int);
/// p6 托管 p
p6.reset(p);
std::cout << *p6 << std::endl; // print: 10
return 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 的 = 重载赋值函数 = delete
std::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的堆对象
// 形式1
std::unique_ptr<int[]> sp1(new int[10]);
// 形式2
std::unique_ptr<int[]> sp2;
sp2.reset(new int[10]);
// 形式3
std::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, 0
1, 1, 1
2, 2, 2
3, 3, 3
4, 4, 4
5, 5, 5
6, 6, 6
7, 7, 7
8, 8, 8
9, 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
// 释放sp2
sp2.reset();
std::cout << wp.use_count() << std::endl; // print: 1
std::cout << wp.expired(); // print: false, 判断所指向的堆内存是否被释放
// 借助 lock() 函数,返回一个和 wp 同指向的 shared_ptr 类型指针,获取其存储的数据
std::cout << *(wp.lock()) << std::endl; // print: 10
return 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和double
enum 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()
{
// 1
int my_array[3] = { 1, 2, 3 };
auto& [_1st, _2nd, _3rd] = my_array;
std::cout << _2nd << std::endl; // 2
// 2
Position pos1{ 1, {10, 12} };
// 觉得应该有 [path_id, [x, y]] 的形式,但没有成功
const auto& [path_idx, position] = pos1;
std::cout << position.y << std::endl; // 12
// 3
std::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 : JL
return 0;
}
使用 auto& 可以减少不必要的拷贝,如果不修改被绑定对象的值,可以用 const auto&。
std::thread 对象在线程函数运行期间必须有效。可以用detach方法让线程对象和线程函数分离,但就无法用线程对象去管理线程了。