Hi,我是小余。 本文已收录到 GitHub Androider-Planet 中。这儿有 Android 进阶生长常识系统,重视大众号 [小余的自习室] ,在成功的路上不走失!

前言

C++作为一门在C和Java之间的言语,其既能够运用C言语中的高效指针,又承继了Java中的面向目标编程思维,在去年编程言语排行榜上更是初次超过Java,进入前三。

前面现已运用一篇文章解说了C++中的指针:【重学C/C++系列(三)】:这一次彻底搞懂指针和引证

今天这篇文章就来解说下C++中的面向目标编程思维。说到面向目标编程,就要讲到目标的三大特性:封装,承继和多态。本篇文章就以这三个特性打开。

目录

【重学C/C++系列(五)】:一文读懂C++中的面向对象编程

封装

封装是面向目标编程三大特性之一。

中心思维1:将特点和行为作为一个整体来体现生活中的事物。

class People {
public:
    string name;
    void eat();
privatestring sex;
};

留意:类中的特点和行为统称为成员特点成员称为成员变量,行为成员称为成员函数

中心思维2:对特点和行为运用权限控制

面向目标编程中的权限包括:

  • 1.public 公共权限,所有类中都能够拜访
  • 2.protected 受维护权限,只有当时类和子类能够拜访
  • 3.private 私有权限,只有当时类能够拜访。

一般封装原则:

对所有成员变量运用private权限,并运用public成员函数set和get对成员变量进行读写操作,能够避免成员变量对外露出。

承继

承继是面向目标编程过程中一个很重要的特性,它允许开发者保存原有类的特性根底上进程扩展,添加功用等。 新承继的类称为派生类(java中习气叫子类),而被承继的类称为基类(java中习气叫父类)。

关于有java根底的同学来说再了解不过了,所以关于Android开发者来说,对C++上手会比其他程序员更快些,尽管字面意思相似,可是C++的承继和java仍是有许多差异的,下面小余会一一道来。

承继的格局

class 派生类名:承继办法 基类的称号
class Apublic B

承继根底代码:

class Father {
public:
    string name = "father";
    int age = 45;
};
class Son :public Father {
public:
    string sex = "male";
    void print() {
        cout << "name:" << name << " age:" << age << " sex:" << sex << endl;
    }
};
​
void extendsTest::mainTest()
{
    Son son;
    son.print();
};
打印成果:
name:father age:45 sex:male

基类中的name和age是子类和父类共有的成员变量,每个人都有名字和年纪,尽管子类中没有界说,可是能够从父类中承继过来,这便是承继的意义。

而sex特点是子类Son中独有的成员变量。父类独有的元素能够运用private修饰,表明这个元素归于当时父类持有,子类也不行获取,这个咱们都了解。

这儿要说下在子类界说承继过程中对父类的承继办法是有说法的:如下的public

class Son :public Father
类成员/承继办法 public承继 protected承继 private承继
父类的public成员 子类的public成员 子类的protected成员 子类private成员
父类的protected成员 子类的protected成员 子类的protected成员 子类private成员
父类的private成员 子类不行见 子类不行见 子类不行见

权限记住规矩:

子类的权限受限于父类的权限以及子类承继的办法,子类对父类的承继办法只是对父类的成员进行再封装,大部分状况下运用public承继办法即可。除非不想让其他类引证该类的父类元素。

子类与父类有同名特点或许办法

假设子类有父类同名元素,则优先运用子类的元素

class Father {
public:
    string name = "father";
    int age = 45;
};
class Son :public Father {
public:
    string sex = "male";
    string name = "son";
    void print() {
        cout << "name:" << name << " age:" << age << " sex:" << sex << endl;
    }
};
​
void extendsTest::mainTest()
{
    Son son;
    son.print();
};
​
打印成果:
name:son age:45 sex:male

假如此刻必定需求拜访父类的元素呢?加上父类修饰符即可。

void print() {
    cout << "name:" << Father::name << " age:" << age << " sex:" << sex << endl;
}

同名办法呢?这儿就触及到了面向目标编程中的函数重载多态问题了,后面再解说

单承继和多承继

C++中的承继不像java中那样,只能承继一个父类,C++中能够承继多个父类, 所以就有单承继和多承继的差异:

单承继

只有一个父类

class Apublic B{
}

多承继

有多个父类

class A:public B,public C {
​
}

菱形承继

菱形承继图:

【重学C/C++系列(五)】:一文读懂C++中的面向对象编程

菱形承继会有啥问题呢?

class A {
public:
    string name;
};
class B :public A {
public:
    int age;
};
class C :public A {
public:
    string sex;
};
class D :public B, public C {
public:
    int id;
};
int main()
{
    D student;
    student.name = "小明";
    student.age = 18;
    student.sex = "男";
    student.id = 666;
    return 0;
}

问题出来了:

【重学C/C++系列(五)】:一文读懂C++中的面向对象编程

原因是B和C一起承继了A,所以B和C一起都拥有name特点,直接运用student.name,编译器无法确认name是归于哪个类,此刻有以下解决办法:

  • 办法1:清晰指明当时name归于哪个类

    student.B::name = "小明";
    
  • 办法2:虚承继:在承继办法前加上virtual。

    class B :virtual public A {
    public:
        int age;
    };
    class C :virtual public A {
    public:
        string sex;
    };
    

多承继是C++杂乱的一个体现。有了多承继,就存在菱形承继,为了解决菱形承继,又呈现了菱形虚拟承继,其底层完结又很杂乱。所以一般不建议规划出多承继,必定不要规划出菱形承继

C++中的几种特殊成员函数

结构函数

C++在编译器会给咱们默许创立一个缺省的结构办法: 如下代码:

class Father {
public:
    string name = "father";
    int age = 45;
    void print() {
        cout << "name:" << name << " age:" << age << endl;
    }
};
class Son :public Father {
public:
    string sex = "male";
};
​
void extendsTest::mainTest()
{
    Son son;
    son.print();
};
运转成果:name:father age:45

能够看到尽管咱们没有清晰声明结构办法,可是依然能够调用无参结构办法。这便是因为编译器主动给咱们创立了一个无参结构办法

假如类界说了自己的结构办法后(包括无参和有残),编译器就不会给咱们创立了,看下面代码:

class Father {
public:
    Father() {
        cout << "Father:" << name << endl;
    }
    string name = "father";
    int age = 45;
    void print() {
        cout << "name:" << name << " age:" << age << endl;
    }
};
class Son :public Father {
public:
    Son(){
        cout << "Son:" << name << endl;
    }
    string sex = "male";    
};
​
void extendsTest::mainTest()
{
    Son son;
    son.print();
};
打印成果:
Father:father
Son:father
name:father age:45

从上面代码也能够看出C++编译器会默许优先调用父类的结构办法,再调用子类的结构办法,

这点和java中是有差异的,java会从子类开始依次调用父类的结构办法,然后回溯子类的结构办法

所以为了确保目标的顺利创立,需求确保父类的结构办法是有效的。 如下代码:

class Father {
public:
    Father(string _name):name(_name){
        cout << "Father:" << name << endl;
    }
    string name = "father";
    int age = 45;
};

此刻父类中创立了一个有参结构办法,前面说过,此刻编译器不会创立默许的无参结构办法,则需求确保在其子类中有初始化父类的操作:即调用父类有参结构办法。 如下代码:

class Son :public Father {
public:
    Son(string name):Father(name) {
        cout << "Son:" << name << endl;
    }
    string sex = "male";
};
​
void extendsTest::mainTest()
{
    Son son1("myName");
};
成果:
Father:myName
Son:myName

析构函数

析构函数用来开释当时目标运用到的内存空间,当目标跳出其作用域范围后就会履行析构函数(除非是有智能指针呈现循环引证的状况,无法开释,导致泄露)。 C++中析构函数和结构函数相反,会优先调用子类的析构函数再调用父类的析构函数。 如下代码:

class Father {
public:
    ~Father() {
        cout << "~Father"<< endl;
    }
    string name = "father";
    int age = 45;
};
class Son :public Father {
public:
    ~Son() {
        cout << "~Son" << endl;
    }   
    string sex = "male";    
};
​
void extendsTest::mainTest()
{
    Son son;
};
运转成果:
~Son
~Father

仿制结构

C++中仿制结构函数格局:

  • 格局1:带const参数 Complex(const Complex& c) { … } 表明以常量目标作为参数

  • 格局2:不带const参数 Complex(Complex& c) { … } 表明以非常量作为参数进行仿制 如下代码:

    class Complex {
     public:
        double real, imag;
        Complex(double _real, double _imag):
            real(_real),imag(_imag)
        {
            cout << "real:" << real << " imag:" << imag << endl;
        }
        void print() {
            cout << "real:" << real << " imag:" << imag << endl;
        }
        Complex(Complex& c) {
            real = c.real+1; imag = c.imag+1;
        }
      };
    ​
    void extendsTest::mainTest()
    {
        Complex c1(1.0, 2.0);
        Complex c2(c1);
        c2.print();
    };
    打印成果:
    real:1 imag:2
    real:2 imag:3
    

仿制结构函数和结构办法相似,C++编译器会给咱们供给默许的仿制结构函数。 将上面代码的仿制结构函数删除后:

class Complex {
public:
    double real, imag;
    Complex(double _real, double _imag):
        real(_real),imag(_imag)
    {
        cout << "real:" << real << " imag:" << imag << endl;
    }
    void print() {
        cout << "real:" << real << " imag:" << imag << endl;
    }
};
​
void extendsTest::mainTest()
{
    Complex c1(1.0, 2.0);
    Complex c2(c1);
    c2.print();
};

依然能够履行仿制结构,此刻c2运用了默许仿制结构函数进行赋值

仿制结构的几种调用形式:

  • 1.当用一个目标去初始化同类的另一个目标时

    Complex c2(c1);
    Complex c2 = c1;
    

    这两天句子是等价的。可是要留意此刻Complex c2 = c1是一个初始化句子,并非一个赋值句子。赋值句子是一个现已初始化后的变量。 如下:

    Complex c1, c2; c1 = c2 ;
    c1=c2;
    

    赋值句子不会触发仿制结构

  • 2.当目标作为一个函数形参时,此刻也会触发目标的仿制结构

    class Complex {
     public:
        double real, imag;
        Complex(double _real, double _imag):
            real(_real),imag(_imag)
        {
            cout << "real:" << real << " imag:" << imag << endl;
        }
        Complex(Complex& c) {
            real = c.real+1; imag = c.imag+1;
            cout << "complex copy" << endl;
        }
      };
    ​
    void func(Complex c) {
        cout << "real:" << c.real << " imag:" << c.imag << endl;
    }
    ​
    void extendsTest::mainTest()
    {   
        Complex c(1.0,2.0);
        func(c);
    };
    ​
    运转成果:
    real:1 imag:2
    complex copy
    real:2 imag:3
    

    能够看到运转成果触发了Complex的仿制结构 以目标作为函数的形参,在函数被调用时,生成的形参要用仿制结构函数初始化,这会带来时间上的开销。 假如用目标的引证而不是目标作为形参,就没有这个问题了

    void func(Complex& c) {
        cout << "real:" << c.real << " imag:" << c.imag << endl;
    }
    

    可是以引证作为形参有必定的风险,因为这种状况下假如形参的值产生改变,实参的值也会跟着改变。 最好的办法便是将函数形参声明为const类型的引证

    void func(const Complex& c) {
        cout << "real:" << c.real << " imag:" << c.imag << endl;
    }
    
  • 3.目标作为函数回来值回来时,也会触发仿制结构。

    Complex func() {
        Complex c(1.0, 2.0);
        return c;
      }
     void extendsTest::mainTest()
      { 
        cout << func().real << endl;
      };
    ​
    成果:
    real:1 imag:2
    complex copy
    2
    

    能够看到此刻func函数中的return c处会触发一次仿制结构,并将仿制后的目标回来。 这点经过函数hack过程也能够看出来:此处call办法履行的是仿制结构办法

    【重学C/C++系列(五)】:一文读懂C++中的面向对象编程

运算符重载函数

运算符重载,便是对已有的运算符从头进行界说,赋予其另一种功用,简化操作 让已有的运算符 习惯不同的数据类型

  • 格局:

    重载+=号运算 ==>operator+=
    重载+运算符 ==>operator+ 
    ...
    

下面举两个运算符重载比方:

  • 1.重载+号

    class Complex {
     public:
        Complex() {
    ​
        }
        double real, imag;
        Complex(double _real, double _imag):
            real(_real),imag(_imag)
        {
            cout << "real:" << real << " imag:" << imag << endl;
        }
        void print() {
            cout << "real:" << real << " imag:" << imag << endl;
        }
        Complex(const Complex& c) {
            real = c.real; imag = c.imag;
            cout << "complex copy" << endl;
        }
        //以大局函数的形式重载
        friend Complex operator+(const Complex& c1, const Complex& c2);
    ​
    };
    Complex operator+(const Complex& c1, const Complex& c2) {
        Complex _c;
        _c.real = c1.real + c2.real;
        _c.imag = c1.imag + c2.imag;
        return _c;
    }
    Complex func() {
        Complex c(1.0, 2.0);
        Complex c1(2.0, 3.0);
        Complex c2 = c + c1;
        return c2;
    }
    ​
    void extendsTest::mainTest()
    {   
        cout << func().real << endl;
    };
    运转成果:
    real:1 imag:2
    real:2 imag:3
    complex copy
    complex copy
    3
    
  • 2.重载+=号运算 代码如下:

    class Complex {
     public:
        ...
        //成员函数重载
        Complex& operator+=(const Complex& c);
      };
    ​
    Complex & Complex::operator+=(const Complex& c1) {
        this->real += c1.real;
        this->imag += c1.imag;
        return *this;
    }
    Complex func() {
        Complex c(1.0, 2.0);
        Complex c1(2.0, 3.0);
        c += c1;
        return c;
    }
    void extendsTest::mainTest()
    {   
        cout << func().real << endl;
    };
    运转成果:
    real:1 imag:2
    real:2 imag:3
    complex copy
    3
    
运算符重载的约束

多数C++运算符都能够重载,重载的运算符不必是成员函数,但有必要至少有一个操作数是用户界说的类型。

  • 1.重载后的运算符有必要至少有一个操作数是用户界说的类型,避免用户为规范类型重载运算符。如:不能将减法运算符(-)重载为核算两个 double 值的和,而不是它们的差。尽管这种约束将对创造性有所影响,但能够确保程序正常运转。

  • 2.运用运算符时不能违背运算符本来的句法规矩。例如,不能将求模运算符(%)重载成运用一个操作数:int x;Time shiva;%x;%shiva;,且不能修正运算符的优先级。

  • 3.不能创立新运算符。例如,不能界说operator **()函数来表明求幂。

  • 4.不能重载下面的运算符。

    【重学C/C++系列(五)】:一文读懂C++中的面向对象编程

运算符重载触及的常识点仍是比较多的,后期文章会单独出一期解说。

多态

多态是指:函数调用的多种形态,运用多态能够使得不同的目标去完结同一件事时,产生不同的动作和成果

C++中多态分为静态多态和动态多态

静态多态

静态多态的中心思维关于相关的目标类型,直接完结他们各自的界说,不需求共有基类,甚至能够没任何关系, 只需求各个详细类的完结中要求相同的接口声明,这儿的接口称之为隐式接口。客户端把操作这些目标的函数界说为模板,当需求操作什么类型的目标时,直接对模板指定该类型实参即可(或经过实参演绎取得)

在模板编程及泛型编程中,是以隐式接口和编译器多态来完结静态多态。

代码如下:

class Circle {
public:
    void Draw() const{
        cout << "Circle draw" << endl;
    }
    int z;
};
class Rectangle {
public:
    void Draw() const{
        cout << "Rectangle draw" << endl;
    }
};
template<typename T>
void test(const T& t) {
    t.Draw();
}
void extendsTest::mainTest()
{   
    //cout << func().real << endl;
    Circle cir;
    test(cir);
    Rectangle rec;
    test(rec);
};
​
打印成果:
Circle draw
Rectangle draw

静态多态实质上便是模板的具现化,静态多态中的接口调用也叫做隐式接口,相关于显现接口由函数的签名式(也便是函数称号、参数类型、回来类型)构成,隐式接口一般由有效表达式组成

动态多态

动态多态中心思维关于相关的目标类型,确认它们之间的一个一起功用集,然后在基类中, 把这些一起的功用声明为多个公共的虚函数接口。各个子类重写这些虚函数, 以完结详细的功用。客户端的代码(操作函数)经过指向基类的引证或指针来操作这些目标, 对虚函数的调用会主动绑定到实际供给的子类目标上去。

动态多态是在运转期完结的,这造就了动态多态机制在处理异质目标集合时的强壮威力(当然,也有了一点点功能丢失)。

如下代码:

class Geometry {
public:
    virtual void Draw() const=0;
};
class Circle :public Geometry{
public:
    void Draw() const{
        cout << "Circle draw" << endl;
    }
    int z;
};
class Rectangle :public Geometry {
public:
    void Draw() const{
        cout << "Rectangle draw" << endl;
    }
};
void extendsTest::mainTest()
{   
    Circle cir;
    const Geometry* e1 = &cir;
    e1->Draw();
    Rectangle rec;
    const Geometry* e2 = &rec;
    e2->Draw();
};
打印成果:
Circle draw
Rectangle draw

//动态多态最吸引人之处在于处理异质目标集合的能力

void DrawVec(std::vector<DynamicPoly::Geometry*> vecGeo)
   {
    const size_t size = vecGeo.size();
    for(size_t i = 0; i < size; ++i)
      vecGeo[i]->Draw();
   }
}

动态多态实质上便是面向目标规划中的承继、多态的概念。动态多态中的接口是显式接口(虚函数)

动态多态构成条件

  • 1.有必要经过基类的指针或许引证调用虚函数。
  • 2.被调用的函数有必要是虚函数,且子类有必要对父类的虚函数进行重写。

动态多态完结原理:虚函数表

class Geometry {
public:
    virtual void Draw() const=0;
};
class Circle :public Geometry{
public:
    void Draw() const{
        cout << "Circle draw" << endl;
    }
    int z;
};
class Rectangle :public Geometry {
public:
    void Draw() const{
        cout << "Rectangle draw" << endl;
    }
};
void extendsTest::mainTest()
{   
    Circle cir;
    const Geometry* e1 = &cir;
    e1->Draw();
};

Circle目标中除了z成员变量外,实际上还有一个指针_vfptr放在目标的前面(有些渠道可能会放到目标的最后面,这个跟渠道有关).

【重学C/C++系列(五)】:一文读懂C++中的面向对象编程

目标中的这个指针叫做虚函数表指针,简称虚表指针,虚表指针指向一个虚函数表,简称虚表,每一个含有虚函数的类中都至少有一个虚表指针。

#include <iostream>
using namespace std;
//父类
class Base
{
public:
    //虚函数
    virtual void Func1()
    {
        cout << "Base::Func1()" << endl;
    }
    //虚函数
    virtual void Func2()
    {
        cout << "Base::Func2()" << endl;
    }
    //一般成员函数
    void Func3()
    {
        cout << "Base::Func3()" << endl;
    }
private:
    int _b = 1;
};
//子类
class Derive : public Base
{
public:
    //重写虚函数Func1
    virtual void Func1()
    {
        cout << "Derive::Func1()" << endl;
    }
private:
    int _d = 2;
};
int main()
{
    Base b;
    Derive d;
    return 0;
}

【重学C/C++系列(五)】:一文读懂C++中的面向对象编程

虚表当中存储的便是虚函数的地址,因为父类当中的Func1和Func2都是虚函数,所以父类目标b的虚表当中存储的便是虚函数Func1和Func2的地址。而子类尽管承继了父类的虚函数Func1和Func2,可是子类对父类的虚函数Func1进行了重写,因而,子类目标d的虚表当中存储的是父类的虚函数Func2的地址和重写的Func1的地址。

这便是为什么虚函数的重写也叫做掩盖,掩盖便是指虚表中虚函数地址的掩盖,重写是语法的叫法,掩盖是原理层的叫法。

虚函数表实质是一个存虚函数指针的指针数组,一般状况下会在这个数组最后放一个nullptr。

当满意多态条件以后,父类的指针或引证调用虚函数时,不是编译时确认的,而是运转时到指向的目标中的虚表中去找对应的虚函数调用,而且引证的底层也是由指针完结,父类在指向子类时会产生切片。 所以指针指向父类的目标,调用的便是父类的虚函数,指向的是子类目标,调用的便是子类的虚函数。

动态多态和静态多态的比较

静态多态长处:

静态多态是在编译期完结,因而效率高,编译器能够进行优化。 有很强的是适配性和松耦合性。 最重要一点经过模板编程为C++带来了泛型规划的概念,比方强壮的STL库

静态多态缺陷:

由所以模板来完结静态多态,因而模板的不足也便是静多态的下风,比方调试困难、编译耗时、代码胀大、编译器支持的兼容性,不能够处理异质目标集合。

动态多态长处:

OO规划,对是客观国际的直觉知道;

完结与接口别离,可复用;

处理同一承继系统下异质目标集合的强壮威力

动态多态缺陷:

运转期绑定,导致必定程度的运转时开销

编译器无法对虚函数进行优化;

粗笨的类承继系统,对接口的修正影响整个类层次;

不同点:

实质不同,静态多态在编译期决议,由模板具现完结,而动态多态在运转期决议,由承继、虚函数完结;

动态多态中接口是显式的,以函数签名为中心,多态经过虚函数在运转期完结,静态多台中接口是隐式的,以有效表达式为中心,多态经过模板具现在编译期完结。

相同点:

都能够完结多态性,静态多态/编译期多态、动态多态/运转期多态;

都能够使接口和完结相别离,一个是模板界说接口,类型参数界说完结,一个是基类虚函数界说接口,承继类负责完结;

总结

本篇文章详解解说了C++中的面向目标编程的三大特性:封装,承继以及多态 以及目标编程中模板编程,虚函数,结构函数,析构函数,仿制结构,操作符重载等常识, 常识点仍是比较多的,需求好好消化下。

因为操作符重载触及的常识点比较多,篇幅问题,预备在后面几节单独出一期进行解说,记住订阅哦

本篇文章触及的代码库房地址

我是小余,欢迎重视看更多文章

参考

C++中文开发手册

reference C++

八个 C++ 开源项目,协助初学者进阶生长

C++模板函数

C++ 静态多态和动态多态 浅析

12 C++的多态

运算符重载

C++仿制结构函数(仿制结构函数)详解

c++:承继(超详解)