编程风格: 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
没有参数,如果要传递参数怎么破?