编程风格: C++ 类的结构

如果一个类比较复杂,我会按照下面的顺序写一个类。

class A : public interface{
public: // 类静态函数
     static class_method1() ; //
private:
     static class_priate_method1() ; // 尽量不要有
public:
     explicit A(); // 尽量使用 explicit
private:
     // 没有拷贝构造函数,就显式禁止拷贝构造函数,如果有,则显示定义
     A( const A& x);
     // 禁止 = 操作符重载。如果有,则显示定义。
     A& operator=(const A& other);
public:
     virtual // 这里一定想好,是不是应该有一个 virtual
     ~A();
private:
     // 实现父类的公有接口。
     virtual void foo();
private:
     // 类私有函数
private:
     // 真正的类私有变量
};

// 尽量不要使用,protected
// 尽量不要使用超过三层以上的继承关系。

详细展开讨论

类方法 (class method, static member function)

这个不属于类对象,通常是一些构造对象用的函数。

class A_interface {
public:
     static A * create();
public:
     virtual void interface1() = 0;
     virtual void interface2() = 0;
};

这是一个比较好的父类结构

  • 类没有成员变量。
  • 类没有构造函数,因为没有成员变量。
  • 类没有私有函数,没有非虚函数。
  • 只有一个 类方法 Create()
  • 从用户的角度上看,这个结构一目了然,调用
A_interface * obj= A_interface::Create();
obj->interface1();
obj->interface2();

构造函数

构造函数尽量使用 explicit 的关键字。这个关键字可以阻止意想不到的构造函 数调用。

拷贝构造函数

如果不想使用拷贝构造函数,给他弄成 private ,这样的好处是,无论是谁, 包括父类,子类,还是用户,都不能访问。拷贝构造函数在很多时候,隐含调用, 增加了调试难度,同时也是 bug 产生的发源地。这样做的之后,又如下限制,

不能声明这样的函数 Foo(A x) ,因为这个需要拷贝构造函数调用。同样原理, 不能使用 vector<A> 等容器。我体会实际使用中,这样类型的类还占大多数。 很多情况下,都是指针或者应用传递才有意义。赋值操作符重载

这个也是 bug 的发源地。原理同拷贝构造函数类似。

析构函数

如果类里面有虚函数,析构函数一定要是虚函数。

虚函数的实现

尽管父类的虚函数是 public ,但是在子类实现的时候,最好是 private

一个例子

我写一个例子程序说明我的想法。这个程序是 busybox ,就是用一个程序实 现很多 unix 下常用的命令。

首先创建一个 cmd.h

#pragma once
#include <string>
// using namespace std
class cmd_c {
public:
    static cmd_c * create(std::string cmd);
public:
    virtual ~cmd_c();
public:
    virtual int execute(int argc, char * argv[]) = 0;
};
  • #progam once 代替

    #ifndef CMD_H
    #define CMD_H
    ....
    #endif
    

    #program once 的好处是,短小。不用三行

    也不用担心 CMD_H 宏重命名的问题。如果你有连个头文件,一不小心(概率 很小),两个头文件用同样的宏保护,而且两个头文件同时被引用,就会出问题。

    坏处就是,这个不是所有的编译器都支持,但是幸运的是,几乎所有的编译器都 支持,包括 vs, gcc, clang 等等。

  • 不要使用 using namespace std

    在头文件的定义中,尽量不要 using namespace std 这样会引起命名空间污 染。如果你觉得 std::stringstring 长,每次输入 std:: 很麻 烦,可以在 cpp文件中,using namespace std 。头文件可能被其他用户 引用,其他用户也许不想使用 namespace std

  • virtual ~cmd_c 很重要,保证析构函数是虚函数,否则不能调用子类的析 构函数。

  • 不用禁止构造函数。

    因为 execute 都是纯虚函数,就不要声明私有的构造函数了。因为如果一 个类有纯虚函数,则不能构造对象。因为只能通过 create 函数创建 cmd_c 的子类对象,否则无法创建一个 cmd_c 的对象。这就是纯虚函数 的好处。

  • 初学 C++ 的,很容易忽略纯虚函数的作用。

创建 main.cpp

#include "cmd.h"
using namespace std;
int main(int argc, char *argv[])
{
    cmd_c * cmd = cmd_c::create(string(argv[0]));
    int ret = cmd->execute(argc,argv);
    delete cmd;
    return ret;
}

从用户的角度上看 cmd_c 这个类,十分清爽。

创建 cmd_args.h ,第一个 cmd_c 的实现

#pragma once
#include "cmd.h"
class cmd_args_c: public cmd_c {
public:
    static cmd_c * create();
private:
    explicit cmd_args_c();
    explicit cmd_args_c(const cmd_args_c&);
    cmd_args_c& operator= (const cmd_args_c&);
    virtual  ~cmd_args_c();
private:
    virtual int execute(int argc, char * argv[]);
};
  • 禁止了构造函数,拷贝构造函数,赋值操作符重载。
  • 也禁止了析构函数。因为父类的析构函数是虚函数,所以子类可以重载虚函数。

创建 cmd_args.cpp

#include <iostream>
using namespace std;
#include "cmd.h"
#include "cmd_args.h"
cmd_args_c::cmd_args_c()
{
}
cmd_args_c::cmd_args_c(const cmd_args_c&)
{
}
cmd_args_c& cmd_args_c::operator= (const cmd_args_c&)
{
    return *this;
}
cmd_args_c::~cmd_args_c()
{
}
cmd_c * cmd_args_c::create()
{
    return new cmd_args_c();
}
int cmd_args_c::execute(int argc, char* argv[])
{
    for(int i = 0; i < argc; ++i){
        cout << "argv[" << i << "]:" << argv[i] << endl;
    }
    return 0;

}

创建 cmd.cpp,定义如何创建具体对象

#include <cassert>
#include "cmd.h"
#include "cmd_args.h"
using namespace std;

cmd_c::~cmd_c()
{
}
static struct {
    const char * name;
    cmd_c* (*func)();
} command_table [] = {
    {"./args", cmd_args_c::create },
    {NULL,NULL}
};
cmd_c * cmd_c::create(string cmd)
{
    cmd_c * ret = NULL;
    for(int i = 0; command_table[i].name !=NULL; ++i){
        if(cmd == command_table[i].name){
            ret = command_table[i].func();
            break;
        }
    }
    assert(ret);
    return ret;

}

在 cmd.cpp 里面可以放心大胆的使用 using namespace std ,爱怎么弄怎么弄。 没有名字污染的问题。

cmd.cpp 里面要定义 ~cmd() 否则链接的时候会报错

cmd_args.cpp:(.text+0xb1): undefined reference to `cmd_c::~cmd_c()'

定义 command_table ,这里使用了匿名结构体。因为只有这一个全局变量, 可以避免名字空间污染,也可以避免为了起名浪费脑细胞。

command_table 是 static 变量,遵循原则,“尽量减少变量的作用域”,

为了遵循这个原则,是不是可以把 command_table 放在 cmd_c::create 的 内部。

cmd_c * cmd_c::create(string cmd)
{
   static .... command_table ...;
}

这样违反了另一个原则,“分离数据和实现” 。这里是用来注册子类的构造方法 的地方, cmd_c 的子类实现者要很容易找到在哪里注册一个新的子类实现, 而不用关心 cmd_c::create 的具体实现了。

cmd_c::create 的结尾用 assert(0) 表示没有实现的功能 ,如果以后写得 复杂了,需要处理如果命令找不到怎么办。这里为了简单,就不处理了。

编译之,哈哈

因为有多个 cpp 的文件,写一个简单的 Makefile

CXXFLAGS += -Wall -Werror -Wextra
all: busybox

busybox: main.o cmd.o cmd_args.o
	$(CXX) -o $@ $+

注意: -Wall -Werror -Wextra 是一个很好的习惯,尽量尽量好准守。我用 了两个“尽量”。项目初期就加上这个,其实影响很小,到了项目后期,如果除掉 所有的 warning 还是很麻烦的事。一个好的程序员,对于 warning 应该产生罪 恶感。

使用 make

% make
% clang++ -Wall -Werror -Wextra   -c -o main.o main.cpp
% clang++ -Wall -Werror -Wextra   -c -o cmd.o cmd.cpp
% clang++ -Wall -Werror -Wextra   -c -o cmd_args.o cmd_args.cpp
% clang++ -o busybox main.o cmd.o cmd_args.o

运行

bash$ ln -s ./busybox args
bash$ ./args
argv[0]:./args
bash$ ./args show me
argv[0]:./args
argv[1]:show
argv[2]:me

创建一个符号连接到 busybox 上,这个是 Linux 下的通用做法。保证 "./args" 是第一个参数 argv[0]

运行结果就自说明了。

这只是一个例子,很容易处理其他情况,例如,

  • 不在当前目录运行的时候,argv[0] 不是 "./args‘怎么处理,
  • 命令找不到怎么处理。
  • 怎么添加一个新的命令。例如 cmd_ls_c, cmd_cp_c

其他问题:

  • cmd.cpp 依赖于所有子类的实现,怎么破?
  • cmd_args_c::create 没有参数,如果要传递参数怎么破?