继承
类成员继承的方式有3种:puclic(公有)继承,protected(保护)继承,private(私有)继承;默认继承方式为private(私有)继承;在实际运用中,基本都是使用公有继承;
1.继承基类成员访问方式的变化:
基类private成员在派生类无论以什么方式继承都是不可见不可访问的。
注意:不可见,但是基类的私有成员还是被继承到了派生类对象中;
如果基类成员不想在类外被直接访问,但是需要在派生类中访问,就定义为protected;
基类的私有成员在子类都是不可见,基类的其他成员在子类的访问方式== Min(成员在基类的访问限定符,继承方式);
几种继承方式代码实现如下:
#include<iostream>
using namespace std;
class A
{
public:
void fun()
{
cout << "A::fun()" << endl;
}
protected:
int a;
private:
int _a;
};
class B : public A //protected A //private A// A
//其中A为基类,B为派生类,public为继承方式
{
public:
void fun() {
cout << "B::fun()" << endl;
}
protected:
int b;
private:
int _b;
};
2.继承中基类与派生类的赋值规则
public 继承,父类和子类是is-a关系,有基类(父类)Person,派生类(子类)是Student,我们可以说 Student is a Person;
观察以下代码:
double d=3.14;
int i&=d;错误代码
//这句代码是错误的,错误的原因不是i 和d 的类型不同,而是因为d 要转换成 i,中间需要产生临时变量再
//赋值给i,而因为临时变量具有常性,所已正确代码应该如下:
double d=3.14;
const int& i=d;
//string &str="sss";//错误代码
const string &str="sss";//正确代码
上述代码存在的问题在基类与派生类的赋值则不存在;
子类对象赋值给父类对象/ 父类指针/ 父类的引用 是很自然的,中间不产生临时对象,这是因为子类继承而来的成员变量父类也必定有,因此子类对象可以直接给父类对象赋值,但是,注意,父类对象不能给子类对象赋值;
#include<iostream>
#include<string>
using namespace std;
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
void Test()
{
Student sobj;
//子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
//2.基类对象不能赋值给派生类对象
sobj = pobj;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj;
Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
ps1->_No = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
ps2->_No = 10;//越界
}
![](https://i-blog.csdnimg.cn/blog_migrate/91adf88804698b48bee1ba838d7fb32d.png)
3.隐藏
基类与派生类的成员变量完全相同构成隐藏,成员函数名称相同,在不构成多态的情况下,即构成隐藏,与返回值与函数参数无关;参考以下代码:
A与B中的fun函数在不同的作用域中,构成隐藏,注意不是构成重载,成员变量a也构成隐藏;
在B在调用被隐藏的A的fun函数,需加域名限定符A::
#include<iostream>
using namespace std;
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
protected:
int a;
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" << i << endl;
}
protected:
int a;
};
//A与B中的fun函数在不同的作用域中,构成隐藏,注意不是构成重载,成员变量a也构成隐藏
4.派生类的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3.派生类的operator=必须要调用基类的operator=完成基类的复制。
4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5.派生类对象初始化先调用基类构造再调派生类构造。
6.派生类对象析构清理先调用派生类析构再调基类的析构
7.因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。参考代码如下:
#include<iostream>
#include<string>
using namespace std;
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)
: Person(name)
, _num(num)
{
cout << "Student()" << endl;
}
/*Student(const char* name, int num)
{
Person(name);//错误代码,必须在初始化列表显示调用构造函数
_num = num;
cout << "Student()" << endl;
}*/
Student(const Student& s)
: Person(s)
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator = (const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s)
{
Person::operator =(s);
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _num; //学号
};
void Test1()
{
Student s1("jack", 18);
Student s2(s1);
Student s3("rose", 17);
s1 = s3;
}
运行结果:
5.继承与友元
友元关系不能继承,即父的朋友无法继承给子 ,也就是说基类的友元不能访问子类的私有和保护成员;
6.继承与静态成员
基类定义了static静态成员,(一般在类内声明,类外初始化),则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。
7.菱形继承与菱形虚拟继承
菱形继承:从下图中我们可以看出,假设A有数据int a,由于B与C分别继承了A,则他们都拥有了数据a,而D同时继承了B与C中的a,D中就有两份a,这就会导致数据的冗余和二义性问题;
参考如下代码:
#include<iostream>
using namespace std;
class A
{
public:
int a;
};
class B:public A
{
public:
int b;
};
class C:public A
{
public:
int c;
};
class D:public B,public C
{
public:
int d;
};
void text()
{
D d;
d.a=1;//代码错误,无法确定给那个a赋值,编译无法通过,需要显示指定访问那个父类中的a;
d.B::a=1;
d.C::a=1;
}
如何解决菱形继承产生的数据的冗余和二义性问题? 在B和C继承A时采用虚拟继承即可解决问题
代码实现如下:
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
int a;
};
class B :virtual public A
{
public:
int b;
};
class C :virtual public A
{
public:
int c;
};
class D :public B, public C
{
public:
int _d;
};
void text()
{
D d;
d.B::a = 1;
d.C::a = 2;
d.b = 4;
d.c = 5;
d._d = 6;
d.a = 9;
}
int main()
{
text();
}
从内存地址我们可以看到,ABCD各自的成员被分别存储,其中,B、C前四个字节存储了一个虚基表指针,即虚基表的地址,虚基表存储了偏移量,通过偏移量可以找到下面的A;
B的虚基表地址为0x0038eb48,虚基表存储的偏移量为16进制的14;
8.继承与组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象;
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
通过以下代码理解组合与继承的区别:
#include<iostream>
#include<string>
using namespace std;
// Car和BMW Car和BYD构成is-a的关系 ;BYD is car
class Car {
protected:
string _colour = "黑色"; // 颜色
string _num = "粤AB6666"; // 车牌号
};
class BMW : public Car {
public:
void Drive() { cout << "操作性强" << endl; }
};
class BYD : public Car {
public:
void Drive() { cout << "舒适性强" << endl; }
};
// Tire和Car构成has-a的关系,car has tire
class Tire {
protected:
string _brand = "MiQilin";//品牌
size_t _size = 17; //尺寸
};
class Car {
protected:
string _colour = "黑色"; // 颜色
string _num = "粤AB6666"; // 车牌号
Tire _t; // 轮胎
};
9.常见题目
1). 什么是菱形继承?菱形继承的问题是什么?
菱形继承是指在一个继承体系中,某个类同时继承自两个不同的类,这两个类又共同继承自一个共同父类,从而形成一个菱形的继承结构。菱形继承的主要问题是会导致数据冗余和二义性,即某些数据会被重复继承,同时在多重继承时会产生二义性。
2). 什么是菱形虚拟继承?如何解决数据冗余和二义性的
菱形虚拟继承是一种解决菱形继承问题的技术,可以消除数据冗余和二义性。在菱形继承中,使用虚拟继承的方式,使得共同父类只在派生类的最顶层继承一次,而不是在每个子类中都继承一次,从而避免了数据冗余和二义性。
3). 继承和组合的区别?什么时候用继承?什么时候用组合?
继承是指一个类可以继承另一个类的属性和方法,从而可以扩展和重写父类功能;
组合是指一个类可以包含另一个类的实例作为自己的一部分,从而可以利用另一个类的功能;
一般来说,当两个类之间存在"is-a"的关系时,可以使用继承来表达它们之间的关系;当两个类之间存在"has-a"的关系时,可以使用组合来实现它们之间的关系。
过度使用继承可能会导致耦合度过高,修改一个类的实现可能会影响到其他的类,同时,继承也破坏了类的封装性,优先使用对象组合有助于保持每个类被封装,并被集中在单个任务上,但是过度使用组合可能会导致代码复杂度增加,需要考虑如何管理和维护组合关系。