一、类的继承
1.1 什么是继承
(1) 继承是C++源生支持的一种语法特性,是C++面向对象的一种表现
(2) 继承特性可以让派生类“瞬间”拥有基类的所有(当然还得考虑权限)属性和方法
(3) 继承本质上是为了代码复用
(4) 类在C++编译器的内部可以理解为结构体,派生类是由基类成员叠加派生类新成员得到的
1.2 继承的特性
(1) 符合现实世界的本质规律,而不是纯粹人为施加的。
(2) 继承inheritance和组合composition是软件重用的2种有效方式
(3) 派生类,范围小,具体。 基类,范围大,抽象
(4) 派生类又叫子类,基类又叫父类
1.3 继承的语法
class 派生类名:访问控制 基类名1,访问控制 基类名2,访问控制 基类名n
{
// 成员变量和成员方法列表(除了构造和析构之外的成员方法)
};
1.4 继承的演示
定义一个基类 Animal ,子类Dog由父类Animal继承而来
class Animal
{
public:
void speak(void)//纯虚函数 相当于只有函数指针
{
std::cout << "~Animal" <<std::endl;
}
};
class Dog:public Animal
{
public:
};
二、继承的权限管控
2.1 类的权限管控
(1) public 类内部可以访问,类的外部可以访问
(2) private 类内部可以访问,类的外部不能访问
(3) protected 类内部可以访问,类的外部不能访问
2.2 不同权限的继承方式对派生类的影响
(1) public 继承(公有继承):父类成员在子类中保持原有访问级别
(2) private 继承(私有继承):父类成员在子类中变为private成员 只有子类内部的成员方法可以访问
(3) protected继承(保护继承):父类中public成员会变成protected
父类中protected成员仍然为protected
父类中private成员仍然为private
(4) 若继承时未描述权限,当派生类为class则默认为private继承;当派生类为struc则默认为public继承
2.3 设计类时如何规定成员的访问权限
(1) 需要被外界访问的成员直接设置为public
(2) 只能在当前类中访问的成员设置为private 作为基类被继承后不能被派生类直接使用,只能通过基类的public方法来实现间接访问
(3) 只能在当前类和子类中访问的成员设置为protected
2.4 继承权限管控的总结
public继承:
父类的public成员 在子类中是public的
父类的protected成员 在子类中是protected的
父类的private成员 在子类中是private的,权限进一步降低,只能通过父类的public方法间接访问
private继承:
父类中的public成员 在子类中是private
父类中的protected成员 在子类中是private
父类中的private成员 在子类中是private,
protected继承:
父类中的public成员 在子类中是protected成员
父类中的protected成员 在子类中是protected成员
父类中的private成员 在子类中是private成员,权限进一步降低,只能通过父类的public方法间接访问
1、父类的private成员,通过public、protected继承在子类中是private成员,访问权限进一步降低,子类只能通过父类中继承public方法来间接访问
2、父类中的private权限成员,通过private继承,访问权限不会降低
3、父类的public成员,在public继承下到子类中还是public,在protected继承下到子类中是protected的,在private继承下到子类中是private的。
4、父类的protected成员,在public继承下到子类中是protected的,在protected继承下到子类中是protected的,在private继承下到子类中是private的。
5、在继承中父类中所有的成员(不管哪种权限)都会被继承给子类,而不会丢失任何一个。
三、继承后子类和父类的关系
3.1 子类的作用
(1) 吸收基类成员:除过构造和析构函数以外的所有成员全部吸收进入派生类中
(2) 更改继承的成员
更改访问控制权限(根据继承类型还有成员在基类中的访问类型决定)
同名覆盖(派生类中同名成员覆盖掉基类中)
(3)添加派生类独有的成员
3.2 本质上是2个独立的类
(1) 继承只是通过父类来快速构建子类的一种语法技术,继承后得到的子类和父类是独立的2个类
(2) 程序中子类的对象和父类的对象是2个独立的对象,没有任何关系,只是形成对象的模板有部分相同
(3) 子类对象中访问的子类从父类继承而来的成员,其实是属于子类自己的,并不是父类(对象)里面的
3.3 站在子类角度看继承
(1) 非继承体系下的类,完全是自己“手工”构建的,所有成员根据访问权限不同而分为1个层级3大块
(2) 继承体系下的子类,部分直接从父类继承,部分自己手工打造,所有成员分成2个层级(继承来的,自己写的),每个层级3大块(访问权限)
(3) 继承就是子类在构建自己的过程中使用的一种快速批量成员获取方法
四、派生类和基类的构造析构关系
class Person
{
public:
string name;
int age;
bool male;
Person();
Person(string setname);
Person(string setname,int setage)
{
std::cout << "Person(string setname,int setage)" << std::endl;
};
~Person();
};
class man:public Person
{
public:
//构造函数
//man();//这是简写
man():Person()//这是完整的写法
{
std::cout << ":man():Person()" << std::endl;
}
man(int manage):Person()//构造函数重载的完整写法
{
std::cout << "man():Person()" << std::endl;
}
//构造函数重载 参数匹配调用基类的构造函数
man(string setname ,int setage,int setmale);
~man()
{
std::cout << "~man" << std::endl;
}
// ~man():~Person;错误写法
};
//构造函数重载 参数匹配调用基类的构造函数
man::man(string setname ,int setage,int setmale):Person(setname,setage)
{
//派生类 内部调用父类的成员变量需要加上作用域符号 否则访问不到
Person::name=setname;
Person::age=setage;
Person::male=setmale;
}
4.1 派生类的构造析构函数
派生类并不继承基类的构造和析构函数,只继承成员变量和普通成员方法,
不继承,意思是派生类中确实没有,不包含基类的构造和析构函数
派生类的构造函数一定会调用基类的构造函数,析构也一样
4.2 派生类的构造(析构)为什么必须调用基类的某个构造(析构)
构造函数的2大作用:初始化成员,分配动态内存
派生类和基类分别有构造函数和析构函数,所以是分别管理各自的成员初始化,各自分配和释放各自所需的动态内存
继承的特性允许派生类调用基类的构造和析构函数,以管理派生类从基类继承而来的那些成员。
派生类的构造和析构处理的永远是派生类自己的对象,只是派生类对象模板中有一部分是从基类继承而来的而已。
4.4 派生类和基类的构造析构函数遵循栈规则
构造函数的调用顺序是先基类再派生类,
析构函数的调用顺序是先派生类再基类,
五、派生类和基类的同名成员问题
class Person
{
public:
string name;
int age;
bool male;
void work(void)
{
std::cout << "Person class work" << std::endl;
}
};
class man:public Person
{
public:
//和基类成员函数同名 若派生类的对象调用 此方法 会覆盖掉基类的同名方法 叫重定义
void work(void)
{
std::cout << "man class work" << std::endl;
}
};
5.1 派生类中再实现一个基类中的方法会怎样?
结论:基类对象调用的是基类的方法,派生类对象调用执行的是派生类中重新提供的方法
派生类中同名同参方法替代掉基类方法的现象,叫做:重定义(redefining)或者 隐藏。
实际同时存在2份同名同参(但在不同类域名中)的方法,同时都存在,只是一个隐藏了另一个
5.2 派生类中如何访问被隐藏的基类方法
派生类对象直接调用时,隐藏规则生效,直接调用的肯定是派生类中重新实现的那一个,就是C语言的作用域 县官不如现管
将派生类强制类型转换成基类的类型(通过指针实现),再去调用这时编译器认为是基类在调用,则调用的是基类那一个,隐藏规则被绕过了
在派生类内部,使用父类::方法()的方式,可以强制绕过隐藏规则,调用父类实现的那一个
int main()
{
//派生类 的对象可通过类作用域符号来访问被隐藏的基类方法,绕过隐藏规则
man PersonMan;
PersonMan.Person::work();
}
5.3 派生类和基类的同名成员问题总结
派生类的成员方法、成员变量都遵循隐藏规则
隐藏规则本质上是大小作用域内同名变量的认领规则问题,实际上2个同名成员都存在当前派生类的对象内存中的
隐藏(redefining)、与重载(overload)、重写(override),这三个概念的区分
六、子类和父类的类型兼容规则
class Person
{
public:
string name;
int age;
bool male;
void work(void)
{
std::cout << "Person class work" << std::endl;
}
};
class man:public Person
{
public:
void sleep(void)
{
std::cout << "man sleep" << std::endl;
}
};
6.1 何为类型兼容规则
派生类是基类的超集,基类有的派生类都有,派生类有的基类不一定有,所以这2个类型间有关联
派生类对象可以裁剪后当作基类对象,而基类对象不能放大成派生类对象
考虑到指针和引用与对象指向后,派生类和基类对象的访问规则就是所谓类型兼容规则
6.2 子类对象可以当作父类对象使用
子类对象可以无条件隐式类型转换为一个父类对象
子类对象可以直接初始化或直接赋值给父类对象
int main()
{
man PersonMan;
//定义时直接复制
Person P1 = PersonMan;//调一次Person类的构造函数
//先定义,再直接赋值
Person P2 ; //调一次Person类的构造函数
P2 = PersonMan;//再一次调Person类的构造函数
}
父类指针可以直接指向子类对象
int main()
{
man PersonMan;
//定义时直接复制
Person * P1 = &PersonMan;//调一次Person类的构造函数
//先定义,再直接赋值
Person *P2 ; //调一次Person类的构造函数
P2 = &PersonMan;//再一次调Person类的构造函数
}
父类引用可以直接引用子类对象
int main()
{
man PersonMan;
Person &p1 = PersonMan;
}
6.5 类型兼容规则的总结
派生类对象可以作为基类的对象使用,但是只能使用从基类继承的成员
类型兼容规则是多态性的重要基础之一
子类就是特殊的父类 (base *p = &child;)
七、继承的优势与不良继承
7.1 为什么会有继承
(1) 本质上为了代码复用
(2) 继承方式很适合用来构建复杂框架体系
(3) 用继承来设计类进而构建各层级对象,符合现实中的需要
7.2 何为不良继承
(1) 鸵鸟不是鸟问题。因为鸵鸟从鸟继承了fly方法但是鸵鸟不会飞
(2) 圆不是椭圆问题。因为圆从椭圆继承了长短轴属性然而圆没有长短轴属性
(3) 不良继承是天然的,是现实世界和编程的继承特性之间的不完美契合
7.3 如何解决不良继承
修改继承关系设计,既然圆继承椭圆是一种不良类设计就应该杜绝。
去掉继承关系,两个类可以继承自同一个共同的父类,不过该类不能执行不对称的setSize计算,然后在圆和椭圆这2个子类中分别再设计以区分
所有不良继承都可以归结为“圆不是椭圆”这一著名具有代表性的问题上。
在不良继承中,基类总会有一些额外能力,而派生类却无法满足它。这些额外的能力通常表现为一个或多个成员函数提供的功能。
要解决这一问题,要么使基类弱化,要么消除继承关系,需要根据具体情形来选择。
八、组合与继承的对比
OO设计原则是优先组合,而后继承
8.1 什么是组合
组合,就是在一个class内使用其他多个class的对象作为成员,语法为:classname Name;
组合,也是一种代码复用方法,本质也是结构体包含
class Person
{
public:
string name;
int age;
bool male;
void work();
};
class kid
{
public:
Person person; //组合
};
8.2 继承的特点
继承是具有传递性,不具有对称性,单向的
继承是白盒复用,继承允许子类根据自己的实现来覆盖重写父类的实现细节,父类的实现对于子类是可见的
继承的白盒复用特点,一定程度上破坏了类的封装特性,因为这会将父类的实现细节暴露给子类
8.3 组合的特点
组合是包含的关系
组合属于黑盒复用,被包含对象的内部细节对外是不可见的,所以它的封装性相对较好,实现上相互依赖比较小 只能通过对象来访问
组合中被包含类会随着包含类创建而创建,消亡而消亡。
可以获取其它具有相同类型的对象引用或指针,在运行期间动态的定义组合。而缺点就是致使系统中的对象过多。
九、多继承的二义性
9.1 什么是多继承
class 派生类名:访问控制 基类名1,访问控制 基类名2,访问控制 基类名n
{
// 成员变量和成员方法列表(除了构造和析构之外的成员方法)
};
多继承就是一个子类有多个父类
多继承和单继承的原理,效果并无明显区别
多继承会导致二义性问题
9.2 多继承的二义性
(1) C多继承自A和B,则C中调用A和B的同名成员时会有二义性
class A//加上virtual关键字可解决菱形继承
{
public:
void work(void)
{
std::cout << "A work" <<std::endl;
}
};
class B
{
public:
void work(void)
{
std::cout << "B work" <<std::endl;
}
};
class c : public A,public B
{
};
int main()
{
c Person;
Person.work();//编译报错 work方法有歧义,不知道调用哪个类里面的方法
}
(2) 菱形继承问题, B1:A, B2:A, C:B1,B2,此时用C的对象调用A中的某个方法
class A
{
public:
void work(void)
{
std::cout << "A work" << std::endl;
}
};
class B1:public A//加上virtual关键字可解决菱形继承
{
};
class B2:public A
{
};
class c : public B1 ,public B2
{
};
int main()
{
c Person;
Person.work();//编译报错 work方法有歧义,不知道调用哪个类里面的方法
}
9.3 多继承二义性如何解决
解决办法1:避免出现,让A和B的public成员命名不要重复冲突,但这个有时不可控。
解决办法2:编码时明确指定要调用哪一个,用c.A::func()明确指定调用的是class A的func而不是class B的
int main()
{
c Person;
//用类作用域符,此时表示通过C的对象Person调用A类的work方法
Person.A::work();
}
解决办法3:在C中重定义work();func,则调用时会调用C中的func,A和B中的都被隐藏了
9.4 总结
以上三种办法都能解决,但是都没有很好的解决
随着系统的变大和变复杂,难免出现二义性,这不是程序员用不用心的问题,是系统自身带来的
解决二义性不能靠程序员个人的细心和调试能力,而要靠机制,也就是编程语言的更高级语法特性
虚函数、虚继承、纯虚函数、抽象类、override(重写,覆盖)、多态等概念就是干这些事的
十、虚继承
10.1 虚继承实现原理
虚基类表指针vbptr和虚基类表virtual table
参考:虚继承实现原理
10.2 虚继承用法
继承时,在继承方式的前面加上关键字 virtual
class A
{
public:
void work(void)
{
std::cout << "A work" << std::endl;
}
};
class B1:virtual public A//加上virtual关键字可解决菱形继承
{
};
class B2:virtual public A
{
};
class c : public B1 ,public B2
{
};
10.3 虚继承解决的问题
虚继承就是为了解决菱形继承和二义性问题而存在
虚继承和虚函数(为了实现多态特性)并没有直接关系
十一、多态、虚函数
通过指针调用对象的方法
不加 virtual 关键字
class Animal
{
public:
void speak(void)
{
std::cout << "Animal speak" <<std::endl;
}
};
class Dog:public Animal
{
public:
void speak(void)
{
std::cout << "wang wang wang!" <<std::endl;
}
};
int main()
{
//本意是希望通过 animal 指针调用dog对象里面的speak()方法,打印出 wang wang wang!
Dog dog;
Animal *animal = &dog;
//实际调用的为类Animal里面的speak()方法
animal->speak(); //结果:Animal speak
}
加 virtual 关键字
class Animal
{
public:
virtual void speak(void)
{
std::cout << "Animal speak" <<std::endl;
}
};
class Dog:public Animal
{
public:
void speak(void)
{
std::cout << "wang wang wang!" <<std::endl;
}
};
int main()
{
//本意是希望通过 animal 指针调用dog对象里面的speak()方法,打印出 wang wang wang!
Dog dog;
Animal *animal = &dog;
//实际调用的为类Dog类里面的speak()方法
animal->speak(); //结果:wang wang wang!
}
11.1 虚函数的用法
在成员方法前加上关键 virtual,就代表当前方法是一个虚函数
class Animal
{
public:
virtual void speak(void)
{
std::cout << "Animal speak" <<std::endl;
}
};
11.2 什么是多态
从宏观讲,多态就是要实现一套逻辑多种具体适配的执行结果
从微观讲,多态就是要一套代码在运行时根据实际对象的不同来动态绑定、执行相匹配的具体函数
函数声明前加virtual的即是虚函数
虚函数是C++实现多态特性的基础,从语法上讲多态特性的基类方法必须是虚函数
基类中方法声明为virtual,派生类中重新实现同名方法,这就叫override(中文为覆盖,或重写)
11.4 overload、redifining、override
这三个的区别
(1) overload 重载 同一个类里面的多个方法,函数名相同但参数列表不同
(2) redifining 重定义,隐藏 继承中子类再次实现父类中同名方法把父类方法隐藏掉
(3) override 覆盖,重写 继承中子类去实现父类中同名virtual方法实现多态特性
十二、纯虚函数和抽象类
12.1 纯虚函数
纯虚函数就是基类中只有原型没有实体的一种虚函数
纯虚函数形式:virtual 函数原型 = 0
纯虚函数是不会用内存,因此纯虚函数所在的类根本无法实例化对象
class Animal
{
public:
virtual void speak(void) = 0;//纯虚函数 相当于只有函数指针
};
12.2 抽象类
(1) 带有纯虚函数的类称为抽象类。抽象类只能作为基类来派生新类,不可实例化对象
(2) 派生类必须实现基类的纯虚函数后才能用于实例化对象
(3) 抽象类的作用:将有关的数据和行为组织在一个类里面,保证派生类具有要求的行为
(4) 抽象类暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现
(5) 抽象类的子类必须实现基类中的纯虚函数,这样子类才能创建对象,否则子类就还是个抽象类
12.3 抽象类实现接口
(1) 接口是一种特殊的类,用来定义一套访问接口,也就是定义一套规约
(2) 接口类中不应该定义任何成员变量
(3) 接口类中所有成员函数都是公有且都是纯虚函数
(4) 接口其实就是个纯粹的抽象基类
十三、虚析构函数
13.1 什么是虚析构函数
就是在显示析构函数前面加上 virtual 关键字
规则:基类有1个或多个虚函数时(注意不要求是纯虚函数),则其析构函数应该声明为 virtual
class Animal
{
public:
// virtual void speak(void) = 0;//纯虚函数 相当于只有函数指针
virtual void speak(void)
{
std::cout << "Animal speak" <<std::endl;
}
~Animal()
{
std::cout << "~Animal" <<std::endl;
}
};
class Dog:public Animal
{
public:
void speak(void)
{
std::cout << "Dog speak" <<std::endl;
}
Dog(){};
Dog(int a)
{
std::cout << "Dog a: " << a << std::endl;
}
~Dog()
{
std::cout << "~Dog" << std::endl;
}
};
13.2 虚析构函数的使用场景
在栈上创建一个基类Animal 的子类Dog的对象dog
当栈自动释放后现象应该为:①先调用Dog的析构函数,②再调用Animal 的析构函数
代码运行结果:
先调用Dog的析构函数输出:~Dog
再调用Animal 的析构函数输出:~Animal
int main()
{
Dog dog;
Animal *animal = &dog;
animal->speak();
}
在堆上创建一个基类Animal 的子类Dog的对象dog
当通过delete 关键字释放后,现象应该为:①先调用Dog的析构函数、②再调用Animal 的析构函数
代码运行结果:只调用了父类的析构函数打印输出了 ~Animal
而子类Dog的析构函数没被调用,造成资源没被释放
int main()
{
Animal * animal;
animal = new Dog();//分配在堆上
animal->speak();
delete animal; //手动释放
}
解决这个问题,就需要在父类的析构函数中加上 virtual 关键字
class Animal
{
public:
// virtual void speak(void) = 0;//纯虚函数 相当于只有函数指针
virtual void speak(void)
{
std::cout << "Animal speak" <<std::endl;
}
virtual ~Animal()
{
std::cout << "~Animal" <<std::endl;
}
};
13.3 虚析构函数总结
虚析构函数在各种情况下总能调用正确的(和对象真正匹配的)析构函数。
虚函数virtual的价值,就是让成员函数在运行时动态解析和绑定具体执行的函数,这是RTTI机制的一部分。
析构函数也是成员函数,加virtual的效果和普通成员函数加virtual没什么本质差异
加virtual是有开销的,运行时动态绑定不如编译时静态绑定效率高资源消耗优,但是可以多态。
十四、using关键字
14.1 using关键字的使用场景
父类的public方法通过private、protected权限继承后,到了子类就成了private、protected而不是public了,无法用子类对象来调用了。
14.2 using关键字的格式用法
在子类public声明中使用 using Base::func; 即可,不带返回值类型不带参数列表
class Animal
{
public:
void speak(void)
{
std::cout << "Animalspeak" <<std::endl;
}
};
class snake:private Animal
{
public:
using Animal::speak;
};
14.3 注意事项
using只用于private/protected继承中的权限损失找回
如果方法在父类中本来就是private权限,在子类中没法用using修饰后访问