$C++$ 面向对象的三大特性:封装、继承、多态

封装

  • 将抽象出的数据成员、代码成员相结合,将它们视为一个整体
  • 目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只需要通过外部接口,以特定的访问权限,来使用类的成员

声明形式

class 类名称{
public:
公有成员(外部接口)
private:
私有成员
protected:
保护型成员
}
  • $public$ :公共权限,类内可访问、类外可访问
  • $protected$ :保护权限,类内可访问、类外不可访问,儿子可以访问父亲的保护内容
  • $private$ :私有权限,类内可访问、类外不可访问,儿子不可以访问父亲的私有内容
    • 如果紧跟在类名称的后面声明私有成员、则关键字 $private$ 可以省略

访问形式

  • 类中成员互访

    • 直接使用成员名

    • this->成员名

    • (*this).成员名

  • 类外访问—-仅能访问public属性的成员

    • 对象名.成员名
    • 对象指针->成员名

构造函数

在对象被创建时使用特定的值构造对象,或者说将对象初始化为一个特定的状态

  • 没有返回值,也不写 $void$

  • 在对象创建时由系统自动调用,且只会调用一次

  • 如果程序中未声明则系统自动产生出一个默认形式的构造函数

  • 允许为内联函数、重载函数、带默认形参值的函数

拷贝构造函数是一种特殊的构造函数,其形参为本类的对象引用。

class 类名{
public:
类名(形参);//构造函数
类名(类名&对象名);//拷贝构造函数
};

类名::类(类名&对象名)//拷贝构造函数的实现
{函数体}
  • 调用函数时,若函数的形参为类对象,实参赋值给形参时,系统自动调用拷贝构造函数
  • 当函数的返回值是类对象时,系统自动调用拷贝构造函数

如果程序员没有为类声明拷贝初始化构造函数,则编译器自己生成一个默认的拷贝构造函数,其功能是用作为初始值的对象的每个数据成员的值,初始化将要建立的对象的对应数据成员。

调用拷贝构造函数的情况:

① 程序中需要新建立一个对象,并用另一个同类的对象对它初始化。

② 当函数的参数为类的对象时。在调用函数时需要将实参对象完整地传递给形参,也就是需要建立一个实参的拷贝,这就是按实参复制一个形参,系统是通过调用复制构造函数来实现的,这样能保证形参具有和实参完全相同的值。

③ 函数的返回值是类的对象。在函数调用完毕将返回值带回函数调用处时。此时需要将函数中的对象复制一个临时对象并传给该函数的调用处

析构函数

完成对象被删除前的一些清理工作,在对象的生存期结束的时刻系统自动调用它,然后再释
放此对象所属的空间

  • 如果程序中未声明析构函数,编译器将自动产生一个默认的析构函数
~类名(){}

示例

#include<iostream>
#include<iomanip>
using namespace std;

class Clock{
public:
Clock(int h=8,int m=0,int s=0); //构造函数
Clock(Clock &clock); //拷贝构造函数
~Clock(); //析构函数

public:
void SetTime(int h,int m,int s);
void ShowTime();

private:
int Hour;
int Minute;
int Second;
};

Clock::Clock(int h,int m,int s) {Hour=h,Minute=m,Second=s;}
//this->Hour=h;
//(*this).Hour=h;

Clock::Clock(Clock &c){
this->Hour=c.Hour;
this->Minute=c.Minute;
this->Second=c.Second;
}

Clock::~Clock() {Hour=Minute=Second=0;}

inline void Clock::SetTime(int h,int m,int s){
if(h>=24||h<0) h=0;
if(m<0||m>60) m=0;
if(s<0||s>60) s=0;
//if(h>24||h<0||m<0||m>60||s<0||s>60) return ;
this->Hour=h;
this->Minute=m;
this->Second=s;
}

inline void Clock::ShowTime(){
std::cout<<setw(2)<<setfill('0')<<this->Hour<<':'
<<setw(2)<<setfill('0')<<this->Minute<<':'
<<setw(2)<<setfill('0')<<this->Second;
}


int main(){
cout<<"Hello World"<<endl;
Clock c1;
c1.ShowTime(); //无参构造函数

cout<<"\n---------------"<<endl;
Clock c2(19,34,52); //含参构造函数
c2.ShowTime();

cout<<"\n---------------"<<endl;
Clock c3(c2); //拷贝构造函数
c3.ShowTime();

cout<<"\n---------------"<<endl;
c3.SetTime(16,35,24);
c3.ShowTime();
return 0;
}

友元

友元是 $C++$ 提供的一种破坏数据封装和数据隐藏的机制

通过将一个模块声明为另一个模块的友元,来引用另一个模块中本来被隐藏的信息。

可以使用友元函数和友元类。

为了确保数据的完整性,及数据封装与隐藏的原则,建议尽量不使用或少使用友元

友元函数

友元函数是在类声明中由关键字 $friend$ 修饰说明的成员函数,在它的函数体中能够通过对象名访问 $private$ 和 $protected$ 成员

作用:增加灵活性,使程序员可以在封装和快速性方面做合理选择。

访问对象中的成员必须通过对象名。

友元类

若一个类为另一个类的友元,则此类的所有成员都能访问对方类的私有成员。

声明语法:将友元类名在另一个类中使用 $friend$ 修饰说明

继承与派生

保持已有类的特性而构造新类的过程称为继承,在已有类的基础上新增自已的特性而产生新类的过程称为派生
被继承的已有类称为基类(或父类),派生出的新类称为派生类。

  • 继承的目的:实现见代码重用
  • 派生的目的:当新的问题出现,原有程序无法解决(或不能完全解决)时,需要对原有程序进行改造

声明形式:

class 派生类名: 继承方式 基类名{
成员声明;
}

三种继承方式

基类 公有继承 保护继承 私有继承
public 派生类的public成员 派生类的protected成员 派生类的private成员
protected 派生类的protected成员 派生类的protected成员 派生类的private成员
private 在派生类中不可见 在派生类中不可见 在派生类中不可见

类型兼容规则

一个公有派生类的对象在使用上可以被当作基类的对象

如果 C 公有继承了 B ,则称 “C is a B”

  • 派生类的对象可以被赋值给基类对象。
  • 派生类的对象可以初始化基类的引用。
  • 指向基类的指针也可以指向派生类。

通过基类对象名、指针只能使用从基类继承的成员

#include <iostream>
using namespace std;

class B0 {
public:
void display() {cout<<"B0::display()"<<endl;}
};
class B1:public B0 {
public:
void display() {cout<<"B1::display()"<<endl;}
};
class D1:public B1 {
public:
void display() {cout<<"D1::display()"<<endl;}
};

void fun(B0 *ptr) {ptr->display();}

int main(){
B0 b0,*p;
B1 b1;
D1 d1;

p=&b0,fun(p);
p=&b1,fun(p);
p=&d1,fun(p);

return 0;
}

输出结果:

B0::display()
B0::display()
B0::display()

三次调用函数都只调用了基类的display函数。

单继承与多继承

  • 单继承:派生类只从一个基类派生
  • 多继承:派生类从多个基类派生
  • 多重派生:由一个基类派生出多个不同的派生类。
  • 多层派生:派生类又作为基类,继续派生新的类。

多继承派生类的声明方式:

class 派生类名: 继承方式1 基类名1,继承方式2 基类名2,.{
成员声明;
}

每一个“继承方式”,只用于限制对紧随其后之基类的继承。

派生类的构造、析构函数

构造函数

  • 当基类中声明有默认形式的构造函数或未声明构造函数时,派生类构造函数可以不向基类构造函数传递参数。
  • 若基类中未声明构造函数,派生类中也可以不声明,全采用默认形式构造函数。
  • 当基类声明有带形参的构造函数时,派生类也应声明带形参的构造函数,并将参数传递给基类构造函数。
派生类名::派生类名(基类1形参,基类2形参,...基类n形参,本类形参):
基类名1(参数),基类名2(参数),....基类名n(参数),对象数据成员的初始化{
本类成员初始化赋值语句
}

构造函数的调用顺序

  1. 调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向石)。
  2. 调用成员对象的构造函数,调用顺序按照它们在类中声明的顺序
  3. 派生类的构造函数体中的内容

拷贝构造函数

  • 若建立派生类对象时调用默认拷贝构造函数,则编译器将自动调用基类的默认拷贝构造函数
  • 若编写派生类的拷贝构造函数,则需要为基类相应的拷贝构造函数传递参数。

析构函数

析构函数也不被继承,派生类自行声明,声明方法与一般(无继承关系时)类的析构函数相同。

  • 不需要显式地调用基类的析构函数,系统会自动隐式调用
  • 析构函数的调用次序与构造函数相反

示例:

#include <iostream>
using namespace std;
//基类B1,构造函数有参数
class B1 {
public:
B1(int i) {cout<<"constructing B1 "<<i<<endl;}
~B1() {cout<<"destructing B1"<<endl;}
};
//基类B2,构造函数有参数
class B2 {
public:
B2(int j) {cout<<"constructing B2 "<<j<<endl;}
~B2() {cout<<"destructing B2"<<endl;}
};
//基类B3,构造函数无参数
class B3 {
public:
B3() {cout<<"constructing B3 *"<<endl;}
~B3() {cout<<"destructing B3"<<endl;}
};

class C: public B2, public B1, public B3 {
public:
//派生类的公有成员
C(int a, int b, int c, int d):B1(a),memberB2(d),memberB1(c),B2(b) {}
//派生类的私有对象成员
private:
B1 memberB1;
B2 memberB2;
B3 memberB3;
};
int main(){
C obj(1,2,3,4);
}

运行结果:

constructing B2 2
constructing B1 1
constructing B3 *
constructing B1 3
constructing B2 4
constructing B3 *
destructing B3
destructing B2
destructing B1
destructing B3
destructing B1
destructing B2

派生类成员的标识与访问

同名隐藏规则

当派生类与基类中有同名成员时:

  • 若未显式指定类名,则通过派生类对象使用的是派生类中的同名成员
  • 如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。
  • 如要通过派生类对象访问基类中被隐藏的同名成员,应使用基类名限定。

虚基类

声明:用 $virtual$ 修饰说明基类

class B1: virtual public B

作用

  • 主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题
  • 为最远的派生类提供惟一的基类成员,而不重复产生多次拷贝。

注意:

  • 在第一级继承时就要将共同基类设计为虚基类。

image_1

虚基类及其派生类构造函数

  • 建立对象时所指定的类称为最(远)派生类。
  • 虚基类的成员是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的
  • 在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中给出对虚基类的构造函数的调用。如果未列出,则表示调用该虚基类的默认构造函数
  • 在建立对象时,只有最派生类的构造函数调用虚基类的构造函数,该派生类的其他基类对虚基类构造函数的调用被忽略

多态

  • 多态:是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为。
  • 目的:达到行为标识统一,减少程序中标识符的个数
  • 实现:函数重载、运算符重载、虚函数

运算符重载

实质

  • 将指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符
    函数的实参。
  • 编系统对重载运算符的选择,遵循函数重载的选择原则,

规则和限制

  • 可以重载C++中除下列运算符外的所有运算符

    . .* :: ?:

  • 只能重载C++语言中已有的运算符,不可臆造新的。

  • 不改变原运算符的优先级和结合性

  • 不能改变操作数个数。

  • 经重载的运算符,其操作数中至少应该有一个是自定义类型

两种形式:类成员函数、非成员函数(通常为友元函数)

声明形式

函数类型 operator 运算符(类型 &形参){

}

重载为类成员函数时(后置++、–除外)参数个数=原操作数个数-1
重载为友元函数时参数个数=原操作数个数,且至少应该有一个自定义类型的形参。

静态绑定和动态绑定

绑定:程序自身彼此关联的过程,确定程序中的操作调用与执行该操作的代码间的关系。
静态绑定:绑定过程出现在编译阶段,用对象名或者类名来限定要调用的函数。
动态绑定:绑定过程工作在程序运行时执行,在程序运行时才确定将要调用的函数

虚函数

虚函数是动态绑定的基础,是非静态的成员函数

  • 在类的声明中,在函数原型之前写virtual。
  • virtual只用来说明类声明中的原型,不能用在函数实现时
  • 具有继承性,基类中声明了虚函数,派生类中无论是否说明,同原型函数都自动为虚函数

本质:不是重载声明而是覆盖
调用方式:通过基类指针或引用,执行时会根据指针指向的对象的类决定调用哪个函数

示例:

#include <iostream>
using namespace std;

class B0 {
public:
virtual void display() {cout<<"B0::display()"<<endl;}
};
class B1:public B0 {
public:
void display() {cout<<"B1::display()"<<endl;}
};
class D1:public B1 {
public:
void display() {cout<<"D1::display()"<<endl;}
};

void fun(B0 *ptr) {ptr->display();}

int main(){
B0 b0,*p;
B1 b1;
D1 d1;

p=&b0,fun(p);
p=&b1,fun(p);
p=&d1,fun(p);

return 0;
}

虚析构函数

何时需要虚析构函数?

  • 当你可能通过基类指针删除派生类对象时
  • 如果你打算充许其他人通过基类指针调用对象的析构函数文(通过delete这样做是正常的),并被析构的对象是有重要的析构函数的派生类的对象,就需要让基类的析构函数成为虚拟的。

纯虚函数与抽象类

带有纯虚函数的类称为抽象类

class 类名{
virtual 类型 函数名(参数表)=0//纯虚函数
}

作用

抽象类为抽象和设计的目的而声明,将有关的数据和行为组织在一个继承层次结构中,保证派生类有要求的行为,
对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现

注意

  • 抽象类只能作为基类来使用。

  • 不能声明抽象类的对象。

  • 构造函数不能是虚函数,析构函数可以是虚函数。