命名规范:
- 变量、函数名和命名空间用snake_case,全局变量加"g_"前缀
- 自定义类名用CamelCase风格,成员函数用snake_case,成员变量加"m_"前缀。
- 宏和常量应当全大写,单词之间用下划线连接
- 尽量不要将下划线作为变量的前缀或后缀,很难识别
CamelCase:驼峰命名法,分为大驼峰,小驼峰。区别为首字符是否要大小写。
snake_case:用的全是小写,单词之间用下划线连接。
#define MAX_PATH_LEN 256 //常量全大写
int g_sys_flag; //全局变量,加"_g"前缀
namespace linux_sys { //名字空间,全小写
void get_rlimit_core(); //函数,全小写
}
class FilePath final //类名,首字母大写
{
public:
void set_path(const string &str); //函数,全小写
private:
string m_path; //成员变量,加"m_"前缀
string m_level;
}
变量/函数的名字长度与它的作用域成正比,局部变量/函数的名字可以短一点,而全局变量/函数的名字应该长一点。
注释规范
// author : xxx
// date : 2022-xx-xx
// purpose : get inner counter value of generic T
// notice : T must have xxx member
// notice : return value maybe -1. means xxx, you should
template<typename T>
int get_value(const T& v);
注释用英文,utf8编码。
源码组织和管理
可以不用实现代码分离(.h和.cpp),只用一个文件实现"hpp",可以理解成(.h + .cpp),类的完整实现都写在里面(极少数语法限制必须放在cpp里的成员除外),相当于把原来放在两个文件里的代码整合在一起,大致如下:
#ifndef _XXX_HPP_INCLUDED_
#define _XXX_HPP_INCLUDED_
class XXX final
{
public:
void function1() {...}
void function2() {...}
};
#endif //_XXX_HPP_INCLUDED_
好处:方便源码管理
代码风格其它注意事项
- 代码行宽尽量限制在80列之内,超过了必须换行缩进对齐
- 花括号"{}"应该保持一致的对齐格式,“{”单独一行或者在行尾都可以,但"}"必须单独一行且后面留以空行
- if-else/for等复合语句,即使只有一行也使用花括号。
- if-else/switch/for等语句的嵌套层不宜过深,否则不仅阅读困难,还增加了逻辑复杂
- 循环语句、函数体不宜过长,尽量控制在50-100行,这样可以在一个页面内显示完整
- 函数的入口不宜过多,如果确实有必须应用struct/tuple打包
包含文件
常用的预处理指令应该是"#include",它的作用是"包含文件"。注意,它不是“包含头文件”,而是包含任意文件。 “#include”其实是非常"弱"的,不做什么检查,就是“死脑筋”地把数据合并进源文件。
所以我们在写头文件的时候,为了防止代码被重复包含,通常加上“Include Guard”,也就是用"#ifndef/#define/#endif"来保护整个头文件,如下:
#ifndef _XXX_H_INCLUDE_ //检查是否定义了宏
#define _XXX_H_INCLUDE_ //没有则定义宏
... //头文件内容
#endif //_XXX_H_INCLUDE
通常C++预处理器还支持使用"#pragma once"防止重复包含,但存在兼容问题。
宏定义
“#define”是预处理编程里的核心指令,它用来定义源码级别的“文本替换”,也就是我们常说的“宏定义”。
- 宏的展开、替换发生在预处理阶段,所以对于一些调用频繁的小代码片段来说,用宏来封装的效果比使用inline关键字要好,因为它实现了源码级的无条件内联。
#define ngx_tolower(c) ((c >= 'A' && c <= 'Z') ? (c | 0x20) : c)
#define ngx_toupper(c) ((c >= 'a' && c <= 'z') ? (c &~ 0x20) : c)
#define ngx_memzero(buf, n) (void) memset(buf, 0, n)
2.宏没有作用域概念,永远全局生效。对于临时的宏,最好用完后尽快使用#undef取消定义,避免发生冲突
#define CLUB(a) (a) * (a) * (a) //定义求立方的宏
cout << CLUB(10) << endl;
cout << CLUB(15) << endl;
#undef CLUB //取消定义
或者
#ifdef AUTH_PWD
# undef AUTH_PWD
#endif
#define AUTH_PWD "xxx"
3. 消除魔鬼数字
#define MAX_BUF_LEN 65535
#define VERSION "10.0.18"
4. 文本替换
#define BEGIN_NAMESPACE(x) namespace x{
#define END_NAMESPACE(x) }
//使用
BEGIN_NAMESPACE(my_own)
... //functions and classes
END_NAMESPACE(my_own)
条件编译
#ifdef __cplusplus //标记C++版本号
extern "C"{
#endif
void a_c_function(int a);
#ifdef __cplusplus
}
#endif
#if __cplusplus >= 202002 //检查C++版本号
cout << "c++20 or later" << endl;
#elif __cplusplus >= 201703
cout << "c++17 or later" << endl;
#else // __cplusplus < 201703
# error "C++ is too old"
#endif
C++里面定义的预定义的宏,可以帮助我们识别编译环境,具体内容如下:
__FILE__:源文件名
__LINE__:源文件行号
__DATE__:预处理时的日期
__has_include:是否存在某个可包含的文件
__cpp_modules:是否支持模块机制
__cpp_decltype:是否支持decltype特性
__cpp_decltype_auto:是否支持decltype(auto)特性
__cpp_lib_make_unique:是否提供函数make_unique()
属性
属性没有新增关键字,而是用两对方括号的形式标识,即"[[...]]",方括号的中间填写的是属性标签。
下面是一个简单的例子,显示声明函数没有返回值,一看就能明白:
[[noreturn]] //属性:函数绝不会返回任何值
int func(bool flag)
{
throw std::runtime_error("XXX"); //只抛出异常
}
目前认为比较有用的属性:
noreturn : 显式声明函数无返回值
nodiscard : 显式声明不允许忽略函数返回值
deprecated : 废弃某段代码,不鼓励使用
maybe_unused : 显示标记某段代码暂时不用,但保留,因为将来可能要用
fallthrough : 仅用于switch语句中
likely/unlikely : 标记某段代码路径更可能/更不可能执行,指示编译器优化
静态断言
assert是一个宏,它在预处理阶段并不生效,而是在运行阶段才起作用,所以assert又叫做“动态断言”。
static_assert是一个专门C++关键字而不是宏,因为它只在编译时生效,运行阶段“看不见”,所以是静态的。它是在编译阶段检测各种条件的断言,表达式值如果是false就会报错,导致编译失败。
可以配合标准库里的“type_traits”,它提供了对应这些概念的各种编译期“函数”,又叫“模板元函数”:
static_assert(is_integral_v<T>); //断言T是整数类型
static_assert(is_pointer_v<T>); //断言T是指针类型
static_assert(is_default_constructible_v<T>); //断言T有默认构造函数