本章内容包括:
- 对类成员使用动态内存分配
- 隐式和显式地复制构造函数
- 隐式和显式地重载赋值操作符
- 在构造函数中使用new所必须完成的工作
- 使用静态类成员
- 将布局new操作符用于对象
- 使用指向对象的指针
- 实现队列抽象数据类型(ADT)
动态内存和类
复习范例和静态类成员
首先设计一个 StringBad类,然后设计一个功能稍强的 String类。
StringBad 和 String 类对象将包含一个字符串指针和一个表示字符串长度的值。这里使用 StingBad 和String 类,主要是为了深入了解 new、delete 和静态类成员的工作原理。因此,构造函数和析构函数调用时将显示一些消息,以便读者能够按照提示来完成操作。
对这个声明,需要注意的有两点。首先,它使用char指针(而不是char 数组)来表示姓名。这意味着类声明没有为字符串本身分配存储空间,而是在构造函数中使用new来为字符串分配空间。这避免了在类声明中预先定义字符串的长度。
strngbad.h
#include <iostream>
#ifndef STRNGBAD_H_
#define STRNGBAD_H_
class StringBad
{
private:
char *str;
int len;
static int num_strings;
public:
StringBad(const char *s);
StringBad();
~StringBad(); // destructor// friend function
friend std::ostream &operator<<(std::ostream &os, const StringBad &st);
};
#endif
strngbad.cpp
#include <cstring>
#include "strngbad.h"
using std::cout;
// initialjzing static class member
int StringBad::num_strings = 0;
// class methods
// construct StringBad fromC string
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[4];
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;
}
这条语句将静态成员num stings的值初始化为0。请注意,不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。
初始化是在方法文件中,而不是在类声明文件中进行的,这是因为类声明位于头文件中,程序可能将头文件包括在其他几个文件中。如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误。
析构函数首先指出自己何时被调用。这部分包含了丰富的信息,但并不是必不可少的。不过,delete语句却是至关重要的。str 成员指向new 分配的内存。当StringBad 对象过期时,str 指针也将过期。但 str指向的内存仍被分配,除非使用deete 将其释放。删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。因此,必须使用析构函数。在析构函数中使用delete 语句可确保对象过期时,由构造函数使用 new分配的内存被释放。
vegnews.cpp
#include <iostream>
using std::cout;
#include "strngbad.h"
void callme1(StringBad &); // pass by reference
void callme2(StringBad); // pass by value
int main()
{
using std::endl;
StringBad headlinel("Celery stalks at Midnight");
StringBad headline2("Lettuce Prey");
StringBad sports("Spinach Leaves Bowl for Dollars");
cout << "headlinel:" << headlinel << endl;
cout << "headline2:" << headline2 << endl;
cout << "sports:" << sports << endl;
callme1(headlinel);
cout << "headlinel:" << headlinel << 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 = headlinel;
cout << "knot: " << knot << endl;
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";
}
提前中断
程序说明
callme1()按引用传递没有发生问题
callme2()按值传递发生问题
首先,将 headline2作为函数参数来传递从而导致析构函数被调用。其次,虽然按值传递可以防止原始参数被修改,但实际上函数已使原始字符串无法识别,导致显示一些非标准字符(显示的具体内存取决于内存中包含的内容)。
当您使用一个对象来初始化另一个对象时,编译器将自动生成上述构造函数(称为复制构造函数,因为它创建对象的一个副本)。自动生成的构造函数不知道需要更新静态变量num_strings,因此会将计数方案搞乱。实际上,这个例子说明的所有问题都是由编译器自动生成的成员函数引起的,下面介绍这主题。
除去所有赋值操作,结果显示正常。
隐式成员函数
StringBad 类中的问题是由自动定义的隐式成员函数引起的,这种函数的行为与类设计不符。
具体来说C++自动提供了下面这些成员函数:
- 默认构造函数,如果没有定义构造函数
- 复制构造函数,如果没有定义
- 赋值操作符,如果没有定义
- 默认析构函数,如果没有定义
- 地址操作符,如果没有定义
更准确地说,编译器将生成上述最后4个函数的定义---如果程序使用对象的方式要求这样做。例如,如果您将:个对象赋给另一个对象,编译器将提供赋值操作符的定义。
结果表明,StringBad 类中的问题是由隐式复制构造函数和隐式赋值操作符引起的。
默认构造函数
复制构造函数
复制构造函数的问题
注意:
使用显式复制构造函数
解决类设计中这种问题的方法是进行深度复制(deepcopy)。
复制构造函数应当复制字符串并将副本的地址赋给s成员,而不仅仅是复制字符串地址。这样每个对象都有自己的字符串,而不是引用另一个对象的字符串。
调用析构函数时都将释放不同的字符串,而不会试图去释放已经被释放的字符串。
可以这样编写Sting的复制构造函数:
必须定义复制构造函数的原因在于,
一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。
赋值操作符
C++允许类对象赋值,这是通过自动为类重载赋值操作符实现的。
这种操作符的原型如下:
赋值的问题
解决赋值的问题
对于由于默认赋值操作符不合适而导致的问题,解决办法是提供赋值操作符(进行深度复制)定义其实现与复制构造函数相似,但也有一些差别
代码首先检查自我复制,这是通过查看赋值操作符右边的地址(&s)是否与接收对象(this)的地址相同来完成的。如果相同,程序将返回*this,然后结束。
如果地址不同,函数将释放 str指向的内存,这是因为稍后将把一个新字符串的地址赋给 str。如果不首先使用 delete操作符,则上述字符串将保留在内存中。由于程序中不再包含指向该字符串的指针,因此这些内存被浪费掉。
改进后的新String类
头文件
// string1.h -- fixed and augmented string class definition
#include <iostream>
using std::istream;
using std::ostream;
#ifndef STRINGl_H_
#define STRINGI_H_
class String
{
private:
char *str;
int len;
// pointer to string
// length of string
static int num_strings; // number of objects
static const int CINLIM = 80; // cin input limit
public:
// constructors and other methods
String(const char *s); // constructor
String(); //.default constructor
String(const String &); // copy constructor
~String();
// destructor
int length() const { return len; }
// overloaded operator methods
String &operator=(const String &);
String &operator=(const char *);
char &operator[](int i);
const char &operator[](int i) const;
// overloaded operator friends
friend bool operator<(const String &st, const String &st2);
friend bool operator>(const String &stl, const String &st2);
friend bool operator==(const String &st, const String &st2);
friend ostream &operator<<(ostream &os, const String &st);
friend istream &operator>>(istream &is, String &st); // static function
static int HowMany();
};
#endif
方法文件
string1.cpp
// stringl.cppString class methods
#include <cstring>
#include "string1.h" //includes <iostream>
using std::cin;
using std::cout;
int String::num_strings = 0;
// static method
int String::HowMany()
{
return num_strings;
}
// class methods
String::String(const char *s) // construct String fromC string
{
len = std::strlen(s);
// set size
// allot storage
str = new char[len + 1];
std::strcpy(str, s);
num_strings++;
}
String::String()
{
len = 4;
str = new char[1];
str[0] = '\0';
// default string
num_strings++;
}
String::String(const String &st)
{
num_strings++;
len = st.len;
str = new char[len + 1];
std::strcpy(str, st.str);
}
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;
}
// assign aC string to a string
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];
}
// read-only char access for const String
const char &String::operator[](int i) const
{
return str[i];
}
// overloaded operator friends
bool operator<(const String &st1, const String &st2)
{
return (std::strcmp(st1.str, st2.str) < 0);
}
bool operator>(const String &stl, const String &st2)
{
return st2.str < stl.str;
}
bool operator==(const String &stl, const String &st2)
{
return (std::strcmp(stl.str, st2.str) == 0);
}
// simple String output
ostream &operator<<(ostream &os, const String &st)
{
os << st.str;
return os;
}
// quick and dirty string input
istream &operator>>(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;
}
程序文件
#include <iostream>
#include "string1.h"
const int ArSize = 10;
const int MaxLen = 81;
int main()
{
using std::cin;
using std::cout;
using std::endl;
String name;
cout << "Hi,what's your name?\n>>";
cin >> name;
cout << name << ",please enter upto " << ArSize
<< "short sayings <empty line to quit>:\n";
String sayings[ArSize]; // array of objects
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; // i not incremented
else
sayings[i] = temp; // overloaded assignment
}
int total = i;
// total # of lines read
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 saying:\n"
<< sayings[shortest] << endl;
cout << "First alphabetically:\n"
<< sayings[first] << endl;
cout << "This program used " << String::HowMany()
<< "String objects. Bye.\n";
return 0;
}
运行结果
在构造函数中使用new时应注意的事项
有关返回对象的说明
返回指向const对象的引用
使用const引用的常见原因是旨在提高效率,但对于何时可以采用这种方式存在一些限制。
如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过传递引用来提高方法的效率。例如,假设要编写函数 Max(),它返回两个 Vector 对象中较大的一个返回对象将调用复制构造函数,而返回引用不会。
引用指向的对象应该在调用函数执行时存在。
第三,v1和v2都被声明为 const引用,因此返回类型必须为const,这样才匹配。
返回非const对象的引用
cout 能连续输出;
返回对象
返回指向对象的指针
加入指针的程序
#include <iostream>
#include "string1.h"
#include <ctime>
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 upto " << ArSize
<< "short sayings <empty line to quit>:\n";
String sayings[ArSize]; // array of objects
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; // i not incremented
else
sayings[i] = temp; // overloaded assignment
}
int total = i;
// total # of lines read
if (total > 0)
{
cout << "Here are your sayings:\n";
for (i = 0; i < total; i++)
cout << sayings[i] << endl;
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;
String *favorite = new String(sayings[choice]);
cout << "My favorite saying: \n " << *favorite << endl;
delete favorite;
}
else
cout << "No much to say, eh?\n";
cout << "Bye.\n";
return 0;
}
析构函数的调用
- 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。
- 如果对象是静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时将调用对象的析构函数。
- 如果对象是用new 创建的,则仅当您显式使用 delete 删除对象时,其析构函数才会被调用。
指针和对象小结
再谈布局new操作符
#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 memory
JustTesting *pc1, *pc2;
// place object in buffer
pc1 = new (buffer) JustTesting;
pc2 = new JustTesting("Heap1", 20); // place object on heap
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) JustTesting("Bad Idea", 6);
pc4 = new JustTesting("Heap2", 10);
cout << "Memory contents:\n";
cout << pc3 << ": ";
pc3->Show();
cout << pc4 << ": ";
pc4->Show();
delete pc2;
delete pc4;
delete[] buffer;
cout << "Done\n";
return 0;
}
在使用布局new操作符时存在两个问题。首先,在创建第二个对象时,布局new操作符使用一-个新对象来覆盖用于第一个对象的内存单元。显然,如果类动态地为其成员分配内存,这将引发问题。
其次,将 delete用于 pc2 和 pc4 时,将自动调用为 pc2 和 pc4 指向的对象调用析构函数;然而,将 delete[]用于 bufer 时,不会为使用布局 new 操作符创建的对象调用析构函数。
解决方法
一
二
原因在于 delete 可与常规 new 操作符配合使用,但不能与布局new操作符配合使用。
例如,指针 pc3没有收到 new 操作符返回的地址,因此 delete pc3 将导致运行阶段错误。在另一方面,指针 pc1指向的地址与 buffer 相同,但 buffer 是使用new[]初始化的,因此必须使用 delete[]而不是 delete 来释放。
即使 buffer是使用 new而不是new[]初始化的,delete pc1也将释放buffer,而不是 pc1。这是因为 new/delete 系统知道已分配的 512 字节块 buffer,但对布局new 操作符对该内存块做了何种处理一无所知。
#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 memory
JustTesting *pc1, *pc2;
// place object in buffer
pc1 = new (buffer) JustTesting;
pc2 = new JustTesting("Heap1", 20); // place object on heap
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("Bad 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;
}
复习各种技术
重载<<操作符
重新定义<<操作符,定义下面友元操作函数: