重学C++之(十)类和动态内存分配


本章将介绍如何对类使用new和delete以及如何处理由于使用动态内存而引发的一些微妙的问题。

1. 动态内存和类

C++使用new和delete运算符来动态控制内存,但是在类中使用这些运算符将导致许多新的编程问题。在这种情况下,析构函数将必不可少。有时候,还必须重构赋值运算符,以保证程序运行。

1.1 特殊成员函数

C++自动提供了下面这些成员函数:

  • 默认构造函数,如果没有定义构造函数;
  • 默认析构函数,如果没有定义;
  • 复制构造函数,如果没有定义;
  • 赋值运算符,如果没有定义;
  • 地址运算符,如果没有定义。

C++11提供了另外两个特殊成员函数:移动构造函数(move constructor)和移动赋值运算符(move assignment operator),这将在C++primer第18讨论。

(1) 默认构造函数

如果没有提供任何构造函数,C++将创建默认构造函数,但是一旦有构造函数将不再提供默认构造函数。默认构造函数形式如下。

Klunk::Klunk(){
    
     };

则我们可以进行默认构造函数定义对象:

Klunk lunk;//调用默认构造函数

带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。这种情况下,就不能再定义一个不带参数的构造函数,因为会引起歧义。带参数的默认构造函数形式如下:

Klunk(int n = 0){
    
    klunk_ct = n;}

(2)复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中,它用于初始化过程中,而不是常规的赋值过程中。它的函数原型通常如下:

Class_name(const Class_name &);

需要知道何时调用和有何功能。

(3)何时调用复制构造函数

新建一个对象并将其初始化为同类现有对象时,复制构造函数将被调用。假设motto是一个StringBad对象,下面4种情况都将调用复制构造函数:

扫描二维码关注公众号,回复: 12215585 查看本文章
StringBad ditto(motto);//calls StringBad( const StringBad &)
StringBad metoo = motto;//calls StringBad( const StringBad &)
StringBad also = StringBad(motto);//calls StringBad( const StringBad &)
StringBad * pStringBad = new StringBad(motto);//calls StringBad( const StringBad &)

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

每当程序生成一个对象副本时,编译器都将使用复制构造函数。比如,当函数按值传递对象时,或返回对象时,都将使用复制构造函数。又比如,3个Vector对象相加时,编译器可能(因编译器而异)生成临时的Vector对象来保存中间结果。

由于按值传递对象都将调用复制构造函数,因此应该按引用传递对象。节省时间和存储新对象的空间。

(4)默认的复制构造函数的功能

默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值

如果是一般的类型复制值不值一提,但是如果类中定义了类似于char* str;之类的指针时,它的值就是地址!尤其是定义了两个对象,那么程序结束的时候将调用两次析构函数,如果在析构函数对指针进行删除时,将删除两次指针,可能导致程序异常终止。

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

解决类设计中的这种问题的方法就是进行深度复制(deep copy)。也就是说,每个对象都有自己的字符串,而不是引用另一个对象的字符串。可以这样编写StringBad的复制构造函数。

StringBad::Stringbad( const StringBad& st)
{
    
    
	len = st.len;
	str = new char [len + 1];//新地址
	std::strcpy(str, st.str);//将对象的字符串复制给新对象,而不是指针复制!!!
}

必须定义复制构造函数的原因在于,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。

如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。复制的另一种形式(成员复制或浅复制)只是复制指针值。浅复制仅浅浅地复制指针信息。

(6)赋值运算符

C++允许类对象赋值,这是通过自动为类重载复制运算符实现的。它的原型为:

Class_name& operator= (const Class_name &);

将一个已有的对象赋给另一个对象时,将使用重载的赋值运算符。

StringBad headlin1("Celery Stalks");
StringBad kont;
knot = headline1;//调用赋值运算符

初始化对象时,并不一定会使用赋值运算符。它调用复制构造函数:

StringBad metoo = knot;

这种=赋值也可能这么执行:首先使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值赋值到新对象中。也就是说,初始化总是会调用复制构造函数,而使用=运算符时也可能调用赋值运算符。

与复制构造函数相似,赋值运算符的隐式实现是一种成员复制操作,但静态数据成员不受影响。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员。

赋值运算符和复制构造函数的问题一样,属于浅复制,所以为了解决指针的问题,需要提供赋值运算符的定义(进行深度复制)。但是也有一些差别:

  • 由于目标对象可能引用以前分配的数据,所以函数应使用delete[]来释放这些数据。
  • 函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
  • 函数返回一个指向调用对象的引用。

为StringBad类编写赋值运算符:

StringBad& StringBad::operator = (cosnt StringBad& st)
{
    
    
	if(this == &st)
		return *this;//避免赋值给自己
	delete [] str;//删除以前的值
	len = st.len;
	str = new char [len + 1];
	std::strcpy(str, st.str);//复制字符串
	return *this;
}

2. 改进后的新String类

String类应该包含标准字符串函数cstring的所有功能,我们这里只介绍String部分工作原理(String类只是一个用作说明的示例,而C++标准string类的内容丰富的多):

int length() const{
    
    return len;}//和size()一样
friend bool operator<(const String &st1, const String &st2);
friend bool operator>(const String &st1, const String &st2);
friend bool operator==(const String &st1, const String &st2);
friend operator>>(istream& is, String &st);
char& operator[] (int i);
const char& operator[] (int i) const;
static int HowMany();

具体实现方法这里就不详细叙述,如需了解可查看书本章节12.2

3. 在构造函数中使用new时应注意的事项

我们知道在使用new初始化对象的指针成员时必须特别小心。

  • 如果在构造函数使用new来初始化指针成员,则应在析构函数中使用delete。
  • new和delete必须相互兼容。new对应delete,new[]对应于delete[]。
  • 如果有多个构造函数,则必须以相同的方式使用new,要么都带括号,要么都不带。因为只有一个析构函数。
  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。
  • 应当定义一个赋值运算符,通过深度赋值将一个对象赋值给另一个对象。

NULL、0、还是nullptr:

  • 以前,空指针可以用0或NULL(在很多头文件中,NULL是一个被定义为0的符号常量)来表示。
  • C程序员通常使用NULL而不是0,以指出这是一个指针,就像使用’\0’而不是0来表示空字符,以指出这是一个字符一样。
  • C++传统上更喜欢用简单的0,而不是等价的NULL。
  • C++11提供了关键字nullptr。

如果类成员是string类型,则进行复制或赋值的时候,类似于普通变量复制或赋值,因为它使用的是成员类型定义的复制构造函数和赋值运算符。

4. 有关返回对象的说明

当成员函数或常规函数返回对象时,有几种返回方式可供选择:返回指向对象的引用、指向对象的const引用或const对象。到目前为止介绍了前两种,第三种还没有介绍。

4.1 返回指向const对象的引用

一般使用const引用的常见原因是旨在提高效率。比如

//第一种情况
Vector Max(const Vector & v1, const Vector& v2)
{
    
    
	if(v1.magval() > v2.magval() )
		return v1;
	else
		return v2;
}
//第二种情况
const Vector&  Max(const Vector & v1, const Vector& v2)
{
    
    
	if(v1.magval() > v2.magval() )
		return v1;
	else
		return v2;
}

上面两种方式都可以,但是第二种返回引用的方式效率更高。第一种返回对象的方式将调用复制构造函数,而返回引用不会。

但是注意:

  • 第一:返回指向的对象应该在调用函数执行时存在。
  • 第二:v1和v2都被声明为const引用,因此返回类型必须为const,这样才匹配。

4.2 返回指向非const对象的引用

两种常见返回非const对象的情况:

  • 第一:重载赋值运算符。为了可以连续赋值。
  • 第二:与cout一起使用的<<运算符。为了可以连续输出。

两者都使用引用的方式是为了避免调用复制构造函数来创建新的String对象。

4.3 返回对象

如果返回的对象是被调用函数中的局部变量,则不应该按引用的方式返回它,因为在被调用函数执行完毕时,局部对象将调用析构函数。因此,当控制权回到调用函数时,引用指向的对象将不再存在。

被重载的算数运算符输出这种情况,比如两个对象相加,需要创建临时对象。这种开销是不可避免的。

4.4 返回const对象

上述算数运算符,比如加法,会产生临时对象,所以可以多项加操作(obj1 + obj2 +obj3)。这种情况,下列表达式将变得合法:

obj1 + obj2 = obj3;//加法产生临时对象

如果你担心这种情况会发生,有一种简单的方案,就是将返回类型声明为const。比如const Vector::operator。

5. 使用指向对象的指针

5.1 再谈new与delete

在下述情况下析构函数将被调用

  • 如果对象是动态变量(如临时变量),则当执行完定义该对象的程序块时,将调用该对象的析构函数。
  • 如果对象是静态变量,则在程序结束时,将调用对象的析构函数。
  • 如果对象是用new创建的,则仅当您显式使用delete删除对象时,其析构函数才会被调用。

5.2 指针和对象小结

使用对象的指针时,需要注意几点:

  • 使用常规表示法来声明指向对象的指针:
    string *glamour;
  • 可以将指针初始化为指向已有的对象:
    string *first = &sayings[0];
  • 可以使用new来初始化指针,这将创建新的对象:
    string *favorite = new string(sayings[choice]);
  • 对类使用new将调用相应的类构造函数来初始化新创建的对象:
    string *gleep = new string;
    string *glop = new string(“hello hello”);
    string *favorite = new string(sayings[choice]);
  • 可以使用->运算符通过指针访问类方法:
    favorite -> length();
  • 可以对对象指针应用解除引用运算符( * )来获取对象:
    if(sayings[i] < *first)//比较两个字符串
    first = &saying{i};

6. 模拟队列

队列是(FIFO,first-in,first-out)的,我们创建一个Queue类(标准类为queue)。我们在这里编写一个ATM自动取款模拟队列程序,主要任务是:设计一个表示顾客的类;编写一个程序来模拟顾客和队列之间的交互。

如果构造函数被设为私有,那么下列方式是不允许的(另外在后续,在课本18章介绍了另一种方法,添加关键字delete):

Queue snick(nip);//不允许
tuck = nip;//不允许

第一,以上的方式避免自动生成的默认方法定义。第二,因为这些方法是私有的,所以不能被广泛使用。


总览目录
上一篇:(九)使用类
下一篇:(十一)类继承


文章参考:《C++ Primer Plus第六版》

猜你喜欢

转载自blog.csdn.net/QLeelq/article/details/112599895