一、继承
继承就是在一个已经存在的类的基础上建立一个新的类,并拥有其特性,体现了代码复用的思想。继承是面向对象的三大特性之一。
已经存在类被称为“基类”或“父类”;
新建立的类被称为“派生类”或“子类”。
派生类除了可以继承基类外,还可以对其做出必要的代码修改(函数隐藏)和增加。
#include <iostream>
using namespace std;
class Father
{
public:
string first_name = "张";
void work()
{
cout << "我是一个木匠" << endl;
}
};
class Son:public Father
{
public:
//隐藏基类中的work()函数
void work()
{
cout << "我是一个程序员" << endl;
}
//新增函数
void play()
{
cout << "我喜欢玩" << endl;
}
};
int main()
{
Father fa;
cout << fa.first_name << endl;
fa.work();
Son son;
//从基类中继承了姓氏
cout << son.first_name << endl;
//对基类中的函数隐藏
son.work();
//可以通过作用域来调用基类中被隐藏的成员
son.Father::work();
//派生类中新增的函数
son.play();
return 0;
}
需要注意的是,基类和派生类是相对的,某个类可能既是类A的基类,又是类B的派生类;派生类往往比基类更具体,基类往往比派生类更抽象。
1.2 继承中的构造函数
1.2.1基类与派生类的关系
类的构造函数和析构函数不能被继承。
#include <iostream>
using namespace std;
class Father
{
private:
string name;
public:
Father(string name):name(name){}
string get_name() const
{
return name;
}
void set_name(string name)
{
this->name = name;
}
};
class Son:public Father
{
public:
};
int main()
{
// Son s("张三"); 错误
return 0;
}
上面的例子可以看出,派生类并没有继承基类的带参数的构造函数。实际上1.1节中的派生类Son也没有继承Father的构造函数,这是因为当程序员不手写构造函数时,编译器会增加以下代码:
- 给基类Father增加一个无参构造函数
- 给派生类Son增加一个无参构造函数
- 在派生类Son的无参构造函数中,调用基类的无参构造函数
因为派生类中省略了基类的代码,因此在创建派生类对象时,需要调用基类的代码完成派生类中基类代码部分内存的开辟,即派生类的任意一个构造函数都必须直接或间接调用基类的任意一个的构造函数。
派生类调用基类构造函数的写法:
- 透传构造
- 委托构造
- 继承构造(只是叫这个名字,并没有继承构造函数)
1.2.2 透传构造
透传构造指的是在派生类的构造函数中直接调用基类的构造函数。
#include <iostream>
using namespace std;
class Father
{
private:
string name = "无名氏";
int age = 1;
public:
Father(string name)
:name(name)
{
cout << "Father的一参构造函数" << endl;
}
Father(string name,int age)
:name(name),age(age)
{
cout << "Father的二参构造函数" << endl;
}
void show()
{
cout << name << " " << age << endl;
}
};
class Son:public Father
{
public:
Son(string name)
:Father(name)
{
}
Son(string name,int age)
:Father(name,age)
{
}
};
int main()
{
Son s("张三",12);
s.show();
Son s2("李四");
s2.show();
return 0;
}
1.2.3 委托构造
委托构造指的是,某个类的构造函数可以调用这个类的另一个重载的构造函数。
#include <iostream>
using namespace std;
class Father
{
private:
string name = "无名氏";
int age = 1;
public:
Father(string name)
:name(name)
{
cout << "Father的一参构造函数" << endl;
}
Father(string name,int age)
:name(name),age(age)
{
cout << "Father的二参构造函数" << endl;
}
void show()
{
cout << name << " " << age << endl;
}
};
class Son:public Father
{
public:
Son(string name)
:Son(name,10)
{
}
Son(string name,int age)
:Father(name,age)
{
}
};
int main()
{
Son s("张三",12);
s.show();
Son s2("李四");
s2.show();
return 0;
}
委托构造需要注意以下几点:
- 如果是派生类,最终委托的构造函数要透传调用基类的构造函数。
- 不要形成委托闭环。
1.2.4 继承构造
是C++11的新写法,在派生类中使用继承构造后,编译器会按照基类的构造函数格式,创建出派生类的构造函数,并且分别使用透传构造调用基类的同参数构造函数。
#include <iostream>
using namespace std;
class Father
{
private:
string name = "无名氏";
int age = 1;
public:
Father(string name)
:name(name)
{
cout << "Father的一参构造函数" << endl;
}
Father(string name,int age)
:name(name),age(age)
{
cout << "Father的二参构造函数" << endl;
}
void show()
{
cout << name << " " << age << endl;
}
};
class Son:public Father
{
public:
using Father::Father; // 继承构造
};
int main()
{
Son s("张三",12);
s.show();
Son s2("李四");
s2.show();
return 0;
}
1.3 对象的创建与销毁
#include <iostream>
using namespace std;
/**
* @brief The Value class
* 作为其它类的变量
*/
class Value
{
private:
string name;
public:
Value(string name):name(name)
{
cout << name << "创建了" << endl;
}
~Value()
{
cout << name << "销毁了" << endl;
}
};
class Father
{
public:
static Value s_value;
Value value = Value("Father类的成员变量");
Father()
{
cout << "Father的构造函数" << endl;
}
~Father()
{
cout << "Father的析构函数" << endl;
}
};
Value Father::s_value = Value("Father类的静态成员变量");
class Son:public Father
{
public:
static Value s_value;
Value value = Value("Son类的成员变量");
Son()
{
cout << "Son的构造函数" << endl;
}
~Son()
{
cout << "Son的析构函数" << endl;
}
};
Value Son::s_value = Value("Son类的静态成员变量");
int main()
{
cout << "主函数开始执行" << endl;
{ // 局部代码块
Son s;
cout << "Son类对象使用中......" << endl;
}
cout << "主函数结束执行" << endl;
return 0;
}
上述运行结果可以发现遵循如下规律:
- 创建流程与销毁流程对称。
- 相同部分的创建,基类先派生类后;相同部分的销毁,派生类先基类后。
- 静态成员先创建,后销毁。
一个派生类对象的创建与销毁需要逐层调用到最上层的基类,因此体现了面向对象编程的特点:编写效率高,运行效率低。
1.4 多重继承
之前的代码都是单继承,即一个派生类只有一个基类,实际上C++是支持多继承的,即一个派生类可以有多个基类,派生类在多重继承中与每一个基类的关系,仍然可以看做是一个单继承。
#include <iostream>
using namespace std;
class Sofa
{
public:
void sit()
{
cout << "能坐着" << endl;
}
};
class Bed
{
public:
void lay()
{
cout << "能躺着" << endl;
}
};
class SofaBed:public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.lay();
sb.sit();
return 0;
}
1.4.1 二义性问题
多重继承容易出现二义性问题,主要有两种情况:
在多继承中,不同的基类拥有同名成员,此时派生类的调用会出现二义性问题。
解决方法:可以通过类名::的方式区分重名成员
#include <iostream>
using namespace std;
class Sofa
{
public:
void sit()
{
cout << "能坐着" << endl;
}
void position()
{
cout << "放在客厅" << endl;
}
};
class Bed
{
public:
void lay()
{
cout << "能躺着" << endl;
}
void position()
{
cout << "放在卧室" << endl;
}
};
class SofaBed:public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.lay();
sb.sit();
// sb.position(); 错误
sb.Bed::position();
sb.Sofa::position();
return 0;
}
菱形(钻石)继承
如果一个基类有两个派生类,这两个派生类又作为基类拥有一个派生类,此时在最终的派生类中访问间接基类的成员会出现二义性。
解决方法1:可以通过类名::的方式区分重名成员
#include <iostream>
using namespace std;
class Furniture // 家具
{
public:
void func()
{
cout << "家里要有家具" << endl;
}
};
class Sofa:public Furniture
{
};
class Bed:public Furniture
{
};
class SofaBed:public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
// sb.func(); 错误
// sb.Furniture::func(); 错误
sb.Bed::func();
sb.Sofa::func();
return 0;
}
虚继承 virtual
虚继承的实现通过虚基类指针与虚基类表完成,当Sofa和Bed使用虚继承继承Furniture类时,会在Sofa和Bed类中增加一个虚基类表,虚基类表中记录的是Furniture的成员,Sofa类(Bed类)的所有对象共用这一张虚基类表,通过每个对象创建时新增一个隐藏成员虚基类表指针调用虚基类表。SofaBed继承了Sofa和Bed类,SofaBed对象也会有继承来的虚基类指针,虚基类指针可以查询Sofa和Bed的虚基类表,在调用时通过查表来区分二义性问题。
#include <iostream>
using namespace std;
class Furniture // 家具
{
public:
void func()
{
cout << "家里要有家具" << endl;
}
};
class Sofa:virtual public Furniture
{
};
class Bed:virtual public Furniture
{
};
class SofaBed:public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.func();
return 0;
}
虚继承在代码形式上完美解决了菱形继承的二义性问题,但是在调用这些原本二义性的代码时,增加了一些开销(虚基类表与虚基类指针内存占用和查询等),因此虚继承代码执行效率比普通继承要低。
二、权限
2.1 不同权限的访问能力
在C++中有三种权限修饰符:private(私有)、protected(保护)和public(公有),这三种权限的访问呢能力如下表所示。
本类 | 派生类 | 全局 | |
private(私有) | 可以访问 | 不可以访问 | 不可以访问 |
protected(保护) | 可以访问 | 可以访问 | 不可以访问 |
public (公有) | 可以访问 | 可以访问 | 可以访问 |
#include <iostream>
using namespace std;
class Father
{
private:
string str1 = "private成员";
protected:
string str2 = "protected成员";
public:
string str3 = "public成员";
void test()
{
cout << str1 << endl;
cout << str2 << endl;
cout << str3 << endl;
}
};
class Son:public Father
{
public:
void test()
{
// cout << str1 << endl; 错误
cout << str2 << endl;
cout << str3 << endl;
}
};
int main()
{
Father f;
f.test();
// cout << f.str1 << endl; 错误
// cout << f.str2 << endl; 错误
cout << f.str3 << endl;
Son s;
s.test();
return 0;
}
2.2 不同权限的继承
在C++有三种权限的继承:
- 公有继承
- 保护继承
- 私有继承
之前使用的继承都是公有继承,实际上公有继承也是使用最多的继承。
2.2.1 公有继承
公有继承的特点如下:
基类的private成员 | 无法在派生类中直接访问 |
基类的protected成员 | 会继续成为派生类的protected成员 |
基类的public成员 | 会继续成为派生类的public成员 |
#include <iostream>
using namespace std;
class Father
{
private:
string str1 = "private成员";
protected:
string str2 = "protected成员";
public:
string str3 = "public成员";
};
class Son:public Father
{
public:
void test()
{
// cout << str1 << endl; 错误:Father的private
cout << str2 << endl; // Son类的protected
cout << str3 << endl; // Son类的public
}
};
class Grandson:public Son
{
public:
void test()
{
// cout << str1 << endl; 错误:Father的private
cout << str2 << endl;
cout << str3 << endl;
}
};
int main()
{
Son s;
// cout << s.str1 << endl; 错误:Father的private
// cout << s.str2 << endl; 错误:Son的protected
cout << s.str3 << endl; // Son的public
return 0;
}
2.2.2 保护继承
保护继承的特点是:
基类的private成员 | 无法在派生类中直接访问 |
基类的protected成员 | 会继续成为派生类的protected成员 |
基类的public成员 | 会继续成为派生类的protected成员 |
#include <iostream>
using namespace std;
class Father
{
private:
string str1 = "private成员";
protected:
string str2 = "protected成员";
public:
string str3 = "public成员";
};
class Son:protected Father
{
public:
void test()
{
// cout << str1 << endl; 错误:Father的private
cout << str2 << endl; // Son类的protected
cout << str3 << endl; // Son类的protected
}
};
class Grandson:public Son
{
public:
void test()
{
// cout << str1 << endl; 错误:Father的private
cout << str2 << endl;
cout << str3 << endl;
}
};
int main()
{
Son s;
// cout << s.str1 << endl; 错误:Father的private
// cout << s.str2 << endl; 错误:Son的protected
// cout << s.str3 << endl; 错误:Son的protected
return 0;
}
2.2.3 私有成员:继承时不写权限默认为私有继承。
私有继承的特点是:
基类的private成员 | 无法在派生类中直接访问 |
基类的protected成员 | 会成为派生类的private成员 |
基类的public成员 | 会成为派生类的private成员 |
#include <iostream>
using namespace std;
class Father
{
private:
string str1 = "private成员";
protected:
string str2 = "protected成员";
public:
string str3 = "public成员";
};
class Son:private Father
{
public:
void test()
{
// cout << str1 << endl; 错误:Father的private
cout << str2 << endl; // Son类的private
cout << str3 << endl; // Son类的private
}
};
class Grandson:public Son
{
public:
void test()
{
// cout << str1 << endl; 错误:Father的private
// cout << str2 << endl; 错误:Son的private
// cout << str3 << endl; 错误:Son的private
}
};
int main()
{
Son s;
// cout << s.str1 << endl; 错误:Father的private
// cout << s.str2 << endl; 错误:Son的protected
// cout << s.str3 << endl; 错误:Son的protected
return 0;
}
三、多态
多态按照字面的意思可以认为是“多种状态”,可以简单概括为“一个接口,多种状态”,即程序在运行时动态决定调用的代码。
多态与模板的区别在于,模板针对不同的数据类型采用同样的策略,而多态针对不同的数据类型采用不同的策略。
多态的实现需要以下3个条件:
1. 要有公有继承
2. 要有函数覆盖:派生类中覆盖基类的成员函数(虚函数)
3. 基类引用/指针指向派生类对象
3.1 函数覆盖 virtual
函数覆盖是通过虚函数实现的,用virtual关键字修饰成员函数,这样的成员函数就是虚函数,虚函数支持函数覆盖(而不是函数隐藏)。函数覆盖是使用多态的前提。
在派生类中,使用之前函数隐藏的方式重新实现一个基类中的虚函数,此时就是函数覆盖,函数覆盖与虚函数具有以下特点:
- 当函数覆盖成功时,虚函数具有传递性
- C++11中可以在派生类的新覆盖的函数后增加override关键字进行覆盖是否成功的验证
- 成员函数与析构函数可以定义为虚函数,静态成员函数与构造函数不可以定义为虚函数。
- 如果成员函数的声明与定义分离,virtual关键字只需要在声明处使用即可。`
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "动物吃东西" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "小狗吃骨头" << endl;
}
};
int main()
{
Animal a;
a.eat(); // 动物吃东西
Dog d;
d.eat(); // 小狗吃骨头
return 0;
}
3.3 多态的基本使用
多态既可以引用的方式,也可以使用指针的方式实现。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "吃东西" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "吃骨头" << endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout << "吃鱼" << endl;
}
};
// 基于引用的多态参数传递
void test_eat1(Animal& a)
{
a.eat();
}
// 基于指针的多态参数传递
void test_eat2(Animal* a)
{
a->eat();
}
int main()
{
Dog d1;
Cat c1;
Animal a1;
// 测试基于引用的多态参数传递
test_eat1(d1); // 吃骨头
test_eat1(c1); // 吃鱼
test_eat1(a1); // 吃东西
Dog* d2 = new Dog;
Cat* c2 = new Cat;
Animal* a2 = new Animal;
// 测试基于指针的多态参数传递
test_eat2(d2); // 吃骨头
test_eat2(c2); // 吃鱼
test_eat2(a2); // 吃东西
return 0;
}
3.4 原理
什么时候是多态的?
基类指针或者引用指向派生类对象,并且调用虚函数,这时就是多态。
当一个类中有虚函数的时候,编译器会为这个类创建一个虚函数表。当此类被继承的时候,虚函数表同样被继承,当派生类中重写了虚函数时,派生类虚函数表也跟着一起变化,把之前的父类中的虚函数隐藏覆盖。
在实际多态的运行中是一个动态类型绑定的过程,当使用基类引用或指针指向派生类对象时,编译器会产生一段代码,用来检查当前内存中的对象的真正类型,在运行时通过对象的虚函数表指针找到真正的调用函数,因此多态也是一个查表的过程。
实现多态后,运行的时候根据传入对象类型的不同,查不同的虚函数表,执行不同的操作。所以虚函数带来一定的时间开销,但是会提高开发效率
虚函数表中记录是虚函数的位置
3.5 多态的缺陷
有可能造成内存泄漏
#include <iostream>
using namespace std;
class Animal{
public:
virtual void eat(){
cout<<"吃东西"<<endl;
}
~Animal(){
cout<<"Animal的析构函数"<<endl;
}
};
class Dog:public Animal{
public:
void eat(){
cout<<"吃肉"<<endl;
}
~Dog(){
cout<<"Dog类的析构函数"<<endl;
}
};
//公共接口
void show(Animal & a){
a.eat();
}
void show2(Animal * a){
a->eat();
}
int main()
{
Dog * d=new Dog;
delete d;
d=NULL;
Animal*a=new Dog;
delete a; //只调用了基类的析构函数
a=NULL;
}
解决方式:需要在基类析构函数前vitual关键字(虚析构函数)
#include <iostream>
using namespace std;
class Animal{
public:
virtual void eat(){
cout<<"吃东西"<<endl;
}
virtual ~Animal(){
cout<<"Animal的析构函数"<<endl;
}
};
class Dog:public Animal{
public:
void eat(){
cout<<"吃肉"<<endl;
}
~Dog(){
cout<<"Dog类的析构函数"<<endl;
}
};
//公共接口
void show(Animal & a){
a.eat();
}
void show2(Animal * a){
a->eat();
}
int main()
{
Dog * d=new Dog;
delete d;
d=NULL;
Animal*a=new Dog;
delete a;
a=NULL;
}
四、抽象类 Abstract Class
抽象类只是一个抽象的概念,并不和具体的对象相关联,不可以创建对象。
作用:给派生类做一个框架
抽象类不能实例化,也不能做参数和函数返回值
如果一个类有纯虚函数,那这个类就是抽象类
如果一个类是抽象类,其中一定有纯虚函数
纯虚函数只有声明,没有定义
纯虚函数形式:virtual关键字修饰的函数等于0。
#include <iostream>
using namespace std;
class Shape{
public:
virtual void area()=0;
virtual void perimeter()=0;
};
int main()
{
Shape s; //错误的 抽象类不能实例化对象
}
如果继承了抽象类,如果没有把里面的纯虚函数都定义出来,那这个类也会是抽象
4.2 抽象类的使用
不考虑多态的情况,抽象类的使用主要是为其派生类提供算法框架。
分为两种情况:
1.抽象类的派生类把抽象类的所有纯虚函数“实现”,实现是:函数覆盖,即在派生类中添加纯虚函数的函数体,使其变为一个普通的虚函数。
2.抽象类的派生类没有把抽象类的所有纯虚函数实现,此时派生类也会成为抽象类,等待它的派生类实现其未实现的纯虚函数。