第四部分 OOP和类设计
第四部分 - OOP和类设计
第四部分 OOP和类设计、类的应用 第一小节
零、绪论:OOP的重要特性:抽象、封装和数据隐藏、多态、继承、代码的可重用性
用户定义类型:实现抽象接口的类设计
指定类型需要完成的工作:
·决定数据对象需要的内存数量
·决定内存中每一位的含义(作用)
·决定对数据对象可执行的操作、方法。
- 类规范的组成:
·类声明:数据成员描述数据部分,成员函数(方法)描述公有接口,一般在头文件中
·类方法定义:类成员函数的实现,一般在源代码文件中。
一、类的声明:
class 类名
{
private:
私有成员
public:
公有成员
};
在此处不能用typename代替class.class指出要声明类。
例子:
*class student
{
private:
int class_;
int grade;
string name;
string school;
public:
void self_introduce();
student get_info();
}
此时可以创建student类型的变量,叫做类对象或实例。(如student A;)
成员函数可以就地定义,也可以用原型表示。
访问控制:使用类对象的程序都可以直接访问公有部分(public标签下的部分),私有成员(private标签下的部分)只能用公有的成员函数访问,外部无法直接访问,是数据隐藏。
类对象的默认访问控制是private,因此可以省略private,而结构也可以拥有成员函数,其默认访问类型是public.
实现类成员函数:使用作用域解析运算符。
类成员函数的限定名 所属类名::成员函数名
如:student::self_introduce()表示student类中的self_introduce()成员函数
成员函数的函数定义:
返回类型 限定名(形参列表)//或 返回类型 所属类名::成员函数名(形参列表)
{
定义
}
此时self_introduce()具有类作用域,其他成员函数可以直接访问它。
成员函数可以访问类的私有成员。
定义位于类声明中的函数自动成为内联函数。在类声明之外定义内联成员函数只需在实现部分中定义函数时使用inline限定符即可。每个使用内联函数的文件中都要对其进行定义。
改写规则:在类的声明中定义方法等同于用原型替换方法定义并在类声明后定义改写为内联函数。
使用对象
创建对象类名 对象名;
使用成员对象名.成员名;
调用成员函数时,它将会使用被用来调用它的对象的数据成员。
每个对象都有自己的存储空间,存储内部变量和类成员,同一类所有的类对象共享同一组方法,每种方法只有一个。
所有结构能做的事情类都能做,如把一个对象赋给同类型的另一个对象。
通常数据成员放在私有部分,成员函数(方法)放在公有部分。
二、构造函数、析构函数
构造函数在创建对象时调用,可以自动初始化对象
构造函数名是类的名称,没有返回值(不是void)
cerr(在iostream中)流输出错误信息。用法同cout.
调用构造函数:
类名 对象名=构造函数名(参数);//显示
类名 对象名(参数);//隐式
在使用new时调用类名* 指针=new 类名(构造函数参数);
注意:不能将类成员名用作构造函数的参数名。
构造函数用来创建对象,不能通过对象调用。
- 默认构造函数: 构造函数名(){定义}当且仅当没有初始值时调用
当且仅当未定义构造函数时编译器才会提供默认的构造函数(隐式版本)。即
构造函数名(){} 自动生成,不做任何事情。
在为类定义非默认构造函数后,而没有定义默认构造函数时,不提供初始值会出错。
可以有多个构造函数,只有一个默认构造函数。
隐式调用默认构造函数时,不要使用圆括号!
析构函数:对象过期时,程序自动调用析构函数进行清理工作。其在类名前加上~,没有参数,没有返回值,若没有提供析构函数,编译器提供默认析构函数。
const成员函数:
声明类只描述了对象的形式,并没有创建对象,所以不能直接使用const在类定义中声明常量。若一个成员函数不会改变数据,声明如下:
返回类型 函数名(形参列表) const;
原书实例:
stock10.h
//* stock10.h Stock class declaration with constructors, destructor added**
#ifndef STOCK1_H_
#define STOCK1_H_
#include ********
class Stock
{****
private:****
**std****::**string company**;******
**long** shares**;******
**double** share_val**;******
**double** total_val**;******
**void** set_tot**()** **{** total_val **=** shares ***** share_val**;** **}******
public:****
Stock**();** *//* *default* *constructor***
Stock**(****const** **std****::**string **&** co**,** **long** n **=** 0**,** **double** pr **=** 0.0**);******
**~**Stock**();** *//* *noisy* *destructor***
**void** buy**(****long** num**,** **double** price**);******
**void** sell**(****long** num**,** **double** price**);******
**void** update**(****double** price**);******
**void** show**();******
};****
#endif
stock10.cpp
// stock1.cpp Stock class implementation with constructors, destructor added**
#include ********
#include “stock10.h”****
// constructors (verbose versions)**
Stock**::Stock()** // default constructor**
{****
**std****::**cout **** shares**)******
**{******
cout ********
#include “stock10.h”****
int main**()******
{****
{****
**using** **std****::**cout**;******
cout **三、this指针:1.每一个类方法都有一个this指针,指向调用它的对象的地址。
2.this指针被作为隐藏参数传递给方法。
3.->运算符同样适用于访问类成员。
4.对象数组:与普通数组差别不大,可以使用大括号并调用构造函数使用。
四、类作用域
·在类中定义的名称作用域都为整个类。
1.作用域为类的常量:由于const变量需要初始化,而声明类只是描述了对象的形式,没有创建对象。因此在创建对象前,没有用于存储const的空间。
(1)声明为枚举格式:enum{符号=值(整数)};
声明枚举不会创建类数据成员,因为此处没有枚举类型名。
编译器在遇到符号时替换为对应的值。
(2)使用static const 类型 变量名=值;
相当于静态常量,与其他静态变量存储在一起
[C++11]2.作用域内枚举:作用域为类
(1) 传统枚举中两个枚举定义的枚举量可能发生冲突
(2) 声明格式:enum class 枚举类型{枚举量};可以用struct代替class
(3) 无论如何该方式都要用枚举名来限定枚举量,格式枚举名::枚举量
(4) 枚举的底层类型默认为int,C++11可以指定底层类型,语法:
enum class:类型 枚举名{枚举量};
(5) 作用域内枚举不能隐式转换为整型。
五、运算符重载:使用运算符函数,属于一种形式的C++多态
格式operator 运算符(形参列表);
只能重载有效的C++运算符,不能虚构。
当其作为一个成员函数时,运算符前是是调用对象,运算符后是运算符函数的参数。
如:sid+sara;种sid对象所属的类有重载+,则它等价于sid.operator+(sara);
拓展:将两个以上的对象相加:t4=t1+t2+t3; +是从左向右的运算符
此时该调用是如下的形式:t4=t1.operator+(t2.operator+(t3));所以该操作可行。
重载运算符时至少有一个操作数是用户定义的类型
使用时不违反原句法规则,如不改变操作数个数
不能改变运算符优先级,不能创建新的运算符
可以重载的运算符:(不一定要通过成员函数重载运算符)
*
只能通过成员函数重载的运算符:
= 赋值()括号[]中括号->间接成员访问运算符
- 在类外使用运算符函数时,要有多个参数,运算符前后都会作为参数。
如:c=a+b;语句种a和b和c均为对象,在当前的作用域中(不在类中)定义的operator+()接受两个参数。
六、友元
- 友元包括:友元函数、友元类、友元成员函数
note:为什么需要友元
在为类重载二元运算符时,由于左侧操作数是调用对象,若左侧为内置的类型右侧为用户定义类型,则无法进行调用。非成员函数不由对象调用,它使用的所有值都是显式调用的。非成员运算符函数的第一个参数为运算符左侧操作数,第二个参数为运算符右侧操作数。
通过让函数成为类的友元,可以赋予其与类成员函数相同的访问权限,它不是成员函数(不作为类的一部分),不可以成员运算符调用,但可以访问私有组件。
创建:在类中声明原型并在原型前加上friend,定义时无需加上friend,且无需使用类的限定方法。
非成员友元函数的应用:为类重载运算符时将非类的项作为其第一个操作数时反转操作数顺序。
▲△实用方法:重载5. ostream重载的6. 类的继承属性可以让ostream&指向ostream和ofstream
cerr是ostream对象,将输出发送到标准错误流(默认为显示器)****
调用时应使用cout本身(或其他ostream及其子类的对象),所以该运算符函数要接受引用,且要保证可以像cout一样可以连续调用,则万用模板如下:****
**ostream& operator9. 状态成员:描述对象所处的状态,一般用枚举来表示
cmath数学库:atan(y,x)反正切函数(已知正切值求角度)、sin(A)正弦、cos(A)余弦、atan2(y,x)反正切函数
cstdlib通用库:rand()函数返回一个从0到某个值之间的随机数(伪随机数)
srand()覆盖默认的种子值,通常用当前时间来覆盖。
- ctime时间库:time()函数接收一个time_t的地址,将时间放到该变量里,并返回它。
NULL宏表示空指针(C++11有关键字nullptr表示空指针)
随机数种子的生成srand(time(NULL));
随机数的生成rand()%需要的最大随机数加一;
如:生成0~7的随机数a=rand()%8;
七、类的自动转换和强制类型转换
C++会自动转换兼容的类型,不兼容的类型可以使用强制类型转换。
接受一个参数的构造函数为类型转换提供了途径,如Stonewt Thing=114514.1;此时会调用Stonewt::Stonewt(double)进行隐式转换。
将用户定义类型的对象初始化为其他类型、将其他类型的值赋给用户定义类型的对象、将其他类型的值传递给接受用户定义类型参数的函数、返回用户定义类型的函数试图返回其他类型时可进行隐式转换。若用户定义类型CLA提供了转换T类型到CLA类型的构造函数,且AT是可以转换为T类型的类型,当且仅当转换不存在二义性时才会将AT转为T再转为CLA(进行二步转换)。
如:Stonewt a=10;此时调用Stonewt::Stonewt(double),若声明了Stonewt::Stonewt(long),则转换存在二义性,编译器会拒绝这样的转换。(因为int可被转为long或double)
防止意外转换:在前面加上explicit,关闭隐式转换,此时可以使用强制类型转换
转换函数:格式operator 类型();
·构造函数只用于从某种类型到类的转换。
·要把类转换为某种类型需要用转换函数,它属于特殊的运算符函数。
·该函数必须为类方法,不能指定返回类型,不能有参数。
·在上下文无法指明隐式转换的类型且转换存在二义性是,编译器无法接受这些语句。
如:cout6. 转换函数与友元函数:
定义转换函数Thing::operator double()后,声明double值a,Thing类对象thing,Thing类有构造函数Thing::Thing(double),则此时执行b=a+thing;时并不会把a转为Thing类型,而是把thing转为double再相加。若此时定义友元函数可以把a作为参数,既可以让其被隐式转换,也可以让其直接作为参数传递。
八、动态内存分配和类
原书实例:StringBad类
strngbad.h
//* strngbad.h – flawed string class definition*
#include ******
#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 // 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 ******
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.
1: “Celery Stalks at Midnight” object created
2: “Lettuce Prey” object created
3: “Spinach Leaves Bowl for Dollars” object created
headline1: Celery Stalks at Midnight
headline2: Lettuce Prey
sports: Spinach Leaves Bowl for Dollars
String passed by reference:
"Celery Stalks at Midnight"
headline1: Celery Stalks at Midnight
String passed by value:
"Lettuce Prey"
“Lettuce Prey” object deleted, 2 left
headline2: @x0019
Initialize one object to another:
sailor: Spinach Leaves Bowl for Dollars
Assign one object to another:
3: “C++” default object created
knot: Celery Stalks at Midnight
Exiting the block.
“Celery Stalks at Midnight” object deleted, 2 left
“Spinach Leaves Bowl for Dollars” object deleted, 1 left
“0__x0019_” object deleted, 0 left
该程序修改了已释放的内存导致了Windows Defender报毒。
·按值传递、用一个对象初始化初始化另一个对象可能会将构造函数调用,对象过期后被析构,从而导致指针内容被删除(同一个指针被多个对象(包括临时对象)引用)
▲▲·C++中的特殊成员函数:若没有定义默认提供
默认构造函数(没有定义构造函数时提供)
默认析构函数****
复制构造函数****
赋值运算符****
地址运算符****
上述程序的问题由隐式赋值运算符和隐式赋值构造函数引起
·默认构造函数
不接受任何参数,不执行任何操作。带参数的构造函数也可以作为默认构造函数,需要所有参数都有默认值。只允许由一个默认构造函数。
·复制构造函数原型类名(const 类名&);
用途:将一个对象复制到新创建的对象中(用于初始化过程和按值传递,不是常规赋值操作)
调用条件
①新建一个对象并初始化为现有对象
当程序生成对象副本时
②函数按值传递
③返回对象
④编译器使用临时对象
功能:逐个复制非静态成员(潜复制,按值复制),如果该成员为类对象,调用其所属类的复制构造函数
使用时应当进行自定义并使用深度复制。
深度复制:将数据全新地拷贝到其他地方,并将旧的指针(引用)进行更新,防止对旧数据进行误操作。在动态内存分配类中,这可以防止数据被篡改。****
·默认赋值运算符:原型:
类名& 类名::operator =(const 类名&);
使用:将已有的对象赋给另外的对象,初始化并不一定使用。
赋值运算符的隐式实现也逐个赋值成员,进行浅复制。如果该成员为类对象,调用其所属类的赋值运算符。赋值的实现:释放目标对象以前分配的数据、避免自己给自己赋值、返回指向调用对象的引用。
·静态成员函数:声明包含static,不能通过对象调用,无this,若该成员函数公有,在类外要用限定名访问。静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。
·▲中括号的重载
通常C++的二元运算符(带两个操作数的运算符)位于两个操作数之间。中括号运算符一个操作数位于左中括号前面,另一个操作数位于中括号内。
重载时可以传入一个下标访问对象的数据,同时为了方便赋值,需要返回引用。如:
char& String::operator(int i);
若声明了const对象,以上的代码将无法使用。重载时C++将区分常量与非常量函数的特征标,则可以提供供const对象使用的版本,如:const char& String::operator[](int i)const;
·在构造函数使用new的注意事项
->new与delete配套使用,new[]与delete[]配套使用
->应定义复制构造函数和赋值运算符,以进行深度复制
->包含类对象成员的类的逐成员复制时,将会调用该成员所属类的复制构造函数进行复制。类中类对象成员进行赋值操作时调用赋值运算符。
·返回对象
->返回指向const对象的引用
引用指向的对象应该在调用函数前存在,不能指向函数结束后被删除的变量。
形参出现了const,注意返回时使用const
->返回指向非const对象的引用
重载赋值运算符时要确保能够连续赋值(因为非const可转为const,const不可转为非const)
->返回对象:
对象是被调用函数中的局部变量,则不应该返回引用。因为局部变量在函数执行完毕后会调用对象的析构函数,导致变量被析构,引用指向它无法使用。
常见应用:算术运算符函数
->返回const对象
以上的算术运算符函数由于返回对象,调用了复制构造函数创造了临时对象表示返回值。则此时可能会使如force1+force2=force3的语句成立(3个变量均为对象,所属类定义了operator+),当类有定义operator==()时,少写一个=便会导致意外。
->总结
返回局部对象应返回对象,此时会使用复制构造函数生成返回的对象.
返回没有公有复制构造函数的类(如ostream),必须返回指向对象的引用
·使用指向对象的指针:使用new初始化对象:
若Class_name是类,value的类型为Type_name,则
Class_name * pclass=new Class_name(value);
调用如下构造函数:Class_name(Type_name);
不存在二义性时发生由原型匹配导致的转换。下面的初始化使用默认构造函数:
Class_name *ptr=new Class_name;
·析构函数调用条件
动态变量:当执行完定义该对象的程序块时,将调用该对象的析构函数。
静态变量:程序结束时调用析构函数
new创建的对象:显式使用delete删除对象时调用。
·定位new与类对象
- 定位new运算符分配空间是根据指定的地址来分配的,若两次分配都用相同的地址分配,会导致第二次创建的对象覆盖第一次创建的对象。
解决:向后偏移相应的内存单元个数。
注:1个内存单元对应1Byte,char类型占用空间正好为1Byte.
- 定位new运算符不能保证对象析构函数的调用,需要显式调用!
警告:必须自行负责管理定位new运算符分配的对象,显式调用析构函数!!!!!****
九、抽象数据类型(Abstract Data Type,ADT)
·基本常识
栈:在同一段添加和删除,后进先出(LIFO,last-in,first-out)
队列:在队尾添加项目,在队首删除项目,先进先出(FIFO,first-in,first-out)
链表:链表由节点序列构成。每一个节点种都包含要保存到链表的信息和指向下一个节点的指针。链表节点结构示例:(这是一个单向链表)
struct Node
{
Item item;
struct Node *next;
}
单向链表的每个节点都只包含一个指向其他节点的指针,知道第一个节点的地址就可以知道每个节点的地址。最后一个节点指向的地址会被设为NULL(0),指出后面没有节点。
1.嵌套类(结构)
嵌套类的作用域为类,若Node结构在Queue的公共部分声明,则在类外面可以声明Queue::Node类型的类。
▲2.成员初始化列表
·调用构造函数时,对象在括号中的代码执行之前被创建
·const成员在创建对象时必须初始化,成员初始化列表可以在创建对象时初始化成员。
·语法:
c_name::c_name(形参列表):成员0(值0),成员1(值1),……
每个初始化器由逗号分隔,若数据成员的名称为member,要初始化为val,则初始化器为member(val)
初始化的值可以是常量或构造函数参数列表的参数,并且可以初始化const成员。
·注意:只有构造函数可以使用该语法,对于const和引用成员必须使用该语法初始化
·数据成员被初始化的顺序与他们出现在类声明中的顺序相同,与初始化器的排列顺序无关。
3.(C++11)类内初始化
class A
{
int mem1=10;
const int mem2=20;
};
与在构造函数中使用成员初始化列表等价。实际的列表可以覆盖这些默认值。
A::A(int n):mem1(n);
4.如果不想将类对象进行复制,可以定义伪私有方法,即把复制构造函数和赋值运算符声明为私有。可以避免自动生成的默认方法定义,且不能被广泛使用。
·RAND_MAX是rand()函数可返回的最大值,0是最小值。
十、类的公有继承、is-a
·基本概念
1.定义:从已有的类派生出新的类,派生类继承了原有类(基类)的特征。这种方法叫做类继承。派生类也叫子类,基类也叫父类。
2.公有派生
声明:
class 派生类:public 公有基类
{
......
}
特点:
·派生类对象包含基类对象
·基类的公有成员将成为派生类的公有成员****
·基类的私有部分会成为基类的一部分,但只能通过基类的公有和保护方法访问。****
->派生类继承了基类的实现,派生类对象可以使用基类的方法
使用:
·派生类需要自己的构造函数,需要添加额外的数据成员和成员函数。
·派生类的构造函数需要给继承的成员提供数据。
①.派生类的构造函数
·创建派生类对象时,程序先创建基类对象
·基类对象在程序进入派生类构造函数时前创建,派生类构造函数函数必须通过成员初始化列表将基类信息传递给基类构造函数.
如:A是OBJ的子类,则要初始化OBJ中定义的成员时,应这样写:A(…):OBJ(…){…}
初始化器中要调用父类的构造函数,如果不在初始化器中调用父类构造函数,则调用父类的默认构造函数。
注意:必须首先创建基类对象,不调用基类的构造函数时程序将使用默认构造函数。
·派生类构造函数应初始化新的成员。
*·除虚基类以外类只能将值传递回相邻基类的构造函数
②派生类的析构函数
派生类对象过期时,先调用派生类的析构函数,再调用基类的构造函数。
③指针、引用的特殊关系
基类指针可以在不进行显式类型转换的情况下指向派生类对象****
基类引用可以在不进行显式类型转换的情况下指向派生类对象****
例子:
RatedPlayer rplayer1(1140, “Mallory”, “Duck”, true);
TableTennisPlayer & rt = rplayer;
TableTennisPlayer * pt = &rplayer;
rt.Name(); // invoke Name() with reference
pt->Name(); // invoke Name() with pointer
*此处RatedPlayer是TableTennisPlayer的派生类。
基类指针或引用只能用于调用基类方法!
要注意:这是单向的,不能够将基类对象和地址赋给派生类引用和指针。
④.可以将派生类对象赋给基类对象,此时调用隐式的重载赋值运算符。因为operator=()接受一个基类的引用,可以把派生类的基类部分复制到别的对象。
3.is-a关系的继承
·C++的3种继承关系:公有继承、保护继承、私有继承
·公有继承建立is-a关系(实际上为is-a-kind-of关系).其内容如下
派生类也是一个基类对象,可对基类对象执行的任何操作,也可以对派生类执行。
·has-a关系:离散数学可能用到代数,但是不能因为这个原因将离散数学认作是代数的分支。通常只会引用代数中的方法,而不是直接纳入其体系。公有继承不建立has-a关系
·公有继承不建立is-like-a关系。继承可以在基类的基础上添加属性,但不可以删除基类的属性
·公有继承不建立is-implemented-as-a(作为……来实现)关系,如不能从数组类派生出栈类,虽然数组可以实现栈
·公有继承不建立uses-a关系
十一、多态公有继承
·若一个方法在基类和派生类中不同,方法的行为取决于调用方法的对象,则该行为称作多态。实现多态有两种方法:在派生类中重新定义基类的方法、使用虚方法
·is-a关系通常不可逆
·若基类为base_c,派生类为depend_c,方法method()在两个类中有不同的定义,则基类中的限定名为base_c::method(),派生类中的为depend_c::method()。在两个类中行为一致的方法,只需在基类中声明。
1.虚方法(使用virtual)
格式 virtual 函数原型;
(1)没有使用关键字virtual时,程序根据引用或指针类型选择方法,如
base_c b();
depend_c d();
base_c& rb=b;
base_c& rd=d;
base_c* pb=&b;
base_c* pd=&d;
rb.method();
rd.method();
当method()方法不是虚方法时,两个调用都使用base_c::method();
若使用了virtual关键字,程序根据指针或引用指向的对象的类型来选择方法,这里为rb.method()调用base_c::method(),rd.method()调用depend_c::method()
(2)经常将派生类中要重新定义的方法声明为虚方法。在基类中声明虚方法后,该方法在派生类中自动成为虚方法(亦即派生类中可以不使用virtual关键字)。
(3)基类中需声明虚析构函数,确保释放对象时,按正确的顺序调用析构函数。
(4)关键字virtual只用于类声明的方法原型中,而不是方法定义中
(5)派生类调用基类方法:若是虚的,要加限定名,使用作用域解析运算符调用,否则――将会调用自己的方法。若不是虚的,派生类没有重新定义,直接调用。
(6)需要虚析构函数的原因:若析构函数不是虚的,则只调用对应于指针类型的析构函数。若没有声明虚析构函数,则只会有基类的析构函数调用。只有在基类有虚析构函数时,才可以让对应的对象执行对应派生类的析构函数,再自动执行基类的析构函数
十二、静态联编和动态联编
1.将源代码中的函数调用解释为执行特定的函数代码块称为函数名联编(binding)。
2.在编译过程中进行联编称为静态联编(static binding,亦即早期联编,early binding)
3.虚函数定义会让使用哪个函数无法确定,须在程序运行时选择正确的虚函数,该方式为动态联编(dynamic binding,亦即晚期联编,late binding).
4.将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),向上强制转换是可传递的。相反的过程为向下强制转换(downcasting),在不使用显式类型转换时,向下强制转换是不允许的。
5.对于使用基类引用或指针作为参数的函数调用,将进行向上转换。
note:编译器对虚方法使用动态联编,静态联编是默认的联编方式。动态联编会产生其他开销,静态联编则不用其他开销,所以静态联编的效率更高。当没有虚方法时,使用静态联编更合理。
*C++的指导原则其一:不要为不使用的特性付出代价
十三、▲虚函数的工作原理
处理方法:给每个对象添加一个隐藏成员,其中保存了一个指向函数地址数组的指针。该数组称作虚函数表(virtual function table,VTBL),其中存储了该类所有虚函数的地址。在基类对象中,虚函数表仅仅包括基类的虚函数的地址,而在派生类对象中,有一个独立于基类虚函数表的虚函数表。若派生类提供虚函数的新定义,该虚函数表将保存新函数的地址,若没有重新定义,将保存函数原始版本的地址。若在派生类中定义了新的函数,则把函数地址添加到虚函数表中。
注意:无论表多大,隐藏成员只是指针,不会因为虚函数太多导致对象太大。
调用:程序查看隐藏成员,找到虚函数表,找到函数。
注意事项:
·在基类声明的虚函数在其所有的派生类中(包括从派生类中派生出来的类)都是虚的。
·构造函数不能是虚的
·析构函数在类作为基类时应声明为虚的。
·友元不能是虚函数,友元不是类成员,只有类成员才可以是虚函数。
·如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则使用最新的虚函数版本,除非基类版本的是隐藏的。
·在派生类中重新定义函数,并不会生成函数的重载版本,而是隐藏基类定义的版本。重新定义继承的方法不是重载,无论参数列表是否相同,所有同名的基类方法都被隐藏。
总结
·重新定义继承的方法,应该确保与原来的原型一致。
·如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。该特性叫做返回类型协变(covariance of return type),此时允许返回类型随类的类型变化而变化。
·如果基类声明被重载,应在派生类中重新定义所有的基类版本。若只重新定义一个版本,其他的版本被隐藏,派生类对象将无法使用它们。如果不需要修改,新定义可以只调用基类版本。
如:基类overloaded()函数有45个版本,则在派生类中定义28个后,其他的只需在函数中调用基类的版本:
class Father
{
public:
……
virtual void overloaded(int);//version 1
virtual void overloaded(double,Father&);//version 2
……
virtual Father& overloaded(int,Father[]);//version 44
virtual void overloaded();//version 45
};
class Son:public Father
…
则子类中可以将version 44的函数修改为virtual Son& overloaded(int,Father[]);
若version 45没有修改的必要,则可以这样写:
void Son::overloaded(){Father::overloaded();}
十四、保护访问控制 protected
·protected标签用来声明保护成员,语法像public和private一样
·在类外只能用公有成员访问protected成员,派生类的成员可以直接访问保护成员。
十五、抽象基类(abstract base class,ABC)
·模型:从不同的类中抽象出共性,让一个类拥有这些特性,并使其作为基类。
如:正方形、矩形、菱形和平行四边形都是四边形,则可以定义一个类,表示四边形。由于特殊四边形可以有自己不同的特性,矩形的对角线相同,但是其他四边形不一定,就需要分别指明两条对角线长,这就造成了一些冗余。此时定义抽象基类可以解决一些问题。
·在抽象基类中,**纯虚函数(pure virtual function)**提供未实现的函数。纯虚函数原型结尾处为=0。声明格式如下:
virtual 返回类型 函数名(形参列表)=0;
·一个抽象基类至少有一个纯虚函数,不能创建抽象基类的对象。
·可以给纯虚函数提供定义
·从抽象基类派生的类叫做具体类(concreat class)。
十六、继承和动态内存分配
1.派生类不使用new
不需要为为派生类定义显示析构函数、复制构造函数、赋值运算符
·没有定义析构函数时,默认提供一个析构函数,在执行完自身的代码之后调用基类的析构函数。
·派生类的默认复制构造函数、默认赋值运算符会调用基类的复制构造函数、赋值运算符来操作对象的基类部分。
2.派生类使用new
必需要为为派生类定义显示析构函数、复制构造函数、赋值运算符
·派生类析构函数在执行完自己的代码后自动执行基类的析构函数析构对象的基类部分
·派生类复制构造函数只能访问派生类成员,要显式地用初始化成员列表调用基类的复制构造函数来处理对象的基类部分数据。
·派生类赋值运算符要显式调用基类赋值运算符(通过作用域解析运算符和函数表示法完成)
note:友元与继承
当基类和派生类都使用了友元时,派生类无法访问基类的私有成员。此时可以借助基类的友元,但是要注意类型转换。
第四部分 第一小节 总结****
技术复习
创建一个自动内存分配类的步骤
基本框架的设计编写
增加实用常用接口
增加运算符重载
编写复制构造函数、默认赋值运算符、修改特殊成员函数。
调试对象的使用和返回等行为
*考虑定位new和其他因素
重载>运算符
ostream &operator>(istream &is,class_name&obj)
{
is>>….
return is;
}
类型转换:
explicit可以防止隐式转换
从其他到类:构造函数转换 c_name(type_name value);
从类到其他:operator type_name();
构造函数使用new:
·在构造函数中使用new在析构函数中就要使用delete!
·new和delete配对使用,new[]和delete[]配对使用
·构造函数应当把指针初始化为new分配的地址或空指针(nullptr,NULL,(void*)0),不能让析构函数的delete释放野指针。
·定义一个分配内存的复制构造函数,使得程序能够将一个类对象初始化为另一个类对象。
原型:className(const className&)
·重载赋值运算符(此处假定用new[]分配空间,c_pointer是c_name的成员,其类型为指向type_name的指针)
c_name& c_name::operator=(const c_name& cn)
{
if(this==&cn)
return *this;//防止自己给自己赋值
delete[] c_pointer;
//销毁旧数据,复制新数据
c_pointer=new type_name[size];
//拷贝其他数据
…
return *this;
}
1.编译器生成的成员函数(特殊成员函数)
·默认构造函数
默认构造函数没有参数或所有的参数都有默认值。
没有定义构造函数时,编译器定义默认构造函数。
默认构造函数会自动调用基类的默认构造函数以及本身是对象的成员所属类的默认构造函数
派生类构造函数的成员初始化列表中没有显式调用基类的构造函数时,编译器使用基类的默认构造函数构造对象的基类部分
在定义了某种构造函数后,编译器将不会定义默认构造函数
·复制构造函数
使用构造函数的情况:
->将新对象初始化为同类对象
->按值将对象传递给函数
->函数按值返回对象
->编译器生成临时对象
在程序没有使用复制构造函数时,编译器提供原型,不提供函数定义
在使用时,新对象的每个成员都被初始化为原始对象相应成员的值。
如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数
·赋值运算符
默认赋值为成员赋值。若成员为类对象,会使用所属类的赋值运算符。
编译器不生成将一种类型赋给另一个类型的赋值运算符。
2.其他类方法
·构造函数
构造函数创建新的对象,其他类方法由对象调用,所以构造函数不可以继承。
·析构函数
若一个类要作为基类,则析构函数应声明为虚函数
·转换
->从参数类型到类类型:调用转换构造函数
在原型中使用explicit可以禁止隐式转换
->将类类型转换为其他类型:转换函数(如 operator 类型() )
转换函数可以是没有参数的类成员函数和返回类型为目标类型的成员函数
[C++11]在转换函数原型中使用explicit可以禁止隐式转换
·按值、按引用传递
通常应按引用传递,可以提高效率。按值传递对象会产生临时对象,调用复制构造函数和析构函数。在函数不修改对象时,应声明为const
·返回对象、返回引用
返回对象时,会生成副本,较为耗时。返回应用节省时间和内存。
函数不能返回在函数中创建的对象的引用。函数在返回函数中创建的临时变量时,要返回对象而不是引用。
函数在返回通过引用或指针传递的对象时,应返回引用。
·▲使用const
const用来保证方法不修改参数、不修改调用它的对象、引用或指针返回值不能用于修改数据。只有在其他的函数保证不会修改参数时,才能将参数声明为指向const的指针和引用的函数的参数传递给其他的函数。
3.公有继承的考虑
·不可继承的有构造函数、析构函数、赋值运算符。当派生类构造函数没有使用成员初始化列表调用基类构造函数,将使用基类的默认构造函数。
·赋值运算符
当程序将一个对象付给同类的另一个对象,它将自动为这个类提供一个赋值运算符。
该运算符的默认版本采用成员赋值
如果对象属于派生类,编译器将使用基类赋值运算符来处理派生类对象中的基类部分的赋值
如果成员为另一个对象的类的对象,则使用其所属类的赋值运算符。
可以将派生类对象赋给基类对象
小结
下表中op=表示+=、*=等格式的赋值运算符
第四部分 OOP和类设计、类的应用 第二小节
零、1.valarray简介
·头文件 valarray
·名称空间 std
·功能:用于处理数值(或与数值类似)的类,是一个数组。
·初始化方式:该类为模板类,需使用以下语法:
valarray 对象名;
·方法:
(1)默认构造函数:构造长度为0的valarray数组
(2)其他构造函数:
valarray::valarray(size_t 长度) 构造指定长度的数组
valarray::valarray(size_t 长度,T 初始值) 构造指定长度的数组,并将初始值设为指定的值
valarray::valarray(数组名,指定长度) 取指定数组的前面几个元素,取的长度为指定长度
valarray 对象名={初始化列表}; //C++11支持
(3)常用方法
size()返回元素个数(长度)
sum()求和并返回
max()最大值
min()最小值
一、包含成员对象的类
1.不继承接口是has-a关系的一个特性
2.对于继承的对象,在使用成员初始化列表中使用类名调用特定的基类构造函数。对于成员对象则使用对象名来调用所属类的构造函数。
3.在构建其他对象之前先构建继承对象的所有成员对象。若省略初始化列表,则会使用成员对象所属类的默认构造函数。
*4.初始化顺序:在初始化列表中,先被声明的先初始化
二、私有继承(可用于实现has-a关系)
1.使用私有继承时,基类的公有成员和保护成员都变为派生类的私有成员
2.包含和私有继承的区别:(被添加的对象叫子对象(subobject))
包含:将对象作为一个有名称的对象加入类中
私有继承:将对象作为一个未命名的继承对象添加到类中。
3.私有继承的声明:使用关键字private(不指出关键字时默认为private)
class 类名:private 基类{ };
4.可以使用多个基类,该方式叫做多重继承(multiple inheritance,MI)
class 类名:继承方式 基类#1,继承方式 基类#2{ };
5.初始化基类组件
与包含相比,初始化列表中使用类名来初始化子对象、
6访问基类的方法;使用类名和作用域解析运算符
7.访问基类对象:使用强制类型转换
8.访问基类的友元函数;使用强制类型转换
9.在私有继承中,未进行显式类型转换的派生类引用或指针,无法赋值给基类的引用或指针
10.私有继承和包含的选用:通常使用包含,若需要访问私有成员或重定义虚函数用保护继承。
11.保护继承:使用关键字protected.私有继承的变体,基类的公有和保护成员都将成为派生类的保护成员。
12.继承总结:如下表.
其中隐式向上转换意味着无需进行显示类型转换,就可以将基类指针或引用指向派生类对象。
13.使用using重定义访问权限:在公有部分使用:using 函数限定名;来达到效果。
如:using std::valarray::min;
三、多重继承
1.MI的主要问题:从基类中继承同名方法、继承同一祖先类的多个实例
2.将继承同一祖先的派生类的引用(指针)赋给祖先类引用(指针)时,由于包含多个实例,便会导致二义性。
e.g.Student和Worker都派生自job类,Student_worker派生自Student和Worker,则将Student_worker赋给job类时要这样操作:
job *a=(*Student) &Student_worker;//将Student子对象的job对象的地址赋给a
job *b=(*Worker) &Student_worker;//将Worker子对象的job对象的地址赋给b
3.虚基类:
·声明:将关键字virtual与public放在一起,不分顺序
·作用:使多个有相同基类派生的类派生出的对象只有一个相同的基类
·派生类的构造函数的使用
对于非虚基类,唯一可以出现在初始化列表的构造函数为基类构造函数。
在基类是虚的时候,不能通过中间类将信息传给基类。若在初始化列表中没有将信息传给基类,则使用基类的默认构造函数。
e.g.Student和Worker虚继承自job,Student_worker继承自Student和Worker,则在Student_worker类的构造函数中可以直接调用job的构造函数。
注意:如果类有间接的虚基类,除非只要使用虚基类的默认构造函数,否则应该显式调用该虚基类默认构造函数。即有间接的虚基类的类的构造函数应该用以下方式编写:
类名(形参列表): 基类(…),…,虚基类(…),…,成员(…),…
4.类方法的冲突与解决
(1)对于单继承,没有重新定义的函数使用最近祖先的定义
(2)若多继承中最近多个祖先都有同名函数,在调用时会出现二义性。
(3)指定要调用的同名方法;
对象.类名::方法(实参列表);
在类的内部,可以使用作用域解析运算符调用基类的方法。
5.混合使用虚基类和非虚基类
当类通过虚途径和非虚途径继承某个特定的基类时,该类会包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。
6.使用非虚基时,若类从不同的类继承了多个同名成员,则在使用时不使用类名限定将导致二义性。
7.虚二义性规则:在使用虚基类时,若某个名称优先于其他所有名称,则使用时,即使不使用限定名,也不会导致二义性。(派生类中的名称优先于直接或间接祖先类中的相同名称,虚二义性规则与访问规则无关)
四、类模板(常用于容器类)
1.定义:在类声明前的开头:
template //typename可换为class
其中的Type为类型说明符,实例化时可用具体类型替代
在编写成员函数时,也要以此为开头,并将限定名改为类型名::
2.模板说明了如何生成类和成员函数定义,模板的具体实现叫做实例化或具体化。不可将模板成员函数放在独立的实现文件中。(不是函数,不能单独编译,需与实例化请求搭配使用)
3.使用:类名 对象名(构造函数实参列表);
必须提供类型参数列表
4.模板的非类型(表达式参数):如template
其中的int n为表达式参数。表达式参数可以是整型、枚举、引用或指针。
模板代码不可修改参数值,也无法取地址(这是右值)
5.默认类型模板参数
类模板可以为类型参数设置默认值,即:(defaultType1为默认值)
template
6.模板的具体化
(1)具体化:包括显式实例化、隐式实例化、显式具体化
(2)隐式实例化:创建模板类对象
(3)显式实例化:让编译器显式生成实例(即使不用),格式:
template class 类名
(4)显式具体化:为特定类型专门定义,如果该类型需要特殊处理要使用。****
格式:template<> class 类型名 {…};
(5)部分具体化:给部分类型参数指定类型
格式:template class 类型名{…};
7.成员模板:
成员函数可以是模板函数,当类为模板时,编写函数实现就要出现两层template,不能合在一起。
8.将模板用作参数:举例:
template class Thing> class Crab
其中模板参数是template class Thing,template class是类型,Thing为参数。Thing用于匹配一个模板类,模板参数的形式应该与指定的一致。
在类Crab中,若使用了Thing,则会被替换为Type.
9.模板的静态成员:每个实例化各自有一个静态成员
10.模板别名(C++11)
使用格式 using 别名=模板;//也可以用于非模板
使用实例:
template
using arrtype=std::array;//arrtype等价于std::array
五、友元:允许访问某一类私有成员的一个实体
分类:
·友元函数(允许访问类私有成员的外部函数,通常用于运算符函数)
·友元类
·友元成员(允许访问私有成员的外部类的成员函数)
1.模板类和友元:分类
·非模板友元
·约束模板友元:友元的类型取决于类被实例化的类型
·非约束模板友元
(1)模板类的非模板友元
在模板类中将一个常规函数声明为友元,则它将成为类模板所有实例化的友元
在友元中若提供了模板类参数,如:
template
class HasFriend
{
friend void report(HasFriend &);
…
};
此时在编写report的实现时,必须为特定类型编写。
(2)模板类的约束模板友元函数:定义步骤:
①在类声明前声明模板函数
②在函数中将模板声明为友元:需要在函数名后用<>指明模板具体化
注意:若友元函数没有参数,需要在<>指出其类型,否则编译器无法推断。
对于模板类特定的实例化,会唯一对应一个约束模板友元函数的实例。
③定义友元函数。
(3)非约束模板友元:
在类内声明模板,每个函数具体化都是每个类的友元。
2.友元类的声明:在类的声明中使用 friend class 友元类;
友元声明可以在公有、私有或保护部分,位置任意。
3.友元成员函数:让另一个类的特定成员函数成为友元,须在类中添加如下声明:
friend 函数原型;//注意函数名要使用限定名。
4.避免类的循环依赖:使用前向声明class 类名;
让整个类成为友元不需要前向声明。
5.其他友元关系:可以让两个类互为友元
6.一个函数可以是多个类共同的友元
六、嵌套类:在另一个类中定义的类
·在被包含的类外的访问控制:与普通成员一致。
·嵌套类在对自生的访问控制是独立的。包含它的类不能直接访问其私有部分,除非是友元。
·公有嵌套类的访问:所在类::嵌套类
这种方式可以推广到多层公有嵌套类。
注:在C++中,结构被进行了增强。结构可以看作一个类,可以拥有成员函数。其和class的唯一区别在于默认访问权限是public.
七、异常
·定义:程序遇到运行时错误而无法继续正常运行。
·异常处理机制:在出现异常时,将控制权从程序的一个部分转移到另一个部分。过程如下:
①抛出异常②捕获异常③处理异常
1.抛出异常:throw 对象;//可以是字符串、数字、各种对象来表示异常
throw;//可以重新抛出原有的异常
2.异常的捕获和处理:
try
{
可能会抛出异常的代码
}
catch(异常对象)
{
处理异常
}
示例:
double div(double x,double y){if(y==0) throw “Divided by zero”;return x/y;}
try{cout(类对象指针或引用)
若能安全转换,返回对象的地址(引用),否则返回空指针(转换引用时引发异常bad_cast,由exception派生)
·通常如果指向的对象(pt)类型为Type或者从Type派生而来的类型,则以下的表达式将指针pt转换为Type类型。
注意:如果在大量的if else中使用了typeid,则要考虑改为虚函数和dynamic_cast
九、类型转换运算符
1.const_cast:改变值为const或volatile,除了const或volatile特征不同外,其他特征要相同,否则转换出错
·可以用于删除对象的const属性,但修改const的结果不确定。当调用函数的形参不是const,实参是const时,若函数不修改参数,就需要去除const让函数接受参数。
·用法:变量=const_cast(const对象);
·示例:const int* pt=arr;int *pc=const_cast(pt);
2.static_cast
·编译器支持的隐式转换,比较安全,若不能隐式转换则出错
·用法:static 对象
3.reinterpret_cast
·危险的转换,不允许删除const
·不允许的转换:不能将指针转为更小的整数浮点型,不能将函数指针转为数据指针。
·用法:reinterpret_cast 对象
4.dynamic_cast:在类层次中向上转换