文章目录
继承的概念及定义
1.继承的概念
在C++中,所谓“继承”就是在一个已存在的类的基础上建立一个新的类。 已存在的类称为“基类 ”或“父类”,新建的类称为“派生类 ”或“子类 ”。
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能;继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
例如:现在定义要两个类Teacher和Student,他们有共同的成员:姓名、性别、年龄、学院等,不同的Teacher类里要有:职务,Student类里要有年级、班级等。那么共同的部分就可以定义在同一个类Person里,Teacher里定义职务,Student里定义年级和班级,共有的部分在自己类里不写,只要继承下来就行了。
class Person
{
public:
void func()
{
cout<<"hello world"<<endl;
}
string _name; // 姓名
int _age; // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
2.继承的定义
定义格式
继承方式和访问限定符
- 基类的protected成员:不能在类外直接被访问,但在派生类中能访问
- 不可见:指的是基类的私有成员被继承到派生类对象中,但派生类对象在类内外都无法对这些私有成员进行访问
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,不提倡使用protetced/private继承
基类和派生类对象的赋值转换
- 切片。派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。
- 基类对象不能赋值给派生类对象
- 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。
赋值兼容(切片)只能是在共有继承下
切片:把子类中的父类部分(切割出来)赋值给父类的对象/引用/指针,子类继承下来的父类私有成员在子类中是不可见(不可操作的),但切片赋值给p后,P是可以使用的
继承时父类一般不建议设置私有成员
int main()
{
Student s;
Person p;
p = s;//对象赋值
Person& ref = s;//赋值给父类引用
Person* ptr = &s;//赋值给父类指针
return 0;
}
这里的赋值不存在类型转换(不存在Student向Person类型转换)
int i = 1;
double d =2.2;
i = d;//隐式类型转换
const int& ri = d;//ri不能直接引用d,中间有类型转换,类型转换换产生临时变量
//临时变量具有常性,需要加上const
私有继承是不能切片的,因为会涉及到权限的转换。私有继承下来的东西是私有的,切片后变成共有,权限放大,是不合理的。
父类是不能给子类赋值的,但有以下情况:
Person p;
Student s;
s = (Student)p;//行不通,类型转接也不行
//以下两种情况可行,但会有越界的风险
Student* ptr = (student*)&p;
Student& ptr = (student&)p;
//访问时不能越过子类中父类部分
继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
子类和父类出现同名成员:隐藏/重定义。同名函数参数是否相同也不会影响.
函数重载要求在同一作用域,它们有各自作用域,不会构成重载,构成隐藏.
- 注意在实际中在继承体系里面最好不要定义同名的成员
class Person
{
public:
void fun()
{
cout << "father" << endl;
a = 1;
}
protected:
string _name = "李天王"; // 姓名
int _num = 111; // 身份证号
int a;
};
class Student : public Person
{
public:
void fun()
{
cout << "child" << endl;
}
void Print()
{
cout << " 姓名:" << _name << endl;
cout << _num << endl;
cout << Person::_num << endl;//访问父类成员的同名成员_num需指定作用域
}
protected:
string _name = "小李子";
int _num = 999; // 学号
};
int main()
{
Student s;
s.fun();
s.Person::fun();//使用父类中的同名函数需要指定作用域
//如果成员名和函数名不相同,则不构成隐藏,就不需要指定域
return 0;
}
ps:局部和全局可以有同名成员,当访问时优先访问局部,(局部优先、就近原则)
派生类的默认成员函数
派生类在C++中有11个默认成员函数,如果我们不写编译器会自己生成,重要的有四个,构造函数、拷贝构造、赋值、析构函数
想想以下问题
1.派生类的四个默认成员函数我们不写,编译器生成的会如何处理?
2.我们要写的话,应该怎么写?
3.如果我们自己写,又会如何处理?
派生类中如果我们不写默认成员函数,那么分两种情形讨论:
1.从父类继承下来的部分
调用父类的构造析构,拷贝构造和赋值使用父类提供的方法
2.派生类自己的部分
内置类型:构造析构不处理,拷贝和赋值进行浅拷贝或值拷贝
自定义类型:调用自定义类型自己的构造析构、拷贝、赋值
总结:继承下来的调用父类的处理,自己的安照普通类规则处理
什么情况下需要自己写默认构造函数?
1.父类没有构造函数,需要自己写构造
(子类示例化时要调用父类的构造函数对继承的部分初始化,自己的部分可由编译器自己生成)
2.如果子类有资源需要释放时,需要自己显示写析构
3.如果子类存浅拷贝问题,需要自己显示写拷贝构造和赋值
让我们看以下代码,派生类的默认构造函数怎么写
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << 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 = 1)
//:_name(name) 子类访问不到父类的私有成员_name
:Person(name) //调用父类构造函数初始化_name
,_num(num)
{}
//s2(s1)
Student(const Student& s)
:Person(s) //s自动切片传给父类拷贝构造函数
,_num(s._num)
{}
//s2=s1
Student& operator=(const Student& s)
{
if (this != &s)//判断是否自己给自己赋值
{
Person::operator=(s);//父子operator=重名构成了重载
_num = s._num;
}
}
//析构函数的名字会被统一处理成destructor() (原理需要多态的知识解释)
//子类和父类的析构就构成隐藏,需要指定类域才能访问到:Person::~Person()
~Student()
{
//子类析构函数结束时会自动调用父类的析构,所以这里不需显示调用父类析构
// 这样可以保证先析构子类再析构父类,也能避免多次析构父类
//构造时先父类再子类,析构时先子类后父类(栈上定义,后进先出)
}
protected:
int _num; // 学号
};
继承与友元的关系
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,友元关系不能被继承
继承和静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。父类和任意子类访问到的静态成员都是同一个。
静态成员不能在类内初始化,因为静态成员属于整个类,而不属于某个对象。
class A
{
private:
static int a = 0;//静态成员在类内不能初始化,只能定义
const int b = 0;//常量成员可以
static const int c = 0;//可以编译,类内初始化的静态数据成员
//必须是具有不可变的常量整型类型
static const double e = 1.234;//编译不通过
const double e = 1.234;//编译通过
}
int A::a = 0;//在类外初始化静态成员
菱形继承和菱形虚拟继承
单继承 > 只有一个直接父类的
多继承 > 一个子类有两个或以上直接父类
菱形继承 > 多继承中的特殊情况
(只列举了最简单的情况)
1.菱形继承存在的问题
数据冗余和二义性
进行了菱形继承的子类,其直接父类都继承了一份原始父类的数据,则菱形继承的子类就会继承多份原始父类数据.而如果原始父类中有一个1w字节的数组,到最后的子类继承下来的空间大小就翻倍了。造成数据冗余。假如原始父类有一个成员_name,菱形继承的子类就继承了多个_name,当子类去访问_name时,会造成访问的对象不明确,即二义性。需要指定是哪个父类继承下来的_name
在使用多继承时一定要尽量避免使用菱形继承!!
多继承算是C++的缺陷之一,Java、Python等后来语言都没有多继承。
如何解决数据冗余和二义性?
使用虚继承 virtual
使用方法
判断哪个类会造成数据冗余和二义性,直接继承这个类的派生类就要使用虚继承
2.virtual是如何解决这些问题的?
使用虚继承后,子类里就只有一份原始父类的数据。其直接父类中的原始父类的数据,都相当于是是这一份数据的引用,是同一份。
从内存上看原理
//有四个类A、B、C、D,B和C继承了A,D继承了B和C,构成菱形继承
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
//观察其在内存的分布情况
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
d._a = 6;
return 0;
}
从内存上看,内存地址由小到大,先继承的放在前,后继承的放在后
在使用虚继承的情况下,B、C、D的_a
使用的都是同一块空间
虚基表
3.继承与组合
- public继承是一种
is-a
的关系。也就是说每个派生类对象都是一个基类对象。
(A就是B。例如Person定义是人,Student也是人,student is a Person) - 组合是一种
has-a
的关系。假设B组合了A,每个B对象中都有一个A对象。(例如车和轮胎的关系,车上有轮胎,但不能说车就是轮胎。有比如链表和节点的关系)
组合
class AA
{
char* arr[100];
};
class BB
{
int c;
AA a;//BB只能访问AA的共有成员,不能访问私有成员
};
例如链表和节点
struct ListNode
{
ListNode* next;
ListNode* prev;
int data;
};
class List
{
public:
void push_back()
{}
private:
ListNode* head;
};
- 优先使用对象组合,而不是类继承
完全符合is-a
用继承
完全符合has-a
用组合,都可以用,优先用组合 - 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用
(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。 - 继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关
系很强,耦合度高。 - 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对
象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),
因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,
耦合度低。优先使用对象组合有助于你保持每个类被封装。 - 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
继承的耦合度高,基类成员的修改有可能对子类的影响很大。
继承那么复杂,能不能抛弃继承只用组合呢?答案是不能,组合不能切片,而且C++的多态是建立在继承之上的。