1: 继承
1.1 继承的概念
继承是三大特性之一,体现代码复用的思想
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程
之前接触的复用都是函数复用,继承是类设计层次的复用
定义:在一个已经存在的类的基础上,建立一个新的类,让新类拥有之前类的特性
1之前存在的类称为"基类"
2新建的类称为"派生类"
3派生类除了可以继承基类外,还可以对其做出必要的代码修改(函数隐藏)和增加
无 demo说 J8
demo
#include <iostream>
using namespace std;
class Human // 定义了一个基类,名字叫Human
{
public:
string name = "小明"; // 基类里面定义了一个string类型的和一个int类型
int age = 18;
};
class Student:public Human // 定义了一个以public方式继承基类的派生类Student
{
public:
int schoolnum = 666; // 在父类的name和age的基础上增加了一个schoolnum
void print()
{
cout << name << endl << age << endl << schoolnum << endl; // 输出
}
};
int main()
{
Student st;
st.print();
return 0;
}
好吧是不是还是看不懂,那把这个代码分成两半
第一部分demo
class Human
{
public:
string name = "小明";
int age = 18;
};
第二部分demo
class Student:public Human
{
public:
int schoolnum = 666;
void print()
{
cout << name << endl << age << endl << schoolnum << endl;
}
};
第一部分的代码就是我们平时使用的class
对于第二部分的解读我们先举一个例子
如果我们要设计一个学校系统,那么对于学生,老师.....一系列的人,我们都是需要将姓名和年龄等必要信息录入,但是单独针对到某一类人,比如学生,除了必要信息外,还单独有一个学号。比如老师除了必要信息外,还有一个单独的职工号
老师
学生
姓名
姓名
一模一样
年龄
年龄
职工号
学号
CSDN@幻茶
第一部分代码:
为了偷懒提高效率,我们这里就可以把姓名和年龄封装到一个class类里面
第二部分代码:
新创一个类,继承原有姓名和年龄类的基础上,再新增学号/职工号
所以说怎么继承呢?
继承格式
class 派生类:继承方式 基类{};
以上述代码为例
class Student:public Human{};
//Student是派生类的名字,public是继承方式,Human是基类的名字
//意思就是说我定义了一个名叫 Student的类 以public的方式 来继承你Human
1.2 同名成员
demo
#include <iostream>
using namespace std;
class Human
{
public:
string name = "基类";
void print()
{
cout << name << endl;
}
};
class Student:public Human
{
public:
string name = "派生类";
void print()
{
cout << name << endl;
}
};
int main()
{
Student st;
cout << st.name << endl; // 派生类
cout << st.Human::name << endl; // 基类
st.print(); // 派生类
st.Human::print(); // 基类
return 0;
}
1.2.1 同名成员变量
- 基类和派生类中出现了同一个成员变量,派生类优先
- 想要访问基类的该成员变量,就需要加上修饰
1.2.2 同名成员函数
同样一个函数在基类和派生类中都存在就构成了隐藏
函数重载发生在同一个作用域,这里的基类和派生类是两个作用域
2想要访问基类的函数需在函数名前写入基类名::
3编译器默认调用派生类中匹配的函数,如果派生类中没有匹配的函数编译器就会报错
虽然成员函数只要函数名相同就构成隐藏,对参数列表没有要求
但如果修改派生类的函数的参数
示例
void print(int x) // 对派生类的print函数加入一个参数
{
cout << name << endl;
}
此时会报错
这是因为编译器默认调用子类中print
函数
但是子类中唯一的print
函数有一个默认的参数
所以编译器无法找到匹配的print
函数,所以就会报错
1.3 基类和派生类的相互赋值
相互赋值错误demo
class Human // 基类
{
public:
string name = "小明";
int age = 18;
};
class Student:public Human // 派生类
{
public:
int schoolnum = 666;
};
int main()
{
Student st;
Human hm;
hm = st; // 将派生类赋值给基类---------对
st = hm; // 将基类赋值给派生类---------错
return 0;
}
error分析
error: no match for 'operator=' (operand types are 'Student' and 'Human')
st = hm;
错误:没有与这些操作数匹配的"="运算符
1.3.1 切片原则
1.3.2 三种赋值方式
= 符号
Student st; //派生类
Human hm; //基类
hm = st;
引用
Student st; // 派生类
Human& hm = st; // 基类
指针
Student st; // 派生类
Human* hm=&st; // 基类
1.4 构造函数
构造函数和析构函数不能被继承
派生类的每一个构造函数都必须直接或间接调用基类的任意一个构造函数
有三种方法:
- 透传构造
- 委托构造
- 继承构造
1.4.1 透传构造
上文代码中,基类与派生类都没有编写构造函数
编译器会为这两个类自动添加无参构造函数
且在派生类无参构造函数中调用了基类无参构造函数
透传构造:在派生类中调用基类的构造函数
透传构造
#include <iostream>
using namespace std;
class Father
{
private:
string name;
int age;
public:
Father(string name,int age)
{
this->name = name;
this->age = age;
}
void show()
{
cout << name << " " << age << endl;
}
};
class Son:public Father
{
public:
// 编译器默认添加的调用基类的无参构造函数
// Son():Father(){}
// 透传构造
Son(string name,int age):Father(name,age)
{
}
// 透传构造
Son():Father("佚名",100)
{
}
};
int main()
{
Son s("张三",12);
s.show();
Son s2;
s2.show();
return 0;
}
1.4.2 委托构造
委托构造:同一个类的构造函数可以调用这个类的另一个构造函数
通过委托构造可以让派生类中的构造函数间接调用基类的构造函数
注意:
1互相委托调用的过程中,不能形成闭环
2最终委托的构造函数需要透传构造
3如果没有继承,也可使用委托构造,效率低,但维护起来方便
委托构造
#include <iostream>
using namespace std;
class Father
{
private:
string name;
int age;
public:
Father(string name,int age)
{
this->name = name;
this->age = age;
}
void show()
{
cout << name << " " << age << endl;
}
};
class Son:public Father
{
public:
// 透传构造
Son(string name,int age):Father(name,age)
{
}
// 委托构造
Son():Son("佚名",100)
{
}
};
int main()
{
Son s("张三",12);
s.show();
Son s2;
s2.show();
return 0;
}
1.4.3 继承构造
继承构造并不是继承了构造函数,在C++11中,可以通过继承构造的语法,让派生类自动实现每个基类的构造函数,并且同样参数的构造函数进行透传
using Father::Father;
#include <iostream>
using namespace std;
class Father
{
private:
string name;
int age;
public:
Father(string name,int age)
{
cout << "Father的构造函数调用了" << endl;
this->name = name;
this->age = age;
}
void show()
{
cout << name << " " << age << endl;
}
};
class Son:public Father
{
public:
// 基类有几个重载的构造函数,派生类就自动添加几个
// 每个派生类的构造函数都去透传调用同样参数格式的基类构造函数
using Father::Father;
// 在本例中,Son类会创建一个两个参数的构造函数,透传调用基类的构造函数
};
int main()
{
Son s("张三",12);
s.show();
return 0;
}
1.5 对象的创建与销毁流程
demo
#include <iostream>
using namespace std;
/**
* @brief 作为其它类的成员变量或静态成员变量
* 自己写的类能把每一个构造函数、析构函数打印出来提示信息
* 如果使用string、int类型作为其它类的成员变量,
* 看不到何时创建何时销毁
*/
class Value
{
private:
string name;
public:
Value(string name):name(name)
{
cout << this->name << "创建了" << endl;
}
~Value()
{
cout << name << "销毁了" << endl;
}
};
class Father
{
public:
static Value s_value;
Value value = Value("Father类的成员变量");
// 上一步为成员变量创建
Father() // 构造初始化列表的前提:变量创建完,否则没有赋值目标
{
cout << "Father类的构造函数" << endl;
}
~Father()
{
cout << "Father类的析构函数" << endl;
}
};
// 创建一个栈内存的Value,将其赋给s_value
Value Father::s_value = Value("Father类的静态成员变量");
class Son:public Father
{
public:
static Value s_value;
Value value = Value("Son类的成员变量");
Son() // 无参构造函数不需要透传,编译器自动添加:Father()
{
cout << "Son类的构造函数" << endl;
}
~Son()
{
cout << "Son类的析构函数" << endl;
}
};
Value Son::s_value = Value("Son类的静态成员变量");
int main()
{
cout << "-----------主函数开始执行-----------" << endl;
{
Son s;
cout << "----------对象使用中----------" << endl;
}
cout << "-----------主函数结束执行-----------" << endl;
return 0;
}
运行结果
Father类的静态成员变量创建了 // 静态成员变量创建时间差是因为所写位置不同
Son类的静态成员变量创建了 // 静态成员变量创建时间差是因为所写位置不同
-----------主函数开始执行-----------
Father类的成员变量创建了
Father类的构造函数
Son类的成员变量创建了
Son类的构造函数
----------对象使用中----------
Son类的析构函数
Son类的成员变量销毁了
Father类的析构函数
Father类的成员变量销毁了
-----------主函数结束执行-----------
Son类的静态成员变量销毁了 // 静态成员变量销毁时间差是因为所写位置不同
Father类的静态成员变量销毁了 // 静态成员变量销毁时间差是因为所写位置不同
- 对象使用之前是创建流程,对象使用后是销毁流程,创建与销毁的流程是对称的。
- 静态成员的创建比非静态成员更早,静态成员的销毁比非静态成员的销毁更晚。
- 基类部分比派生类部分先创建,基类部分比派生类部分后销毁。
派生类对象的创建与销毁要调用基类的相关代码,实际开发中继承结构可能是多级的
这也是为什么面向对象的程序运行效率较低的原因,但是编程效率得到了提升
1.6 多重继承
1.6.1 多重继承概念
C++支持多重继承
一个派生类有多个直接继承的基类,属于单一继承的拓展
其中每个基类和派生类的关系仍然可以看作是一个单继承
demo
#include <iostream>
using namespace std;
/**
* @brief The Sofa class 沙发
*/
class Sofa
{
public:
void sit()
{
cout << "沙发能坐着" << endl;
}
};
/**
* @brief The Bed class 床
*/
class Bed
{
public:
void lay()
{
cout << "床能躺着" << endl;
}
};
/**
* @brief The SofaBed class 沙发床类
*/
class SofaBed:public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.lay();
sb.sit();
return 0;
}
1.6.2 多重继承二义性
多重继承在使用的过程中可能会出现一些二义性问题
这也是其它的面向对象的编程语言不支持多继承的原因
1.6.2.1 多个基类有相同名称的成员
可以通过类名::的方式区分不同基类的同名成员
ambiguous
#include <iostream>
using namespace std;
/**
* @brief The Sofa class 沙发
*/
class Sofa
{
public:
void sit()
{
cout << "沙发能坐着" << endl;
}
void position()
{
cout << "沙发放在客厅" << endl;
}
};
/**
* @brief The Bed class 床
*/
class Bed
{
public:
void lay()
{
cout << "床能躺着" << endl;
}
void position()
{
cout << "床放在卧室" << endl;
}
};
/**
* @brief The SofaBed class 沙发床类
*/
class SofaBed:public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.lay();
sb.sit();
// sb.position(); 错误
sb.Sofa::position();
sb.Bed::position();
return 0;
}
1.6.2.2 菱形继承[钻石继承]
如果一个派生类继承了多个基类,而这些基类又有一个共同的基类时
可以通过基类的类名::的方式区分,但是这种方式属于“治标不治本”
更好的解决方法是使用虚继承
当虚继承时,虚继承的派生类(Sofa类与Bed类)会增加一个虚基类表
虚基类表只有一份,所有派生类对象共用这一份,记录公共基类中成员的存储地址
虚继承的派生类对象(Sofa对象与Bed对象)都会多一个虚基类指针的成员
虚基类指针指向虚基类表
当虚继承的派生类被当做基类继承时,每个基类的虚基类指针也会继承给其派生类,但虚基类表不会继承
实际上虚继承是牺牲一部分性能来换取的解决方案,因为查表有性能开销
demo
#include <iostream>
using namespace std;
/**
* @brief The Furniture class 家具
*/
class Furniture
{
public:
void func()
{
cout << "家里没有家具,就叫家徒四壁" << endl;
}
};
// 虚继承
class Sofa:virtual public Furniture
{
};
// 虚继承
class Bed:virtual public Furniture
{
};
class SofaBed:public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
// sb.Furniture::func();
sb.Sofa::func();
sb.Bed::func();
sb.func();
return 0;
}
1.7 继承的总结
- 基类
private
成员无论以什么方式继承到派生类中都是不可见的。
这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类
private
成员在派生类中不能被访问,如果基类成员不想在派生类外直接被访问,但需要在派生类中访问,就定义为protected
。可以看出保护成员限定符
是因继承才出现的。 - 基类的私有成员在派生类都是不可见;基类的其他成员在派生类的访问方式就是访问限定符和继承方式中权限更小的那个(权限排序:
public > protected > private
) - 使用关键字
class
时默认的继承方式是private
- 使用
struct
时默认的继承方式是public
,但最好显式地写出继承方式