目录
一、继承的概念及定义
1、概念
继承(inheritance)机制是面向对象程序设计中可以使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。通过继承产生的新类就叫做派生类(或者子类),被继承的类叫做基类(或者父类)。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。注意:继承是类设计层次的复用!
父类示例如下:
//定义一个名为Father的父类
class Father{
public:
string name = "Alex";
int age = 18;
};
定义一个以public方式继承父类的子类Children:
class Children:public Father{
public:
string schoolname = "XXschool";//在子类中添加的一个信息
void print()
{
cout<<name<<endl<<age<<endl<<schoolname<<endl;
}
};
int main()
{
Children Cd;
Cd.print();
return 0;
}
2、定义
(1)定义的格式
由上述示例也可以看出,在继承时,格式应该是:
class 子类的名字:继承方式 父类的名字{};
(2)继承方式和访问限定符
继承中有三种继承方式,分别是public继承、protected继承、private继承,相应地就有三个访问限定符:public访问、protected访问、private访问。
(3)继承后子类成员访问权限
这是非常重要的一个表
类成员\继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
- 基类中的private成员在派生类中无论以那种方式继承都是不可见的,不可见不是指不能被继承,是指基类的private成员依然被继承到了派生类中,只是无法被访问,语法上限制派生类对象不管是在类里面还是在类外面都不能对其进行访问;
- 如果基类成员不想在类外被直接访问,而需要再派生类中被访问,就将该成员定义为protected, protected限定符就是因为继承才出现的;
- 由上述表格可以发现,基类的private成员在派生类中始终不可见,而其他成员则根据一个不等式public>protected>private遵循取小的原则;
- 使用关键字class构建类时,默认继承方式为private,使用struct时默认为public,但最好是显示地写出继承方式;
- 在实际使用中一般都是public继承,很少使用protected和private继承,因为protected和private继承下来的成员只能在派生类中使用,在实际中扩展维护性不强。
二、继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域;
- 在实际应用中最好不要在继承体系里定义同名成员。
三、基类和派生类(子类和父类)
1、基类和派生类的相互赋值
派生类对象可以赋值给基类的对象、基类的指针、基类的引用。
基类的对象不能赋值给派生类的对象;基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但必须当基类的指针是指向派生类对象时才是安全的,此处如果基类是多态类型可以使用RTTI(RunTime Type Information)的dynamic_cast来进行识别后进行安全转换。
//定义一个名为Father的父类
class Father{
public:
string name = "Alex";
int age = 18;
};
class Children:public Father{
public:
string schoolname = "XXschool";//在子类中添加的一个信息
void print()
{
cout<<name<<endl<<age<<endl<<schoolname<<endl;
}
};
int main()
{
Children Cd;
Father Fh;
Fh = Cd;//将子类的对象赋值给父类
Fh *p = Cd;//子类的对象赋值给父类的指针
Fh &pp = Cd;//将子类的对象赋值给父类的引用
return 0;
}
2、同名的成员变量
基类和派生类中有同名成员,派生类中成员将屏蔽基类中同名成员的直接访问,这种情况叫做隐藏,也叫做重定义(覆盖),若要在派生类成员函数中对同名成员进行访问,可以使用基类::基类成员 进行显示访问。
class father{
public:
string name = "father";
};
class son:public father{
public:
string name = "son";
void print(){
cout<<name<<endl;
}
};
int main(){
son s;
s.print();
return 0;
}
代码输出结果应该是son而不是father,若要访问父类中的name变量,则需要如下进行:
cout<<father::name<<endl;
在上述理解中,要将重写和重载进行区分,重写是针对不同层次,而重载是针对同一层次。
3、同名成员函数
需要注意的是,如果是成员函数的隐藏,只要是函数名相同就满足了隐藏条件;
四、派生类的默认成员函数
1、构造函数
作用:完成主要的初始化工作;
调用顺序:程序运行时会先调用父类的构造函数,再调用子类的构造函数。
2、析构函数
作用:完成主要的清理工作,释放资源,避免内存泄漏
调用顺序:程序结束时先调用子类的析构函数,再调用父类的析构函数。
注意:不要在子类中调用父类的析构函数!避免造成野指针的问题。
3、拷贝构造
子类中调用父类的拷贝构造时,直接传入子类对象即可。
class human {
public:
human(string name="小明")
:_name(name)
{
cout << name << endl;
}
protected:
string _name;
};
class student:public human {
public:
student(string name, int age)
:_age(age)
{
cout << name << endl << age << endl;
}
student(student& s)//拷贝构造
:human(s)//直接将st传过来通过切片拿到父类中的值
,_age(s._age)//拿除了父类之外的值
{
cout << s._age << endl<<s._name<<endl;
}
protected:
int _age;
};
int main()
{
student st("小红",18);
student st2(st);
return 0;
}
代码输出结果应该是:
小明
小红
18
18
小明
4、赋值运算符重载
子类的operator =必须要显示调用父类的operator=完成父类的赋值。
class human{
public:
human(string name = "小明")
:_name(name)
{
}
human& operator = (const human& p)
{
if (this != &p)
{
cout<<"调用父类"<<endl;
_name = p._name;
}
return *this;
}
protected:
string _name;
};
class student:public human{
public:
student(string name, int age)
:_age(age)
{
}
stduent(student& s)
:human(s)
,_age(s._age)
{
}
student& operator = (const student& s)
{
if (this ! = &s)
{
cout<<"调用子类"<<endl;
human::operator=(s);//必须显示调用父类运算符
_age = s._age;
_name = s._name;
}
return *this;
}
protected:
int _age;
};
int main()
{
student st("小红", 18);
student st2(st);
student st3("小刚", 16);
st = st3;
return 0;
}
五、继承与友元
在类中用关键字friend来进行声明得到友元函数或者友元类,可以访问类中private成员,不受控制权限的限制,注意:友元关系不能被继承。
六、继承与静态成员
七、单继承
一个子类只有一个直接父类的继承关系。
class A←class B: public A←class C: public B
八、多继承
一个子类有两个或两个以上直接父类的继承关系。
class A class B
class C: public A, public B
九、复杂的菱形继承及菱形虚拟继承
简单示例菱形继承:
class A
↓
class B: public A class C: public A
↓
class D:public B, public C
1、菱形继承内数据冗余和二义性问题
class A{
public:
string name;
};
class B:public A{
public:
int age;
};
class C:public A{
public:
sting sex;
};
class D:public B, public C{
public:
int id;
};
int mian(){
D st;
st.name = "Alex";
st.age = 18;
st.sex = "female";
st.id = 1;
return 0;
}
注意,这段代码是有问题的,会报错D::name不明确,因为这里的B和C都继承了A中的name,D不知道继承的是B中的name还是C中的name,这就是菱形继承的二义性问题。
2、解决数据冗余和二义性问题
下面有两种方法来解决上述问题:
1. 加修饰限定
在main函数中做出如下修改:
st.B::name = "Alex";//或者st.C::name = "Alex";
指定name的继承来源就能使代码顺利运行
2. 虚拟继承
在继承方式前面加上virtual。
class B:virtual public A{
public:
int age;
};
class C: virtual public A{
public:
string sex;
};
一般不建议设计出多继承,一定不要设计出菱形继承。
十、组合
一个类包含另一个类对象就称为组合。
示例:
class A{
//...
};
class B{
//...
protected:
A _a;
};
继承是一种is-a的关系,也就是说每个子类对象都是一个基类对象;
组合则是一种has-a的关系,每个B对象中都有一个A对象。
优先使用组合而不是类继承,在必要时候才选择使用继承。
继承允许用户根据基类的实现来定义派生类的视线,这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言的:在继承中,基类的内部细节对派生类可见,继承一定程度上破坏了基类的封装,基类的改变对派生类有很大的影响,派生类和基类间的依赖关系很强,耦合度高。
对象组合式另外一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节不可见。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。