coding之痛:C++中编译器为类生成的几个默认的函数

        当你定义一个空类的时候,c++编译器会默认为这个空类定义几个函数:1. 空的构造函数 2. 拷贝构造函数 3. 析构函数 4. 赋值操作符函数(operator=)。

        在这里,我想讨论两个问题,第一问题是这几个默认定义的函数里面有无虚拟函数,第二个问题是拷贝构造函数和赋值操作符函数的缺陷所在。

1. 默认定义的函数里面有无虚拟函数?

        答案是没有的。

        先补充一个知识点,定义了虚函数的类在分配内存时要多出4个字节的空间来存储指向虚函数表头的地址,可参见VC++,掀起你的盖头来

        构造函数不存在虚拟这一说法。析构函数在我们平时用的时候一般都会将其定义为虚拟的,原因就是保证在将创建的子类对象空间赋给父类指针时,在使用delete方式释放该父类指针指向的空间时,能够先调用子类的析构函数来释放资源;如果我们把析构函数定义为非虚拟的,那么在上面这种情况下,只会去调用父类的析构函数。那么基于这种原因,按理来说,c++编译器应该在默认定义析构函数时将其定义为虚拟的。而c++编译器并没有这么做,原因可能就是它无法预知开发人员在定义类时会赋予类什么样的行为,而且定义虚方法会为类空间多增加4个字节的内存PS: 这只是我的猜测,具体原因不详。)。

        将赋值操作符函数定义为虚拟的是没有意义的!为什么这么说,下面我举个例子:

#include <iostream>
using namespace std;

class Parent
{
public:
	Parent(const int a) :m_a(a){}
	void			setA(const int a) { m_a = a; }
	int				a() const { return m_a; }
	virtual Parent& operator= (const Parent& other)
	{
		m_a = other.m_a;
		return *this;
	}

protected:
	int m_a;
};

class Son : public Parent
{
public:
	Son(const int a = 0) :Parent(a) {}
	virtual Son& operator= (const Son& other)
	{
		m_a = other.m_a+1;
		return *this;
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	Parent *s1 = new Son(5);
	Parent *s2 = new Son(10);
	cout<<"s1::m_a = "<<s1->a()<<endl;
	*s1 = *s2;
	cout<<"s1::m_a = "<<s1->a()<<endl;

	return 0;
}

在这个示例代码里面,我为了区分是调用Parent::operator=方法还是调用Son::operator=方法,我在Son::operator=的赋值方法中赋m_a+1。我将这两个operator=方法定义为虚拟的一个目的是让语句

*s1 = *s2;
执行的是Son::operator=方法,如果执行的是此方法,那么第二个打印语句打印的数据应该为11,如果执行的方法是Parent::operator=方法,那么打印的数据应该为10。看下VS运行的结果:


这个结果表明调用的是Parent::operator=方法,其实这个语句的另一个写法就是:

s1->operator= (*s2);
这是为什么呢?C++类的转换有两种,一种是向上转换,即将子类型转换为父类型,这种转换可以隐式直接转换即可;另一种是向下转换,即将父类型转换为子类型,这种转换需要使用dynamic_cast语法进行转换。如果上面的语句是调用的Son::operator=方法,那么可以继续将这条语句补齐:

s1->operator= ((const Son&) *s2);
这个语句是错误的,父类转自类需要使用dynamic_cast,所以编译器执行的效果是调用的Parent::operator=方法。如果要让编译器调用Son::operator=方法,必须先将这两个指针转换为子类指针,如下代码:

dynamic_cast<Son&>(*s1) = dynamic_cast<Son&>(*s2);

所以将赋值操作符函数定义为虚拟的是没有意义的,最终还是需要转换为子类型来调用子类中重载的赋值操作符函数,将这个结果衍生一下就是,将任何带有以当前子类作为参数的函数定义为虚拟的是没有意义的,就比如operator==,operator+等等。所以上面的代码应改为:

#include <iostream>
using namespace std;

class Parent
{
public:
	Parent(const int a) :m_a(a){}
	virtual	~Parent() {}
	void	setA(const int a) { m_a = a; }
	int		a() const { return m_a; }
	Parent& operator= (const Parent& other)
	{
		m_a = other.m_a;
		return *this;
	}

protected:
	int m_a;
};

class Son : public Parent
{
public:
	Son(const int a = 0) :Parent(a) {}
	virtual	~Son() {}
	Son&	operator= (const Son& other)
	{
		m_a = other.m_a+1;
		return *this;
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	Parent *s1 = new Son(5);
	Parent *s2 = new Son(10);
	cout<<"s1::m_a = "<<s1->a()<<endl;
	dynamic_cast<Son&>(*s1) = dynamic_cast<Son&>(*s2);
	cout<<"s1::m_a = "<<s1->a()<<endl;

	system("pause");
	return 0;
}
注意:dynamic_cast只能转换多态类型。声明或继承了虚函数的类叫做多态类。可参见 Why does dynamic_cast only work if a class has at least 1 virtual method?

        另外关于一个在类的哪一层定义虚方法的问题,就比如析构函数,最好是在基类就定义析构函数为虚函数,函数的虚拟性是存在继承的特征的,这样在子类中即使不显示将析构函数定义为虚拟的,它还是虚拟函数。这个函数虚拟的继承特性在普通的函数重载中也是一样的,在父类中定义了一个普通的虚拟函数,那么在子类中重实现这个函数时,即使不显示将其定义为虚拟的,那它还是虚拟的

2. 拷贝构造函数和赋值操作符函数的缺陷

        在实际的工程编码当中,默认的拷贝构造函数和赋值操作符函数会给你带来很多的麻烦!!

        默认的拷贝构造函数和赋值操作符函数均是采用的位拷贝(也称“浅拷贝”)方式来给类的属性变量进行赋值。拷贝方式分两种,位拷贝(也称“浅拷贝”)和值拷贝(也称“深拷贝”),位拷贝是指将一个对象的内存映像原封不动地赋值给另一个对象,值拷贝则是将一个对象的内存映像所指向的值赋值给另一个对象。编译器默认采用了一种共享的方式来定义拷贝的行为,而实际上编译器也只能做到如此,但是这样一个特性饱受诟病,那为何又要保留这个特性呢?C++语言之父Bjarne Strousrup在他写的《The design and evolution of C++》中写到这样一句话:

        I personally consider it unfortunate that copy operations are defined by default and I prohibit copying of objects of many of my classes. However, C++ inherited its default assignment and copy constructors from C, and they are frequently used.

        首先这个特性是继承自C语言的,其主要的一个目的主要是为了与C语言兼容。这个特性既是C++的一个大的缺陷,也正式C++流形的一个主要原因。

        使用面向对象,大家就一定会使用设计模式,而设计模式可分为两大类:继承的体系结构,聚合/组合的体系结构。在使用第二类设计模式时,相信大家都很能体会到这个特性的缺陷,为了防止发生浅拷贝,我们“必须”去重载拷贝构造函数和赋值操作符函数。而除了重载这两个默认函数外,另外一个途径就是禁用这两个函数,将这两个函数定义为私有的访问类型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值