深入理解c++之struct构造函数

        是否曾好奇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&);
};

 

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页