C++中的特殊成员函数

具体地说,C++自动提供了下面这些成员函数:

        默认构造函数,如果没有定义构造函数;

        默认析构函数,如果没有定义;

        复制构造函数,如果没有定义;

        赋值运算符,如果没有定义;

        地址运算符,如果没有定义;

编译器将生成上述最后三个函数的定义——如果程序使用对象的方式要求这样做。

1、默认构造函数

        如果没有提供任何构造函数,c++将创建默认构造函数,默认构造函数使对象类似于一个常规的自动变量,它的值在初始化时还是未知的。

2、复制构造函数

        复制构造函数用于将一个对象复制到心创建的对象中。它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数的原型通常如下,它接受一个指向类对象的常量引用作为参数。

Class_name(const Class_name &);

何时调用赋值构造函数?

        新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。

StringBad ditto(motto);
StringBad metoo = motto;
StringBad also = StringBad(motto);
StringBad * pStringBad = new StringBad(motto);

        其中中间的2种声明可能会使用复制构造函数直接创建metoo和also,也可能使用复制构造函数生成一个临时对象,然后将临时对象的内容赋给metoo和also,这取决于具体的实现。最后一种声明使用motoo初始化一个匿名对象,并将新对象的额地址赋给pStringBad指针。

        每当程序生成了对象副本时,编译器都将使用复制构造函数。具体来说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。由于按值传递对象将调用复制构造函数,因此应该按引用传递对象。这样可以节省调用复制构造函数的时间以及存储新对象的空间。

        默认的复制构造函数逐个复制非静态成员(成员复制也称浅复制),复制的是成员的值。也就是语句StringBad sailor = sports;与下面的代码等效

StringBad sailor;
sailor.str = sports.str;
sailor.len = sports.len;

但这样做会出错。在sailor.str = sports.str;中,str是char类型的指针,所以这里复制的是指向字符串的指针。将sailor初始化为sports后,得到的是两个指向同一个字符串的指针。当析构函数被调用时,就会出现问题。析构函数StringBad释放指向str指针指向的内存。然而当释放sports时,sports.str指向的内存已经被sailor的析构函数释放,因此这将导致不确定的、可能有害的结果。

定义一个显式复制构造函数以解决问题

        解决类设计中这种方法的问题是进行深度复制。复制构造函数应当复制字符串并将副本的地址赋给str成员,而不仅仅是字符串地址。这样每个对象都有自己的字符串,而不是引用另一个对象的字符串。调用析构函数时都将释放不同的字符串,而不会试图去释放已被释放的字符串。因此可以编写StringBad的复制构造函数。

StringBad::StringBad(const StringBad &st)
{
	num_strings++;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);  //copy string to new location
	cout << num_strings << ": \"" << str << "\" object created\n";
}

        警告:如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。成员复制或者说浅复制只是复制指针值,而不会深入挖掘以复制指针引用的结构。

3、赋值运算符

        C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的。这种运算符的原型如下,接受并返回一个指向类对象的引用。

Class_name & Class_name::operator=(const Class_name &);
StringBad hadline1("Celery Stals at Midnight");
...
StringBad knot;
knot = headline1;  //assignment operator invoked
StringBad metoo = knot; //use copy constructor, possibly assignment, too

        初始化对象时,并不一定会使用赋值运算符。这里metoo是一个新创建的对象,被初始化为knot的值,因此使用复制构造函数。但实现时也可能分两步来处理这条语句:使用复制构造函数创建一个临时对象,再通过赋值将临时对象的值复制到新对象中。也就是说初始化总是会调用复制构造函数,而使用=运算符时也允许调用赋值运算符。

        与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制。如果成员本身是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。因为静态成员是单独存在的,不属于对象的一部分。

赋值出现的问题

        如 knot = headline1; 当首先为knot调用析构函数时没有问题,但再为headline1调用时就会出现数据受损的问题,出现乱码。这也是成员复制的问题,即导致knot.str与headline1.str指向相同的地址。

如何解决

        对于由于默认赋值运算符不合适而导致的问题,解决办法是提供赋值运算符(进行深度复制)定义。实现时,应注意:

        (1)由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据。

        (2)函数应当避免将对象赋给自身,否则给对象重新赋值前,释放内存操作可能删除对象的内容

        (3)函数返回一个指向调用对象的引用。

        通过返回一个对象,函数可以像常规赋值操作一样连续进行赋值。

StringBad & StringBad::operator=(const StringBad &st)
{
	if (this == &st)  //object assigned to itself
		return *this;   //all alone
	delete[] str;  //free old string
	len = st.len;
	str = new char[len + 1];  //get space for new string
	std::strcpy(str, st.str);  //copy the string
	return *this;  //return reference to invoking object
}

        如果地址不同,函数将释放str指向的内存,因为稍后将把一个新字符串的地址赋给str,如果不首先使用delete,则上述字符串将保留在内存中。由于程序不再包含指向该字符串的指针,因此这些内存会被浪费掉。


猜你喜欢

转载自blog.csdn.net/wxn704414736/article/details/80236158