目录
前言:
C++中,继承是一种创建新类的方式,新创建的类(子类)会继承已存在的类(父类)的特性,继承允许我们定义可重用、扩展和特化基类的派生类;
继承的优点:
- 代码重用:子类可以复用父类的代码和数据;
- 可扩展性:子类可以扩展父类的功能;
- 可维护性:子类可以重用父类的实现同时子类可以有自己的实现,使得维护更加方便;
- 多态性:子类可以覆盖父类的方法,实现不同的行为,这是实现运行时多态的基础;
继承的定义
示例:
假设需要设计一个学校人员管理系统,学校中有老师,学生等角色,需要将各个角色的信息都管理起来,若设计两个类(学生类、教师类)存放各自的属性信息,但是学生类与教师类存在共有属性,比如姓名,年龄,电话号,家庭住址等等,如此便造成代码冗余,为使代码可以复用,继承便应运而生,继承的本质为类设计层次的复用;
cpp
// 父类/基类
class Person
{
public:
//父类成员函数
void print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "Peter";
int _age = 20;
};
// 子类/派生类
class Student :public Person
{
protected:
int _stuid;//学号
};
// 子类/派生类
class Teacher :public Person
{
protected:
int _jobid;//工号
};
int main()
{
Student s;
Teacher t;
s.print();//子类Student调用父类Person的成员函数
t.print();//子类Teacher调用父类Person的成员函数
return 0;
}
运行结果:
监视窗口:
注:继承后父类Person的成员变量与成员函数成为子类Teacher与子类Student的一部分;
继承定义
定义格式
继承方式限定了基类成员变量、成员函数在派生类中的访问权限;
继承父类成员在子类中访问方式的变化
示例一:
示例二:
示例三:
总结:
父类中的私有成员(访问限定符private修饰的成员),无论子类以何种方式继承,在子类的类内,类外不可被访问;
父类中的其他成员(访问限定符public/protected修饰的成员)在子类中的这些成员的访问权限取父类成员的的访问限定符与继承方式取最小者;
( 其中访问权限/继承方式: public > protected > private )
- 关键字class默认继承方式为private,关键字struct默认继承方式为public,实践时最好显示写出继承方式;
cpp
struct Student : Person //默认为公有(public)继承
class Student : Person //默认为私有(private)继承
若父类成员不想在类外直接被访问,但在子类中可以访问,此时将父类成员定义为protected;
继承的目的是代码复用,因此实际应用中一般使用public继承;
赋值兼容规则(切片)
子类对象赋值给父类对象
子类对象赋值给父类指针
子类对象赋值给父类引用
子类对象可以赋值给父类对象、父类的指针、父类的引用;这种操作叫做赋值兼容/切割/切片,意为将子类对象中继承于父类的成员切割下来赋值给父类对象,这不是类型转换,是天然的赋值行为;
注意:切片仅限公有继承;
示例:
cpp
//父类公有
class Person
{
public:
string _name = "Peter";
int _age = 20;
};
// 子类保护继承/私有继承
class Student :protected Person
{
public:
protected:
int _stuid = 1;//学号
};
基类对象不能赋值给派生类对象;
继承中的作用域
- 在继承体系中基类和派生类的作用域相互独立;
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义;(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏;
- 注意在实际中在继承体系里面最好不要定义同名的成员;
示例一:成员变量的隐藏
cpp
class Person
{
protected:
string _name = "Peter";
int _num = 123456;//身份证号
};
class Student :public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "学号:" << _num << endl;
cout << "身份证号:" << _num << endl;//这种方式是不能输出父类中的_num的信息的
cout << "身份证号:" << Person::_num << endl;//必须以父类::父类成员的方式访问同名的父类的成员变量
}
protected:
int _num= 1;//学号
};
int main()
{
Student s;
s.Print();
return 0;
}
运行结果:
示例二:成员函数的隐藏
cpp
class A
{
public:
void Func()
{
cout << "调用A类的Func()函数" << endl;
}
};
class B :public A
{
public:
void Func(int n)
{
cout << "调用B类的Func()函数" << endl;
}
};
int main()
{
B b;
//调用B类(子类)的Func()函数
b.Func(10);
//继承体系的不同作用域中,同名函数构成隐藏,无法以如下方式调用父类成员函数
//b.Func();
//只有通过父类::父类成员的方式才能调用构成隐藏关系的父类成员函数
b.A::Func();
return 0;
}
运行结果:
父类成员变量与成员函数被隐藏的本质原因为局部优先原则,子类中存在即在子类中查找,子类中不存在才在父类中查找,若在子类中已经查找到父类中不用查找;
子类的默认成员函数
- 父、子类中各自的成员处理方式
子类中有两部分成员,一类是子类原生的成员,另一类是继承于父类的成员;
对于子类中的原生成员,按照普通类调用默认成员函数的规则进行处理;
对于继承于父类的成员,将会调用父类中的默认成员函数规则进行处理;
- 需要程序员写默认成员函数的情形
- 父类没有默认构造函数( 无参构造),子类需要程序员显式实现构造;
- 子类涉及资源申请,会导致析构两次的问题,需要程序员显式实现拷贝构造和赋值;
- 子类需要释放资源,需要程序员显式实现析构函数;
子类的构造函数(先父后子)
构造函数的规则:
- 内置类型若给出缺省值,按照缺省值处理;
- 内置类型若不给出缺省值,初始化为随机值,某些编译器会初始化为0;
- 自定义类型调用自身的默认构造函数;
cpp
class Person
{
public:
Person()
{
cout << "调用父类构造函数" << endl;
}
protected:
string _name="karlen";
};
class Student : public Person
{
public:
Student()
{
cout << "调用子类构造函数" << endl;
}
protected:
int _stuid=10;
};
int main()
{
Student d1;
return 0;
}
运行结果:
构造函数先父后子的原因:
编译器规则:对象当中的成员变量,存储顺序按照声明顺序确定;
所以初始化列表初始化的顺序按照声明的顺序确定,而不是出现的顺序;
case 1:父类构造函数为非默认构造函数,子类构造函数必须在初始化列表位置显示调用父类的构造方法;
cpp
class Person
{
public:
//父类不存在默认构造函数(无参构造、全缺省构造、编译器自动生成的构造)
//父类显示实现构造函数,编译器不再自动生成
Person(const char* name)
: _name(name)//string类单参数的构造函数支持类型转换
{}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(const char* name = "Peter", int id = 10)
//: _name(name) (×)非法的成员初始化
//: person:: _name(name) (×) 子类成员隐藏才需要指定父类的作用域
//子类中的父类成员必须调用父类的构造函数初始化
: Person (name)//子类中的父类成员显示调用父类构造函数的规定写法
, _stuid(id)
{}
protected:
int _stuid; //子类自身成员
};
int main()
{
Student d("Karlen", 20);
return 0;
}
监视窗口:
case 2:
父类构造函数为默认构造函数,子类构造函数可以在初始化列表位置显示调用父类构造或者不显示调用父类构造;
cpp
class Person
{
public:
//父类默认构造函数
Person(const char* name="Linda")
: _name(name)
{
}
protected:
string _name; //姓名
};
class Student : public Person
{
public:
//写法一
Student(const char* name = "Peter", int id = 10)
:_stuid(id)
, Person(name)//父类成员显示调用父类构造函数的规定写法
{}
//写法二
Student(int id)
:_stuid(id)
{}
protected:
int _stuid; //子类自身成员
};
int main()
{
Student d1("Karlen", 20);
Student d2(10);
return 0;
}
监视窗口:
子类的拷贝构造函数
拷贝构造函数的规则:
- 拷贝构造函数对于内置类型按字节序完成值拷贝;
- 拷贝构造函数对于自定义类型调用其拷贝构造函数完成;
若基类的拷贝构造已经定义且子类中涉及资源申请,子类的拷贝构造函数必须在其初始化列表的位置显式调用基类的拷贝构造;
cpp
class Person
{
public:
Person(const char* name)
:_name(name)
{}
//父类拷贝构造函数显示实现
//拷贝构造函数也是构造函数,父类显示写出,父类无默认构造函数可用,因此子类必须在初始化列表位置显示调用
Person(const Person& p)//p为person类对象,p引用切片对象
:_name(p._name)
{}
protected:
string _name;//姓名
};
class Student : public Person
{
public:
//子类构造函数
Student(const char* name,int id)
:_stuid(id)
, Person(name)
{}
//子类拷贝构造函数(假设涉及资源申请)
Student(const Student& stu)
: _stuid(stu._stuid)
, Person(stu)//stu为student类对象,显示调用父类拷贝构造,传参为赋值兼容原则
{
}
protected:
//string _name //继承于父类的子类成员---自定义类型
int _stuid; //子类自身成员---内置类型
};
int main()
{
Student d1("karlen", 20);
Student d2(d1);
return 0;
}
监视窗口:
子类的赋值运算符重载
赋值运算符重载的规则:
- 赋值运算符重载对于内置类型成员变量直接赋值;
- 赋值运算符重载对于自定义类型成员变量调用对应类的赋值运算符重载完成赋值;
若基类的赋值运算符重载函数已经定义且子类中涉及资源申请,子类的赋值运算符重载函数中必须显示调用父类的赋值运算符重载;
cpp
class Person
{
public:
//父类的构造函数
Person(const char* name)
:_name(name)
{}
//父类赋值运算符重载显示实现
Person& operator=(const Person& p)
{
if (this != &p)
{
_name = p._name;
}
return *this;
}
protected:
string _name;//姓名
};
class Student : public Person
{
public:
//子类构造函数
Student(const char* name,int id)
:_stuid(id)
, Person(name)
{}
//子类的赋值运算符重载(假设涉及资源申请)
Student& operator=(const Student& stu)
{
if (this != &stu)
{
//父类成员切片传入父类的赋值运算符重载函数
Person::operator=(stu);//继承体系中只要函数名相同就构成隐藏关系,访问父类成员方式 父类::父类成员
//子类成员根据是否涉及资源申请实现深浅拷贝
_stuid = stu._stuid;
}
return *this;
}
protected:
int _stuid;
};
int main()
{
Student d1("karlen", 20);
Student d2("Linda", 10);
d1 = d2;
return 0;
}
监视窗口:
**注:**子类赋值运算符重载中调用父类赋值运算符重载,通过切片,完成父类成员的赋值;
子类的析构函数(先子后父)
析构函数的规则:
- 析构函数对于内置类型成员变量不做任何处理,最后由操作系统将其内存回收;
- 析构函数对于自定义类型成员变量调用对应类的析构函数完成资源清理;
cpp
//子类析构函数
class Person
{
public:
Person(const char* name)
:_name(name)
{}
//父类析构函数
~Person()
{
cout << "调用父类的析构函数" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
//子类构造函数
Student(const char* name, int id)
:_stuid(id)
, Person(name)
{}
//子类析构函数
~Student()
{
cout << "调用子类的析构函数" << endl;
}
protected:
int _stuid;
};
int main()
{
Student d1("karlen", 20);
return 0;
}
运行结果:
析构函数先子后父的原因:
若父类中存在资源申请,若先析构父类,父类中的资源已经被清理释放,子类析构函数又去访问父类的成员,容易存在野指针等风险,所以必须保证先子后父;
为保证先子后父,编译器会自动调用父类的析构;
若子类中涉及资源申请,子类的析构函数才需要显示实现,但子类的析构函数中不需要显式调用父类的析构函数,手动调用父类析构将会造成重复析构;
cpp
class Person
{
public:
Person(const char* name)
:_name(name)
{}
//父类析构函数
~Person()
{
cout << "调用父类的析构函数" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
//子类构造函数
Student(const char* name, int id)
:_stuid(id)
, Person(name)
{}
//子类析构函数
~Student()
{
cout << "调用子类的析构函数" << endl;
//父类析构函数的显示调用方式
Person::~Person();
//析构函数的名字会被编译器统一处理为destructor()
//子类的析构函数和父类的析构函数之间构成隐藏,只能通过父类::父类成员的方式调用
}
protected:
int _stuid;
};
int main()
{
Student d1("karlen", 20);
return 0;
}
运行结果:
欢迎大家批评指正,博主会持续输出优质内容,谢谢大家观看,码字画图不易,希望大家给个一键三连支持~ 你的支持是我创作的不竭动力~