面向对象程序设计

1.面向对象程序设计核心思想

  封装、继承、多态

2.继承

2.1 概念

  继承是一种类的层次关系,层次关系的根部叫基类,其他类由基类直接继承或间接继承而来,继承而来的类叫派生类

2.2 继承与访问控制

1)public公有继承:基类的公有成员成为派生类的公有成员,基类的保护成员成为派生类的保护成员

2)protected保护继承:基类的公有成员和保护成员成为派生类的保护成员

3)private私有继承:基类的公有成员和保护成员成为派生类的私有成员

4)总结:无论以何种方式继承,派生类都不能直接使用基类的私有成员

class base {};
class son :public base {};//公有继承
//省略访问说明符,class默认是private,struct默认是public
class son :base {};//私有继承
class son :protected base {};//保护继承
class son :private base {};//私有继承

2.3 继承与静态成员

1)基类定义的静态成员在整个继承体系只存在唯一的定义

2)派生类遵循访问控制规则访问基类定义的静态成员

2.4 使用final防止继承

1)c++11使用关键字final防止继承的发生

class NoDerived final{};//NoDerived不能作为基类

2)还能把基类的某个虚函数定义成final,防止派生类覆盖该函数,注意:final只作用于虚函数

class base{
    virtual void fun(int) const final;
};

class son: public base {
    void fun(int) const;//错误!!!fun(int)已经被声明为final
};

2.5 继承与类作用域

1)派生类的作用域位于基类作用域之内

2)声明在内层作用域的函数不会重载声明在外层作用域的函数(因为编译器由内层作用域向外层作用域查找,一旦找到名字就不再继续查找了),因此只要派生类的成员与基类的某个成员同名,则派生类将隐藏从基类继承而来的该成员(即使形参列表不一致)

class base {
public:
    void fun()
    {
        cout << 666 << endl;
    }
};

class son : public base {
public:
    void fun(int)
    {
        cout << 777 << endl;
    }
};

base baba;
son erzi;
erzi.fun();//错误!!!因为fun()在派生类中被隐藏
erzi.fun(5);//打印777
erzi.base::fun();//打印666

3)可以通过作用域运算符来使用被隐藏的成员

4)可以使用using让派生类对基类中所有的重载函数都可见,而不是隐藏

2.6 继承与容器

1.允许在一个保存基类对象的容器中添加派生类对象,此时对象的派生类部分将被切掉

2.在容器中存放有继承关系的对象时,通常存放基类指针或智能指针 

2.7 继承与构造函数

1)派生类的构造函数必须要显式调用基类的构造函数,先初始化基类部分,再初始化派生类部分

2)派生类并非以常规方式继承基类的构造函数,而是要使用using声明让派生类继承基类的所有的(有两个例外)构造函数

3)例外一:派生类自己的构造函数与基类的某个构造函数有相同的形参,则该构造函数不会被继承

      例外二:默认、拷贝和移动构造函数不会被继承

4)using声明不会改变该构造函数的访问级别:不管using声明出现在哪,基类的共有/受保护/私有构造函数在派生类中还是一个共有/受保护/私有构造函数,

5)using声明不能指定explicit和constexpr:如果基类的构造函数是explicit或constexpr,则继承的构造函数也拥有相同的属性

6)继承的构造函数不算自定义的构造函数,所以仍然可以满足合成构造函数的生成规则

7)如果基类的构造函数有默认实参,则实参不会被继承;如果有两个形参,一个形参有默认实参,另一个形参没有,则通过继承会获得两个构造函数:一个有两个形参,另一个有一个形参(对应没有默认实参的那个),总之,默认实参都不会被继承

2.8 继承与析构函数

1)派生类的析构函数不显式调用基类的析构函数,它只负责销毁派生类部分

2)当销毁一个派生类对象,编译器会先执行派生类的析构函数,再执行基类的析构函数

2.9 继承与拷贝/移动构造函数

1)派生类的拷贝/移动构造函数必须显式调用基类构造函数初始化对象的基类部分

class base {
    /*……*/
};

class son :public base {
public:
    son(const son& erzi): base(erzi) {
        /*……*/
    }
    son(son&& erzi): base(std::move(erzi)) {
        /*……*/
    }
};

2.10 继承与拷贝赋值运算符

  派生类的拷贝赋值运算符也必须显式调用基类的拷贝赋值运算符

class base {
    /*……*/
};

class son :public base {
public:
    son& operator=(const son& erzi)
    {
        if (this != &erzi)
        {
            base::operator=(erzi);
            /*派生类部分拷贝赋值*/
        }
        return *this;
    }
};

3.基类和派生类的隐式转换

1)存在派生类到基类的隐式转换,此时是向基类的拷贝/移动构造函数(参数都是引用)传递一个派生类对象,派生类对象的基类部分被保留,派生类部分被切掉

2)转换的可访问性只有当派生类public继承基类时,编译器可以在类外自动进行派生类到基类的隐式转换

3)不存在基类到派生类的隐式转换

4.基类指针(或引用)、派生类指针(或引用)与基类对象、派生类对象的匹配方式

其实用指针或者引用绑定非同等类型的对象时,就是等号右边的对象发生隐式的类型转换,所以此处与上面的  3.基类和派生类的隐式转换  规则类似,特别要注意它的第2)条

1)直接用基类指针(或引用)绑定基类对象

2)直接用派生类指针(或引用)绑定派生类对象

3)基类指针(或引用)可以绑定派生类对象,但是无法使用不存在于基类只存在于派生类的元素,原因如下:

  在内存中,一个基类类型的指针是覆盖N个单位长度的内存空间。当其指向派生类的时候,由于派生类元素在内存中堆放是:前N个是基类的元素(如果有虚函数则还有vptr),N之后的是派生类的元素。于是基类的指针就可以访问到前N元素了,但是此时无法访问到派生类(就是N之后)的元素

//基类指针或引用绑定派生类对象时,只能绑定到派生类的基类部分,如果试图使用派生类部分,则必须通过指针或引用的强制类型转换
class base {};
class son :public base {};
son erzi;
base baba;
base *p = &baba;    //p指向基类
p = &erzi;    //p指向派生类的基类部分
base& y = erzi;    //y引用派生类的基类部分

4)派生类指针(或引用)绑定基类对象

  派生类指针只有经过强制类型转换,才能引用基类对象,原因如下:

  有个people类是基类,数据成员有“姓名”和“身份证号”,其派生类student,添加了一个数据成员“学号”,如果让student的指针ps指向people类对象peo,则peo只有两个成员变量,而*pt有3个,现在ps->学号这个变量在ps下是可以使用的,但它指向的实体却没有这个变量,所以出错,于是C++直接就避免了这样的隐式转换

5.静态绑定

5.1 概念

  在编译时就知道对象的实际类型

6.动态绑定

6.1 概念

  在运行时才知道对象的实际类型

6.2 何时发生?

  只有当使用基类的指针或引用调用虚函数时,才发生动态绑定

7.静态类型和动态类型

7.1 概念

1)变量的静态类型在编译时是已知的,是变量声明时的类型

2)变量的动态类型是内存中对象的类型,在运行时才可知

3)如果表达式既不是指针也不是引用,则它的动态类型永远和静态类型一致,当且仅当基类指针或引用调用虚函数时,基类指针或引用的静态类型和动态类型才不同,非虚函数的调用是静态绑定

8.虚函数

8.1 概念

1)基类中:对于某些函数,基类希望它的派生类各自定义自身的版本(简称覆盖),因此基类将这些函数声明为虚函数,用关键字vitual修饰虚函数,vitual只能出现在类的内部

2)派生类中:

  • 派生类可在自身版本的虚函数前加virtual关键字,也可以不加;c++11规定,可以添加关键字override(覆盖)在虚函数形参列表后(包括const和引用限定符)来显式地指明派生类中的虚函数;
  • 如果派生类没有覆盖某个虚函数,则将直接继承基类的版本
  • 如果派生类覆盖了某个虚函数,则它的形参(包括const和引用限定符)必须和对应的基类虚函数一致(不一致将发生隐藏,参数不同的这个函数将是一个新的函数,派生类将继承基类虚函数的版本)

  • 派生类的虚函数的返回类型也必须和基类函数匹配,但有一个例外:当虚函数的返回类型是类本身的指针或引用时
  • 如果虚函数使用默认实参,则基类和派生类的默认实参最好一致

8.2 所有虚函数必须有定义

  通常情况下,我们不使用某个函数,则无需为该函数提供定义,但是对于虚函数,所有虚函数都必须有定义,不管它是否被用到了,因为直到程序运行时才知道到底要调用虚函数的哪个版本,在这之前,编译器也无法确定到底会使用哪个版本的虚函数

8.3 任何构造函数之外的非静态函数都可以是虚函数

1)虚函数的本意在于通过基类的指针或引用来调用派生类版本的成员函数,只需要知道函数接口,不需要对象的具体类型;而构造函数是在创建对象时调用,需要知道对象的具体类型。如果调用构造函数是虚函数的话,编译器构没办法判断要构造继承树中的哪种类型

2)基类定义的静态成员, 将按访问规则被所有的派生类共享

8.4 虚函数与默认实参

1)虚函数也可以有默认实参

2)调用虚函数时如果使用了默认实参,则实参值由本次调用的静态类型决定,换句话说,当我们通过基类的指针调用虚函数时,即使绑定的是派生类对象,也仍然使用基类虚函数中的默认实参

3)所以基类和派生类的虚函数默认实参最好一致

8.5 回避虚函数的机制(非多态调用)

1)使用作用域运算符(::)可以回避

class base {
public:
    virtual void fun()
    {
        cout << 666 << endl;
    }
};

class son : public base {
public:
    void fun() override
    {
        cout << 777 << endl;
    }
};
base baba;
son erzi;
base *p = &erzi;
p->base::fun();//打印结果为666

2)回避虚函数机制通常发生在一个派生类的虚函数里调用基类的虚函数版本,此时如果没有使用作用域运算符,将导致无限递归

8.6 虚函数与内联

  使用非多态调用时,编译器可以选择内联

8.7 虚函数与private

  c++虚函数大多都是public,但也允许是private(java中函数设为private的时,就已经隐式声明为final,所以拒绝多态)

class Base {
private:
    virtual string classID()
    {
        return string("base");
    }

public:
    void work()
    {
        cout << "this class is " << classID() << endl; // 调用私有虚函数
    }

};

class son : public Base {
private:
    string classID() override
    {
        return string("son");
    }
};

int main()
{
    Base* bp = new son();
    bp->work();//打印"this class is son"
    delete bp;
}

8.8 在构造函数和析构函数中调用虚函数时需要注意的问题(effective c++条款9:绝不在构造和析构过程中调用虚函数)

  如果在构造函数和析构函数中需要调用虚函数,必须调用与构造函数和析构函数所属类型对应的虚函数版本,因为:

1)构造一个派生类对象时,先调用的是基类的构造函数,如果在基类的构造函数中调用了派生类版本的虚函数,然后此时派生类成员尚未初始化,显然会出错

2)析构一个派生类对象时,先调用的是派生类的析构函数,再调用基类的析构函数,如果在基类的析构函数中调用了派生类版本的虚函数,此时派生类成员已经被析构,显然会出错

9.虚折构函数

9.1为什么要将基类的析构函数定义为虚函数?

1)首先要明确的是,每个析构函数只会清理自己所在层级创造的成员

2)当delete一个指向派生类对象的基类指针时,如果基类的析构函数没有定义成虚函数,则编译器实现静态绑定,在delete这个基类的指针时,只会执行基类的析构函数,释放基类成员,派生类成员得不到释放,此时会导致释放内存不完全,导致内存泄露,所以必须将基类的析构函数定义为虚函数,保证编译器执行正确的析构函数版本

3)因为基类的析构函数是虚函数,所以无论派生类的析构函数有没有重写(override),都将是虚函数

10.多态

1)接口的多种不同的实现方式即为多态

2)c++支持多态性的根本:指针或引用的静态类型和动态类型可能不同

3)使用基类的指针或引用调用虚函数时,基类指针会依赖运行时的指向(地址值)调用不同版本的虚函数,实现多态

11.纯虚函数

1)在基类中没有实现定义的虚函数,只想要派生类定义自己的版本

2)在基类中用“=0”将一个虚函数说明为纯虚函数

12.抽象基类(抽象类)

1)含有纯虚函数的类是抽象基类

2)抽象基类不能被实例化

13.多重继承

13.1 概念

  派生类由多个直接基类继承而来

13.2 派生类的构造函数

1)多重继承中,如果派生类使用using从多个基类中继承了相同的构造函数(形参列表相同),则将发生错误

class base1 {
public:
    base1(const string& s);
};

class base2 {
public:
    base2(const string& s);
};

class son :public base1, public base2 {
public:
    //编译错误:son试图从两个直接基类都继承son(const string& s)
    using base1 :: base1;
    using base2 :: base2;
};

2)如何解决:派生类为该构造函数定义自己的版本

class son :public base1, public base2 {
public:
    using base1 :: base1;
    using base2 :: base2;
    son(const string& s) :base1(s), base2(s) {}
};

13.3 多重继承和类作用域

1)多重继承下,派生类的作用域还是位于基类作用域之内,但值得注意的是:函数名查找在所有直接基类中同时进行

2)如果函数名在多个基类中被找到,则对该名字的使用将具有二义性,但是这是合法的,在调用时用作用域运算符(::)指明版本就不会出错若在调用时没有指明则会出错

class A {
public:
    void fun()
    {
        cout << 666 << endl;
    }
};

class B {
public:
    
    void fun()
    {
        cout << 777 << endl;
    }
    
};

class C: public A, public B {

};

int main()
{
    C c;
    c.A::fun();    //输出:666
    c.B::fun();    //输出:777

    return 0;
}

14.虚基类

14.1概念

1)如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会有这个共同基类的多份拷贝,若只想保存这个基类的一个实例,可以将这个基类说明为虚基类

2)说明方法:虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的

class CBase { };

class ChildA1: virtual public CBase { };//访问说明符前加virtual,让CBase成为虚基类
class ChildA2: virtual public CBase { };//访问说明符前加virtual,让CBase成为虚基类

class ChildB: public ChildA1, public ChildA2 { };

3)为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中都声明为虚基类,否则仍然会出现对共同基类的多次继承

class CBase { };

class ChildA1 : virtual public CBase { };//访问说明符加了virtual
class ChildA2 : virtual public CBase { };//访问说明符加了virtual
class ChildA3 : public CBase { };//没有加virtual!!!

class ChildB : public ChildA1, public ChildA2, public ChildA3 { };//从ChildA1和ChildA2路径派生的部分只保留一份基类成员,但是从ChildA3路径派生的部分还保留一份基类成员!!!所以此时CBase不是虚基类

14.2虚基类的实现原理

1)虚继承底层实现通过虚基类指针vbptr虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用类对象的存储空间)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了)

2)当虚继承的子类被当做父类继承时,虚基类指针也会被继承

3)虚基类表指针vbptr(virtual base table pointer)指向一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员

  D中的int a不是来自B也不是来自C,而是另外的一份从A直接靠过来的成员

  写的很烂,看下面的参考资料吧……

参考资料

https://blog.csdn.net/bxw1992/article/details/77726390

https://blog.csdn.net/xiejingfa/article/details/48028491

https://blog.csdn.net/a2796749/article/details/44014821

15.面向过程和面向对象的区别

  面向过程就是分析出解决问题所需要的步骤,然后按这些步骤一步一步去实现;面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述在整个解决问题中的某类行为

16.面向对象设计的基本原则

1)单一职责原则(Single Responsibility Principle):每一个类应该专注于做一件事情。

原文地址:https://www.cnblogs.com/Joezzz/p/10001880.html