是否曾好奇struct定义的数据结构类型,当我拷贝构造时,或者赋值操作时会发生什么?倘若我结构中存在指针引用对象时,又能否正确处理?带着这些疑问,我们来对struct的构造函数进行研究,以解答以下几个疑问:
1) 何时编译器会自动为struct合成构造函数
2) 如何能保证携带指针引用对象的struct正确拷贝或拷贝构造
让我们先来看第一个问题,考虑如下代码。ServerConfig只有两个简单的成员,通过反汇编可见编译器合成了ServerConfig的构造函数,并调用其成员的构造函数。若我们移除addr成员,编译器则不会为ServerConfig合成构造函数。由此不难发现,当struct成员存在构造函数时,编译器会自动为其生成构造函数。
但是值得注意class的默认构造函数不是必须的,也就是说。默认构造函数是编译器所需要的,它用以保证程序的正确运行,如初始化虚表指针;并非为程序提供默认初始值之类。当class继承自含默认构造函数的父类时,具有默认构造函数的成员时,存在virtual function时,或者virtual继承时; 会触发编译器合成默认构造函数。
#include <stdio.h>
#include <string.h>
#include <stdint.h>
class CString
{
public:
CString()
{
m_str = strdup("");
}
CString(const char *str)
{
m_str = strdup(str);
}
~CString()
{
delete m_str; m_str= NULL;
}
private:
char *m_str;
};
typedef struct {
int port;
CString addr;
}ServerConfig;
int main(int argc, char *argv[])
{
ServerConfig config1;
return 0;
}
(gdb) disassemble main
Dump of assembler code for function main(int, char**):
...
0x000000000040065d <+16>: lea -0x20(%rbp),%rax # config1地址放入rax
0x0000000000400661 <+20>: mov %rax,%rdi # 通过rdi传入this指针
0x0000000000400664 <+23>: callq 0x4006ce <ServerConfig::ServerConfig()> # 构造config1
0x0000000000400669 <+28>: mov $0x0,%ebx
0x000000000040066e <+33>: lea -0x20(%rbp),%rax
0x0000000000400672 <+37>: mov %rax,%rdi
0x0000000000400675 <+40>: callq 0x4006ec <ServerConfig::~ServerConfig()>
...
(gdb) disassemble ServerConfig::ServerConfig
Dump of assembler code for function ServerConfig::ServerConfig():
...
0x00000000004006d6 <+8>: mov %rdi,-0x8(%rbp) # 获取this指针
0x00000000004006da <+12>: mov -0x8(%rbp),%rax #
0x00000000004006de <+16>: add $0x8,%rax # this + 0x08, 偏移掉port计算得到addr的地址
0x00000000004006e2 <+20>: mov %rax,%rdi #
0x00000000004006e5 <+23>: callq 0x400684 <CString::CString()> # 调用CString构造函数
...
但是上面的代码是危险的,只要我们对ServerConfig引用了拷贝构造或者赋值操作时,会引发double free。那这又是为什么呢?让我们考虑如下代码,编译后我们进行反汇编。不难发现ServerConfig并未合成拷贝构造函数,而是进行了按位拷贝。因此config2拷贝了config1内addr成员携带的指针值而非指针引用对象,引起重复释放。
int main(int argc, char *argv[])
{
ServerConfig config1;
ServerConfig config2 = config1;
return 0;
}
(gdb) disassemble main
Dump of assembler code for function main(int, char**):
...
0x000000000040065d <+16>: lea -0x30(%rbp),%rax # 获取config1地址
0x0000000000400661 <+20>: mov %rax,%rdi
0x0000000000400664 <+23>: callq 0x4006ea <ServerConfig::ServerConfig()> # 构造config1
0x0000000000400669 <+28>: mov -0x30(%rbp),%rax
0x000000000040066d <+32>: mov %rax,-0x20(%rbp) # 拷贝config1.port 到 config2.port
0x0000000000400671 <+36>: mov -0x28(%rbp),%rax
0x0000000000400675 <+40>: mov %rax,-0x18(%rbp) # 拷贝config1.addr.m_str 到 config2.addr.m_str
0x0000000000400679 <+44>: mov $0x0,%ebx
0x000000000040067e <+49>: lea -0x20(%rbp),%rax
0x0000000000400682 <+53>: mov %rax,%rdi
0x0000000000400685 <+56>: callq 0x400708 <ServerConfig::~ServerConfig()> # 析构config2
0x000000000040068a <+61>: lea -0x30(%rbp),%rax
0x000000000040068e <+65>: mov %rax,%rdi
0x0000000000400691 <+68>: callq 0x400708 <ServerConfig::~ServerConfig()> # 析构config1
我们现在开始回答第二个问题,如何能保证携带指针引用对象的struct正确拷贝或拷贝构造。那就是其含有指针引用之类的成员,都应正确声明拷贝构造函数和赋值操作函数。如本例中CString正确声明如下,这样编译器会正确为ServerConfig合成拷贝构造函数和赋值操作函数。
class CString
{
public:
CString()
{
m_str = strdup("");
}
CString(const char *str)
{
m_str = strdup(str);
}
CString(const CString &cstr)
{
m_str = strdup(cstr.m_str);
}
CString &operator =(const CString &cstr){
delete m_str;
m_str = strdup(cstr.m_str);
return *this;
}
~CString()
{
delete m_str; m_str= NULL;
}
private:
char *m_str;
};
(gdb) disassemble main
Dump of assembler code for function main(int, char**):
...
0x0000000000400677 <+42>: callq 0x4007a6 <ServerConfig::ServerConfig(ServerConfig const&)>
0x000000000040067c <+47>: lea -0x20(%rbp),%rdx
0x0000000000400680 <+51>: lea -0x30(%rbp),%rax
0x0000000000400684 <+55>: mov %rdx,%rsi
0x0000000000400687 <+58>: mov %rax,%rdi
0x000000000040068a <+61>: callq 0x4007e0 <ServerConfig::operator=(ServerConfig const&)>
...
当我们明确不允许拷贝的时候,一定要禁止拷贝构造和赋值操作函数。可以继承如下禁止拷贝基类即可。
class IUncopyable
{
public:
~IUncopyable(){};
private:
IUncopyable(IUncopyable &);
IUncopyable & operator=(const IUncopyable&);
};