目录
(一)继承的概念
- 什么是继承? 继承是面向对象编程中的一种基本特性,它允许新创建的类(派生类或子类)继承一个或多个已存在的类(基类或父类)的属性和方法。
- 继承有什么用? 实现代码复用,提高软件的可维护性和可扩展性。
(二)继承的使用
(1)派生类如何定义?
定义格式:
派生类可以继承多个基类,一个基类也可以有多个派生类,切记莫要菱形继承。
class 派生类名 : 权限 基类名 , 权限 基类名 ...
- 菱形继承: 如果一个派生类从两个基类继承,而这两个基类又从一个共同的基类继承就叫菱形继承;菱形继承可能会导致二义性。
- 示例代码:
#include <iostream>
#include <string>
using namespace std;
// 定义第一个基类
class Demo_1
{
public:
string name;
};
// 定义第二个基类
class Demo_2
{
public:
int id;
};
// 定义派生类,继承自Demo_1和Demo_2
class Son : public Demo_1, public Demo_2
{
public:
void show()
{
cout << "name = " << name << " id = " << id << endl;
}
};
int main()
{
Son s;
s.name = "张三";
s.id = 1;
s.show();
return 0;
}
/*
输出结果:
name = 张三 id = 1
*/
(2)派生类和基类的关系
- 一个派生类可以继承一个或多个基类,派生类将继承所有基类的成员属性和成员方法(除构造函数和析构函数)。
- 派生类可以添加新的成员变量和成员函数。
- 如果派生类中添加的成员与基类成员同名,则派生类的成员会隐藏基类的成员。此时,可以通过基类名和作用域解析运算符
::
来访问基类的被隐藏成员。 - 基类可以作为派生类的类型 而派生类不能作为基类的类型
1. 基类和派生类对象赋值转换
- 派生类对象可以赋值给基类的对象、基类的指针或基类的引用,这被称为切片或切割。
- 基类对象不能赋值给派生类对象。
- 基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用,但必须是基类的指针指向派生类对象时才是安全的。
(三)继承的权限
-
public
(公有继承):
基类的公有成员和保护成员在派生类中保持其原有的访问权限。
基类的私有成员在派生类中仍然是不可访问的。 -
protected
(保护继承):
基类的公有成员和保护成员在派生类中都被视为保护成员。
这些成员只能在派生类内部和派生类的友元中访问,但在派生类的子类中是不可访问的。 -
private
(私有继承):
基类的公有成员和保护成员在派生类中都被视为私有成员。
这些成员在派生类外部和派生类的子类中都是不可访问的。总结:
- 公有继承 继承后 成员权限不变;
- 保护继承 继承后 除私有成员 都变成保护成员
- 私有继承 继承后 都变成私有成员
- 所有继承方式继承基类后 基类中的私有成员继承到派生类中后仍然是私有的。
(四)构造函数和析构函数
-
隐式调用: 如果基类没有定义任何构造函数只有默认构造函数,或者定义了无参构造函数且没有其他构造函数,那么派生类的构造函数在创建对象时会隐式地调用基类的默认/无参构造函数。
-
显式调用: 如果基类定义了有参构造函数,那么派生类的每个构造函数都必须在其初始化列表中显式地调用一个基类的构造函数。
-
构造函数: 在创建对象时调用。派生类构造函数会隐式调用其基类的构造函数。如果基类构造函数需要参数,派生类构造函数必须显式传递这些参数。
-
析构函数: 在对象销毁时调用。派生类的析构函数会自动调用基类的析构函数。
-
示例代码:
#include <iostream>
#include <string>
using namespace std;
class father
{
public:
int num;
public:
father()
{
cout << "father 无参构造函数" << endl;
}
father(int num)
:num(num)
{
cout << "father 有参构造函数 " << this->num << endl;
}
};
class son: public father
{
public:
son() // 子类无参构造函数 自动调用 父类无参构造函数 如果父类没有无参构造函数会报错
{
cout << "son 无参构造函数" << endl;
}
};
int main(int argc, char const *argv[])
{
son s;
return 0;
}
输出结果:
father 无参构造函数
son 无参构造函数
*/
当把基类中的无参构造函数屏蔽掉后会报错;派生类调用无参构造函数时 会隐式调用 基类的构造函数,而此时基类只有一个有参构造函数 ,编译器不会在基类中生成默认的无参构造参数,而基类中唯一的有参构造函数必须要有参数传递进来 因此会报错
报错信息:
(1)派生类调用基类有参构造函数
- 派生类调用基类有参构造函数时,派生类构造函数必须显式传递这些参数;通过初始化列表调用基类有参构造函数。
- 示例代码:
#include <iostream>
#include <string>
using namespace std;
class father
{
public:
string name;
int id;
public:
father(string name , int id)
:name(name) , id(id)
{
cout << "father 有参构造函数 name: " << this->name << " id: " << this->id << endl;
}
};
class son: public father
{
public:
son(string name , int id)
:father(name , id) // 通过初始化列表调用基类构造函数
{
cout << "son 有参构造函数 name: " << this->name << " id: " << this->id << endl;
}
};
int main(int argc, char const *argv[])
{
son s("小明" , 1);
return 0;
}
(2)构造函数和析构函数的调用顺序
-
构造函数的调用顺序:
- 首先调用基类的构造函数,后调用派生类的构造函数。如果基类还有自己的基类,那么会先调用基类的基类的构造函数,依此类推,直到最顶层的基类。这被称为构造函数的“深度优先”调用。
-
析构函数的调用顺序:
- 先调用派生类的析构函数最后调用基类的析构函数;析构函数的调用顺序与构造函数的调用顺序相反,这确保了资源被正确地释放。
-
示例代码:
#include <iostream>
#include <string>
using namespace std;
class father
{
public:
father()
{
cout << "father 无参构造函数" << endl;
}
~father()
{
cout << "father 析构函数" << endl;
}
};
class son: public father
{
public:
son()
{
cout << "son 无参构造函数" << endl;
}
~son()
{
cout << "son 析构函数" << endl;
}
};
class grandson : public son
{
public:
grandson()
{
cout << "grandson 无参构造函数" << endl;
}
~grandson()
{
cout << "grandson 析构函数" << endl;
}
};
int main(int argc, char const *argv[])
{
grandson gson;
return 0;
}
/*
输出结果:
father 无参构造函数
son 无参构造函数
grandson 无参构造函数
grandson 析构函数
son 析构函数
father 析构函数
*/