欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

如何写出好看的C++代码

程序员文章站 2022-03-27 09:52:26
...

  常跟人说,今年是我编程第十年了,听起来很夸张,不过是真的。经过2010、15和17年,现在算有自己的编码习惯和风格;但是每次回头看前一阵的代码,都会有很多地方不满意,甚至确信不该那么写;或许是因为懈怠,或许是并未熟记本应坚持的好习惯。

  360离职前交接代码,老大帮我把1、2、3、4、5、6、7改成enum结构体,用XXXX_BIT代替;甚为惭愧,于是把一坨if-else改成switch-case,具体内容抽象成函数。写go parser那段时间,扶摇教给我如何写规范的git commit log,以及scala如何优雅的关闭文件描述符。这些细节让我印象深刻,好看的代码是如此重要,列一些点提醒自己时刻牢记。

  函数里尽量不要出现数字和字符串常量

  最低级的代码就是充斥着大量数字如0、1、-1等,没人知道是什么意思,作为函数返回值,意义极其不明确;或者各种裸字符串"something wrong",每次都要跳到代码逻辑里修改。正确的做法是定义成宏或者const常量,或者enum(c++11以后考虑enum class)。

  不管是用作返回值,还是标志位、大小等,都会变得意义明确;而且放在合适的头文件,一次修改,全体收益。

  我要是review代码,绝对不给出现数字和字符串的通过。

  #define INVALID_FD -1

  #define STATUS_OK 0

  #define STATUS_FAIL 0

  #define TEXT_FAIL "something wrong"

  if (get_file_content(data)==STATUS_OK) {

  ...

  }

  命名风格统一且清晰明了

  最好能通过名字,一眼看出是类、函数、const函数还是宏,是全局变量、静态变量、局部变量还是常量;是锁、文件描述符、socket还是指针;功能是get、set、push、pop还是del。类的成员变量和成员函数,也要保持一致的明明习惯,能通过名字看出是类成员。

  其实参考内核代码就很好,例如Windows一般是大驼峰,而linux则是小写加下划线。而c++类名一般大写开头,宏一般全大写,而函数则全部小写加下划线;变量名加单字符前缀用以区分。

  // 例如全局变量加g_, 静态变量加c_,类成员变量加_,局部变量什么都不加

  g_global_num, s_static_num, c_const_num, m_local_num, _class_data

  g_mutex, g_log_fd, g_server, client, sock, ret

  控制好空格和回车

  if(x>0){...}

  for(int i=0;i

  ...

  }

  int x(){

  ...

  }

  int y(){

  ...

  }

  上面这段代码看着很乱,好看的应该是这样的:

  if (x>0) {

  ...

  }

  for (int i=0; i

  ...

  }

  int x() {

  ...

  }

  int y() {

  ...

  }

  声明放在合适的头文件/源文件

  尽量把公用的声明放到头文件,例如宏、通用函数,不仅能减少代码,还会促使人们更好的组织自己的代码结构。举个例子,redis代码风格很差,而nginx和leveldb简直是典范。

  这还涉及到文件划分,什么函数或类应该单独抽出来,哪些可以混在一起;还是要根据耦合程度,完全独立且可能被其他地方引用的代码,就要公用。我见过每个文件都有一个类似convert_to_string的函数,互相拷贝,连抽成一个common.cpp都懒得做。

  头文件放太多东西,也会导致编译速度下降,很多人提倡PImpl,可惜这个要求太高了,最后难免弄得更乱。

  但是有一点要牢记:项目里不出现两个功能一模一样的函数。

  引用的每个头文件都要知道其作用,且规范引用路径

  这个简直是编译灾难,而且新人看代码会一脸懵逼,想单独写个简单测试程序,甚至得把所有项目依赖搞过来。

  要我说,就不应该有把所有头文件include到一起,功能划分好,按需引用;而且引用路径要规范,有的是#include "test.h",有的是#include

  最好的结果是把源文件、引用的头文件拎出来,能直接编译;不然就是不规范。手游拍卖平台拔出萝卜带出泥,光引用的头文件就十几个,它们又引用一堆,代码怎么能算组织好。

  函数参数尽量不要超过四个

  经常见到这样的函数:

  int set_something_to_redis(int count, int number, DataInfo* info, int timeout, bool exist, int& succ_num, bool& result, int& used_time) {

  ...

  }

  真的太长了,要么把参数组织成结构体:

  #define __OUT

  enum class REDIS_OPT {

  REDIS_GET=0,

  REDIS_SET=1,

  ...

  };

  struct RedisOptData{

  REDIS_OPT opt;

  DataInfo *info;

  __OUT int succ_num;

  __OUT bool result;

  };

  要么回车分割好,堆在一行真的不行。

  控制函数行数,小屏幕也能看全

  一个好的函数应该是这样的(每行也尽量注意,别有的太长,其他太短):

  int good_function(Param* param)

  {

  int status=STATUS_OK;

  do {

  if (!check_param(param)) {

  ERR_LOG(TEXT_PARAM_ERROR);

  status=STATUS_PARAM_ERROR;

  break;

  }

  if (do_thing1(param) !=STATUS_OK) {

  ...

  break;

  }

  ...

  } while (0);

  release_resources();

  return status;

  }

  整个屏幕能看到所有代码,如果已经足够抽象,还是太长,那就没办法,业务太复杂。

  尽量让每条语句逻辑独立且意义完整

  也是针对函数,从头看到尾,每句话都要跟这个函数相关,而不要把人带出去思考:这个是干啥的?怎么实现的不重要,做了什么一定要清晰,要么函数命名规范,要么加注释。

  更进一步,需要多条语句完成的工作,为什么不抽象成另一个函数?如果觉得函数调用影响效率,inline或者lambda或者宏?

  这里有一条边界,一个工作需要多个工作流程来完成,每个流程是一条语句(包括函数调用、for循环等语句块);如果一个流程需要不止一个操作,那么这个流程就是可以抽象成单独的函数。

  这其实也对函数做了一定要求:函数名字即所功能实现,函数功能要么很新鲜很简短。大家应该都见过好多个代码相似难辨差异的函数,绝大多数流程一样,只是为了业务需求,拷贝过来做了简单修改,却留下一堆垃圾。

  有一次,某个并不复杂的业务需求,同事写了个递归全排列(先不说代码的bug:错误检查、死循环等),这个风格并不好。如果需要全排列,我们可以写个生成全排列的函数,得到排列结果再进一步处理;而不是直接把业务逻辑揉在一起。

  局部变量在最小作用域声明

  经常见到函数开头几句定义一大堆变量,实际上很多只在某个for循环内部用到,这个完全可以定义在for里头。我的意见是尽量靠近第一次使用的地方定义,同时尽量限定在最小作用域;还有一个好处,在循环这种场景,每次都会执行默认初始化,不