一、动态内存和类
C++在分配内存的时候是让程序是在运行时决定内存分配,而不是在编译时再决定。C++使用new和delete运算符来动态控制内存。但是在类中使用这些运算符将导致许多新的编程问题,在这种情况下,析构函数是必不可少的。有时候还必须重载赋值运算符,以保证程序正常运行。
静态类成员函数,首先设计一个StringBad类,然后设计一个功能稍强的String。StringBad和String类对象将包含一个字符串指针和一个表示字符串长度的值。这里使用StringBad和String类,主要是深入了解new、delete和静态成员的工作原理。因此,构造函数和析构函数调用时将显示一些消息,以便你能够按照提示来完成操作。另外,将省略一些有用的成员和友元函数,如重载++和>>运算符以及转换函数,以简化类接口。
// strngbad.h -- flawed string class definition
#include <iostream>
#ifndef STRNGBAD_H_
#define STRNGBAD_H_
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 function
friend std::ostream & operator<<(std::ostream & os,
const StringBad & st);
};
#endif
为何将这个类命名为StringBad?为了说明这是一个不太完整的类,它是使用动态内存分配开发类的第一个阶段,正确地完成了一些显而易见的工作,例如,在构造函数和析构函数中正确的使用new和delete。这个类并没有什么错误,但忽略了一些不明显却必不可少的定西。通过了解这个类的存在,将有助于后面将其转换为更强大的String类,所做的不明显修改。
在这个声明里,首先,它使用char指针(而不是char数组)来表示姓名。这意味着类声明没有为字符串本身分配存储空间,而是在构造函数中使用new来为字符串分配空间。这避免了在类声明中预先定义字符串长度。其次,将num_strings成员声明为静态存储类。静态成员有一个特点就是无论创建多少对象,程序都只创建一个静态变量副本。也就是说,类的所有对象共享同一个静态成员。`
// strngbad.cpp -- StringBad class methods
#include <cstring> // string.h for some
#include "strngbad.h"
using std::cout;
// initializing static class member
int StringBad::num_strings = 0;//初始化为0
// class methods
// construct StringBad from C string
StringBad::StringBad(const char * s)
{
len = std::strlen(s); // set size
str = new char[len + 1]; // allot storage
std::strcpy(str, s); // initialize pointer
num_strings++; // set object count
cout << num_strings << ": \"" << str
<< "\" object created\n"; // For Your Information
}
StringBad::StringBad() // default constructor
{
len = 4;
str = new char[4];
std::strcpy(str, "C++"); // default string
num_strings++;
cout << num_strings << ": \"" << str
<< "\" default object created\n"; // FYI
}
StringBad::~StringBad() // necessary destructor
{
cout << "\"" << str << "\" object deleted, "; // FYI
--num_strings; // required
cout << num_strings << " left\n"; // FYI
delete [] str; // required
}
std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
os << st.str;
return os;
}
// vegnews.cpp -- using new and delete with classes
// compile with strngbad.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;
{
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 = 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";
// std::cin.get();
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";
}
二、改进后新的string类
首先,复制构造函数和赋值运算符,使类能够正确管理类对象使用的内存。其次,由于已经知道了对象何时被创建和释放,因此可以让类构造函数和析构函数保持沉默,不再在每次被调用时都显示消息。另外,也不用再监视构造函数的工作情况,因此可以简化默认构造函数,使之创建一个空字符串,而不是“C++"。
1.修订后的默认构造函数
str = new char[1]与类析构函数兼容。析构函数中包含如下的代码:delete [] str;
2.比较成员函数
在String类中,执行比较操作的方法有3个。按机器排序序列,第一个字符串在第二个字符串之前,则Operator<()函数返回true。要实现字符串比较函数,最简单的方法时使用标准的trcmp()函数,如果按照字母顺序,第一个参数位于第二个参数之前,则该函数返回一个负值:如果两个字符串相同,则返回0;如果第一个参数位于第二个参数之后,则返回一个正值。
3.使用中括号表示法访问字符
可以使用方法operator来重载该运算符,通常,二元C++运算符位于两个操作数之间。但对于中括号运算符,一个操作数位于第一个中括号的前面,另一个操作数位于两个中括号之间。
4.静态成员函数
可以将成员函数声明为静态的,(函数声明必须包含关键字static,但如果函数定义是独立的,则其中不能包含关键字static)这样做有两个重要的后果。
首先,不能通过对象调用静态成员函数:实际上,静态成员函数甚至不能使用this指针,如果静态成员函数是在公有部分声明的,则可以使用类名个作用域接写运算符来调用它。
其次,由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。
5.进一步重载赋值运算符
程序使用构造函数String(const char*)来创建一个临时String对象,其中包含temp中的字符副本。
使用String&String::operator = (const String &)函数将临时对象中的信息复制到name对象中。
程序调用析构函数~String()删除临时对象。
三、在构造函数中使用new时应注意的事项
- 如果构造函数中使用new来初始化指针成员,则应在析构函数中使用delete;
- new和delete必须相互兼容,new对应于delete,new[]对应于delete[];
- 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空,这是因为delete可以用于空指针。
- 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。
- 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。
四、有关返回对象的说明
当成员函数或独立的函数返回对象时,有几种返回方式可供选择。可以返回指向对象的引用、指向对象的const引用或const对象。
1.返回指向const对象的引用
使用const引用的常见原因是旨在提高效率,但对于何时可以采用这种方式存在一些限制。如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高其效率。
//version 1
Vector Max (const Vector & v1,const Vector & v2)
{
if (v1.magva1() > v2.magva1())
return v1;
else
return v2;
}
//version 2
const Vector & Max (const Vector & v1,const Vector & v2)
{
if (v1.magva1() > v2.magva1())
return v1;
else
return v2;
}
首先,返回对象将调用复制构造函数,而返回引用不会。因此,第二个版本所做的工作更少,效率更高。
其次,引用指向的对象应该在调用函数执行时存在。
第三,v1和v2都被声明为const引用,因此返回类型必须为const,这样才匹配。
2.返回指向非const对象的引用
两种常见的返回非const对象的情形是,重载赋值运算符以及重载与cout一起使用的<<运算符,前者这样做旨在提高效率,而后者必须这样做。
3.返回对象
如果被返回对象是在被调用函数中的局部变量,则不应该按引用的方式返回它,因为在被调用函数执行完毕时,局部对象将调用析构函数。因此,当控制权回到调用函数时,引用指向的对象将不再存在,在这种情况下,应返回对象而不是引用。通常,被重载的算术运算符属于这一类。
4.返回const对象
前面的Vector::operator+()定义有一个奇异的属性,它旨在让您能够以下面的方式使用它:
net = force1+force2
然而,这种定义也允许您这样使用它:
force1 + force2 = net;
cout << (force1 +force2 = net).magval()<<endl;
五、使用指向对象的指针
// sayings2.cpp -- using pointers to objects
// compile with string1.cpp
#include <iostream>
#include <cstdlib> // (or stdlib.h) for rand(), srand()
#include <ctime> // (or time.h) 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; // 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] << "\n";
// use pointers to keep track of shortest, first strings
String * shortest = &sayings[0]; // initialize to first object
String * first = &sayings[0];
for (i = 1; i < total; i++)
{
if (sayings[i].length() < shortest->length())
shortest = &sayings[i];
if (sayings[i] < *first) // compare values
first = &sayings[i]; // assign address
}
cout << "Shortest saying:\n" << * shortest << endl;
cout << "First alphabetically:\n" << * first << endl;
srand(time(0));
int choice = rand() % total; // pick index at random
// use new to create, initialize new String object
String * favorite = new String(sayings[choice]);
cout << "My favorite saying:\n" << *favorite << endl;
delete favorite;
}
else
cout << "Not much to say, eh?\n";
cout << "Bye.\n";
// keep window open
/* if (!cin)
cin.clear();
while (cin.get() != '\n')
continue;
cin.get();
*/
return 0;
}
1,再谈new和delete
使用new创建的每一个对象的名称字符串分配存储空间,这是在构造函数中进行的,因此析构函数使用delete来释放这些内存。因为字符串是一个字符数组,所以析构函数使用的是带中括号的delete。这样,当对象被释放时,用于存储字符串内容的内存将被自动释放。
这不是为要存储的字符串分配内存,而是为对象分配内存。也就是说,为保存字符串地址的str指针和len成员分配内存。创建对象将调用构造函数,后者分配用以保存字符串的内存,并将字符串的地址赋给str。然后,当程序不再需要该对象时,使用delete删除它。对象是单个的,因此,程序使用不带中括号的delete。与前面介绍的相同,这将只释放用于保存str指针和len成员的空间,并不释放str指向的内存,而该任务由析构函数来完成。
在下述情况下析构函数将被调用:
-
如果对象是动态变量,则当执行定义该对象的程序块时,将调用该对象的析构函数。
-
如果对象是静态变量,则在程序结束时将调用对象的析构函数。
-
如果对象是new创建的,则仅当您显示使用delete删除对象是,其析构函数才会被调用。
2.指针和对象小结 -
使用常规表示法来声明指向对象的指针:String * glamuor;
-
可以将指针初始化为指向已有的对象:string *first = &sayings[0];
-
可以使用new来初始化指针,这将创建一个新的对象:String * favorite = new String(saying [choice])
-
对类使用new将调用相应的类构造函数来初始化创建的对象:String * gleep = new String;
-
可以使用->运算符通过指针访问类方法:if (sayings[i].length() < shortest ->length())
-
可以对对象指针应用解除引用运算符(* )来获得对象;
if(saying[i]< * first)
first = &sayings[i];
3.再谈定位new运算符
定位new运算符能够在分配内存时能够指定内存位置。
// placenew2.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 memory
JustTesting *pc1, *pc2;
pc1 = new (buffer) JustTesting; // place object in buffer
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;
// fix placement new location
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; // free Heap1
delete pc4; // free Heap2
// explicitly destroy placement new objects
pc3->~JustTesting(); // destroy object pointed to by pc3
pc1->~JustTesting(); // destroy object pointed to by pc1
delete [] buffer; // free buffer
// std::cin.get();
return 0;
}
六、复习各种技术
1.重载<<运算符
要重新定义<<运算符,以便将它和cout一起用来显示对象的内容。请定义下面的友元运算符函数:
ostream & operator << (ostream & os, const c_name & obj)
{
os << ……;
return os ;
}
其中,c_name是类名,如果该类提供了能够返回所需内容的公有方法,则可在运算符函数中使用这些方法,这样便不用将它们设置为友元函数。
2.转换函数
要将单个值转换为类型,需要创建原型如下所示的类构造函数:
c_name(type_name value)
其中c_name为类名,type_name是要转换的类型的名称。
要将类型转换为其他类型,需要创建原型如下所示的类成员函数:
operator type_name();
虽然该函数没有返回声明类型,但应该返回所需类型的值。
3.其构造函数使用new的类
- 对于指向的内存是new分配的所有成员,都应在类的析构函数中对其使用delete,该运算符
将释放分配的内存。 - 如果析构函数通过对指针类成员使用delete来释放内存,则每个构造函数都应当使用new来初始化指针,或将它设置为空指针。
- 构造函数中要么使用new[],要么使用new,而不能混用。如果构造函数使用的new[],则析构函数应使用delete[];反之。
- 应定义一个分配内存的复制构造函数,这样程序将能够将类对象初始化为另一个类对象。
七、队列模拟
队列是一种抽象的数据类型,可以存储有序的项目序列。新项目被添加在队尾,并可ui删除队首的项目。队列有点像栈,但栈在同一端进行添加和删除。栈是后进先出的结构,而队列是先进先出的。
1.队列类
队列的特征:
- 存储有序的项目序列
- 容纳的项目数有一定的限制
- 能够创建空队列
- 检查队列是否为空
- 检查队列是否为满的
- 在队尾添加项目
- 从队首删除项目
- 能够确定队伍的项目数
2.Customer类
class Customer
{
private:
long arrive;
int processtime;
public:
Customer() {arrive = processtime = 0;}
void set(long when);
long when() cosnt{return arrive;}
int ptime() const{return processtime;}
};
void Customer::set(long when)
{
procresstime = std::rand() %3/1;
arrive = when;
}
默认构造函数创建一个空客户。set()成员函数将到达时间设置为参数,并将处理时间设置为1~3中的一个随机值。