围绕继承与多态,对相关的面向对象知识点进行学习,这些内容也是最重要的内容。
1. 继承
1.1 基础使用
继承就是在一个已经存在的类的基础上新建立一个类,新创建的类拥有之前类的特性。继承是面向对象的三大特性之一,体现了代码复用的思想。
- 已经存在的类被称为“基类 Base Class”或“父类”
- 新创建的类被称为“派生类”或“子类Sub Class”
下面是一个最简单的单一继承:
#include <iostream>
using namespace std;
/**
* @brief The Father class 基类
*/
class Father
{
public:
string first_name = "欧阳";
void work()
{
cout << "我是一个厨师" << endl;
}
};
/**
* @brief The Son class 派生类
*/
class Son:public Father
{
};
int main()
{
Son s;
cout << s.first_name << endl;
s.work();
return 0;
}
上面的继承在实际开发中没有任何意义,因为派生类对基类没有做出任何改变。通常派生类都会在基类的基础上做出一些代码修改或增加。
#include <iostream>
using namespace std;
/**
* @brief The Father class 基类
*/
class Father
{
public:
string first_name = "欧阳";
void work()
{
cout << "我是一个厨师" << endl;
}
};
/**
* @brief The Son class 派生类
*/
class Son:public Father
{
public:
void play() // 增加的代码
{
cout << "打游戏" << endl;
}
// 【函数隐藏】与基类的work函数签名相同
void work()
{
cout << "我要做码农" << endl;
}
};
int main()
{
Son s;
cout << s.first_name << endl;
s.work();
// 也可以调用被隐藏的基类函数
s.Father::work();
s.play();
return 0;
}
基类和派生类是相对的,一个类既可以作为基类又可以作为派生类,取决于两个类之间的关系。派生类是基类的具象化,基类是派生类的抽象化。
对于基类的私有成员,派生类可以继承,但是无法直接访问,访问需要通过基类提供的接口。
#include <iostream>
using namespace std;
class Father
{
private:
string first_name = "欧阳";
public:
string get_first_name() const
{
return first_name;
}
void set_first_name(string fn)
{
first_name = fn;
}
};
class Son:public Father
{
public:
Son()
{
// cout << first_name << endl; 错误
}
};
class Test
{
};
int main()
{
Son s;
// cout << s.first_name << endl; 错误
Test t;
Father f;
cout << sizeof(f) << endl; // 4
cout << sizeof(s) << " " << sizeof(t) << endl; // 4 1
// 通过基类接口可以访问继承的基类私有成员
s.set_first_name("司马");
cout << s.get_first_name() << endl; // 司马
return 0;
}
1.2 构造函数
派生类的构造函数必须直接或间接调用基类的任意一个构造函数。如果程序员不手动在派生类的构造函数中调用基类的构造函数,编译器会尝试调用基类的无参构造函数。
#include <iostream>
using namespace std;
class Father
{
public:
Father()
{
cout << "基类的构造函数" << endl;
}
};
class Son:public Father
{
public:
// 编译器增加以下代码
Son():Father() // 透传构造:在派生类的构造函数中调用基类的构造函数
{
}
};
int main()
{
Son s;
return 0;
}
编译器无法处理所有情况,例如基类没有无参构造函数。
#include <iostream>
using namespace std;
class Father
{
private:
string name;
public:
Father(string name)
{
this->name = name;
cout << "基类的构造函数" << endl;
}
void set_name(string name)
{
this->name = name;
}
string get_name() const
{
return name;
}
};
class Son:public Father
{
public:
// 编译器只会自动调用基类的无参构造
// 基类现在没有无参构造
// 报错!
};
int main()
{
Son s;
return 0;
}
上面的情况必须程序员手动在派生类的构造函数中直接或间接调用基类的构造函数:
- 透传构造
- 委托构造
- 继承构造(C++11)
注:构造函数和析构函数是不能被继承的,继承构造仅仅是一种用法的名称,并没有继承构造函数。
1.2.1 透传构造
透传构造属于在派生类的构造函数中直接调用基类的构造函数。
#include <iostream>
using namespace std;
class Father
{
private:
string name;
int age;
public:
Father(string name)
{
this->name = name;
age = 1;
}
Father(string name,int age)
:name(name),age(age){}
void show()
{
cout << name << " " << age << endl;
}
};
class Son:public Father
{
public:
Son():Father("佚名") // 通过透传构造调用基类的一参构造函数
{
}
Son(string name,int age):
Father(name,age) // 通过透传构造调用基类的二参构造函数
{}
};
int main()
{
Son s;
s.show();
Son s1("李白",29);
s1.show();
return 0;
}
1.2.2 委托构造
委托构造本身可以脱离继承使用,指的是在某个类中构造函数A可以调用构造函数B。在继承中就可以让构造函数A调用构造函数B,构造函数B透传调用基类的构造函数,这样构造函数A就间接调用了基类的构造函数。
#include <iostream>
using namespace std;
class Father
{
private:
string name;
int age;
public:
Father(string name)
{
this->name = name;
age = 1;
}
Father(string name,int age)
:name(name),age(age){}
void show()
{
cout << name << " " << age << endl;
}
};
class Son:public Father
{
public:
// 构造函数A委托调用构造函数B(Son的二参构造函数)
Son():Son("佚名",2)
{
}
// 构造函数B
Son(string name,int age):
Father(name,age) // 通过透传构造调用基类的二参构造函数
{}
};
int main()
{
Son s;
s.show();
Son s1("李白",29);
s1.show();
return 0;
}
委托构造要避免委托闭环。
#include <iostream>
using namespace std;
class Father
{
private:
string name;
int age;
public:
Father(string name)
{
this->name = name;
age = 1;
}
Father(string name,int age)
:name(name),age(age){}
void show()
{
cout << name << " " << age << endl;
}
};
class Son:public Father
{
public:
// 构造函数A委托调用构造函数B(Son的二参构造函数)
Son():Son("佚名",2)
{
}
// 构造函数B委托调用构造函数A
Son(string name,int age):
Son()
{}
};
int main()
{
Son s;
s.show();
Son s1("李白",29);
s1.show();
return 0;
}
在上面代码中,程序会卡在第46行,因为构造函数执行不完,互相委托形成闭环。
1.2.3 继承构造(熟悉)
继承构造是C++11的新特性,并不是表示能继承构造函数,而是一种简便的写法,可以一句话实现一种透传构造。
在派生类中使用下面的语句,可以让派生类生成n(n为基类的构造函数数量)个构造函数,同时这n个构造函数参数与基类的n个构造函数相同,每个派生类的构造函数都透传参数相同的基类构造函数。
#include <iostream>
using namespace std;
class Father
{
private:
string name;
int age;
public:
Father(string name)
{
this->name = name;
age = 1;
}
Father(string name,int age)
:name(name),age(age){}
void show()
{
cout << name << " " << age << endl;
}
};
class Son:public Father
{
public:
// 自动创建2个构造函数
// 这两个构造函数的参数:
// (1) string
// (2) string int
// 前者透传Father(string name)
// 后者透传Father(string name,int age)
using Father::Father;
};
int main()
{
Son s("王维");
s.show();
Son s1("李白",29);
s1.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:
Value value = Value("Father类的成员变量");
static Value s_value;
Father()
{
cout << "Father类的构造函数" << endl;
}
~Father()
{
cout << "Father类的析构函数" << endl;
}
};
Value Father::s_value = Value("Father类的静态成员变量");
class Son:public Father
{
public:
Value value = Value("Son类的成员变量");
static Value s_value;
Son()
{
cout << "Son类的构造函数" << endl;
}
~Son()
{
cout << "Son类的析构函数" << endl;
}
};
Value Son::s_value = Value("Son类的静态成员变量");
int main()
{
cout << "主函数开始执行" << endl;
Son* s = new Son;
delete s;
cout << "主函数结束执行" << endl;
return 0;
}
在上面的结果中可以得到如下规律:
1. 创建与销毁过程是对称的。
2. 静态的周期贯穿整个程序。
3. 创建过程中,先执行基类;销毁过程中,后执行基类。因为派生类依赖于基类。
虽然推荐理解记忆,但是也可以直接记忆,根据自己的学习习惯自行选择。
1.4 多重继承
1.4.1 基础使用(掌握)
C++支持多继承,即一个派生类可以有多个基类。
#include <iostream>
using namespace std;
class Sofa
{
public:
void sit()
{
cout << "能坐着" << endl;
}
};
class Bed
{
public:
void lay()
{
cout << "能躺着" << endl;
}
};
/**
* @brief The SofaBed class
* 多重继承
*/
class SofaBed:public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.lay();
sb.sit();
return 0;
}
1.4.2 二义性
1.4.2.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;
}
};
/**
* @brief The SofaBed class
* 多重继承
*/
class SofaBed:public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.lay();
sb.sit();
// sb.position(); 错误:二义性
// 区分二义性
sb.Sofa::position();
sb.Bed::position();
return 0;
}
1.4.2.2 菱形继承(钻石继承)
如果一个类A有两个派生类B和C,类D同时继承B和C,此时就出现了菱形继承,当对象D调用A的成员时,会产生二义性。
#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(); 错误:二义性
return 0;
}
解决方法1:使用类名::进行区分。
解决方法2:虚继承。
#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();
sb.Bed::func();
sb.Sofa::func();
return 0;
}
当使用虚继承时,Furniture类内部会生成一张虚基类表(程序运行时加载进内存),内部存储Furniture的成员的调用地址,每个Sofa和Bed类对象内部都会有一个隐藏成员指向虚基类表。
SofaBed对象同时继承了Sofa和Bed类的虚基类表指针,调用func()函数时查表比对,防止二义性的出现。
虚继承的本质是查表,因此会降低调用效率。
#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()
{
Furniture f;
cout << sizeof(f) << endl; // 1 占位
Sofa s;
cout << sizeof(s) << endl; // 4 虚基类表指针
Bed b;
cout << sizeof(b) << endl; // 4 虚基类表指针
SofaBed sb;
cout << sizeof(sb) << endl; // 8 两个虚基类表指针
sb.func();
sb.Bed::func();
sb.Sofa::func();
return 0;
}
1.5 权限
1.5.1 权限修饰符
本类中 | 派生类中 | 全局(例如主函数中) | |
private 私有(默认) | √ | X | X |
protected 保护 | √ | √ | X |
public 公有 | √ | √ | √ |
#include <iostream>
using namespace std;
class Father
{
protected:
string s1 = "Father的保护权限";
public:
string s2 = "Father的公有权限";
void func()
{
cout << s1 << endl;
}
};
class Son:public Father
{
public:
void test()
{
cout << s1 << endl;
cout << s2 << endl;
}
};
int main()
{
Son s;
s.test();
Father f;
f.func();
// cout << f.s1 << endl; 错误
return 0;
}
1.5.2 不同权限的继承
三种权限修饰符可以修饰继承:
- 公有继承
- 保护继承
- 私有继承
1.5.2.1 公有继承
使用的最多的一种继承,之前的继承都是公有继承。在公有继承中,基类的所有成员均可以被派生类继承,但是基类的私有成员无法被派生类直接访问,基类的保护成员和公有成员继承到派生类中作为派生类的保护成员和公有成员(权限不变)。
#include <iostream>
using namespace std;
class Father
{
private:
string s0 = "Father的私有权限";
protected:
string s1 = "Father的保护权限";
public:
string s2 = "Father的公有权限";
};
class Son:public Father
{
public:
Son()
{
// cout << s0 << endl; 错误
cout << s1 << endl;
cout << s2 << endl;
}
};
class Grandson:public Son
{
public:
Grandson()
{
cout << s1 << endl;
}
};
int main()
{
Son s;
cout << sizeof(s) << endl; // 12
// cout << s.s1 << endl; 错误
cout << s.s2 << endl;
Grandson gs;
return 0;
}
1.5.2.2 保护继承(掌握)
在保护继承中,基类的所有成员均可以被派生类继承,但是基类的私有成员无法被派生类直接访问,基类的保护成员和公有成员继承到派生类中作为派生类的保护成员。
#include <iostream>
using namespace std;
class Father
{
private:
string s0 = "Father的私有权限";
protected:
string s1 = "Father的保护权限";
public:
string s2 = "Father的公有权限";
};
class Son:protected Father
{
public:
Son()
{
// cout << s0 << endl; 错误
cout << s1 << endl;
cout << s2 << endl;
}
};
class Grandson:public Son
{
public:
Grandson()
{
cout << s1 << endl;
cout << s2 << endl;
}
};
int main()
{
Son s;
cout << sizeof(s) << endl; // 12
// cout << s.s1 << endl; 错误
// cout << s.s2 << endl; 错误
Grandson gs;
return 0;
}
1.5.2.3 私有继承(掌握)
在私有继承中,基类的所有成员均可以被派生类继承,但是基类的私有成员无法被派生类直接访问,基类的保护成员和公有成员继承到派生类中作为派生类的私有成员。
#include <iostream>
using namespace std;
class Father
{
private:
string s0 = "Father的私有权限";
protected:
string s1 = "Father的保护权限";
public:
string s2 = "Father的公有权限";
};
class Son:private Father
{
public:
Son()
{
// cout << s0 << endl; 错误
cout << s1 << endl;
cout << s2 << endl;
}
};
class Grandson:public Son
{
public:
Grandson()
{
// cout << s1 << endl; 错误
// cout << s2 << endl; 错误
}
};
int main()
{
Son s;
cout << sizeof(s) << endl; // 12
// cout << s.s1 << endl; 错误
// cout << s.s2 << endl; 错误
Grandson gs;
return 0;
}
2. 多态
2.1 概念
从广义上讲,多态可以分为静态多态和动态多态。
静态多态(编译时多态)发生在程序的编译阶段,主要包括函数重载和运算符重载,在编译时就能确定调用关系。
动态多态(运行时多态),本章讨论的主要是动态多态。因此从狭义上讲,多态指的是动态多态。
多态(polymorphism)按照字面的意思是“多种状态”,简单的概括为“一个接口,多种状态”,一个函数接口,在运行时根据传入的参数类型执行不同的策略。
多态的实现需要有三个前提条件:
- 公有继承
- 函数覆盖(函数重写)override
- 基类引用/指针指向派生类对象
2.2 函数覆盖
函数覆盖(函数重写)的前提是虚函数,虚函数使用关键字virtual修饰成员函数实现,普通的虚函数目的是实现函数覆盖。
虚函数的格式:
virtual 返回值类型 函数名 (参数表){}
一句话表达:在之前函数隐藏的前提下,把被隐藏的基类函数使用virtual修饰,就变成了函数覆盖。
虚函数具有以下性质:
- 在Qt Creator中斜体表示虚函数
- 虚函数具有传递性,基类被覆盖的虚函数会自动传递给派生类覆盖的新函数,使后者也变为虚函数。
- 成员函数和析构函数可以设置为虚函数,静态成员函数和构造函数不能设置为虚函数。
- 如果函数的声明与定义分离,virtual只需要修饰在声明处。
- C++11中,可以在派生类新覆盖的函数后面使用override关键字修饰,如果函数覆盖成功则不会报错。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "吃东西" << endl;
}
virtual void speak();
};
void Animal::speak()
{
cout << "fgsjkfghklsj" << endl;
}
class Dog:public Animal
{
public:
void eat()
{
cout << "吃屎" << endl;
}
void speak() override
{
cout << "哼" << endl;
}
};
int main()
{
return 0;
}
2.3 实现
多态往往伴随着函数的调用和传参,基类引用/指针指向派生类对象通常出现在函数传参中。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "吃东西" << endl;
}
virtual void speak();
};
void Animal::speak()
{
cout << "fgsjkfghklsj" << endl;
}
class Dog:public Animal
{
public:
void eat()
{
cout << "吃屎" << endl;
}
void speak() override
{
cout << "哼" << endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout << "吃鱼" << endl;
}
void speak()
{
cout << "害" << endl;
}
};
class Mouse:public Animal
{
public:
void eat()
{
cout << "吃老鼠药" << endl;
}
void speak()
{
cout << "吱吱吱" << endl;
}
};
/**
* @brief test_polymorphysism
* @param a 基类引用,传递栈内存
*/
void test_polymorphysism(Animal& a)
{
a.eat();
a.speak();
}
/**
* @brief test_polymorphysism
* @param a 基类指针,传递堆内存对象
*/
void test_polymorphysism(Animal* a)
{
a->eat();
a->speak();
}
int main()
{
Animal a;
Dog d;
Cat c;
Mouse m;
// 根据传入参数的类型不同,执行不同代码
test_polymorphysism(a);
test_polymorphysism(d);
test_polymorphysism(c);
test_polymorphysism(m);
Animal* a1 = new Animal;
Dog* d1 = new Dog;
Cat* c1 = new Cat;
Mouse* m1 = new Mouse;
// 根据传入参数的类型不同,执行不同代码
test_polymorphysism(a1);
test_polymorphysism(d1);
test_polymorphysism(c1);
test_polymorphysism(m1);
delete a1;
delete d1;
delete c1;
delete m1;
return 0;
}