运算符重载
运算符重载
- 将运算符看作是一种特殊的函数(操作数是函数的参数,运算结果是函数的返回值)
- 运算符重载现象(移位运算符>>和<<,用作流输入和输出运算符)
重载的运算符
- 定义重载运算符和定义普通函数类似(函数名由关键字operator 和其后要定义的运算符组成:operator=, operator<<, operator+)
- 返回类型:运算结果的类型
- 参数表:提供参与运算的操作数(参数个数取决于运算符的操作数个数和运算符函数是成员函数还是非成员函数)
- 函数体:进行运算,返回运算结果,即表达式的值
类的成员运算符函数
- this 指向的对象被作为运算符的第一个操作数(左)
- 一元运算符函数不需要提供参数
- 二元运算符提供一个参数作为右操作数
非成员运算符函数
- 一元运算符要提供一个类类型的参数
- 二元运算符需要提供两个参数分别作为左、右操作数,其中至少一个参数必须是类类型的(通常声明为类的友元,以便访问私有数据成员)
类X的二元运算符“+”(算术加)
//X的成员运算符函数operator+()类外定义
X X::operator+(const X& right){...}
//X的非成员运算符函数operator+()
X operator+(const X& left, const X& right){...}
类X的一元运算符“!”(逻辑非)
//X的成员运算符函数operator!()类外定义
bool X::operator!(){...}
//X的非成员运算符函数operator!()
bool operator!(const X& oprand){...}
运算符函数
- 运算符函数只有在类类型的对象参与运算时才起作用(当运算符作用于内置类型的运算对象时,不会改变该运算符原来的含义)
- 将运算符作用于类型正确的实参时,会引起重载运算符的调用
- 只有至少一个操作数是用户自定义类型时,才可能调用重载的运算符
- 运算符重载不会改变内置类型的表达式中运算符的含义
常用的运算符重载
- 如果类执行I/O 操作,可以定义移位运算符>>和<<
- 如果类的某个操作是检查相等性,则定义operator==;这时候通常也应该有operator!=
- 如果类包含内在的单序比较操作,则定义operator<;此时也应该有其他关系操作
注:重载运算符的返回类型通常情况下应该与内置版本的返回类型兼容
定义运算符函数时选择成员还是非成员?
- 赋值(=)、下标([])、函数调用(())和成员函数访问箭头(->)运算符必须是成员函数(赋值运算符只能用成员函数重载,复合赋值运算符可以用成员或非成员重载)
- 改变对象状态的运算符或者与给定类型密切相关的运算符,如自增、自减和解引用运算符,通常应该是成员
- 具有对称性的运算符可能转换两个操作数中的任何一个,如算术、关系和位运算符等,通常应该是非成员函数
- 重载移位运算符<<和>>用于对象的I/O 操作时,左操作数是标准库流对象,右操作数才是类类型的对象,只能用非成员函数
输入输出运算符的函数原型
istream& operator>>(istream&, type&);
ostream& operator<<(ostream&, const type&);
组合与继承
对象成员与组合关系
将一个类的对象作为另一个类的成员,被称作组合或包含
对象成员的初始化
- 创建包含对象成员的组合对象时,会执行成员类的构造函数初始化对象成员,成员对象的初始化使用初始化列表语法
- 当组合对象被撤销时,会执行其析构函数,成员对象的析构函数也会被执行,析构函数的执行次序和构造函数相反
成员对象的初始化表
- 类中如果包含多个对象成员,在初始化列表中将它们用逗号隔开
- 成员初始化的次序和成员声明的次序相同,并不考虑它们在初始化列表中的排列顺序
继承
- 继承是面向对象的核心特征之一。面向对象的核心特征封装,继承,多态性
- 继承:在已有类的基础上创建新类的过程。 一个 B 类继承A类,或称从类 A 派生类 B
-
被继承的已有类称为基类;继承得到的新类称为派生类;派生类可以再被继承,这样构成的层次结构称为继承层次
-
类继承关系的语法形式
class 派生类名 : 基类名表
{
数据成员和成员函数声明
}; -
访问控制 表示派生类对基类的继承方式,使用关键字:
public 公有继承
private 私有继承
protected 保护继承
访问控制
不论以何种方式继承基类,派生类都不能直接使用基类的私有成员
派生类的生成过程
派生类的生成过程经历了三个步骤:
●吸收基类成员(全部吸收(构造、析构除外),但不一定可见)
●改造基类成员(通过在派生类中定义同名成员(包括成员函数和数据成员)来屏蔽(隐藏)在派生类中不起作用的部分基类成员)
●添加派生类新成员
重名成员
- 派生类定义了与基类同名的成员,在派生类中访问同名成员时屏蔽(hide)了基类的同名成员
- 在派生类中使用基类的同名成员,显式地使用类名限定符:类名 :: 成员
#include<iostream>
using namespace std ;
class A
{ public:
int a1, a2 ;
A( int i1=0, int i2=0 ) { a1 = i1; a2 = i2; }
void print()
{ cout << "a1=" << a1 << '\t' << "a2=" << a2 << endl ; }
};
class B : public A
{ public:
int b1, b2 ;
B( int j1=1, int j2=1 ) { b1 = j1; b2 = j2; }
void print() //定义同名函数
{ cout << "b1=" << b1 << '\t' << "b2=" << b2 << endl ; }
void printAB()
{ A::print() ; //派生类对象调用基类版本同名成员函数
print() ; //派生类对象调用自身的成员函数
}
};
int main()
{ B b ; b.A::print(); b.printAB(); }
派生类中访问静态成员
- 基类定义的静态成员,将被所有派生类共享(基类和派生类共享基类中的静态成员)
- 根据静态成员自身的访问特性和派生类的继承方式,在类层次体系中具有不同的访问性质
- 派生类中访问静态成员,用以下形式显式说明:类名 :: 成员 或通过对象访问 对象名 . 成员
基类的初始化
- 在创建派生类对象时用指定参数调用基类的构造函数来初始化派生类继承基类的数据
- 派生类构造函数声明为: 派生类构造函数 ( 变元表 ) : 基类 ( 变元表 ) , 对象成员1( 变元表 ) … 对象成员n ( 变元表 ) ;
- 构造函数执行顺序:基类 ——> 对象成员——> 派生类
//调用构造函数顺序测试,构造函数无参数
#include<iostream>
using namespace std ;
class Base
{ public : Base ( ) { cout << "Base created.\n" ; }
} ;
class D_class : public Base
{ public : D_class ( ) { cout << "D_class created.\n" ; }
} ;
int main ( )
{ D_class d1 ; }
派生类构造函数和析构函数的定义规则
- 基类的构造函数和析构函数不能被继承
- 如果基类没有定义构造函数或有无参的构造函数, 派生类也可以不用定义构造函数
- 如果基类无无参的构造函数,派生类必须定义构造函数
- 如果派生类的基类也是派生类,则每个派生类只负责直接基类的构造
- 派生类是否定义析构函数与所属的基类无关
派生类构造函数的定义
在C++中,派生类构造函数的一般格式为:
派生类::派生类名(参数总表):基类名(参数表)
{
// 派生类新增成员的初始化语句
}
注意:这是基类有构造函数且含有参数时使用
派生类析构函数
(1)当派生类中不含对象成员时
●在创建派生类对象时,构造函数的执行顺序是:基类的构造函数→派生类的构造函数;
●在撤消派生类对象时,析构函数的执行顺序是:派生类的析构函数→基类的析构函数。
(2)当派生类中含有对象成员时
●在定义派生类对象时,构造函数的执行顺序:基类的构造函数→对象成员的构造函数→派生类的构造函数;
●在撤消派生类对象时,析构函数的执行顺序:派生类的析构函数→对象成员的析构函数→基类的析构函数。
#include<iostream.h>
class base {
int n;
public:
base(int a) {
cout<<"constructing base class"<<endl;
n=a; cout<<"n="<<n<<endl; }
~base(){cout<<"destructing base class"<<endl;}
};
class subs:public base {
base bobj; int m;
public:
subs(int a,int b,int c):base(a),bobj(c) {
cout<<"constructing sub cass"<<endl;
m=b; cout<<"m="<<m<<endl; }
~subs(){cout<<"destructing sub class"<<endl;}
};
void main() {
subs s(1,2,3); }
//
constructing base class
n=1
constructing base class
n=3
constructing sub cass
m=2
destructing sub class
destructing base class
destructing base class
赋值兼容的可行性
利用赋值兼容规则
- 派生类的对象可以赋给基类对象(强制类型转换)
- 派生类的对象可以初始化基类的引用
- 派生类的对象的地址可以赋给基类类型的指针
class Base{
…
};
class Derived:public Base{
…
};
- 可以用派生类对象给基类对象赋值。这样赋值的效果是,基类对象中所有数据成员都将具有派生类对象中对应数据成员的值。
- 可以用派生类对象来初始化基类的引用。
Derived d;
Base &br=d; -
可以把派生类对象的地址赋值给指向基类的指针。例如:
Derived d;
Base *bptr=&d;
//这种形式的转换,是在实际应用程序中最常见到的。 -
可以把指向派生类对象的指针赋值给指向基类对象的指针。例如:
Derived *dptr,obj; dptr=&obj;
Base *bptr=dptr;
- 在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员
赋值兼容应注意的问题
- 声明为指向基类的指针可以指向它的公有派生类的对象,但不允许指向它的私有派生类的对象。例如:
class B {…};
class D:private B {…};
B b1,*pbl;D d1;
pb1=&b1; //合法,基类B的对象b1和B类的指针
pb1=&d1; //非法,不允许将基类指针指向它的私有派生类对象
继承的应用
- 允许将一个声明为指向基类的指针指向其公有派生类对象,但是不能将一个声明为指向派生类对象的指针指向其基类的一个对象。
- 声明为指向基类对象的指针,当其指向公有派生类对象时,只能用它来直接访问派生类中从基类继承来的成员,而不能直接访问公有派生类的定义的成员。
如果多个类既共享数据也共享行为,应该让它们从一个共同基类继承而来,并在基类里定义共用的数据和操作
- 继承的目的是通过“定义能为多个派生类提供共有元素的基类”的方式编写更精简的代码。继承能够把这些共有元素集中在一个基类中,从而避免在多处出现重复的代码和数据。
虚函数与多态性
- 多态性是指一个名字,多种语义;或界面相同,多种实现。
- 重载函数是多态性的一种简单形式。
- 虚函数允许函数调用与函数体的联系在运行时才进行,称为动态联编。
- 冠以关键字 virtual 的成员函数称为虚函数
- 实现运行时多态的关键首先是要说明虚函数,另外,必须用 基类指针调用派生类的不同实现版本
- 派生类对基类中声明虚函数重新定义时,关键字virtual可以不写。
- 一般通过基类指针访问虚函数时才能体现多态性。
- 一个虚函数无论被继承多少次,保持其虚函数特性。
- 虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态函数
- 析构函数可以是虚函数,通常声明为虚函数
虚函数和基类指针
基类指针虽然获取派生类对象地址,却只能访问派生类从基类继承的成员
- 一个虚函数,在派生类层界面相同的重载函数都保持虚特性
- 虚函数必须是类的成员函数
- 虚函数可以是另一个类的友元
- 析构函数可以是虚函数,但构造函数不能是虚函数
虚函数的重载特性
- 在派生类中重载基类的虚函数要求函数名、返回类型、参数个数、参数类型和顺序完全相同
- 如果仅仅返回类型不同,C++认为是错误重载
- 如果函数原型不同,仅函数名相同,丢失虚特性
class base
{ public :
virtual void vf1 ( ) ;
virtual void vf2 ( ) ;
virtual void vf3 ( ) ;
void f ( ) ;
} ;class derived : public base
{ public :
void vf1 ( ) ; // 虚函数
void vf2 ( int ) ; // 重载,参数不同,虚特性丢失
char vf3 ( ) ; // error,仅返回类型不同
void f ( ) ; // 非虚函数重载
} ;
纯虚函数和抽象类
- 纯虚函数是一种特殊的虚函数,
- 在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
-
纯虚函数是一个在基类中说明的虚函数,在基类中没有定义, 要求任何派生类都定义自己的版本
-
纯虚函数为各派生类提供一个公共界面
-
纯虚函数说明形式: virtual 类型 函数名(参数表)= 0 ;
-
一个具有纯虚函数的基类称为抽象类。