文章目录
1、继承的概念
- 继承的作用就是为了解决代码重复的问题
- 继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有特性的基础上进行拓展,增加功能,这样产生新的类,称为派生类。
- 继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认识过程。以前我们接触的复用的函数复用(写一个通用的方法来避免每次重复写大段的代码,例如:打印函数),继承是类设计层次的复用。
class Person
{
public:
void SetPerson(const string& name, const string &sex, int age)
{
name_ = name;
sex_ = sex;
age_ = age;
}
private:
string name_;
string sex_;
int age_;
};
class Student : public Person
{
public:
void SetStudent(const string& name, const string &sex, int age, int stuID)
{
SetPerson(name, sex, age);
stuID = stuID_;
}
private:
int stuID_;
};
2、继承方式
2.1 继承权限
访问限定符:public,protected,private
继承方式:共有继承,保护继承,私有继承
访问限定符的作用:限制成员能够在类外进行直接访问,限制成员能否在子类的直接访问。
2.2 继承语法格式
// class 派生类:继承方式 基类
// 单继承
class student : public person
{};
// 多继承
class A : public B1, public B2
{};
2.3 不同继承方式,基类被不同访问限定符修饰的成员在子类中的权限
2.4 C++中class和struct的区别
class的默认继承方式是private,struct的默认继承方式是public
模板参数列表
template<class T, typename U>
3、父类和子类
前提:子类必须public继承
3.1 对象模型
子类和父类的关系:可以将子类对象看成是一个基类对象----子类和父类是 is a 的关系
3.2 子类和父类的赋值兼容规则
1)子类对象可以给父类对象赋值,反之则不行
2)父类的指针或引用可以指向子类对象,反之则不行
3)不能使用子类指针或者引用指向基类对象,但是可以强转(尽量不要使用,因为强转虽然可以编译,但是运行会报错)
4、父类和子类隶属于不同的作用域
4.1 同名隐藏
同名隐藏:父类和子类中具有相同名称成员时,如果使用子类对象访问该成员,优先访问到的是子类定义的成员,基类相同名称的成员将无法直接访问(被隐藏了)
同名隐藏条件:
- 成员函数:与函数的原型是否相同的没有关系----只要成员函数名字相同
- 成员变量:与成员变量的类型是否相同无关----只要成员变量的名字相同
如果子类要访问基类被隐藏的同名成员------子类对象.基类名称::同名成员
class Base
{
public:
void TestFunc()
{
cout << "Base::TestFunc()" << endl;
}
private:
int b_;
};
class Derived : public Base
{
public:
void TestFunc(int b)
{
cout << "Derived::TestFunc(int)" << endl;
}
private:
int d_;
char b_;
};
int main()
{
Derived d;
d.TestFunc(10);
// 子类对象访问同名隐藏的函数
d.Base::TestFunc();
return 0;
}
5、子类构造函数、析构函数的规则
5.1 子类构造、析构过程
- 构造过程:先调用基类的构造函数,在调用子类的构造函数
- 析构过程:先调用子类的析构函数,在调用基类的析构函数
- 注意:创建那个类的对象,编译器就会调用那个类的构造函数,析构那个类的对象,编译器就会调用那个类的析构函数
class Base
{
public:
Base(int b)
: b_(b)
{}
private:
int b_;
};
class Derived : public Base
{
public:
Derived(int b, int d)
: Base(b) // 注意
, d_(d)
{}
private:
int d_;
char b_;
};
5.2 子类构造函数的限制
1)如果父类没有显示定义构造函数,那么子类构造选择性添加构造函数
2)如果父类有无参和全缺省的构造函数,那么派子选择性添加构造函数
3)如果父类有参构造函数,则子类必须显示定义构造函数
6、子类的默认函数
包含构造,拷贝构造,赋值运算符重载,析构
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
派生类的牌operator=必须要调用基类的operator=完成基类的赋值
class Base
{
public:
Base(int b = 10)
: b_(b)
{
cout << "Base::Base(int)" << endl;
}
Base(const Base& b)
: b_(b.b_)
{}
Base& operator=(const Base& b)
{
if (this != &b)
{
b_ = b.b_;
}
return *this;
}
~Base()
{
cout << "Base::~Base()" << endl;
}
private:
int b_;
};
class Derived : public Base
{
public:
// 构造
Derived(int b = 10, int d = 20)
: Base(b)
, d_(d)
{
cout << "Derived::Derived(int, int)" << endl;
}
// 拷贝构造
Derived(const Derived& d)
: Base(d)
, d_(d.d_)
{}
// 赋值运算符重载
Derived& operator=(const Derived& d)
{
if (this != &d)
{
Base::operator=(d);
d_ = d.d_;
}
return *this;
}
// 析构
~Derived()
{
cout << "Derived::Derived()" << endl;
// call ~Base(); 最后一条语句会调用父类的析构函数
}
private:
int d_;
};
7、不同的继承体系
1)单继承
2)多继承
如果有多个基类,每个基类前都需要添加继承权限;如果没有添加,则为默认的继承方式。
3)菱形继承 (单继承+多继承 = 菱形继承)
8、菱形继承的问题
二义性问题 (因为在最顶层的B类中的成员在D中存在两份)
解决方式一:访问明确化(d.C1::b_ = 1)
解决方式二:只需做到让最顶层基类B中的成员子类D中只存储一份即可(引入:虚拟继承)
// 菱形继承二义性----借助虚拟继承
class B
{
public:
void Test()
{}
private:
int b_;
};
class C1 : virtual public B
{
private:
int c1_;
};
class C2 : virtual public B
{
private:
int c2_;
};
class D : public C1, public C2
{
private:
int d_;
};
9、虚拟继承
// 虚拟继承
class Base
{
private:
int b_;
};
class Derived : virtual public Base
{
private:
int d_;
};
普通的单继承不要设计成虚拟继承,虚拟继承唯一的作用就是为了解决菱形继承的二义性问题。
9.1 虚拟继承的特性
1) 虚拟继承对象模型会多4个字节,放虚基表的地址
2) 编译器给子类生成了构造函数,如果用户显示定义了构造函数,则编译器会对构造函数进行修改
目的:给对象前4个字节填充数据
3) 对象模型是倒立的
4) 子类对象给父类中成员变量赋值方式不同
从对象前四个字节中取出虚基表的地址,从虚基表地址往后偏移4个字节的位置取出:子类对象起始位置相对于基类部分的偏移量,给基类成员进行赋值----将基类对象地址往后偏移----取到的偏移量字节
10、继承和组合的优缺点
1)组合定义
在新类里面创建原有类的对象,重复利用已有类的功能。(has-a关系)
2)继承定义
可以使用现有类的功能,并且在无需重复编写原有类的情况下对原有类进行功能上的扩展。(is-a关系)
10.1继承的优缺点
继承的优点:
- 支持扩展,通过继承父类实现,但会使系统结构较复杂
- 易于修改被复用的代码
继承的缺点:
- 代码白盒复用,父类的实现细节暴露给子类,破坏了封装性
- 当父类的实现代码修改时,可能使得子类也不得不修改,增加维护难度。
10.2 组合的优缺点
组合的优点:
- 代码黑盒复用,被包括的对象内部实现细节对外不可见,封装性好。
- 整体类与局部类之间松耦合,相互独立。
- 每个类只专注于一项任务
- 支持动态扩展,可在运行时根据具体对象选择不同类型的组合对象(扩展性比继承好)
组合的缺点:
- 创建整体类对象时,需要创建所有局部类对象。导致系统对象很多。
10.3 什么情况下使用继承,什么情况下使用组合
尽量不要使用继承,因为过多的使用继承会破坏代码的可维护性,当父类被修改时,会影响到所有继承自它的子类,从而增加程序的维护难度和成本。