[关闭]
@Pigmon 2021-11-24T14:45:55.000000Z 字数 11949 阅读 666

C++11 笔记

C++


Old QQ: Little_Xin_1026
New: LittleXin_1026

1. Lambda

基本形式:

  1. std::function<返回类型(参数表)> lamda_name = [scope](参数表){ 函数体 };

目的:可以执行函数功能的封装,又能当做变量来传递。Scope说明见1.2。

1.1 基本用法举例

  1. class Graphic
  2. {
  3. public:
  4. void draw()
  5. {
  6. whatToDraw();
  7. }
  8. std::function<void()> whatToDraw; // (1) 定义一个函数类型变量,可以用Lamda为它赋值。
  9. };
  10. class ofApp : ofBaseApp
  11. {
  12. protected:
  13. vector<Graphic> graphicElements;
  14. };

在cpp中,给不同图形赋予不同的绘制过程,可以通过Lamda简洁的实现。

  1. void ofApp::setup()
  2. {
  3. Graphic circle;
  4. // (1)
  5. circle.whatToDraw = []()
  6. {
  7. ofDrawCircle(50,50,20);
  8. };
  9. this->graphicElements.push_back(circle);
  10. Graphic rectangle;
  11. // (1)
  12. rectangle.whatToDraw = []()
  13. {
  14. ofRect(50,50,200,200);
  15. };
  16. this->graphicElements.push_back(rectangle);
  17. }
  18. void ofApp::draw()
  19. {
  20. // 多态性的一种实现
  21. for(auto & graphic : this->graphicElements)
  22. {
  23. graphic.draw();
  24. }
  25. }

1.2 Scope 常见用法

用法1,直接把需要传入的变量放入[]

  1. float multiplier = 4.0f;
  2. auto multiplyByOurMultiplier = [multiplier](float x)
  3. {
  4. return x * multiplier;
  5. };
  6. std::cout << multiplyByOurMultiplier(2.0f) << std::endl; // prints 8

用法2,传变量引用

  1. float multiplier = 4.0f;
  2. float plus = 2.0f;
  3. auto multiplyByOurMultiplier = [&multiplier, plus](float x)
  4. {
  5. multiplier *= 2.0f;
  6. return x * multiplier + plus;
  7. };
  8. std::cout << multiplyByOurMultiplier(2.0f) << std::endl; // prints 18

用法3,[=]可以传入任何可见且使用到的变量

  1. float multiplier = 4.0f;
  2. float plus = 2.0f;
  3. auto multiplyByOurMultiplier = [=](float x)
  4. {
  5. return x * multiplier + plus;
  6. };
  7. std::cout << multiplyByOurMultiplier(2.0f) << std::endl; // prints 10

用法4,[&]将使用到的,且可见的变量都以引用的方式传入

  1. float multiplier = 4.0f;
  2. float plus = 2.0f;
  3. auto multiplyByOurMultiplier = [&](float x)
  4. {
  5. multiplier *= 2.0f;
  6. plus += 1.0f;
  7. return x * multiplier + plus;
  8. };
  9. std::cout << multiplyByOurMultiplier(2.0f) << std::endl; // prints 19

用法5,[this]对象作用域内可以传入

  1. // 类定义 Test.h
  2. #include <functional>
  3. class Test
  4. {
  5. public:
  6. float a, b, c;
  7. Test(float _a, float _b, float _c) : a(_a), b(_b), c(_c) {}
  8. std::function<float(void)> average = [this]() { return (a + b + c) / 3.0f; };
  9. };
  1. // 调用 main.cpp
  2. #include <iostream>
  3. #include "Test.h"
  4. int main()
  5. {
  6. Test obj(1.0f, 2.0f, 3.0f);
  7. std::cout << obj.average() << std::endl; // prints 2
  8. return 0;
  9. }

2. 宏

‘#’把宏参数直接变成字符串,想不出有什么用处:

  1. #define TO_STRING(x) #x
  2. const char* str = TO_STRING(3.14);

‘##’用于连接左右的内容:

  1. #define TAG(x) ID_##x
  2. std::cout << TO_STRING(TAG(211)) << std::endl; // prints: ID_211。这不就用到了?

__VA_ARGS__关键字用于宏定义中的可变参数列表

  1. #define LOG(...) printf(__VA_ARGS__)
  2. #define _LOG_(fmt, ...) printf(fmt, ##__VA_ARGS__)
  3. const char* name = "Tom";
  4. const int age = 23;
  5. LOG("My Name is %s.\n", name); // prints: My Name is Tom.
  6. _LOG_("I am %d years old.\n", age); // prints: I am 23 years old.

编译时输出宏的内容,用于检查错误

  1. #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* 时间

取消某个宏的定义

  1. #undef MACRO_NAME

减少啰嗦程序,这个不知道为什么用#pragma message打印不出来

  1. #define PROPERTY(Name) \
  2. int m_##Name; \
  3. int Get##Name() const { \
  4. return m_##Name; \
  5. } \
  6. void Set##Name(int _##Name) { \
  7. m_##Name = _##Name; \
  8. }
  9. class Student
  10. {
  11. public:
  12. PROPERTY(Age);
  13. };
  14. Student obj;
  15. obj.SetAge(23);
  16. std::cout << obj.GetAge() << std::endl; // prints: 23

常见的do{}while(0), 目的是防止上下文中不符合编译条件的情况,如在switch语句中不能定义变量,在if else的大括号后面不能有;等。

  1. #define SWAP_INT(x, y) do {int tmp = x; x = y; y = tmp;} while (0)
  2. int x = 1, y = 2;
  3. if (x < y)
  4. SWAP_INT(x, y);
  5. else
  6. printf("It's OK.\n");
  7. std::cout << "x = " << x << ", y = " << y << std::endl; // prints: x = 2, y = 1

3. 右值与 Move 语义

3.1 基本定义

左值:非临时对象,可以在多个语句中使用的值,比如一般的变量;
右值:临时对象。

如:

  1. int a = 0; // a 是左值,0是右值

右值一般情况下不会被改变,但并非不可以,如:

  1. T().Get().Set(2);

3.2 基本形式

右值引用是以2个&符号标志的。

  1. void process_value(int& _n)
  2. {
  3. std::cout << "Process LValue of " << _n << std::endl;
  4. }
  5. void process_value(int&& _n)
  6. {
  7. std::cout << "Process RValue of " << _n << std::endl;
  8. }
  9. void forward_value(int&& _n)
  10. {
  11. process_value();
  12. }
  13. void main()
  14. {
  15. int a = 0;
  16. process_value(a); // print: Process LValue of 0.
  17. process_value(1); // print: Process RValue of 1.
  18. forward_value(2); // print: Process LValue of 2. 因为临时对象经过传递会变成左值
  19. }

3.3 Move 语义

Move 语义相比 Copy 语义,减少临时对象的创建和维护,节省时间和空间。可以在类中定义 Move 语义的 Move 构造函数和 Move 赋值函数,以移动而非深拷贝的方式初始化含有指针成员的类对象,在将右值传递给构造函数或赋值函数时触发自动调用。
Move 语义的构造函数基本形式如下:

  1. T(T&& _obj)
  2. {
  3. // 浅拷贝的方式赋值;
  4. // 对_obj的包含指针进行重置,避免资源被释放;
  5. }

例如:

  1. MyString(MyString&& _other)
  2. {
  3. _len = _other._len;
  4. _data = _other._data;
  5. _other._len = 0; // 这句没必要吧?
  6. _other._data = NULL;
  7. }

Move 赋值操作符重载

  1. MyString& operator=(MyString&& _other)
  2. {
  3. if (this != &_other)
  4. {
  5. _len = _other._len;
  6. _data = _other._data;
  7. _other._len = 0;
  8. _other._data = NULL;
  9. }
  10. return *this;
  11. }

3.4 std::move

std::move 可以将一个左值引用转换成右值引用来使用,例如:

  1. void process_value(int& _n)
  2. {
  3. std::cout << "Process LValue of " << _n << std::endl;
  4. }
  5. void process_value(int&& _n)
  6. {
  7. std::cout << "Process RValue of " << _n << std::endl;
  8. }
  9. void main()
  10. {
  11. int a = 0;
  12. process_value(a); // print: Process LValue of 0
  13. process_value(std::move(a)); // print: Process RValue of 0
  14. }

std::move 可提高swap操作的效率:

  1. void swap<T>(T& a, T& b)
  2. {
  3. T tmp(std::move(a));
  4. a = std::move(b);
  5. b = std::move(tmp);
  6. }

避免3次拷贝。

3.5 精确传递 (Perfect Forwarding)

指在值的传递过程中,除了值不变以外,属性(左右值 / const 等)也保持不变。通过右值引用传递参数,可以保证属性不变。即T&&的推导规则为:右值实参为右值引用,左值实参仍然为左值引用
其应用场景例如在需要保持const属性的函数中,如果不使用右值引用,则需要重载:

  1. void process_value(const T& _value);
  2. void process_value(T& _value);

这种情况,如果参数很多就需要重载很多的函数。而使用右值引用则不需要进行重载:

  1. void process_value(T&& _value);

4. 智能指针

64位系统下,unique_ptr 占 8 字节,其余2个 16 字节。

4.1 shared_ptr

智能指针,用于托管由 new 操作符申请的指针对象。维护引用计数,当计数为0时,自动对被托管指针对象执行 delete 操作。
基本形式:

  1. #include <memory>
  2. std::shared_ptr<T> var_name(new T);

多个shared_ptr对同一对象托管,以及reset和计数清0的例子:

  1. #include <iostream>
  2. #include <memory>
  3. class A
  4. {
  5. private:
  6. int m_i;
  7. public:
  8. A(int n) : m_i(n) {}
  9. ~A() { std::cout << m_i << " dstructed.\n"; }
  10. void Print() { std::cout << m_i << std::endl; }
  11. };
  12. int main()
  13. {
  14. /// sp1, sp2, sp3 都托管 A(2)
  15. std::shared_ptr<A> sp1(new A(2));
  16. std::shared_ptr<A> sp2(sp1);
  17. std::shared_ptr<A> sp3;
  18. sp3 = sp2;
  19. std::cout << sp1->Print() << ", " << sp2->Print() << ", " << sp3->Prrint() << std::endl;
  20. /// get 操作
  21. A* ptr = sp3.get();
  22. std::cout << ptr->Print() << std::endl;
  23. /// reset 会将 share_ptr 的托管对象重置,同时将原托管对象的引用计数 -1
  24. sp1.reset(new A(3));
  25. sp2.reset(new A(4));
  26. sp3.reset(new A(5));
  27. /// 这里因为 A(2) 的引用计数已经是0,所以会自动调用 delete 操作。
  28. std::cout << sp1->Print() << ", " << sp2->Print() << ", " << sp3->Prrint() << std::endl;
  29. std::cout << "end.\n";
  30. }

输出:

  1. 2, 2, 2
  2. 2
  3. 2 dstructed.
  4. 3, 4, 5
  5. end.
  6. 5 dstructed.
  7. 4 dstructed.
  8. 3 dstructed.

注意事项:
shared_ptr 不要托管非 new 操作申请空间的指针对象,虽然编译可以通过,但执行会产生错误(当自动调用delete)。
不能用下面的方式使得两个 shared_ptr 对象托管同一个指针:

  1. A *ptr = new A(10);
  2. std::shared_ptr<A> sp1(ptr), sp2(ptr);

sp1 和 sp2 并不会共享同一个对 p 的托管计数,而是各自将对 p 的托管计数都记为 1(sp2 无法知道 p 已经被 sp1 托管过)。这样,当 sp1 消亡时要析构 p,sp2 消亡时要再次析构 p,这会导致程序崩溃。

补充:
初始化方法:

  1. std::shared_ptr<T> sp1(std::make_shared<T>(...));

4.2 unique_ptr

参考:http://c.biancheng.net/view/7909.html
特殊的 shared_ptr, 区别是其指向的堆内存无法与其他 unique_ptr 共享。
例子:

  1. std::unique_ptr<int> p4(new int);
  2. std::unique_ptr<int> p5(p4);//错误,堆内存不共享
  3. std::unique_ptr<int> p5(std::move(p4));//正确,调用移动构造函数
  4. /// p5 获得了 p4 所指向的堆内存空间的所有权,p4变成 nullptr。

默认情况下,unique_ptr 指针采用 std::default_delete 方法释放堆内存。当然,我们也可以自定义符合实际场景的释放规则。值得一提的是,和 shared_ptr 指针不同,为 unique_ptr 自定义释放规则,只能采用函数对象的方式。例如:

  1. //自定义的释放规则
  2. struct myDel
  3. {
  4. void operator()(int *p) {
  5. delete p;
  6. }
  7. };
  8. std::unique_ptr<int, myDel> p6(new int);

完整例子:

  1. #include <iostream>
  2. #include <memory>
  3. int main()
  4. {
  5. std::unique_ptr<int> p5(new int);
  6. *p5 = 10;
  7. // p 接收 p5 释放的堆内存
  8. int *p = p5.release();
  9. std::cout << *p << std::endl; // print: 10
  10. // p5 是否为空指针?
  11. if (p5) /// 重载了 operator bool() 判断是否为空指针
  12. std::cout << "p5 is not a nullptr.\n";
  13. else
  14. std::cout << "p5 is a nullptr.\n"; // print here.
  15. std::unique_ptr<int> p6(new int);
  16. /// p6 托管 p
  17. p6.reset(p);
  18. std::cout << *p6 << std::endl; // print: 10
  19. return 0;
  20. }

补充:
c++ 14 开始,提供了 std::make_unique 方式对 unique_ptr 进行初始化。

  1. #include <iostream>
  2. #include <memory>
  3. int main()
  4. {
  5. std::unique_ptr<int> sp1 = std::make_unique<int>(101);
  6. /// std::unique_ptr<int> sp2(sp1); // 无法编译通过,因为 unique_ptr 的拷贝构造函数 = delete
  7. /// std::unique_ptr<int> sp3;
  8. /// sp3 = sp1; // 无法编译通过,因为 unique_ptr 的 = 重载赋值函数 = delete
  9. std::unique_ptr<int> sp4(std::move(sp1)); // 可以通过move语义赋值
  10. std::cout << *sp4 << std::endl;
  11. std::unique_ptr<int> sp5;
  12. sp5 = std::move(sp4); // 可以通过move语义赋值
  13. std::cout << *sp5 << std::endl;
  14. }

移动构造函数和移动赋值=重载的区别就是参数是 && 引用,以及对传入对象进行清理。如:

  1. TClass(TClass&& rhs)
  2. {
  3. this->member = rhs.member;
  4. rhs.member = nullptr;
  5. }
  6. TClass& operator=(TClass&& rhs)
  7. {
  8. this->member = rhs.member;
  9. rhs.member = nullptr;
  10. return *this;
  11. }

此外,unique_ptr 还可以持有一组堆对象(shared_ptr 和 weak_ptr 也可以):

  1. #include <iostream>
  2. #include <memory>
  3. int main()
  4. {
  5. // 创建10个int的堆对象
  6. // 形式1
  7. std::unique_ptr<int[]> sp1(new int[10]);
  8. // 形式2
  9. std::unique_ptr<int[]> sp2;
  10. sp2.reset(new int[10]);
  11. // 形式3
  12. std::unique_ptr<int[]> sp3(std::make_unique<int[]>(10));
  13. for (int i = 0; i < 10; i++)
  14. {
  15. sp1[i] = i;
  16. sp2[i] = i;
  17. sp3[i] = i;
  18. }
  19. for (int i = 0; i < 10; i++)
  20. {
  21. std::cout << sp1[i] << ", " << sp2[i] << ", " << sp3[i] << std::endl;
  22. }
  23. return 0;
  24. }

输出:

  1. 0, 0, 0
  2. 1, 1, 1
  3. 2, 2, 2
  4. 3, 3, 3
  5. 4, 4, 4
  6. 5, 5, 5
  7. 6, 6, 6
  8. 7, 7, 7
  9. 8, 8, 8
  10. 9, 9, 9

有时候,只是调用 delete 无法完整的析构被管理的对象,形式为:

  1. std::unique_ptr<T, DeletorFunlction>

例如 Socket 对象,需要在析构之前调用 socket.close() 关闭 socket 才行:

  1. #include <iostream>
  2. #include <memory>
  3. class Socket
  4. {
  5. public:
  6. Socket() {
  7. //...
  8. }
  9. ~Socket() {
  10. std::cout << "Destructor.\n";
  11. }
  12. void Close() {
  13. std::cout << "Close Function.\n";
  14. }
  15. };
  16. int main()
  17. {
  18. auto deletor = [](Socket* pSocket)
  19. {
  20. pSocket->Close();
  21. delete pSocket;
  22. };
  23. std::unique_ptr<Socket, void(*)(Socket* pSocket)> spSocket(new Socket(), deletor);
  24. return 0;
  25. }

4.3 weak_ptr

参考: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 的状态正常后再发送报文.

例子:

  1. #include <iostream>
  2. #include <memory>
  3. int main()
  4. {
  5. std::shared_ptr<int> sp1(new int(10));
  6. std::shared_ptr<int> sp2(sp1);
  7. std::weak_ptr<int> wp(sp2);
  8. // 输出和 wp 同指向的 shared_ptr 类型指针的数量
  9. std::cout << wp.use_count() << std::endl; // print: 2
  10. // 释放sp2
  11. sp2.reset();
  12. std::cout << wp.use_count() << std::endl; // print: 1
  13. std::cout << wp.expired(); // print: false, 判断所指向的堆内存是否被释放
  14. // 借助 lock() 函数,返回一个和 wp 同指向的 shared_ptr 类型指针,获取其存储的数据
  15. std::cout << *(wp.lock()) << std::endl; // print: 10
  16. return 0;
  17. }

5. std::initializer_list

目的是让自定义类型也可以使用大括号方式来初始化容器列表(数组什么的)。

例子1:

  1. #include <iostream>
  2. #include <vector>
  3. #include <string>
  4. #include <initializer_list>
  5. class MyVec
  6. {
  7. private:
  8. std::vector<int> m_vector;
  9. public:
  10. MyVec(std::initializer_list<int> list)
  11. {
  12. m_vector.clear();
  13. m_vector.insert(m_vector.end(), list.begin(), list.end());
  14. }
  15. ~MyVec()
  16. {
  17. m_vector.clear();
  18. }
  19. void Append(std::initializer_list<int> list)
  20. {
  21. m_vector.insert(m_vector.end(), list.begin(), list.end());
  22. }
  23. std::string ToString() const
  24. {
  25. std::string ret = "[";
  26. for (auto number : m_vector)
  27. {
  28. ret += std::to_string(number) + ", ";
  29. }
  30. ret += "]";
  31. return ret;
  32. }
  33. };
  34. int main()
  35. {
  36. MyVec obj{ {1, 2, 3} };
  37. std::cout << obj.ToString() << std::endl;
  38. obj.Append( {4, 5, 6} );
  39. std::cout << obj.ToString() << std::endl;
  40. return 0;
  41. }

例子2:

  1. #include <iostream>
  2. #include <string>
  3. #include <initializer_list>
  4. #include <vector>
  5. // 省略大部分类型,只用string和double
  6. enum class JsonType
  7. {
  8. jsonTypeNull,
  9. jsonTypeString,
  10. jsonTypeDouble
  11. };
  12. struct JsonNode
  13. {
  14. // value只用string存储,因此要有一个单独的变量存储值的类型;
  15. // 根据重载的构造函数版本判断值的类型
  16. JsonType m_value_type;
  17. std::string m_key;
  18. std::string m_value;
  19. // String 版本的构造函数
  20. JsonNode(const char* key, const char* value) :
  21. m_value_type(JsonType::jsonTypeString),
  22. m_key(key), m_value(value)
  23. {
  24. std::cout << "[JsonNode <String> constructor]\n";
  25. }
  26. // Double 版本的构造函数
  27. JsonNode(const char* key, const double value) :
  28. m_value_type(JsonType::jsonTypeDouble),
  29. m_key(key), m_value(std::to_string(value))
  30. {
  31. std::cout << "[JsonNode <Double> constructor]\n";
  32. }
  33. // 非常简略的打印函数,Debug用
  34. std::string ToString() const
  35. {
  36. std::string ret = "[\'" + m_key + "\' : ";
  37. switch (m_value_type)
  38. {
  39. case JsonType::jsonTypeString:
  40. ret += "\'" + m_value + "\'], ";
  41. break;
  42. case JsonType::jsonTypeDouble:
  43. ret += m_value + "], ";
  44. break;
  45. }
  46. return ret;
  47. }
  48. };
  49. class Json
  50. {
  51. private:
  52. std::vector<JsonNode> m_nodes;
  53. public:
  54. // 传入 std::initializer_list<JsonNode> 参数的构造函数;
  55. // 可以使用 ObjName {"Key", <T>Value} 的形式初始化
  56. Json(std::initializer_list<JsonNode> nodes)
  57. {
  58. m_nodes.clear();
  59. m_nodes.insert(m_nodes.end(), nodes.begin(), nodes.end());
  60. }
  61. Json() = delete;
  62. ~Json()
  63. {
  64. m_nodes.clear();
  65. }
  66. // 非常简略的打印函数,Debug用
  67. std::string ToString() const
  68. {
  69. std::string ret = "{";
  70. for (auto node : m_nodes)
  71. {
  72. ret += node.ToString();
  73. }
  74. ret += "}";
  75. return ret;
  76. }
  77. };
  78. int main()
  79. {
  80. // 大括号初始化方式
  81. Json json{ {"Key1", "StringValue1"}, {"Key2", 10.10} };
  82. std::cout << json.ToString() << std::endl;
  83. return 0;
  84. }

输出:

  1. [JsonNode <String> constructor]
  2. [JsonNode <Double> constructor]
  3. {['Key1' : 'StringValue1'], ['Key2' : 10.100000], }

6. 结构化绑定

形式:

  1. auto [a, b, c, ...] = expression;

expression 可以是函数调用、{}表达式或者支持结构化绑定的类型变量。
其目的是为了简化代码,并可以赋予绑定结果一些可读的名字。
如:

  1. #include <iostream>
  2. #include <map>
  3. struct Point
  4. {
  5. double x;
  6. double y;
  7. Point() = default;
  8. Point(const double _x, const double _y)
  9. : x(_x), y(_y)
  10. {
  11. }
  12. };
  13. struct Position
  14. {
  15. int index;
  16. Point pos;
  17. Position() = default;
  18. Position(const int _idx, const Point _pt)
  19. : index(_idx), pos(_pt)
  20. {
  21. }
  22. };
  23. int main()
  24. {
  25. // 1
  26. int my_array[3] = { 1, 2, 3 };
  27. auto& [_1st, _2nd, _3rd] = my_array;
  28. std::cout << _2nd << std::endl; // 2
  29. // 2
  30. Position pos1{ 1, {10, 12} };
  31. // 觉得应该有 [path_id, [x, y]] 的形式,但没有成功
  32. const auto& [path_idx, position] = pos1;
  33. std::cout << position.y << std::endl; // 12
  34. // 3
  35. std::map<std::string, int> city_map;
  36. city_map["BJ"] = 10;
  37. city_map["JL"] = 432;
  38. for (const auto& [city_name, area_code] : city_map)
  39. {
  40. std::cout << area_code << " : " << city_name << std::endl;
  41. }
  42. // 10 : BJ
  43. // 432 : JL
  44. return 0;
  45. }

使用 auto& 可以减少不必要的拷贝,如果不修改被绑定对象的值,可以用 const auto&。

7. std::thread

std::thread 对象在线程函数运行期间必须有效。可以用detach方法让线程对象和线程函数分离,但就无法用线程对象去管理线程了。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注