You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

Tool_tutorial.md 33 kB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012
  1. # 工具使用合集
  2. [toc]
  3. ## Visual Studio 使用说明
  4. 比赛**只保证!!!支持** VS 2022 最新版本,选手使用其他版本后果自负(实际上应该不能编译)。
  5. ### 生成模式的设置
  6. 菜单栏下方一行
  7. ![image-20230416010705076](https://raw.githubusercontent.com/shangfengh/THUAI6/new/resource/image-20230416010705076.png)
  8. 可以更改生成模式为 `Debug` 或 `Release`
  9. ### 命令行参数的设置
  10. 左上方菜单栏 `调试->调试属性`
  11. ![image-20230416010816392](https://raw.githubusercontent.com/shangfengh/THUAI6/new/resource/image-20230416010816392.png)
  12. 在命令参数一栏中加入命令行参数进行调试
  13. ### cmd 脚本的参数修改
  14. 右键点击 `.cmd` 或 `.bat` 文件之后,选择编辑就可以开始修改文件。通过在一行的开头加上 `::`,可以注释掉该行。
  15. ## C++ 接口必看
  16. **在此鸣谢\xfgg/\xfgg/\xfgg/,看到这里的选手可以到选手群膜一膜!!! **
  17. 除非特殊指明,以下代码均在 MSVC 19.28.29913 x64 `/std:c++17` 与 GCC 10.2 x86_64-linux-gnu `-std=c++17` 两个平台下通过。
  18. 由于我们的比赛最终会运行在 Linux 平台上,因此程设课上学到的一些只适用于 Windows 的 C++ 操作很可能并不能正确执行。此外,代码中使用了大量 Modern C++ 中的新特性,可能会使选手在编程过程中遇到较大困难。因此,此处介绍一些比赛中使用 C++ 接口必须了解的知识。
  19. ### 计时相关
  20. 编写代码过程中,我们可能需要获取系统时间等一系列操作,C++ 标准库提供了这样的行为。尤其注意**不要**使用 Windows 平台上的 `GetTickCount` 或者 `GetTickCount64` !!!应当使用 `std::chrono`
  21. 头文件:`#include <chrono>`
  22. 可以用于获取时间戳,从而用于计时、例如计算某个操作花费的时间,或者协调队友间的合作。
  23. ```c++
  24. #include <iostream>
  25. #include <chrono>
  26. int main()
  27. {
  28. auto sec = std::chrono::duration_cast<std::chrono::seconds>(
  29. std::chrono::system_clock::now().time_since_epoch()).count();
  30. auto msec = std::chrono::duration_cast<std::chrono::milliseconds>(
  31. std::chrono::system_clock::now().time_since_epoch()).count();
  32. std::cout << "从 1970 年元旦到现在的:秒数" << sec << ";毫秒数:" <<
  33. msec << std::endl;
  34. return 0;
  35. }
  36. ```
  37. ### 线程睡眠
  38. 由于移动过程中会阻塞人物角色,因此玩家可能要在移动后让线程休眠一段时间,直到移动结束。C++ 标准库中使线程休眠需要包含头文件:`#include <thread>`。示例用法:
  39. 我们推荐小步移动,不太建议玩家使用线程睡眠超过一帧
  40. ```cpp
  41. std::this_thread::sleep_for(std::chrono::milliseconds(20)); // 休眠 20 毫秒
  42. std::this_thread::sleep_for(std::chrono::seconds(2)); // 休眠 2 秒
  43. // 下面这个也能休眠 200 毫秒
  44. std::this_thread::sleep_until(std::chrono::system_clock::now()
  45. += std::chrono::milliseconds(200));
  46. ```
  47. 休眠过程中,线程将被阻塞,而不继续进行,直到休眠时间结束方继续向下执行。
  48. ### 异步接口的使用
  49. 本届比赛中,我们可能会看到类似 `std::future<bool>` 这样类型的接口返回值,这实际上是一个异步接口。在调用同步接口后,在接口内的函数未执行完之前,线程通常会阻塞住;但是异步接口的调用通常不会阻塞当前线程,而是会另外开启一个线程进行操作,当前线程则继续向下执行。当调用 `get()` 方法时,将返回异步接口的值,若此时异步接口内的函数依然未执行完,则会阻塞当前线程。
  50. 如果不需要返回值或没有返回值,但是希望接口内的函数执行完之后再进行下一步,即将接口当做常规的同步接口来调用,也可以调用 `wait()` 方法。
  51. ```c++
  52. #include <iostream>
  53. #include <thread>
  54. #include <future>
  55. #include <chrono>
  56. int f_sync()
  57. {
  58. std::this_thread::sleep_for(std::chrono::seconds(1));
  59. return 8;
  60. }
  61. std::future<int> f_async()
  62. {
  63. return std::async(std::launch::async, []()
  64. { std::this_thread::sleep_for(std::chrono::seconds(1));
  65. return 8; });
  66. }
  67. int main()
  68. {
  69. auto start = std::chrono::system_clock::now();
  70. std::cout << std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
  71. std::chrono::system_clock::now() - start).count() << std::endl;
  72. auto x = f_async();
  73. std::cout << std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
  74. std::chrono::system_clock::now() - start).count() << std::endl;
  75. std::cout << x.get() << std::endl;
  76. std::cout << std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
  77. std::chrono::system_clock::now() - start).count() << std::endl;
  78. auto y = f_sync();
  79. std::cout << std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
  80. std::chrono::system_clock::now() - start).count() << std::endl;
  81. std::cout << y << std::endl;
  82. std::cout << std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
  83. std::chrono::system_clock::now() - start).count() << std::endl;
  84. }
  85. ```
  86. ### `auto` 类型推导
  87. C++11开始支持使用 `auto` 自动推导变量类型,废除了原有的作为 storage-class-specifier 的作用:
  88. ```c++
  89. int i = 4;
  90. auto x = i; // auto 被推导为 int,x 是 int 类型
  91. auto& y = i; // auto 仍被推导为 int,y 是 int& 类型
  92. auto&& z = i; // auto 被推导为 int&,z 是 int&&&,被折叠为 int&,即 z 与 y 同类型
  93. auto&& w = 4; // auto 被推导为 int,w 是 int&& 类型
  94. ```
  95. ### STL相关
  96. #### `std::vector`
  97. 头文件:`#include <vector>`,类似于可变长的数组,支持下标运算符 `[]` 访问其元素,此时与 C 风格数组用法相似。支持 `size` 成员函数获取其中的元素数量。
  98. 创建一个 `int` 型的 `vector` 对象:
  99. ```cpp
  100. std::vector<int> v { 9, 1, 2, 3, 4 }; // 初始化 vector 有五个元素,v[0] = 9, ...
  101. v.emplace_back(10); // 向 v 尾部添加一个元素,该元素饿构造函数的参数为 10(对于 int,只有一个语法意义上的构造函数,无真正的构造函数),即现在 v 有六个元素,v[5] 的值是 10
  102. v.pop_back(); // 把最后一个元素删除,现在 v 还是 { 9, 1, 2, 3, 4 }
  103. ```
  104. 遍历其中所有元素的方式:
  105. ```cpp
  106. // std::vector<int> v;
  107. for (int i = 0; i < (int)v.size(); ++i)
  108. {
  109. /*可以通过 v[i] 对其进行访问*/
  110. }
  111. for (auto itr = v.begin(); itr != v.end(); ++itr)
  112. {
  113. /*
  114. * itr 作为迭代器,可以通过其访问 vector 中的元素。其用法与指针几乎完全相同。
  115. * 可以通过 *itr 得到元素;以及 itr-> 的用法也是支持的
  116. * 实际上它内部就是封装了指向 vector 中元素的指针
  117. * 此外还有 v.cbegin()、v.rbegin()、v.crbegin() 等
  118. * v.begin()、v.end() 也可写为 begin(v)、end(v)
  119. */
  120. }
  121. for (auto&& elem : v)
  122. {
  123. /*
  124. * elem 即是 v 中每个元素的引用,也可写成 auto& elem : v
  125. * 它完全等价于:
  126. * {
  127. * auto&& __range = v;
  128. * auto&& __begin = begin(v);
  129. * auto&& __end = end(v);
  130. * for (; __begin != __end; ++__begin)
  131. * {
  132. * auto&& elem = *__begin;
  133. * // Some code
  134. * }
  135. * }
  136. */
  137. }
  138. ```
  139. 例如:
  140. ```cpp
  141. for (auto elem&& : v) { std::cout << elem << ' '; }
  142. std::cout << std::endl;
  143. ```
  144. 作为 STL 的容器之一,其具有容器的通用接口。但是由于这比较复杂,在此难以一一展开。有兴趣的同学可以在下方提供的链接里进行查阅。
  145. **注:请千万不要试图使用 `std::vector<bool>`,若需使用,请用 `std::vector<char>` 代替!**
  146. 更多用法参见(点击进入):[cppreference_vector](https://zh.cppreference.com/w/cpp/container/vector)
  147. #### std::array
  148. 头文件:`#include <array>`,C 风格数组的类封装版本。
  149. 用法与 C 风格的数组是基本相似的,例如:
  150. ```cpp
  151. std::array<double, 5> arr { 9.0, 8.0, 7.0, 6.0, 5.0 };
  152. std::cout << arr[2] << std::endl; // 输出 7.0
  153. ```
  154. 同时也支持各种容器操作:
  155. ```cpp
  156. double sum = 0.0;
  157. for (auto itr = begin(arr); itr != end(arr); ++itr)
  158. {
  159. sum += *itr;
  160. }
  161. // sum 结果是 35
  162. ```
  163. 更多用法参见(点击进入):[cppreference_array](https://zh.cppreference.com/w/cpp/container/array)。
  164. ### fmt 库的使用
  165. 选手包中内置了 `fmt` 库,因此选手可以通过 `fmt` 库来格式化字符串,而无须自己手动格式化。
  166. ```c++
  167. std::string str_std = "number: " + std::to_string(1) + teststr;
  168. std::string str_fmt = fmt::format("number: {}{}", 1, teststr); // 两种方法等价
  169. ```
  170. `fmt`库还支持更多强大的操作,例如更精确的格式化、更简便的转化等。
  171. 更多用法参见(点击进入):[fmt_index](https://fmt.dev/latest/index.html)。
  172. ## Python 接口必看
  173. 比赛**只保证!!**支持 Python 3.9,不保证支持其他版本
  174. 比赛中的 Python 接口大多使用异步接口,即返回一个类似于 `Future[bool]` 的值。为了获取实际的值,需要调用 `result()` 方法。
  175. ```python
  176. from concurrent.futures import Future, ThreadPoolExecutor
  177. import time
  178. class Cls:
  179. def __init__(self):
  180. self.__pool: ThreadPoolExecutor = ThreadPoolExecutor(10)
  181. def Test(self, a: int, b: int) -> Future[int]:
  182. def test():
  183. time.sleep(0.5)
  184. return a + b
  185. return self.__pool.submit(test)
  186. if __name__ == '__main__':
  187. f1 = Cls().Test(1, 2)
  188. print(time.time())
  189. print(f1.result())
  190. print(time.time())
  191. ```
  192. ## C++ 相关小知识
  193. ### lambda 表达式
  194. #### lambda 表达式概述
  195. lambda 表达式是 C++ 发展史上的一个重大事件,也是 C++ 支持函数式编程的重要一环。可以说,lambda 表达式不仅给 C++ 程序员带来了极大的便利,也开创了 C++ 的一个崭新的编程范式。但是同时 lambda 表达式也带来了诸多的语法难题,使用容易,但精通极难。
  196. lambda 表达式确实是一个非常有用的语法特性。至少个人在学了 lambda 表达式之后,编写 C++ 代码就再也没有离开过。因为,它真的是非常的方便与易用。
  197. lambda 表达式首先可以看做是一个临时使用的函数。它的一般格式如下:
  198. ```c++
  199. [捕获列表] + lambda 声明(可选) + 复合语句
  200. lambda 声明指的是:
  201. (参数列表) + 一堆修饰符(可选)
  202. ```
  203. 下面是一个简单的例子:
  204. ```c++
  205. #include <iostream>
  206. using namespace std;
  207. int main(void)
  208. {
  209. auto GetOne = []{ return 1; }; // GetOne 是一个 lambda 表达式
  210. cout << GetOne() << endl; // 使用起来就像一个函数,输出 1
  211. return 0;
  212. }
  213. ```
  214. 它还可以有参数:
  215. ```c++
  216. #include <iostream>
  217. using namespace std;
  218. int main(void)
  219. {
  220. auto GetSum = [](int x, int y){ return x + y; };
  221. cout << GetSum(2, 3) << endl; // 5
  222. return 0;
  223. }
  224. ```
  225. 或者临时调用:
  226. ```c++
  227. #include <iostream>
  228. using namespace std;
  229. int main(void)
  230. {
  231. cout << [](int x, int y){ return x + y; }(2, 3) << endl; // 5
  232. return 0;
  233. }
  234. ```
  235. #### lambda 表达式的捕获
  236. ##### 捕获的概念
  237. lambda 表达式是不能够直接使用函数内的局部变量的(之后你将会看到这是为什么)。如果需要使用函数内的局部变量,需要手动进行捕获。捕获的方式有两种:按值捕获与按引用捕获。按值捕获,只会获得该值,而按引用捕获,则会获得函数内局部变量的引用。声明要捕获的变量就在 lambda 表达式的 `[]` 内:
  238. + `[]`:不捕获任何局部变量
  239. + `[x]`:按值捕获变量 `x`
  240. + `[&y]`:按引用捕获变量 `y`
  241. + `[=]`:按值捕获全部局部变量
  242. + `[&]`:按引用捕获全部局部变量
  243. + `[&, x]`:除了 `x` 按值捕获之外,其他变量均按引用捕获
  244. + `[=, &y]`:什么意思不用我都说了吧
  245. + `[r = x]`:声明一个变量 `r` ,捕获 `x` 的值
  246. + `[&r = y]`:声明一个引用 `r`,捕获 `y` 的引用
  247. + `[x, y, &z, w = p, &r = q]`:作为练习
  248. + `[&, x, y, p = z]`:这个也作为练习
  249. 这样我们就可以写出下面的代码了:
  250. ```cpp
  251. #include <iostream>
  252. using namespace std;
  253. int main(void)
  254. {
  255. int x, y, z;
  256. cin >> x >> y;
  257. [x, y, &z](){ z = x + y; }();
  258. cout << z << endl; // z = x + y
  259. return 0;
  260. }
  261. ```
  262. ##### 捕获 `this` 与 `*this`
  263. 当 lambda 表达式位于类的成员函数内时,该如何使用该类的成员变量呢?我们知道,在类的成员函数体内使用成员变量,都是通过 `this` 指针访问的,此处 `this` 作为成员函数的一个参数,因此只需要捕获 `this` 指针,就可以在 lambda 体内访问其成员变量了!
  264. 捕获时,我们可以选择捕获 `[this]`,也可以捕获 `[*this]`。区别是,前者捕获的是 `this` 指针本身,而后者是按值捕获 `this` 指针所指向的对象,也就是以 `*this` 为参数复制构造了一个新的对象。看下面的代码:
  265. ```c++
  266. #include <iostream>
  267. using namespace std;
  268. struct Foo
  269. {
  270. int m_bar;
  271. void Func()
  272. {
  273. [this]()
  274. {
  275. cout << ++m_bar << endl;
  276. }();
  277. }
  278. };
  279. int main()
  280. {
  281. Foo foo;
  282. foo.m_bar = 999;
  283. foo.Func(); // 输出 1000
  284. }
  285. ```
  286. ##### 附注
  287. 需要注意的是,lambda 表达式的捕获发生在 **lambda 表达式定义处**,而不是 lambda 表达式调用处,比如:
  288. ```c++
  289. int a = 4;
  290. auto f = [a]() { cout << a << endl; }; // 此时捕获 a,值是 4
  291. a = 9;
  292. f(); // 输出 4,而非 9
  293. ```
  294. > **C++ 真奇妙:不需要捕获的情况**
  295. >
  296. > 看这特殊的引用块就知道,本段内容仅作介绍,感觉较难者请跳过本块。
  297. >
  298. > 有时,即使是局部变量,不需要捕获也可以编译通过。这是 C++ 标准对编译器实现做出的妥协。这种现象叫做“常量折叠(constant folding)”;与之相对的是不能直接使用,必须进行捕获的情况,通常称作“odr-used”。这两个概念比较复杂,在此不做过多展开。看下面的例子:
  299. >
  300. > ```c++
  301. > int Func1(const int& x) { return x; }
  302. > void Func2()
  303. > {
  304. > const int x = 4;
  305. > []()
  306. > {
  307. > int y = x; // OK, constant folding
  308. > int z = Func1(x); // Compile error! odr-used! x is not captured!
  309. > }();
  310. > }
  311. > ```
  312. >
  313. > 但是个别较老的编译器即使是 odr-used 也可能会编译通过
  314. #### lambda 表达式的修饰符 `mutable`
  315. lambda 表达式可以有一些修饰符,例如 `noexcept`、`mutable `等,这里仅介绍 `mutable`。
  316. lambda 表达式按值捕获变量时,捕获的变量默认是不可修改:
  317. ```c++
  318. int a = 4;
  319. auto f = [a]()
  320. {
  321. ++a; // Compile error: a cannot be modified!
  322. };
  323. ```
  324. 但是我们可以通过加 `mutable` 关键字让它达到这个目的:
  325. ```c++
  326. int a = 4;
  327. auto f = [a]() mutable
  328. {
  329. ++a; // OK
  330. cout << a << endl;
  331. };
  332. f(); //输出 5
  333. cout << a << endl; //输出 4
  334. ```
  335. 需要注意的是,按值捕获变量是生成了一个新的变量副本,而非原来的变量,所以在 lambda 外的 `a` 的值仍然是 `4`
  336. #### lambda 表达式的本质
  337. 本段内容仅是粗略地讲述,不做深入讨论。读者也可以跳过本块。
  338. 上面说了这么多语法规定,但是 lamdba 表达式究竟是什么?知道了这个可以帮助我们理解 lambda 表达式的这些规定。
  339. C++17 标准中如此定义 lambda 的类型:
  340. > The type of a *lambda-expression* (which is also the type of the closure object ) is a unique, unnamed non-union class type, called the closure type....
  341. lambda 表达式类型是一个独一无二的、没有名字的、并且不是联合体的类类型。我们把它叫做“**closure type**”。
  342. 后面还有一堆关于它性质的约束,这里就不展开了,大致上就是编译器可以自由决定它的很多性质,有兴趣的可以去翻阅《ISO/IEC 14882: 2017》第 8.1.5.1 款。
  343. 大体来看,一个 lamdba 表达式与一个类是大致上相同的。也就是说,lambda 表达式:
  344. ```c++
  345. int a = 0, b = 0;
  346. auto f = [a, &b](int x) { return a + b + x; }
  347. f(5);
  348. ```
  349. 和下面的代码大致相同:
  350. ```c++
  351. int a = 0, b = 0;
  352. class __lambda__
  353. {
  354. private:
  355. int a;
  356. int& b;
  357. public:
  358. __lambda__(int& a, int& b) : a(a), b(b) {}
  359. auto operator(int x) const { return a + b + x; }
  360. };
  361. __lambda__ f(a, b);
  362. f.operator()(5);
  363. ```
  364. 不过它们两个**并不完全相同**。首先,不同编译器的实现本身就有不同;另外,它们在语法上的规定也有一些差别。篇幅所限,在此不做过多展开。
  365. #### lambda 表达式的应用
  366. 看了上面这么多介绍,你可能要问:这东西能用什么用处?为什么不直接写个函数,或者是干脆不用 lambda 表达式而直接写在函数体里呢?有这个疑问是正常的。因为我上面给的例子都是可以不用 lambda 表达式就能轻松解决的。但是,lambda 表达式在很多应用场景具有不可替代的优势。最简单的例子,比如在局部,你要重复某些操作,但是另写一个函数又不是很方便,就可以用 lambda 表达式完成。此外,它最大的作用就是在函数式编程中,或者是其他需要回调函数的情况,以 lambda 表达式作为函数的参数以作为回调函数。在下面的教程中,例如多线程、智能指针,我们将会多次用到 lambda 表达式。届时你将会看到使用 lambda 表达式是多么的方便。
  367. #### 关于 lambda 表达式的其他说明
  368. lambda 表达式还有很多有趣之处,例如泛型 lambda、返回 lambda 表达式的 lamdba 表达式,此外 `decltype` 在 lambda 表达式中的使用也是光怪陆离……总之,lambda 表达式非常有趣。
  369. 到了这里,相信你对 lambda 表达式已经有了相当的理解,就让我们来做一道简单的练习吧(狗头)
  370. > 请给出下面程序的输出(该程序选自《ISO/IEC 14882: 2017 Programming Language --- C++》第 107 页):
  371. >
  372. > ```c++
  373. >#include <iostream>
  374. > using namespace std;
  375. >
  376. > int main()
  377. > {
  378. > int a = 1, b = 1, c = 1;
  379. > auto m1 = [a, &b, &c]() mutable
  380. > {
  381. > auto m2 = [a, b, &c]() mutable
  382. > {
  383. > cout << a << b << c;
  384. > a = 4; b = 4; c = 4;
  385. > };
  386. > a = 3; b = 3; c = 3;
  387. > m2();
  388. > };
  389. > a = 2; b = 2; c = 2;
  390. > m1();
  391. > cout << a << b << c << endl;
  392. > return 0;
  393. > }
  394. > ```
  395. > 相信聪明的你一下就看出了答案。没错,答案就是我们小学二年级学习的数字:**123234**!怎么样,你答对了吗?
  396. >
  397. 如果阅读本文之后你觉得 lambda 表达式很有趣,欢迎阅读 《ISO/IEC 14882: 2017 Programming Language --- C++》110~120 页,或点击进入网址:[cppreference_lambda](https://zh.cppreference.com/w/cpp/language/lambda) 获取更多信息。
  398. ### `std::thread`
  399. 头文件:`#include <thread>`。用于开启新的线程。示例代码:
  400. ```c++
  401. #include <iostream>
  402. #include <thread>
  403. #include <functional>
  404. void Func(int x, int& cnt)
  405. {
  406. for (int i = 0; i < 110; ++i)
  407. {
  408. std::cout << "In Func: " << x << std::endl;
  409. ++cnt;
  410. std::this_thread::sleep_for(std::chrono::milliseconds(20));
  411. }
  412. }
  413. int main()
  414. {
  415. int cnt = 0;
  416. // 由于这种情况下函数的调用与传参不是同时的,提供参数在函数调用之前,因此以引用方式传递参数时需要用 std::ref
  417. std::thread thr(Func, 2021, std::ref(cnt));
  418. for (int i = 0; i < 50; ++i)
  419. {
  420. std::cout << "In main: " << 110 << std::endl;
  421. ++cnt;
  422. std::this_thread::sleep_for(std::chrono::milliseconds(20));
  423. }
  424. thr.join(); // 等待子线程结束,在 thr 析构前若未 detach 则必须调用此函数,等待过程中主线程 main 被阻塞
  425. std::cout << "Count: " << cnt << std::endl;
  426. return 0;
  427. }
  428. ```
  429. 或者使用 lambda 表达式达到同样效果:
  430. ```c++
  431. #include <iostream>
  432. #include <thread>
  433. #include <functional>
  434. int main()
  435. {
  436. int cnt = 0, x = 2021;
  437. std::thread thr
  438. (
  439. [x, &cnt]()
  440. {
  441. for (int i = 0; i < 110; ++i)
  442. {
  443. std::cout << "In Func: " << x << std::endl;
  444. ++cnt;
  445. std::this_thread::sleep_for(std::chrono::milliseconds(20));
  446. }
  447. }
  448. );
  449. for (int i = 0; i < 50; ++i)
  450. {
  451. std::cout << "In main: " << 110 << std::endl;
  452. ++cnt;
  453. std::this_thread::sleep_for(std::chrono::milliseconds(20));
  454. }
  455. thr.join();
  456. std::cout << "Count: " << cnt << std::endl;
  457. return 0;
  458. }
  459. ```
  460. 如果不希望等待子线程结束,`main` 结束则程序结束,则可以构造临时对象调用 `detach` 函数:
  461. ```c++
  462. #include <iostream>
  463. #include <thread>
  464. #include <functional>
  465. int main()
  466. {
  467. int cnt = 0, x = 2021;
  468. std::thread
  469. (
  470. [x, &cnt]()
  471. {
  472. for (int i = 0; i < 110; ++i)
  473. {
  474. std::cout << "In Func: " << x << std::endl;
  475. ++cnt;
  476. std::this_thread::sleep_for(std::chrono::milliseconds(20));
  477. }
  478. }
  479. ).detach();
  480. for (int i = 0; i < 50; ++i)
  481. {
  482. std::cout << "In main: " << 110 << std::endl;
  483. ++cnt;
  484. std::this_thread::sleep_for(std::chrono::milliseconds(20));
  485. }
  486. std::cout << "Count: " << cnt << std::endl;
  487. return 0;
  488. }
  489. ```
  490. 更多内容请参看(点击进入):[cppreference_thread](https://en.cppreference.com/w/cpp/thread/thread)
  491. ### 智能指针
  492. #### 总述
  493. 头文件:`include <memory>`
  494. 智能指针是 C++ 标准库中对指针的封装,它的好处是可以不需要 `delete`,而自动对其指向的资源进行释放,这在一定程度上降低了 C++ 程序员管理内存的难度,但同时智能指针的使用也具有一定的技巧。
  495. 智能指针主要有三种:`shared_ptr`、`weak_ptr`、`unique_ptr`。
  496. #### `std::shared_ptr`
  497. ##### 概览
  498. `shared_ptr` 的用法最为灵活,内部实现方式是**引用计数**。即,它会记录有多少个 `shared_ptr` 正在指向某个资源,并当指向该资源的 `std::shared_ptr` 数为零时,调用相应的释放函数(默认为 `delete` 操作符)释放该资源。不过也需要注意,使用 `std::shared_ptr` 会比传统的指针带来额外的引用计数的开销,因此只有当后面将会介绍的 `std::unique_ptr` 无法满足要求时方可考虑 `std::shared_ptr`。
  499. 像 `new` 会在自由存储区动态获取一块内存并返回其一样,如果要动态分配一块内存并得到其智能指针,可以使用 `std::make_shared` 模板,例如:
  500. ```c++
  501. #include <memory>
  502. void Func()
  503. {
  504. int* p = new int(110); // 在自由存储区 new 一个 int 对象,初值为 110
  505. auto sp = std::make_shared<int>(110); // 在自由存储区 new 一个 int 对象,初值为 110
  506. // sp 被自动推导为 std::shared_ptr<int> 类型
  507. delete p; // 释放内存
  508. // 编译器调用 sp 的析构函数,并将其指向的 int 释放掉
  509. }
  510. ```
  511. 关于引用计数:
  512. ```cpp
  513. #include <memory>
  514. void Func()
  515. {
  516. int x = 110;
  517. {
  518. auto sp1 = std::make_shared<int>(x); // 得到一个 int,初值为 110。
  519. // 上述此语句执行过后,只有一个智能指针 sp1 指向这个 int,引用计数为 1
  520. {
  521. auto sp2 = sp1; // 构造一个 std::shared_ptr sp2,指向 sp1 指向的对象,并将引用计数加一
  522. // 故此处引用计数为2
  523. std::cout << *sp2 << std::endl; // 输出 110
  524. // 此处 sp2 生存期已到,调用 sp2 的析构函数,使引用计数减一,因此此时引用计数为 1
  525. }
  526. // 此处 sp1 生命期也已经到了,调用 sp1 析构函数,引用计数再减一,故引用计数降为 0
  527. // 也就是不再有 std::shared_ptr 指向它了,调用 delete 释放
  528. }
  529. }
  530. ```
  531. 将裸指针交给 `std::shared_ptr` 托管:
  532. ```cpp
  533. int* p = new int(110);
  534. int* q = new int(110);
  535. std::shared_ptr sp(p); // 把 p 指向的对象交给 sp 托管,此后 p 便不需要 delete,sp 析构时会自动释放
  536. std::shared_ptr sq; // sq 什么也不托管
  537. sq.reset(q); // 让 sq 托管 q
  538. //此后 p 与 q 便不需要再 delete
  539. ```
  540. 需要注意的是,这种写法是非常危险的,既可能导致 `p` 与 `q` 变为野指针,也可能造成重复 `delete`,我们应该更多使用 make_shared。
  541. ##### 自定义释放函数
  542. 之前说过 ,默认情况下是释放内存的函数是 `delete` 运算符,但有时我们并不希望这样。比如下面的几个情况:
  543. + 使用 `std::shared_ptr` 托管动态数组
  544. + C++11 起
  545. ```cpp
  546. #include <memory>
  547. void IntArrayDeleter(int* p) { delete[] p; }
  548. int main()
  549. {
  550. std::shared_ptr<int> sp(new int[10], IntArrayDeleter); // 让 IntArrayDeleter 作为释放资源的函数
  551. sp.get()[0] = 0; // 访问第 0 个元素
  552. // sp 析构时自动调用 IntArrayDeleter 释放该 int 数组
  553. return 0;
  554. }
  555. // 或者利用 lambda 表达式:std::shared_ptr<int> sp(new int[10], [](int* p) { delete[] p; });
  556. ```
  557. + C++17 起
  558. ```cpp
  559. std::shared_ptr<int[]> sp(new int[10]);
  560. sp[0] = 0; // 访问第 0 个元素
  561. ```
  562. + C++20 起
  563. ```cpp
  564. auto sp = std::make_shared<int[]>(10);
  565. sp[0] = 0; // 访问第 0 个元素
  566. ```
  567. + 释放系统资源
  568. 在编程过程中,难免与操作系统打交道,这时我们可能需要获取一系列的系统资源,并还给操作系统(实际上 `new` 和 `delete` 也就是一个例子)。一个比较有特色的例子就是 Windows API。在传统的 Win32 程序中,如果我们要在屏幕上进行绘制图形,我们首先需要获取设备的上下文信息,才能在设备上进行绘图。设想这样一个情景:我们有一个窗口,已经获得了指向这个窗口的句柄(即指针)`hWnd`,我们要在窗口上绘图,就要通过这个窗口句柄获取设备上下文信息。代码如下:
  569. ```c++
  570. HDC hdc; // DC: Device context,一个指向 DC 的句柄(HANDLE)
  571. hdc = GetDC(hWnd); // 获取设备上下文
  572. /*执行绘图操作*/
  573. ReleaseDC(hWnd, hdc); // 绘图完毕,将设备上下文资源释放,归还给 Windows 系统
  574. ```
  575. 使用 `std::shared_ptr` 对其进行托管,代码如下:
  576. ```c++
  577. // 使用 lambda 表达式写法(推荐)
  578. std::shared_ptr<void> sp(GetDC(hWnd), [hWnd](void* hdc) { ReleaseDC(hWnd, (HDC)hdc); });
  579. ```
  580. ```cpp
  581. // 不使用 lambda 表达式的写法:
  582. struct Releaser
  583. {
  584. HWND hWnd;
  585. Releaser(HWND hWnd) : hWnd(hWnd) {}
  586. void operator()(void* hdc)
  587. {
  588. ReleaseDC(hWnd, (HDC)hdc);
  589. }
  590. };
  591. void PaintFunc()
  592. {
  593. /*...*/
  594. std::shared_ptr<void> sp(GetDC(hWnd), Releaser(hWnd));
  595. /*...*/
  596. }
  597. ```
  598. ##### 常见的错误用法
  599. `std::shared_ptr` 虽然方便,但是也有一些错误用法,这个是常见的:
  600. ```c++
  601. #include <memory>
  602. void Func()
  603. {
  604. int* p = new int(110);
  605. std::shared_ptr<int> sp(p); // 让 sp 托管 p
  606. std::shared_ptr<int> sq(p); // 让 sq 托管 p
  607. // Runtime Error! 程序至此崩溃
  608. }
  609. ```
  610. 这是因为,只有复制构造函数里面才有使引用计数加一的操作。即当我们写 `std::shared_ptr<int> sq = sp` 的时候,确实引用计数变成了 2,但是我们都用一个外部的裸指针 `p` 去初始化 `sp` 和 `sq`,智能指针并不能感知到它们托管的内存相同。所以 `sp` 和 `sq` 所托管的内存被看做是独立的。这样,当它们析构的时候,均会释放它们所指的内存,因此同一块内存被释放了两次,导致程序出错。所以个人还是推荐使用 `make_shared` ,而不是用裸指针去获取内存。
  611. 另一个著名的错误用法,请继续阅读 `std::weak_ptr`。
  612. #### `std::weak_ptr`
  613. 看完了上面的 `shared_ptr` 的讲述,相信你已经对使用智能指针胸有成竹了。一切都用 `shared_ptr`、`make_shared` 就万事大吉了嘛!但事情可能没那么简单。看下面的例子:
  614. ```c++
  615. #include <iostream>
  616. #include <memory>
  617. class B;
  618. class A
  619. {
  620. public:
  621. void SetB(const std::shared_ptr<B>& ipB)
  622. {
  623. pB = ipB;
  624. }
  625. private:
  626. std::shared_ptr<B> pB;
  627. };
  628. class B
  629. {
  630. public:
  631. void SetA(const std::shared_ptr<A>& ipA)
  632. {
  633. pA = ipA;
  634. }
  635. private:
  636. std::shared_ptr<A> pA;
  637. };
  638. void Func()
  639. {
  640. auto pA = std::make_shared<A>();
  641. auto pB = std::make_shared<B>();
  642. pA->SetB(pB);
  643. pB->SetA(pA);
  644. // 内存泄露!!!
  645. }
  646. /*...*/
  647. ```
  648. 太糟糕了!上面的 `pA` 指向的的对象和 `pB` 指向的对象一直到程序结束之前永远不会被释放!如果不相信,可以在它们的析构函数里输出些什么试一试。相信学习了引用计数的你,一定能想出来原因。我们就把它当作一道思考题作为练习:为什么这两个对象不会被释放呢?(提示:注意只有引用计数降为0的时候才会释放)
  649. 实际上,`std::shared_ptr` 并不是乱用的。它除了作为一个指针之外,还表明了一种逻辑上的归属关系。从逻辑上看,类的成员代表一种归属权的关系,类的成员属于这个类。拥有 `shared_ptr` 作为**成员**的对象,是对 `shared_ptr` 所指向的对象具有所有权的,`shared_ptr` 也是基于这个理念设计的。但是,有时候我们并不希望这是个所有权的关系,例如我们有双亲和孩子的指针作为“人”的成员,但是人与人之间是平等相待和谐共处的,我们不能说一个人是另一个人的附属品。这时候,`std::weak_ptr` 便应运而生了!
  650. `std::weak_ptr` 与 `shared_ptr` 的区别是,它指向一个资源,并不会增加引用计数。当指向一个资源的 `shared_ptr` 的数量为 0 的时候,即使还有 `weak_ptr` 在指,资源也会被释放掉。也是因此,`weak_ptr`也是存在悬垂指针的可能的,即它指向的资源已经被释放掉。 也是因此,`weak_ptr` 不允许直接地被解引用,必须先转换为相应的 `shared_ptr` 才能解引用,获取其所指的资源。它的用法如下:
  651. ```cpp
  652. auto sp = std::make_shared<int>(5);
  653. std::weak_ptr<int> wp = sp; // 正确,让 wp 指向 sp 指向的资源
  654. // std::shared_ptr<int> sp1 = wp; // 错误,weak_ptr 不能直接赋值给 shared_ptr
  655. /* Do something */
  656. if (wp.expired())
  657. {
  658. std::cout << "The resource has been released!" << std::endl;
  659. }
  660. else
  661. {
  662. // std::cout << *wp << std::endl; // Compile error! weak_ptr 不能直接使用!
  663. auto sp1 = wp.lock(); // 从 weak_ptr 中恢复出 shared_ptr,sp1 的类型为 std::shared_ptr<int>
  664. std::cout << *sp1 << std::endl;
  665. }
  666. ```
  667. 从类的设计本身来看,`weak_ptr` 不会增加引用计数;从逻辑上看,`weak_ptr` 描述了一种联系,即 `weak_ptr` 的拥有者与其指向的对象之间不是一种归属关系,而是一种较弱的联系。一个类的对象只需知道另一个类的对象是谁,而不对其拥有占有权,这时候用 `weak_ptr` 是合适的。
  668. 上面的 `A` 类和 `B` 类的问题,将 `A` 和 `B` 成员从 `shared_ptr` 换成 `weak_ptr` 就会解决内存泄露的问题了!
  669. #### `std::unique_ptr`
  670. `std::unique_ptr` 顾名思义,独有的指针,即资源只能同时为一个 `unique_ptr` 所占有,是基于 RAII 的思想设计的智能指针,并且相比于原始指针并不会带来任何额外开销,是智能指针的首选。它部分涉及到对象的生命期、右值引用与移动语义的问题,在此不做过多展开,仅提供一个例子作为参考:
  671. ```cpp
  672. {
  673. auto p = std::make_unique<int>(5); // 创建一个 int 对象并初始化为 5
  674. std::cout << *p << std::endl; // 输出 5
  675. // 该 int 对象随着 p 的析构而被 delete
  676. }
  677. ```
  678. 需要注意的是,由于[标准委员会的疏忽~忘了~(partly an oversight)](https://herbsutter.com/gotw/_102/),C++14 中才引进`std::make_unique`,C++11 中无法使用。因此 C++11 若想使用则需自定义 `make_unique`:
  679. ```cpp
  680. namespace
  681. {
  682. template<typename T, typename... Args>
  683. std::unique_ptr<T> make_unique( Args&&... args )
  684. {
  685. return std::unique_ptr<T>(new T( std::forward<Args>(args)...));
  686. }
  687. }
  688. ```
  689. 更多关于智能指针的知识,可以参考(点击进入):
  690. + [cppreference_shared_ptr](https://zh.cppreference.com/w/cpp/memory/shared_ptr)
  691. + [cppreference_weak_ptr](https://zh.cppreference.com/w/cpp/memory/weak_ptr)
  692. + [cppreference_unique_ptr](https://zh.cppreference.com/w/cpp/memory/unique_ptr)