1. 动态内存和类
(1)不能在类声明中初始化静态成员变量。
这是因为声明描述了如何分配内存,但并不分配内存。对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。
注意:静态数据成员在类声明中声明,在包含类方法文件中初始化,初始化时使用作用域运算符来指出静态成员所属的类。但如果静态数据成员是整型或枚举型const,则可以在类声明中初始化。
#pragma once
// stringbad.h
#ifndef STRINGBAD_H_
#define STRINGBAD_H_
#include <iostream>
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 // !STRINGBAD_H_
// stringbad.cpp
#include <iostream>
#include <cstring>
#include "stringbad.h"
using std::cout;
// 初始化静态类成员
int StringBad::num_strings = 0;
// 类方法
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[len];
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;
}
字符串并不保存在对象中,字符串单独保存在堆内存中,对象仅保存了指出到哪里去查找字符串的信息。
删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。因此必须使用析构函数,在其中使用delete语句释放new分配的内存。
下述代码的问题由隐式复制构造函数和隐式赋值运算符引起。
// 由于编译器自动生成的复制构造函数,而造成很多奇怪现象,甚至无法编译
// vegnews.cpp
#include <iostream>
#include "stringbad.h"
using std::cout;
void callme1(StringBad&); // 按引用传递
void callme2(StringBad); // 按值传递
int main() {
using std::endl;
{
cout << "Starting an inner block.\n";
StringBad headline1("Celery Stalks at Midnight");
StringBad headline2("Lettuce 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 = StringBad(sports);
// 其原型为:StringBad(const StringBad&)
// 当使用一个对象来初始化另一个对象时,编译器将自动生成上述构造函数(复制构造函数,因为它创建对象的一个副本)
StringBad sailor = sports;
cout << "sailor: " << sailor << endl;
cout << "Assign one object to another:\n";
StringBad knot;
knot = headline1;
cout << "knot: " << knot << endl;
cout << "Exiting the block.\n";
}
cout << "End of main()\n";
return 0;
}
void callme1(StringBad& rsb) {
cout << "String passed by reference:\n";
cout << " \"" << rsb << "\"\n";
}
void callme2(StringBad sb) {
cout << "String passed by value:\n";
cout << " \"" << sb << "\"\n";
}
(2)特殊成员函数
C++自动提供了下面这些成员函数:
- 默认构造函数,如果没有定义构造函数;
- 默认析构函数,如果没有定义;
- 复制构造函数,如果没有定义;
- 赋值运算符,如果没有定义;
- 地址运算符,如果没有定义。
更准确地说,编译器将生成上述最后三个函数地定义——如果程序使用对象地方式要求这样做。例如,如果程序员将一个对象赋给另一个对象,编译器将提供赋值运算符的定义。
C++11提供另外两个特殊成员函数:
- 移动构造函数
- 移动赋值运算符
1)默认构造函数
默认构造函数使类类似于一个常规的自动变量,也就是说它的值在初始化时是未知的。带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。但只能有一个默认构造函数。
2)复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数原型通常如下:
Class_name(const Class_name&);
何时调用?
新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。
例如:
StringBad ditto(motto);
StringBad metoo = motto;
StringBad also = StringBad(motto);
StringBad* pStringBad = new StringBad(motto); // 使用motto初始化一个匿名对象,并将新对象的地址赋给pstring指针。
注意:每当程序生成了对象副本时,编译器都将使用复制构造函数。当函数按值传递对象或函数返回对象时,都将使用复制构造函数。按值传递意味着创建原始变量的一个副本。编译器生成临时对象时,也将使用复制构造函数。由于按值传递对象将调用复制构造函数,因此应该按引用传递对象,这样可以节省调用构造函数的时间以及存储新对象的空间。
默认的复制构造函数的功能?
默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。静态成员不受影响,因为它们属于整个类。
3)深度复制
复制构造函数应该复制字符串并将副本的地址赋给str成员,而不仅仅是复制字符串地址。这样每个对象都有自己的字符串。
必须定义复制构造函数的原因在于,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。
注意:如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。复制的另一种形式(成员复制或浅复制)只是复制指针值。浅复制仅浅浅地复制指针信息,而不会深入“挖掘”以复制指针引用的结构。
4)赋值运算符
何时使用重载的赋值运算符?
- 将已有的对象赋给另一个对象时
- 初始化对象时,并不一定会使用赋值运算符
与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。
提供赋值运算符(进行深度复制)定义:
- 由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据;
- 函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容;
- 函数返回一个指向调用对象的引用。
赋值运算符是只能由类成员函数重载的运算符之一。
#pragma once
// string1.h -- fixed and augmented string class definition
#ifndef STRING1_H_
#define STRING1_H_
#include <iostream>
using std::ostream;
using std::istream;
class String {
private:
char* str;
int len; // 字符串长度
static int num_strings; // 构造对象的数量
static const int CINLIM = 80; // cin input limit
public:
// 构造函数和其他函数
String(const char* s); // 带一个参数的构造函数
String(); // 默认构造函数
String(const String& st); // 复制构造函数
~String(); // 析构函数
int length() const { return len; } // 内联函数:获取字符串长度
// 重载运算符成员方法
String& operator=(const String&); // 重载赋值运算符
String& operator=(const char*); // 同上
char& operator[](int i);
const char& operator[](int i) const;
// 重载运算符友元函数
// 将比较函数作为友元,有助于将String对象与常规的C字符串进行比较
friend bool operator<(const String& st, const String& st2);
friend bool operator>(const String& st1, const String& st2);
friend bool operator==(const String& st, const String st2);
friend std::ostream& operator<<(std::ostream& os, const String& st);
friend std::istream& operator>>(std::istream& is, String& st);
// 静态函数
static int HowMany();
};
#endif // !STRING1_H_
// string1.cpp
#include <cstring>
#include "string1.h"
using std::cin;
using std::cout;
// 初始化静态成员
int String::num_strings = 0;
// 静态方法
int String::HowMany() {
return num_strings;
}
// 类方法
String::String(const char* s) {
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str, s);
num_strings++;
}
String::String() {
len = 0;
str = new char[1]; //delete[]与使用new[]初始化的指针和空指针都兼容
str[0] = '\0';
num_strings++;
}
String::String(const String& st) {
len = st.len;
str = new char[len + 1];
std::strcpy(str, st.str);
num_strings++;
}
String::~String() {
--num_strings;
delete[] str;
}
// !!重点学习
String& String::operator=(const String& st) {
if (this == &st) return *this;
delete[] str;
len = st.len;
str = new char[len + 1];
std::strcpy(str, st.str);
return *this;
}
String& String::operator=(const char* s) {
delete[] str;
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str, s);
return *this;
}
// 只读形式的数组访问
char& String::operator[](int i) {
return str[i];
}
// 制度形式的数组访问(const String)
const char& String::operator[](int i) const {
return str[i];
}
bool operator<(const String& st, const String& st2) {
return (std::strcmp(st.str, st2.str) < 0);
}
bool operator>(const String& st1, const String& st2) {
return (std::strcmp(st1.str, st2.str) > 0);
}
bool operator==(const String& st, const String st2) {
return (std::strcmp(st.str, st2.str) == 0);
}
std::ostream& operator<<(std::ostream& os, const String& st) {
os << st.str;
return os;
}
std::istream& operator>>(std::istream& is, String& st) {
char temp[String::CINLIM];
is.get(temp, String::CINLIM);
if (is)
st = temp;
while (is && is.get() != '\n')
continue;
return is;
}
// saying1.cpp
#include <iostream>
#include "string1.h"
const int ArSize = 10;
const int MaxLen = 81;
int main() {
using std::cout;
using std::cin;
using std::endl;
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];
int i;
for (i = 0; i < ArSize; i++) {
cout << i + 1 << ": ";
cin.get(temp, MaxLen);
while (cin && cin.get() != '\n') continue; // 去掉非法输入
// 较早的get(char*, int)版本在读取空行后,返回的值不为false。
// 然而对于这些版本来说,如果读取了一个空行,则字符串中第一个字符将是一个空字符
if (!cin || temp[0] == '\0') //空行?
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][0] << ": " << sayings[i] << endl;
int shortest = 0;
int first = 0;
for (i = 1; i < total; i++) {
if (sayings[i].length() < sayings[shortest].length()) shortest = i;
if (sayings[i] < sayings[first])
first = i;
}
cout << "Shortest sayings:\n" << sayings[shortest] << endl;
cout << "First alphabetically:\n" << sayings[first] << endl;
cout << "This program used " << String::HowMany() << " String objects. Bye.\n";
}
else {
cout << "No input! Bye.\n";
}
return 0;
}
2. 在构造函数中使用new时应注意的事项
使用new初始化对象的指针成员时必须特别小心:
- 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete;
- new和delete必须互相兼容。new对应于delete,new[]对应于delete[];
- 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或C++11中的nullptr),这是因为delete(无论带不带中括号)可以用于空指针。
- 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。具体来说,复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。另外,还应该更新所有受影响的静态类成员。
- 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。具体地说,该方法应该完成这些操作:检查自我赋值的情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。
3. 有关返回对象的说明
如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须返回一个指向这种对象的引用。最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,则应首选引用,因为其效率更高。
4. 类和定位new运算符
程序员使用定位new运算符,要使用不同的内存单元,程序员需要提供两个位于缓冲区的不同地址,并确保这两个内存单元不重叠。
其次,如果使用定位new运算符来为对象分配内存,必须确保其析构函数被调用。delete可与常规new运算符配合使用,但不能与定位new运算符配合使用。这种问题的解决方案是,显式地为使用定位new运算符创建的对象调用析构函数。
以下程序清单对定位new运算符使用的内存单元进行管理,加入到合适的delete和显式析构函数调用。需注意删除顺序,对于使用定位new运算符创建的对象,应以与创建顺序相反的顺序进行删除。原因在于,晚创建的对象可能依赖于早创建的对象。
// placenew2.cpp
#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" << endl;
}
void Show() const {
cout << words << ", " << number << endl;
}
};
int main() {
char* buffer = new char[BUF];
JustTesting* pc1, * pc2;
pc1 = new(buffer) JustTesting;
pc2 = new JustTesting("Heap1", 20);
cout << "Memory block addresses:\n" << "buffer: "
<< (void*)buffer << " heap: " << pc2 << endl;
cout << "Memory contents:\n";
cout << pc1 << ": ";
pc1->Show();
cout << pc2 << ": ";
pc2->Show();
JustTesting* pc3, * pc4;
pc3 = new(buffer + sizeof(JustTesting))JustTesting("Better Idea", 6);
pc4 = new JustTesting("Heap2", 10);
cout << "Memory contents:\n";
cout << pc3 << ": ";
pc3->Show();
cout << pc4 << ": ";
pc4->Show();
delete pc2;
delete pc4;
pc3->~JustTesting();
pc1->~JustTesting();
delete[] buffer;
cout << "Done\n";
return 0;
}
5. 队列类
只有构造函数可以使用初始化列表语法。对于const类成员,必须使用这种语法。对于被声明为引用的类成员,也必须使用这种语法。这是因为引用与const数据类似,只能在被创建时初始化。对于本身就是类对象的成员来说,使用成员初始化列表的效率更高。
C++11的类内初始化。
模拟顾客队列:
#pragma once
// queue.h -- 12.10 -- interface for a queue
#ifndef QUEUE_H
#define QUEUE_H
class Customer {
private:
long arrive;
int processtime;
public:
Customer() { arrive = processtime = 0; }
void set(long when);
long when() const { return arrive; }
int ptime() const { return processtime; }
};
typedef Customer Item;
class Queue {
private:
struct Node {
Item item;
struct Node* next;
};
enum { Q_SIZE = 10 };
Node* front; // 指向链表头
Node* rear; // 指向链表尾
int items; // 目前链表里的成员数量
const int qsize; // 链表里Item的最大数量
Queue(const Queue& q): qsize(0){}
Queue& operator=(const Queue& q) { return *this; }
public:
Queue(int qs = Q_SIZE);
~Queue();
bool isempty() const;
bool isfull() const;
int queuecount() const;
bool enqueue(const Item& item); // 结尾加节点
bool dequeue(Item& item); // 链表头删节点
};
#endif // !QUEUE_H
// queue.cpp
#include <cstdlib>
#include "queue.h"
Queue::Queue(int qs) : qsize(qs) {
front = rear = nullptr;
items = 0;
}
Queue::~Queue() {
Node* temp;
while (front != nullptr) {
temp = front;
front = front->next;
delete temp;
}
}
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 (front == nullptr)
return false;
item = front->item;
items--;
Node* temp = front;
front = front->next;
delete temp;
if (items == 0)
rear = nullptr;
return true;
}
// time set to a random value in the range 1-3
void Customer::set(long when) {
processtime = std::rand() % 3 + 1;
arrive = when;
}
// bank.cpp
#include <iostream>
#include <cstdlib> // rand(), srand(), RAND_MAX
#include <ctime> // time()
#include "queue.h"
const int MIN_PER_HR = 60;
bool newcustomer(double x); // 判断是否有新用户到来
int main() {
using std::cin;
using std::cout;
using std::endl;
using std::ios_base;
std::srand(std::time(0)); // 随机初始化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;
// 循环每1分钟运行一次
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;
long turnaways = 0; // 因满队列而拒绝接收的人数
long customers = 0; // 加入队列的数量
long served = 0; // 仿真期间服务的人数
long sum_line = 0; // 累计队列长度
int wait_time = 0; // 自动存取机的空闲时间
long line_wait = 0; // 累计在队列中的时间
// 仿真开始
for (int cycle = 0; cycle < cyclelimit; cycle++) {
if (newcustomer(min_per_cust)) // 有新顾客到来
{
if (line.isfull())
turnaways++;
else {
customers++;
temp.set(cycle); // cycle = time of arrival
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();
}
// 报告结果
if (customers > 0) {
cout << "custormers 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);
}
6. 小结
(1)如果定义了一个类,其指针成员是使用new初始化的,请指出可能出现的3个问题以及如何纠正这些问题。
首先,当这种类型的对象过期时,对象的成员指针指向的数据仍将保留在内存中,这将占用空间,同时不可访问,因为指针已经丢失。可以让类析构函数删除构造函数中new分配的内存,来解决这种问题。
其次,析构函数释放这种内存后,如果程序将这样的对象初始化为另一个对象,则析构函数将试图释放这些内存两次。这是因为将一个对象初始化为另一个对象的默认初始化,将复制指针值,但不复制指向的数据,这将使两个指针指向相同的数据。解决方法是,定义一个复制构造函数,使初始化复制指向的数据。
第三,将一个对象赋值给另一个对象也导致两个指针指向相同的数据。解决方法是重载赋值运算符,使之复制数据而不是指针。
(2)如果没有显式提供类方法,编译器将自动生成哪些类方法?请描述这些隐式生成的函数的行为。
- 默认构造函数(不完成任何工作,但使得能够声明数组和未初始化的对象)
- 复制构造函数(使用成员赋值)
- 赋值运算符(使用成员赋值)
- 默认析构函数(不完成任何工作)
- 地址运算符(返回调用对象的地址,即this指针的值)