概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特
性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,
体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
定义
![](https://img-blog.csdnimg.cn/img_convert/4adbac1329f6a8e7da1375c239066cd2.png)
class Person
{
public:
Person(const char* name="mwb")
{}
void print()
{
cout << "name: " << _name << endl;
cout<<"age: " << _age << endl;
}
private:
string _name = "peter";
int _age = 18;
};
class Student : public Person
{
public:
Student(const char* name, int num)
:Person(name)
, _num(num)
{}
protected:
int _stuid = 0;
int _num = 111;
};
class Teacher : public Person
{
protected:
int _jobid = 0;
int _num = 999;
};
基类和派生类进行赋值转换
赋值兼容规则:
赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员,而且所有成员的访问控制属性也和基类完全相同。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。赋值兼容规则中所指的替代包括以下的情况:
派生类的对象可以赋值给基类对象。
派生类的对象可以初始化基类的引用。
派生类对象的地址可以赋给指向基类的指针。
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
何为切片
注意:子类赋值给父类的时候会发生切片。
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
若执行以下代码,就会发生如图所示的切片:
Student S;
Person& rp=S;
如图所示:
![](https://img-blog.csdnimg.cn/img_convert/bd4c4a4f41933c0f39255c3d925fe5ac.png)
解析:顾名思义就是将子类对象中的成员直接给给父类对象,虽然类型不同但是也没有产生临时变量。
与非切片类型转换的区别:
int a=1;
double b=2.2;
double& ra=a;
与之相似的以上代码就是错误的,由于ra的类型与a不同,当类型转换的时候,a需要生成一个临时变量,然而临时变量是常量,引用不能引用一个常量,所以应该在double前面加一个const修饰一下。
即为:
int a=1;
double b=2.2;
const double& ra=a;
作用域
隐藏
概念:
1、子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定
义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
2、 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
成员变量的隐藏
代码如下:
class Person
{
public:
int _num = 1;
};
class Student : public Person
{
public:
int _num = 2;
};
int main()
{
Student s;
cout << s._num << endl;
return 0;
}
由于构成隐藏,所以s对象会屏蔽基类对同名成员的直接访问。
![](https://img-blog.csdnimg.cn/img_convert/50f9b206f9348a1a4c3bc8361294c5af.png)
如果想访问父类中的成员变量可以在成员变量前加一个作用域限定一下。
class Person
{
public:
int _num = 1;
};
class Student : public Person
{
public:
int _num = 2;
};
int main()
{
Student s;
cout << s.Person::_num << endl;
return 0;
}
![](https://img-blog.csdnimg.cn/img_convert/666d8fc0fc83629f93acaee2e6d26e01.png)
成员函数的隐藏
class Person
{
public:
void fun(int _num = 999)
{
cout << _num << endl;
}
int _num = 1;
};
class Student : public Person
{
public:
void fun(const char* name = "mwb", int _num = 666)
{
cout <<name<< _num << endl;
}
string name;
int _num = 2;
};
int main()
{
Student s;
s.fun();
return 0;
}
由于构成隐藏,所以s对象会屏蔽基类对同名成员的直接访问。
![](https://img-blog.csdnimg.cn/img_convert/2ef0a1f3eaa7148e2914bc813ebb987a.png)
如果想访问父类中的成员变量可以在成员函数前加一个作用域限定一下。
class Person
{
public:
void fun(int _num = 999)
{
cout << _num << endl;
}
int _num = 1;
};
class Student : public Person
{
public:
void fun(const char* name = "mwb", int _num = 666)
{
cout <<name<< _num << endl;
}
string name;
int _num = 2;
};
int main()
{
Student s;
s.Person::fun();
return 0;
}
![](https://img-blog.csdnimg.cn/img_convert/b56417b80f2d5c762291021376631882.png)
派生类的默认成员函数
重中之重:
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函
数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类
对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
构造、拷贝构造、赋值重载
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;
};
//派生类中
//1、构造函数,父类成员调用父类的构造函数完成初始化
class Student : public Person
{
public:
//构造
Student(const char* name="mawenbo", int num=3)
:Person(name)
, _num(num)
{}
Student(const Student& s)
:Person(s)//必须调用基类的拷贝构造进行初始化
,_num(s._num)
{}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);//这里如果不调用基类的将一直循环
//operator=(s);
_num = s._num;
}
return *this;
}
protected:
int _num;
};
int main()
{
return 0;
}
析构
class Person
{
public:
Person()
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
Student()
{
cout << "Student()" << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Student s;
return 0;
}
![](https://img-blog.csdnimg.cn/img_convert/7e79bed445b9bc5bc4f56c546065ae42.png)
由运行结果我们可以得出以下几点:
派生类初始化先调用基类构造函数再调用派生类构造函数。
清理的时候先调用派生类的析构再调用基类的析构。
调用派生类析构函数的后会自动去调用基类的析构函数,这也为了满足派生类的析构函数一定先于基类的析构函数调用。
继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,好比你爸爸的朋友不一定就是你的朋友。
继承与静态成员
class Person
{
public:
Person()
{
_count++;
}
void Print()
{
cout << _count << endl;
}
public:
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
public:
Student()
{
Person::_count++;
}
void Print()
{
cout << Person::_count++ << endl;
}
};
int main()
{
Person p;
p.Print();
Student s;
s.Print();
cout << Person::_count << endl;
Person::_count = 3;
cout << Person::_count << endl;
Person::_count = 99;
cout << Person::_count << endl;
}
![](https://img-blog.csdnimg.cn/img_convert/507543512db2b6c86034a01bbf4d6b70.png)
解析:静态变量不属于任何对象,属于全局。当无论在哪里改变出了作用域都会改变。
菱形继承
概述
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;
};
![](https://img-blog.csdnimg.cn/img_convert/34e03eae3a8c9e4c8ca2ce000d7c823b.png)
顾名思义,继承的关系像一个菱形一样,如上图所示。
菱形继承的二义性与改进方法(菱形虚拟继承)
二义性
先看代码:
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;
};
int main()
{
D d;
d._a;
return 0;
}
![](https://img-blog.csdnimg.cn/img_convert/7654d7701410d221184c9d2e84351f11.png)
由于B和C里面都有A继承下来的_a,此时我们使用d去访问_a时就会产生二义性,编译器会不知道到底应该访问哪个_a。
菱形虚拟继承
代码如下:
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;
};
我们只需要在继承方式的前面,也就是菱形的腰部位置加两个virtual,就构成了菱形虚拟继承。
此时我们再用d去访问_a就不会报错了,因为此时菱形虚拟继承已经吧B和C中的_a当成一个变量来处理了。
原理
代码如下:
虚拟继承之前
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;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
内存情况:
![](https://img-blog.csdnimg.cn/img_convert/96f2f3e259892727bee4efad6c524a6b.png)
我们去查看内存的时候发现B和C中的_a是两个值,因而导致二义性。
虚拟继承后
代码如下:
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;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
内存情况:
![](https://img-blog.csdnimg.cn/img_convert/8520ad06eb7c8b040fab92e15086d482.png)
我们发现此时B和C中的_a已经成为一个值了,那么在内存中是怎样实现的呢,并且我们发现在内存中_a被放在了最下边独立于其他成员变量。
我们发现在_b和_c的存储位置上方有一个虚基表指针,我们在内存中去使用他们的指针所指向的地址可以找到他们对应的值,叫做偏移量,当我们用虚基表指针自身的地址与偏移量相加可获得_a的地址,因此内存就是以这种方式来寻找_a存放的位置的。
注意:
很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有
菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在
复杂度及性能上都有问题。多继承可以认为是C++的缺陷之一,很多后来的OO语言(Object Oriented,OO,也就是面向对象的语言)都没有多继承,如Java。
组合
//继承 is-a关系
class Person
{
protected:
string _name;
};
class Student : public Person
{
protected:
int _id;
};
//组合 has-a关系
class Hair
{
protected:
string _colour;
};
class Animal
{
protected:
int _age;
Hair h;
};
组合的耦合度低,继承的耦合度高,能使用组合就不使用继承,因为继承一定程度的破坏了代码的封装,派生类和基类的依赖关系很强。