深入理解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&);
};

 

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
struct 是一种用户自定义的数据类型,用于封装多个不同类型的数据成员。在 C++ 中,struct构造函数用于初始化 struct 的实例化对象。 struct构造函数与类的构造函数相似,用于在创建对象时对数据成员进行初始化。通过在 struct 中声明和定义构造函数,可以方便地初始化对象的数据成员,提高代码的可读性和重用性。 struct构造函数可以有以下特点: 1. 构造函数的函数名与 struct 的名称相同,并且没有返回类型(连 void 也没有)。 2. 构造函数可以有多个重载版本,根据参数的不同进行区分。 3. 构造函数可以有默认参数,使部分参数可以省略。 4. 构造函数可以在构造时执行一些初始化操作,例如给数据成员赋初值、打开文件、分配内存等。 使用 struct 构造函数可以在创建对象时,直接传入参数进行初始化,而无需通过额外的成员函数进行赋值操作。这样可以简化代码,并且保证了对象的初始化过程的一致性。 例如,定义一个 struct Person,其中包含了姓名和年龄两个数据成员。可以通过定义构造函数来初始化这两个成员: struct Person { string name; int age; Person(string n, int a) { name = n; age = a; } }; 通过定义了一个带有两个参数的构造函数,可以在创建 Person 对象时,直接传入参数进行初始化: Person p1("张三", 20); Person p2("李四", 25); 这样就可以通过 struct 构造函数来方便地初始化对象的数据成员。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值