《C++ Primer Plus》第12章 类和动态内存分配

第12章 类和动态内存分配

动态内存和类

动态内存分配让程序在运行时决定内存分配,而不是在编译时决定。

C++使用new和delete运算符来动态控制内存。

使用关键字static可以将类成员声明为静态存储类。对于静态类成员,无论创建了多少对象,程序都只创建一个静态类变量副本。这对于所有类对象都具有相同值的类私有数据是非常方便的。

不能在类声明中初始化静态成员变量,声明仅描述了如何分配内存,但并不分配内存。可在类声明之外使用单独的语句来进行初始化,因为静态类成员是单独存储的,而不是对象的组成部分。初始化语句需要使用作用域运算符并指出类型,但并不使用关键字static。

静态成员变量初始化在方法文件中进行,而不是在类声明文件中进行。因为类声明位于多个头文件中,程序可能将头文件包括在多个文件中,从而出现多个初始化语句副本,从而引发错误。

静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出静态成员所属的类。如果静态成员是const整数类型或枚举型,则可以在类声明中初始化。

当对象过期时,删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员指针指向的内存。

在构造函数中使用new来分配内存时,必须在相应的析构函数中使用delete来释放内存。如果使用new[ ]来分配内存,则应使用delete[ ]来释放内存。

C++自动提供如下的成员函数:

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

如果没有提供任何构造函数,C++将创建默认构造函数,这个构造函数不接受任何参数,也不执行任何操作,在创建对象时会调用这个构造函数,且对象的值在初始化时未知。

如果定义了构造函数,C++将不会定义默认构造函数。如果希望在创建对象时不显示的对其进行初始化,则必须显示定义默认构造函数,这种构造函数可以没有任何参数,但可以使用它来设置特定的值,也可以带参数并且所有参数都有默认值。但只能有一个构造函数。

复制构造函数用于将一个对象复制到新创建的对象中,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。

类的复制构造函数原型为Class_name(const Class_name &),它接受一个指向类对象的常量引用作为参数。

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

  • 创建一个新对象并将其初始化为同类现有对象时
  • 当函数按值传递对象或函数返回对象时

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

默认的复制构造函数逐个复制非静态成员(浅复制),复制的是成员的值(若成员值为指针,则复制该指针,而非指针所指向的数据)。如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。

静态成员不受影响,因为它们属于整个类,而不是各个对象。

默认的复制构造函数不说明其行为,因此它不指出创建过程。但是析构函数在任何对象过期时都将被调用,无论对象是如何被创建的。这可能导致释放同一部分内存两次,从而造成程序异常终止。

如果类中包含随新对象创建而发生变化的数据成员时,需要提供一个显示复制构造函数来处理计数问题。

复制构造函数应当不仅仅复制指针成员的值,还要复制指针所指向地址的内容(深度复制),并将复制的指针副本赋给复制构造函数创建的对象,这样每个对象都有自己的字符串,而不是引用另一个对象的字符串。调用析构函数是都将释放不同的字符串,而不会去试图释放已经被释放的字符串。

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

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

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

赋值运算符:

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

改进后的新String类

 delete[ ]与使用new[ ]初始化的指针和空指针都兼容,而与new初始化的指针不兼容。

关键字nullptr用于表示空指针。

比较成员函数可利用标准的strcmp( )函数,返回类型为布尔值,并利用<运算符来表示>运算符。

使用方法operator[ ]( )来重载中括号运算符,可实现通过中括号访问String对象。通常二元运算符位于两个操作数之间,但对于中括号运算符,一个操作数位于第一个中括号的前面,另一个操作数位于两个中括号之间。

需要提供一个仅供const String对象使用的operator[ ]( )版本,以实现对String常量的方括号访问。

可以将成员函数声明为静态的,且函数声明必须包含关键字static,但如果函数定义是独立的,则其中不能包括关键字static。

不能通过对象调用静态成员函数,静态成员函数亦不可使用this指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。

可以使用静态成员函数设置类级(classwide)标记,以控制某些类接口的行为。例如类级标记可以控制显示内容的方法所使用的格式。

重载赋值运算符使之能够直接使用常规字符串,从而避免创建和删除临时对象。

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

如果在构造函数中使用new来初始指针成员,则应在析构函数中使用delete。

new和delete必须相互兼容。new对应于delete,new[ ]对应delete[ ]。

如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数将指针初始化为空,因为delete无论带中括号还是不带中括号都可以用于空指针。

定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。具体的说,复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。另外,还应该更新所有受影响的静态类成员。通常,这种构造函数与下面相似:

String::String(const String & st)
{
    num_strings++; // handle static member update if necessary
    len = st.len; // same length as copied string
    str = new char [len + 1]; // allot space
    std::strcpy(str, st.str); // copy string to new location
}

应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。具体的说,该方法应完成这些操作:

  • 检查自我赋值的情况
  • 释放成员指针以前指向的内存
  • 复制数据而不仅仅是数据的地址
  • 返回一个指向调用对象的引用

通常,该类方法与下面类似:

String & String::operator=(const String & st)
{
    if (this == &st)
        return *this;
    delete [] str;
    len = st.len;
    str = new char [len + 1];
    std::strcpy(str, st.str);
    return *this;
}

包含动态内存分配类型成员的类(即该类的成员中有使用动态内存分配的类型,而非使用动态内存分配为该类的成员开辟存储空间),不需要为其本身编写复制构造函数和赋值运算符。默认的逐成员复制和赋值行为有一定的智能。但如果类中的其他成员需要定义复制构造函数和赋值运算符,则这些函数必须显式调用使用动态内存分配类型成员的复制构造函数和赋值运算符。

有关返回对象的说明

当成员函数或独立的函数返回对象时,有几种返回方式可以选择。可以返回指向对象的引用、指向对象的const引用或const对象。

返回指向const对象的引用旨在提高效率。返回对象将调用复制构造函数,而返回引用不会。

引用指向的对象应该在调用函数执行时存在。

两种常见的返回非const对象情形是,重载赋值运算符以及重载与cout一起使用的<<运算符。前者返回引用可避免调用复制构造函数来创建一个新的对象,但返回的引用不是const,因为重载赋值运算符的返回值可以被修改。而operator<<( )的返回值用于串接输出,因此必须是ostream &而非ostream。如果返回类型为ostream,将要求调用ostream类的复制构造函数,而ostream没有公有的复制构造函数,而返回指向cout(ostream类对象)的引用则不会带来任何问题,因为cout已经在调用函数的作用域内。

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

将返回类型声明为const对象,可以避免函数返回值被错误的赋值。

如果方法或函数要返回局部对象,则应返回对象,而不是对象的引用。在这种情况下,将使用复制构造函数来生成要返回的对象。如果方法或函数要返回一个没有公共复制构造函数的类(如ostream类)的对象,它必须返回一个指向这种对象的引用。最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首选引用,因为其效率更高。

使用指向对象的指针

在构造函数中使用new为对象的某个数据成员创建存储空间,并在析构函数中使用delete来释放这些内存,与使用new为整个对象分配内存,是在两个层次上使用了new与delete。

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

  • 如果对象是动态变量,则当执行完定义该对象的成型块时,将调用该对象的析构函数
  • 如果对象是静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时将调用对象的析构函数。
  • 如果对象是使用new创建的,则仅当显式使用delete删除对象时,其析构函数才会被调用

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

  • 使用常规表示法来声明指向对象的指针
  • 可以将指针初始化为指向已有的对象
  • 可以使用new来初始化指针,这将创建一个新的对象
  • 对类使用new将调用相应的类构造函数来初始化新创建的对象
  • 可以使用->运算符通过指针访问类方法
  • 可以对对象指针应用解除引用运算符(*)来获得对象

复习各种技术

  • 重载<<运算符

要重新定义<<运算符,以便将它和cout一起用来显示对象的内容,需要定义如下的友元运算符函数:

ostream & operator<<(ostream & os, const c_name & obj)
{
    os << ...;
    return os;
}

如果该类提供了能够返回所需内容的公有方法,则可在运算符函数中使用这些方法,这样便不用将它们设置为友元函数了。

  • 转换函数

要将单个值转换为类类型,需要创建原型如下所示的类构造函数:

c_name(type_name value);

要将类转换为其他类型,需要创建原型如下所示的类成员函数:

operator type_name()

虽然该函数没有声明返回类型,但应返回所需类型的值。

使用转换函数时要小心。可以在声明构造函数时使用关键字explicit,以防止它被用于隐式转换。

其构造函数使用new的类

如果类使用new运算符来分配类成员指向的内存,在设计时应采取一些预防措施:

  • 对于指向的内存是由new分配的所有类成员,都应在类的析构函数中对其使用delete,该运算符将释放分配的内存
  • 如果析构函数通过对指针类成员使用delete来释放内存,则每个构造函数都应当使用new来初始化指针,或将它设置为空指针
  • 构造函数中要么使用new[],要么使用new,而不能混用。如果构造函数使用的是new[],则析构函数应使用delete[];如果构造函数使用的是new,则析构函数应使用delete
  • 应定义一个分配内存(而不是将指针指向已有内存)的复制构造函数,这样程序能够将类对象初始化为另一个类对象
  • 应定义一个重载赋值运算符的类成员函数

队列模拟

在类声明中声明的结构、类或枚举被称为是嵌套在类中,其作用域为整个类。这种声明不会创建数据对象,而只是指定了可以在类中使用的类型。如果声明式在类的私有部分进行的,则只能在这个类使用被声明的类型;如果声明是在公有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型。

成员初始化列表由逗号分隔的初始化列表组成(前面带冒号),它位于参数列表的右括号之后,函数体左括号之前。如果Classy是一个类,而mem1、mem2、和mem3都是这个类的数据成员,则类构造函数可以使用如下的语法来初始化数据成员:

Classy::Classy(int n, int m) : mem1(n), mem2(0), mem3(n * m + 2)
{
    ...
}

上述代码将mem1初始化为n,将mem2初始化为0,将mem3初始化为n*m+2。从概念上说,这些初始化是在对象创建时完成的,此时还未执行括号中的任何代码,需要注意以下几点:

  • 这种格式只能用于构造函数
  • 必须用这种格式来初始化非静态const数据成员
  • 必须用这种格式来初始化引用数据成员(引用与const数据成员类似,只能在被创建时进行初始化)
  • 数据成员初始化的顺序与他们出现在类声明中的顺序相同,与初始化器中的排列顺序无关

成员初始化列表使用的括号方式也可用于常规初始化,这使得初始化内置类型就像初始化类对象一样。

 

posted @ 2020-06-05 14:49  溪嘉嘉  阅读(219)  评论(0编辑  收藏  举报