两种关系示例
在使用C++ socket编写服务器的时候,我们会使用struct指定一系列结构体作为消息报文,一般来说这些结构体都会有相同的首部,并在首部包含该结构体类型的信息。关于首部和其他消息的关系,我们一般可以选择继承关系和复合关系两种。示例如下:
- 继承关系
struct Msg { Msg(CMD cmd = CMD::LOG_ERROR, int len = sizeof(Msg)) :_cmd(cmd), _len(len) {} virtual ~Msg() {} CMD _cmd; int _len; }; struct MsgLogin : public Msg { MsgLogin() : Msg(CMD::LOGIN, sizeof(MsgLogin)) , _username(""), _password("") {} virtual ~MsgLogin() {}; char _username[NAME_LEN]; char _password[PASSWD_LEN]; };
- 复合关系
struct MsgHeader { MsgHeader(CMD cmd = CMD::LOG_ERROR, int len = sizeof(MsgHeader)) :_cmd(cmd), _len(len) {} CMD _cmd; int _len; }; struct MsgLogin { MsgLogin() : _header(CMD::LOGIN, sizeof(MsgLogin)) , _username(""), _password("") {} MsgHeader _header; char _username[NAME_LEN]; char _password[PASSWD_LEN]; };
对比分析
在bilibili上刘远东老师的C++高并发服务器教程中,老师使用的是继承关系。然而我认为,无论在什么情况下都更应使用复合关系。继承关系的缺点在于析构函数上:
-
如果不使用虚析构函数,那么当一个
Msg
类的指针指向MsgLogin
对象时,如果对该指针使用delete
函数,则无法调用MsgLogin
的析构函数,造成内存泄露。Msg* msg = new MsgLogin; delete msg; /*内存泄漏*/
-
如果使用虚析构函数,则这一系列类就是包含虚函数的类,编译器会为他们创建虚函数表。这时候类的内存结构会发生变化。以下是一个
Msg
的内存结构。
在这种情况下,我们无法简单地通过
Msg msg; recv(socket, (char*)&msg, sizeof(msg), 0);
来将接收到的信息写入到
Msg
实例中。而是需要仔细计算一个指针的偏移量来接收信息。Msg msg; recv(socket, (char*)&msg+sizeof(void*), sizeof(msg)-sizeof(void*), 0)
这样首先可读性不强,在这里看到个
sizeof(void*)
一般人的反应都是一脸懵逼,其次就是风险大。忘写虚析构和忘写偏移量都会导致致命的错误。
对于复合关系,相对于C语言的struct
来说,在增加了构造函数提供的便利的同时,避免了析构函数引起的繁琐的问题。所以对于网络报文格式的定义,复合关系是比继承关系更合适的一种设计。