CPP风格
Effective C++
- 视 C++ 为一个语言联邦(C、Object-Oriented C++、Template C++、STL)
- 宁可以编译器替换预处理器(尽量以 const、enum、inline 替换 #define)
- 尽可能使用 const
- 确定对象被使用前已先被初始化(构造时赋值(copy 构造函数)比 default 构造后赋值(copy assignment)效率高)
- 了解 C++ 默默编写并调用哪些函数(编译器暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符、析构函数)
- 若不想使用编译器自动生成的函数,就应该明确拒绝(将不想使用的成员函数声明为 private,并且不予实现)
- 为多态基类声明 virtual 析构函数(如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数)
- 别让异常逃离析构函数(析构函数应该吞下不传播异常,或者结束程序,而不是吐出异常;如果要处理异常应该在非析构的普通函数处理)
- 绝不在构造和析构过程中调用 virtual 函数(因为这类调用从不下降至 derived class)
- 令 operator= 返回一个 reference to *this (用于连锁赋值)
- 在 operator= 中处理 “自我赋值”
- 赋值对象时应确保复制 “对象内的所有成员变量” 及 “所有 base class 成分”(调用基类复制构造函数)
- 以对象管理资源(资源在构造函数获得,在析构函数释放,建议使用智能指针,资源取得时机便是初始化时机(Resource Acquisition Is Initialization,RAII))
- 在资源管理类中小心 copying 行为(普遍的 RAII class copying 行为是:抑制 copying、引用计数、深度拷贝、转移底部资源拥有权(类似 auto_ptr))
- 在资源管理类中提供对原始资源(raw resources)的访问(对原始资源的访问可能经过显式转换或隐式转换,一般而言显示转换比较安全,隐式转换对客户比较方便)
- 成对使用 new 和 delete 时要采取相同形式(new 中使用 [] 则 delete [],new 中不使用 [] 则 delete)
- 以独立语句将 newed 对象存储于(置入)智能指针(如果不这样做,可能会因为编译器优化,导致难以察觉的资源泄漏)
- 让接口容易被正确使用,不易被误用(促进正常使用的办法:接口的一致性、内置类型的行为兼容;阻止误用的办法:建立新类型,限制类型上的操作,约束对象值、消除客户的资源管理责任)
- 设计 class 犹如设计 type,需要考虑对象创建、销毁、初始化、赋值、值传递、合法值、继承关系、转换、一般化等等。
- 宁以 pass-by-reference-to-const 替换 pass-by-value (前者通常更高效、避免切割问题(slicing problem),但不适用于内置类型、STL 迭代器、函数对象)
- 必须返回对象时,别妄想返回其 reference(绝不返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个 local static 对象而有可能同时需要多个这样的对象。)
- 将成员变量声明为 private(为了封装、一致性、对其读写精确控制等)
- 宁以 non-member、non-friend 替换 member 函数(可增加封装性、包裹弹性(packaging flexibility)、机能扩充性)
- 若所有参数(包括被 this 指针所指的那个隐喻参数)皆须要类型转换,请为此采用 non-member 函数
- 考虑写一个不抛异常的 swap 函数
- 尽可能延后变量定义式的出现时间(可增加程序清晰度并改善程序效率)
- 尽量少做转型动作(旧式:(T)expression、T(expression);新式:const_cast(expression)、dynamic_cast(expression)、reinterpret_cast(expression)、static_cast(expression)、;尽量避免转型、注重效率避免 dynamic_casts、尽量设计成无需转型、可把转型封装成函数、宁可用新式转型)
- 避免使用 handles(包括 引用、指针、迭代器)指向对象内部(以增加封装性、使 const 成员函数的行为更像 const、降低 “虚吊号码牌”(dangling handles,如悬空指针等)的可能性)
- 为 “异常安全” 而努力是值得的(异常安全函数(Exception-safe functions)即使发生异常也不会泄露资源或允许任何数据结构败坏,分为三种可能的保证:基本型、强列型、不抛异常型)
- 透彻了解 inlining 的里里外外(inlining 在大多数 C++ 程序中是编译期的行为;inline 函数是否真正 inline,取决于编译器;大部分编译器拒绝太过复杂(如带有循环或递归)的函数 inlining,而所有对 virtual 函数的调用(除非是最平淡无奇的)也都会使 inlining 落空;inline 造成的代码膨胀可能带来效率损失;inline 函数无法随着程序库的升级而升级)
- 将文件间的编译依存关系降至最低(如果使用 object references 或 object pointers 可以完成任务,就不要使用 objects;如果能过够,尽量以 class 声明式替换 class 定义式;为声明式和定义式提供不同的头文件)
- 确定你的 public 继承塑模出 is-a(是一种)关系(适用于 base classes 身上的每一件事情一定适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base class 对象)
- 避免遮掩继承而来的名字(可使用 using 声明式或转交函数(forwarding functions)来让被遮掩的名字再见天日)
- 区分接口继承和实现继承(在 public 继承之下,derived classes 总是继承 base class 的接口;pure virtual 函数只具体指定接口继承;非纯 impure virtual 函数具体指定接口继承及缺省实现继承;non-virtual 函数具体指定接口继承以及强制性实现继承)
- 考虑 virtual 函数以外的其他选择(如 Template Method 设计模式的 non-virtual interface(NVI)手法,将 virtual 函数替换为 “函数指针成员变量”,以 tr1::function 成员变量替换 virtual 函数,将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数)
- 绝不重新定义继承而来的 non-virtual 函数
- 绝不重新定义继承而来的缺省参数值,因为缺省参数值是静态绑定(statically bound),而 virtual 函数却是动态绑定(dynamically bound)
- 通过复合塑模 has-a(有一个)或 “根据某物实现出”(在应用域(application domain),复合意味 has-a(有一个);在实现域(implementation domain),复合意味着 is-implemented-in-terms-of(根据某物实现出))
- 明智而审慎地使用 private 继承(private 继承意味着 is-implemented-in-terms-of(根据某物实现出),尽可能使用复合,当 derived class 需要访问 protected base class 的成员,或需要重新定义继承而来的时候 virtual 函数,或需要 empty base 最优化时,才使用 private 继承)
- 明智而审慎地使用多重继承(多继承比单一继承复杂,可能导致新的歧义性,以及对 virtual 继承的需要,但确有正当用途,如 “public 继承某个 interface class” 和 “private 继承某个协助实现的 class”;virtual 继承可解决多继承下菱形继承的二义性问题,但会增加大小、速度、初始化及赋值的复杂度等等成本)
- 了解隐式接口和编译期多态(class 和 templates 都支持接口(interfaces)和多态(polymorphism);class 的接口是以签名为中心的显式的(explicit),多态则是通过 virtual 函数发生于运行期;template 的接口是奠基于有效表达式的隐式的(implicit),多态则是通过 template 具现化和函数重载解析(function overloading resolution)发生于编译期)
- 了解 typename 的双重意义(声明 template 类型参数是,前缀关键字 class 和 typename 的意义完全相同;请使用关键字 typename 标识嵌套从属类型名称,但不得在基类列(base class lists)或成员初值列(member initialization list)内以它作为 basee class 修饰符)
- 学习处理模板化基类内的名称(可在 derived class templates 内通过 this-> 指涉 base class templates 内的成员名称,或藉由一个明白写出的 “base class 资格修饰符” 完成)
- 将与参数无关的代码抽离 templates(因类型模板参数(non-type template parameters)而造成代码膨胀往往可以通过函数参数或 class 成员变量替换 template 参数来消除;因类型参数(type parameters)而造成的代码膨胀往往可以通过让带有完全相同二进制表述(binary representations)的实现类型(instantiation types)共享实现码)
- 运用成员函数模板接受所有兼容类型(请使用成员函数模板(member function templates)生成 “可接受所有兼容类型” 的函数;声明 member templates 用于 “泛化 copy 构造” 或 “泛化 assignment 操作” 时还需要声明正常的 copy 构造函数和 copy assignment 操作符)
- 需要类型转换时请为模板定义非成员函数(当我们编写一个 class template,而它所提供之 “与此 template 相关的” 函数支持 “所有参数之隐式类型转换” 时,请将那些函数定义为 “class template 内部的 friend 函数”)
- 请使用 traits classes 表现类型信息(traits classes 通过 templates 和 “templates 特化” 使得 “类型相关信息” 在编译期可用,通过重载技术(overloading)实现在编译期对类型执行 if…else 测试)
- 认识 template 元编程(模板元编程(TMP,template metaprogramming)可将工作由运行期移往编译期,因此得以实现早期错误侦测和更高的执行效率;TMP 可被用来生成 “给予政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码)
- 了解 new-handler 的行为(set_new_handler 允许客户指定一个在内存分配无法获得满足时被调用的函数;nothrow new 是一个颇具局限的工具,因为它只适用于内存分配(operator new),后继的构造函数调用还是可能抛出异常)
竞赛风格指南
利用 C++17。使用 -Wall -Wextra -Wshadow 标志进行编译,并尝试消除所有警告消息,这将防止您遇到一些愚蠢的错误。还有更多调试标志,例如 -fsanitize=undefined 可帮助您消除运行时数组超出范围访问和整数溢出等错误。有关更多信息,请查看“阅读更多”部分。
C++库对函数和类使用 snake_case,为了将用户定义的代码与标准库区分开来,我们将使用 CamelCase。
- 类型使用 UpperCamelCase:
Point,SegTree - 函数和变量使用 lowerCamelCase:
someMethod,someVariable - 宏和常量使用由以下部分 _ 隔开的所有大写字母:
SOME_MACRO、MAX_N、MOD - 使用有意义的名字,或者至少对你来说足够有意义。
使用 #include <bits/stdc++.h> 代替许多包含。
使用 using namespace std; 而不是 std:: 每次都键入。
例如,使用 using 代替 typedef using ll = long long; 。基本原理:它更符合现代 C++的风格。
使用 struct 代替 class .基本原理:它默认为公开,您不需要在竞争性编程中封装!
不要使用太多宏,但不要害怕使用宏!基本原理:调试和读取充满丑陋宏的代码并不容易。但我们毕竟是黑客!
用于 const 定义常量,而不是 #define 。基本原理:常量有一个类型,它们在编译时被计算。
为了避免错误,您可以对每个 switch 语句大小写使用大括号。
用于 auto 增加可读性并减小代码大小。
使用支撑的初始值设定项列表。
在处理对和元组时使用 emplace 和 emplace_back for 容器。理由: (elem1, elem2, ...) 而不是 ({elem1, elem2, ...}) .
使用 lambda 函数!当将函数作为参数传递时,它们特别有用 sort ,例如 in 。不要重复自己,在代码中使用 lambda 函数而不是复制/粘贴。
用 代替 nullptr NULL 或 0 。
布尔值是 true 和 false !
使用 ios::sync_with_stdio(false); 和 cin.tie(nullptr); 以获得更快的 I/O 使用 cin/cout 。
使用以 __builtin 开头的内置函数。
GCD 和 LCM 在 C++17 中可用,位于 gcd 和 lcm 下。
对循环使用 C++11 for each 样式。 for (auto& elem : vec)
使用 C++17 绑定样式,如 for (auto& [key, val] : dic) 和 auto [x, y] = myPoint;
使用 C++ 模板参数推导 pair p{1, 2.5}; 而不是 pair<int, double> p{1, 2.5}; .
如果您有很多嵌套循环和条件,请重构!您可能应该使用函数。
切勿使用 goto !但是,当你想从几个嵌套循环中解脱出来时,要勇敢地使用 goto (以防你无法重构它)!
一些网站(如代码部队)使用标志 -DONLINE_JUDGE 来编译您的代码,这意味着您可以自动删除您的 cerr s 或调试函数,或者将输入/输出重定向到文件而不是 stdin/stdout 等。
比如
#define all(x) (x).begin(), (x).end()
sort(all(vec));
sort(vec.begin(), vec.end());
sort(1 + all(vec)); // 1 + (x).begin(), (x).end()
int nxt() {
int x;
cin >> x;
return x;
}
// 未使用nxt
int n, m;
cin >> n >> m;
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
--u, --v;
g[u].push_back(v);
g[v].push_back(u);
};
//使用nxt
int n = nxt(), m = nxt();
for (int i = 0; i < m; ++i) {
int u = nxt() - 1, v = nxt() - 1;
g[u].push_back(v);
g[v].push_back(u);
}
但是,要小心!如果 pts 是点的向量,那么最好按以下方式读取它:
for (int i = 0; i < (int)pts.size(); ++i) {
cin >> pts[i].x >> pts[i].y;
}
或至少采用以下 C++ 17 方式:
for (auto& [x, y] : pts) {
cin >> x >> y;
}
但不要执行以下操作:
for (int i = 0; i < (int)pts.size(); ++i) {
pts[i] = {nxt(), nxt()};
}
因为在最后一个实现中,没有定义调用的 nxt() 顺序。
您也可以更改函数模板的类型 long long ,甚至制作函数模板,但我真的不觉得我需要最后一个。
非常感谢我的前队友 savinov 和 sokian 在 2015 年我加入他们的团队模板中向我介绍了这一点。 nxt() 稍后将在本博客中返回。
memset & fill
你们中的许多人都知道,如果想用特定字节填充一段内存,他们可以使用 memset .它的速度非常快,可用于用零和负一填充一些(通常是一维)C 阵列。
#include <bitset>
#include <climits>
#include <cstring>
#include <iostream>
int main()
{
int a[4];
using bits = std::bitset<sizeof(int) * CHAR_BIT>;
std::memset(a, 0b1111'0000'0011, sizeof a);
for (int ai : a)
std::cout << bits(ai) << '\n';
}
00000011000000110000001100000011
00000011000000110000001100000011
00000011000000110000001100000011
00000011000000110000001100000011
如果你想用一个装满一个容器怎么办?答案就像馅饼一样简单:
fill(all(vec), 1);
如果你需要用连续的数字来填充它,你可以使用 std::iota .现在,具有两个优化的 Dsu 类的构造函数可能如下所示:
int n;
vector<int> parent, rank;
Dsu(int _n): n(_n), parent(_n), rank(_n) {
iota(all(parent), 0);
}
这里 0 表示 *parent.begin() 的值,每个下一个值都是通过预增量从前一个值获得的。
std::generate
C++20
如果你有一个 0 元函数(即没有参数)并且想通过它的调用来填充一个范围,而不是编写一个 for 循环,你可以调用 std::generate : vec 用随机值填充向量(假设它是 rand() 调用)可能看起来像
generate(all(vec), rand);
我最喜欢的:先读 n,然后写 n 数字
int n = nxt();
vector<int> a(n);
generate(all(a), nxt);
或者,如果你不需要 later 的 n 值,甚至
vector<int> a(nxt());
generate(all(a), nxt);
最后三个功能有一个 _n 类似物。而不是结束迭代器/指针, fill_n iota_n 并将 generate_n size 作为第二个参数。因此,如果你想将第一个 n 数字读入一个大于 n 的向量 vec 中,你可以使用
generate_n(vec.begin(), n, nxt);
而不是更长
generate(vec.begin(), vec.begin() + n, nxt);
从向量构造集合
我们中没有多少人知道,但是如果你想 std::set 从一个向量创建一个,并发现该集合没有向量的构造函数,你可以去写一个带有 ok face 的循环,比如:
set<int> S;
for (int x : a) {
S.insert(x);
}
但是, std::set 具有两个迭代器的构造函数,因此可以编写
set<int> S(all(a));
这实际上是很自然的,人们可以推断出该集合应该有这样的构造函数,我才知道。
检查 set 或 map 是否有 key
if (S.find(key) != S.end()) {
// ...
}
但是,set 和 map 有一个 .count() 方法,如果键在容器中,则返回 1,否则返回 0:
if (S.count(key)) {
// ...
}
之所以这样称呼它并具有 int 类型, std::multiset 是因为并且 std::multimap 也有这个方法,对于它们,该方法返回元素计数,显然可能超过 1。
当然,如果你需要对元素做一些事情,如果它存在于集合中(例如,擦除它),那么实际调用 .find() 方法以便通过迭代器而不是按元素擦除(因此有点缓存一棵树下降)可能会很有用。
多个值的最小值
int x = min({a, b, c, d});
在 if 语句中引入变量
想象一下:你有一个函数 f() ,它需要时间来计算,如果它的值满足某个条件,你想以某种方式使用它。你不想写
if (is_good(f())) {
use_somehow(f());
}
因为它需要两次调用. f 你可以这样写:
int x = f();
if (is_good(x)) {
use_somehow(x);
}
但这并不是很干净,并且留下了一个当时未使用的变量,也许在一个可能有用的名称下。为了避免这种情况,可以将所有这些包装成一个块,如下所示:
{
int x = f();
if (is_good(x)) {
use_somehow(x);
}
}
但较短的版本将执行以下操作:
if (int x = f(); is_good(x)) {
use_somehow(x);
}
这是可能的,因为 C++17。
尽量少使用 using 指示
using namespace std;
应该多使用 using 声明
int x;
std::cin >> x ;
std::cout << x << std::endl;
或者
using std::cin;
using std::cout;
using std::endl;
int x;
cin >> x;
cout << x << endl;