- 继承的定义
- 基类和派生类对象赋值兼容转换规则
- 继承中的作用域
- 派生类的默认成员函数
- 继承与友元,静态成员
- 多继承与菱形继承
- 继承的总结
一.继承的定义
1.概念
继承是oo语言的三大特性之一。通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。我们称已存在的用来派生新类的类为基类,又称为父类。由已存在的类派生出的新类称为派生类,又称为子类。
2.格式
class 派生类名 : 继承方式 基类名 {类体}
3.继承方式和访问限定符
继承方式有三类:
- public,公有继承
- protected,保护继承
- private,私有继承
访问限定符有三种:
- public,公共访问限定符
- protected,保护访问限定符
- private,私有访问限定符
三种继承方式下,访问限定符的变化如下:
public(公有继承) | protected(保护继承) | private(私有继承) | |
---|---|---|---|
基类public | 派生类public | 派生类protected | 派生类private |
基类protected | 派生类protected | 派生类protected | 派生类private |
基类private | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
- 基类中访问权限为private的成员,不管是何种继承方式,都在派生类中不可见,不可见就是该成员被继承到派生类中,但是派生类中的成员不可访问它,想要在派生类中访问它只能通过基类的接口来访问之。
- 对于public,protected的访问限定符,不管是什么继承方式,在派生类中的访问权限总是以两者中权限小的那个为主。
- 由于基类private在派生类中不可见,所以一般我们在基类中都是定义public和protected权限的成员,这也是protected权限的由来
- protected:该权限下,类内可以访问,类外不可访问
- private:该权限下,类内可以访问,类外不可访问
- private和protected的区别主要是在继承下的不同,对于类和对象都是相同的 - class关键字默认的继承方式为private,即:
class 派生类名: 基类名{}
这里没有显示指定继承方式,所以按照默认继承方式来继承,struct默认的继承方式为public,即:struct 派生类名: 基类名{}
- 一般我们都是在基类定义public和protected权限的成员,继承方式主要public继承,因为其他继承方式不利于维护。
4.实例
class person
{
public:
void func()
{}
protected:
int _name;
int _age;
};
class student: public person
{
private:
int _id;
int _grade;
};
二.基类和派生类对象赋值兼容转换规则
int main()
{
double d = 1.2d;
int a = d;
return 0;
}
在上述int a = d
的过程中,由于a和d的类型不一致,中间发生了隐式类型转换,生成了临时变量。
基类和派生类之间的赋值会不会生成临时变量呢?
class person
{
public:
void func()
{}
protected:
int _name;
int _age;
};
class student: public person
{
private:
int _id;
int _grade;
};
int main()
{
student s1;
person p1 = s1;
return 0;
}
- 上述代码不会生成临时变量,由于语法规定,派生类可以直接赋值给基类的对象,指针,引用。这就是基类和派生类之间的赋值兼容转换规则。形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类的对象不能赋值给派生类对象
- 基类的指针/引用可以通过强制类型转换赋值给派生类的指针/引用,但必须基类的指针是指向派生类对象时,这种赋值才是安全的
- 基类和派生类的赋值兼容转换规则只有在public继承下才有意义
class person
{
public:
void func()
{}
protected:
int _name;
int _age;
};
class student: public person
{
private:
int _id;
int _grade;
};
int main()
{
student s1;
person p1 = s1;
person* p2 = &s1;
person& p3 = s1;
return 0;
}
三.继承中的作用域
- 在继承体系中,基类和派生类占用不同的作用域
- 当基类和派生类中有同名成员时,基类中的同名成员将被屏蔽,这种情况叫隐藏,也叫重定义,但是可以使用
::
作用域限定符来访问基类中的同名成员 - 当出现成员函数的隐藏,只需要函数名相同就行
- 派生类和基类中的同名成员函数不构成函数重载,因为函数重载必须在相同作用域下。
:::tips
由于基类和派生类占用不同的作用域,如果在派生类中访问一个成员,那么优先在当前作用域内找寻,如果在当前作用域中找到,就不会去基类的作用域找,此时就隐藏了基类中的同名成员,但是如果使用作用域限定符来指定作用域找寻,那么就可以访问基类中的同名成员了
:::
注意:在继承体系中,最好不要定义同名成员。
class person
{
public:
void func()
{
cout << _id << endl;
}
protected:
int _name;
int _age;
int _id = 11;
};
class student: public person
{
pulic:
void func()
{
//访问派生类中_id
cout << _id << endl;
//访问基类中_id
cout << person::_id << endl;
}
private:
int _id = 22;
int _grade;
};
int main()
{
student s1;
//调用派生类中func
s1.func();
//调用基类中func
s1.person::func();
return 0;
}
四.派生类的默认成员函数
类有六大默认成员函数,如果不显示给出,编译器就会自动生成。派生类也是类,如果我们不写,编译器会自动给出它的默认成员函数
- 派生类对象初始化:先调用基类构造,再调用派生类构造
- 派生类对象析构:先调用派生类析构,再调用基类析构
- 如果派生类析构函数和基类析构函数没有加virtual的话,派生类的析构和基类的析构会构成隐藏,因为编译器将派生类和基类的析构函数名变为destructor(由于多态)
1.默认构造函数
派生类的构造函数必须调用基类的构造函数来完成对基类成员的初始化,如果基类没有默认构造函数,则必须在派生类的初始化列表显示调用基类的构造函数。
class person
{
public:
person()
{
cout << "person" << endl;
}
protected:
string _name;
int _age;
};
class student :public person
{
public:
student(int id)
:_id(id)
{}
private:
int _id;
};
int main()
{
student s1; //这里会调用person的默认构造函数,
return 0;
}
2.拷贝构造函数
派生类的拷贝构造函数必须调用基类的拷贝构造函数进行初始化基类成员。
class person
{
public:
person()
{
cout << "person" << endl;
}
protected:
string _name;
int _age;
};
class student :public person
{
public:
student(){}
student(const student& s)
:person(s), //这里由于赋值兼容转换规则,所以可以直接给出
_id(s._id)
{}
private:
int _id;
};
int main()
{
student s1;
student s2(s1);
return 0;
}
3.赋值重载函数
派生类的赋值重载函数必须调用基类的赋值重载函数
class person
{
public:
person()
{
cout << "person" << endl;
}
protected:
string _name;
int _age;
};
class student :public person
{
public:
student(){}
student& operator=(student& s)
{
if (this != &s)
{
//这里必须加作用域限定符,否则会因为构成隐藏造成死循环
person::operator=(s);
_id = s._id;
}
}
private:
int _id;
};
4.析构函数
派生类的析构函数在被调用后会自动调用基类的析构函数,因为这样才能保证派生类对象先清理,然后再清理基类对象成员。
class person
{
public:
person()
{
cout << "person" << endl;
}
protected:
string _name;
int _age;
};
class student :public person
{
public:
student(){}
~stuent()
{}
private:
int _id;
};
五.继承与友元,静态成员
1.继承与友元
- 友元关系并不会继承下去,基类友元函数不能访问派生类的私有和保护成员
2.继承与静态成员
- 基类定义了静态成员,那么由其派生出来的所有派生类都和基类共用一个静态成员
六.多继承与菱形继承
1.多继承
- 多继承就是一个类是多个类的派生类
2.多继承的格式
class C:public B, public C{}
- 继承顺序决定了构造函数和析构函数的调用顺序,先被继承者先调用,后析构
3.菱形继承
- 菱形继承是多继承的一种特殊形式
- 菱形继承会造成数据的冗余和数据访问的二义性。因为A的成员被D包含了两次
- 数据访问二义性可以通过作用域限定符来消除
class A
{
public:
int _a;
};
class B:public A
{
public:
int _b = 2;
};
class C :public A
{
public:
int _c = 3;
};
class D:public B, public C
{
public:
int _d = 4;
};
int main()
{
D data;
// data._a = 1; 错误代码,指向不明确
data.B::_a = 1;
data.C::_a = 2;
return 0;
}
- 数据冗余和二义性可以通过虚继承来消除,虚拟继承不能在其他地方使用
class A
{
public:
int _a;
};
class B: virtual public A //virtual只能在这里
{
public:
int _b = 2;
};
class C : virtual public A //virtual只能在这里
{
public:
int _c = 3;
};
class D:public B, public C
{
public:
int _d = 4;
};
int main()
{
D data;
// data._a = 1; 错误代码,指向不明确
data.B::_a = 1;
data.C::_a = 2;
return 0;
}
4.虚拟继承的原理
下图是菱形继承导致的数据冗余问题。可以看到b中有一个a,c中有一个a,造成了数据冗余。
下图是虚继承之后,data的内存布局图,
- 可以明显看出,虚继承之后a只有一份了,b和c共用这个a,成功消除了数据的冗余性和二义性,那么b和c如何找到a呢?b和c里面还有两个值,这两个值是什么呢?这两个值是地址,指向的是一张表,表里存放的是b到a,c到a的偏移地址,通过偏移地址,b和c就可以找到a了。我们称这两个指针叫做虚基表指针,这两张表叫做虚基表
- 这里虚基表里面有两个int,第一个是为了多态的使用,第二个是存放偏移地址
- 虚继承后,bc的对象模型也会变为和d一样的
七.继承的总结
- c++语法复杂,原因之一就是有多继承,有了多继承就有了菱形继承,也就有了虚拟继承,这会导致底层实现很复杂
- 多继承可以认为是c++的缺陷之一,后续的oo语言,很多都没有多继承
1.继承与组合
什么是组合呢?
- 一个类中有另一个类的对象,就是组合。比如我们学过的反向迭代器,容器适配器…都是组合
class A
{};
class B
{
private:
A _a;
};
- public继承是一种is-a的关系,每个派生类对象都是基类对象。比如student-person,每个学生是一个人
- 组合是一种has-a的关系,是一种包含关系。比如汽车-轮胎给关系
- 具体选择哪种方法,可以判断是is-a的关系,还是has-a的关系,如果两者都是,则优先使用组合
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
- 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,也可以用组合,那么用组合较好。