类和动态内存

1. 动态内存和类
本章先从一个错误的字符串类设计来揭示在C++类设计中可能存在的问题,特别是在使用动态内存的情况。错误代码示例(使用 VS 2019):
// stringbad.h -- flawed string class definition
#pragma once
#include <iostream>
class StringBad
{
private:
char* str;
int len;
//////////类声明中不能初始化静态成员变量
static int num_strings;
public:
StringBad(const char* s);
StringBad();
~StringBad();
// friend function
friend std::ostream& operator<<(std::ostream& os, const StringBad& st);
};
// stringbad.cpp -- StringBad class methods
#include <cstring> // string.h for some
#include "stringbad.h"
using std::cout;
//////////// 初始化静态类成员(注意这里使用了作用域操作符)
int StringBad::num_strings = 0;
// construct StringBad from c string
StringBad::StringBad(const char* s)
{
len = std::strlen(s);
str = new char[len + 1];
//std::strcpy(str, s);
strcpy_s(str, len + 1, s);
num_strings++;
cout << num_strings << ": \"" << str << "\" object created\n";
}
StringBad::StringBad()
{
len = 4;
str = new char[4];
//std::strcpy(str, "C++");
strcpy_s(str, len, "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;
}
// vegnews.cpp -- using new and delete with classes
#include <iostream>
#include "stringbad.h"
using std::cout;
void callme1(StringBad&); // pass by reference
void callme2(StringBad); // pass by value
int main()
{
using std::endl;
StringBad headline1("Celery Stalks at Midnight");
StringBad headline2("Letture Prey");
StringBad sports("Spinach Leaves Bowl for Dollars");
cout << "headline1: " << headline1 << endl;
cout << "headline2: " << headline2 << endl;
cout << "sports: " << sports << endl;
callme1(headline1);
cout << "headline1: " << headline1 << endl;
/////////// 调用了隐式的复制构造函数!!!
callme2(headline2);
cout << "headline2: " << headline2 << endl;
cout << "Initialize one object to another: \n";
/////////// 调用了隐式的复制构造函数!!!
StringBad sailor = sports;
cout << "Sailor: " << sailor << endl;
cout << "Assign one object to another: \n";
StringBad knot;
/////////// 调用隐式的重载赋值(=)操作符!!!
knot = headline1;
cout << "Knot: " << knot << endl;
cout << "End of main()\n";
return 0;
}
void callme1(StringBad& rsb)
{
cout << "String passed by reference: \n";
cout << " \"" << rsb << "\"\n";
}
//////// 按值传递:调用隐式复制构造函数,生成类的一个临时对象。函数结束后调用参数sb的析构函数!!!
void callme2(StringBad sb)
{
cout << "String passed by value: \n";
cout << " \"" << sb << "\"\n";
}
-
上述程序输出错误的主要原因是程序调用了默认的复制构造函数和重载的赋值操作符,而默认的复制构造函数和赋值操作符只复制了
str
指针的地址,而没有复制其对应的值,导致该值可能被delete
了多次。 -
静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态类变量副本。也就是说,类的所有对象共享同一个静态成员。
-
静态数据成员在类声明中声明,在类实现文件中初始化,初始化时需使用作用域操作符。如果静态成员是整型或枚举型
const
,则可以在类声明中初始化。 -
记住:在构造函数中使用
new
来分配内存时,必须在相应的析构函数中使用delete
来释放内存。 -
C++自动提供的隐式成员函数
- 没有定义构造函数,自动提供默认构造函数
- 如没定义自动提供复制构造函数
- 如没定义自动提供赋值操作符
- 如没定义自动提供默认析构函数
- 如没定义自动提供地址操作符
-
如果希望在创建对象时显式对它初始化,或需要创建对象数组时,必须显式定义默认构造函数。
-
带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。
-
复制构造函数用于将一个对象复制到新创建的对象中。原型通常如下:
Class_name(const Class_name&);
- 何时调用复制构造函数:每当程序生成对象副本时,编译器都将使用复制构造函数。
- 函数按值传递
- 生成临时对象
- 函数返回对象
- 复制构造函数的功能:逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。
-
警告:如果类中包含使用
new
初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。 - C++允许类对象赋值,这是通过自动为类重载赋值操作符实现的,原型如下:
Class_name & Class_name::operator=(const Class_name& );
- 实现赋值操作符应注意以下三点:
- 目标对象可能引用以前分配的数据,所以应使用
delete
或delete[]
来释放数据
- 目标对象可能引用以前分配的数据,所以应使用
- 应该避免将对象赋给自身,否则,给对象重新赋值前,释放内存操作可能删除对象内容
- 函数返回一个指向对象的引用
- 在重载时,C++将区分常量和非常量函数的特征标。
- 静态成员函数(声明中添加
static
关键字,独立的定义则不包含)- 不能通过对象调用静态成员函数。实际上,静态成员函数设置不能使用
this
指针。 - 静态成员函数只能使用静态数据成员。
- 不能通过对象调用静态成员函数。实际上,静态成员函数设置不能使用
- 函数返回对象的几种方式
- 指向对象的引用
- 指向对象的
const
引用 -
const
对象
2. 队列模拟
- 成员初始化列表(member initializer list) 由逗号分隔的初始化列表组成(前面带冒号),位于参数列表的右括号之后、函数体左括号之前。成员初始化列表只能用于构造函数。
Queue::Queue(int qa) : qsize(qs)
{
// ...
}
- 对于
const
类成员和被声明为引用的类成员必须使用成员初始化列表进行初始化。 - 数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关。
- 将复制构造函数和赋值操作符定义为私有可限制对象不被赋值:
class Queue
{
private:
const int qszie;
// 阻止公开复制
Queue(const Queue& q) : qsize(0) { }
Queue& operator=(const Queue& q) { return *this; }
...
}
//-----------
Queue snick(nip); // 不允许
ruck = nip; // 不允许
- 书中设计的队列
// 顾客类声明
class Customer
{
private:
long arrive; // 顾客到达时间
int proceetime; // 处理时间
public:
Customer() { arrive = proceetime = 0; }
void set(long when);
long when() const { return arrive; }
int ptime() const { return proceetime; }
};
// 顾客类实现
#include <cstdlib>
#include "Customer.h"
void Customer::set(long when)
{
proceetime = std::rand() % 3 + 1;
arrive = when;
}
// 队列类声明
#include "Customer.h"
typedef Customer Item;
class Queue
{
private:
// class scope definitions
// 嵌套结构:用于构造内部链表
struct Node { Item item; struct Node* next; };
enum { Q_SIZE = 10 };
// private class member
Node* front; // pointer to front Queue
Node* rear; // pointer to rear of Queue
int items; // current number of items in Queue
const int qsize; // maximum number of items in Queue
// 私有的复制构造函数和赋值操作符重载
Queue(const Queue& q) : qsize(0) { }
Queue& operator=(const Queue& q) { return *this; }
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
};
// 队列类实现
#include <cstddef>
#include "Queue.h"
// 使用成员初始化列表初始化常量成员qsize
Queue::Queue(int qs) : qsize(qs) // initialize qsize to qs
{
front = rear = NULL;
items = 0;
}
Queue::~Queue()
{
Node* temp;
// 因为入列时使用动态内存,因此析构需delete所有节点
while (front != NULL)
{
temp = front; // save address of front item
front = front->next; // reset pointer to next item
delete temp; // delete former front
}
}
bool Queue::isempty() const
{
return items == 0;
}
bool Queue::isfull() const
{
return items == qsize;
}
int Queue::queuecount() const
{
return items;
}
// add item to queue
bool Queue::enqueue(const Item& item)
{
if (isfull())
return false;
// 使用动态内存创建Node结构
Node* add = new Node;
if (add == NULL)
return false;
add->item = item;
add->next = NULL;
items++;
if (front == NULL)
front = add;
else
rear->next = add;
rear = add;
return true;
}
// place front item into item variable and remove from queue
bool Queue::dequeue(Item& item)
{
if (front == NULL)
return false;
item = front->item; // set item to first item in queue
items--;
Node* temp = front; // save location of first item
front = front->next; // reset front to next item
// 因为入列时使用动态内存,因此出列需要使用delete
delete temp;
if (items == 0)
rear = NULL;
return true;
}
网友评论