Chapter12 类和动态内存分配

Chapter12 类和动态内存分配

静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态类变量副本。也就是说,类的所有对象共享同一个静态成员。
image-20201025121313087

不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但不分配内存。对于静态类成员,须在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。(初始化应在方法文件中进行,而非类声明文件中,这是因为类声明位于头文件中,程序可能将头文件包括在其他几个文件中。如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误

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

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

当使用一个对象来初始化另一个对象时,编译器将自动生成复制构造函数(创建对象的一个副本)。

12.1 特殊成员函数

C++自动提供了下面这些成员函数:
(1)默认构造函数,如果没有定义构造函数;
(2)默认析构函数,如果没有定义;
(3)复制构造函数,如果没有定义;
(4)赋值运算符,如果没有定义:
(5)地址运算符(返回调用对象的地址,this指针),如果没有定义。

12.1.1 默认构造函数

如果没有提供任何构造函数,C++将创建默认构造函数(不接受任何参数,也不执行任何操作),如果希望在创建对象时不显式地对它进行初始化,则必须显式地定义默认构造函数。带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。

12.1.2 复制构造函数

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

1
Class_name (const Class_name & );

它接受一个指向类对象的常量引用作为参数。

  • 何时调用复制构造函数
    (1)新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。
    (2)每当程序生成了对象副本时,编译器都将使用复制构造函数。具体地说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。(由于按值传递对象将调用复制构造函数,因此应该按引用传递对象,这样可以节省调用构造函数的时间以及存储新对象的空间
  • 默认复制构造函数的功能
    默认的复制构造函数逐个复制非静态成员(成员复制也成为浅复制),复制的时成员的值。image-20201025125243335

如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制

12.1.3 赋值运算符

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

1
Class_name & Class_name::operator=(const Class_name & );

它返回一个指向类对象的引用。

将已有的对象赋给另一个对象时,将使用重载的赋值运算符。(初始化总是会调用复制构造函数,而使用=运算符时也可能调用赋值运算符)

与赋值构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响(存储空间不同)。

对于由于默认赋值运算符不合适而导致的问题,解决办法是提供赋值运算符定义:
(1)由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据;
(2)函数应当避免将对象赋给自身;否则,给对象重新赋值之前,释放内存操作可能删除对象的内容;
(3)函数返回一个指向调用对象的引用。

1
2
3
4
5
6
7
Class_name & Class_name::operator=(const Class_name & obj)
{
if(this == &obj)
return *this;
delete [] obj;
...//deep copy
}

Strcmp()函数,如果依照字母排序,第一个参数位于第二个参数之前,则该函数返回一个负值;如果两个字符串相同,则返回0;如果第一个参数位于第二个参数之后,则返回一个正值。

对于中括号运算符([]),一个操作数位于第一个中括号的前面,另一个操作数位于两个中括号之间。

不能通过对象调用静态成员函数,如果静态成员函数实在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。

由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。

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

  • 如果在构造函数中使用new来初始化指针成员,则应该在析构函数中使用delete。
  • new和delete必须相互兼容。new对应于delete,new[]对应于delete[]。
  • 如果有多个构造函数,则必须以相同的方式使用new,因为只有一个析构函数,所有的构造函数都必须与它兼容。
  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。(复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。还应该更新所有受影响的静态类成员)
  • 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。(检查自我赋值的情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用)。

12.3 有关返回对象的说明

  • 返回指向const对象的引用
    使用const引用的常见原因是旨在提高效率。返回对象将调用复制构造函数,而返回引用则不会。

  • 返回指向非const对象的引用
    两种常见的返回非const对象情形是,重载赋值运算符以及重载插入(<<)运算符。

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

    如果方法或函数要返回局部对象,则应该返回对象,而不是指向对象的引用。

12.4 使用指向对象的指针

通常,如果Class_name是类,value的类型为Type_name,则下面的语句:

1
Class_name * pclass = new Class_name(value);

将调用如下构造函数:

1
Class_name (const Type_name &);

image-20201025132924314

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

12.4.1 指针和对象小结

  • 使用常规表示法声明指向对象的指针:String * glamour;
  • 可以将指针初始化为指向已有的对象:String * first = & sayings[0]
  • 可以使用new来初始化指针,这将创建一个新的对象:String * favorite=new String(sayings[choices]);
  • 对类使用new将调用相应的类构造函数来初始化新建的对象。image-20201025134033444
    image-20201025134054599
  • 可以使用->运算符通过指针访问类方法
  • 可以对对象指针应用接触引用运算符(*)来获得对象。

对于使用定位new运算符创建的对象,应以与创建顺序相反的顺序进行删除。原因在于,晚创建的对象可能依赖于早创建的对象。另外,仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区。

12.5 复习各种技术

12.5.1 重载<<运算符

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

1
2
3
4
5
ostream & operator<<(ostream & os, const c_name & obj)
{
os << ...;
return os;
}

12.5.2 转换函数

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

1
c_name (type_name value);

其中c_name为类名,type_name是要转换的类型的名称。

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

1
operator typename();

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

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

12.5.3 构造函数使用new的类

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

  • 对于指向的内存是由new分配的所有类成员,都应在类的析构函数中对其使用delete,该运算符将释放分配的内存。

  • 如果析构函数通过对指针类成员使用delete来释放内存,则每个构造函数都应当使用new来初始化指针,或将它设置为空指针。

  • 构造函数中要么使用new[],要么使用new,不能混用。

  • 应定义一个分配内存(而不是建该指针指向已有内存)的复制构造函数。这样程序将能够将类对象初始化为另一个类对象,这种构造函数的原型通常如下:className(const className &)

  • 应定义一个重载赋值运算符的类成员函数,其函数定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    c_name & cname::operator=(const c_name & cn)
    {
    if(this == & cn)
    return * this;
    delete [] c_pointer;
    c_pointer = new type_name[size];
    ...
    return * this;
    }

成员初始化列表由都好分隔的初始化列表组成(前面带冒号),它位于参数列表的右括号之后、函数体左括号之前。(对于const类成员和被声明为引用的类成员必须使用这种语法,这是因为引用与const数据类似,只能在被创建时进行初始化)。

1
2
3
4
5
6
7
//如果Classy是一个类,而mem1、mem2、mem3都是这个类的数据成员,则构造函数可使用列表初始化语法来初始化成员
Classy::Classy(int n, int m):mem1(n),mem2(0),mem3(n * m + 2){}
/*
* 这种格式只能用于构造函数
* 必须使用这种格式来初始化非静态const数据成员
* 必须用这种格式来初始化引用数据成员
*/