一、继承的概念
1.定义
继承是面向对象编程中的一个重要概念,它允许我们创建一个新类(派生类或子类),从一个已有的类(基类或父类)中继承成员变量和成员函数。这就好比子女继承父母的某些特征一样,派生类继承基类的属性和行为,并且可以在这个基础上进行扩展和修改。
2.意义
-
可以实现代码复用,减少重复代码的劳动量。
-
继承是实现多态的必要条件。
-
继承本身就是为了实现多态,顺便实现了代码复用。
3.关于继承的称呼
一个类C 继承来自 类A 我们一般称呼:C继承了A A派生了C
A类:父类 基类
C类:子类 派生类
4.示意图
二、基本语法
class 子类名称:继承方式 父类名称
{
子类新增的成员;
}
class C:public A
{
}
1.基类对象初始化
在子类的构造函数中,通常需要在初始化列表中显式调用基类的构造函数,以完成对基类成员的初始化。
在继承时,需要在子类构造函数的初始化列表中 显性的调用父类的构造函数 来完成对父类成员的初始化
格式:构造函数名(int val_1 .,int val_2 ):class A(val_1 , val_2)
2.派生类中调用基类
在派生类中可以通过以下方式调用基类的成员:
类外访问:
对象名.基类名::变量
对象名.基类名::函数名(函数的实参表);
类内访问:
基类名::变量;
基类名::函数名(函数的实参表)
3.代码示例:
#include <iostream>
#include <string>
using namespace std;
class person
{
private:
/* data */
public:
string name;
int id;
int height;
person(string name,int id,int height)
:name(name),id(id),height(height)
{
cout << "我是父类构造函数" << endl;
}
};
class man:public person
{
private:
string name;
public:
man(string name,int id,int height)
:person(name,id,height)
{
}
void show_date()
{
cout << person::name << " " << person::id << " " << this->height << endl;
}
};
int main(int argc, char const *argv[])
{
man m("张三", 1, 131);
m.show_date();
return 0;
}
三、继承方式
1.类中的访问控制权限
类内 | 子类 | 类外 | |
---|---|---|---|
public | √ | √ | √ |
protected | √ | √ | × |
private | √ | × | × |
2.继承的三种访问方式
访问关系图:
2.1.公有继承(public)
基类成员在派生类中的访问权限:
-
基类的公有成员在派生类中仍然是公有成员,这意味着派生类的对象可以直接访问基类的公有成员。外部函数也可以通过派生类的对象访问基类的公有成员。
-
基类的保护成员在派生类中仍然是保护成员,派生类的成员函数可以访问基类的保护成员,但外部函数不能通过派生类的对象访问基类的保护成员。
-
基类的私有成员在派生类中不可直接访问,派生类的成员函数不能访问基类的私有成员,就好像这些私有成员被隐藏起来了一样。
基类成员访问属性 | 继承方式 | 派生类成员访问属性 |
private 成员 | public | 无法访问 |
protected 成员 | public | protected |
public 成员 | public | public |
示例代码
class Base {
public:
int publicVar;
protected:
int protectedVar;
private:
int privateVar;
};
class Derived : public Base {
public:
void accessMembers() {
publicVar = 10; // 可以访问基类的公有成员
protectedVar = 20; // 可以访问基类的保护成员
// privateVar = 30; // 错误,不能访问基类的私有成员
}
};
2.2.私有继承(private)
基类成员在派生类中的访问权限
-
基类的公有成员和保护成员在派生类中都变为私有成员。这意味着派生类的成员函数可以访问它们,但外部函数不能通过派生类的对象访问这些成员。
-
基类的私有成员在派生类中仍然不可直接访问。
基类成员访问属性 | 继承方式 | 派生类成员访问属性 |
private 成员 | private | 无法访问 |
protected 成员 | private | private |
public 成员 | private | private |
示例代码
class Base {
public:
int publicVar;
protected:
int protectedVar;
private:
int privateVar;
};
class Derived : private Base {
public:
void accessMembers() {
publicVar = 10; // 可以访问,但变为私有成员
protectedVar = 20; // 可以访问,但变为私有成员
// privateVar = 30; // 错误,不能访问基类的私有成员
}
};
2.3.保护继承(protected)
基类成员在派生类中的访问权限
-
基类的公有成员和保护成员在派生类中都变为保护成员。这意味着派生类的成员函数可以访问它们,派生类的派生类(孙子类)也可以访问这些成员,但外部函数不能通过派生类的对象访问这些成员。
-
基类的私有成员在派生类中仍然不可直接访问。
基类成员访问属性 | 继承方式 | 派生类成员访问属性 |
private 成员 | protected | 无法访问 |
protected 成员 | protected | protected |
public 成员 | protected | protected |
示例代码
class Base {
public:
int publicVar;
protected:
int protectedVar;
private:
int privateVar;
};
class Derived : protected Base {
public:
void accessMembers() {
publicVar = 10; // 可以访问,但变为保护成员
protectedVar = 20; // 可以访问,但变为保护成员
// privateVar = 30; // 错误,不能访问基类的私有成员
}
};
四、继承模型
实际开发中一般继承都采用 public 继承方式,如果不写继承方式,默认使用的都是 private 方式继承。
- 子类中会继承父类中所有的成员,包括私有成员,只不过在子类中不能直接访问父类中不能直接访问父类私有成员需要父类提供共有的函数来访问父类的私有成员
- 当父子类中出现了同名的函数时,访问来也不会冲突,即使形参不同,也不构成重载关系,原因是两个函数不在同一个空间内
如果想访问父类的成员 需要加类名::来修饰
类外访问:
对象名.基类名::变量
对象名.基类名::函数名(函数的实参表);
类内访问:
基类名::变量;
基类名::函数名(函数的实参表)
- 父类中私有成员也是被子类继承下去了,只是有编译器给隐藏后访问不到
五、派生类的构造函数和析构函数
1.构造函数
1.1作用
当创建派生类的对象时,首先会调用基类的构造函数来初始化从基类继承的成员,然后再调用派生类的构造函数来初始化派生类自己的成员。
1.2语法
-
如果基类有默认构造函数(无参数的构造函数),派生类的构造函数可以不显示调用基类的构造函数,编译器会自动调用基类的默认构造函数。
-
如果基类没有默认构造函数,或者派生类需要给基类的构造函数传递参数,那么派生类的构造函数需要显式地调用基类的构造函数。可以使用初始化列表来实现。
格式:Derived Class::Derived Class(parameters) : BaseClass(arguments) { // 派生类构造函数的其他初始化操作 }
1、父类的构造函数不会被子类继承
2、需要在子类的构造函数的初始列表中,显性的调用父类函数的构造函数完成对父类中继承过来的成员初始化
3、如果没有在子类的构造函数初始化列表中调用父类中的构造函数 则使用无参构造如果父类没有无参构造 会报错
4、构造函数的调用顺序是
先调用父类函数的构造函数
在调用子类函数的构造函数
1.3代码示例
class Base {
public:
int baseVar;
Base(int var) : baseVar(var) {}
};
class Derived : public Base {
public:
int derivedVar;
Derived(int baseArg, int derivedArg) : Base(baseArg), derivedVar(derivedArg) {}
};
2.析构函数
2.1作用
当派生类的对象销毁时,首先会调用派生类的析构函数,然后再调用基类的析构函数。这是为了确保派生类和基类的资源都能正确地释放。
2.2语法和注意事项
-
析构函数的名称是类名前面加一个
~
符号。与构造函数不同,析构函数不能有参数。 -
派生类的析构函数不需要显式地调用基类的析构函数,编译器会自动处理这个顺序。
1、父类的析构函数不会被子类继承
2、不管是否显性调用父类的析构函数,父类的析构函数都会被调用完成对父类中继承过来的成员的善后工作
3、子类的析构函数中,无需调用父类的析构函数
4、析构函数的调用顺序
先调用子类函数的析构函数
在调用父类函数的析构函数
2.3示例代码
class Base {
public:
~Base()
{
std::cout << "Base destructor called." << std::endl;
}
};
class Derived : public Base{
public:
~Derived()
{
std::cout << "Derived destructor called." << std::endl;
}
};
3、拷贝构造
如果子类中没有显性的定义拷贝构造函数,编译器会给予子类提供一个默认的拷贝构造函数,而且默认提供的拷贝构造函数,会自动调用父类的拷贝构造函数,完成对父类中继承过来的成员初始化。
如果子类中显性的定义了拷贝构造函数,需要在子类的拷贝构造函数的初始化表中显性的调用父类的拷贝构造函数;如果没有显性的调用父类的拷贝构造函数,默认会调用父类的无参构造函数来完成对从父类中继承过来的成员的初始化.
如果父类中没有指针成员,使用默认的拷贝构造函数;但是如果父类中有指针,则需要考虑一下深浅拷贝的问题。
六、多继承
1.概念
一个派生类可以从多个基类继承,这就是多继承。例如,一个 “水陆两栖车” 类可以同时从 “汽车” 类和 “船” 类继承,它既有汽车的行驶功能,又有船的水上航行功能。
2、语法
构造函数的调用顺序 和 声明的顺序有关
class 子类名:继承方式 父类名 ,继承方式 父类名
3、注意事项和问题解决
多继承可能会导致命名冲突的问题。当多个基类中有同名的成员(成员变量或成员函数)时,需要在派生类中明确指定要访问的是哪个基类的成员。可以使用作用域解析运算符::来解决这个问题。
多继承可能会导致菱形继承问题。
例如,有类 A、类 B 和类 C,其中类 B 和类 C 都继承自类 A,然后有一个类 D 同时继承自类 B 和类 C。在这种情况下,类 D 中会有两份类 A 的成员,可能会导致二义性和资源浪费等问题。可以使用虚拟继承来解决菱形继承问题,在继承时使用virtual关键字,如class B : virtual public A和class C : virtual public A。