现代 CMake 使用 C++ 依赖项时已经相对方便了。比如 find_package(OpenSSL 1.0 REQUIRED) 就可以自动在本地机器上查找已安装的与 OpenSSL 1.0 兼容的包，然后就可以直接 target_link_libraries(targetName PRIVATE OpenSSL::SSL) 使用，不必再手动写头文件和库文件配置。 要做到「本地机器上已安装」，macOS 和 Linux 系统都非常方便，大多数库都可以直接通过系统级包管理工具安装，而 Windows 上就稍显麻烦。我之前比较喜欢的是，比较复杂的库还是留着在各平台手动安装，简单的则使用 CMake 的 FetchContent 模块下载使用。但这样的缺点是每次重新生成工程时，都需要下载一遍依赖并编译。尤其最近国内的网络状态，HTTPS 连接 GitHub 经常超慢。所以想想还是用现成的包管理工具吧。 目前市面上流行的包管理工具中：vcpkg 虽然很多人用，但是微软那套逻辑我始终表示审美不能；Hunter 虽然是纯 CMake 解决方案，但是官方涵盖的库偶有不足；而 Conan 我一开始是不喜欢的，不但需要使用 Python 安装，而且它的前世 biicode 当年也风光过现在已经挂了。不过现在看来，Conan 似乎是这几个之中比较成熟好用的解决方案，至少，符合我的审美就是了。 安装 官方推荐用 Python 在虚拟环境里用 pip install conan 安装，可以随时保持最新。 我在 macOS 上使用 Homebrew 安装，可以少管理一个虚拟环境。感觉 Arch Linux 这样滚动更新的系统也可以直接使用系统包管理工具安装。 找到想要的库 想要使用 spdlog 时，首先搜索： $ conan search spdlog -r conan-center Existing package recipes: spdlog/0.14.0@bincrafters/stable spdlog/0.16.3@bincrafters/stable spdlog/0.17.0@bincrafters/stable spdlog/1.0.0@bincrafters/stable spdlog/1.1.0@bincrafters/stable spdlog/1.2.1@bincrafters/stable spdlog/1.3.0@bincrafters/stable spdlog/1.3.1@bincrafters/stable spdlog/1.4.1@bincrafters/stable spdlog/1.4.2 spdlog/1.4.2@bincrafters/stable spdlog/1.5.0 命令行中的 -r conan-center 表示所要搜索的仓库，conan-center 是官方自带的默认仓库，如果你本地添加了多个仓库的话，也可以用 all 表示搜索所有仓库。不带这个选项时则是在本地的缓存中查找。 搜索结果中每一行都是一个可用的包的名称，使用 @user/channel 后缀的是完整的包命名方式。官方 conan-center 仓库中，最近的包都是通过 CI 自动构建二进制文件的，这些包使用纯 name/version 的命名方式。 想要知道某个版本/包的详情，可以使用这样的命令查看： $ conan inspect spdlog/1.5.0 会列出一些信息和安装时的可选参数。 当然，你也可以直接在网站 https://conan.io/center/ 查找 conan-center 仓库中的包。 依赖的指定、安装、使用 一般使用名为 conanfile.txt 的纯文本文件指定依赖，格式类似 INI 文件。 [requires] spdlog/1.5.0 [generators] cmake_find_package [requires] 部分很简单，列出你所需要依赖的包的名称即可。[generators] 部分指定所需要的「生成器」，可以生成与 CMake、SCons 等工具的对接文件。 使用 conan install /path/to/source-dir 可以安装依赖并生成对接文件，参数为包含 conanfile.txt 的目录。当然，这样做会把「对接文件」生成在当前目录，可以使用 -if 参数指定输出目录，推荐放在 CMake 的构建目录。 这样，Conan 就会把 1.5.0 版本的 spdlog 安装到自己管理的目录（一般是 ~/.conan），然后在输出目录输出一个 Findspdlog.cmake 文件。 CMakeLists.txt 中，要让 find_package 使用 Findspdlog.cmake 文件，把它所在的目录加入到 CMAKE_MODULE_PATH 中即可： # 因为我们把 Findspdlog.cmake 输出到了构建目录 list(APPEND CMAKE_MODULE_PATH "${CMAKE_BINARY_DIR}") # 按照正常方式搜索 find_package(spdlog REQUIRED) # ... # 这个生成器导出的目标是 package::package target_link_libraries(targetName PRIVATE spdlog::spdlog) 当然，官方教程中使用的是 cmake 生成器，它不会生成 FindXXX.cmake，而是生成一个 conanbuildinfo.cmake，你需要在 CMakeLists.txt 中手动初始化： include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) conan_basic_setup(TARGETS) # ... # 这个生成器导出的目标是 CONAN_PKG::package target_link_libraries(targetName PRIVATE CONAN_PKG::spdlog) 这种方法把 Conan 显式写入了 CMake 配置里，我个人不是很喜欢。（但是 CONAN_PKG::package 的表述确实比 package::package 好一些，因为一些库官方提供的目标并不都是 package::package。） 其它零碎 构建版本 Conan 默认安装/构建的二进制是 Release 版本的。而 CMake 的默认构建方式则是 Debug。 所以，尤其在使用类似 MSVC 的编译器时，你可能需要手动指定安装 Debug 版： $ conan install . -s build_type=Debug 当然你也可以试试 cmake_multi 或者 cmake_find_package_multi 生成器，可以同时安装 Debug 和 Release 版本。抑或是使用官方提供的CMake 集成，自己写脚本把 CMake 和 Conan 的构建类型同步起来。 包的参数 在使用 conanfile.txt 指定依赖时，还可以同时指定一些可选参数。比如指定使用 spdlog 的动态链接版本： [options] spdlog:shared=True 好了，这就是大致的 Conan 使用介绍。 真正上手，还请参阅官方文档 https://docs.conan.io/。
在知乎上看到《constexpr 究竟有什么用？》的问题，正巧前段时间趁着了解 constexpr if 的时候刚做过相关资料的搜集，拼凑了一下搞了一个回答。篇幅所限，完整版放在这里，主要是追加一些例子并补完一些周边内容 😄 constexpr 关键词是 C++11 引入的；C++14 中放宽了对 constexpr 函数的语法要求；而 C++17 则复用了该关键字，引入了 constexpr if。 constexpr 主要为 C++03 引入了以下变动： 拓展了「常量表达式」的概念 提供了「强制要求」表达式在编译时求值（compile-time evaluation）的方法 提供了编译时的 if 条件判断 拓展常量表达式的概念 之所以要拓展常量表达式的概念，是因为 C++03 中的一些尴尬，这从原本的标准库中就能看出来。 比如我们都知道 INT_MAX 是 C 语言的遗产，C++ 则更希望大家使用 std::numeric_limits<int>::max() 来拿 int 型的上限。然而不幸的是，后者是个函数调用而不是整型常量，使用起来可能需要花更多心思在性能或者别的东西上，没有前者那么自由。 int foo[std::numeric_limits<char>::max()]; // error: 不是常量表达式 又比如标准文件流，它的构造函数可以带上这样的第二个参数： std::fstream foo("foo.txt", std::ios::in | std::ios::out); 这个参数是 openmode 类型的，是由实现具体定义的一种 Bitmask 类型。出于类型安全的考虑，通常用枚举值实现而不是整型。但是这样一来就有个问题，同样是写 std::ios::in | std::ios::out，如果用整型的话可以作为常量表达式使用，而为了类型安全考虑换用枚举实现（尤其是重载 operator|）后，就再也不可能是常量表达式了。 inline openmode operator|(openmode lhs, openmode rhs) { return openmode(int_type(lhs) | int_type(rhs)); } 明明是这样简单的函数，对它的调用却不是常量表达式，就更别提编译时求值了。这就让委员会陷入了必须在「类型安全」和「效率」里二选一的尴尬境地。 标准库里会遇到这样的问题，大家日常使用也会遇到。加之标准委员会很想借此机会把原本标准中对于「常量表达式」（尤其是整型常量表达式）复杂的定义重构简化，引入 constexpr 来标记一些简单的函数，让对它们的调用能够作为常量表达式存在就很合情合理了。 而 constexpr 成员函数也可以类推，比独立的 constexpr 函数多一些对于类本身的限制便是了。 至于 constexpr 函数「到底应该多简单」，其实并没有必要刻意去记那些限制。如果你标记了 constexpr 的函数不够简单，编译器会提醒你哪里有问题的。 强制要求表达式编译时求值 说到这里，「constexpr 函数」并不能和「编译时求值」划等号，它只表达了「这个函数具备这样的能力」。 如果需要强制要求表达式在编译时求值，那么我们需要在变量定义前添加 constexpr，这样，用来初始化这个变量的表达式就「必须」是常量表达式，否则会报错。 #include <fstream> // 该函数不是 constexpr 函数 int identity(std::ios::openmode mode) { return mode; } constexpr auto out = std::ios::out; // 此处为 constexpr int main() { constexpr int modeFail = std::ios::in | identity(out); // 出错 int modePass = std::ios::in | identity(out); // 正常 } constexpr 函数只有同时满足 所有参数都是常量表达式 返回的结果被用于常量表达式（比如用于初始化 constexpr 数据） 才会触发编译时求值。如果只有参数是常量表达式而结果不是，那么是否触发编译时求值取决于具体实现。 constexpr if C++17 的 constexpr if 严格意义上不是 constexpr 而是 if 的一部分。 constexpr if 的主要用途是简化模板代码（这也意味着除非你是「库作者」或者「模版狂魔」，很少会用到）。很多原本需要绕弯借助类型 Tag 或者 SFINAE 来实现，需要拆成 N 个函数的功能，可以借助 constexpr if 写到一个函数里。 以下面的 get_value 函数为例。这个函数正常情况下会把参数原样返回，但如果传入指针，则返回被指针指向的内容。 如果用简单的类型 Tag 来实现，需要拆成三个模板： template <typename T> auto get_value(T t, std::true_type) { return *t; } template <typename T> auto get_value(T t, std::false_type) { return t; } template <typename T> auto get_value(T t) { return get_value(t, std::is_pointer<T>{}); } 如果用 SFINAE 来实现，需要拆成两个模板： template <typename T, std::enable_if_t<std::is_pointer_v<T>, int> = 0> auto get_value(T t) { return *t; } template <typename T, typename std::enable_if_t<!std::is_pointer_v<T>, int> = 0> auto get_value(T t) { return t; } 而如果用 constexpr if 来实现，可以简化为一个： template <typename T> auto get_value(T t) { if constexpr (std::is_pointer_v<T>) { return *t; } else { return t; } } 也有人把 constexpr if 拿来代替当作 #if 用，相信你可以想象得出具体的用法。但……我觉得那是邪道，所以不写了 😛 constexpr 和 const 最初接触到 C++11 时，constexpr 和 const 的关系让我很是害怕，因为排列组合似乎很多。不过这回终于下了决心，整理了下，没有初见时想象的那么复杂，总共似乎也有两个情况。 在修饰数据（变量）时，constexpr 是隐含 const 语义的。同时，constexpr 只和变量本身有关，遇到类似 const int *p 和 int *const p 的破事时，建议还是写明： constexpr int const foo = 42; constexpr int foo = 42; // 和上面等价 constexpr int const *pb = &bar; // 此处 &bar 必须是常量表达式 在涉及 constexpr 成员函数时，成员函数可能会有 const 修饰的情况。C++11 中，constexpr 成员函数是隐含 const 的；C++14 以来则没有这个限制。这里似乎可以借用 Python 的哲学「Better explicit than implicit」来表达一下，总共就几个字母的事情，就别偷懒了，你没法保证别人和你一样清楚这些「不那么直观」的东西。 以上就是所有内容了，感觉还是有些流水账的样子。 顺便小小吐槽一下「模板」，macOS 自带的拼音输入法只能通过 mú bǎn 打出来，所以我老是打成「模（mó）版」……
前几天看 API 文档时候遇到了 std::error_code 这个东西，当时以为是 errno 的 Alias，后续查阅文档才发现并没有那么简单。 system_error std::error_code 包含在 <system_error> 头文件中，相关的共有四个组件： std::system_error std::error_code std::error_condition std::error_category std::system_error 是一个继承自 std::runtime_error 的异常类，用以表示与 OS 交互时得到的以错误码形式返回的错误。因此除了提供标准的 what() 函数外，它还额外暴露了一个返回 std::error_code 的 code() 函数。同时，因为必须提供错误码，它既不提供空构造、也不提供接受字符串的构造函数。 从名字上就可以看出来（同时也是当时的提案 N2241 中指出的），整个 <system_error> 头文件都是为实现这个异常而生的。STL 有意将 std::error_code 作为 std::system_error 的附属品，而非单独的错误处理机制。 std::error_code STL 中早期类似的错误码实现需求来自（C++17 才正式进入标准的）文件系统 API，当时的实现认为错误分类应在 errno 和操作系统原生错误码上二选一。但由于 STL 中网络、各种奇形怪状的 Boost 库的陆续加入，大家急需一个可扩展的错误码表示方案，才演变成现在的 std::error_code。 std::error_code 顾名思义表示错误码，是由一个 int 型的 value 和一个 std::error_category * 型的 category 组成的值类型类。 之所以在 value 外还需要保存一个 category，是因为即使是同样的错误码，在不同的库或者场景下表示的意义可能不同。同样是 42，在一个库的错误码中可能表示「文件不存在」，在另一个库中可能就表示「DNS 解析失败」。如果你熟悉 Cocoa 的那一套，那么这就类似 NSError 的 domain 属性。 此外，虽然没有明说 value 在等于 0 时表示无错误，但整个系统就是建立在这样的假设上的，例如： operator bool 是根据 value 是否非 0 来的 clear() 函数会将 value 设为 0 构造 std::error_code std::error_code 的构造函数共有三种重载： () 构造一个类似 clear() 后的对象 (value, category) 用参数填充对应的字段 (enum_value) 调用 make_error_code(enum_value) 工厂函数 前两种没啥好说的，第三种却值得推敲。 template<class ErrorCodeEnum> error_code(ErrorCodeEnum e) noexcept; 此处的 ErrorCodeEnum 只是名字上说是枚举，但其实只要是用户定义类型就行（比如 enum class / enum / class），所以理论上可以从异常直接构造 std::error_code。 通常，如果构造函数只接受一个参数，那么我们推荐将它标记为 explicit 以免发生不必要的隐式转换。但这个函数不然，它要的就是让原始的 ErrorCodeEnum 可以隐式转换为 std::error_code，从而实现这两者的直接比较。 catch (std::system_error const& e) { if (e.code() == std::errc::invalid_argument) { // blah blah blah... } } 当然，为了避免从任意类型的值都能搞个 std::error_code 出来，此重载只有当 std::is_error_code_enum<ErrorCodeEnum>::value 是 true 时才会有效。所以你自定义错误码枚举时，需要在 std 命名空间中特化此模版。 #include <iostream> #include <system_error> enum class YourErrorCode { kSuccess = 0, // 别忘了 0 应该表示无错误 kNetworkError, kBadRequest, kServerError, }; // 特化模版，启用对应的重载 namespace std { template<> struct is_error_code_enum<YourErrorCode>: true_type {}; } // 提供工厂函数 // 工厂函数不必要写在 std 中 std::error_code make_error_code(YourErrorCode code) { return { static_cast<int>(code), std::generic_category(), // 这里暂时用自带的 category }; } int main() { std::error_code e = YourErrorCode::kBadRequest; std::cout << e << '\n'; // 自带一个输出流的重载 } 当然，如果后续 C++ 中终于引入 Concept 的话，这个重载应该就没有这么复杂了吧。 std::error_category 前面说到 std::error_code 里保存的其实是指向 std::error_category 的指针，而非对象。这是因为这个类就是应该被当作单例来用的，实际也只能这么用才对，因为它的 operator=() 是通过直接比较 this 指针来实现的。 此外，std::error_category 还是个纯虚类，你必须实现的纯虚函数有： name() 返回这类错误的名字 message(int) 为给出的「错误码」返回对应的文字描述 STL 自带 std::error_category 的几个子类，很好地示范了「如何正确使用纯虚类」：不暴露具体的子类，而是暴露工厂函数，只通过接口（纯虚类）访问子类。 // 得到的都是 std::error_category const& auto const& gec = std::generic_category(); auto const& sec = std::system_category(); std::error_condition 2007 年，<system_error> 正式进入标准前夕，委员会对 N2241 提案给出了一些意见，其中包括一条 Obscure the distinction between system-specific errors and the general and portable notion of an error condition. 于是，为了区分「系统相关的错误」和「平台无关的错误」，std::error_condition 诞生了。 因此你可以看到，std::error_condition 是一个与 std::error_code 除了语义几乎没有差别的东西。从库作者的角度，你可以理解为封装底层细节时用 std::error_code，而对外暴露接口时推荐使用 std::error_condition。 一起玩 std::error_condition 与 std::error_code 虽然是两个独立的类，但它们可以通过 std::error_category 连接在一起。 这两个类的对象互相比较时，STL 提供了对应的 operator=() 等函数的重载，通过调用双方的 category().equivalent(other) 来比较。只要任何一方的 category 认为对方与自己等价，两者就会被判为相等。 // 卖个萌 :-P enum class MyErrorCondition { kChenggong, kWangluoCuowu, kQingqiuCuowu, kFuwuqiCuowu, }; class MyErrorCategory: public std::error_category { public: // 还记得 std::error_cateory 是个单例么？ static MyErrorCategory const& instance() { static MyErrorCategory instance; return instance; } char const *name() const noexcept override { return "MyErrorCategory"; } std::string message(int code) const override { return "Message"; // 偷个懒 } bool equivalent(std::error_code const& code, int condition) const noexcept override { // 理论上你用不着在这里处理 code.category() == this->instance 的情况 // 因为是个单例，所以某些情况下不得不用这么绕的办法来拿 auto const& yourErrorCodeCategory = std::error_code(YourErrorCode{}).category(); if (code.category() == yourErrorCodeCategory) { switch (static_cast<MyErrorCondition>(condition)) { case MyErrorCondition::kChenggong: return code == YourErrorCode::kSuccess; case MyErrorCondition::kWangluoCuowu: return code == YourErrorCode::kNetworkError; case MyErrorCondition::kQingqiuCuowu: return code == YourErrorCode::kBadRequest; case MyErrorCondition::kFuwuqiCuowu: return code == YourErrorCode::kServerError; } } return false; } }; // error_condition 同样需要特化模版启动重载 namespace std { template<> struct is_error_condition_enum<MyErrorCondition>: true_type {}; } // error_condition 同样可以通过工厂函数构造 std::error_condition make_error_condition(MyErrorCondition code) { return {static_cast<int>(code), MyErrorCategory::instance()}; } int main() { std::error_code code = YourErrorCode::kNetworkError; std::error_condition condition = MyErrorCondition::kWangluoCuowu; std::cout << (code == condition) << '\n'; } 看上去有点绕，但由于通常 std::error_code 是底层接口定死的，而你所做的只能是用 std::error_condition 来适配的情况来说，还是很合理的。 还是 STL 轻松，直接抛 std::system_error 给你。std::error_code 甩脸，要抽象你自己去用 std::error_condition 搞，哈哈。 顺带一提 上面已经提到此处的 std::system_error 异常与 std::error_code 的关系。事实上，至少在提出引入 std::error_code 提案的作者看来，STL 乃至 C++ 中推荐的错误汇报方法还是异常。由于存在与操作系统等底层 API 的交互，才引入了「从 API 返回的错误码构建异常」以及「将异常转换为错误码传给 API」这样的需求。 当然，为了方便统一范式，便于不支持异常的系统使用文件系统 API。C++17 才正式进入标准的文件系统 API 也添加了「默认抛异常，但额外传一个 std::error_code 就不抛异常」的机制。 以上。
最近终于找到本有兴趣的书啦，就是这本《C++ 并发编程实战》。我会找一些自己觉得没想到、好玩、或者有自己想法的地方整理下。 顺带吐个槽：In Action 系列的封面人物真心诡异，还是 O'reilly 萌萌哒的奇怪可爱小动物看着顺心。 join() 的异常安全 std::thread 要求在析构函数被调用时，该对象必须没有与之相关联的线程，否系统分分钟自尽给你看（调用 std::terminate()）。 一般情况下，我们主要需要注意的地方是析构前对 join() 或者 detach() 的调用。如果要分离线程，线程启动后就可以立即调用 detach()，基本不会带来什么问题。而 join() 则没法立即调用，这样就会带来可能会被忽视的异常安全问题：线程启动后，如果某处抛出异常，可能把本应执行的 join() 跳过，导致程序自行退出。 void foo() { std::thread t(thread_func); do_something(); // 如果抛出异常，就可能把下面的 join() 跳过 t.join(); } 把 do_something(); 用 try / catch 包裹起来，并在 catch 里调 t.join() 最后把异常重新 throw 出去的方案固然可行，但对于 C++ 而言，更好的方案是 RAII： class thread_guard { public: explicit thread_guard(std::thread& t) : thread_(t) { } ~thread_guard() { if (thread_.joinable()) { // join() 前对象必须满足 joinable thread_.join(); } } thread_guard(thread_guard const&) = delete; thread_guard& operator=(thread_guard const&) = delete; private: std::thread& thread_; }; void foo() { std::thread t(thread_func); thread_guard g(t); do_something(); } 当然你也可以考虑把 std::thread 移动进去而不是传引用，书里管那种东西叫 scoped_thread，调用时不必为 std::thread 对象单独开一个名字。 线程传参二三事 给线程函数传参是通过 std::thread 的构造函数完成的，其原理与 std::bind 类似。所有参数先在当前线程被复制 / 移动到对象内部，然后在新线程中完成对线程函数的调用。 这两步的划分会导致会被一些人说「坑」（虽然实际上是他们自己没注意）。 比如从「被复制」到「实际调用线程函数」之间可能并不连续，那么被复制的参数届时是否还有效就可能成为问题，例如： void thread_func(std::string const& s); void foo(int bar) { char buffer[1024]; sprintf(buffer, "%d", bar); std::thread t(thread_func, buffer); t.detach(); } 前面说过构造函数仅负责原样复制 / 移动参数，所以 char * 到 std::string 的转换是在新线程中的第二步时做的，于是例子中 t 对象构造函数暂存的其实是指向栈上的 buffer 的指针。而由于 detach()，foo 返回后可能新线程还未正式启动。如果 thread_func 真正被调用时 foo 已经返回，那么这个指针参数就无效了。 一个解法是把代码改成 std::thread t(thread_func, std::string(buffer));这样构造函数就会把实参 std::string 移动进去（即便在 std::string 无法移动只能复制的平行宇宙，这样做也是安全的）。
前几天找资料，顺手看到了这篇博文，讲了博主如何优化自己以前的算法的过程。于是立马想起了 GoingNative 2013 上的演讲 C++ Seasoning 以及前不久看到的这篇 From goto to std::transform。于是不禁想，「寻找前 K 大数」这样的任务，能不能直接用 C++ 标准库来完成？如果能的话，性能又如何呢？于是我重新登录了那个以前做了两道题就尘封多年的 OJ 帐号…… 凭着最直观最 naïve 想法，第一个版本我用了 std::sort 来代替手工排序。不出所料，这样的做法跑了 562 ms，要排到 700 名开外的样子。 #include <stdio.h> #include <algorithm> #include <functional> #include <vector> int main() { int numVillagers; int topCount; while (scanf("%d %d\n", &numVillagers, &topCount) == 2) { if (numVillagers == 0 && topCount == 0) { break; } std::vector<int> wealth(numVillagers); for (int i = 0; i < numVillagers; i++) { scanf("%d", &(wealth[i])); } std::sort(wealth.begin(), wealth.end(), std::greater<int>()); if (topCount > numVillagers) { topCount = numVillagers; } for (int i = 0; i < topCount - 1; i++) { printf("%d ", wealth[i]); } printf("%d\n", wealth[topCount - 1]); } } 于是优化：考虑到每次新建 std::vector 的开销，把 wealth 拎出来放到循环外，reserve() 题目中的最大值，每次在循环里重新 resize() 后，时间顿时缩短到了 296 ms，可以排到 180+ 的样子。 std::vector<int> wealth; wealth.reserve(100000); 而在前面说过的演讲中，我还听到了一个之前从未留意的函数 std::nth_element。其作用是确保调用后第 N 个元素是范围内第 N 大的元素。调用后，[begin, N) 内的任意元素都小于 (N, end) 内的任意元素。 std::nth_element(wealth.begin(), wealth.begin() + topCount, wealth.end(), std::greater<int>()); std::sort(wealth.begin(), wealth.begin() + topCount, std::greater<int>()); 尝试修改成以上的略显罗嗦的代码后，程序运行时间缩短到了 171 ms，可以排到 70+ 的样子。时间上和博文中给出的最小堆实现相同，但是内存占用要比它大很多。 既然上面是先用 std::nth_element 大致排序了一下，而后再用 std::sort 排序前半部分，那么，STL 里是否存在一次性只排 [begin, N] 范围内的数，而无视 (N, end) 内的顺序的函数呢？答案是存在，可以直接使用 std::partial_sort 解决。 #include <stdio.h> #include <algorithm> #include <functional> #include <vector> int main() { int numVillagers; int topCount; std::vector<int> wealth; wealth.reserve(100000); while (scanf("%d %d\n", &numVillagers, &topCount) == 2) { if (numVillagers == 0 && topCount == 0) { break; } wealth.resize(numVillagers); for (int i = 0; i < numVillagers; i++) { scanf("%d", &(wealth[i])); } if (topCount > numVillagers) { topCount = numVillagers; } std::partial_sort(wealth.begin(), wealth.begin() + topCount, wealth.end(), std::greater<int>()); for (int i = 0; i < topCount - 1; i++) { printf("%d ", wealth[i]); } printf("%d\n", wealth[topCount - 1]); } } 此时的程序执行时间 156 ms，排名第 25 位。而截止到此时，我个人其实什么都没有干，实际任务都交给了 STL。 如同各种 MyString、MyVector 一样，一般情况下自己去实现这些通用算法在 STL 面前几乎没有任何优势。尤其是 C++11 带来 lambda 表达式以来，大大方便了 <algorithm> 中各种算法的使用，我觉得不去用 STL 的理由越发稀少了。 以上。
趁着双十一买了好几本想买的书，其中就有这本《Windows 核心编程》。首先需要吐槽的就是好好的《Windows via C/C++》这么高端大气上档次的名字怎么就被翻译成了这么个蛋疼样，而且中文版的封面也是扑面而来的一股浓郁的上个世纪气息，以至于在正文里看到 Vista 的字样都感觉各种穿越…… 本文是关于这本书的第一篇读书笔记。天知道会不会有第二篇。 虽说之前已经写过好几篇关于字符编码的文章，都快写吐了，不过读了这本书的开篇还是感觉相见恨晚。这里对于前面这几篇里关于字符串的说明进行一下查漏补缺。 Unicode 和 UTF-16 前文书说到，Unicode 定义的是码位（Code Point），即为每一个字符赋一个唯一编号，记作 U+XXXX。Unicode 目前实际占用了 016 到 10FFFF16 的码位，有些已经分配了字符，有些还没有。 Unicode 中最初推出的 U+0000 到 U+FFFF 这 216 个码位称为基本多文种平面（Basic Multilingual Plane，BMP）。早年只有这一个平面的时候，Unicode 与 UTF-16 是等价的，因为可以做到一一对应。然而时过境迁，区区 16 位已经满足不了 U+10000 到 U+10FFFF 的码位了。 与 UTF-8 编码规则的爽快不同，UTF-16 在沦为实实在在的「编码」时，还需要考虑到向后兼容性问题。如何在维持 U+0000 到 U+FFFF 一一对应的同时，把剩余的 U+10000 到 U+10FFFF 编进来？ 答案是：把 U+D800 到 U+DFFF 命名为 UTF-16 编码专用字符。即如果 UTF-16 数据流中出现了这一范围内的字符，意味着它本身不是一个字符，紧接着它的 16 位数据需要加上一定的偏移值才是真正的数据。 这一土豪做法让我目瞪口呆。至于 UTF-32，目前来说与它与 Unicode 码位一一对应起来绰绰有余。当然，如果哪天发现了外星文明，需要把他们的字符也编码进来，导致 Unicode 占用的码位暴增，说不定这一一对应的任务就只能交给未来的 UTF-64 了。这就是把问题留给子孙后代去解决的大智慧啊！ CHAR 和 LPSTR 前文书里吐槽过 Windows API 的丑陋不堪，也说过这也是历史遗留、不得已而为之。 没错的，CHAR 和 char 在语义上并不等价，前者专指 8 位字符，后者则只是字符而已，说不定在哪个古董机器上就是 7 位了。（尽管我不认为你会在那上面跑 Windows……） 而 LPSTR 尽管被定义为 CHAR *，但它实际上还借助了编译器扩展实现了「以 NUL 结尾」的语义： typedef __nullterminated CHAR *LPSTR; Unicode 和 ANSI 版本 说实话我依旧不喜欢官方的这两个名字，我更喜欢「宽字符版本」和「多字节字符版本」这两个更拗口但更准确的名字。 由于现代 Windows 内部都是以 UTF-16 存储（承袭自 UCS-2），实际提供的 API 本身也都是 Unicode 版本的，即以 W 结尾的版本。而相应的 A 结尾的 ANSI 版本则是在 W 版本基础上的封装。 于是可以想象，全局使用 ANSI 相比全局使用 Unicode 而言要占更多的内存，理论上也要更慢一些。 以上。
本文源自于今天对 neuront 童鞋的这篇文章的末尾的那段代码的 C++ 实现的思考。（好多「的」……） 尽管 std::accumulate() 和 Python 的 reduce() 类似，但因为 C艹 的 std::map<std::string, std::map<std::string, int>> 和 std::map<std::string, int> 是不同的类型，所以似乎只能自己用可变参数模板写一个了。 简单起见，我们还是退一步，来解决一个更简单、更不通用、而且似乎和 reduce() 完全无关的问题吧：如何才能一次性取得任意层次的字典值？用更直白的代码表达，就是我们需要一个 GetMapValue() 函数，实现这样的功能： // 用于缓解眼花缭乱感的宏 #define MAP_LITERAL(...) { __VA_ARGS__ } // 简单映射 std::map<std::string, std::string> simple_dict = MAP_LITERAL({"Hello", "World"}); // 「我勒个去居然这么麻烦」映射 std::map<std::string, std::map<std::string, std::map<std::string, int>>> nested_dict = MAP_LITERAL( { "x", MAP_LITERAL( { "y", MAP_LITERAL( { "a", 10 } ) }, { "z", MAP_LITERAL( { "b", 20 } ) } ) }); auto value1 = GetMapValue(simple_dict, "Hello"); std::cout << value1 << std::endl; auto value2 = GetMapValue(nested_dict, "x", "y", "a"); std::cout << value2 << std::endl; 初始的版本 初步分析，GetMapValue() 需要接受一个 Map 以及至少一个 Key。如此，参数可变，首选 C++11 的可变参数模板。一次性将所有 Key 拿到手后，每次用第一个 Key 获取下一级 Map，而后用余下的 Key 递归，最终获取所要的值。而要写递归，最好先从最简单的情况写起。 template <typename MapType> auto GetMapValue(MapType const& map, typename MapType::key_type const& key) -> typename MapType::mapped_type { return map.at(key); } 鉴于本人目前对于右值引用还不熟悉，就只用 const& 了（& 只能引用左值；const& 既能引用左值、又能引用右值，但无法修改）。 这里需要注意的是 STL 的几种 Map 里 typedef 到的数据类型，略坑： key_type 是 Key 的类型 mapped_type 是 Value 的类型 value_type 是 Map 中实际存储的键值对的具体类型（如 std::pair<key_type, mapped_type>） 不过这样一来，我们的 GetMapValue() 就可以在 STL 的这几种 Map 里通用了。 不被支持的递归 decltype 模板 本来我想，递归版本按照上面这个最简版本写一下就 OK 了。可变参数模板的基本用法我以前的日志有写过；这里唯一麻烦的是返回值，因为通用性，只有最终被调用的版本才知道确切的返回值，不过 decltype 似乎可以救场： template <typename MapType, typename... MoreKeyTypes> auto GetMapValue(MapType const& map, typename MapType::key_type const& key, typename MapType::mapped_type::key_type const& anotherKey, MoreKeyTypes... moreKeys) -> decltype(GetMapValue(map.at(key), anotherKey, moreKeys...)) { return GetMapValue(map.at(key), anotherKey, moreKeys...); } 然而，编译器不给面子，直接提示模板推导失败，decltype 时找不到接受两个 Key 版本的 GetMapValue()（三个参数）。 似乎，decltype 是不支持递归调用的，亦或者推导时自身还不存在。decltype 以及模板的一些规则真心还不是很熟，所以暂时没有找到真凭实据。 Traits 于是我终于知道为什么世界上会有 Traits 这种东西存在了。（比如 std::basic_string 的模板参数之一就是 class Traits = std::char_traits<CharT>。）Traits 是用来描述某个类型周边信息的东西。这里，我们需要一个 Traits 来计算一个 Map 被使用若干次 Key 后的 Value 类型。 那么很显然的，这个 MapTraits 依旧是可变参数模板、依旧是递归实现。 template <typename MapType, typename KeyType, typename... KeyTypes> struct MapTraits { typedef typename MapTraits<typename MapType::mapped_type, KeyTypes...>::mapped_type mapped_type; }; template <typename MapType, typename KeyType> struct MapTraits<MapType, KeyType> { typedef typename MapType::mapped_type mapped_type; }; 完全体 不出所料，最终的代码是一番如此纠结的景象。看这样的代码眼睛压力山大。 template <typename MapType> auto GetMapValue(MapType const& map, typename MapType::key_type const& key) -> typename MapType::mapped_type { return map.at(key); } template <typename MapType, typename... MoreKeyTypes> auto GetMapValue(MapType const& map, typename MapType::key_type const& key, typename MapType::mapped_type::key_type const& anotherKey, MoreKeyTypes... moreKeys) -> typename MapTraits<typename MapType::mapped_type, typename MapType::mapped_type::key_type, MoreKeyTypes...>::mapped_type { return GetMapValue(map.at(key), anotherKey, moreKeys...); } 完整的测试代码见这个 Gist：https://gist.github.com/timothyqiu/6877974 Traits 的小插曲 我第一次写的时候，把 MapTraits 的特化形式写成了这样： template <typename MapType> // 正式版本: typename MapType, typename KeyType struct MapTraits<MapType, typename MapType::key_type> // 正式版本: MapType, KeyType { typedef typename MapType::mapped_type mapped_type; }; 其实只是用 typename MapType::key_type 替换了一个正式版本里的模板参数 KeyType 而已。这样做其实也并非不可，只不过产出的是「高标准、严要求」的代码： // 正式版本可行，但是在这个版本里一堆模板错误 auto value2 = GetMapValue(nested_dict, "x", "y", "a"); // 两个版本都可行 auto value2 = GetMapValue(nested_dict, std::string("x"), std::string("y"), std::string("a")); 原理大致是，这个版本因为把特化形式的第二个模板参数固定成了第一个参数的 key_type，所以如果使用了和 key_type 不同但是可以隐式转换的类型，会导致模板推导失败。
C 语言中的字符串 抽象数据类型 C 语言贴近机器模型，所以并不提供字符串数据类型。但是，字符串却在 C 语言中作为 ADT 存在着：使用字符指针代表字符串，同时提供了 string.h 作为字符串的操作库，而在实际存储时则使用字符数组。 虽说单引号引起的字符是该字符对应的一个整型常量，但是 'A' 和 65 没有任何关系。因为 C 语言并没有要求每个实现都用 ASCII 字符集作为执行字符集。 事实上，C 语言标准要求每个 C 语言实现都要定义两个字符集：代码字符集、执行字符集。每个字符集又都由基础字符和扩展字符构成。扩展字符可有可无，完全由该实现所支持的区域设置决定；基础字符只划定了字符，字符对应的码位留由实现决定。 字符与 char 类型 字符集顾名思义就是字符的集合，字符集除了划定所包含的字符外，还将其所包含的字符按照一定顺序编号，这样的编号称作码位。著名的 ASCII 参照已有电报报文设计，分别用数字 0 到 127 作为英文大小写字母、阿拉伯数字、标点符号等的码位。由于出现早、使用广，后辈字符集、字符编码几乎都与之兼容为荣。 然而 ASCII 本身并不完美，尽管名字叫「美国信息交换标准代码」，但是却连英语中的一些诸如 naïve、café、élite 的外来词都无法表示，欧洲各国那些稀奇古怪的字母就更别提了。所幸，我们很快来到了一个字节 8 位的年代，char 能够容纳 0 到 255 一共 256 个值，于是各国纷纷物尽其用，将一个字节中 ASCII 并没有用到的 128 到 255 对应为自己国家所需要用到字符。这些五花八门的对应规则便是后来的代码页（code page），同样的值可以在不同的代码页中表示不同的字符。 到目前为止，char 类型还算是名副其实的字符类型。然而，东亚文化圈广泛使用的汉字成千上万，少说也需要上千个码位，一个字节所能提供的这区区 256 个码位实在是杯水车薪。 计算机科学里的任何问题都可以通过增加一个中间层来解决。 既然码位不得不超过 255，一个字节放不下，那么将以前的「码位→字节」变成「码位→编码→字节序列」如何？ 举个栗子：Unicode 字符集中，字符「汉」的码位是 27721（十六进制 6C49），一个字节显然容不下，我们可以通过 UTF-8 编码将其变成字节序列。 通过字符集找到对应码位：汉→ U+6C49； 根据 UTF-8 编码规则，介于 U+0800 和 U+FFFF 之间的码位需要按照 1110xxxx 10xxxxxx 10xxxxxx 的形式编码为三个字节； 6C49 的二进制形式是 0110 1100 0100 1001，代入上面的模板得到 11100110 10110001 10001001； 得到最终的字节序列 E6 B1 89。 看上去不错，但这样一来，char 这个「字符」类型就有点名不副实了：一个 char 可能只是一个字符的一部分，只有两个或者更多的 char 拼起来才能够表示一个字符。更为麻烦的是，相关的标准库也变得不再可靠了：类似 strlen() 这种 too simple, sometimes naive 的方法顿时从返回字符串的字符数降级成了返回字符串的字节数。 printf("%zu\n", strlen("\xE6\xB1\x89")); // 输出 3；但实际上只有一个字：汉 多字节字符串 为了解决 strlen() 的这种尴尬，C 语言中引入了「多字节字符串」（multibyte string）的概念，一个字符可以对应多个字节；而原先的字符串则对应称为「字节字符串」（byte string），一个字符只能对应一个字节。 char const *multibyteString = "Hello 世界"; // 和普通字符串没什么区别 多字节字符串依旧是 char *，所以这两者的区别其实只是抽象概念上的，何况严格来说，字节字符串也是一种特殊的多字节字符串。 不同的编码每个字符占用的字节数可能不同，即便是同一种编码，不同的字符也可能占用不同的字节数。所以多字节字符串面临的主要问题便是「下一个字符到底占几个字节」。C 语言引入了 mblen() 函数来回答这个问题： int mblen(char const *s, size_t n); 其中 s 是一个多字节字符串，而 n 表示至多检查多少个字节；这个函数会根据当前区域设置（Locale）的 LC_CTYPE 分类中所指定的编码来解析这个多字节字符串，返回该字符串的字一个字符占几个字节。 char const *multibyte= "\xE6\xB1\x89"; // 「汉」的 UTF-8 编码 printf("%d\n", mblen(multibyte , 5)); // 输出 -1；因为默认区域设置为 C，无法识别这样的序列。 setlocale(LC_CTYPE, "en_US.UTF-8"); // 非 Windows 平台用，Windows 平台不支持 UTF-8 的区域设置 printf("%d\n", mblen(multibyte, 5)); // 输出 3；因为当前区域设置为 UTF-8 编码。 宽字符 wchar_t 多字节字符串的引入终于算是「一定程度上解决了问题」，但这种做法实在繁琐。于是，C 语言同时引入了宽字符类型 wchar_t 和配套支持库 wchar.h，试图一劳永逸地解决这个问题。 wchar_t const *wideString = L"Hello World"; // 使用前缀 L 与 char 的一个字节不同，wchar_t 类型本身可以占多个字节，保证永远与字符一一对应，省下了编码的步骤。如此就可以通过 wchar.h 中提供的 wcslen() 函数正确计算 wchar_t * 字符串中的字符数了。 多字节字符串和宽字符串的转换 与 mblen() 类似，C 语言提供了两组共四个函数，根据当前的区域设置进行多字节字符（串）和宽字符（串）的转换。 wctomb() 将宽字符转换为多字节字符 mbtowc() 将多字节字符转换为宽字符 wcstombs() 将宽字符串转换为多字节字符串 mbstowcs() 将多字节字符串转换为宽字符串 误区 看到这里，你是否想过这个能够保证与字符一一对应的 wchar_t 类型到底需要占几个字节呢？没错，不一定占多少，但总之至少一个字节就对了！ 事实上，关于 wchar_t 的准确要求是：能够容纳下所支持的区域设置中，最大的扩展字符集中的每一个码位。 an integer type whose range of values can represent distinct codes for all members of the largest extended character set speciﬁed among the supported locales. 也就是说，如果有一个只支持 ASCII 字符集的奇葩的 C 语言实现存在，那么在这个实现下，wchar_t 完全可以只占 8 位。 如此，我们经常听到的「宽字符和 Unicode 有关」的说法也就不攻自破了。 C11 中引入的定长字符类型 时过境迁，转眼到了 2011 年。正如 C99 中在 stdint.h 中加入了 uint32_t 之类的定长整数类型一样，C11 也引入了两种定长字符类型以满足 Unicode 的需要： char16_t 占 16 位，2 字节，可以保存 UTF-16 字符。 char32_t 占 32 位，4 字节，可以保存 UTF-32 字符。 同时，继 string.h、wchar.h 后，又进入了 uchar.h 作为这两种字符类型的支持库。 char16_t *utf16String = u"Hello UTF-16"; // 使用前缀 u char32_t *utf32String = U"Hello UTF-32"; // 使用前缀 U C++ 中的字符串 C++ 极尽所能兼容 C 语言，陆续支持了和 C 语言相同的那些 char、wchar_t、char16_t 和 char32_t 类型，这里就不再重复了。 至于 std::string，用 C++ 不可能没听说过，它封装了常用的字符串操作。但你是否知道 std::wstring 的存在呢？ 如果有心，你会发现 std::string 实际上是对 std::basic_string<T> 模板的特化，形式类似于： typedef std::basic_string<char> std::string; typedef std::basic_string<wchar_t> std::wstring; 至于 C++11 中，由于 char16_t 和 char32_t 的引入，也对应增加了 std::u16string 和 std::u32string 的特化版本。 std::basic_string::data() 在 C++11 以来的改变 std::basic_string<T> 其实只是对 T 字符数组的封装，如果需要使用 C 风格的字符串 T *，可以通过 c_str() 方法获取。 而如果说 c_str() 方法表示「获取该类对应的 C 字符串表示」的语义，那么 data() 方法则表示「获取该类实际保存的数据」的语义。 在 C++11 之前，通过 data() 方法获取的 T * 并不保证 NUL 结尾，即仅保证 [data(), data() + size()) 有效。而自 C++11 起，data() 变为保证 NUL 结尾，与 c_str() 的行为完全相同。 从 C++17 开始，我们还可以 data() 方法获取可修改的缓冲。 Windows API 中的字符串类型 不得不说，Windows 的各种大写的自定义类型实在丑陋得令人发指。经常不由得让人发出「好好起名字会死么？」的感叹。 #define CHAR char #define LPSTR char * #define LPCSTR char const * #define WCHAR wchar_t #define LPWSTR wchar_t * #define LPCWSTR wchar_t const * 当然，微软如此做法自然有其历史原因，我对所谓的「向前兼容」表示非常无奈。不过话说回来，除了 Windows API 如此大张旗鼓地使用 wchar_t，其余地方真不怎么能见到宽字符的身影。现代 Windows 内部使用 UTF-16，所以 wchar_t 能够保证是 16 位的。 Windows API 中所有与字符串相关的函数、数据结构等，都存在 ANSI 和 Unicode 两个版本，可以通过宏 UNICODE 来切换。比如一个普通的 MessageBox() 函数，实际定义是这样的： int MessageBoxW(HWND hWnd, LPCWSTR text, LPCWSTR caption, UINT type); // 宽字符串版本 int MessageBoxA(HWND hWnd, LPCSTR text, LPCSTR caption, UINT type); // 多字节字符串版本 #ifdef UNICODE #define MessageBox MessageBoxW #else #define MessageBox MessageBoxA #endif 字符类型也是类似，使用 TCHAR 作为一个通用的字符名字。字符常量则使用 TEXT() 宏来定义。 #ifdef UNICODE #define TCHAR WCHAR #define TEXT(s) L ## s #else #define TCHAR CHAR #define TEXT(s) s #endif 如果你用的是 Visual C++，这个 UNICODE 宏通常并不需要手动设置，而是作为工程属性的一员出现的。 p.s. 不要被这个宏的名字骗了，还记得之前我们说过的 wchar_t 和 Unicode 一点关系都没有吗？可以这么理解：wchar_t 只在 Windows API 中表示 UTF-16 数据。 宽窄字符串的转换 Windows 平台也可以用之前提到的四个函数转换 char 字符串和 wchar_t 字符串。但 Windows 本身提供的 API 似乎更有效率： MultiByteToWideChar() 多字节字符串转换为宽字符串 WideCharToMultiByte() 宽字符串转换为多字节字符串 再次吐槽 M$ 这蛋疼的命名，命名是转换字符串，起名字起得跟转换单个字符一样。 BSTR BSTR 可以解释为 Basic String 或者 Binary String，是 Windows 下用于 COM、自动化的字符串类型。定义为： typedef OLECHAR *BSTR; OLECHAR 是 Windows 的历史遗留，类似于根据宏 OLE2ANSI 转换的 TCHAR，但 OLE2ANSI 是 16 位时代的产物，于是 Win32 环境下，OLECHAR 永远是 WCHAR，也就是 wchar_t。 尽管如此，BSTR 并不能等同于 OLECHAR *，因为它们的内存布局不同：BSTR 在它自身前拥有一个四个字节的前缀、以双 NUL 结尾。其中，前缀用于描述字符串内容所占的字节数，它本身所指向的是字符串内容而非长度。 -4 0 2 4 6 8 10 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | 6 | H | i | ! | NUL | NUL | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ ↑ bString 这一点，从用于创建 BSTR 的接口 SysAllocString() 便可以看出： BSTR SysAllocString(OLECHAR const *psz); 倘若 BSTR 与 OLECHAR * 相同，那么岂不变成了鸡生蛋、蛋生鸡的问题？ 虎头蛇尾 呃……前面讲 C 语言字符串的时候似乎还是觉得挺有说头，足足规划了一个星期之久，但后面似乎越来越不知道该讲什么，留待日后积累到新的东西再完善吧～ 以上。
众所周知，char 类型是独立于 unsigned char 和 signed char 之外的一种实现相关类型，这着实带来了不少麻烦。 std::string a = "Hello"; std::string b = "世界"; // UTF-8 或者 GBK 皆可 那么请问，a[0] < b[0] 是什么结果？OK，那么 a < b 呢？ 当然，貌似在开篇头一句就已经被我剧透的情况下作出错误的回答还是蛮困难的。没错，无论是 GBK 还是 UTF-8 存储，答案都应该是： a[0] < b[0] 与实现相关。在我的 Windows 7 32-bit 和 Arch Linux 64-bit 上，都是 false a < b 永远为 true 前者很好理解，因为实质是两个 char 在比较，所以谁大谁小还得看 char 到底是无符号的还是有符号的。而后者为什么肯定是 true 呢？ 尽管 std::string 是 std::basic_string 在字符类型为 char 时的特化，但 C++ 标准中明确规定： The two-argument members eq and lt shall be defined identically to the built-in operators == and < for type unsigned char. 也算是 C++ 造福人类的某种方式吧。 以上。
上一次看 cocos2d-x 是去年的事情了，下了个 SDK 而后按照官方的那个 pew-pew-pew 教程接二连三捣鼓了几下，之后几乎就没怎么去碰了。最近再看感觉连当时自己写的代码都快不认识了，果然还是需要记下笔记 = = 获取 SDK 前往 cocos2d-x 官网下载稳定版打包，或者去官方 GitHub 仓库 git clone 一份（注意看清分支哟）。 个人这回是下载了最新稳定版本，目前是 2.1.4。 创建工程 Cocos2d-x 由若干部分组成，有些采用静态链接、有些则是动态链接、更有些是直接源代码分发的；加上工程本身的跨平台属性，需要针对不同平台写初始化代码；总而言之，从零开始手写一个空工程是相当繁琐的。 所以老版本的策略是：为不同平台下的开发工具提供模板、脚本之类的东西以便获得每次的初始空工程。比如它会为 Visual Studio 安装一个 cocos2d-x 程序模板，这样就可以直接在新建对话框里选择新建一个 Visual C++ 的空 cocos2d-x 工程了。 当然，从 2.1.4 版本开始，官方就不再为 VS 提供模板了，逐步在各平台采用统一的 Python 脚本创建跨平台工程。（吐槽：虽然不提供模板了，但是原先用来安装模板的 install-templates-msvc.bat 文件居然还在，虽然一运行就报错的说……） 这个创建脚本使用 Python 2.X，同时它还有一个 Bug：使用当前目录而不是脚本文件所在目录查找模板文件（GitHub 上的 develop 版已经修复）。所以，要创建工程，我们需要先从命令行进入 tools/project-creator 目录，然后再使用 create_project.py -project <工程名称> -package <Java 包名> -language <语言> 就会在 projects 目录下创建相应的工程。这一点在直接运行 create_project.py 时会给出提示。（3.0 版本将在 cocos2d-x 根目录提供 create-multi-platform-projects.py 文件为你间接调用 tools/project-creator/create_project.py）那么作为一个示例，我们可以输入： create_project.py -project Whatever -package com.timothyqiu.Whatever -language cpp 咳咳，如果你对为什么语言是 C++ 也要输入「Java 包名」感到困惑，那么看一眼生成后的工程目录结构就明白了： Whatever -+- Classes |- Resources |- proj.android |- proj.blackberry |- proj.ios |- proj.linux |- proj.mac |- proj.marmalade `- proj.win32 结构非常精巧：平台无关的（自己写的逻辑之类的）代码都在 Classes 目录下；程序用到的资源都在 Resources 目录下；其余的平台相关（预置的初始化之类的）代码及工程文件都在各自的目录下。 所以，既然有 Android 工程在，那么创建时显然是需要包名的。 渲染树 如果你熟悉 3D 也许你也熟悉「渲染树」的概念：加入渲染树的节点才能够被渲染，而节点的父子关系也对其位移、缩放、旋转有相应影响。渲染一帧图像的过程就是遍历整棵渲染树、依次绘制各个节点的过程。 cocos2d-x 中的节点，以 CCNode 表示，它除了作为一棵渲染树的节点外，还提供了定时回调以及执行 CCAction 动作的功能。当然， CCNode 还封装了一些类似位置、缩放、旋转的基本属性，其中锚点（Anchor Point）是非常重要的属性：节点的位置和节点左上角、右下角的位置是什么关系？缩放以哪个点为定点？旋转以哪个点为中心？这些问题的答案就是锚点。 cocos2d-x 中对象的属性使用 Getter/Setter 封装，均为 setPropertyName(value) 和 getPropertyName()、isPropertyName() 的形式。一些 CCNode 常用属性如下： 位置（Position）默认为 (0, 0)，是一个 CCPoint 类型的值。 旋转（Rotation）默认为 0，顺时针以角度计。 缩放（Scale）默认为 1.0f。 尺寸（ContentSize）默认为 (0, 0)，是一个 CCSize 类型的值。 可见（Visible）默认为 true。不可见和不存在是两码事。 锚点（AnchorPoint）默认为 (0, 0)。每个分量都是一个浮点数，0.0f 表示左上，1.0f 表示右下（所以 0.5f 表示中心点）。取值也可以不在 0 和 1 的范围内，表示锚点位置在节点范围外。 另外还有 Tag、UserData、UserObject 三人组，类型分别是 int、void * 和 CCObject，都是用户为节点自定义的数据，CCNode 只是把它存起来以供你日后获取，不作它用。 CCNode 常见的一些子类包括： CCScene 场景，一般作为渲染树的根节点存在 CCLayer 层，一般作为精灵的容器存在，能够接受外部输入（触摸事件、加速计等） CCSprite 精灵，图像节点 CCMenu 抽象菜单，可以添加 CCMenuItem 的 UI 元素。本身是从 CCLayer 派生的 出于拯救大众于水火的目的，以上子类的锚点都被默认设成了 (0.5, 0.5)。 p.s. cocos2d-x 中的绝大多数类均以 CC 作为前缀。但这样的做法明显是与它们所在的 cocos2d 这个 namespace 重复的，所幸在将来的 3.X 新版本中这些重复的前缀都会被删掉。 无规矩不成方圆 cocos2d-x 基于 OpenGL，故使用右手坐标系：从左到右、从下到上为坐标轴正方向。 前面说过的 CCPoint 类型，可以用来表示坐标点，也可以用来表示向量；而 CCSize 类型类似，用来表示尺寸；另外还有一个 CCRect 是 CCPoint 和 CCSize 的组合，表示矩形区域的左上角和尺寸。 因为 CCPoint 和 CCSize 实质都是两个 float 的组合，所以其构造函数也都需要两个 float。介于每次传入不同类型变量的都显式去 static_cast<float> 过于手残，cocos2d-x 给出了三个对应的宏来帮你写转型：CCPointMake(x, y)、CCSizeMake(w, h) 和 CCRectMake(x, y, w, h)。当然，CCPoint 太常用了，以至于它还有一个更省击键数的宏 ccp(x, y)。 前面还说过旋转属性，它是一个角度值。cocos2d-x 提供了 CC_DEGREES_TO_RADIANS(deg) 和 CC_RADIANS_TO_DEGREES(rad) 宏进行弧度、角度的换算。 内存管理 cocos2d-x 使用了来自 cocos2d-iphone 的 Objective-C 风格，这不仅体现在代码风格上，还体现在内存管理风格上。 Objective-C 的内存管理使用的是（手动）引用计数技术。简而言之，就是对象默认存在 1 个引用、用 retain() 可以增加一个引用、用 release() 可以减少一个引用。一旦引用减少到 0，对象自动释放。 聪明的你一定想到了不少实现方法。不过由于目前版本中所有对象都只是裸指针（例如 CCDirecter *director 的本质是一个指针而非对象），所以，似乎在这一前提下实现引用计数的唯一方法就是：所有对象都继承自同一根类，由这个根类负责进行引用计数工作。当然如此一来，所有对象都应该创建在堆上。 顺带一提，cocos2d-x 当前版本并没有使用智能指针，一是因为 cocos2d-iphone 就是如此，二是因为 cocos2d-x 项目启动时 C++11 仍未定稿，三是因为当时各手机平台上支持情况不一样。不过时过境迁，在 2013 都过去了一半的现在，官方已经决定在 3.0 新版本里引入 C++11 的智能指针。 根对象 cocos2d-x 用来提供引用计数功能的根类就是 CCObject。主打这四个函数： CCObject::CCObject() CCObject::retain() CCObject::release() CCObject::autorelease() 前三个好说，分别是将当前对象的引用计数置一、增一、减一。那么最后一个 autorelease() 是干啥的？ 借用 Objective-C 中的解释：release() 是引用计数立即减一，而 autorelease() 则是在不久的将来减一（至少在当前函数之后）。 在 cocos2d-x 的实现中，使用了名叫 CCAutoReleasePool 的「自动释放池」对象。它会在自身被释放（析构）时调用其所持有的所有对象的 release() 方法。CCObject::autorelease() 所做的正是将自己加入自动释放池中。 cocos2d-x 会在每一帧的开始创建一个 CCAutoReleasePool，并在该帧结束时释放它。所以，所有被调用过 autorelease() 的 CCObject 都有机会在当前帧结束后自动释放。（之所以是「有机会」，因为即使调用 release() 同样也只是有机会释放，是否真正释放由引用计数决定。） 所以，大致小结一下就是： 每一次 new 或者 retain() 都应该对应存在一次 autorelease() 或者 release() autorelease() 是有代价的，所以应该尽量使用 release() 创建和销毁 cocos2d-x 中没有使用异常，所以对象的创建使用了两步构造法（Two-phase Construction）。所谓两步构造法，就是构造函数只用来为变量赋初值而不执行逻辑相关代码，所有初始化用的功能代码另写一个初始化函数。（构造函数没有返回值，所以只能用异常与外界沟通。） CCSprite *sprite = new CCSprite(); // 创建对象 sprite->initWithFile("background.png"); // 初始化对象 上面这两行就是两步构造法的一个实例。介于这么写相当的繁琐，cocos2d-x 为每一个 initXXX() 都提供一个相应的工厂方法 createXXX()： CCSprite *sprite = CCSprite::create("background.png"); 其实现很简单： CCSprite *pobSprite = new CCSprite(); if (pobSprite && pobSprite->initWithFile(pszFileName)) { pobSprite->autorelease(); return pobSprite; } CC_SAFE_DELETE(pobSprite); return NULL; 注意到其中的 autorelease() 了吗？这意味着，如果只是 create() 而没有做其它任何操作，那么这个对象会在本帧结束时释放；但如果在 create() 后调用了 sprite->retain()，那么这个对象就不会自动释放（如果是 this->addChild(sprite) 则同理，因为 addChild() 会间接调用 retain()）。 需要注意的是，这种创建方式也导致父类无法自动初始化，必须在子类的 initXXX() 方法里手动去调用直接父类的 initXXX() 方法。于是（如果手写），一般的初始化都类似于： bool HelloWorldLayer::init() { bool success = false; do { CC_BREAK_IF(!CCLayer::init()); // actual init code here, break if failed success = true; } while (0); // clean up if nescesary return success; } 容器 因为 cocos2d-x 的这种「特殊」内存管理方式，对 retain()、release() 一无所知的 STL 容器或许并不适合你，于是便有了 CCArray、CCSet、CCDirectory。（在 STL 容器中直接存放裸指针，可能由于野指针带来各种麻烦。当然，如果考虑使用智能指针的话，存入对应 std::shared_ptr 即可保证容器生命周期内所含对象有效；而 cocos2d-x 在未来版本中全面引入智能指针代替 Objective-C 风格的内存管理后，单存入 std::weak_ptr 就可以检测到野指针了。） cocos2d-x 所提供的这些容器所存放的都是 CCObject 对象（其实是指针，但鉴于正确的使用方法只可能是指针，所以方便起见以后就不强调是指针了）。在对象进入容器时，容器自动调用一次 retain() 取得其引用；在对象移出容器或是容器本身被销毁时，容器自动为对象调用一次 release() 放弃引用。 这样就保证了所有在这些容器中的对象都是有效的对象。（还记得 CCAutoReleasePool 吗？它就是用 CCArray 实现的。） 对于容器，除却添加删除，常用的还有遍历。与 STL 提供迭代器不同，CCArray 和 CCDirectory 都是用 For Each 宏来遍历的。（CCSet 和 STL 相同。） CCObject *element = nullptr; CCARRAY_FOREACH(arrayOfSprites, element) { auto sprite = dynamic_cast<CCSprite *>(element); // ... } 导演 CCDirector 类就是导演类，（总）导演每部戏只有一个，所以它是个单件类。cocos2d-x 中所有单件类都通过类似 CCName::sharedName() 的方式获得 CCName * 单件对象。这种（命名）习惯是从 cocos2d-iphone 里带来的，在 3.X 中，将会变成 CCName::getInstance() 这种更方便的方式。 导演的主打功能是场景调度和游戏流程控制。 runWithScene() 开始运行，设置初始场景 replaceScene() 切换场景，不保留旧场景 pushScene()、popScene() 常见的场景栈式管理 pause() 暂停当前场景（仍有会绘制场景，但是不会执行逻辑） resume() 恢复当前场景 end() 结束运行 细枝末节 在 SDK 为我们提供的空工程中，除了上面提到的这些「大头」，还有一些零碎的细枝末节。 入口 事实上，每一个 proj.* 目录下都有自己平台的初始化代码，也就是各自类似于 main() 的东东。 而在 Classes 目录下，平台无关代码中，程序的入口便是 AppDelegate 类。没错，这个类（依旧）源自 cocos2d-iphone，确切的说是 iOS，个人理解，这是一个用来接收程序 UI 状态消息的类。 程序通过其中仅有的三个顾名思义的方法 applicationDidFinishLaunching() applicationDidEnterBackground() applicationWillEnterForeground() 来感知自身的状态，以作出反应。比如 Did finish launching 时初始化引擎；Did enter background 时暂停背景音乐、暂存状态；Will enter foreground 时取出状态、恢复背景音乐等等。 现有的「空工程」中，applicationDidFinishLaunching() 里的核心其实就是这两句： CCScene *pScene = HelloWorld::scene(); pDirector->runWithScene(pScene); 它搭建了 Classes 目录下 AppDelegate 和 HelloWorld 这两个类的桥梁：将场景创建出来后交给导演，然后导演就自顾自干活去了。 选择器 在 HelloWorld.cpp 中，我们可以看到这样的语句： CCMenuItemImage *pCloseItem = CCMenuItemImage::create( "CloseNormal.png", "CloseSelected.png", this, menu_selector(HelloWorld::menuCloseCallback) ); 这个 menu_selector 好神奇，究竟是什么？ 好吧，这货（没错，还）是从 cocos2d-iphone 来的，我们来看实际代码（2.1.4 版本的 C 风格转换看着不爽，这里使用 C++ 风格类型转换稍作修改）： typedef void (CCObject::*SEL_MenuHandler)(CCObject*); #define menu_selector(_SELECTOR) static_cast<cocos2d::SEL_MenuHandler>(&_SELECTOR) 你看，没什么大不了的，各种 selector 都只是「成员函数指针 + 强制类型转换」而已，它和它的前一个参数（CCObject *）共同指定了一个回调。（回想一下，你还记得 C++ 里该如何定义、初始化、使用成员函数指针吗？） 当然，在不久的将来，3.X 版本会使用更灵活的 std::function 和 std::bind 来代替这种「selector」。 以上。