最全面的c++中类的构造函数高级使用方法及禁忌

说明一下,我用的是gcc7.1.0编译器,标准库源代码也是这个版本的。

本篇文章讲解c++中,构造函数的高级用法以及特殊使用情况。

1. 拷贝构造和移动构造区别

对于拷贝构造和移动构造,还是看一下这段代码:

#include <iostream>
#include <string.h>
using namespace std;

class CPtr
{
	private:
		char *m_pData;
		int m_iSize;
	public:
		//without param constructors
		CPtr()
		{
			m_iSize = 1024;
			m_pData = new char[m_iSize];
		}
		~CPtr()
		{
			if ( m_pData != nullptr )
			{
				delete []m_pData;
				m_pData = nullptr;
			}
		}
		//with param constructors
		CPtr(const int p_iSize)
		{
			m_iSize = p_iSize;
			m_pData = new char[p_iSize];
		}
		//copy constructors
		CPtr(const CPtr& ptr)
		{
			if (ptr.m_pData != nullptr)
			{
				m_iSize = strlen(ptr.m_pData)+1;
				m_pData = new char[m_iSize];
				strncpy(m_pData, ptr.m_pData, m_iSize-1);
			}
		}
		//move constructors
		CPtr(CPtr&& ptr)
		{
			m_pData = ptr.m_pData;
			m_iSize = ptr.m_iSize;
			ptr.m_pData = nullptr;
			ptr.m_iSize = 0;
		}
		//赋值构造函数
		CPtr& operator=(const CPtr& ptr)
		{
			if (ptr.m_pData != nullptr)
			{
				m_iSize = strlen(ptr.m_pData)+1;
				m_pData = new char[m_iSize];
				strncpy(m_pData, ptr.m_pData, m_iSize-1);
			}
            return *this;
		}	
		//移动赋值构造函数
		CPtr& operator=(CPtr&& ptr)
		{
			m_pData = ptr.m_pData;
			m_iSize = ptr.m_iSize;
			ptr.m_pData = nullptr;
			ptr.m_iSize = 0;
            return *this;
		}
		void setData(const char* str)
		{
			if (  str == nullptr)
			{
				cout << "str is nullptr" << endl;
				return;
			}
			if ( m_iSize == 0)
			{
				cout << "the memory is nothing" << endl;
				return;
			}
			int iSize = strlen(str);
			if ( iSize < m_iSize )
			{
				strncpy(m_pData, str, iSize);
			}
			else
			{
				strncpy(m_pData, str, m_iSize-1);
			}
		}
		void print(const char* object)
		{
			cout << object << "'s data is " << m_pData << endl;
		}
};

int main()
{
	CPtr p1(1024);
	p1.setData("lilei and hanmeimei");
	p1.print("p1");
	CPtr p2(p1);
	p2.print("p2");
	CPtr p3 = p1;
	p3.print("p3");
	CPtr p4(move(p1));
	p4.print("p4");
	CPtr p5 = move(p2);
	p5.print("p5");
	return 0;
}

根据以上代码,我们可以总结出如下两点:

  • 拷贝构造从拷贝类型上讲,是属于深拷贝,它会重新申请一块新的内存,并把另外一个对象的内容完全复制过来,且不会破坏另外一个对象的内容;
  • 移动构造从拷贝类型上讲,是属于浅拷贝,按照字面意思,它就是把另外一个对象的内容移动到当前对象来,至于之前的对象,我们不确保它还是可用的,移动构造一般用于对象数据需要保存,而对象则需要丢弃的情况;

2. 构造函数是否可以为虚函数

答案是不可以,看如下代码:

#include <iostream>
using namespace std;

class CPtr
{
	private:
		char *m_pData;
		int m_iSize;
	public:
		virtual CPtr()
		{
			m_iSize = 1024;
			m_pData = new char[m_iSize];
		}
		~CPtr()
		{
			if ( m_pData != nullptr )
			{
				delete []m_pData;
				m_pData = nullptr;
			}
		}
};

int main()
{
	return 0;
}

编译后报错:错误:constructors cannot be declared ‘virtual’,可见构造函数是不能声明为virtual的,这与虚函数的机制有关,虚函数是存放在虚表的,而虚表是在构造函数执行完成以后才建立的,构造函数声明为virtual就会陷入到是先有鸡还是先有蛋的尴尬境地,所以编译器做了限制。

3. 构造函数是否可以抛出异常

答案是可以,看如下代码:

#include <iostream>
using namespace std;

class CPtr
{
	private:
		char *m_pData;
		int m_iSize;
	public:
		CPtr()
		{
			cout << "call constructors" << endl;
			m_iSize = 1024;
			m_pData = new char[m_iSize];
			if ( m_iSize > 0)
			{
				throw 1024;
			}
		}
		~CPtr()
		{
			cout << "call Destructor" << endl;
			if ( m_pData != nullptr )
			{
				delete []m_pData;
				m_pData = nullptr;
			}
		}
};

int main()
{
	try
	{
		CPtr p1;
	}
	catch(...)
	{
		cout << "throw something" << endl;
	}
	return 0;
}

编译可以通过,说明构造函数允许抛出异常,但是这里有个隐含的问题,我们执行一下程序,结果如下:

call constructors
throw something

可以看到没有执行析构函数,那如果构造函数在申请动态内存以后抛出异常,就会出现内存泄露的问题,那么为什么没有执行析构函数呢,因为构造函数没有执行完成,相当于对象都还没有建立,何谈执行虚构函数呢,我们应该在构造函数抛出异常前,把所有动态内存先释放掉。

代码改为如下:

#include <iostream>
using namespace std;

class CPtr
{
	private:
		char *m_pData;
		int m_iSize;
	public:
		CPtr()
		{
			cout << "call constructors" << endl;
			m_iSize = 1024;
			m_pData = new char[m_iSize];
			if ( m_iSize > 0)
			{
				delete []m_pData;
				m_pData = nullptr;
				throw 1024;
			}
		}
		~CPtr()
		{
			cout << "call Destructor" << endl;
			if ( m_pData != nullptr )
			{
				delete []m_pData;
				m_pData = nullptr;
			}
		}
};

int main()
{
	try
	{
		CPtr p1;
	}
	catch(...)
	{
		cout << "throw something" << endl;
	}
	return 0;
}

总结:构造函数可以抛出异常,若有动态分配内存,则要在抛异常之前手动释放。

4. c++11增加的=default和=delete用法

还是先看一段代码:

#include <iostream>
using namespace std;

class CPtr
{
	private:
		char *m_pData;
		int m_iSize;
	public:
		CPtr()
		{
			cout << "call constructors" << endl;
			m_iSize = 1024;
			m_pData = new char[m_iSize];
		}
		~CPtr()
		{
			cout << "call Destructor" << endl;
			if ( m_pData != nullptr )
			{
				delete []m_pData;
				m_pData = nullptr;
			}
		}
		CPtr(CPtr &) =delete;
		CPtr(CPtr &&) = default;
};

int main()
{
	CPtr p1;
	CPtr p2(p1);
	CPtr p3(move(p1));
	return 0;
}

编译时报错如下:

test.cpp: 在函数‘int main()’中:
test.cpp:32:12: 错误:使用了被删除的函数‘CPtr::CPtr(CPtr&)’
  CPtr p2(p1);

说明声明为=delete以后不再允许调用,去掉p2的定义,则编译通过,但此时执行的话,还是会报double free的问题,因为p3调用一次析构,p1调用一次析构,就double free啦。

实际上,=delete就相当于以前在private里面声明,即声明为=delete以后则不再允许调用,而声明为=default以后,则告诉编译器,你帮我自动生成一下吧,我懒得去实现它了,但结合上面的问题,在存在动态内存的class里面使用移动构造就要小心了,一不小心就会出现问题哦,具体移动构造怎么实现可以参考上面第一点中的代码。

5. 继承时构造函数执行顺序

代码为先,如下:

#include <iostream>
using namespace std;

class CPtr
{
	private:
		char *m_pData;
		int m_iSize;
	public:
		CPtr()
		{
			cout << "call base constructors" << endl;
			m_iSize = 1024;
			m_pData = new char[m_iSize];
		}
		~CPtr()
		{
			if ( m_pData != nullptr )
			{
				delete []m_pData;
				m_pData = nullptr;
			}
		}
};

class CSon:public CPtr
{
	public:
		CSon()
		{
			cout << "call son constructors" << endl;
		}
};

int main()
{
	CSon son;
	return 0;
}

编译后执行结果如下:

call base constructors
call son constructors

所以对于子类对象而言,是先执行父类构造函数,再执行子类构造函数,那这里再思考一下上面第二点,如果构造函数可以为虚函数,那根据多态规则,父类的构造函数将不会被执行,这也是不成立的。

6. 什么情况下必须使用构造函数初始化列表而不能赋值

有这样一段代码:

#include <iostream>
using namespace std;

class CPtr
{
	private:
		const int m_iSize;
	public:
		CPtr()
		{
			m_iSize = 2;
		}
};

int main()
{
	return 0;
}

我们猜猜看编译这段代码会报错吗,答案是会报错,报错信息如下:

test.cpp: 在构造函数‘CPtr::CPtr()’中:
test.cpp:9:3: 错误:uninitialized const member in ‘const int’ [-fpermissive]
   CPtr()
   ^~~~
test.cpp:7:13: 附注:‘const int CPtr::m_iSize’ should be initialized
   const int m_iSize;
             ^~~~~~~
test.cpp:11:14: 错误:向只读成员‘CPtr::m_iSize’赋值
    m_iSize = 2;
              ^

有两个报错,一个是未初始化常量成员,二个是向只读成员赋值。

实际上,我们这里首先应该思考一下初始化列表和赋值有什么区别,初始化列表其实相当于调用一次构造函数,而赋值呢,是首先调用一次构造函数,然后再调用赋值函数,相当于先声明,然后又定义一次,但我们初次接触c++的时候就应该知道有些类型是必须要声明的时候就有初值的,这里我想到的有以下类型:

  • const声明的变量,必须要有初值;
  • reference引用声明的变量,必须要有初值;
  • 没有默认构造函数但存在有参构造函数的类,它必须初始化的时候给一个入参。

以上三种情况都必须使用初始化列表而不能在构造函数中进行赋值。

7. 什么构造函数会在main函数之前执行

想当年面试的时候我想破头都想不出来这个问题,因为main函数是程序入口嘛,但其实这个问题很简单,根据程序的执行规则,在main函数之前,会先处理全局变量和局部静态变量,那就很清晰了,在main函数执行以前,全局变量和静态变量的构造函数会先执行。

还是用一段代码来佐证:

#include <iostream>
using namespace std;

class CPtr
{
	private:
		int m_iSize;
	public:
		CPtr()
		{
			cout << "call CPtr constructors" << endl;
			m_iSize = 2;
		}
};

CPtr ptr;
int main()
{
	static CPtr ptr1;
	cout << "exec main() " << endl;
	return 0;
}

执行后,输出如下:

call CPtr constructors
call CPtr constructors
exec main()

所以答案是全局变量和静态变量的构造函数会在main函数之前执行。

同理,如果发现程序崩溃,而调试的时候发现还没开始main函数的执行,那么就要检查一下是否有全局变量或者静态变量的构造函数崩溃了。

8. 怎么防止类对象被拷贝和赋值

防止类对象被拷贝和赋值,换句话说,就是不能调用类的拷贝函数和赋值运算符重载函数,我们首先能想到的就是把这两个函数声明为private的,或者私有继承一个基类,而到了c++11,又多了一种办法,就是把构造函数加=delete,这里就不给代码了,具体的可以参考上面第4点。

9. 是否可以在构造函数中调用虚函数

答案是可以,首先看这段代码:

#include <iostream>
using namespace std;

class CPtr
{
	private:
		int m_iSize;
	public:
		CPtr()
		{
			cout << "call CPtr constructors" << endl;
			m_iSize = 2;
			print();
		}
		virtual void print()
		{
			cout << "call virtual function" << endl;
		}
};

int main()
{
	CPtr ptr1;
	return 0;
}

编译执行结果如下:

call CPtr constructors
call virtual function

对于这个类本身而言,其实是否虚函数没有区别,下面看看如果是继承,子类构造函数中调用虚函数会发生什么:

#include <iostream>
using namespace std;

class CPtr
{
	public:
		CPtr()
		{
			cout << "call CPtr constructors" << endl;
		}
		virtual void print()
		{
			cout << "call virtual function" << endl;
		}
};

class CSon:public CPtr
{
	public:
		CSon()
		{
			cout << "call CSon constructors" << endl; 
			print();
		}
		virtual void print()
		{
			cout << "call son virtual function" << endl;
		}
};

int main()
{
	CPtr* son = new CSon;
	delete son;
	return 0;
}

编译执行后结果如下:

call CPtr constructors
call CSon constructors
call son virtual function

再把子类的print函数注释掉,再次执行,结果如下:

call CPtr constructors
call CSon constructors
call virtual function

也就是说,对于子类而言,在构造函数中调用虚函数也是调用的它自身的函数,而当子类没有实现的时候才调用父类的虚函数,这一幕是不是很熟悉,实际上就是发生了多态的效果,通过gdb跟踪CSon的构造函数,输出当前对象的数据,如下:

(gdb) p *this
$2 = (CSon) {<CPtr> = {_vptr.CPtr = 0x400dd0 <vtable for CSon+16>}, <No data fields>}

实际上构造函数执行的同时虚表已经建立了,那虚表既然建立了,必然就会发生多态呀。

综上,不论是基类还是继承类,他们的构造函数中都可以直接调用虚函数。

最全面的c++中类的构造函数高级使用方法及禁忌

上一篇:剑指offer(1) 赋值运算符函数


下一篇:RT-Thread中关于浮点数如何使用rt_kprintf打印输出