文章目录
一、面向对象程序设计概述
二、C++基础
三、类和对象
四、类与对象的其他特性
4.1类的静态成员
1)静态数据成员
静态数据成员是类的所有对象共享的数据成员,解决了同一个类的不同对象之间数据成员和成员函数的共享问题。静态数据成员的值更新一次,所有对象都会存取更新后的值,可以提高时间效率
2)静态数据成员的定义
static 类型名 静态数据成员名;
class S{
public:
static int abort;//定义静态数据成员abort
};
(1)静态数据成员属于本类的所有对象共享,脱离于特定的类对象而存在
(2)静态数据成员同样遵守 public、private、protected 访问原则
3)静态数据成员的初始化
类型 类名::静态数据成员=初始化值;
#include<iostream>
using namespace std;
class S{
public:
static int abort;//定义静态数据成员abort
};
int S::abort = 20;
int main(){
cout<<S::abort;
return 0;
}
静态数据成员的初始化在类外,并且在对象生成之前
4.静态成员函数
定义方式:
static 返回类型 静态成员函数名(参数表);
静态成员函数定义方式和普通成员函数相同,可在类内定义也可在类外定义,类外定义时不使用static前缀
调用方式:
类名::静态成员函数名(实参表)
或
对象名.静态成员函数名(实参表)
静态成员函数用于处理静态数据成员,静态成员函数不属于某一个对象,因此没有this指针,不能访问类的默认非静态成员(包括非静态数据成员和非静态成员函数),只能访问本类中的静态成员(包括静态数据成员和静态成员函数)
class A{
int y;
static int B;
public:
static void Output(){
cout << B << endl;//合法引用
cout << y << endl;//y为非静态数据成员,不合法引用
}
};
但是,静态成员函数并非绝对不能引用本类的非静态成员,只是无法知道应该去找哪个对象,应该加上对象名和成员运算符”.“并通过对象来调用非静态成员
#include<iostream>
using namespace std;
class ST{
private:
int a;//非静态成员
static int b;//静态成员
public:
static void Point(ST c){
cout << "a=" << c.a << endl;//通过形参对象访问传递对象的值a
cout <<"b="<<b<<endl;
}
ST(int d):a(d){}//类构造函数
};
int ST::b = 100;//静态数据成员初始化
int main(){
ST m1(20), m2(50);
ST::Point(m1);//类名访问静态成员函数,传递对象m1
ST::Point(m2);//传递对象m2
return 0;
}
4.2友元
通常情况下类的私有成员需要通过类的成员函数进行访问,其他函数无法访问类的私有成员,如果都定义为公有将破坏数据的隐藏性,多次调用还会影响程序的运行效率,但友元机制使得非成员函数也可以访问类内私有成员,且能够提高程序运行效率
1)友元函数
友元函数不是当前类中的成员函数,它可以不属于任何一个类,也可以是另外一个类的成员函数,将一个函数声明为一个类的友元函数后,它就可以通过对象名访问类的公有、私有和保护成员
(1)非成员函数作为友元函数
friend 返回值类型 函数名(参数表)
#include<iostream>
using namespace std;
class A {
private:
int a1;
public:
friend int showA(A &a,int x);//不属于当前类的成员函数
};
int showA(A &a,int x) {//类外非成员函数
a.a1=x;//通过形参对象名a访问其私有成员a1
return a.a1;//返回a.a1的值
}
int main() {
A Obj;
int b=showA(Obj,200);//传递对象Obj和值200
cout<<"a.a1="<<b<<endl;
return 0;
}
(2)类的成员函数作为友元函数
friend 返回值类型 类名::函数名(参数表)
#include <iostream>
using namespace std;
class Aug;//注意声明Aug类
class Add
{ //加数类
private:
int mm;
public:
Add(int a)// Add构造函数
{
mm = a;
}
void addend(Aug &add_1);//Add类的成员函数
};
class Aug
{
private:
int mm;
public:
Aug(int a)
{
mm = a;
}
friend void Add::addend(Aug &add_1);//声明addend函数为Aug类的友元函数,能够访问该类成员
};
void Add::addend(Aug &add_1){
cout<<"Add类中mm的值="<<mm<<" "<<"Aug类中mm的值="<<add_1.mm<<endl;
//通过形参对象add_1访问num2里的值mm
}
int main()
{
Add num1(20);//定义对象num1并给mm赋值20
Aug num2(100);//定义对象num2赋值100
num1.addend(num2);//传递对象num2给形参对象add_1
return 0;
}
2)友元类
一个类可以声明为另一个类的友元类,假设A类为B类的友元类,那么A类中的每一个成员函数都可以访问B类中的任何类型的成员
friend class 类名;
#include<iostream>
using namespace std;
class A;
class B {
private:
int c;
public:
void output();
friend class A;//声明A类为B类的友元类
B(int m):c(m){}
};
void B::output() {
cout<<"class B..."<<endl;
}
class A {
private:
int a;
public:
//A类的成员函数可以访问B类的任何类型成员
void visitB(B &b);
};
void A::visitB(B &b){
cout<<b.c<<endl;//访问对象b1的成员c
b.output();//访问B类的成员函数output
}
int main() {
B b1(9);
b1.output() ;
A a1;
a1.visitB(b1);//传递对象b1
return 0;
}
4.3类的作用域和对象的生存期
1)类的作用域
类的作用域指在类的定义中由一对花括号所括起来的部分,包括数据成员和成员函数。在类的作用域中,类的成员函数可以不受限制地访问本类成员,在类外则通过对象的句柄访问本类成员,句柄是对象名、对象引用或对象指针。
示范案例如下:
#include<iostream>
using namespace std;
class Count {
public:
int x;
void calcute(int x) { //形参与数据成员同名
int y;
y=x+2;
Count::x=y*2;//数据成员名前面加上类名和作用域运算符::
}
void print() {
cout<<x<<endl;
}
};
int main() {
// Count count;
// count.calcute(3);
// count.print();
Count count;
Count *count_ptr=&count;//*count_ptr指针指向count对象的地址
Count &count_Ref=count;//count_Ref是count的引用(别名)
cout<<"使用对象"<<endl;
count.x=1;
cout<<"调用Calcute()函数前x=";
count.print();//x=1
count.calcute(count.x);
cout<<"调用Calcute()函数后x=";//x=6
count.print();
cout<<"使用引用";
count_Ref.x=2;//count.x=2
cout<<"调用Calcute()函数前x=";
count.print();//x=2
count.calcute(count_Ref.x);
cout<<"调用Calcute()函数后x=";
count_Ref.print();//x=8
}
在calcute函数中存在函数作用域,如果类中成员函数定义了与类作用域内同名的另一个变量,在函数作用域内,函数作用域内的变量将隐藏作用域内的变量,需加类名和作用域运算符::限定
2)对象的生存期
对象的生存期是指对象从被创建开始到被释放为止的时间,生存期的不同,对象可分为局部对象、全局对象、静态对象和动态对象
(1)局部对象:指定义在一个程序块或函数体内的对象,定义对象时系统自动调用构造函数,对象的生存期开始,当退出该对象所在的函数体或程序块时,调用析构函数释放该对象,对象的生存期结束
(2)全局对象:指定义在函数体外的对象,该对象的生存期自程序开始时开始,程序结束时终止
(3)静态对象:指以关键字static标识的对象,该对象的生存期从定义该对象时开始,到整个程序结束时终止
(4)动态对象:指以运算符new创建,以运算符delete释放的对象,当程序执行运算符new时创建该动态对象,对象的生存期开始,当执行运算符delete时释放该动态对象,对象的生存期结束
4.4常量类型
C++中常量的定义使用类型修饰符const,常量的值在程序运行过程中不能被改变,因此,定义或说明常量时必须对其初始化
常量包含简单数据类型常量、对象类型常量、引用类型常量、常量对象成员、数组常量和指针常量
1)常量对象
特点:对象数据成员的值在对象的整个生存期内都不能被修改
定义格式:
<类名> const <对象名>;
或
const <类名> <对象名>;
2)常量成员
包括常量成员函数和常量数据成员
1.常量成员函数
定义格式:
<返回值类型> 函数名(参数表)const;
#include<iostream>
using namespace std;
class Sample{
public:
void getValue() const;//类内声明常量成员函数
//void print(){}普通成员函数
};
//类外定义常量成员函数
void Sample::getValue() const{
cout << "hello" << endl;
}
int main(){
const Sample s;//常量对象
// s.print();常量对象不可访问非常量成员函数
s.getValue();
return 0;
}
注意:
(1)const是函数类型的一个组成部分,在类外定义时也要带const关键字
(2)常量成员函数不能调用没有用const修饰的成员函数,只能调用成员函数。但非常量成员函数不但可以调用非常量成员函数,也可以调用常量成员函数
2.常量数据成员
使用const关键字说明的数据成员为常量数据成员,若在一个类中定义了常量数据成员,那么除了构造函数外的任何函数都不能对该数据成员赋值,且构造函数对常量数据成员初始化时,只能通过初始化列表进行
#include<iostream>
using namespace std;
class A{
const int a;//常量数据成员
static const int b;//静态常量数据成员
public:
A();//构造函数
A(int i);//有参数的构造函数
void output();//输出信息
};
const int A::b = 20;//静态常量数据成员初始化同样在类外进行
A::A():a(15){}//无参构造函数利用初始化列表
A::A(int i):a(i){}//带参构造函数利用初始化列表进行
int main(){
return 0;
}
3.常量引用
在声明引用时用const修饰,那么被声明的引用就是常量引用,常量引用所引用的对象不能被改变,可用在函数的形参上,保证实参不会被改变
声明格式:
const 类型说明符 &引用名;
这里打算对常量i发生值的变化,程序报错,证明了常量引用来的实参是不可改变的右值
4.常量指针与指向常量的指针
用const修饰指针指向的变量,即修饰指针所指向变量的内容,称为指向常量的指针。
用const修饰指针,即修饰存储在指针里的地址,称为常量指针。
(1)常量指针
定义格式:
类型名 *const 指针名;
例如:
double y=2;
double *const m=&y;
double z=3;
m=&z;//错误,不能改变常量指针指向的变量
*m=3.1;//使用正确,可以变更常量指针指向的变量的值
常量指针必须有一个初始值(地址),且只能指向这个初始变量,不能再指向其他变量,指针所指向的地址不能被改变,但变量的值可以改变
(2)指向常量的指针
定义格式:
const 类型名 *指针名;
指向常量的值不能被改变,但指针所指向的地址可以改变
例如:
const double *m;
double y=3;
m=&y;
*m=3.1;//错误,不能改变指向常量的指针指向变量的内容
double z=4;
m=&z;//使用正确,可以改变指向常量的指针指向的地址
指向常量的指针和常量引用常用作函数的形参,称为常量参数,使用常量参数表面该函数不能更改某个参数所指向或所引用的对象,参数传递过程中就不需要执行拷贝构造函数,将改善程序运行效率
综合运用:
#include <iostream>
using namespace std;
int main()
{
int x = 6;
double y=5;
int *const p = &x;//定义常量指针p指向x的地址
//p = &y;常量指针地址不能改变
cout << "x的地址" << &x << endl;
cout << "x的地址" << p << endl;
const double *m;//定义指向常量的指针m
m = &y;//m指向y的地址
// *m = 3.4;指向常量的指针常量值不可变
cout << *m << endl;
return 0;
}
五、继承与派生
继承性是面向对象程序设计中最重要的机制之一,它允许程序员在保持原有类特性的基础上,进行更加具体详细的说明,实现对类的扩充,C++继承机制可以扩充完善旧的程序设计缺陷以适应新的需要,节省了程序开发时间,并且实现了代码的重用
5.1 类的继承与派生概念
继承概念:在一个已存在的类的基础上建立一个新类,实质是利用已有的数据类型定义出新的数据类型
(1)被继承的类称为基类
(2)定义出的新类称为派生类
基类与派生类的继承关系:派生类继承基类,基类所有成员是派生类的成员,派生类有自己的成员,派生类同样也可以作为基类再派生新的类,一个基类可以派生出一个或多个派生类。(图5.1)
派生类可以有以下几种变化:
(1)增加新的数据成员//性质扩展
(2)增加新的成员函数//性质扩展
(3)重新定义已有成员函数//性质约束
(4)改变现有成员的属性//性质约束
(5)改变基类成员的访问权限//性质约束
性质约束:对基类性质加以约束。性质扩展:增加派生类的特征
C++继承关系同样可分为单继承和多重继承,一个派生类只有一个直接的基类称为单继承,派生类的直接基类有两个或两个以上称为多重继承(图5.2)
继承案例如下:
#include <iostream>
using namespace std;
class Father
{ //父亲
//父亲身高、体重
int fatherheight, fatherweight;
public:
//设置父亲的身高
void setFatherHeight(int fatherheight)
{
this->fatherheight = fatherheight;
}
//设置父亲的体重
void setFatherweight(int fatherweight)
{
this->fatherweight = fatherweight;
}
//输出父亲的身高和体重
void outputfatherheightweight()
{
cout << "父亲身高" << fatherheight << endl;
cout << "父亲体重" << fatherweight << endl;
}
};
class Son:public Father{//儿子继承父亲
private:
int sonwidth, sonlength;
public:
//设置儿子的肩宽、臂长
void setSonwidth(int sonwidth){
this->sonwidth = sonwidth;
}
void setSonlength(int sonlength){
this->sonlength = sonlength;
}
//输出儿子的数据
void outputSonData(){
cout << "儿子的肩宽" << sonwidth << endl;
cout << "儿子的臂长" << sonlength << endl;
}
};
int main()
{
Son son;
//儿子继承了父亲
son.setSonwidth(60);
son.setSonlength(50);
son.setFatherHeight(90);
son.setFatherweight(600);
son.outputSonData();
son.outputfatherheightweight();
return 0;
5.2基类与派生类
1)派生类的声明
class <派生类名>:<继承方式><基类名>
class A{
public:
int a;
private:
void setA();
};
class B:public A{
public:
int ab;
private:
void setB();
};
//基类名A,派生类名B,以公有继承为方式
派生类成员成分:
(1.派生类继承除了析构和构造函数外的全部数据成员和成员函数
(2.对基类访问控制方式进行改造、定义与基类同名的成员(同名覆盖)
(3.增添新的数据成员和成员函数
2)派生类的生成过程
吸收基类成员;改造基类成员;增加派生类成员。
吸收:基类中的成员全部接收,构造函数和析构函数不被继承
改造:一是改造基类成员访问控制方式,依靠声明派生类的继承方式来控制
二是对基类数据成员或成员函数的覆盖,即在派生类中声明一个与基类数据成员或成员函数同名的成员,此时派生类的新成员将覆盖基类的同名成员,如果派生类对象调用该成员名就只能访问到派生类中声明的成员
增加: 在派生类中添加新的数据成员或成员函数
3)继承方式和派生类的访问权限
不同的继承方式,决定了基类成员在派生类中的访问属性
访问又分为两方面,一是派生类新增成员对基类继承来的成员的访问,另一方面是派生类外部(非类成员)通过派生类对象对从基类继承来的成员的访问
公有继承:子类可直接访问公有,保护需用接口函数访问,私有不可访问,基类成员在派生类中访问属性为公有和保护
#include <iostream>
using namespace std;
class father
{
public:
void Money();
protected:
void privacy();
// private:
// void Cae(){
// cout << "无权访问" << endl;
// }
};
class son : public father
{
public:
void inter(); //基类接口函数
};
void father::Money()
{
cout << "公共成员钱可直接访问" << endl;
}
void father::privacy()
{
cout << "受保护的隐私不能直接访问,需通过接口函数" << endl;
}
void son::inter()
{
privacy();
// cae();
}
int main()
{
son s1;
s1.Money();
s1.inter();
return 0;
}
私有继承:公共方法访问基类公有和保护成员,私有同样不可访问,基类成员在派生类中访问属性为私有
直接访问公共成员受阻
访问私有成员同样出现了之前的错误
正确写法√:
#include <iostream>
using namespace std;
class father
{
public:
void Money();
protected:
void privacy();
// private:
// void Cae(){
// cout << "无权访问" << endl;
// }
};
class son : private father
{
public:
void inter(); //基类接口函数
};
void father::Money()
{
cout << "公共成员钱可直接访问" << endl;
}
void father::privacy()
{
cout << "受保护的隐私不能直接访问,需通过接口函数" << endl;
}
void son::inter()
{
privacy();
Money();
//cae();
}
int main()
{
son s1;
//s1.Money();
s1.inter();
return 0;
}
保护继承:公共方法访问基类公有和保护成员,私有不可访问,基类成员在派生类中访问属性为保护
5.3派生类的构造函数和析构函数
1)派生类构造函数
<基类名>::<基类名>(基类形参,内嵌对象形参,本类形参):<基类名>(参数表)
#include<iostream>
using namespace std;
class A{
protected:
int a;//A类数据成员
public:
A(int i){//基类构造函数声明与定义
a = i;
cout << "基类数据成员初始化成功" << endl;
}
};
class A_1:public A{
public:
A_1(int i, int j);//子类构造函数声明
void Display();
private:
int a_1;//A_1类数据成员
};
A_1::A_1(int i,int j):A(i)//子类构造函数定义
{
a_1 = j;
}
void A_1::Display(){
cout <<"基类的值初始化为"<< a << endl;
cout <<"子类的值初始化为"<< a_1<< endl;
}
int main(){
A_1 a2(3,9);
a2.Display();
return 0;
}
2)派生类析构函数
当对象被删除时,执行派生类析构函数,基类析构函数同样不被继承,所以在执行派生类的析构函数时,基类的析构函数也会被调用,如未定义析构函数,系统会自动生成一个默认的析构函数
定义格式不变:
~类名(){函数体};
3)派生类构造函数和析构函数的执行顺序
1.派生类构造函数的执行顺序
(1)先调用基类构造函数
(2)再按照数据成员声明顺序,依次调用构造函数
(3)最后执行派生类构造函数的函数体
2.派生类析构函数的执行顺序
(1)先执行派生类的析构函数,清理派生类新增普通成员
(2)根据内嵌对象声明的相反顺序,依次调用析构函数,清理派生类新增对象成员
(3)最后调用基类析构函数,清理基类继承来的成员
案例:
#include <iostream>
using namespace std;
class father
{
private:
int a;
public:
father(int i)
{
a = i;
cout << "构造函数,基类a值:" << a << endl;
}
~father()
{
cout << "析构函数,基类a的值:" << a << endl;
}
};
class son : public father
{
private:
int b;
public:
son(int, int j);
~son()
{
cout << "析构函数,子类b的值:" << b << endl;
}
};
son::son(int i, int j) : father(i)
{
b = j;
cout << "构造函数,子类b的值:" << b << endl;
}
int main()
{
son s(1, 2);
return 0;
}
5.4多重继承
多重继承是指派生类具有多个基类,派生类与每一个基类的关系可以看作是一种单继承,派生类得到多个已有基类的特性
1)多重继承的声明
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,···,
{
派生类新增加的成员;
};
#include <iostream>
using namespace std;
class father
{
private:
int tall;
public:
void setA(int a)
{
tall = a;
}
void printA()
{
cout << "f身高=" << tall << endl;
}
};
class mother
{
private:
int weight;
public:
void setB(int b)
{
weight = b;
}
void printB(){
cout << "m体重=" << weight << endl;
}
};
class son : public father, private mother//公共继承father,私有继承mother
{
private:
int age;
public:
//设立方法给weight赋值,私有继承mother不可直接访问weight
void setweightB(int b){
setB(b);
}
void setC(int c)
{
age = c;
}
void printC()
{
printA(),
printB(),
cout << "年龄=" << age << endl;
}
};
int main()
{
son a;
a.setA(175);
//a.setB(99);public继承mother可访问,但私有不行
a.setweightB(99);
a.setC(88);
a.printC();
return 0;
}
基类中的成员按照继承方式来确定其在派生类中的访问方式
2)多重继承的构造函数和析构函数
构造函数声明:
<派生类名>::<派生类名>(基类形参,内嵌对象形参,本类形参):<基类名1>(参数表),<基类名2>(参数表),····,<内嵌对象1>(参数表1),···,
{
本类成员初始化赋值语句;
};
#include<iostream>
using namespace std;
class A{
private:
int a;
public:
A(int a_1) : a(a_1) { cout << "a的值=" << a << endl; }
~A() { cout << "基类A结束" << endl; }
};
class B{
private:
int b;
public:
B(int b_1):b(b_1){cout << "b的值=" << b << endl; }
~B() { cout << "基类B结束" << endl; }
};
class C:public B,public A{
private:
int c;
public:
C(int a_1,int b_1,int v);
~C() { cout << "派生类C结束" << endl; }
};
C::C(int a_1,int b_1,int v):A(a_1),B(b_1)//多重继承子类构造函数定义
{
c = v;
cout << "c的值=" <<c << endl;
}
int main(){
C c1(9,10,12);
return 0;
}
1.同一层次的各基类构造函数执行顺序取决于定义派生类时所指定的各基类顺序,如本案例中先继承B,后继承A,则先执行B的构造
2.多重继承中构造与析构函数执行顺序与单继承相同,遵循同样的原则
3)多重继承中的二义性
多重继承下派生类可能有多个直接基类或间接基类,可能造成对基类中某个成员的访问具有不确定性,使得这种访问出现二义性
情况1:派生类的不同基类有同名成员
#include<iostream>
using namespace std;
class A{
public:
void f(){
cout << "来自A" << endl;
}
};
class B{
public:
void f(){
cout << "来自B" << endl;
}
void g();
};
class C:public A,public B{
public:
void g();
void h();
// void f(){//同名覆盖法
// A::f();
// }
};
int main(){
C c1;
// c1.f();未明确目标
c1.A::f();//作用域标识符限定访问目标
c1.B::f();
//c1.f();
return 0;
}
案例中基类成员中皆有同名成员f(),使得直接通过对象访问出现了二义性,同时也提供了两种解决方法:
(1)使用作用域运算符::进行限定
对象名.基类名::成员名
对象名.基类名::成员名(参数表)
(2)同名覆盖法
在派生类中重新定义与基类同名的成员(如果是成员函数,则参数表也要相同),这样可以隐藏掉同名的基类成员,在访问同名成员时,就会访问派生类中重新定义的同名成员
情况2:派生类中引用公共基类中成员时出现二义性
#include <iostream>
using namespace std;
class Grandpa//间接基类
{ public:
int p1;
void op()
{
cout << "p1=" << p1 << endl;
}
};
class Father : public Grandpa//直接基类
{ public:
int p1_1;
};
class Uncle : public Grandpa//直接基类
{ public:
int p1_2;
};
class Grandson : public Father, public Uncle{};
int main()
{
Grandson obj;
//obj.p1 = 3;
//obj.op();
//访问目标不够明确,造成了二义性
obj.Father::p1 = 9;//加上直接基类名细化所要访问的目标
obj.Father::op();
obj.Uncle::p1 = 0;
obj.Uncle::op();
return 0;
}
正常输出:
我们同样地,对所要访问的目标进一步展开,不可越过直接基类访问间接基类,应从间接基类衍生出的派生类对象到间接基类名再到直接基类对象成员
4)虚基类
在上面提到了多重继承的二义性,对于派生类对象来说,继承来的同名成员在内存中同时拥有多个副本,这增加了内存的开销,为了解决这一问题,C++提供了虚基类技术
具体来说就是将共同基类设为虚基类,这样继承的同名成员只有一个副本,不会再引起二义性问题
class <派生类名>:virtual <继承方式><基类名>
{
};
#include <iostream>
using namespace std;
class L1
{
public:
int m1;
void f1()
{
cout << "layer 1->m1="<< m1 << endl;
}
L1(int i){
m1 = i;
cout << "layer 1->m1="<< m1 << endl;
}
};
class L2_2 : virtual public L1
{ // L1为虚基类,公有派生L2_2类
public:
int m2_2;
L2_2(int i,int j):L1(i){//类L2_2的构造函数,需要对虚基类L1进行初始化
m2_2 = j;
cout<< "layer 2->m2_2="<< m2_2 << endl;
}
};
class L2_1 : virtual public L1
{ // L1虚基类,公有派生L2_1类
public:
int m2_1;
L2_1(int i,int k):L1(i){
m2_1 = k;
cout<< "layer 1->m2_1="<< m2_1 << endl;
}
};
class L3 : public L2_1, public L2_2
{ // L3多重继承
public:
int m3;
void f3()
{
cout << "lay 3_m3=" << m3 << endl;
}
L3(int i,int j,int k,int m):L1(i),L2_1(i,k),L2_2(i,j){
m3 = m;
cout << "lay 3_m3=" << m3 << endl;
}
};
int main()
{
L3 obj(5,6,7,8);
obj.m3 = 4;
obj.f3();
obj.m1 = 1;//L1为虚基类,可直接访问
obj.f1();
return 0;
}
当虚基类定义有非默认构造函数,此时在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的初始化列表中给出对虚基类的初始化
5.5子类型与赋值兼容规则
1)子类型
子类型,是指当一个类型至少包含了另一个类型的所有行为,则称该类型是另一个类型的子类型
子类型中有一个类型适应的概念,指两种类型之间的关系,如果类型B是类型A的子类型,则称类型B适应于类型A,类型B的对象能够适用于类型A的对象所能使用的场合
2)赋值兼容规则
赋值兼容规则是指在公有继承下,某些场合中,一个派生类对象可以作为基类对象来使用
书中举出以下三种情况:
(1)派生类对象可以赋值给基类对象
Derived A;
Base B;
B=A;
(2)派生类对象可以初始化基类的引用
Derived A;
Base &B=A;
(3)派生类对象的地址可以赋给指向基类的指针
Derived A;
Base *B=&A;
```cpp
#include <iostream>
using namespace std;
class Base
{
public:
void print()
{
cout << "父类::print()!" << endl;
}
};
class Derived : public Base
{
public:
void f();
};
void fun(Base &base1)
{ //形参为基类Base的引用
base1.print();
}
int main()
{
//(1)派生类对象可以赋值给基类对象
Derived d1;
Base b1;
b1 = d1;
b1.print();
//(2)派生类对象可以初始化基类的引用
Derived d2;
Base &b2 = d2;
b2.print();
//(3)派生类对象的地址可以赋给指向基类的指针
Derived d3;
Base *b3 = &d3;
b3->print();
return 0;
// Base base; //父类对象
// base.print(); //父类对象调用父类的公有成员函数
// Derived derived1; //子类对象
// derived1.print();
// fun(derived1); //将子类对象传递给父类对象的引用,等同于把子类对象当成父类对象用
// //赋值兼容原则
return 0;
}
六、多态性
多态性是面向对象程序设计的又一个重要特征,它是指不同对象接收到同一消息时会产生不同的行为。多态性也分为两种:静态多态性和动态多态性,之前学的函数重载就可以理解为是静态多态性
6.1多态性的概念
多态,即指一种事物有多种状态,在设计新类时我们希望所设计的类具有相同的风格,不同的类中具有相似功能的函数,在新类的设计中只需要添加相同的数据成员并改写相应的成员函数,不同类的对象调用自己的函数成员,这便是多态性
1、多态的类型
多态性实现方式有4种:重载多态、强制多态、类型参数化多态、包含多态
函数重载和运算符重载都属于重载多态
强制多态是将一个变量的类型加以强制转换来满足某种操作要求
包含多态是定义于不同类中的同名成员函数的多态行为,主要是继承过程中通过虚函数来实现
类型参数化多态是指当一个函数或类对若干个类型参数操作时,这些类型具有某些公共的语义特性
2、静态关联和动态关联
关联这个概念与C++的多态性密切相关,当我们的一个源程序需要编译连接才能形成可执行文件,在这个过程中会把调用函数和对应函数连接在一起,这个过程就是关联,关联是确定调用的具体对象的过程。
在编译时就能确定具体的调用对象,称为静态关联;在编译时还不能确定具体的调用对象,在程序运行过程中才能确定具体调用对象,称为动态关联,静态关联在程序运行前进行关联,动态关联在程序运行中进行关联,静态关联实现的多态称为静态多态,动态关联实现的多态称为动态多态
6.2运算符重载
运算符重载指对已有运算符赋予它新的含义,通过运算符重载函数来实现,实质属于函数重载,是实现静态多态的一个重要手段
1、运算符重载概念
“重载”即重新赋予新的含义,同函数重载一样,运算符也可以重载。在对加法运算符的使用中,C++系统已经重载好了对于不同类型的数运算,如9+5、9.1+8.8。而对于“<<"和”>>"运算符既可以是位移运算符又可以是流插入、流提取运算符,类似如此…
另外,对于系统定义的数据类型,可以使用系统大部分的运算符,但是用户定义的新类型则不能使用系统提供的这些运算符,需要自行定义运算符重载函数
2、运算符重载方法
运算符重载通过定义一个重载函数来实现,在需要执行被重载的运算符时,系统会自动调用该函数,来实现相应的运算
函数类型 operator 运算符名称(形参列表)
{对运算符的重载处理}
operator和运算符名称构成函数名,其他与普通函数定义时类似,operator是C++保留关键字,用于定义重载运算符
#include<iostream>
using namespace std;
class Point{
public:
int x, y;
public:
Point(){
}
Point(int x, int y) : x(x), y(y){}
//friend Point operator+(const Point &, const Point &);
//类内重载,运算符重载函数作为类的成员函数
Point operator+(const Point &a){
Point ret;
ret.x = this->x + a.x;
ret.y = this->y + a.y;
return ret;
}
};
//类外重载,运算符重载函数作为类的友元函数
// Point operator+(const Point &a,const Point &b){
// Point ret;
// ret.x = a.x +b.x;
// ret.y = a.y+b.y;
// return ret;
// }
int main(){
Point a(2, 4), b(5, 3);
Point c = a + b;
cout << "x:" << c.x << endl;
cout << "y:" << c.y << endl;
return 0;
}
这里涉及重载函数的类内类外问题,类内重载即视为类的成员函数,类外重载当调用到类内私有成员时需定义为类的友元函数,如果只是调用类的公有成员或只是通过函数形参来传递运算所需操作数,则像正常函数定义即可