类和动态内存分配
本章内容包括:
对类成员使用动态内存分配
隐式和显示复制构造函数
隐式和显示重载赋值运算符
在构造函数中使用 new 所必须完成的工作
使用静态类成员
将定位 new 运算符用于对象
使用指向对象的指针
实现队列抽象数据类型(ADT)
12.1 动态内存和类
C++ 让程序在运行时决定内存分配,而不是在编译时决定;
C++ 使用 new 和 delete 运算符来动态控制内存;
遗憾的是,使用 new 和 delete 运算符将导致许多新的编程问题:
析构函数将是必不可少的;
有时要重载赋值运算符
12.1.1 复习示例和静态类成员
使用 char 指针(而不是 char 数组)来表示姓名:
类声明没有为字符串本身分配存储空间,而是在构造函数中使用 new 来为字符串分配空间;
避免了在类声明中预先定义字符串长度
静态类成员有一个特点: 无论创建了多少对象,程序都只创建一个静态变量副本;
注意: 静态数据成员在类声明中声明,在包含类方法文件中初始化;
初始化时使用作用域运算符来指出静态成员所属的类;
如果静态成员是整型或者枚举型 const,则可以在类声明中初始化
警告: 在构造函数中使用 new 来分配内存时,必须在相应的析构函数中使用 delete 来释放内存;
使用 new [ ] 来分配内存,应该使用 delete [ ] 来释放内存
12.1.2 特殊成员函数
C++ 自动提供的成员函数:
默认构造函数,如果没有定义构造函数;
默认析构函数,如果没有定义;
复制构造函数,如果没有定义;
赋值运算符,如果没有定义;
地址运算符,如果没有定义
默认构造函数不完成任何工作,但使得能够声明数组和未初始化的对象;
默认赋值构造函数和默认赋值运算符使用成员赋值;
默认析构函数不完成任何工作;
隐式地址运算符返回调用对象的地址(即this指针 )
StringBad 类中的问题由 隐式复制构造函数 和 隐式赋值运算符 引起的;
1. 默认构造函数
如果没有提供任何构造函数,C++ 将创建默认的构造函数;
如果定义了构造函数,C++ 将不会定义默认构造函数,如果希望创建对象时不显示的对其进行初始化,则要显示的定义默认构造函数;
只能有一个默认构造函数,编译器不能处理二义性
2. 复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中;
复制构造函数用于初始化过程中,而不是赋值过程中;
类的复制构造函数原型:
Class_name ( const Class_name & );
3. 何时调用复制构造函数
新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用:
StringBad ditto ( motto ); // calls StringBad ( const StringBad & )
StringBad metoo = motto; // calls StringBad ( const StringBad & )
StringBad also = StringBad( motto ); // calls StringBad ( const StringBad & )
StringBad * pStringBad = new StringBad ( motto ); // calls StringBad ( const StringBad & )
每当程序生成了对象副本时,编译器都将使用复制构造函数:
函数按值传递对象;
函数返回对象;
编译器生成临时对象
4. 默认的复制构造函数的功能
默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制);
如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象;
静态函数不受影响,因为它们不属于整个类
12.1.3 回到StringBad: 复制构造函数的哪里出现了问题
问题一: 程序使用默认的复制构造函数创建对象,而没有进行计数更新
问题二: 隐式复制构造函数是按值进行复制的,没有生成新的内存空间
1. 定义一个显示复制构造函数以解决问题
解决类设计中这种问题方法是进行深度复制(deep copy):
复制构造函数应当复制字符串并将副本的地址赋给 str 成员,调用析构函数将释放不同的字符串;
StringBad :: StringBad ( const StringBad & st )
{
num_strings ++; // handle static member update
len = st.len; // same length
str= new char [ len + 1 ]; // allot space
std :: strcpy( str, st.str ); // copy string to new location
cout << num_strings << ": "" << str << "" object created ";
}
必须定义复制构造函数的原因,一些类成员是使用 new 初始化的,指向数据的指针而不是数据本身;
警告: 如果类中包含了使用 new 初始化的指针成员,应当定义一个复制构造函数,以指向的数据而不是指针,被称为深度复制
复制的另一种形式(成员复制或浅复制)只是复制指针值,浅复制不会深入“挖掘”以复制指针引用的结构
12.1.4 StringBad 的其他问题: 赋值运算符
C++ 允许类对象赋值, 这是通过自动为类重载赋值运算符实现的,原型如下:
Class_name & Class_name :: operator = ( const Class_name & );
接受并返回一个指向类对象的引用
1. 赋值运算符的功能以及何时使用它
将已有的对象赋给另一个对象时,将使用重载的赋值运算符
初始化对象时,并不一定会使用赋值运算符,使用复制构造函数
StringBad metoo = knot; 实现时可能分两步来处理这条语句:
使用复制构造函数创建一个临时对象;
通过赋值将临时对象的值复制到新对象中
解决赋值的问题
StringBad & StringBad :: operator = ( const StringBad & st )
{
if( this == &st )
return *this;
delete [ ] str;
len = st.len;
str = new char [len + 1];
std :: strcpy( str, st.str );
return *this;
}
12.2 改进后的新 String 类
添加一些说明 String 类工作原理的方法:
int length() const {return len;}
friend bool operator < ( const String &st1, const String &st2 );
friend bool operator > ( const String &st1, const String &st2 );
friend bool operator == ( const String &st1, const String &st2 );
firend operator >> (istream & is, String & st);
char & operator [ ] (int i);
const char & operator [ ] (int i) const;
static int HowMany();
C++ 11 引入新关键字 nullptr,用于表示空指针
12.2.2 比较成员函数
比较操作符重载应用接受两个参数的标准 strcmp() 函数:
如果第一个参数位于第二个参数之前,返回一个负值;
如果第一个参数位于第二个参数之后,返回一个正值;
如果两个参数相等,返回0;
12.2.3 使用括号表示法访问字符
C++ 中,两个中括号组成一个运算符——中括号运算符,可以使用方法 operator [ ] ( );
中括号运算符,一个操作数位于第一个中括号的前面,另一个操作数了位于两个中括号之间; // city[0]
operator 是一个 String 对象,对于 operator [4]:
C++查找名称和特征标于此相同的方法: String :: operator [ ] ( int i );
将 opera[4] 替换为 opera.operator[ ] (4);
opera 对象调用该方法,数组下标成为该函数的参数
12.2.4 静态类成员函数
可以将成员函数声明为静态的:
不能通过对象调用静态成员函数,如果静态成员函数实在公有部分声明的,则可以使用类名和作用域解析运算符调用;
静态成员函数不与特定的对象相关联,因此只能使用静态数据成员
12.2.5 进一步重载赋值运算符
提高将常规字符串复制到 String 对象的效率,最简单的方法是重载赋值运算符,使之能够使用常规字符串
程序清单 12.4 string1.h
1 #ifndef STRING1_H_ 2 #define STRING1_H_ 3 #include<iostream> 4 using std::ostream; 5 using std::istream; 6 7 class String 8 { 9 private: 10 char *str; 11 int len; 12 static int num_strings; 13 static const int CINLIM = 80; // cin input limit 14 public: 15 // 构造函数与其他方法 16 String(const char * s); // 构造函数 17 String(); // 默认构造函数 18 String(const String &); // 复制构造函数 19 ~String(); // 析构函数 20 int length() const { return len; } 21 22 // 重载操作符方法 23 String & operator = (const String &); 24 String & operator = (const char *); 25 char & operator[](int i); 26 const char & operator[](int i) const; 27 28 // 重载操作符的友元 29 friend bool operator < (const String & st1, const String & st2); 30 friend bool operator > (const String & st1, const String & st2); 31 friend bool operator == (const String & st1, const String & st2); 32 friend ostream & operator << (ostream & os, const String & st); 33 friend istream & operator >> (istream & is, String & st); 34 35 // 静态成员函数 36 static int HowMany(); 37 }; 38 39 #endif
程序清单 12.5 string1.cpp
1 #pragma warning(disable:4996) 2 #include<cstring> 3 #include"string1.h" 4 using std::cin; 5 using std::cout; 6 7 // 初始化静态类成员 8 int String::num_strings = 0; 9 10 // 静态方法 11 int String::HowMany() 12 { 13 return num_strings; 14 } 15 16 // 类方法**************************************************** 17 18 // 构造函数传入C标准字符串参数 19 String::String(const char * s) 20 { 21 len = std::strlen(s); 22 str = new char[len + 1]; 23 std::strcpy(str, s); 24 num_strings++; 25 } 26 27 // 默认构造函数 28 String::String() 29 { 30 len = 4; 31 str = new char[1]; 32 str[0] = '