一、OOP概述
面向对象的程序设计(object-oriented programming)的核心思想是数据抽象、继承和动态绑定;
继承:基类、派生类、虚函数
派生列表:首先是冒号,后面紧跟以逗号分隔的基类列表,每个基类前面可以有访问说明符;
派生类可以在重新定义的虚函数前面加 virtual,也可以不加;
派生类可以显示的注明使用哪个成员函数改写基类的虚函数,做法是在参数列表后加 override 关键字;
动态绑定:
当使用基类的指针或引用调用一个虚函数时,将发生动态绑定,由于在运行时发生绑定,所以又称为运行时绑定;
二、定义基类和派生类
1、定义基类
基类通常都应该定义虚析构,即使该函数不执行任何操作也是如此;
构造函数和静态函数不可以是虚函数;
virtual 只能出现在声明中,不能出现在定义中;
如果基类把函数声明成虚函数,则在派生类中隐式的也是虚函数;
派生类能访问公有成员,不能访问私有成员;
派生类能访问受保护成员,而其他用户则不能访问;
2、定义派生类
override 关键字放在 const 关键字和引用限定符后面;
能将基类的指针或引用绑定到派生类对象中的基类部分,编译器会隐式的执行派生类到基类的转换;
在派生类中含有与其基类对应的组成部分,是继承的关键所在;
派生类的构造函数:
派生类必须使用基类的构造函数初始化基类部分;也就是每个类控制它自己的成员初始化部分;
派生类构造函数通过初始化列表将实参传递给基类构造函数;形式是类名加圆括号内的实参列表;
首先初始化基类部分,然后按照声明顺序依次初始化派生类的成员;
class BB : AA
{
public:
BB(int a, int b) :AA(a), b(b) {}
private:
int b;
}
派生类可以访问基类的公有成员和受保护成员;
继承与static成员:
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义;
如果基类的static成员是private的,则派生类无权访问;如果是public的,则既可以通过基类使用,也可以通过派生类使用;
派生类的声明:
声明中包含类名,但不包含派生列表;一个类不能派生自身;
class a : a //错误
{};
被用作基类的类
仅仅声明而没被定义的类不能用作基类;
一个类是基类的同时也可以是派生类;
最终派生类将包含直接基类子对象以及每个间接基类的子对象;
防止继承的发生:
防止一个类作为基类,可以在类名后面加关键字 final ,但可以继承别的类;
class a
{};
class b final : public a
{};
class c : public b //错误
{};
3、类型转换与继承
智能指针也支持派生类向基类的转换;
不存在从基类向派生类的隐式类型转换;
类类型的对象之间不存在类型转换:
当用一个派生类对象为一个基类对象初始化或赋值时,只有派生类中的基类部分被初始化和赋值,派生类部分被忽略掉了;
4、存在继承关系的类型之间的转换规则
1、派生类向基类的类型转换只对指针有效;
2、基类向派生类不存在隐式类型转换;
3、派生类向基类的转换可能由于访问权限而变得不可行;
4、通常能够将一个派生类对象拷贝、赋值、移动给一个基类对象,但只处理派生类对象的基类部分;
三、虚函数
动态绑定只有通过指针或引用调用虚函数时才发生;
当且仅当对指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型与静态类型可能不同;
派生类中的虚函数:
派生类的虚函数的形参类型必须与基类的完全一致;
返回类型与基类的也应该一致,但如果基类返回B*,则派生类可返回D*,但要求从D到B的类型转换是可访问的;
override关键字:
class B
{
public:
virtual void f1() {}
virtual void f2() {}
void f3() {}
};
class D : public B
{
public:
void f1() override {}
void f2(int) override {} //错误,参数列表不同
void f3() override {} //错误,基类的f2不是虚函数
void f4() override {} //错误,基类没有该虚函数
};
final 关键字:
final既可以修饰类也可以修饰成员函数,如果修饰函数,只能修饰虚函数;
final和override只能出现在参数列表(包括const和引用限定符)和尾置返回类型之后;
class B
{
public:
virtual void f1() {}
};
class D1 : public B
{
public:
void f1() override final {} //由于基类f1为虚函数,所以f1默认为虚函数
};
class D2 : public D1
{
public:
void f1() override {} //错误,由于D1的f1有关键字final修饰,所以不能被派生类覆盖
};
虚函数与默认实参:
虚函数也可以拥有默认实参,实参值由本次调用的静态类型决定;
class B
{
public:
virtual void f1(int a = 1)
{
cout << a << endl;
}
};
class D : public B
{
public:
virtual void f1(int a = 2)
{
cout << a << endl;
}
};
shared_ptr<B> p = make_shared<D>();
p->f1(); //调用D中的f1,但却输出1
如果虚函数使用默认实参,则基类与派生类的默认实参最好一致;
回避虚函数机制
使用作用域运算符强制执行虚函数的某个版本;
p->B::f1();//调用B中的f1;
四、抽象基类
含有(或者未覆盖而直接继承)纯虚函数的类是抽象基类;
不能创建抽象基类的对象,注意没有覆盖纯虚函数的派生类也是抽象基类;
派生类构造函数只初始化它的直接基类;
五、访问控制与继承
受保护成员:
派生类及其友元不能访问基类对象的受保护成员,只能访问派生类对象中基类部分的;
class B
{
protected:
int a = 1;
};
class D : public B
{
friend void f1(B & a);
friend void f2(D & a);
};
void f1(B & a)
{
cout << a.a << endl; //错误,派生类及其友元不能访问基类对象的受保护成员
}
void f2(D & a)
{
cout << a.a << endl; //正确,派生类及其友元只能访问派生类对象中基类部分的
}
公有、私有、受保护继承:
派生访问说明符对于派生类的成员及其友元能否访问其直接基类的成员没有影响,只跟其直接基类的访问说明符有关;
派生访问说明符的目的是在于控制派生类用户(包括派生类的派生类)对基类成员的访问权限;
派生类向基类转换的可访问性:
只有当D公有的继承B时,用户代码才能使用派生类向基类的转换,其他继承方式则不行;
不论D以什么方式继承B,D的成员函数及其友元都能使用派生类向基类的转换;
D以公有或受保护继承B,D的派生类的成员函数及其友元能使D向B的转换,私有继承则不行;
总之:如果基类的公有成员是可访问的,则派生类向基类的转换也是可访问的,反之不行;
class B{};
class D1 : protected B
{
B *pb = this; //正确,不论D以什么方式继承B,D的成员函数及其友元都能使用派生类向基类的转换
};
class D2 : public D1
{
B *pb = new D1; //正确,D以公有或受保护继承B,D的派生类的成员函数及其友元能使D向B的转换
};
B *pb = new D1; //错误,只有当D公有的继承B时,用户代码才能使用派生类向基类的转换
友元与继承:
友元既不能传递也不能继承
改变个别成员的可访问行:通过using声明
通过在类内部使用using声明,可将该类的直接或间接基类中的可访问成员(非私有成员)标记出来;
using声明中名字的访问权限由using声明语句之前的访问说明符决定:
class B
{
public:
int a;
int b;
};
class D : private B //私有继承
{
public:
using B::a; //变为公有
protected:
using B::b; //变为受保护
};
默认的继承保护级别:
使用class关键字定义的派生类是私有继承的;
使用struct关键字定义的派生类是公有继承的;
class B {};
class D1 :B {}; //私有继承
struct D2 : B {}; //公有继承
设置保护级别时,要显示的声明出来,不要依赖于默认的保护级别;
六、继承中的类作用域
1、派生类的作用域嵌套在基类的作用域之内;
2、在编译时进行的名字查找:
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的;
3、名字冲突与继承
派生类的成员将隐藏基类的同名成员;所以可以通过作用域运算符来使用隐藏的成员;
建议:除了覆盖继承而来的虚函数外,派生类最好不要重用基类中的成员名字;
名字查找先于类型检查:即使派生类成员函数的参数列表与基类中同名成员的参数列表不一致,也会隐藏基类成员;
4、派生类覆盖基类的重载函数:
派生类一旦声明了一个和基类重载函数同名的函数,派生类将会覆盖基类的所有重载函数;
class Base
{
public:
void func() { printf("Base func()\n"); };
void func(int a) { printf("Base func(int a)\n");}
};
class D1 : public Base
{
public:
void func(string& str) { printf("D1 func(string& str)\n"); } //覆盖基类的所有重载函数
};
using声明语句指定一个名字而不指定形参列表,一条using声明语句可以将基类成员函数的所有重载实例添加到派生类作用域中;
class D1 : public Base
{
public:
using Base::func; //将基类的func所有重载实例到包含到D1中
void func(string& str) { printf("D1 func(string& str)\n"); }
};
七、拷贝控制与销毁
1、虚析构函数:
如果基类的析构函数不是虚函数,则delete一个指向派生类的基类指针将产生未定义的行为;
虚析构函数会阻止合成移动操作;
2、合成拷贝控制与继承
类似于类中包含其他类类型的成员;
3、派生类的拷贝控制成员
当派生类定义了拷贝或移动操作时,该操作也要负责包括基类在内的整个对象;
如果想拷贝或移动基类部分,则必须在派生类构造函数的初始值列表中显示的使用基类的拷贝或移动构造函数:
class B{};
class D : public B
{
public:
D() :B() {}
D(const D & d) :B(d) {}
D(D&&d) :B(std::move(d)) {}
}
如果初始值列表中没有显示的调用,则基类部分则使用默认的构造函数进行初始化;
与拷贝或移动构造一样,派生类的赋值运算也必须显示的调用基类的赋值运算符;
D& operator=(const D &d)
{
B::operator=(d);
return *this;
}
D& operator=(D &&d)
{
B::operator=(std::move(d));
return *this;
}
派生类的析构函数:
派生类的析构函数首先执行,然后时直接基类的,一次类推;
在构造函数和析构函数中调用虚函数:
如果构造函数和析构函数调用了某个虚函数,则应该执行与构造函数和析构函数相对应的虚函数版本;
4、继承的构造函数
提供一条(直接)基类名的using声明语句;
对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数;
class B
{
public:
B(int a){}
};
class D
{
public:
using B::B;
//等价方式
//derived(parms):base(args){}
//D(int a):B(a){}
};
如果派生类含有自己的成员,这些成员将被默认初始化;
继承的构造函数的特点:
和普通成员的using声明不一样,构造函数的using声明不会改变该构造函数的访问级别;
using声明语句不能指定explicit或constexpr;
如果基类包含多个构造函数,除了两种情况,派生类会继承所有这些构造函数;
1、如果派生类的构造函数与基类的构造函数由相同的参数列表,则不会被继承;
2、默认、拷贝、移动构造函数不会被继承,这些构造函数会被合成;
八、容器与继承
当派生类对象赋值给基类对象时,派生类部分将被切掉,因此容器和存在继承关系的类型无法兼容;
在容器中放置(智能)指针,而非对象;因为派生类(智能)指针可以转换为基类的(智能)指针;