第12章 类和动态内存分配
1.动态内存和类
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);
};
(1)静态类成员:static int num_strings;
特点:无论创建了多少对象,程序都只创建一个静态类变量副本,即类的所有对象共享同一个静态成员
注意:静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出讲台成员所属的类。但如果静态成员是const整数类型或枚举型,则可以在类声明中初始化
StringBad::~StringBad()
{
cout << "\"" << str << "\" object deleted,";
--num_strings;
cout << num_strings << " left\n";
delete[] str;
}
(2)在构造函数中使用new来分配内存时,必须在相应的析构函数中使用delete来释放内存。如果使用new[]来分配内存,则应使用delete[]释放内存
(3)特殊成员函数
C++自动提供了下面这些成员函数:
——默认构造函数:如果没有定义构造函数
——默认析构函数:如果没有定义
——复制构造函数:如果没有定义
——赋值运算符:如果没有定义
——地址运算符:如果没有定义
(4)复制构造函数
类的复制构造函数原型如下:Class_name(const Class_name &);
何时调用复制构造函数?(例如,假设motto是一个StringBad对象,则下面4种声明都将调用复制构造函数:)
StringBad ditto(motto);
StringBad metoo=motto;
StringBad also=StringBad(motto);
StringBad *pStringBad=new StringBad(motto);
当函数按值传递对象(创建原始变量的一个副本)或函数返回对象时,都将使用复制构造函数;当编译器生成临时对象时,也将使用复制构造函数。
(5)定义一个显式复制构造函数以解决问题
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";
}
复制构造函数应当复制字符串并将副本的地址赋给str成员,而不仅仅是复制字符串地址
警告:如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。复制的另一种形式(成员复制或浅复制)只是复制指针值。浅复制仅浅浅地复制指针信息,而不会深入“挖掘”以复制指针引用的结构。
(6)赋值运算符
Class_name & Class_name::operator=(const Class_name &);
解决赋值问题:提供赋值运算符(进行深度复制)
a. 由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据
b.函数应该避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容
c.函数返回一个指向调用对象的引用
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;
}
解释:代码首先检查自我复制,这是通过查看赋值运算符右边的地址(&s)是否与接收对象(this)的地址相同来完成的。如果相同,程序将返回*this,然后结束。如果地址不同,函数将释放str指向的内存,这是因为稍后将把一个新字符串的地址赋给str。如果不首先使用delete运算符,则上述字符串将保留在内存中。由于程序中不再包含指向该字符串的指针,因此这些内存被浪费掉。接下来的操作与复制构造函数类似,即为新字符串分配足够的内存空间,然后将赋值运算符右边的对象中的字符串复制到新的内存单元中。上述操作完成后,程序返回*this并结束。
2.改进后的新String类
int length() const { return len; }
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();
(1)静态类成员函数:
a. 不能通过对象调用静态成员函数;实际上,静态成员函数甚至不能使用this指针
方法是在类声明中添加如下原型/定义:
static int HowMany() { return num_strings; }
调用它的方式如下: int count=String::HowMany();
b. 静态成员函数不与特定的对象相关联,因此只能使用静态数据成员
(2)使用中括号表示法访问字符
char & String::operator[](int i)
{
return str[i];
}
const char & String::operator[](int i) const
{
return str[i];
}
3.在构造函数中使用new时应注意的事项
(1)如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete
(2)new和delete必须相互兼容。new对应于delete,new[]对应于delete[]
(3)如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数必须与它兼容。然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或C++11中的nullptr),这是因为delete(无论带与不带中括号)都可以用于空指针
(4)应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象
(5)应定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象
4.有关返回对象的说明
(1)返回指向const对象的引用
如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,通过返回引用来提高其效率
(2)返回指向非const对象的引用(重载赋值运算符以及重载与cout一起使用的<<运算符)
(3)返回对象
如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回它(被重载的算术运算符)
(4)返回const对象
总结:1)如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用,将使用复制构造函数来生成返回的对象;2)如果方法或函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须返回一个指向这种对象的引用;
3)有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首选引用,因为其效率更高
5.使用指向对象的指针
(1)使用new初始化对象
如果Class_name是类,value的类型为Type_name,则下面的语句:
Class_name * pclass=new Class_name(value);
将调用如下构造函数:
Class_name(Type_name);
而下面的初始化方式将调用默认构造函数:
Class_name *ptr=new Class_name;
(2)析构函数被调用情况
1)如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数
2)如果对象是静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时将调用对象的析构函数
3)如果对象是new创建的,则仅当显式使用delete删除对象时,其析构函数才会被调用
(3)指针和对象小结
1)使用常规表示法来声明指向对象的指针:
String * glamour;
2)使用指针初始化为指向已有的对象
String *first=&sayings[0];
3)使用new初始化指针,创建一个新对象
String *favorite=new String(sayings[choice]);
4)对类使用new将调用相应的类构造函数来初始化新创建的对象:
//调用默认构造函数
String * gleep=new String;
//调用String(const char*)构造函数
String *glop=new String("my my my");
//调用String(const String&)构造函数
String *favorite=new String(sayings[choice]);
5)可以使用->运算符通过指针访问类方法:
if(sayings[i].length()<shortest->length())
6)可以对对象指针应用解除引用运算符(*)来获得对象
if(sayings[i]<*first)
first=&sayings[i];
(4)再谈定位new运算符
显式调用析构函数:
pc3->JustTesting();
pc1->JustTesting();
对于定位new运算符创建的对象,应与创建顺序相反进行删除,仅当所有对象被销毁后,才能释放用于存储这些对象的缓冲区
6.队列模拟
(1)队列概念
队列是一种抽象的数据类型(ADT),可以存储有序的项目序列。新项目被添加在队尾,并可以删除队首的项目,队列是先进先出的(FIFO)
(2)链表
链表能够很好地满足队列的要求,链表由节点序列构成,每一个节点中都包含要保存到链表中的信息以及一个指向下一个节点的指针。通常链表中的最后一个节点中的指针被设置为NULL(或0),以指出后面没有节点了,C++11使用关键字nullptr
(3)嵌套结构和类
class Queue
{
private:
struct Node{ Item item; struct Node * next;};
enum{Q_SIZE=10};
//私有类成员
Node *front; //队首
Node *rear; //队尾
int items;
const int qsize;
public:
......
};
Node是这样的一种类型:可以使用它来声明类成员,也可以将它作为类方法中的类型名称,但只能在类中使用,这种声明不会创建数据对象,而只是指定了可以在类中使用的类型
(4)成员初始化列表
Class::Class(int n,int m): mem1(n),mem2(0),mem3(n*m+2)
{
//...
}
从概念上说,这些初始化工作是在对象创建时完成的,此时还未执行括号中的任何代码
1)这种格式只能用于构造函数;
2)必须用这种格式来初始化非静态const数据成员(至少在C++11之前是这样的)
3)必须用这种格式来初始化引用数据成员
4)数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关
(5)入队
bool Queue::enqueue(const Item & item)
{
if(isfull())
return false;
Node *add=new Node;
add->item=item;
add->next=NULL;
items++;
if(front==NULL)
front=add;
else
rear->next=add;
rear=add;
return true;
}
(6)出队
bool Queue::dequeue(Item & item)
{
if(front==NULL)
return false;
item=front->item;
items--;
Node *temp=front;
front=feont->next;
delete temp;
if(items==0)
rear=NULL;
return true;
}