第12章 类和动态内存分配
12.1 动态内存和类
// 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
对于该类声明,需要注意两点:
使用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;
// 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;
}
上述代码中存在一条code:
int StringBad::num_strings=0;
注意:不能再类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。初始化是在方法文件中,而不是在类声明文件中进行的。
构造函数中使用strcpy()将传递的字符串复制到新的内存中,字符串并不保存在对象中。字符串单独保存在堆内存中,对象仅指出到哪里去查找字符串的信息,不能使用如下方法:
str=s; //not the way to go
该code只保存了地址,而没有创建字符串副本。
使用new构造函数对应的析构函数也要使用delete
String::~StringBad()
{
cout<<"\"<<str<<"\"object deleted,";
--num_strings;
cout<<num_strings<<"left\n";
delete [] str;
}
12.1.2 特殊成员函数
C++自动提供了下面这些成员函数:
默认构造函数,如果没有定义构造函数
默认析构函数,如果没有定义
复制构造函数,如果没有定义
赋值构造函数,如果没有定义
地址运算符,如果没有定义
1)默认构造函数
如果没有提供任何构造函数,C++将创建默认构造函数。假设定义了一个Klunk类,但没有提供任何构造函数,则编译器将提供下述默认构造函数:
Klunk:Klunk(){} //implicit default constructor
即编译器将提供一个不接受任何参数,也不执行任何操作的构造函数。也就是说,它的值在初始化时是未知的。
但是如果定义了构造函数,C++将不会定义默认构造函数。可以显式地定义默认构造函数,即:
Klunk:Klunk() //explict default constuctor
{
Klunk_ct=0;
...
}
- 复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数)。类的复制构造函数原型如下:
Class_name(const Class_name &);
它接受一个指向类对象的常量引用作为参数。例如,String类的复制构造函数的原型如下:
StringBad(const StringBad &);
那么何时调用复制构造函数。即:新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。例如,假设motto是一个StringBad对象,则下面4种声明都将调用复制构造函数:
StringBad ditto(motto); //calls StringBad(const StringBad &)
StringBad metto=motto; //calls StringBad(const StringBad &)
StringBad also =StringBad(motto);
//calls StringBad(const StringBad &)
StringBad *pStringBad=new StringBad(motto);
//calls StringBad(const StringBad &)
由于按值传递对象将调用复制构造函数,因此应该按引用传递对象。
默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。即:
StringBad sailor=sports;
该语句与下面的代码等效:
StringBad sailor;
sailor.str=sports.str;
sailor.len=sports.len;
浅复制仅仅只是复制地址,这就会导致一些问题存在。为了解决该问题,利用以下方法解决浅复制的问题:
定义一个显式复制构造函数以解决问题(复制构造函数应当复制字符串并将副本的地址赋给str成员)
StringBad::StringBad(const StringBad &st)
{
num_strings++;
len=st.len;
str=new char [len+1];
std::strcpy(str,st.str);
cout<<num_strings<<": \""<<str<<"\" object created\n";
}
必须显式定义复制构造函数的原因在于,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这就被称为深度复制。
12.1.4 赋值运算符
C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的。这种运算符的原型如下:
Class_name & Class_name::operator=(const Class_name &);
它接受并返回一个指向类对象的引用。例如,StringBad类的赋值运算符的原型如下:
StringBad & StringBad::operator=(const StringBad &);
对于由于默认赋值运算符不合适而导致的问题,解决办法是提供赋值运算符(进行深度复制)定义。下面的代码是说明了显式重载赋值运算符:
StringBad & StringBad::operator=(const StringBad & st)
{
if (this == &st) //object assigned to itself
return *this; //all done
delete [] str; //free old string
len=st.len;
str=new char[len+1]; //get space for new string
std::strcpy(str,st.str); //copy the string
return *this; //return reference to invoking object
}
在有了上述的知识后,改进后的新string类。
此时在类声明中添加以下部分:
int length () const {return len;}
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 operator>>(istream &is,String &st);
char & operator[](int i);
const char & operator[](int i)const;
static int HowMany;
修改后的默认构造函数,它与下面类似:
String::String()
{
len=0;
str=new char[len+1];
str[0]='\0';
}
12.2.3 使用中括号表示法访问字符
在C++中,两个中括号组成一个运算符–中括号运算符,可以使用operator来重载该运算符。
假设opera是一个String对象,下面是该方法的简单实现:
char &String::operator[](int i)
{
return str[i];
}
12.2.4 静态类成员函数
可以将成员函数声明为静态的(函数声明必须包含关键字static),这样就有两个结果:
首先,不能通过对象调用静态成员函数;如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。例如,可以给String类添加一个名为HowMany()的静态成员函数,方法是在类声明中添加如下原型/定义:
statics int HowMany(){return num_strings;}
调用它的方式如下:
int count=String::HowMany(); //invoking a static member function
由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。
12.2.5 进一步重载赋值运算符
为提高处理效率,最简单的方法是重载赋值运算符,下面是一种可能实现的方法:
String & String::operator=(const char *s)
{
delete []str;
len=std::strlen(s);
str=new char[len+1];
std::strcpy(str,s);
return *this;
}
一般来说,必须释放str指向的内存,并为新字符串分配足够的内存。
12.3 在构造函数中使用new时应注意的事项
在使用new初始化对象的指针成员时必须注意以下事项:
如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete;
new和delete必须相互兼容。new对应于delete,new[ ]对应于delete[ 。
如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。
应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。通常,这种构造函数与下面类似:
StringBad::StringBad(const StringBad &st)
{
num_strings++;
len=st.len;
str=new char [len+1];
std::strcpy(str,st.str);
}
应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象,即:
StringBad & StringBad::operator=(const StringBad & st)
{
if (this == &st) //object assigned to itself
return *this; //all done
delete [] str; //free old string
len=st.len;
str=new char[len+1]; //get space for new string
std::strcpy(str,st.str); //copy the string
return *this; //return reference to invoking object
}
12.5 使用指向对象的指针
使用 new初始化对象
如果Class_name是类,value的类型为Type_name,则下面的语句:
Class_name *pclass = new Class_name(value);
将调用如下构造函数:
Class_name(Type_name);
12.5.2 指针和对象小结
使用对象指针时,需要注意以下几点:
使用常规表示法来声明指向对象的指针:
String * glamour;
可以将指针初始化为指向已有的对象:
String * first=&sayings[0];
可以使用new来初始化指针,这将创建一个新的对象
String * favorite=new String(sayings[choice]);
对类使用new将调用相应的类构造函数来初始化新创建的对象:
//invokes default constructor
String * gleep=new String;
//invokes the String(const char *)constructor
String * glop=new String("my my my");
//invokes the String(const String &)constructor
String * favorite=new String(sayings(choices));
可以使用->运算符通过指针访问类方法:
if (sayings[i].length() < shortest->length())
可以对对象指针应用解除引用运算符(*)来获得对象:
if (sayings[i] < *first) // compare object values
first = &sayings[i]; // assign object addresds
12.6 复习各种技术
12.6.1 重载<<运算符
12.6.2 转换函数
12.6.3 构造函数使用new的类
12.7 队列模拟