当前位置: 首页 > news >正文

Google C++ Style / 谷歌C++开源风格

文章目录

  • 前言
  • 1. 头文件
    • 1.1 自给自足的头文件
    • 1.2 #define 防护符
    • 1.3 导入你的依赖
    • 1.4 前向声明
    • 1.5 内联函数
    • 1.6 `#include` 的路径及顺序
  • 2. 作用域
    • 2.1 命名空间
    • 2.2 内部链接
    • 2.3 非成员函数、静态成员函数和全局函数
    • 2.4 局部变量
    • 2.5 静态和全局变量
    • 2.6 thread_local 变量
  • 3. 类
    • 3.1 构造函数的内部操作
    • 3.2 隐式类型转换
    • 3.3 可拷贝类型和可移动类型
    • 3.4 结构体还是类
    • 3.5 结构体、数对还是元组
    • 3.6 继承
    • 3.7 运算符重载
    • 3.8 访问控制
    • 3.9 声明次序
  • 4. 函数
    • 4.1 输入和输出
    • 4.2 编写简短函数
    • 4.3 函数重载
    • 4.4 缺省参数
    • 4.5 函数返回类型后置语法
  • 5. 来自Google的奇技
    • 5.1 所有权与智能指针
    • 5.2 Cpplint
  • 6. 其他C++特性
    • 6.1 右值引用
    • 6.2 函数重载
    • 6.3 缺省参数
    • 6.4 变长数组和 alloca ()
    • 6.5 友元
    • 6.6 异常
    • 6.7 运行时类型识别(RTTI)
    • 6.8 类型转换
    • 6.9 流
    • 6.10 前置自增和自减
    • 6.11 const 用法
    • 6.12 constexpr 用法
    • 6.13 整型
    • 6.14 64 位下的可移植性
    • 6.15 预处理宏
    • 6.16 0、nullptr 和 NULL
    • 6.17 sizeof
    • 6.18 auto
    • 6.19 列表初始化
    • 6.20 Lambda 表达式
    • 6.21 模板编程
    • 6.22 Boost 库
    • 6.23 C++11
  • 7. 命名约定
    • 7.1 通用命名规则
    • 7.2 文件命名
    • 7.3 类型命名
    • 7.4 变量命名
    • 7.5 常量命名
    • 7.6. 函数命名
    • 7.7 命名空间命名
    • 7.8 枚举命名
    • 7.9 宏命名
    • 7.10 命名规则特例
  • 8. 注释
    • 8.1 注释风格
    • 8.2 文件注释
    • 8.3 类注释
    • 8.4 函数注释
    • 8.5 变量注释
    • 8.6 实现注释
    • 8.7 标点,拼写和语法
    • 8.8 TODO 注释
    • 8.9 弃用注释
  • 9. 格式
    • 9.1 行长度
    • 9.2 非 ASCII 字符
    • 9.3 空格还是制表位
    • 9.4 函数声明与定义
    • 9.5 Lambda 表达式
    • 9.6 函数调用
    • 9.7 列表初始化格式
    • 9.8 条件语句
    • 9.9 循环和开关选择语句
    • 9.10 指针和引用表达式
    • 9.11 布尔表达式
    • 9.12 函数返回值
    • 9.13 变量及数组初始化
    • 9.14 预处理指令
    • 9.15 类格式
    • 9.16 构造函数初始值列表
    • 9.17 命名空间格式化
    • 9.18 水平留白
    • 9.19 垂直留白

前言

  • Google C++ Style Guide
  • Google 开源项目风格指南 - 中文版

Google C++风格进行了总结,主要依照上面的中文版内容,此外我增加了一些旁注,方便阅读时理解,部分内容我只进行了很简单的呈现,详见上面的Google官网和中文翻译官网。阅读Google C++ Style可以学会更好的使用C++,阅读时也能再次比较深入的理解C++的特性,如何写出好的C++推荐Effective C++。

这里附一张大佬做的一张图总结Google C++编程规范(Google C++ Style Guide)。

20140713220242000.png

1. 头文件

每个.cc文件应有一个配套的.h文件,单元测试和仅有main()函数的.cc文件是常见例外。

正确使用头文件会大大改善代码的可读性和执行文件的大小、性能。

1.1 自给自足的头文件

头文件应该自给自足 (可以独立编译),并以 .h 为扩展名. 头文件要有头文件防护符 (1.2** #define 防护符),并导入它所需的所有其它头文件。

若头文件声明了内联函数或模版,而且头文件的使用者需要实例化这些组件时,头文件必须直接或通过导入的文件间接提供这些组件的实现 。

内联函数的这种替换行为是在编译阶段发生的,编译器需要知道内联函数的完整定义才能进行替换操作。

模板的实例化是由编译器根据模板的使用情况在编译时完成的。和内联函数类似,编译器需要知道模板的完整定义才能进行实例化。

1.2 #define 防护符

所有头文件都应该用 #define 防护符来防止重复导入防护符的格式是: 项目>_<路径>_<文件名>_H_

为了保证符号的唯一性,防护符的名称应该基于该文件在项目目录中的完整文件路径。例如,foo 项目中的文件 foo/src/bar/baz.h 应该有如下防护:

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif  // FOO_BAR_BAZ_H_

1.3 导入你的依赖

若代码文件或头文件引用了其他地方定义的符号,该文件应该直接导入提供该符号的声明或者定义的头文件。不应该为了其他原因而导入头文件。

不要依赖间接导入. 这样,人们删除不再需要的 #include 语句时,才不会影响使用者. 此规则也适用于配套的文件:若 foo.cc 使用了 bar.h 的符号,就需要导入 bar.h,即使 foo.h 已经导入了 bar.h

1.4 前向声明

定义:前向声明 (forward declaration) 是没有对应定义的声明

// 在 C++ 源码文件中:
class B; // 前向声明
void FuncInB();
extern int variable_in_b;
ABSL_DECLARE_FLAG(flag_in_b);

优点:节约编译时间、避免不必要的重复编译。

缺点:隐藏依赖关系、自动化工具识别困难、限制库的修改与维护、对std::命名空间的符号进行前向声明可能引发未定义行为、使用判断困难及代码含义改变风险、代码冗长与性能及复杂度问题。

尽量避免使用前向声明(弊大于利),应导入所需头文件。

1.5 内联函数

只把 10 行以下的小函数定义为内联 (inline)。

优点:只要内联函数体积较小,内联函数可以令目标代码更加高效。鼓励对存取函数用于获取类中成员变量值的函数)、变异函数(用于修改类中成员变量值的函数)和其它短小且影响性能的函数使用内联展开。

缺点:滥用内联将拖慢程序。根据函数体积,内联可能会增加或减少代码体积。通常,内联展开非常短小的存取函数会减少代码大小,但内联一个巨大的函数将显著增加代码大小。在现代处理器上,通常代码越小执行越快,因为指令缓存利用率高。

注意:谨慎对待析构函数。析构函数往往比表面上更长,因为会暗中调用成员和基类的析构函数!虚函数和递归函数通常不会被内联。

1.6 #include 的路径及顺序

推荐按照以下顺序导入头文件:配套的头文件,C 语言系统库头文件,C++ 标准库头文件,其他库的头文件,本项目的头文件。

头文件的路径应相对于项目源码目录,不能出现 UNIX 目录别名 . (当前目录) 或 .. (上级目录). 例如,应该按如下方式导入 google-awesome-project/src/base/logging.h

#include "base/logging.h"

注意 C 语言头文件 (如 stddef.h) 和对应的 C++ 头文件 (cstddef) 是等效的。两种风格都可以接受,但是最好和现有代码保持一致。

举例来说,google-awesome-project/src/foo/internal/fooserver.cc 的导入语句如下:

#include "foo/server/fooserver.h"#include <sys/types.h>
#include <unistd.h>#include <string>
#include <vector>#include "base/basictypes.h"
#include "foo/server/bar.h"
#include "third_party/absl/flags/flag.h"

**例外:**有时平台相关的代码需要有条件地导入,此时可以在其他导入语句后放置条件导入语句。 当然,尽量保持平台相关的代码简洁且影响范围小。 例如:

#include "foo/public/fooserver.h"#include "base/port.h"  // 为了 LANG_CXX11.#ifdef LANG_CXX11
#include <initializer_list>
#endif  // LANG_CXX11

2. 作用域

2.1 命名空间

**除了少数特殊情况,都应该在命名空间内放置代码。**命名空间应该有独一无二的名字,其中包含项目名称,也可以选择性地包含文件路径。禁止使用 using 指令 (例如 using namespace foo)。 禁止使用内联命名空间。

定义:命名空间可以将全局作用域 划分为独立的、有名字的作用域,因此可以有效防止全局作用域中的命名冲突。

优点:在大型程序里,不同项目或模块可能会定义同名的符号(比如都定义了名为Foo的类),若没有命名空间进行区分,在编译或运行时就会产生冲突。而将代码放置在各自的命名空间下(如project1::Fooproject2::Foo),就能让这些原本同名的符号成为不同作用域下的独立符号,避免了命名冲突,保障程序顺利运行。

内联命名空间会自动把其中的标识符置入外层作用域,比如:

namespace outer {
inline namespace inner {void foo();
}  // namespace inner
}  // namespace outer

此时表达式 outer::inner::foo()outer::foo() 等效. 内联命名空间的主要用途是保持不同 ABI 版本之间的兼容性。

缺点

  • 理解难度增加:命名空间的存在使得代码阅读和理解变得相对困难,因为要确定一个标识符对应的定义,需要清楚它所在的命名空间,可能涉及多层嵌套等情况,查找起来不如全局作用域下直观。

  • 内联命名空间特殊问题:内联命名空间更是加大了理解复杂度,其内部标识符不仅出现在自身声明的命名空间内,还会被置入外层作用域,导致代码逻辑的追踪变得更复杂,并且它主要用途局限于作为版本控制策略的一部分,适用场景较窄。

  • 代码冗长问题:在部分场景下,为了准确引用符号,需要使用完全限定名称,若命名空间存在多层嵌套,书写起来会让代码显得冗长,降低代码的简洁性。

使用建议

  • 基本使用规范:遵循命名空间命名规则,并且像示例那样通过注释注明命名空间名字,方便阅读代码时快速知晓。在导入语句、gflags声明 / 定义以及其他命名空间的类的前向声明完成后,用命名空间包裹整个源代码文件,无论是.h文件中的声明部分还是.cc文件中的函数定义部分,都放置在对应的命名空间内,保持代码结构清晰且符合规范。
// .h 文件
namespace mynamespace {// 所有声明都位于命名空间中.
// 注意没有缩进.
class MyClass {public:...void Foo();
};}  // namespace mynamespace// .cc 文件
namespace mynamespace {// 函数定义位于命名空间中.
void MyClass::Foo() {...
}}  // namespace mynamespace
  • 与标准库相关:明确禁止在std命名空间内声明任何东西,也不要对标准库的类进行前向声明。
  • 命名空间别名使用限制:一般不允许头文件引入命名空间别名,除非是明显标注为内部使用的命名空间,
  • 内联命名空间禁用:明确禁止使用内联命名空间,避免引入不必要的代码维护和理解问题。
  • 特定命名空间标识:如果命名空间名称包含 “internal”,意味着这属于内部使用的 API,外部用户不应使用相关符号,以此来区分公开和内部的代码逻辑。
// Absl 以外的代码不应该使用这一内部符号.
using ::absl::container_internal::ImplementationDetail;
  • 嵌套命名空间声明鼓励形式:鼓励新代码采用单行的嵌套命名空间声明方式,这种形式相对简洁明了,符合代码书写的简洁性和规范性趋势。
namespace foo::bar {
...
}  // namespace foo::bar

2.2 内部链接

若其他文件不需要使用 .cc 文件中的定义,这些定义可以放入匿名命名空间或声明为 static,以实现内部链接。但是不要在 .h 文件中使用这些手段。

内部链接与声明为static类似。它们都将所修饰的实体(函数或者变量)的作用域限制在一个特定的编译单元(通常是一个.cc文件)内。

定义:所有放入匿名命名空间中的声明都会内部链接。声明为 static 的函数和变量也会内部链接。这意味着其他文件不能访问你声明的任何事物。即使另一个文件声明了一模一样的名称,这两个实体也都是相互独立的。

结论:建议 .cc 文件中所有不需要外部使用的代码采用内部链接。不要在 .h 文件中使用内部链接.匿名命名空间的声明应与具名命名空间的格式相同。在末尾的注释中,不用填写命名空间名称:

namespace {
...
}  // namespace

2.3 非成员函数、静态成员函数和全局函数

建议将非成员函数放入命名空间。尽量不要使用完全全局的函数。不要仅仅为了给静态成员分组而使用类。类的静态方法应当和类的实例或静态数据紧密相关。

优点:非成员函数和静态成员函数在某些情况下有用。若将非成员函数放在命名空间内,可以避免命名冲突,不会污染全局命名空间。

缺点:有时非成员函数和静态成员函数更适合成为一个新的类的成员,尤其是当它们需要访问外部资源或有明显的依赖关系时。

在良好的面向对象设计中,类应该是对具有紧密关联的属性和行为的封装。如果只是因为函数需要访问外部资源或有依赖关系就将非成员函数或静态成员函数随意纳入一个新类,可能会导致类的职责不明确。

从代码阅读者的角度来看,当一个类包含了很多原本作为非成员函数或静态成员函数添加进来的操作时,会使类的功能变得难以理解。

结论:有时我们需要定义一个和类的实例无关的函数,这样的函数可以定义为静态成员函数或非成员函数。非成员函数不应该依赖外部变量,且大部分情况下应该位于命名空间中。不要仅仅为了给静态成员分组而创建一个新类,这相当于给所有名称添加一个公共前缀,而这样的分组通常是不必要的。如果你定义的非成员函数仅供本 .cc 文件使用,请用内部链接限制其作用域。

2.4 局部变量

应该尽可能缩小函数变量的作用域,并在声明的同时初始化。

尽可能缩小变量的作用域,且声明离第一次使用的位置越近越好。更容易找到声明,了解变量的类型和初始值. 特别地,应该直接初始化变量而非先声明再赋值,比如:

int i;
i = f();     // 不好: 初始化和声明分离.
int i = f(); // 良好: 声明时初始化。
int jobs = NumJobs();
// 更多代码...
f(jobs);      // 不好: 初始化和使用位置分离.
int jobs = NumJobs();
f(jobs);      // 良好: 初始化以后立即 (或很快) 使用.
vector<int> v;
v.push_back(1);  // 用花括号初始化更好.
v.push_back(2);
vector<int> v = {1, 2}; // 良好: 立即初始化 v.

通常应该在语句内声明用于 ifwhilefor 语句的变量,这样会把作用域限制在语句内. 例如:

while (const char* p = strchr(str, '/')) str = p + 1;

需要注意的是,如果变量是一个对象,那么它每次进入作用域时会调用构造函数,每次退出作用域时都会调用析构函数.

// 低效的实现:
for (int i = 0; i < 1000000; ++i) {Foo f;  // 调用 1000000 次构造函数和析构函数.f.DoSomething(i);
}

在循环的作用域外面声明这类变量更高效:

Foo f;  // 调用 1 次构造函数和析构函数.
for (int i = 0; i < 1000000; ++i) {f.DoSomething(i);
}

2.5 静态和全局变量

禁止使用静态储存周期的变量,除非它们可以平凡地析构。

平凡初始化:简单直接的初始化方式,通常在编译阶段就能完成大部分工作,不需要复杂的运行时操作。这种初始化方式主要涉及基本数据类型,如intcharfloat等。

不平凡初始化:涉及复杂的操作,通常需要在运行时执行,并且可能包括动态内存分配、调用复杂的构造函数、从外部资源获取数据等来初始化对象。如String类的构造函数中,需要动态分配内存(new char[length + 1])。

平凡析构:简单的析构函数,它不执行任何用户定义的、具有实质性影响的操作。

不平凡析构:在类中进行了资源分配(如动态内存分配、获取文件句柄、获取网络套接字等),在析构函数中就需要释放这些资源,以避免资源泄漏。

定义:每个对象都有与生命周期相关的储存周期。静态储存周期对象的存活时间是从程序初始化开始,到程序结束为止。这些对象可能是命名空间作用域内的变量 (全局变量)、类的静态数据成员或者用 static 修饰符声明的函数局部变量。对于函数局部静态变量,初始化发生在在控制流第一次经过声明时,所有其他对象会在程序启动时初始化。程序退出时会销毁所有静态储存周期的对象 (这发生在未汇合 (join) 的线程终止前)。

初始化过程可以是动态的,也就是初始化过程中有不平凡的操作。其他初始化都是静态初始化。二者并非水火不容:静态储存周期的变量 一定 会静态初始化 (初始化为指定常量或给所有字节清零),必要时会随后再次动态初始化。

优点:静态储存周期的变量(特别是全局或静态变量)在很多场景下都能发挥作用,例如可充当具名常量方便代码中常量的使用和理解;作为编译单元内部的辅助数据结构来辅助实现某些功能;用于存储命令行旗标以控制程序不同的运行行为;记录日志信息;实现注册机制方便模块注册管理;搭建后台基础设施等,对程序整体的功能实现和结构组织有一定帮助。

缺点

  • 代码复杂度及错误风险:使用动态初始化或者具有非平凡析构函数的全局和静态变量,会显著增加代码的复杂度。因为不同编译单元中这类变量的动态初始化顺序是不确定的,相应地析构顺序也不确定(只知道是初始化顺序的逆序),这就容易引发一些难以察觉的错误。
  • 生命周期相关问题:若静态变量的初始化代码引用了另一个静态储存周期的变量,可能出现访问时间在另一变量的生命周期开始前(导致访问未初始化的变量)或生命周期结束后(产生非法访问)的情况。而且在程序结束时,若存在未汇合的线程,这些线程可能在静态变量析构后继续尝试访问它们,进而引发程序错误。

平凡的析构函数不受执行顺序影响。其他析构函数则有风险,可能访问生命周期已结束的对象。因此,只有拥有平凡析构函数的对象才能采用静态储存周期。基本类型 (例如指针和 int) 可以平凡地析构,可平凡析构的类型所构成的数组也可以平凡地析构。注意,用 constexpr 修饰的变量可以平凡地析构。

constexpr是 C++ 11 引入的一个关键字,用于指定一个表达式或函数在编译期可以求值。它主要用于常量表达式的计算,通过使用constexpr,可以让编译器在编译阶段尽可能地计算出表达式的值,而不是等到运行时再计算,从而提高程序的性能并且可以用于一些需要在编译期确定值的场景。

关于析构

const int kNum = 10;  // 允许struct X { int n; };
const X kX[] = {{1}, {2}, {3}};  // 允许void foo() {static const char* const kMessages[] = {"hello", "world"};  // 允许
}// 允许: constexpr 可以保证析构函数是平凡的.
constexpr std::array<int, 3> kArray = {1, 2, 3};
// 不好: 非平凡的析构.
const std::string kFoo = "foo";// 和上面相同的原因, 即使 kBar 是引用 (该规则也适用于生命周期被延长的临时对象).
const std::string& kBar = StrCat("a", "b", "c");void bar() {// 不好: 非平凡的析构.static std::map<int, int> kData = {{1, 0}, {2, 0}, {3, 0}};
}

注意,引用不是对象,因此它们的析构函数不受限。但是,它们仍需遵守动态初始化的限制。特别地,我们允许形如 static T& t = *new T; 的函数内局部静态引用。

关于初始化

初始化是更复杂的话题,因为我们不仅需要考虑构造函数的执行过程,也要考虑初始化表达式的求值过程。

int n = 5;    // 可以
int m = f();  // ? (依赖 f)
Foo x;        // ? (依赖 Foo::Foo)
Bar y = g();  // ? (依赖 g 和 Bar::Bar)

除了第一行语句以外,其他语句都会受到不确定的初始化顺序影响。

我们所需的概念在 C++ 标准中的正式称谓是常量初始化。这意味着初始化表达式是常量表达式,并且如果要用构造函数进行初始化,则该构造函数也必须声明为 constexpr

struct Foo { constexpr Foo(int) {} };int n = 5;  // 可以, 5 是常量表达式.
Foo x(2);   // 可以, 2 是常量表达式且被选中的构造函数也是 constexpr.
Foo a[] = { Foo(1), Foo(2), Foo(3) };  // 可以

可以自由使用常量初始化。应该用 constexprconstinit 标记静态变量的常量初始化过程. 应该假设任何没有这些标记的静态变量都是动态初始化的,并谨慎地检查这些代码。

constinit是 C++ 20 引入的一个关键字,用于声明具有静态存储期或线程存储期的变量的初始化是常量初始化。它主要用于确保变量在编译期进行初始化,并且这种初始化方式更加明确和严格,相比于其他初始化方式,constinit强调初始化的常量性和确定性。

作为反例,以下初始化过程有问题:

// 下文使用了这些声明.
time_t time(time_t*);      // 不是 constexpr!
int f();                   // 不是 constexpr!
struct Bar { Bar() {} };// 有问题的初始化.
time_t m = time(nullptr);  // 初始化表达式不是常量表达式.
Foo y(f());                // 同上
Bar b;                     // 被选中的构造函数 Bar::Bar() 不是 constexpr.

我们不建议且通常禁止动态地初始化全局变量。不过,如果这一初始化过程不依赖于其他初始化过程的顺序,则可以允许。若满足这一要求,则初始化的顺序变化不会产生任何区别。例如:

int p = getpid();  // 若其他静态变量不会在初始化过程中使用 p,则允许.

允许动态地初始化静态局部变量 (这是常见的)。

常用的语法结构

  • 全局字符串:如果需要具名的全局或静态字符串常量,可以采用constexpr修饰的string_view变量、字符数组或者指向字符串字面量的字符指针,因为字符串字面量本身具有静态储存周期,往往能满足相应需求。
  • 动态容器:对于像字典和集合等动态容器,若要以静态变量储存不变的数据,不建议使用标准库的动态容器(因其拥有非平凡的析构函数),可以考虑用平凡类型的数组替代(如int数组的数组用于模拟字典、数对的数组等),对于少量数据利用线性搜索并借助absl/algorithm/container.h中的工具实现常见操作,必要时保持数据有序采用二分查找法;若确实需要使用标准库动态容器,建议使用函数内局部静态指针的方式来处理。

Abseil 是一个由谷歌开发的开源 C++ 库。它的目的是为 C++ 开发提供一系列高质量、可复用的组件,包括算法、容器、字符串处理、内存管理等方面的工具。这些组件帮助 C++ 程序员更高效地编写代码,同时遵循最佳实践。

Abseil 是对标准 C++ 库的补充。它并不是要替代标准库,而是在标准库的基础上提供了更多的功能和优化。

  • 智能指针:智能指针(如std::unique_ptrstd::shared_ptr)由于在析构时有释放资源的操作,属于非平凡析构,不能作为静态变量使用,这种情况下可以思考是否适用其他模式,简单的解决办法是用裸指针指向动态分配的对象且永不删除它。
  • 自定义类型的静态变量:对于自定义类型的静态数据或常量数据,应给该类型设置平凡的析构函数以及constexpr修饰的构造函数,使其符合作为静态储存周期变量的条件;若上述各种建议都不适用,可以采用函数内局部静态指针或引用,通过动态分配一个对象且永不删除的方式来处理(如static const auto& impl = *new T(args...);)。

2.6 thread_local 变量

必须使用编译期常量初始化在函数外定义的 thread_local 变量,且必须使用 ABSL_CONST_INIT 属性来强制执行这一规则。优先采用 thread_local,而非其他定义线程内局部数据的方法。

thread_local是 C++ 11 引入的一个存储期说明符。它用于声明一个变量具有线程存储期,这意味着每个线程都有该变量的一个独立副本。与全局变量(整个程序只有一个副本)和局部变量(在函数执行期间存在于栈上)不同,thread_local变量为每个线程提供了专属的数据存储。

可以用 thread_local 修饰符声明变量,其能在命名空间内、函数内或类的静态成员内声明,但不能在类的普通成员内声明。例如

thread_local Foo foo =...;

展示了基本的声明形式,且不同线程访问该变量时,会访问各自独立的对象,从本质上来说它相当于一组不同的对象分布在各个线程中。

thread_local 实例与静态变量的初始化过程有相似之处,不过 thread_local 变量是在每个线程启动时初始化,而静态变量是在程序启动时初始化。正因如此,函数内的 thread_local 变量是线程安全的,避免了多线程同时访问同一变量可能引发的数据竞争问题。但在访问其他 thread_local 变量时,和静态变量类似存在初始化顺序问题,且由于线程的复杂性,这个问题可能更严重。

在析构方面,线程终止时,thread_local 变量的销毁顺序是初始化顺序的逆序,这一点和 C++ 中其他相关规则一致。但如果在 thread_local 变量的析构过程中访问了该线程中已销毁的其他 thread_local 变量,就容易出现难以调试的释放后使用(野指针)问题,需要格外留意。

优点

  • 防止竞态条件与助力并行化:线程的局部数据通过 thread_local 变量来定义,可以从根本上防止竞态条件,因为通常只有一个线程访问自己对应的 thread_local 变量副本,所以在多线程并行处理任务时,能有效避免因多个线程同时读写共享数据导致的问题,从而有助于程序的并行化实现,提升整体执行效率。
  • 语法标准支持优势:在创建线程局部数据的各种方法中,thread_local 是由语法标准支持的唯一方法,这意味着它具有规范性和通用性,使用它符合 C++ 语言标准规范,不用担心因使用非标准或不规范的方式带来的兼容性、可维护性等问题。

缺点

  • 运行时不确定性:在线程启动或首次使用 thread_local 变量时,可能触发很多难以预测、运行时间不可控的其他代码。这会给程序的性能分析、调试等带来困难,因为无法准确预估这部分代码对整体运行时间等方面的影响。
  • 类似全局变量的缺点:尽管 thread_local 变量在每个线程中有独立副本实现了线程安全,但本质上它类似全局变量,依然具备全局变量除线程安全外的其他缺点,比如可能导致代码结构不够清晰、模块间耦合性增加等问题,不利于代码的模块化设计和维护。
  • 内存占用问题:在最坏情况下,thread_local 变量占用的内存与线程数量成正比,若线程数量众多,其占用量可能十分巨大,对系统内存资源是个较大挑战,需要合理评估和控制线程数量以及 thread_local 变量的使用规模。
  • 析构相关风险:成员数据若要声明为 thread_local 必须是静态的,而且若 thread_local 变量拥有复杂的析构函数,可能会遇到野指针问题,尤其是析构函数不能(直接或间接地)访问任何有可能已被销毁的其他 thread_local 变量,而实际中很难检查这一规则是否被遵守,增加了程序出现错误的风险。
  • 资源泄漏风险:那些用于全局 / 静态变量预防野指针的方法不适用于 thread_local 变量。对于全局或局部变量,即使跳过析构函数,随着程序终止其生命周期结束,操作系统能较快回收泄露的内存和其他资源;但对于 thread_local 变量,若跳过析构函数,资源泄漏量会和程序运行期间创建的线程数量成正比,这是个不容忽视的潜在风险。

建议:

  • 位于类或命名空间中的 thread_local 变量只能用真正的编译时常量来初始化,也就是禁止动态初始化,必须用 ABSL_CONST_INIT 修饰(也可使用 constexpr 修饰,但不常见)来保证初始化符合要求,以此确保变量初始化的规范性和确定性,避免因不当初始化引发后续问题。
  • 函数中的 thread_local 变量不存在初始化的顾虑,但在线程退出时有释放后使用的风险。可以通过用静态方法暴露函数内的 thread_local 变量来模拟类或命名空间中的 thread_local 变量,不过要特别注意线程退出时变量的析构情况,如果析构函数使用了任何其他(可能已经销毁的) thread_local 变量,就会遇到难以调试的野指针问题,所以建议使用平凡的类型,或者析构函数中没有自定义代码的类型,以减少访问其他 thread_local 变量的可能性,降低出现野指针等错误的风险。

3. 类

类 (class) 是 C++ 中最基本的代码单元。因此,类在 C++ 中被广泛使用。

3.1 构造函数的内部操作

构造函数中禁止调用虚函数,应避免在无错误处理机制下进行可能失败的初始化。

构造函数可完全初始化对象,使其可作为const类型并方便在标准容器或算法中使用,但存在一些缺点,如调用虚函数的隐患、难以报告错误、初始化失败导致对象异常状态等。

若初始化可能失败,可定义Init()方法或工厂函数,确保从对象状态可知公用方法可用性。

3.2 隐式类型转换

不应定义隐式类型转换,类型转换运算符和单参数构造函数应标记为explicit,但拷贝和移动构造函数除外。

隐式类型转换使类型更易用、简化函数重载、列表初始化简洁,但可能掩盖类型不匹配错误、降低代码可读性、导致调用歧义等问题。

对于某些可互换类型,若隐式类型转换必要且恰当,可申请豁免。接受多个参数或单个std::initializer_list参数的构造函数可省略explicit标记。

3.3 可拷贝类型和可移动类型

类的公有接口应明确指明是否支持拷贝和移动操作,支持的话应正确定义相关函数,不支持则应显式删除。

可移动和可拷贝类型使 API 更简单、安全、通用,相关函数易于确保正确性和高效性,但某些类型不应支持拷贝,拷贝构造函数隐式调用可能导致问题。

若类的拷贝或移动操作易被误解或有意外开销,应设计为不可拷贝或不可移动,可拷贝类型的移动操作应在高效时定义,同时应检查默认实现正确性,避免对象切割风险,基类最好为抽象类。

3.4 结构体还是类

仅用struct定义储存数据的被动对象,包含常量成员,所有成员为公共,无不变式关系,可含构造函数等,但不能要求或实现不变式。

若需更多功能、不变式约束或结构体用途广泛且会更新,应使用类。不确定时优先选类,无状态类型可使用结构体以与 STL 保持一致,类和结构体成员变量命名规则不同。

3.5 结构体、数对还是元组

若能给成员起有意义名字,优先用结构体,其比数对和元组更可读,尽管使用数对(pair)和元组(tuple)可节省编写代码时间,但它们适合通用代码或与现有代码 / API 交互。

  • std::pairstd::tuple在 C++11 中引入。

  • 元组是数对的扩展,可以存储任意数量的值,每个值可以是不同类型的。它定义在<tuple>头文件中,通过std::tuple模板类实现。例如,std::tuple<int,double,std::string>可以用来存储一个整数、一个双精度浮点数和一个字符串。

  • 元组中的值可以通过std::get函数模板按照索引或类型来访问。例如:

std::tuple<int, double, std::string> myTuple(42, 3.14, "World");
int value = std::get<0>(myTuple);  // 访问第一个值(42)
double number = std::get<1>(myTuple);  // 访问第二个值(3.14)
std::string word = std::get<2>(myTuple);  // 访问第三个值("World")
// 也可以根据类型访问(前提是类型在元组中是唯一的)
std::string text = std::get<std::string>(myTuple);  

3.6 继承

通常组合比继承更合适,继承应使用public访问权限,避免过度使用实现继承,尽量只在 “is-a” 关系时使用。

实现继承可复用代码、减少代码量,接口继承可强制公开特定 API,但实现继承使子类实现难理解,多重继承问题更严重,应明确使用overridefinal关键字限定虚函数或虚析构函数,允许多重继承但避免多重实现继承。

3.7 运算符重载

谨慎使用运算符重载,禁止自定义字面量,重载运算符应意义明确、符合常理且与内置运算符行为一致,只为自定义类型定义且避免在类外不同文件定义同一运算。

重载运算符可使自定义类型行为类似内置类型,代码更简洁直观,但实现难度大,易引起困惑和错误,过度使用会使代码难理解,某些运算符重载危险,自定义字面量有诸多问题。

3.8 访问控制

类的所有数据成员应声明为私有,除非是常量,使用 Google Test 时,测试夹具类数据成员在.cc文件中可声明为受保护,若声明在.h文件中则应为私有。

3.9 声明次序

声明应按相似性分组,公有部分放最前面,各部分建议按特定顺序声明,避免在类定义中放置大段函数定义,只有简单、对性能重要且简短的方法可声明为内联函数。

建议使用以下顺序:

  1. 类型和类型别名 (typedefusingenum,嵌套结构体和类,友元类型)
  2. (可选,仅适用于结构体) 非静态数据成员
  3. 静态常量
  4. 工厂函数 (factory function)
  5. 构造函数和赋值运算符
  6. 析构函数
  7. 所有其他函数 (包括静态与非静态成员函数,还有友元函数)
  8. 所有其他数据成员 (包括静态和非静态的)

4. 函数

4.1 输入和输出

返回值倾向于按值返回,如果失败按引用返回,避免返回原始指针(除非可为空)。

非可选输入参数用值或 const 引用,非可选输出和输入 / 输出参数用引用(不能为空),可选参数按情况使用 std::optional、const 指针或非 const 指针。

将仅输入参数置于输出参数之前,但不是硬性规定,需考虑实际情况。

4.2 编写简短函数

倾向于简短凝练的函数,虽不硬性限制长度,但超过 40 行可考虑分割,便于阅读和修改,不要害怕修改复杂长函数。

4.3 函数重载

如果打算重载一个函数,可以试试改在函数名里加上参数信息。如用 AppendString()AppendInt()

class MyClass {public:void Analyze(const string &text);void Analyze(const char *text,size_t textlen);
};

优点:使代码直观,模板化代码需要。

缺点:读者需熟悉匹配规则,派生类重载部分变体时继承语义易混淆。

4.4 缺省参数

只用于非虚函数,值须始终一致,与函数重载规则相同,一般建议用函数重载。

优点:处理函数参数默认与非默认情况便利,语法简洁。

缺点:与函数重载类似,虚函数调用缺省参数有问题,在调用点重新求值致代码膨胀,干扰函数指针。

4.5 函数返回类型后置语法

C++11 允许在函数名前用 auto,参数列表后后置返回类型,如auto foo (int x) -> int

优点:显式指定 Lambda 表达式返回值,依赖模板参数时书写和阅读可能更简单。

缺点:语法新,读者可能陌生,新旧混用不规整。

5. 来自Google的奇技

5.1 所有权与智能指针

**所有权是用于登记和管理动态内存及其他资源的一种技术。**对于动态分配的对象,其所有主可以是一个对象或函数,这个所有主承担着在该对象不再有用时自动将其销毁的责任。而且所有权存在不同情况,既可以是单一固定的,也可以在多个主体间共享,共享时由最后一个持有所有权的主体负责销毁对象,还可以在代码中直接将所有权传递给其他对象。

智能指针本质上是一个类,它通过重载 *-> 运算符,使其表现得如同普通指针一样。其重要作用在于自动化所有权的登记工作,确保销毁义务能正确执行。例如,std::unique_ptr 是 C++ 11 引入的一种智能指针类型,用于表示对动态分配对象独一无二的所有权,一旦 std::unique_ptr 离开其作用域,对应的对象就会被销毁,并且它不能被复制,只能通过 C++ 11 的 move 语法移动给新的所有主;而 std::shared_ptr 同样表示动态分配对象的所有权,不过它可以被共享、被复制,对象的所有权由所有复制者共同拥有,直到最后一个复制者被销毁时,对象才随之被销毁。

优点

  • 有效管理内存:在处理动态分配的内存时,通过明确所有权归属,能很好地把控动态内存的生命周期,避免内存泄漏等问题。
  • 降低开销与简化操作:传递对象所有权的开销相较于复制对象(如果对象可复制的话)通常更小,而且相比于通过 “借用” 指针或引用来管理对象生命周期(这需要协调多个使用者对对象生命周期的操作),传递所有权更为简单直接,能省去不少协调工作。
  • 提升代码可读性与简化代码:当所有权逻辑清晰、有文档记录且不混乱时,代码的可读性会显著提升,同时借助智能指针还能自动完成所有权登记工作,大大简化了代码编写过程,减少因手动管理所有权可能出现的诸多错误。
  • 对 const 对象更友好:对于 const 对象而言,使用智能指针操作简单方便,并且比深度复制这类操作更具效率优势,能在保证对象不可变的同时高效地处理其所有权相关事宜。

缺点

  • 指针语义复杂性:无论是智能指针还是原生指针,使用指针来表示和传递所有权,其指针语义远比值语义复杂得多,尤其在 API 设计中,不仅要考虑所有权问题,还需要顾及别名、生命周期、可变性等诸多方面的问题,增加了代码理解和编写的难度。
  • 性能与可读性权衡问题:虽然传递所有权在某些情况下能降低开销,但实际上值语义的开销常被高估,所以有时候所有权传递带来的性能提升可能并不足以弥补其导致的可读性下降以及代码复杂度增加的损失,需要权衡利弊来决定是否采用所有权传递的方式。
  • 限制客户端内存管理模型:如果 API 依赖所有权的传递,那么客户端就只能遵循这一单一的内存管理模型,限制了客户端在内存管理方面的灵活性,可能给客户端代码的编写和集成带来不便。
  • 资源释放位置不直观:使用智能指针时,资源释放具体发生的位置变得不那么明显了,不像手动管理内存那样清晰可查,这在调试和理解代码执行过程中可能造成一定困扰。
  • 语法迷惑性与系统重构问题std::unique_ptr 的所有权传递依靠 C++ 11 的 move 语法,而这个语法相对较新,容易让程序员产生迷惑。并且如果原本的所有权设计已经比较完善,后续若要引入所有权共享机制(比如使用 std::shared_ptr),可能不得不对整个系统进行重构,成本较高。
  • 运行时开销与特殊情况问题:所有权共享机制(如 std::shared_ptr)的登记工作是在运行时进行的,会产生一定的开销,在某些极端情况下(比如存在循环引用时),被共享所有权的对象可能永远不会被销毁,导致资源泄漏等问题。此外,智能指针并不能完全替代原生指针,原生指针在一些特定场景下仍有其不可替代的作用。

结论

如果必须进行动态分配,更倾向于让分配者保持对象的所有权。当其他地方需要使用该对象时,优先考虑传递它的拷贝(如果可行的话),或者传递一个不改变所有权的指针或引用,以此来尽量简化所有权管理,避免不必要的复杂性。

std::unique_ptr<Foo> FooFactory();
void FooConsumer(std::unique_ptr<Foo> ptr);

倾向于使用 std::unique_ptr 来明确所有权的传递,例如通过函数返回 std::unique_ptr 以及在函数参数中接收 std::unique_ptr 的方式,清晰地展现所有权的转移过程。而对于共享所有权的情况,不要轻易使用,只有在有很好的理由时才考虑,比如为了避免开销昂贵的拷贝操作,并且要确保性能提升非常明显,同时操作的对象是不可变的(如 std::shared_ptr<const Foo> 这种形式),如果确实需要使用共享所有权,建议使用 std::shared_ptr。另外,明确指出不要使用 std::auto_ptr,而是用 std::unique_ptr 来替代它,以遵循更好的编程规范和避免潜在问题。

5.2 Cpplint

使用 cpplint.py 检查风格错误。

cpplint.py 是一个用来分析源文件,能检查出多种风格错误的工具。它不并完美,甚至还会漏报和误报,但它仍然是一个非常有用的工具. 在行尾加 // NOLINT,或在上一行加 // NOLINTNEXTLINE,可以忽略报错。

某些项目会指导你如何使用他们的项目工具运行 cpplint.py。如果你参与的项目没有提供,你可以单独下载 cpplint.py。

6. 其他C++特性

6.1 右值引用

仅在定义移动构造函数与移动赋值操作时使用。

优点:可实现移动语义,提升性能,支持通用函数封装和可移动但不可拷贝类型,是使用某些标准库类型的必需操作。

缺点:是相对较新的特性,规则复杂,尚未被广泛理解。

6.2 函数重载

若要重载函数,可试试在函数名中加入参数信息。

优点:使代码更直观,方便模板化代码,为使用者带来便利。

缺点:读者需熟悉匹配规则,派生类重载部分变体时继承语义易令人困惑。

6.3 缺省参数

除特定情况外,不允许使用,应改用函数重载,如AppendString()AppendInt() 等。

优点:可修改缺省参数,语法清晰,能区分必选和可选参数。

缺点:会干扰函数指针,造成代码臃肿,在调用点有重复。

void func(int a, int b);
// 改成下面情况
void func(int a, int b = 0);void (*funcPtr)(int, int) = &func; // 会报错

会出现问题,因为现在 func 的函数签名改变了,funcPtr 所期望的函数签名与修改后的 func不匹配了,再通过这个函数指针去调用函数时,编译器可能会报错,代码的逻辑就被破坏了。

6.4 变长数组和 alloca ()

不允许使用,改用更安全的分配器,std::vectorstd::unique_ptr<T[]>

优点:语法自然,执行高效。

缺点:不是标准 C++ 组成部分,可能导致内存越界错误。

alloca()是一个函数,用于在栈上动态分配内存。它的主要特点是分配的内存是从栈空间获取的,当调用alloca()的函数返回时,分配的内存会自动释放,无需像使用malloc()等函数分配堆内存那样需要手动释放。

6.5 友元

允许合理使用,通常应定义在同一文件内。

优点:扩大了类的封装边界,是处理特定情况的较好选择。

6.6 异常

不使用 C++ 异常。

优点:允许高层处理底层失败,与其他语言更一致,在测试框架中好用,是处理构造函数失败的唯一途径,有些第三方库依赖异常。

缺点:添加异常语句时需检查所有调用点,扰乱执行流程,增加二进制文件数据,延长编译时间,可能鼓励滥用,与现有代码整合困难。

6.7 运行时类型识别(RTTI)

禁止使用,尽量避免,单元测试中可使用,有合理用途但易被滥用。

优点:标准替代方案可能需要修改类层级,在单元测试和管理对象关系中有用。

缺点:运行时判断类型通常意味着设计问题,使代码难以维护,基于类型的判断树难以修改。

RTTI(Run - Time Type Information)即运行时类型信息,是 C++ 语言的一个特性,它允许程序在运行时获取对象的类型信息。在 C++ 中,通过typeid运算符和dynamic_cast运算符来实现 RTTI 功能。

6.8 类型转换

使用 C++ 的类型转换,如 static_cast<>() 等,不使用 C 风格转换。

优点:C++ 类型转换机制更清晰,查找更醒目。

缺点:语法较复杂。

static_cast 替代 C 风格的值转换,或某个类指针需要明确的向上转换为父类指针时。

const_cast 去掉 const 限定符。

reinterpret_cast 指针类型和整型或其它指针之间进行不安全的相互转换。仅在你对所做一切了然于心时使用。

6.9 流

只在记录日志时使用。

优点:打印时无需关心对象类型,构造和析构函数自动处理文件。

缺点:使某些功能函数难执行,部分格式化操作性能低,不支持字符串操作符重新排序。

6.10 前置自增和自减

对迭代器和模板对象使用前缀形式。

优点:通常效率更高,可以避免一次不必要的拷贝。

缺点:C 开发中传统做法是使用后置自增,部分人认为后置自增更易懂。

6.11 const 用法

在可能的情况下使用 const,有时改用 constexpr 更好,保持代码一致性。

优点:增强代码理解、类型检测和安全性。

缺点:具有入侵性,调用库函数时较麻烦。

6.12 constexpr 用法

用于定义真正的常量和常量初始化,避免复杂函数定义。

优点:可定义多种常量,实现 C++ 常量机制。

缺点:过早优化为 constexpr 变量后修改麻烦,定义限制可能导致方法模糊。

6.13 整型

C++ 内建整型中只使用 int,根据情况使用 <stdint.h> 中精确大小的整型。

优点:保持声明统一。

缺点:整型大小因编译器和体系结构而异。

6.14 64 位下的可移植性

代码应对 64 位和 32 位系统友好,注意打印、比较、结构体对齐等问题。

优点:确保程序在不同系统上的兼容性。

缺点:需处理不同系统下的差异,如格式化指示符、结构体对齐等。

6.15 预处理宏

谨慎使用,尽量用内联函数、枚举和常量代替,遵循特定用法模式。

优点:可实现一些其他技术无法实现的功能。

缺点:可能导致异常行为,测试困难。

6.16 0、nullptr 和 NULL

指针使用 nullptr,字符使用 ‘\0’。

优点:保证指针类型安全,提高代码可读性。

6.17 sizeof

尽可能用 sizeof (varname) 代替 sizeof (type)。

优点:变量类型改变时自动更新。

6.18 auto

用于绕过烦琐类型名,提高局部变量声明的便利性,但需注意可读性,不用于局部变量之外。

优点:简化复杂类型声明,方便使用中间变量。

缺点:类型不明显时影响代码可读性,需区分 auto 和 const auto&,与列表初始化结合时易混淆,在接口中使用可能导致 API 变化。

6.19 列表初始化

在考虑可移植性下,可以使用列表初始化,C++03 中聚合类型可用,C++11 中推广到任何对象类型。

优点:提供了统一方便的初始化方式,适用于多种类型,可简化代码编写,如在初始化容器、自定义类型等场景中,使代码更简洁直观,增强可读性。

6.20 Lambda 表达式

适当使用 lambda 表达式。当 lambda 将转移当前作用域时,首选显式捕获。

仅在 lambda 生存期明显短于潜在捕获时使用默认引用捕获,仅用默认按值捕获绑定少量变量且不捕获this,避免用捕获引入新名称或改变现有名称含义,使用时遵循相关格式要求,考虑团队成员对代码的理解和维护能力。

优点:是创建匿名函数对象的简易途径,在传函数对象给 STL 算法时最简易且可读性好,适当使用默认捕获可消除冗余,与std::functionsstd::bind可搭配成通用回调机制,方便编写接收有界函数为参数的函数。

缺点:变量捕获可能导致悬空指针错误,按值默认捕获可能产生误导,捕获语法与常规变量声明不同,初始化捕获依赖类型推导有与auto类似缺点且语法不提示推导,lambda 使用过度会使代码难理解。

6.21 模板编程

不要使用复杂的模板编程。

优点:能实现灵活类型安全的接口和极好性能,如 Google Teststd::tuplestd::functionBoost.Spirit 等工具依赖模板实现。

缺点:技巧晦涩难懂,复杂模板代码难读懂、调试和维护,编译出错信息不友好,大量使用会使重构工具难以发挥作用。

6.22 Boost 库

只使用 Boost 中被认可的库。

优点:代码质量高、可移植性好,填补 C++ 标准库空白,如提供更好的智能指针、型别特性等。

缺点:部分库的编程实践可读性差,如元编程和高级模板技术等。

6.23 C++11

适当使用库和语言扩展,使用前考虑项目可移植性,除个别情况外<ratio><cfenv><fenv.h>头文件、默认 lambda 捕获等特性最好不用。

优点:是官方标准,被大多编译器支持,标准化了很多扩展,简化操作,改善性能和安全。

缺点:相对于前身更复杂,开发者可能不熟悉,部分扩展对可读性有害或与原有机制冲突,带来困惑和迁移代价。

7. 命名约定

**最重要的一致性规则是命名管理。**命名的风格能让我们在不需要去查找类型声明的条件下快速地了解某个名字代表的含义:类型,变量,函数,常量,宏,等等,甚至. 我们大脑中的模式匹配引擎非常依赖这些命名规则。

命名规则具有一定随意性,但相比按个人喜好命名,一致性更重要,所以无论你认为它们是否重要,规则总归是规则。

7.1 通用命名规则

函数命名,变量命名,文件命名要有描述性; 少用缩写。

尽可能使用描述性的命名,别心疼空间,毕竟相比之下让代码易于新读者理解更重要。不要用只有项目开发者能理解的缩写,也不要通过砍掉几个字母来缩写单词。

int price_count_reader;    // 无缩写
int num_errors;            // "num" 是一个常见的写法
int num_dns_connections;   // 人人都知道 "DNS" 是什么int n;                     // 毫无意义.
int nerr;                  // 含糊不清的缩写.
int n_comp_conns;          // 含糊不清的缩写.
int wgc_connections;       // 只有贵团队知道是什么意思.
int pc_reader;             // "pc" 有太多可能的解释了.
int cstmr_id;              // 删减了若干字母.

注意,一些特定的广为人知的缩写是允许的,例如用 i 表示迭代变量和用 T 表示模板参数。

模板参数的命名应当遵循对应的分类:类型模板参数应当遵循类型命名的规则,而非类型模板应当遵循变量命名的规则。

7.2 文件命名

文件名要全部小写,可以包含下划线 (_) 或连字符 (-),依照项目的约定。如果没有约定,那么 “_” 更好。

可接受的文件命名示例:

  • my_useful_class.cc
  • my-useful-class.cc
  • myusefulclass.cc
  • myusefulclass_test.cc // _unittest_regtest 已弃用.

C++ 文件要以 .cc 结尾,头文件以 .h 结尾。专门插入文本的文件则以 .inc 结尾。

不要使用已经存在于 /usr/include 下的文件名 ,如 db.h

通常应尽量让文件名更加明确。http_server_logs.h 就比 logs.h 要好。定义类时文件名一般成对出现,如 foo_bar.hfoo_bar.cc,对应于类 FooBar

内联函数定义必须放在 .h 文件中。如果内联函数比较短,就直接将实现也放在 .h 中。

7.3 类型命名

类型名称的每个单词首字母均大写,不包含下划线:MyExcitingClassMyExcitingEnum

所有类型命名 —— 类,结构体,类型定义 (typedef),枚举,类型模板参数 —— 均使用相同约定,即以大写字母开始,每个单词首字母均大写,不包含下划线。例如:

// 类和结构体
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...// 类型定义
typedef hash_map<UrlTableProperties *, string> PropertiesMap;// using 别名
using PropertiesMap = hash_map<UrlTableProperties *, string>;// 枚举
enum UrlTableErrors { ...

7.4 变量命名

变量 (包括函数参数) 和数据成员名一律小写,单词之间用下划线连接。类的成员变量以下划线结尾,但结构体的就不用,如:a_local_variablea_struct_data_membera_class_data_member_

普通变量命名

举例:

string table_name;  // 好 - 用下划线.
string tablename;   // 好 - 全小写.string tableName;  // 差 - 混合大小写

类数据成员

不管是静态的还是非静态的,类数据成员都可以和普通变量一样,但要接下划线。

class TableInfo {...private:string table_name_;  // 好 - 后加下划线.string tablename_;   // 好.static Pool<TableInfo>* pool_;  // 好.
};

结构体变量

不管是静态的还是非静态的,结构体数据成员都可以和普通变量一样,不用像类那样接下划线:

struct UrlTableProperties {string name;int num_entries;static Pool<UrlTableProperties>* pool;
};

7.5 常量命名

声明为 constexprconst 的变量,或在程序运行期间其值始终保持不变的,命名时以 “k” 开头,大小写混合。例如:

const int kDaysInAWeek = 7;

所有具有静态存储类型的变量都应当以此方式命名。对于其他存储类型的变量,如自动变量等,这条规则是可选的。如果不采用这条规则,就按照一般的变量命名规则。

7.6. 函数命名

常规函数使用大小写混合,取值和设值函数则要求与变量名匹配MyExcitingFunction()MyExcitingMethod()my_exciting_member_variable()set_my_exciting_member_variable()

一般来说,函数名的每个单词首字母大写 (即 “驼峰变量名” 或 “帕斯卡变量名”),没有下划线。对于首字母缩写的单词,更倾向于将它们视作一个单词进行首字母大写 (例如,写作 StartRpc() 而非 StartRPC())。

AddTableEntry()
DeleteUrl()
OpenFileOrDie()

同样的命名规则同时适用于类作用域与命名空间作用域的常量,因为它们是作为 API 的一部分暴露对外的,因此应当让它们看起来像是一个函数,因为在这时,它们实际上是一个对象而非函数的这一事实对外不过是一个无关紧要的实现细节。

取值和设值函数的命名与变量一致。一般来说它们的名称与实际的成员变量对应,但并不强制要求. 例如 int count()void set_count(int count)

7.7 命名空间命名

命名空间以小写字母命名。最高级命名空间的名字取决于项目名称。要注意避免嵌套命名空间的名字之间和常见的顶级命名空间的名字之间发生冲突。

顶级命名空间的名称应当是项目名或者是该命名空间中的代码所属的团队的名字。命名空间中的代码,应当存放于和命名空间的名字匹配的文件夹或其子文件夹中。

注意不使用缩写作为名称的规则同样适用于命名空间。命名空间中的代码极少需要涉及命名空间的名称,因此没有必要在命名空间中使用缩写。

要避免嵌套的命名空间与常见的顶级命名空间发生名称冲突。由于名称查找规则的存在,命名空间之间的冲突完全有可能导致编译失败。尤其是,不要创建嵌套的 std 命名空间. 建议使用更独特的项目标识符 (websearch::indexwebsearch::index_util) 而非常见的极易发生冲突的名称 (比如 websearch::util).

对于 internal 命名空间,要当心加入到同一 internal 命名空间的代码之间发生冲突 (由于内部维护人员通常来自同一团队,因此常有可能导致冲突)。在这种情况下,请使用文件名以使得内部名称独一无二 (例如对于 frobber.h,使用 websearch::index::frobber_internal)。

7.8 枚举命名

枚举的命名应当和常量或宏一致:kEnumName 或是 ENUM_NAME

单独的枚举值应该优先采用常量的命名方式。但宏方式的命名也可以接受。枚举名 UrlTableErrors (以及 AlternateUrlTableErrors) 是类型,所以要用大小写混合的方式。

enum UrlTableErrors {kOK = 0,kErrorOutOfMemory,kErrorMalformedInput,
};
enum AlternateUrlTableErrors {OK = 0,OUT_OF_MEMORY = 1,MALFORMED_INPUT = 2,
};

2009 年 1 月之前,我们一直建议采用宏的方式命名枚举值。由于枚举值和宏之间的命名冲突,直接导致了很多问题。由此,这里改为优先选择常量风格的命名方式。新代码应该**尽可能优先使用常量风格。**但是老代码没必要切换到常量风格,除非宏风格确实会产生编译期问题。

7.9 宏命名

你并不打算使用宏,对吧? 如果你一定要用,像这样命名:MY_MACRO_THAT_SCARES_SMALL_CHILDREN

参考预处理宏; 通常 不应该 使用宏。如果不得不用,其命名像枚举命名一样全部大写,使用下划线:

#define ROUND(x) ...
#define PI_ROUNDED 3.0

7.10 命名规则特例

如果你命名的实体与已有 C/C++ 实体相似,可参考现有命名策略.

bigopen(): 函数名,参照 open() 的形式。

uint: typedef

bigpos: structclass,参照 pos 的形式。

sparse_hash_map: STL 型实体; 参照 STL 命名约定。

LONGLONG_MAX: 常量,如同 INT_MAX

8. 注释

注释虽然写起来很痛苦,但对保证代码可读性至关重要。下面的规则描述了如何注释以及在哪儿注释。当然也要记住:注释固然很重要,但最好的代码应当本身就是文档。有意义的类型名和变量名,要远胜过要用注释解释的含糊不清的名字。

你写的注释是给代码读者看的,也就是下一个需要理解你的代码的人。所以慷慨些吧,下一个读者可能就是你!

8.1 注释风格

使用 ///* */,统一即可。

///* */ 都可以; 但 // 常用. 要在如何注释及注释风格上确保统一。

8.2 文件注释

每个文件开头应加入版权公告和许可证引用,重大修改时可考虑删除原作者信息。

如果.h文件声明了多个概念,需对文件内容做大致说明及概念联系,不需要在.h.cc间复制注释。

8.3 类注释

除非功能明显,每个类定义都要附带注释,描述功能、用法、同步前提(多线程相关),可包含使用示例,声明和定义分开时,注释也应分开。

// Iterates over the contents of a GargantuanTable.
// Example:
//    GargantuanTableIterator* iter = table->NewIterator();
//    for (iter->Seek("foo"); !iter->done(); iter->Next()) {
//      process(iter->key(), iter->value());
//    }
//    delete iter;
class GargantuanTableIterator {...
};

8.4 函数注释

函数声明处注释描述功能、用途、输入输出、调用相关信息(如参数保持引用、空间分配、参数空指针情况、性能隐患、可重入同步前提等),避免啰嗦,函数重载重点注释重载部分,构造 / 析构函数注明特殊操作,简单明显的可省略注释。

函数定义处若实现巧妙,需注释编程技巧、步骤或理由,不要复制声明处注释,重点在实现。

8.5 变量注释

类数据成员需注释用途、特殊值、关系、生命周期等,全局变量注释含义、用途及作为全局变量的原因。

8.6 实现注释

代码前注释用于解释巧妙或复杂代码段,行注释用于隐晦地方,可连续多行注释并对齐,函数参数意义不明显时,优先改进代码(如用常量名、更改参数类型、使用类或结构体、用具名变量),最后才考虑在调用点注释。

避免描述明显现象和翻译代码,应解释原因和目的,追求自文档化代码。

8.7 标点,拼写和语法

注释用正确大小写和结尾句号,完整叙述性语句可读性更高,短注释可随意但保持风格一致,注意标点、拼写和语法。

8.8 TODO 注释

用于临时、短期解决方案或不完美代码,格式为TODO(标识),可附明确时间或事项,添加者一般写自己名字。

8.9 弃用注释

DEPRECATED标记弃用接口,注明姓名、邮箱等,包含指引帮助修复调用点,需主动修正调用点或找人帮忙,修正后不再使用弃用接口。

9. 格式

通过统一的格式规则,可以提高代码的可读性和可维护性,便于团队协作开发。

9.1 行长度

每行代码字符数不超 80,注释、包含长路径的#include语句、头文件保护可例外。

9.2 非 ASCII 字符

尽量不用,用则需 UTF - 8 编码,特殊情况如分析外部数据文件或单元测试代码中可包含,可使用十六进制编码增强可读性,避免使用char16_tchar32_twchar_t(调用 Windows API 除外)。

9.3 空格还是制表位

只用空格,每次缩进 2 个空格,编辑器应将制表符转为空格。

9.4 函数声明与定义

返回类型和函数名同行,参数尽量同行,放不下则按函数调用方式分行,注意各种格式细节,如括号、空格、大括号位置,参数名使用规则,未使用或用途明显参数可省略参数名,不明显的在定义处注释,属性写在返回类型前。

函数看上去像这样:

ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {DoSomething();...
}

如果同一行文本太多,放不下所有参数:

ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,Type par_name3) {DoSomething();...
}

甚至连第一个参数都放不下:

ReturnType LongClassName::ReallyReallyReallyLongFunctionName(Type par_name1,  // 4 space indentType par_name2,Type par_name3) {DoSomething();  // 2 space indent...
}

注意以下几点:

  • 使用好的参数名。
  • 只有在参数未被使用或者其用途非常明显时,才能省略参数名。
  • 如果返回类型和函数名在一行放不下,分行。
  • 如果返回类型与函数声明或定义分行了,不要缩进。
  • 左圆括号总是和函数名在同一行。
  • 函数名和左圆括号间永远没有空格。
  • 圆括号与参数间没有空格。
  • 左大括号总在最后一个参数同一行的末尾处,不另起新行。
  • 右大括号总是单独位于函数最后一行,或者与左大括号同一行。
  • 右圆括号和左大括号间总是有一个空格。
  • 所有形参应尽可能对齐。
  • 缺省缩进为 2 个空格。
  • 换行后的参数保持 4 个空格的缩进。

9.5 Lambda 表达式

Lambda 表达式对形参和函数体的格式化和其他函数一致; 捕获列表同理,表项用逗号隔开。

若用引用捕获,在变量名和 & 之间不留空格。

int x = 0;
auto add_to_x = [&x](int n) { x += n; };

短 lambda 就写得和内联函数一样。

std::set<int> blacklist = {7, 8, 9};
std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) {return blacklist.find(i) != blacklist.end();}),digits.end());

9.6 函数调用

要么一行写完函数调用,要么在圆括号里对参数分行,要么参数另起一行且缩进四格。如果没有其它顾虑的话,尽可能精简行数,比如把多个参数适当地放在同一行里。

函数调用遵循如下形式:

bool retval = DoSomething(argument1, argument2, argument3);

如果同一行放不下,可断为多行,后面每一行都和第一个实参对齐,左圆括号后和右圆括号前不要留空格:

bool retval = DoSomething(averyveryveryverylongargument1,argument2, argument3);

参数也可以放在次行,缩进四格:

if (...) {......if (...) {DoSomething(argument1, argument2,  // 4 空格缩进argument3, argument4);}

把多个参数放在同一行以减少函数调用所需的行数,除非影响到可读性。有人认为把每个参数都独立成行,不仅更好读,而且方便编辑参数。不过,比起所谓的参数编辑,我们更看重可读性,且后者比较好办:

如果一些参数本身就是略复杂的表达式,且降低了可读性,那么可以直接创建临时变量描述该表达式,并传递给函数:

int my_heuristic = scores[x] * y + bases[x];
bool retval = DoSomething(my_heuristic, x, y, z);

或者放着不管,补充上注释:

bool retval = DoSomething(scores[x] * y + bases[x],  // Score heuristic.x, y, z);

如果某参数独立成行,对可读性更有帮助的话,那也可以如此做。参数的格式处理应当以可读性而非其他作为最重要的原则。

此外,如果一系列参数本身就有一定的结构,可以酌情地按其结构来决定参数格式:

// 通过 3x3 矩阵转换 widget.
my_widget.Transform(x1, x2, x3,y1, y2, y3,z1, z2, z3);

9.7 列表初始化格式

参照函数调用格式,若有名字则将名字视作函数调用名,{}视作括号,无名字则视为长度为零,断行时按相应规则处理。

// 一行列表初始化示范.
return {foo, bar};
functioncall({foo, bar});
pair<int, int> p{foo, bar};// 当不得不断行时.
SomeFunction({"assume a zero-length name before {"},  // 假设在 { 前有长度为零的名字.some_other_function_parameter);
SomeType variable{some, other, values,{"assume a zero-length name before {"},  // 假设在 { 前有长度为零的名字.SomeOtherType{"Very long string requiring the surrounding breaks.",  // 非常长的字符串, 前后都需要断行.some, other values},SomeOtherType{"Slightly shorter string",  // 稍短的字符串.some, other, values}};
SomeType variable{"This is too long to fit all in one line"};  // 字符串过长, 因此无法放在同一行.
MyType m = {  // 注意了, 您可以在 { 前断行.superlongvariablename1,superlongvariablename2,{short, interior, list},{interiorwrappinglist,interiorwrappinglist2}};

9.8 条件语句

倾向于不在圆括号内使用空格。关键字 ifelse 另起一行。

对基本条件语句有两种可以接受的格式。一种在圆括号和条件之间有空格,另一种没有。

最常见的是没有空格的格式。哪一种都可以,最重要的是 保持一致。如果你是在修改一个文件,参考当前已有格式。如果是写新的代码,参考目录下或项目中其它文件。还在犹豫的话,就不要加空格了。

if (condition) {  // 圆括号里没有空格....  // 2 空格缩进.
} else if (...) {  // else 与 if 的右括号同一行....
} else {...
}

如果你更喜欢在圆括号内部加空格:

if ( condition ) {  // 圆括号与空格紧邻 - 不常见...  // 2 空格缩进.
} else {  // else 与 if 的右括号同一行....
}

注意所有情况下 if 和左圆括号间都有个空格。右圆括号和左大括号之间也要有个空格:

if(condition)     // 差 - IF 后面没空格.
if (condition){   // 差 - { 前面没空格.
if(condition){    // 变本加厉地差.
if (condition) {  // 好 - IF 和 { 都与空格紧邻.

如果能增强可读性,简短的条件语句允许写在同一行。只有当语句简单并且没有使用 else 子句时使用:

if (x == kFoo) return new Foo();
if (x == kBar) return new Bar();

如果语句有 else 分支则不允许:

// 不允许 - 当有 ELSE 分支时 IF 块却写在同一行
if (x) DoThis();
else DoThat();

通常,单行语句不需要使用大括号,如果你喜欢用也没问题; 复杂的条件或循环语句用大括号可读性会更好。也有一些项目要求 if 必须总是使用大括号:

if (condition)DoSomething();  // 2 空格缩进.if (condition) {DoSomething();  // 2 空格缩进.
}

但如果语句中某个 if-else 分支使用了大括号的话,其它分支也必须使用:

// 不可以这样子 - IF 有大括号 ELSE 却没有.
if (condition) {foo;
} elsebar;// 不可以这样子 - ELSE 有大括号 IF 却没有.
if (condition)foo;
else {bar;
}
// 只要其中一个分支用了大括号, 两个分支都要用上大括号.
if (condition) {foo;
} else {bar;
}

9.9 循环和开关选择语句

switch 语句可以使用大括号分段,以表明 cases 之间不是连在一起的。在单语句循环里,括号可用可不用。空循环体应使用 {}continue

switch 语句中的 case 块可以使用大括号也可以不用,取决于你的个人喜好。如果用的话,要按照下文所述的方法。

如果有不满足 case 条件的枚举值,switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理,编译器将给出 warning)。如果 default 应该永远执行不到,简单的加条 assert

switch (var) {case 0: {  // 2 空格缩进...      // 4 空格缩进break;}case 1: {...break;}default: {assert(false);}
}

在单语句循环里,括号可用可不用:

for (int i = 0; i < kSomeNumber; ++i)printf("I love you\n");for (int i = 0; i < kSomeNumber; ++i) {printf("I take it back\n");
}

空循环体应使用 {}continue,而不是一个简单的分号.

while (condition) {// 反复循环直到条件失效.
}
for (int i = 0; i < kSomeNumber; ++i) {}  // 可 - 空循环体.
while (condition) continue;  // 可 - contunue 表明没有逻辑.
while (condition);  // 差 - 看起来仅仅只是 while/loop 的部分之一.

9.10 指针和引用表达式

句点或箭头前后不要有空格。指针/地址操作符 (*,&) 之后不能有空格。

下面是指针和引用表达式的正确使用范例:

x = *p;
p = &x;
x = r.y;
x = r->y;

注意:

  • 在访问成员时,句点或箭头前后没有空格。
  • 指针操作符 *& 后没有空格。

在声明指针变量或参数时,星号与类型或变量名紧挨都可以:

// 好, 空格前置.
char *c;
const string &str;// 好, 空格后置.
char* c;
const string& str;
int x, *y;  // 不允许 - 在多重声明中不能使用 & 或 *
char * c;  // 差 - * 两边都有空格
const string & str;  // 差 - & 两边都有空格.

在单个文件内要保持风格一致,所以,如果是修改现有文件,要遵照该文件的风格。

9.11 布尔表达式

如果一个布尔表达式超过标准行宽,断行方式要统一一下。

下例中,逻辑与 (&&) 操作符总位于行尾:

if (this_one_thing > this_other_thing &&a_third_thing == a_fourth_thing &&yet_another && last_one) {...
}

注意,上例的逻辑与 (&&) 操作符均位于行尾。这个格式在 Google 里很常见,虽然把所有操作符放在开头也可以。可以考虑额外插入圆括号,合理使用的话对增强可读性是很有帮助的。此外,直接用符号形式的操作符,比如 &&~,不要用词语形式的 andcompl

9.12 函数返回值

不要在 return 表达式里加上非必须的圆括号。

只有在写 x = expr 要加上括号的时候才在 return expr; 里使用括号。

return result;                  // 返回值很简单, 没有圆括号.
// 可以用圆括号把复杂表达式圈起来, 改善可读性.
return (some_long_condition &&another_condition);
return (value);                // 毕竟您从来不会写 var = (value);
return(result);                // return 可不是函数!

9.13 变量及数组初始化

=(){} 均可。

您可以用 =(){},以下的例子都是正确的:

int x = 3;
int x(3);
int x{3};
string name("Some Name");
string name = "Some Name";
string name{"Some Name"};

请务必小心列表初始化 {...}std::initializer_list 构造函数初始化出的类型。非空列表初始化就会优先调用 std::initializer_list,不过空列表初始化除外,后者原则上会调用默认构造函数。为了强制禁用 std::initializer_list 构造函数,请改用括号。

vector<int> v(100, 1);  // 内容为 100 个 1 的向量.
vector<int> v{100, 1};  // 内容为 100 和 1 的向量.

此外,列表初始化不允许整型类型的四舍五入,这可以用来避免一些类型上的编程失误。

int pi(3.14);  // 好 - pi == 3.
int pi{3.14};  // 编译错误: 缩窄转换.

9.14 预处理指令

预处理指令不要缩进,从行首开始。

即使预处理指令位于缩进代码块中,指令也应从行首开始。

// 好 - 指令从行首开始if (lopsided_score) {
#if DISASTER_PENDING      // 正确 - 从行首开始DropEverything();
# if NOTIFY               // 非必要 - # 后跟空格NotifyClient();
# endif
#endifBackToNormal();}
// 差 - 指令缩进if (lopsided_score) {#if DISASTER_PENDING  // 差 - "#if" 应该放在行开头DropEverything();#endif                // 差 - "#endif" 不要缩进BackToNormal();}

9.15 类格式

访问控制块的声明依次序是 public:protected:private:,每个都缩进 1 个空格。

类声明的基本格式如下:

class MyClass : public OtherClass {public:      // 注意有一个空格的缩进MyClass();  // 标准的两空格缩进explicit MyClass(int var);~MyClass() {}void SomeFunction();void SomeFunctionThatDoesNothing() {}void set_some_var(int var) { some_var_ = var; }int some_var() const { return some_var_; }private:bool SomeInternalFunction();int some_var_;int some_other_var_;
};

注意事项:

  • 所有基类名应在 80 列限制下尽量与子类名放在同一行。
  • 关键词 public:protected:private: 要缩进 1 个空格。
  • 除第一个关键词 (一般是 public) 外,其他关键词前要空一行。如果类比较小的话也可以不空。
  • 这些关键词后不要保留空行。
  • public 放在最前面,然后是 protected,最后是 private

9.16 构造函数初始值列表

构造函数初始化列表放在同一行或按四格缩进并排多行。

下面两种初始值列表方式都可以接受:

// 如果所有变量能放在同一行:
MyClass::MyClass(int var) : some_var_(var) {DoSomething();
}// 如果不能放在同一行,
// 必须置于冒号后, 并缩进 4 个空格
MyClass::MyClass(int var): some_var_(var), some_other_var_(var + 1) {DoSomething();
}// 如果初始化列表需要置于多行, 将每一个成员放在单独的一行
// 并逐行对齐
MyClass::MyClass(int var): some_var_(var),             // 4 space indentsome_other_var_(var + 1) {  // lined upDoSomething();
}// 右大括号 } 可以和左大括号 { 放在同一行
// 如果这样做合适的话
MyClass::MyClass(int var): some_var_(var) {}

9.17 命名空间格式化

命名空间内容不缩进。

命名空间不要增加额外的缩进层次,例如:

namespace {void foo() {  // 正确. 命名空间内没有额外的缩进....
}}  // namespace

不要在命名空间内缩进:

namespace {// 错, 缩进多余了.void foo() {...}}  // namespace

声明嵌套命名空间时,每个命名空间都独立成行.

namespace foo {
namespace bar {

9.18 水平留白

水平留白的使用根据在代码中的位置决定。永远不要在行尾添加没意义的留白。

void f(bool b) {  // 左大括号前总是有空格....
int i = 0;  // 分号前不加空格.
// 列表初始化中大括号内的空格是可选的.
// 如果加了空格, 那么两边都要加上.
int x[] = { 0 };
int x[] = {0};// 继承与初始化列表中的冒号前后恒有空格.
class Foo : public Bar {public:// 对于单行函数的实现, 在大括号内加上空格// 然后是函数实现Foo(int b) : Bar(), baz_(b) {}  // 大括号里面是空的话, 不加空格.void Reset() { baz_ = 0; }  // 用空格把大括号与实现分开....

添加冗余的留白会给其他人编辑时造成额外负担。因此,行尾不要留空格。如果确定一行代码已经修改完毕,将多余的空格去掉; 或者在专门清理空格时去掉(尤其是在没有其他人在处理这件事的时候)。

循环和条件语句

if (b) {          // if 条件语句和循环语句关键字后均有空格.
} else {          // else 前后有空格.
}
while (test) {}   // 圆括号内部不紧邻空格.
switch (i) {
for (int i = 0; i < 5; ++i) {
switch ( i ) {    // 循环和条件语句的圆括号里可以与空格紧邻.
if ( test ) {     // 圆括号, 但这很少见. 总之要一致.
for ( int i = 0; i < 5; ++i ) {
for ( ; i < 5 ; ++i) {  // 循环里内 ; 后恒有空格, ;  前可以加个空格.
switch (i) {case 1:         // switch case 的冒号前无空格....case 2: break;  // 如果冒号有代码, 加个空格.

操作符

// 赋值运算符前后总是有空格.
x = 0;// 其它二元操作符也前后恒有空格, 不过对于表达式的子式可以不加空格.
// 圆括号内部没有紧邻空格.
v = w * x + y / z;
v = w*x + y/z;
v = w * (x + z);// 在参数和一元操作符之间不加空格.
x = -5;
++x;
if (x && !y)...

模板和转换

// 尖括号(< and >) 不与空格紧邻, < 前没有空格, > 和 ( 之间也没有.
vector<string> x;
y = static_cast<char*>(x);// 在类型与指针操作符之间留空格也可以, 但要保持一致.
vector<char *> x;

9.19 垂直留白

垂直留白越少越好。

这不仅仅是规则而是原则问题了: 不在万不得已,不要使用空行. 尤其是:两个函数定义之间的空行不要超过 2 行,函数体首尾不要留空行,函数体中也不要随意添加空行。

基本原则是: 同一屏可以显示的代码越多,越容易理解程序的控制流。当然,过于密集的代码块和过于疏松的代码块同样难看,这取决于你的判断。但通常是垂直留白越少越好。

下面的规则可以让加入的空行更有效:

  • 函数体内开头或结尾的空行可读性微乎其微。
  • 在多重 if-else 块里加空行或许有点可读性。

相关文章:

Google C++ Style / 谷歌C++开源风格

文章目录 前言1. 头文件1.1 自给自足的头文件1.2 #define 防护符1.3 导入你的依赖1.4 前向声明1.5 内联函数1.6 #include 的路径及顺序 2. 作用域2.1 命名空间2.2 内部链接2.3 非成员函数、静态成员函数和全局函数2.4 局部变量2.5 静态和全局变量2.6 thread_local 变量 3. 类3.…...

Windows图形界面(GUI)-QT-C/C++ - QT Tab Widget

公开视频 -> 链接点击跳转公开课程博客首页 -> ​​​链接点击跳转博客主页 目录 一、概述 1.1 什么是 QTabWidget&#xff1f; 1.2 使用场景 二、常见样式 2.1 选项卡式界面 2.2 动态添加和删除选项卡 2.3 自定义选项卡标题和图标 三、属性设置 3.1 添加页面&…...

【大数据技术】教程05:本机DataGrip远程连接虚拟机MySQL/Hive

本机DataGrip远程连接虚拟机MySQL/Hive datagrip-2024.3.4VMware Workstation Pro 16CentOS-Stream-10-latest-x86_64-dvd1.iso写在前面 本文主要介绍如何使用本机的DataGrip连接虚拟机的MySQL数据库和Hive数据库,提高编程效率。 安装DataGrip 请按照以下步骤安装DataGrip软…...

C++:结构体和类

在之前的博客中已经讲过了C语言中的结构体概念了&#xff0c;重复的内容在这儿就不赘述了。C中的结构体在C语言的基础上还有些补充&#xff0c;在这里说明一下&#xff0c;顺便简单地讲一下类的概念。 一、成员函数 结构体类型声明的关键字是 struct &#xff0c;在C中结构体…...

MATLAB的数据类型和各类数据类型转化示例

一、MATLAB的数据类型 在MATLAB中 &#xff0c;数据类型是非常重要的概念&#xff0c;因为它们决定了如何存储和操作数据。MATLAB支持数值型、字符型、字符串型、逻辑型、结构体、单元数组、数组和矩阵等多种数据类型。MATLAB 是一种动态类型语言&#xff0c;这意味着变量的数…...

UE求职Demo开发日志#19 给物品找图标,实现装备增加属性,背包栏UI显示装备

1 将用到的图标找好&#xff0c;放一起 DataTable里对应好图标 测试一下能正确获取&#xff1a; 2 装备增强属性思路 给FMyItemInfo添加一个枚举变量记录类型&#xff08;物品&#xff0c;道具&#xff0c;装备&#xff0c;饰品&#xff0c;武器&#xff09;--> 扩展DataT…...

C++泛型编程指南09 类模板实现和使用友元

文章目录 第2章 类模板 Stack 的实现2.1 类模板 Stack 的实现 (Implementation of Class Template Stack)2.1.1 声明类模板 (Declaration of Class Templates)2.1.2 成员函数实现 (Implementation of Member Functions) 2.2 使用类模板 Stack脚注改进后的叙述总结脚注2.3 类模板…...

使用MATLAB进行雷达数据采集可视化

本文使用轮趣科技N10雷达&#xff0c;需要源码可在后台私信或者资源自取 1. 项目概述 本项目旨在通过 MATLAB 读取 N10 激光雷达 的数据&#xff0c;并进行 实时 3D 点云可视化。数据通过 串口 传输&#xff0c;并经过解析后转换为 三维坐标点&#xff0c;最终使用 pcplayer 进…...

【Elasticsearch】allow_no_indices

- **allow_no_indices 参数的作用**&#xff1a; 该参数用于控制当请求的目标索引&#xff08;通过通配符、别名或 _all 指定&#xff09;不存在或已关闭时&#xff0c;Elasticsearch 的行为。 - **默认行为**&#xff1a; 如果未显式设置该参数&#xff0c;默认值为 …...

54【ip+端口+根目录通信】

上节课讲到&#xff0c;根目录起到定位作用&#xff0c;比如我们搭建一个php网站后&#xff0c;注册系统是由根目录的register.php文件执行&#xff0c;那么我们给这个根目录绑定域名https://127.0.0.1&#xff0c;当我们浏览器访问https://127.0.0.1/register.php时&#xff0…...

python算法和数据结构刷题[3]:哈希表、滑动窗口、双指针、回溯算法、贪心算法

回溯算法 「所有可能的结果」&#xff0c;而不是「结果的个数」&#xff0c;一般情况下&#xff0c;我们就知道需要暴力搜索所有的可行解了&#xff0c;可以用「回溯法」。 回溯算法关键在于:不合适就退回上一步。在回溯算法中&#xff0c;递归用于深入到所有可能的分支&…...

DeepSeek横空出世,AI格局或将改写?

引言 这几天&#xff0c;国产AI大模型DeepSeek R1&#xff0c;一飞冲天&#xff0c;在全球AI圈持续引爆热度&#xff0c;DeepSeek R1 已经是世界上最先进的 AI 模型之一&#xff0c;可与 OpenAI 的新 o1 和 Meta 的 Llama AI 模型相媲美。 DeepSeek-V3模型发布后&#xff0c;在…...

聚簇索引、哈希索引、覆盖索引、索引分类、最左前缀原则、判断索引使用情况、索引失效条件、优化查询性能

聚簇索引 聚簇索引像一本按目录排版的书&#xff0c;用空间换时间&#xff0c;适合读多写少的场景。设计数据库时&#xff0c;主键的选择&#xff08;如自增ID vs 随机UUID&#xff09;会直接影响聚簇索引的性能。 什么是聚簇索引&#xff1f; 数据即索引&#xff1a;聚簇索引…...

OpenAI 实战进阶教程 - 第四节: 结合 Web 服务:构建 Flask API 网关

目标 学习将 OpenAI 接入 Web 应用&#xff0c;构建交互式 API 网关理解 Flask 框架的基本用法实现 GPT 模型的 API 集成并返回结果 内容与实操 一、环境准备 安装必要依赖&#xff1a; 打开终端或命令行&#xff0c;执行以下命令安装 Flask 和 OpenAI SDK&#xff1a; pip i…...

python的pre-commit库的使用

在软件开发过程中&#xff0c;保持代码的一致性和高质量是非常重要的。pre-commit 是一个强大的工具&#xff0c;它可以帮助我们在提交代码到版本控制系统&#xff08;如 Git&#xff09;之前自动运行一系列的代码检查和格式化操作。通过这种方式&#xff0c;我们可以确保每次提…...

架构技能(四):需求分析

需求分析&#xff0c;即分析需求&#xff0c;分析软件用户需要解决的问题。 需求分析的下一环节是软件的整体架构设计&#xff0c;需求是输入&#xff0c;架构是输出&#xff0c;需求决定了架构。 决定架构的是软件的所有需求吗&#xff1f;肯定不是&#xff0c;真正决定架构…...

Linux环境下的Java项目部署技巧:安装 Nginx

Nginx 的简介&#xff1a; Nginx 是一个高性能的 HTTP 和反向代理服务器&#xff0c;也是一个 IMAP / POP3 / SMTP 代理服务器。它可以作为网站静态资源的 web 服务器&#xff0c;也可以作为其他应用服务器的反向代理服务器。同时&#xff0c; Nginx 还具有负载均衡的功能。 N…...

前端 Vue 性能提升策略

一、引言 前端性能优化是确保 Web 应用快速响应和流畅用户体验的关键。对于使用 Vue.js 构建的应用,性能优化不仅涉及通用的前端技术,还包括针对 Vue 特性的特定优化措施。本文将从多个方面探讨如何全面提升前端和 Vue 应用的性能。 二、前端性能优化基础 1. 减少初始加载…...

深入理解linux中的文件(上)

1.前置知识&#xff1a; &#xff08;1&#xff09;文章 内容 属性 &#xff08;2&#xff09;访问文件之前&#xff0c;都必须打开它&#xff08;打开文件&#xff0c;等价于把文件加载到内存中&#xff09; 如果不打开文件&#xff0c;文件就在磁盘中 &#xff08;3&am…...

Unity特效插件GodFX

2022Unity安装使用方法​​,将MinDrawer.cs文件MinAttribute改成UnityEngine.PostProcessing.MinAttribute 参考链接: Unity3D特效插件GodFX使用教程_哔哩哔哩_bilibili...

从 C 到 C++:理解结构体中字符串的存储与操作

对于刚入门 C/C 的程序员来说&#xff0c;字符串的存储和操作可能是个容易混淆的知识点。在 C 中&#xff0c;std::string 提供了非常友好的接口&#xff0c;我们可以轻松地在结构体中使用字符串类型&#xff0c;无需关注底层细节。然而&#xff0c;在 C 语言中&#xff0c;字符…...

Linux进阶——时间服务器

NTP是网络时间协议&#xff08;network time protocol&#xff09;的简称&#xff08;应用层的协议&#xff09;&#xff0c;通过UDP123端口进行网络时钟同步。 Chrony是一个开源自由的网络时间协议NTP的客户端和服务器软件。它能让计算机保持系统时钟与时钟服务器&#xff08…...

力扣 295. 数据流的中位数

&#x1f517; https://leetcode.cn/problems/find-median-from-data-stream/ 题目 数据流中不断有数添加进来&#xff0c;add 表示添加数据&#xff0c;find 返回数据流中的中位数 思路 大根堆存储数据流中偏小的数据小根堆存储数据流中偏大的数据若当前的 num 比大根堆的…...

【Linux】进程状态和优先级

个人主页~ 进程状态和优先级 一、进程状态1、操作系统进程状态&#xff08;一&#xff09;运行态&#xff08;二&#xff09;阻塞态&#xff08;三&#xff09;挂起态 2、Linux进程状态&#xff08;一&#xff09;R-运行状态并发执行 &#xff08;二&#xff09;S-浅度睡眠状态…...

携程Java开发面试题及参考答案 (200道-上)

说说四层模型、七层模型。 七层模型(OSI 参考模型) 七层模型,即 OSI(Open System Interconnection)参考模型,是一种概念模型,用于描述网络通信的架构。它将计算机网络从下到上分为七层,各层的功能和作用如下: 物理层:物理层是计算机网络的最底层,主要负责传输比特流…...

Docker 部署教程jenkins

Docker 部署 jenkins 教程 Jenkins 官方网站 Jenkins 是一个开源的自动化服务器&#xff0c;主要用于持续集成&#xff08;CI&#xff09;和持续交付&#xff08;CD&#xff09;过程。它帮助开发人员自动化构建、测试和部署应用程序&#xff0c;显著提高软件开发的效率和质量…...

深入理解开放寻址法中的三种探测序列

一、引言 开放寻址法是解决散列表中冲突的一种重要方法&#xff0c;当发生冲突&#xff08;即两个不同的键通过散列函数计算得到相同的散列值&#xff09;时&#xff0c;它会在散列表中寻找下一个可用的存储位置。而探测序列就是用于确定在发生冲突后&#xff0c;依次尝试哪些…...

图像噪声处理技术:让图像更清晰的艺术

在这个数字化时代&#xff0c;图像作为信息传递的重要载体&#xff0c;其质量直接影响着我们的视觉体验和信息解读。然而&#xff0c;在图像采集、传输或处理过程中&#xff0c;难免会遇到各种噪声干扰&#xff0c;如高斯噪声、椒盐噪声等&#xff0c;这些噪声会降低图像的清晰…...

linux运行级别

运行级别&#xff1a;指linux系统在启动和运行过程中所处的不同的状态。 运行级别之间的切换&#xff1a;init (级别数) 示例&#xff1a; linux的运行级别一共有7种&#xff0c;分别是&#xff1a; 运行级别0&#xff1a;停机状态 运行级别1&#xff1a;单用户模式/救援模式…...

深入剖析Electron的原理

Electron是一个强大的跨平台桌面应用开发框架&#xff0c;它允许开发者使用HTML、CSS和JavaScript来构建各种桌面应用程序。了解Electron的原理对于开发者至关重要&#xff0c;这样在设计应用时能更合理&#xff0c;遇到问题也能更准确地分析和解决。下面将从多个方面深入剖析E…...