1. 动态内存和类
C++使用new
和delete
运算符动态控制内存。在类中使用这些运算符将导致许多新的编程问题。这种情况下,析构函数必不可少。
构造函数必须分配足够的内存来存储数据,然后再将数据复制到内存中。析构函数需要包含delete
语句删除成员指针指向的内存。
当使用一个对象初始化另一个对象时,编译器自动生成复制构造函数,这个构造函数不知道需要更新类中自定义的静态变量,因此会将类的设计方案搞乱。
1.1 特殊成员函数
C++自动提供以下成员函数:
- 默认构造函数
- 默认析构函数
- 复制构造函数
- 赋值运算符
- 地址运算符
C++11提供了另外连个特殊成员函数:移动构造函数(move constructor)和移动赋值运算符(move assignment operator)。
1.1.1 默认构造函数
创建对象时总会调用构造函数。如果定义了构造函数,C++将不会定义默认构造函数。如果希望在创建对象时不显式地对它进行初始化,则必须显式地定义默认构造函数。
只要所有参数都有默认值,带参数的构造函数也是默认构造函数。但是一个类只能有一个默认构造函数。
1.1.2 复制构造函数
用于将一个对象复制到新构建的对象中。原型通常为:Class_name(const Class_name &);
它接受一个指向类对象的常量引用作为参数。
新建一个对象并将其初始化为同类现有对象时,复制构造函数将被调用。每当程序生成了对象副本,编译器都将使用复制构造函数;即,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。
默认的复制构造函数逐个复制费静态成员(浅复制),复制的是成员的值。
1.1.3 赋值运算符
C++允许类对象赋值,是通过自动为类重载赋值运算符实现。原型为Class_name & Class_name::operator=(const Class_name &);
它接受并返回一个指向类对象的引用。
赋值运算符的隐式实现对成员进行逐个赋复制(浅复制)。
2. 在构造函数汇总使用new
应注意的事项
- 如果在构造函数中使用
new
初始化指针成员,则应在析构函数中使用delete
。 new
和delete
必须相互兼容。- 如果有多个构造函数,则必须以相同的方式使用
new
(可以使用指针初始化为空,由于delete
可以用于空指针)。因为只有一个析构函数。 - 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。
- 应定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。
以前,空指针可以用0
或NULL
表示。C++传统上更喜欢用0
,但是C++11提供关键字nullptr
。
3. 有关返回对象的说明
3.1 返回指向const
对象的引用
使用const
引用的常见原因是为了提高效率。
如果函数返回传递给它的对象,可以通过返回引用来提高其效率。
首先,返回对象将调用复制构造函数,而返回引用不会。其次,引用指向的对象应该在调用函数执行时存在。第三,如果函数被声明为const
引用,则返回对象的类型必须为const
。
const Vector & Max(const Vector & v1, const Vector & v2){
if (v1.magval() > v2.magval())
return v1;
else
return v2;
}
3.2 返回指向非const
对象的引用
3.3 返回对象
如果被返回的对象是被调用函数中的局部变量,则不应该采用引用的方式返回它。通常,被重载的算术运算符属于这一类。
在这种情况下,存在调用复制构造函数来创建被返回的对象的开销,这是无法避免的。
4. 使用指向对象的指针
// sayings2.cpp -- using pointers to objects
// compile with string1.cpp
#include <iostream>
#include <cstdlib> // for rand(), srand()
#include <ctime> // for time()
#include "string1.h"
const int ArSize = 10;
const int MaxLen = 81;
int main(){
using namespace std;
String name;
cout << "Hi, what's your name?\n>> ";
cin >> name;
cout << name << ", please enter up to " <<ArSize
<< " short sayings <empty line to quit>:\n";
String sayings[ArSize];
char temp[MaxLen]; // temporary string storage
int i;
for(i = 0; i < ArSize; i++){
cout << i + 1 << ": ";
cin.get(temp, MaxLen);
while(cin && cin.get()!='\n')
continue;
if(!cin || temp[0]=='\0') // empty line?
break;
else
sayings[i] = temp;
}
int total = i;
if (total > 0){
cout << "Here are your sayings:\n";
for(i = 0; i < total; i++)
cout << sayings[i] << endl;
// using pointers to keep track of shortest, first strings
String * shortest = &sayings[0];
String * first = &sayings[0];
for(i = 1; i < total; i++){
if (sayings[i].length() < shortest->length())
shortest = &sayings[i]
if (sayings[i] < *first)
first = &sayings[i];
}
cout << "Shortest saying:\n" << *shortest << endl;
cout << "First alphabetically:\n" << *first << endl;
srand(time(0));
int choice = rand() % total;
// use new to create, initialize new String object
String *favorite = new String(syaings[choice]);
cout << "My favorite saying:\n" << *favorite << endl;
delete favorite;
}
else
cout << "Not much to say, eh?\n";
cout << "Bye.\n";
return 0;
}
String * favorite = new String(sayings[choice]);
使用new
为整个对象分配内存,即为保存字符串地址的str
指针和·len
成员分配内存。创建对象将调用构造函数,构造函数分配用于保存字符串的内存,并将字符串的地址赋给str
。
当程序不需要该对象时,使用delete
删除;这只释放用于保存str
指针和len
成员的空间,并不释放str
指向的内存。释放str
指向的内存由析构函数完成。
// placenew1.cpp -- new, placement new, no delete
#include <iostream>
#include <string>
#include <new>
using namespace std;
const int BUF = 512;
class JustTesting{
private:
string words;
int number;
public:
JustTesting(const string & s = "Just Testing", int n = 0)
{words = s; number = n; cout << words << " constructed\n.";}
~JustTesting(){cout << words << " destroyed\n";}
void Show() const { cout << words << ", " << number << endl;}
};
int main(){
char * buffer = new char[BUF]; // get a block of mem
JustTesting *pc1, *pc2;
pc1 = new(buffer) JustTestng; // place object in buffer
pc2 = new JustTesting("Heap1", 20); // place object on heap
cout << "Memory block address:\n" << "buffer: "
<< (void *) buffer << "\theap: " << pc2 << endl;
cout << "Memory contents:\n" << pc1 << ": ";
pc1->Show();
cout << pc2 << ": ";
pc2->Show();
JustTesting *pc3, *pc4;
pc3 = new(buffer) JustTestng("Bad Idea", 6); // place object in buffer
pc4 = new JustTesting("Heap2", 10); // place object on heap
cout << "Memory contents:\n" << pc3 << ": ";
pc3->Show();
cout << pc4 << ": ";
pc4->Show();
delete pc2; // free Heap1
delete pc4; // free Heap2
delete [] buffer; // free buffer
cout << "Done\n";
return 0;
}
发生错误,未知原因
placement1.cpp: error: 'JustTesting' does not name a type
pc1 = new(buffer) JustTestng; // place object in buffer
error: expected type-specifier before ‘JustTestng’
pc3 = new(buffer) JustTestng("Bad Idea", 6); // place object in buffer
以上程序在使用placement new运算符时存在两个问题:
- 创建
*pc2
时,placement new运算符使用一个新对象来覆盖用于第一个对象的内存单元。如果类动态地为其成员分配内存,将引发问题 - 将
delete
用于pc2
和pc4
时,自动为二者指向的对象调用析构函数;然而,将delete[]
用于buffer
时,不会为使用placement new运算符创建的对象调用析构函数。
delete
不能与placement new运算符配合使用。解决方案:显式地为使用placement new运算符创建的对象调用析构函数。
pc1->~JustTesting(); // destroy object pointed to by pc1
5. 复习各种技术
5.1 重载<<
运算符
以便和cout
一起使用以显示对象内容。定义以下友元运算符函数:
ostream & operator<<(ostream & os, const c_name & obj){
os << obj; // display object contents
return os;
}
5.2 转换函数
将单个值转换为类类型,创建原型如下的类构造函数:
c_name(type_name value);
要将类转换为其他类型,创建原型如下的类成员函数:
operator type_name();
该函数虽然没有声明返回类型,但是需要返回所需类型的值。
使用转换函数需要小心。可以在声明构造函数时使用关键字explicit
,以防止转换函数被用于隐式转换。
5.3 构造函数使用new
的类
对于指向的内存是由new
分配的所有类成员,都应该在类的析构函数中对其使用delete
释放分配的内存。
如果析构函数通过对指针类成员使用delete
释放内存,则每个构造函数都应当使用new
来初始化指针,或者将其设置为空指针nullptr
应定义一个分配内存的复制构造函数,将类对象初始化为另一个类对象。
应定义一个重载赋值运算符的类成员函数c_name & c_name::operator=(const c_name & cn);
6. 队列模拟
6.1 队列类Queue
class Queue{
private:
// class scope definitions
struct Node { Item item; struct Node * next;}; // 嵌套结构声明
enum {Q_SIZE = 10};
// private class members
Node * front;
Node * rear;
int items;
const int qsize;
public:
// Queue类的接口
Queue(int qs=Q_SIZE);
~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
};
成员初始化列表(member initializer list)由逗号分隔的初始化列表组成,位于参数列表右括号之后、函数体左括号之前。由它完成常量成员的初始化以及被声明为引用的类成员的初始化。
只有构造函数可以使用这种初始化列表语法。
// public methods
Queue::Queue(int qs) : qsize(qs)
{
front = rear = nullptr;
items = 0;
// qsize = qs; // not acceptable
}
// or
Queue::Queue(int qs) : qsize(qs), front(nullptr), rear(nullptr), items(0){}
C++11允许直接初始化:
class Classy{
int mem1 = 10;
const int mem2 = 20;
}
bool Queue::isempty() const{
return items == 0;
}
bool Queue::isfull() const{
return items == qsize;
}
int Queue::queuecount() const{
return items;
}
bool Queue::enqueue(const Item & item){
if(isfull())
return false;
Node * add = new Node;
// on failure, new throws std::bad_alloc exception
add->item = item;
add->next = nullptr;
items++;
if(front == nullptr) front = add;
else rear->next = add;
rear = add;
return true;
}
bool Queue::dequeue(Item & item){
if(isempty())
return false;
item = front->item;
items--;
Node * temp = front;
front = temp->next;
delete temp;
if(items==0)
rear = nullptr;
return true;
}
Queue::~Queue(){
Node *temp;
while(front != nullptr){
temp = front;
front = front -> next;
delete temp;
}
}
可以如下定义对象不允许被复制的类:
class Queue{
private:
Queue(const Queue q) : qsize(0) {} // preemptive definition
Queue & operator=(const Queue & q) { return *this;}
};
C++11可以使用关键字delete
实现上述功能(第18章)。
6.2 Customer
类
class Customer{
private:
long arrive; // arrival time
int process_time; // processing time
public:
Customer() { arrive = process_time = 0; }
void set(long when);
long when() const { return arrive;}
int ptime() const { return process_time;}
};
void Customer::set(long when){
process_time = std::rand() % 3 + 1;
arrive = when;
}
typedef Customer Item;
6.3 ATM模拟
// bank.cpp -- using the Queue
#include <iostream>
#include <cstdlib>
#include <ctime>
#include "queue.h"
const int MIN_PER_HR = 60;
bool newcustomer(double x); // is there a new customer?
int main(){
using std::cin;
using std::cout;
using std::endl;
using std::ios_base;
// setting things up
std::srand(std::time(0)); // random initializing of rand()
cout << "Case Study: Bank of Heather Automatic Teller\n";
cout << "Enter maximum size of queue: ";
int qs;
cin >> qs;
Queue line(qs);
cout << "Enter the number of simulation hours: ";
int hours;
cin >> hours;
long cyclelimit = MIN_PER_HR * hours;
cout << "Enter the average number of customers per hour: ";
double perhour;
cin >> perhour;
double min_per_cust;
min_per_cust = MIN_PER_HR / perhour;
Item temp; // new customer data
long turnaways = 0; // turned away by full queue
long customers = 0; // joined the queue
long served = 0; // served during the simulation
long sum_line = 0; // cumulative line length
int wait_time = 0; // time until autoteller is free
long line_wait = 0; // cumulative time in line
// running the simulation
for(int cycle = 0; cycle < cyclelimit; cycle++){
if (newcustomer(min_per_cust)){
if(line.isfull()) turnaways++;
else{
customers++;
temp.set(cycle);
line.enqueue(temp);
}
}
if(wait_time <= 0 && !line.isempty()){
line.dequeue(temp);
wait_time = temp.ptime();
line_wait += cycle - temp.when();
served++;
}
if(wait_time > 0) wait_time--;
sum_line += line.queuecount();
}
// reporting results
if (customers > 0){
cout << "customers accepted: " << customers << endl;
cout << " customers served: " << served << endl;
cout << " turnaways: " << turnaways << endl;
cout << "average queue size: ";
cout.precision(2);
cout.setf(ios_base::fixed, ios_base::floatfield);
cout << (double) sum_line /cyclelimit << endl;
cout << " average wait time: " << (double) line_wait/served << "minutes\n";
}
else
cout << "No customers!\n";
cout << "Done!\n";
return 0;
}
bool newcustomer(double x){
return (std::rand() * x / RAND_MAX < 1);
}