目录
一、继承的概念和定义
1.1 继承的概念
在此之前,我们创建过一些具有相似属性的类,例如Student类和Teacher类,它们都包含了一些相同属性:姓名、性别、标识ID等。我们要实现两个类,就需要使每个类都包含这些信息。但是一个两个相同信息还好,如果有很多相同信息就会很麻烦,那么能不能把这些相同信息聚合在一起,让每一类都包含这些信息呢?答案是可以的,方法就是使用继承。
继承是面向对象编程中的重要概念,用来实现代码的复用和扩展。它允许一个类(称为派生类)从另一个类(称为基类)那里继承属性和行为。通过继承,派生类可以重用基类的成员变量和成员函数,并且可以在此基础上添加新的成员变量和成员函数,或者修改已有的成员函数实现,从而实现代码的复用和扩展。
1.2 继承的定义
1.2.1 定义格式
在C++中,继承的定义格式如下:
其中派生类又称子类,基类又称父类。继承方式又分三种:public、protected、private。
1.2.2 继承方式和访问限定符
之前我们介绍过访问限定符有三种:public、protected、private。【C++】类和对象(一)
而继承方式也分为public继承、protected继承、private继承
1.2.3 继承基类成员访问方式的变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected 成员 | 派生类protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成 员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。
不可见是指基类的私有成员仍被继承到派生类对象中,但是语法上限制派生类对象不能去访问它。
而私有是指类里面可用,类外不可用。- 保护成员限定符是因继承才出现的:如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。
- 基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
例如:
#include <iostream>
using namespace std;
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name; //姓名
char _sex; //性别
int _age; //年龄
private:
string _tel; //电话号码
};
class Student :public Person
{
public:
void Study()
{
cout << "studying" << endl;
}
protected:
int _No; //学号
};
class Teacher : public Person
{
public:
void Teach()
{
cout << "teaching" << endl;
}
protected:
int _id; //职工编号
};
其中,Student类采用public继承方式继承Person类并进行扩展,
Person类中的公有的Print函数可以被派生类访问和使用,也可以在派生类的对象访问和使用;
Person类中的保护的姓名和年龄成员变量可以被派生类访问和使用,但不可以在派生类的对象访问和使用;
Person类中的私有的电话号码成员变量在派生类中不可见。
Teacher类同理。
二、基类和派生类对象赋值转换
2.1 临时对象
double d = 2.2;
int i = d;
观察上面的代码,我们知道 d 并不是直接赋值给 i ,而是生成了一个int类型的临时对象,再将这个临时对象赋值给 i 。
double d = 2.2;
int i = d;
const int& r = d;
临时变量具有常性,将double类型的d赋值给int类型的常量引用,会产生一个临时变量,所以应该在引用前加上const。
Student s;
Person p = s;
Person& rp = s; //不用加const
public继承,父类和子类是一个 is-a 的关系。
子类对象赋值给一个父类的 对象 / 指针 / 引用,我们认为是天然的,中间不产生临时对象,
这个叫做父子类复制兼容规则(切割/切片)。
2.2 派生类对象赋值给基类对象
一个派生类对象可以赋值给一个基类的 对象 / 指针 / 引用,该操作叫做切片/切割。
表示将派生类中 父类/基类 那部分切出来赋值过去。
对象切片是指只将派生类对象中的基类部分进行赋值,而忽略派生类部分额外的数据。这是由于基类对象无法存储派生类的额外信息,因此只能复制基类部分的数据。
class Base
{
public:
int x;
};
class Derived : public Base
{
public:
int y;
};
int main()
{
Base b;
Derived d;
d.x = 10;
d.y = 20;
b = d; // 对象切片,只会复制基类部分的数据,派生类部分的数据将被忽略
Base* pb = &d;
Base& rb = d; //中间不产生临时对象
return 0;
}
2.3 基类对象赋值给派生类对象
基类对象不能赋值给派生类对象。当将一个基类对象赋值给一个派生类对象时,只会复制基类部分的数据,派生类部分的数据将被忽略。这是因为派生类对象可能拥有额外的成员变量和成员函数,而基类对象无法包含这些额外的信息。因此,将基类对象赋值给派生类对象将导致数据丢失。
class Base
{
public:
int x;
};
class Derived : public Base
{
public:
int y;
};
int main()
{
Base b;
Derived d;
b.x = 10;
d = b; // 错误,只会复制基类部分的数据,派生类部分的数据将被忽略
return 0;
}
注:
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的
- 如果希望在基类和派生类之间进行对象赋值转换并保留派生类的额外信息,可以使用指针或引用进行操作,而不是直接进行对象赋值
三、继承中的作用域
在 C++ 继承中,作用域指的是变量或函数的可见性范围。在继承体系中基类和派生类都有独立的作用域。
在派生类中访问基类成员时,需要注意作用域的影响。
- 当派生类继承了一个基类后,它可以使用基类的公有成员和保护成员(如果是私有成员,则只能在基类内部被访问,派生类不能访问)。
- 派生类可以通过以下方式来访问基类成员(公有和保护成员):
1. 使用作用域解析运算符 `::`。
2. 直接访问基类成员。
例如,如果基类有一个名为 '_var' 的公有成员,那么在派生类的成员函数中,可以通过 'Base::_var' 来访问该成员变量。- 如果派生类中定义了与基类同名的成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。那么可以使用作用域解析运算符 '::' 来指定要访问的是基类的成员还是派生类自己的成员。
例如,如果派生类有一个同名成员 '_var' ,那么可以使 'Base::_var' 来访问基类的成员变量,使用 'Derived::_var' 来访问派生类的成员变量。
注:
1.如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
2.在实际中在继承体系里面最好不要定义同名的成员。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected:
string _name = "张三"; // 姓名
int _num = 000; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 身份证号:" << Person::_num << endl;
cout << " 学号:" << _num << endl; //容易与基类身份证号混淆
}
protected:
int _num = 999; // 学号
};
void Test1()
{
Student s1;
s1.Print();
};
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" << i << endl;
}
};
void Test2()
{
B b;
b.fun(10);
}
四、派生类的默认成员函数
在之前的学习中了解到在 C++ 中,类有6个默认成员函数。【C++】类和对象(二)
派生类会继承基类的默认成员函数。如果基类有默认构造函数、拷贝构造函数、取地址运算符重载、析构函数或赋值运算符重载函数,并且派生类没有定义自己的版本,那么派生类就会继承基类的这些默认成员函数。
下面的代码中我创建了Person类和Student类,实现了它们的构造、拷贝构造、赋值运算符重载和析构函数。
class Person
{
public:
Person(const char* name = "张三")
: _name(name)
{
cout << "Person(const char* name)" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(const char* name, int num)
: Person(name)
, _num(num)
{
cout << "Student(const char* name, int num)" << endl;
}
Student(const Student& s)
: Person(s)
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator = (const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s)
{
Person::operator =(s);
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _num; //学号
};
4.1 构造
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员(不论初始化列表是否含有对基类构造函数的使用或基类构造函数的使用放在初始化列表后面的位置,都会先执行基类的构造函数)。
- 如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类对象初始化先调用基类构造再调派生类构造。
void Test1()
{
Student s1("张三", 18);
}
该函数创建了Student类的对象s1,其中初始化的过程为:使用Student类的构造函数、初始化列表时跳转到基类的构造函数,初始化基类的成员,返回,继续执行剩余的初始化。
因为基类没有默认的构造函数,所以必须在派生类构造函数的初始化列表阶段显示调用基类的构造函数Person(name)
4.2 拷贝构造
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
void Test2()
{
Student s1("张三", 18);
Student s2(s1);
}
- 将Student类的s给Person的拷贝构造传参,用到了我们上面学到的切片的概念。
- S2对S1拷贝构造,调用了Person的拷贝构造。
结果:
4.3 赋值运算符重载
- 派生类的operator=必须要调用基类的operator=完成基类的复制
void Test3()
{
Student s1("张三", 18);
Student s3("李四", 17);
s1 = s3;
}
这里给基类的赋值运算符重载传参也用到了切片。
4.4 析构函数
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
- 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个后面讲多态时会介绍)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
例如在前面的那些结果中,析构函数总是先执行派生类Student的,再执行基类Person的。
总之,派生类默认会继承基类的构造函数、析构函数、拷贝构造函数和赋值操作符重载函数。这些默认成员函数提供了对从基类继承的成员变量和行为的处理和管理。