1 动态内存和类

C++在分配内存时是让程序在运行时决定内存分配,而不是在编译时决定。这样,可根据程序的需要,而不是根据一系列严格的存储类型规则来使用内存。C++使用new和delete运算符来动态控制内存。

1.1 复习示例和静态类成员

这个程序使用了一个新的存储类型:静态类成员:

//strngbad.h
#include<iostream>
#ifndef STRNGBAD_H_
#define STRNGBAD_H_
class StringBad
{
private:
	char * str;      //指向字符串的指针
	int len;        //字符串的长度
	static int num_strings;    //对象个数
public:
	StringBad(const char * s);   //构造函数
	StringBad();                 //默认构造函数
	~StringBad();                //析构函数
	//友元函数
	friend std::ostream & operator<<(std::ostream & os,
		const StringBad & st);
};
#endif

首先,它使用char指针(而不是char数组)来表示姓名。这意味着类声明没有为字符串本身分配存储空间,而是在构造函数中使用new来为字符串分配空间。 这避免了在类声明中预先定义字符串的长度。

其次,将num_strings成员声明为静态存储类。 静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态类变量副本。 也就是说,类的所有对象共享同一个静态成员。 假设创建了10个StringBad对象,将有10个str成员和10个len成员,但只有一个共享的num_strings成员。这对于所有类对象都具有相同值的类私有数据是非常方便的。 例如,num_strings成员可以记录所创建的对象数目。


//strngbad.cpp 
#include<cstring>
#include"strngbad.h"
using std::cout;
 
//初始化静态类成员
int StringBad::num_strings = 0;
 
//类方法
//以C字符型构造StringBad
StringBad::StringBad(const char * s)
{
	len = std::strlen(s);        //设置长度
	str = new char[len + 1];     //分配内存
	std::strcpy(str, s);         //初始化指针
	num_strings++;               //设置对象数量
	cout << num_strings << ": \"" << str
		<< "\" object created\n";
}
StringBad::StringBad()            //默认构造函数
{
	len = 4;
	str = new char[4];
	std::strcpy(str, "C++");      //默认字符串
	num_strings++;
	cout << num_strings << ": \"" << str << "\" default object created\n";
}
StringBad::~StringBad()           //必要的析构函数
{
	cout << "\"" << str << "\" object deleted, ";
	--num_strings;               //有要求
	cout << num_strings << " left\n";
	delete[] str;                 //有要求
}
std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
	os << st.str;
	return os;
}

程序中使用了下面语句:

int StringBad::num_strings = 0;

注意: 不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。 对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。

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

析构函数:str成员指向new分配的内存。当StringBad对象过期时,str指针也将过期。但str指向的内存仍被分配,除非使用delete将其释放。删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。因此,必须使用析构函数。在析构函数中使用delete语句可确保对象过期时,由构造函数使用new分配的内存被释放。

当类对象作为函数参数值传递时,析构函数将被调用。

另外,下面的代码:

StringBad sailor= sports;

这使用的是哪个构造函数呢?不是默认构造函数,也不是参数为const char*的构造函数。记住,这种形式的初始化等效于下面的语句:

StringBad sailor = StringBad(sports);    //使用sports的构造函数

因为sports的类型为StringBad,因此相应的构造函数原型应该如下:

StringBad(const StringBad &);

当您使用一个对象来初始化另一个对象时,编译器将自动生成上述构造函数(称为复制构造函数,因为它创建对象的一个副本)。自动生成的构造函数不知道需要更新静态变量num_string,因此会将计数方案搞乱。 实际上,这个例子说明的所有问题都是由编译器自动生成的成员函数引起的。

1.2 特殊成员函数

StringBad类的问题是由特殊成员函数引起的。 这些成员函数是自动定义的,就StringBad而言,这些函数的行为与类设计不符。 具体地说,C++自动提供了这些成员函数:

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

如果没有提供任何构造函数,C++将创建默认构造函数。 例如,加入定义了一个Klunk类,但没有提供任何构造函数,则编译器将提供下述默认构造函数:

Klunk::Klunk() { }               //隐式默认构造函数

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

默认构造函数使Lunk类似于一个常规的自动变量,也就是说,它的值在初始化时是未知的。

如果定义了构造函数,C++将不会定义默认构造函数。 如果希望在创建对象时不显式地对它进行初始化,则必须显式地定义默认构造函数。这种构造函数没有参数,但可以使用它来设置特定地值:

Klunk::Klunk()               //显式默认构造函数
{
klunk_ct=0;
…
}

带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。 例如,Klunk类可以包含下述内联构造函数:

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

但只能有一个默认构造函数。 也就是说,不能这样做:

Klunk()  {klunk_ct = 0;}              //构造函数#1

Klunk(int n =0) {klunk_ct=n;}           //具有二义性的构造函数#2

例如:

Klunk kar(10);                        //明确地与#1匹配

Klunk bus;                           //与两个构造函数均可匹配

第二个声明既与构造函数#1(没有参数)匹配,也与构造函数#2(使用默认参数0)匹配。

  1. 复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中。 类的复制构造函数的原型如下:

Class_name(const Class_name &);

它接受一个指向类对象的常量引用作为参数。 例如,StringBad类的复制构造函数原型如下 :

StringBad(const StringBad &);

对于复制构造函数,需要知道两点:何时调用和有何功能。

  1. 何时调用复制构造函数

新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。 这在很多情况下都可能发生,最常见的情况是将新对象显式地初始化为现有地对象。例如,假设motto是一个StringBad对象,则下面4种声明都将调用复制构造函数:

StringBad ditto(motto);   //调用StringBad(const StringBad &)

StringBad metoo = motto;  //调用StringBad(const StringBad &)

StringBad also = StringBad(motto);      //调用StringBad(const StringBad &)

StringBad * pStringBad = new StringBad(motto);  
////调用StringBad(const StringBad &)

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

  1. 默认的复制构造函数的功能

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

StringBad sailor = sports;

与下面代码等价(由于私有成员是无法访问的,因此这些代码不能通过便于):

StringBad sailor;

sailor.str=sports.str;

sailor.len=sports.len;

如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。 静态函数(如num_strings)不受影响,因为它们属于整个类,而不是各个对象。

1.3 复制构造函数哪里出了问题

当callme2()被调用时,复制构造函数被用来初始化callme2()的形参,还被用来将对象sailor初始化为对象sports。 默认的复制构造函数不说明其行为,因此它不指出创建过程,也不增加计数器num_strings的值。但析构函数更新了计数,并且在任何对象过期时都将被调用,而不管对象是如何被创建的。 这是一个问题,因为这意味着程序无法准确地记录对象计数。 解决方法是提供一个对计数进行更新地显式复制构造函数:

StringBad::StringBad(const StringBad & s)
{
    num_strings++;
    ...
}

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

StringBad::StringBad(const StringBad & st)
{
    num_strings++;         //处理静态成员更新
    len= st.len;          //相同长度
    str = new char[len + 1];    //分配空间
    std::strcpy(str,st.str);   //将字符串复制到新位置
    cout<< num_strings << ":\"" << str
         << "\" object created\n";
}

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

1.4 赋值运算符

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

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

它接受并返回一个指向类对象的引用。 例如,StringBad类的赋值运算符的原型如下:

StringBad & StringBad::operator=(constStringBad &);
  1. 赋值运算符的功能以及何时使用它:
StringBad headline1(“Celery Stalks atMidnight”);
…
StringBad knot;
Knot = headline1;         //赋值运算符被调用

初始化对象时,并不一定会使用赋值运算符:

StringBad metoo=knot;    //可能使用复制构造函数,也可能是赋值运算符

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

  1. 解决赋值的问题

对于由于默认赋值运算符不合适导致的问题,解决办法时提供赋值运算符(进行深度复制)定义。 其实现与复制构造函数相似,但也有一些差别。

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

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

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

StringBad & StringBad::operator=(const StringBad & st)
{
    if (this == &st)               //对象赋值给自身
         return *this;               //结束
    delete[] str;                  //释放老字符串
    len = st.len;
    str = new char[len + 1];        //为新字符串开辟空间
    std::strcpy(str,st.str);       //复制字符串
    return *this;                  
}

如果地址相同,程序将返回*this,然后结束。 如果地址不同,函数将释放str指向的内存,这是因为稍后把一个新字符串的地址赋给str。 如果不首先使用delete运算符,则上述字符串将保留在内存中。赋值操作并不创建新的对象,因此不需要调整静态数据成员num_strings的值。

2 改进后的新String类

2.1 修订后的默认构造函数

String::String()
{
    len = 0;
    str = new char[1];  //这是为了和类析构函数兼容;
    str[0] = ’\0’;
}

delete[ ]与使用new[ ]初始化的指针和空指针都兼容。空指针建议用nullptr表示:

str = nullptr;  //C++11的空指针表示法

以其他方式初始化的指针,使用delete[]时,结果将是不确定的。

2.2 比较成员函数

friend bool operator==(const String &st1, const String &st2);

将比较函数作为友元,有助于将String类对象与常规的C字符串进行比较。

if ("love" == answer)

将被转换为

if (operator==("love", answer));

然后编译器将使用某个构造函数将代码转换为:

if (operator==(String("love"), answer));

2.3 使用中括号表示法访问字符

String opera("The Magic Flute");

对于表达式opera[4],C++将查找名称与特征标与此相同的方法;

String::operator[](int i)

如果找到匹配的原型,编译器将使用下面的函数调用来替代表达式opera[4]:

opera.operator[ ](4)

opera对象调用该方法,数组下标4成为该函数的参数。

有了上述定义:

cout <<opera[4];

被转换为:

cout <<opera.operator[4];

返回值将是opera.str[4]。由此公有方法可以访问私有数据。

将返回类型声明为char &,便可以给特定元素赋值。

String means("might");
means[0] = 'r';

第二条语句被转换为一个重载运算符函数调用:

means.operator = ‘r’;

这里将r赋给方法的返回值,上述代码等同于:

means.str[0] = 'r';

但在重载时,C++将区分常量和非常量函数的特征标,因此可以提供另一个仅供const String对象使用的operator版本。

2.4 静态类成员函数

可以将成员函数声明为静态的,函数声明必须包含关键字static。但如果函数定义是独立的,则其中不能包含关键字static,这样做有两个重要的后果。

首先,不能通过对象调用静态成员函数 ,实际上,静态成员函数甚至不能使用this指针。

如果静态成员函数是在公有部分声明的,则可以使用类名和作用域运算符来调用它。

例如:可以给String类添加一个名为HowMany()的静态成员函数,方法是在类声明中添加如下原型定义:

static int HowMany() {return num_strings;}

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

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

2.5 进一步重载赋值运算符

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

如果在构造函数中使用new来初始化对象的指针成员时必须特别小心。

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

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

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

  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。通常,这种构造函数与下面类似。

String::String(const String & st)
{
	num_string++;
	len = st.len;
	str = new char [len+1];
	std::strcpy(str, st.str);
}
  • 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象,
StringBad & StringBad::operator=(const StringBad & st)
{
    if(this == &st)
        return *this
    delete [] str;
    len = st.len;
    str = new char [len+1];
    std::strcpy(str, st.str);
    return *this;
}

3.1 应该和不应该

3.2 包含类成员的类的逐成员复制

class Magazine
{
private:
    String title;
    string publisher;
...
};

String和string都使用动态内存分配,这是否意味着需要为Magazine类编写复制构造函数和赋值运算符?不,至少对这个类本身来说不需要。默认的逐成员复制和赋值行为有一定的智能。如果您将一个Magazine对象复制或赋值给另一个Magazine对象,逐成员复制将使用成员类型定义的赋值构造函数和赋值运算符。也就是说,复制成员title时,将使用String的复制构造函数,而将成员title赋给另一个Magazine对象时,将使用String赋值运算符,以此类推。

然而,如果Magazine类因其他成员需要定义复制构造函数和赋值运算符,情况将更加复杂;在这种情况下,这些函数必须显式地调用String和string的复制构造函数和赋值运算符,这将在第13章中介绍。

3.3 有关返回对象的说明

返回方式:

  • 返回指向对象的引用;
  • 返回指向对象的const引用;
  • 返回const对象;
  1. 返回指向const对象的引用

使用const引用的常见原因是旨在提高效率,但对于何时可以采用这种方式存在一些限制。

假如要编写函数Max(),它返回两个Vector对象中较大的一个,其中Vector是第11章开发的一个类。

Vector force1(50, 60);
Vector force2(10,70);
Vector max;
max = Max(force1, force2);
//version 1
Vector Max(const Vector & v1, const Vector & v2)
{
    if (v1.magval() > v2.magval())
        return v1;
    else
        return v2;
}

//version 2
const Vector & Max(const Vector & v1, const Vector & v2)
{
    if (v1.magval() > v2.magval())
        return v1;
    else
        return v2;
}

返回对象将调用复制构造函数,而返回引用不会。因此第二个版本所做的工作更少,效率更高。引用指向的对象应该在调用函数执行时同时存在。

注意v1和v2被声明为const引用,所以返回类型必须为const,这样才匹配。

  1. 返回指向非const对象的引用

常用于两种情况:

  • 重载赋值运算符

Operator=()的返回值作用域连续赋值;

String s1("Good stuff");
String s2,s3;
s3=s2=s1;

上述代码中,s2.operator=()的返回值被赋给s3。为此,返回String对象或String对象的引用都是可行的。但是通过引用可以避免调用复制构造函数,提高效率

  • 重载与cout连用的«运算符

Operator«()的返回值用于串接输出:

String s1(“Good stuff”);

cout « s1 « “is coming!";

operator<<(cout, s1)的返回值成员一个用于显示字符串"is coming!“的对象。返回类型必须是ostream &,而不能仅仅是ostream。如果使用返回类型ostream,将要求调用ostream类的赋值构造函数,而ostream没有公有的赋值构造函数。幸运的是,返回一个指向cout的引用不会带来任何问题,因为cout已经在调用函数的作用域内。

3.4 返回对象

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

通常,被重载的算术运算符属于这一类。

Vector force1();
Vector force2();
Vector net;
net = force1+force2;

返回的不是force1,也不是force2,force1和force2在这个过程中应该保持不变。因此,返回值不能是指向在调用函数中已存在的对象的引用。相反,在Vector::operator+()中计算得到的两个矢量和被存储在一个新的临时对象中,该函数也不应返回指向该临时对象的引用,而应该返回实际的Vector对象,而不是引用。

在这种情况下,存在调用复制构造函数来创建被返回的对象的开销,然而这时无法避免的。

3.5 返回const对象

前面的Vector::operator+()的定义中有一个奇异的属性

net = force1 +force2; //语句1
force1 +force2 =net; //语句2

这种代码都可行,是因为复制构造函数将创建一个临时对象来表示返回值。因此,在前面的代码中,表达式force1+force2的结果为一个临时对象。在语句1中,该临时对象被赋给一个net;在语句2中,net被赋给该临时对象。

如果您担心这种行为引发滥用。有个简单的解决方案,就是把返回类型声明为const Vector。那么语句2就非法了,因为返回对象无法被赋值,只能赋值给别人。

总之,如果方法或函数要返回局部对象,就必须要返回对象,而不是返回对象的引用。这种情况下将使用赋值构造函数来生成返回的对象。这样的开销是不可避免的,也是必须的。

如果方法或函数要返回一个没有公有复制构造函数的类(例如ostream类)的对象,它必须返回一个指向这种对象的引用。

最后,有些方法和函数可以返回对象,也可以返回指向对象的引用,在这种情况下,应首选引用,因为其效率更高。

4 使用指向对象的指针

4.1 再探new和delete

new为创建的每一个对象的名称字符串分配存储空间,这是在构造函数中进行的;

析构函数使用delete来释放这些内存。

字符串是一个字符数组,所以析构函数使用的是带中括号的delete。使用new来为整个对象分配内存:

String * favorite = new String(saying[choice]);

这不是为要存储的字符串分配内存,而是为对象分配内存;也就是说,为保存字符串地址的str指针和len成员分配内存。

4.2 指针和对象小结

  • 使用常规表示法来声明指向对象的指针:
String * glamour;
  • 可以将指针初始化为指向已有的对象;
String * first = &saying[0];
  • 可以使用new来初始化指针,这将创建一个新的对象
String * favorite = new String(sayings[choice]);
  • 对类使用new将调用相应的类构造函数来初始化新创建的对象:
//调用默认构造函数
String * gleep = new String;

//调用 String(const char *) 构造函数
String * glop = new String("my my my");

//调用 String(const String &)构造函数
String * favorite = new String(saying[choice]);
  • 可以使用->运算符通过指针访问类方法:
if (saying[i].length()< shortest->length())
  • 可以对对象指针应用解除引用运算符(*)来获得对象
if (saying[i]<*first)
    first = &saying[i];

4.3 再谈定位new运算符

这里说的定位new运算符,是一种相对于普通的new运算符,可以指定内存地址的运算符,程序直接使用我们提供的地址,不管它是否已经被使用,而且可以看到新值直接覆盖在旧值上面。

定位new运算符直接使用传递给它的地址,它不负责判断哪些内存单元已被使用,也不查找未使用的内存块。

char * buffer = new char[BUF];

JustTesting * pc1;

pc1 = new (buffer) JustTesting;  //定位new运算符

pc2 = new JustTesting("Heap", 20);

pc3 = new (buffer +sizeof(JustTesting)) JustTesting("Better Idea", 6);

使用定位new运算符来为对象分配内存,必须确保其析构函数被调用。如何确保呢?、对于在堆中创建的对象,可以这样做。

delete pc2;

但是对于pc1却不可以,原因在于delete可以与常规new运算符配合使用,却不能与定位new运算符配合使用。

Pc3没有收到new运算符返回的地址。因此delete pc3将导致运行阶段错误。

Delete pc1将释放buffer,而不是pc1;

系统没有为定位new运算符在该内存块中创建的对象调用析构函数。

对于这种情况,需要显式地为定位new运算符创建的对象调用析构函数。这是少数几个需要显式调用析构函数的情形。

pc3->~JustTesting();

pc1->~JustTesting();//销毁对象的顺序,先销毁后创建的对象。类似于栈的先进后出顺序。

仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区。

5 复习各种技术

6 队列模拟

队列:是一种抽象的数据类型(Abstract Data Type),可以存储有序的项目序列。新项目被添加在队尾,并可以删除队首的项目。队列有些像栈。栈是在同一端进行添加和删除。这使得栈是一种后进先出的结构,队列是先进先出的。

问题:Heather银行要在Food Hea超市门口开设一个ATM机。Food Heap需要了解ATM对超市交通可能造成的影响。Heather银行希望对顾客排队等待的时间进行评估,编写一个程序模拟这种情况。

设计:设计一个队列类,队列中的项目是顾客。设计一个表示顾客的类,编写一个程序来模拟顾客和队列之间的交互。

6.1 队列类

队列的特征:

  • 队列能够存储有序的项目序列;
  • 队列所能容纳的项目数有一定的限制;
  • 应当能够创建空队列;
  • 应当能够检查队列是否为空;
  • 应当能够检查队列是否是满的;
  • 应当能够在队尾添加项目;
  • 应当能够在队首删除项目;
  • 应当能够确定队列中的项目数;

7 总结

本章介绍了定义和使用类的许多重要方面。其中一些方面是非常微妙甚至是很难理解的。

在类构造函数中,可以使用new为数据分配内存。然后将内存地址赋给类成员。这样,类便可以处理长度不同的字符串,而不用在类设计时提前固定数组的长度。

在类构造函数中使用new,也可能在对象过期时引发问题。

如果对象包含成员指针,同时它指向的内存是由new分配的。则释放用于保存对象的内存并不会自动释放对象成员指针所指向的内存。因此在构造函数中使用new来分配内存时,应在类析构函数中使用delete来释放分配的内存。这样,当对象过期时,将自动释放其指针成员指向的内存。

如果对象包含指向new分配的内存的指针成员,则将一个对象初始化为另一个对象,或将一个对象赋给另一个对象时,也会出现问题。(浅复制),在默认情况下,C++逐个对成员初始化和赋值,这意味着被初始化或被赋值的对象的成员将与原始对象完全相同。如果原始对象的成员指向一个数据块,则副本成员将指向同一个数据块。当程序最终删除这两个对象时,类的析构函数将试图删除同一个内存数据块两次,这将出错。

解决方法是:定义一个特殊的复制构造函数来重新定义初始化,并重载赋值运算符。在上述任何一种情况下,新的定义都将创建指向数据的副本,并使新的对象指向这些副本。这样,旧对象和新对象都将引用独立的,相同的数据,而不会重叠。由于同样的原因,必须定义赋值运算符。对于每一种情况,最终目的都是执行深度复制,也就是说,复制实际的数据,而不仅仅是复制指向数据的指针。

对象的存储持续性为自动或外部时,在它不再存在时将自动调用其析构函数。如果使用new运算符为对象分配内存,并将其地址赋给一个指针,则当您将delete用于该指针时将自动为该对象调用析构函数。

然而使用定位new运算符(而不是常规new运算符),为对象分配内存,必须显式地为该对象调用析构函数。方法是使用指向该对象的指针调用析构函数的方法。

C++允许在类中包含结构、类和枚举定义。这些嵌套类型的作用域为整个类,这意味着它们被局限于类中,不会与其他地方定义的同名结构、类和枚举发生冲突。

C++为类构造函数提供了一种可用来初始化数据成员的特殊语法。这种语法包括冒号和有逗号分隔的初始化列表。被放在构造函数参数的右括号后,函数体的左括号前。每个初始化器都有被初始化的成员的名称和包含初始值的括号组成。从概念上讲,这些初始化操作是在对象创建时进行的,此时函数体中的语句还没有被执行。语法如下:

Queue(int qs): qsize(qs), items(0), front(NULL), rear(NULL){ }

如果数据成员是非静态const成员或引用,则必须采用这种格式,但可将C++11新增的类内初始化用于非静态const成员。

C++允许类内初始化,即在类定义中进行初始化:

这与使用成员初始化列表等价。然而,使用成员初始化列表的构造函数将覆盖相应的类内初始化。

Class Queue
{
…
private:
    Node * front = NULL;
    enum {Q_SIZE = 10};
    node * rear = NULL;
    int items = 0;
    const int qsize =Q_SIZE;
…
};