编程风格: 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::string比string长,每次输入std::很麻 烦,可以在 cpp文件中,using namespace std。头文件可能被其他用户 引用,其他用户也许不想使用 namespacestd。
- 
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没有参数,如果要传递参数怎么破?