![d094dda2110825d32d99a16b4a017783.png](https://i-blog.csdnimg.cn/blog_migrate/bdf7ea26d673a494805a14c4338eb29d.jpeg)
本章内容包括:
- 对类成员使用动态内存分配
- 隐式和显式赋值构造函数
- 隐式和显式重载赋值运算符
- 在构造函数中使用new所必须完成的工作
- 使用静态类成员
- 将定位new运算符用于对象
- 使用指向对象的指针
- 实现队列抽象数据类型
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.3 在构造函数中使用 new 时应注意的事项
在构造函数中使用 new 来初始化指针成员,则应在析构函数使用 delete;
new 和 delete 必须互相兼容。new 对应于 delete,new [ ] 对应于 delete [ ];
只有一个析构函数,如果有多个构造函数,必须以相同的方式使用 new,所有的构造函数都必须与它兼容;
delete (无论带不带 [ ])可以用于空指针(NULL、0、nullptr);
定义一个复制构造函数,通过深度复制将对象初始化为另一个对象:
String :: String ( const String & st ){ num_string++; // 更新静态成员 len = st.len; str = new char [ len + 1 ]; // 分配新地址空间 std::strcpy(str,st.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; // 返回一个指向调用对象的引用}
12.3.2 包含类成员的类的逐成员复制
对于包含类成员的类,类的类成员使用动态内存分配:
默认的逐成员复制和赋值行为有一定的智能;
逐成员复制将使用成员类型定义的复制构造函数和赋值运算符
如果类因其他成员需要定义复制构造函数和赋值运算符,这些函数必须显示地调用 String 和 string 的复制构造函数和赋值运算符
12.4 有关返回对象的说明
当成员函数或独立的函数返回对象时,有几种返回方式可供选择:
指向对象的引用;
指向对象的 const 引用;
const 引用;
12.4.1 返回指向 const 对象的引用
使用 const 引用的常见原因是旨在提高效率:
返回对象将调用复制构造函数,而返回引用不会;
引用指向的对象应该在调用函数执行时存在;
参数被声明为 const 引用,返回类型必须为 const;
12.4.2 返回指向非 const 对象的引用
operator = () 的返回值用于连续赋值:
Sring s1 ( "Good stuff" ); String s2, s3; s3 = s2 = s1;
返回类型不是 const 因为方法 operator = () 返回一个指向 s2 的引用,可以对其进行修改。返回引用可以不调用复制构造函数
operator << ( ) 的返回值用于串接输出:String s1( "Good stuff" );cout << s1 << "is coming!";operator << ( cout, s1 ) 的返回值成为一个用于显示字符串 " is coming! "的对象;
返回类型必须为 ostream &,而不能是 ostream,因为 ostream 没有公有的复制构造函数
12.4.3 返回对象
如果返回的对象是调用函数中的局部变量,不能按引用返回它,被调用函数执行完毕后,局部对象将调用其析构函数,引用指向的对象不再存在;
返回对象而不是引用,将调用复制构造函数
12.4.4 返回方式总结
如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用,将使用复制构造函数来生成返回的对象;
如果方法或函数返回一个没有公有复制构造函数的类的对象,它必须返回这种对象的引用;
有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,首选引用,因为效率高
12.5 使用指向对象的指针
12.5.1 再谈 new 和 delete
![fa833b5d352e8ae973a644ab32740531.png](https://i-blog.csdnimg.cn/blog_migrate/3bff4798947a8f3bf615175157f35f3d.jpeg)
12.5.2 指针和对象小结
使用对象指针注意:
使用常规表示法来声明指向对象的指针: String * glamour;可以将指针初始化为已有的对象: String * first = &sayings[0];可以使用 new 来初始化指针,这将创建一个新对象(使用构造函数): String * favorite = new String (sayings[choice]); String * gleep = new String // 使用默认构造函数 String * glop = new String ( "my my my " ) // 使用构造函数 String ( const char * ) String * favorite = new String ( sayings[ choice ] ) // 使用复制构造函数 String ( const String & )可以使用 -> 运算符通过指针访问类方法: shortest -> length();可以对对象指针应用解除引用运算符 (*)来获得对象: *first
12.6 复习各种技术
12.6.1 重载 << 运算符
要重新定义 << 运算符,以便将它和 cout 一起用来显示对象的内容,定义下面友元运算符函数:
ostream & operator << ( ostream & os, const c_name & obj ){ os << . . . ; // display object contents return os;}
其中 c_name 为类名, 如果该类能够返回所需内容的公有方法,则可以在运算符中使用这些方法,这样便不用将它们设置为友元函数了
12.6.2 转换函数
要将单个值转换为类类型,需要定义类构造函数:
c_name ( type_name value ); // c_name为类名, type_name是要转换的类型名称
要将类转换为其他类型,需要创建类成员函数:
operator type_name ( ); // type_name 为要转换的类型
使用转换函数时要小心,可以在声明构造函数时使用关键字 explicit,防止被用于隐式转换
12.6.3 其构造函数使用 new 的类
如果使用 new 运算符来分配类成员指向的内存,在设计时应采取一些措施:
对于指向的内存是由 new 分配的所有类成员,都应在析构函数中对其使用 delete,该运算符将释放分配的内存;
如果析构函数通过对指针类成员使用 delete 来释放内存,则每个构造函数都应当使用 new 来初始化指针,或设置为空指针;
构造函数使用 new[ ] 或 new,不能混用;
new [ ] 对应于 delete [ ] , new 对应于 delete;
应定义一个分配内存(而不是使用指针指向已有内存)的复制构造函数。这样程序能将类对象初始化为另一个类对象;
应定义一个重载赋值运算符的类成员函数,这样程序能将类对象赋值给另一个类对象
12.7 队列模型
队列特征:
队列存储有序的项目序列;
队列所能容纳的项目数有一定的限制;
应当能够创建空队列;
应当能够检查队列是否为空;
应当能够检查队列是否为满;
应当能够在队尾添加项目;
应当能够在队首删除项目;
应当能够确定队列中的项目数目
1. Queue类的接口
2. Queue类的实现
确定队列的数据结构——链表:
struct Node { Item item; struct Node * next; }
嵌套结构和类:
在类声明中声明的结构、类或枚举被称为是被嵌套在类中,其作用域为整个类;
3. 类方法
类构造函数应提供类成员的值,对于非静态 const 数据成员,必须在执行到构造函数体之前,创建对象时进行初始化;
C++ 提供: 成员初始化列表:
成员初始化列表有逗号分隔的初始化列表组成(前面带冒号);
通常,初始值可以是常量或构造函数的参数列表中的参数,不限于初始化常量;
只有构造函数可以使用这种初始化列表语法;
引用于 const 数据类似,只能在被创建时进行初始化;
对于简单数据成员,使用成员初始化列表和在函数体中使用赋值没有什么区别
注意:
这种格式只能用于构造函数;
必须使用这种格式来初始化非静态 const 数据成员;
必须使用这种格式来初始化引用数据成员;
数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的顺序无关
警告: 不能将成员初始化列表语法用于构造函数之外的其他类方法
成员初始化列表使用的括号的方式也可用于常规初始化:
int games = 162;
double talk = 2.71828;
替换为:
int games(162);
double talk(2.71828);
这使得初始化内置类型就像初始化类对象一样
C++ 11允许类内初始化: class Classy { int mem1 = 10; const int mem2 = 20; . . . }
禁止复制构造函数和赋值运算符的方法:
1. 将所需的方法定义为伪私有方法:
class Queue{private: Queue ( const Queue & q ): qsize(0) { } Queue & operator = ( const Queue & q ) { return *this; }}
避免了本来将自动生成的默认方法定义;
方法是私有的,所以不能被广泛使用
2. C++ 11提供了另一种禁用方法的方式——使用关键字 delete
12.8 总结
在类构造函数中,可以使用 new 为数据分配内存,然后将内存地址赋给类成员,这样便可以处理长度不同的字符串;在类构造函数中使用 new 如果对象包含成员指针,指向的内存由 new 分配: 释放保存对象的内存并不会自动释放成员指针指向的内存; 应在类析构函数中使用 delete 来释放分配的内存如果对象包含指向 new 分配的内存的指针成员,则将对象初始化为另一个对象,或将一个对象赋值给另一个对象会出现问题: 浅复制, 没有创建指针指向内容的副本 解决方法:定义一个特殊的复制构造函数来重新定义初始化,并重载赋值运算符: 新的定义都将创建指向数据的副本,并使新对象指向这些副本,旧对象与新对象都将引用独立的相同的数据对象的存储持续性为自动或外部时,在它不再存在时将自动调用其析构函数;如果使用 new 运算符为对象分配内存,并将地址赋给一个指针,则当使用 delete 时自动为对象调用析构函数;如果使用定位 new 运算符为类对象分配内存,则必须显示的为该对象调用析构函数: 方法是使用指向该对象的指针调用析构函数的方法C++ 允许在类中包含结构、类和枚举定义,这些嵌套类型的作用域为整个类;C++ 为类构造函数提供了一种可用来初始化数据成员的特殊语法:成员初始化列表 如果数据是非静态 const 成员或引用,则必须采用这种格式 C++11 允许类内初始化,与成员初始化列表等价,使用成员初始化列表的构造函数将覆盖相应的类内初始化