第12章 类和动态内存分配
本章介绍如何对类使用new和delete,以及如何处理由于使用动态内存而引起的一些微妙的问题。
- 对类成员使用动态内存分配
- 隐式和显式复制构造函数
- 隐式和显式重载赋值运算符
- 在构造函数中使用new所必须完成的工作
- 使用静态类成员
- 将定位new运算符用于对象
- 使用指向对象的指针
- 实现队列抽象数据类型(ADT)
12.1 动态内存和类
在运行时决定内存分配,更灵活。
12.1.1 复习示例和静态类成员
class StringBad
{
private:
char* str; // pointer to string
int len; // length of string
static int num_strings; // number of objects
public:
StringBad(const char* s); // constructor
StringBad(); // default constructor
~StringBad(); // destructor
friend std::ostream& operator<<(std::ostream& os, const StringBad& st);
};
num_strings是静态类成员,无论创建了多少对象,程序都只创建一个静态类变量副本。用类声明以外单独语句来初始化(在类声明h中声明,在包含类方法的文件cpp中初始化):
int StringBad::num_strings = 0;
除了自定义的这两个构造函数,还有一个隐藏的复制构造函数。用一个对象初始化另一个对象时,编译器会自动生成复制构造函数:
StringBad(const StringBad&); // 复制构造函数
StringBad sailor = sports; // 会去调用复制构造函数
12.1.2 特殊成员函数
C++自动提供了下面这些成员函数:
- 默认构造函数
- 默认析构函数
- 复制构造函数:不是常规赋值过程,而是初始化过程中(包括按值传递)。默认的赋值构造函数功能,是逐个复制非静态成员(
浅复制
)。 - 赋值运算符
- 地址运算符
12.1.3 回到StringBad:复制构造函数的哪里出了问题
如果类中包含一个计数的静态数据成员(值将在新对象被创建时发生变化),则应该提供一个显式复制构造函数来处理计数问题。
StringBad::StringBad(const StringBad& s)
{
num_strings++;
...// important stuff to go here
}
如果按值复制的是指针,而指向的东西delete了,会出错。解决方法是使用深复制
(deep copy),复制构造函数将复制字符串闭并将副本的地址给str成员,而不仅仅复制字符串地址。
StringBad::StringBad(const StringBad& st)
{
num_strings++; // handle static member update
len = st.len; // same length
str = new char[len + 1]; // allot space
std::strcpy(str, st.str); // copy string to new location
}
- 浅复制:仅浅浅的复制指针信息,不会深入“挖掘”以复制指针引用的结构。
- 深复制:如果类中包含了使用new初始化的指针成员,应定义一个复制构造函数,以复制指向的数据。而不是指针。
12.1.4 StringBad的其他问题:赋值运算符
赋值运算符原型Class_name& Class_name::operator=(const Class_name&);
StringBad& StringBad::operator=(const StringBad&);
赋值也会出现浅复制导致使用了被释放的内存的问题,解决方法是提供赋值运算符定义(进行深复制)。
- 由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放。
- 应避免将自身赋给自身。否则会被释放。
- 返回一个指向调用对象的引用。
StringBad& StringBad::operator=(const StringBad& st)
{
if (this == &st) // object assigned to itself,赋值自己给自己
return *this; // all done
delete[] str; // free old string
len = st.len;
str = new char[len + 1]; // get space for new string
std::strcpy(str, st.str); // copy the string
return *this; // return reference to invoking object,返回调用对象的引用
}
12.2 改进后的新String类
将StringBad类改头换面为模仿的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 istream& operator>>(istream& is, String& st);
char& operator[](int i);
const char& operator[](int i) const;
static int HowMany();
12.2.1 修订后的默认构造函数
12.2.2 比较成员函数
12.2.3 使用中括号表示法访问字符
12.2.4 静态类成员函数
12.2.5 进一步重载赋值运算符
12.3 在构造函数中使用new时应注意的事项
- 如果在构造函数中使用new来初始化指针成员,应在析构函数中使用delete
- new和delete必须兼容。new对应delete,new[]对应delete[]
- 多个构造函数,用相同的方式用new,要么都[]要么都不带
- 空指针用nullptr,代替0或者NULL
- 需要定义一个复制构造函数,通过深复制将一个对象初始化为另一个对象
- 需要定义一个赋值运算符,通过深复制将一个对象复制给另一个对象
12.3.1 应该和不应该
12.3.2 包含类成员的类的逐成员复制
12.4 有关返回对象的说明
12.4.1 返回指向const对象的引用
如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高效率。
const Vector& Max(const Vector& v1, const Vector& v2)
{
if (v1.magval() > v2.magval())
return v1;
else
return v2;
}
12.4.2 返回指向非const对象的引用
- 重载赋值运算符,提高效率。operator=()返回值用于连续赋值。
- 重载<<,用于串接输出。operator<<(cout, s1)
12.4.3 返回对象
如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回他,应返回对象。例如重载的算数运算符。
Vector Vector::operator+(const Vector& b) const
{
return Vector(x + b.x, y + b.y);
}
12.4.4 返回const对象
让返回的对象不被修改。
12.5 使用指向对象的指针
12.5.1 再谈new和delete
析构函数的调用时机:
- 对象是动态的,执行完定义该对象的程序块时,调用。
- 对象是静态的,程序结束后调用。
- 对象是new的,显式使用delete时调用。
12.5.2 指针和对象小结
- 使用常规表示法来声明指向对象的指针
- 可以将指针初始化为指向已有的对象
- 可以用new来初始化指针,这将创建一个新的对象
- 对类使用new将调用相应的类构造函数来初始化新创建的对象
- 可以使用->运算符用过指针访问类方法
- 可以对对象指针用解除因引用运算符(*)来获得对象
String* glamour; // 1
String* first = &sayings[0]; // 2
String* favorite = new String(sayings[choice]); // 3
String* gleep = new String; // 4
if (sayings[i].length() < shortest->length()); // 5
if (sayings[i] < *first>); // 6
12.5.3 再谈定位new运算符
定位new运算符,能够在分配内存时指定内存位置。
略,感觉暂时不用。(P371)
12.6 复习各种技术
12.6.1 重载<<运算符
定义友元运算符函数:
ostream& operator<<(ostream& os, const c_name& obj)
{
os << ...; // display object contents
return os;
}
12.6.2 转换函数
要将单个值转换为类类型,创建如下类构造函数:
c_name(type_name value);
要将类转换为其他类型,创建如下成员函数:
operator type_name();
12.6.3 其构造函数使用new的类
同12.3
12.7 队列模拟
队列先进先出
12.6.1 队列类
队列的特征:
- 存储有序的项目序列
- 容纳的项目数有一定的限制
- 能够创建空队列
- 能够检查队列是否为空
- 能够检查队列是否是满的
- 能够在队尾添加项目
- 能够从队首删除项目
- 能够确定队列中的项目数
class Queue
{
enum {Q_SIZE = 10};
private:
// private representation to be developed later
public:
Queue(int qs = Q_SIZE); // create queue with a qs limit
~Queue();
bool isempty() const;
bool isfull() const;
int queuecount() const;
bool enqueue(const Item& item); // add item to end
bool dequeue(Item& item); // remove item from front
}
用链表表示节点:
struct Node
{
Item item; // data stored in the node
struct Node* next; // pointer to next node
}
成员初始化列表:
- 只能用于构造函数
- 必须用这种格式来初始化非静态const数据成员
- 必须用这种格式来初始化引用数据成员
Queue::Queue(int qs) : qsize(qs) // initialize qsize to qs
{
...
}
12.6.2 Customer类
12.6.3 ATM模拟
12.8 总结
- 在类构造函数中使用new来分配内存时,应在类析构函数中delete。
- 复制构造函数。
- 重载赋值运算符。
- 定位new。
- 成员初始化列表。
12.9 复习题
12.10 编程练习
略
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 cdd@ahucd.cn