1.1 概念
概念:继承 (inheritance) 就是(自顶向下)在一个已存在的类的基础上建立一个新的类。
作用:使代码可以复用。
它允许程序员在保持原有类特性的基础上进行扩展,增加功能。
一些角度:
从集合交叉(自底向上)的角度上说,可以解决【不同的类有很多相同的动作,代码重复写n变】的问题,只需要把它们共同的动作抽取出来成为父类,它们作为子类。
从层次(自顶向下)的角度上说,可以解决为父类【增加功能、扩展功能】的问题。
引用「我是小白呀」大大的说法:
已存在的类: 基类 (base class) 或父类 (father class)
新建立的类: 派生类 (derived class) 或子类 (son class)
一个新类从已有的类获得其已有特性, 称为类的继承.
通过继承, 一个新建的子类从已有的父类那里获得父类的特性
派生类继承了基类的所有数据成员和成员函数, 并可以对成员做必要的增加或调整
从已有的类 (父类) 产生一个新的子类, 称为类的派生.
类的继承是用已有的类来建立专用新类的编程技术
一个基类可以派生出多个派生类, 每一个派生类又可以作为基类再派生出新的派生类. 因此基类和派生类是相对而言的
派生类是基类的具体化, 而基类则是派生类的抽象
基类和父类的关系:一对多(多重继承),多对一(一个父类派生多个子类),一对一(一个父类派生一个子类)。
1.2 继承方式:单继承和多重继承
单继承 (single inheritance) 指一个派生类只从一个基类派生.
- 单继承关系形成的层次是一个树形结构
- 箭头表示继承的方向, 从派生类指向基类
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/8422c6c4e1f4f494281e67535f699305.png)
1.2.2 多重继承
多重继承 (multiple inheritance) 是指一个派生类有两个或多个基类. 派生类不仅可以从一个基类派生, 也可以从多个基类派生.
————————————————
版权声明:本文为CSDN博主「我是小白呀」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_46274168/article/details/116592728
1.2 核心语法
1.2.1 子类的声明
class 派生类名:[继承方式]基类名{
派生类新增加的成员
};
1.2.2 成员限定符,继承方式与继承结果(private,protected,public)
成员访问限定符 (默认 private):
- public (公用的)
- private (私有的)
- protected (受保护的)
继承方式包括 (默认 private):
- public (公用的)
- private (私有的)
- protected (受保护的)
表格(水管):
1.2.2 子类的构造函数(负责基类初始化!)
一般写法:
派生类构造函数名 (总形式参数表列) : 基类构造函数名 (实际参数表列) {
派生类中新增数据成员初始化语句
}
子类构造函数的注意点:
- 需要负责基类初始化+子类初始化。而对于编译器来说,它先调用基类构造函数,再调用子类构造函数。
- 构造函数既可以类内定义,也可以类外定义。(见下文例子)
(1)类内定义:
class Student1 :public Student {
private:
int age;
string address;
public:
Student1(int num, string n, char s, int a, string addr) : Student(num, n, s), age(a), address(addr) {};
void show1();
};
类外定义:
class Student1 :public Student {
private:
int age;
string address;
public:
Student1(int num, string n, char s, int a, string addr);
void show1();
};
Student1::Student1(int num, string n, char s, int a, string addr) : Student(num, n, s) {
age = a;
address = addr;
}
PS:构造函数的写法还有很多种,大家不必拘泥,灵活使用,其他构造函数的方法可以参看:
【C++】结构体构造函数和实例化详解-打包解决你的所有困惑(●‘◡‘●)_Keroro军曹大人的博客-CSDN博客https://blog.csdn.net/icecreamTong/article/details/130627646?spm=1001.2014.3001.5501因为C++11结构体的构造函数参考了类的构造函数,所以两篇文章可以互通的。
(2)子类构造函数和析构函数的顺序:
调用顺序:构造时先调用基类后调用派生类,析构时相反
#include<iostream>
using namespace std;
class Base {
protected:
Base() {
cout << "Base Constructed." << endl;
};
~Base() {
cout << "Base Destructed." << endl;
};
};
class Derived :public Base {
public:
Derived(char c) {
cout << "Derived Constructed, value:" << c << endl;
};
~Derived() {
cout << "Derived Destructed" << endl;
};
};
int main() {
Derived d('l');
return 0;
}
运行结果:
总结:
- 当不需要对派生类新增的成员函数进行任何初始化操作时, 派生类构造函数体可以为空
- 基类没有构造函数或构造函数参数为空, 在派生类构造函数中可不写调用基类构造函数的语句, 盗用派生类构造函数时系统会自动调用基类的默认构造函数
- 基类中定义了有参的构造函数, 派生类构造函数总必须写出基类的构造函数及其参数
- 基类中既定义无参数的构造函数,又重载了有参数的构造函数, 派生类构造函数中可以调用带参的基类构造函数, 也可以不调用基类的构造函数
1.2.3 多次继承
一般写法:
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
//访问修饰符继承方式是 public、protected 或 private 其中的一个,用来修饰每个基类,各个基类之间用逗号分隔
例子:
square.h
#include <iostream>
using namespace std;
// 基类 Shape
class Shape
{
public:
Shape(int w, int h) :width(w), height(h) {};
int getArea()
{
return (width * height);
}
protected:
int width;
int height;
};
// 基类 PaintCost
class PaintCost
{
public:
int getCost(int area)
{
return area * 70;
}
};
// 派生类
class Rectangle : public Shape, public PaintCost
{
public:
Rectangle(int w, int h) :Shape(w, h) {};
int getCost1() {
int area=getArea();
return getCost(area);
}
};
main.cpp
#include <iostream>
#include"square.h"
int main() {
Rectangle Rect(5, 7);
cout << "Total area: " << Rect.getArea() << endl;// 输出对象的面积
cout << "Total paint cost: $" << Rect.getCost1() << endl;// 输出总花费
return 0;
}
运行结果:
1.2.3 拷贝构造函数
1. 一个类可以设置多个构造函数。
2. 实例化时,一个对象可以选择且只能选择一种构造函数。因为类不允许对象多次初始化,哪怕选择不同的构造函数。
3.拷贝构造函数用到【切片】或【重载运算符=】的技术。
#include<string>
#include<iostream>
using namespace std;
class Person
{
public:
string _name;
int _age;
char _sex;
Person(string name, int age, char sex)// 父类没有默认构造函数
:_name(name)
, _age(age)
, _sex(sex)
{}
};
class Student :public Person
{
public:
Student(string name = "wt", int age = 20, char sex = 'M',string id = "123")
:Person(name, age, sex) // 显式调用父类的构造函数
, _id(id)
{}
Student(const Student& s)
:Person(s) // 调用父类的拷贝构造,使用子类对象切片拷贝给父类那部分成员
{
_id = s._id;
}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s); // 调用父类的赋值,去赋值父类的那部分成员
_id = s._id;
}
}
~Student()
{
//Person::~Person(); // 手动调用父类的析构函数时也需要加作用域
} // 因为C++在类中将析构函数名统一处理成destructor
protected: // 因为子类的析构函数胡自动调用父类的,所以自己不要显式去调用,不然会导致父类被析构两次
string _id;
};
1.2.4 成员变量/函数的继承(包括同名变量/函数的问题)
首先了解下继承的作用域:
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同(不管参数和返回值是否相同)就构成隐藏。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
————————————————
版权声明:本文为CSDN博主「风继续吹TT」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Edward_Asia/article/details/124035345
关于隐藏的解释:
- 1. 隐藏是指,在父子函数有同名函数的情况下,不执行同名的父函数,只执行同名的子函数。
- 2. 隐藏不是重载,因为不是在同一作用域。
- 3. 无论是在子类的成员函数里,或是在子类对象上,如果要使用同名的父函数,只需要加上表示父函数的作用域::即可。
以下是父子函数有同名函数的例子:
square.h
#include <iostream>
using namespace std;
// 基类 Shape
class Shape
{
public:
Shape(int w, int h) :width(w), height(h) {};
int getArea()
{
return (width * height);
}
protected:
int width;
int height;
};
// 基类 PaintCost
class PaintCost
{
public:
int extra_cost = 100;
int getCost(int area)
{
cout << "using PaintCost's getCost" << endl;
return area * 70;
}
};
// 派生类
class Rectangle : public Shape, public PaintCost
{
public:
int extra_cost = 50;
Rectangle(int w, int h) :Shape(w, h) {};
int getCost() {
int area=getArea();
cout << "extra_cost is:" << extra_cost<<endl;
return PaintCost::getCost(area);
}
};
main.cpp
#include <iostream>
#include"square.h"
int main() {
Rectangle Rect(5, 7);
cout << "Total area: " << Rect.getArea() << endl;// 输出对象的面积
cout << "Total paint cost: $" << Rect.getCost() << endl;// 输出总花费
cout << Rect.PaintCost::getCost(10) << endl;
//[注]这个花费是输入新的面积10得到的花费,即10*70=700,不是成员变量变量相乘后得到的面积35的花费
//这启示我们父类的函数可以单独拎出来调用,可以传入新的参数,而不必拘泥于仅把子类的成员变量作为参数
cout << Rect.extra_cost<<endl;
cout << Rect.PaintCost::extra_cost<<endl;
return 0;
}
如果试图调用基类的同名函数getCost()并传入参数,编译器会报错。说明子类对象Rect.getCost()调用的同名函数是子类的同名函数。
如果想要在子类对象中调用父类的同名函数,应加上父类的作用域:
1.3 细节
private 成员变量的作用:隐藏和封装。
show(); //显示private成员是多少,但不可改
类的继承和派生:解决的问题
【更像是公共特性和独有特性的关系】
*****************************************************************************************
1.子类的声明
class [childname] : [继承方式] [父类名]{};
class Student: public Person{};
*****************************************************************************************
2.子类的构造函数
(1)模板
(1.1)一般情况
class Student : public Person{
public:
Student([1*]):Person([2*]){
[3*]
}
};
1*外部传入的参数列表,要写参数类型
2*一一对应赋值给Person构造函数的参数列表,不要写参数类型
3*子类特殊参数的初始化
(1.2)含有子对象的派生类的构造函数:
派生类名(参数总表):基类名(参数表0),子对象名1(参数表1),...,子对象名n(参数表n)
{
派生类新增成员的初始化语句
}
【附录】
说明:父类有参,一定要构造;派生只负责直接基类的初始化
1、当基类构造函数不带参数时,派生类不一定需要定义构造函数;然而当基类的构造函数哪怕只带有一个参数,它所有的派生类都必须定义构造函数,甚至所定义的派生类构造函数的函数体可能为空,它仅仅起参数的传递作用。
2、若基类使用默认构造函数或不带参数的构造函数,则在派生类中定义构造函数时可略去“:基类构造函数名(参数表)”,此时若派生类也不需要构造函数,则可不定义构造函数。
3、如果派生类的基类也是一个派生类,每个派生类只需负责其直接基类数据成员的初始化,依次上溯。
————————————————
版权声明:本文为CSDN博主「白鳯」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44368437/article/details/117563488
*****************************************************************************************
3.子类的成员函数调用父类成员函数
class Student : public Person{
public:
void show(){
Person::show(); //虽然不可直接访问父类的private,但是可以通过父类的public的函数访问
}
};
作用:
【1、一些不想被外部访问的函数之间的调用,不想给外部留接口】
【2、内部调用,实现封装,不想麻烦外部】
4.子类对象的成员函数,调用父类成员函数
class Person {
private:
string name;
public:
void Getname(){
cout << name;
}
};
class Student :public Person {...};
//实例化
Student stu("While", "110103**********23", 12, 123);
stu.Getname();
【作用:减少代码重用】
*****************************************************
【想清楚传递性关系(窄口管道)】
(1)类:本类,子类,其他类
(2)对象:本类对象,子类对象,其他类对象 (3)普通函数
父类
private:
(1)类:本类
protected:
(1)类:本类,子类(继承的方式(作用域:: Person::Getname();))
public:
(1)类:本类,子类,其他类
(2)对象:本类对象,子类对象(继承的方式(作用域:: Person::PerShow();)),其他类对象(相当于普通函数的方式) (3)普通函数([父类] [父对象名]){ 父对象名.公共函数 }
|
|
向下传递
|
|
子类(有没有发现,子类的公有保护私有的函数的限定和父类是一样的)
private:
(1)类:本类
protected:
(1)类:本类,子类的子类
public:
(1)类:本类,子类的子类,其他类
(2)对象:本类对象,子类的子类对象(继承的方式(作用域:: Person::PerShow();)),其他类对象(相当于普通函数的方式) (3)普通函数([子类] [子类对象名]){ 子类对象名.公共函数 }
子类的子类还能不能访问父类,要看继承方式。
*********************************************************************************
多继承与虚基类
*********************************************************************************
多继承:一个子类继承多个父类
格式
-class [子类名]: [继承方式] [父类1],[继承方式] [父类2],[继承方式] [父类3] {};
虚基类
【解决的问题】
多继承的访问相同变量,究竟都要对父类的拷贝操作,还是各自再拷贝一份,各自操作的问题。
【场景】
类的结构关系:
-Parent {};
- Child1:Parent{}; Child2:Parent{}; Child3:Parent{};
-Grandchild:Child1,Child2,Child3{};
【详解】
如果不用虚基类办法,如果C1,C2,C3对Parent的同一成员都有操作,Grandchild在初始化构造C1,C2,C3 时,
除第一个继承的子类(C1),会拷贝Parent的成员到每一个子类(C1,C2,C3)里。并且第一个继承Parent的C1的a是Parent的a,不是拷贝,C2,C3的是拷贝。
如果用虚基类,Parent只存在一个拷贝(即只有一个数据成员a),C1,C2,C3中对对a的访问都是Parent的a而不是它在C2,C3的拷贝。
总结:多继承+对基类都有操作的场景下,使用虚基类,只右父类的1份拷贝;不用虚基类,除第一个子类用父类的拷贝外,每个子类再拷贝1份
【总结】
继承:(一个/多个)子类复用父类的功能。(@多叉树)
二、友元
(1)这个类的成员函数
(2)友元。——谁是我的friend?
(1)友元函数。
可以是另一个类的成员函数 or 普通函数
在被访问的类里,要声明友元函数!声明方法:friend +函数声明。声明可以放在被访问类的公有/保护/私有(有什么区别)
访问方式:()入口传递对象名/对象指针/对象引用
(1)普通函数
(2)类的成员函数 例子:class Score{friend void Student::show(Score &sc) }class Student { show(Score &sc)};
(2)友元类
当一个类被说明为另一个类的友元类时,它所有的成员函数都成为另一个类的友元函数,
private 成员函数的作用:
如何访问?
protected 成员变量的作用:
如何访问?
protected 成员函数的作用:
如何访问?
public 成员变量的作用:
如何访问?
public 成员函数的作用:
如何访问?
三、虚函数
*********************************************************************************
虚函数
*********************************************************************************
【语法1】
(1)抽象类的定义:virtual void funtion()=0
(在类外声明虚函数时,不用再加 virtual)
(2)在派生类中,虚函数被重新定义时,它的函数原型和基类的函数原型必须完全相同。
(5)虚函数必须是其所在类的【成员函数】,而不能是友元函数,也不能是静态成员函数,(?)因为虚函数调用要靠特定的对象来决定该激活哪个函数。
(6)使用对象名和点运算符的方式调用虚函数是在【编译时进行的】,【是静态联编】(?),(?)【没有利用虚函数的特性】。只有通过基类指针访问虚函数时才能获得运行时的多态性。
【语法2】
派生类调用基类函数,注意:{
(1)if(派生类要重写基类函数){
调用于派生类的基类函数,不用加 : : 域作用符
}
(2)if(派生类不重写基类函数){
调用于派生类的基类函数,要加 : : 域作用符
}
}
(1)的常见用法是在重写基类函数的函数(即派生类的虚函数)中,再调用基类函数,目的是减少代码量
BrassPlus::ViewAcct(){
Brass::ViewAcct();//基类虚函数的东西
... //派生类的虚函数新实现的东西
}
总结:继承+改写
【语法三】
【语法四】虚析构函数
如果析构函数不声明virtual,普通析构函数根据【指针类型】来找到要析构的类
声明了virtual的析构函数,将调用相应【对象类型】的析构函数。
调用虚析构函数,可以确保正确的析构函数序列(?)
什么时候基类必须用虚析构函数(?)
*********************************************************************************
虚函数表
*********************************************************************************
*********************************************************************************
纯虚函数 & 抽象类
*********************************************************************************
*纯虚函数:如果【一个类的virtual函数只有定义没有声明(/实现)】,该基类叫抽象类,该virtual函数叫纯虚函数。
纯虚函数的声明由派生类来实现。
因为抽象类的纯虚函数没有声明,所以抽象类不能直接生成对象。
为什么要有抽象类?
1、(?)防止派生类忘记实现虚函数,
2、(?)在某些场景下,创建基类对象是不合理的,
四、模板
如何访问?
*********************************************************************************
静态联编和动态联编
*********************************************************************************