【C++】继承

文章目录

  • 一、继承的定义
    • 1.1 继承的定义方式
    • 1.2 继承关系与访问限定符
  • 二、基类和派生类对象赋值转换
  • 三、继承中的作用域
  • 四、派生类的默认成员函数
  • 五、继承和友元
  • 六、继承和静态成员
  • 六、多继承导致的菱形继承
  • 七、继承总结


一、继承的定义

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保
持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象
程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继
承是类设计层次的复用

class person
{
public:
	string name;
	size_t year;
};
class student : public person
{
public:
	int id;
};

在这里插入图片描述


1.1 继承的定义方式

继承包含三个部分:

  1. 基类 (父类)
  2. 继承方式
  3. 派生类(子类)

在这里插入图片描述

1.2 继承关系与访问限定符

继承关系有3种:public继承,protected继承,private继承
访问限定符有3种:public,protected, private
组合共3*3 = 9种
在这里插入图片描述

举几个例子:

class person
{
public:
	string name;
protected:
	size_t year;
};
class A: public person
{ };
class B:protected person
{ };
int main(void)
{
	A a;
	a.name = "sfs";//正常
	a.year = 15;//报错
	//public继承下:name在A中是公有, year在A中是保护
	B b;
	b.name = "SFS";//报错
	b.year = 34;//报错
	//protected继承下:name和year在B中都是保护
	return 0;
}

那么如何进行记忆呢?

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的这里的不可见是指基类的私
    有成员还是被继承到了派生类对象中
    ,但是语法上限制派生类对象不管在类里面还是类外面
    都不能去访问它。
  2. 除了private成员, 基类的其他成员在子类的访问方式 == min(成员在基类的访问限定符,继承方式) 且满足 public > protected > private。

特殊情况:
默认继承方式,当我们没写继承方式时,class定义的类按照private继承,struct按照public继承
在这里插入图片描述


虽然这里的组合有9种,但绝大多数情况我们只使用其中的两种:
在这里插入图片描述


二、基类和派生类对象赋值转换

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。

class A
{
protected:
	int m = 4;
	int n = 3;
};
class a : public A
{ 
	void print()
	{
		cout << m;
	}
public:
	int x = 2;
};
int main(void)
{
	a a1;
	// 子类对象可以赋值给父类对象/指针/引用
	A A1 = a1;
	A& A2 = a1;
	A* A3 = &a1;
	return 0;
}

但这种赋值并不是任意的,它有以下条件:

  1. 必须是public的继承方式,如果是protected或者private的继承方式,它会出现以下错误
    在这里插入图片描述

  2. 基类的对象不能赋值给派生类的对象
    在这里插入图片描述

  3. 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
    的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。
    在这里插入图片描述


对于切片的理解:
对于引用权限有了解的读者,一定对上面的A& A1 = a1有疑惑。一般情况,当二者的类型不同进行时会进行强制类型转换:即a1会产生一个A类型的临时变量,而后再将临时变量给A1。由于临时变量具有常性,需要const的引用即const A& A1 = a1。但真实情况是不用加const。这说明了一个事实,就是A& A1 = a1并没有发生强制类型转换。它的真实情况如下:
在这里插入图片描述
就是直接将派生类的基类的那部分直接切给基类的对象/引用/指针,形象的称为切片,因此这个过程并没有强制类型转换

在这里插入图片描述


三、继承中的作用域

看下列代码:a1.id指的是那个id?

class A
{
public:
	int id;
};
class a : public A
{ 
public:
	int id;
};
int main(void)
{
	a a1;
	a1.id = 3;
	return 0;
}

答案是 a中定义的id

在继承体系中基类和派生类都有独立的作用域。., 若子类和父类中有同名成员子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义

也就是说一般情况下,我们访问a中的id,会优先选择a新定义的id。
那如果想访问A中的id,则需要使用使用 基类::基类成员 显示访问
a1.A::id = 3

特别:
成员函数的隐藏,只要函数名相同就会隐藏,不需要参数也相同。

class A
{
public:
	void func(int k = 4)
	{
		cout << "A";
	}
	int id;
};
class a : public A
{ 
public:
	void func()
	{
		cout << "a";
	}
	int id;
};
int main(void)
{
	a a1;
	a1.func(3);//报错
	return 0;
}

四、派生类的默认成员函数

默认成员函数有6个
在这里插入图片描述
当我们调用派生类的默认成员函数时,必须调用基类的默认成员函数。

  1. 编译器会自动调用的是构造函数(无参)和析构函数。且基类构造函数是在派生类构造函数之前调用,析构函数是在派生类的析构函数之后调用。
    在这里插入图片描述
    如果你要使用有参的构造函数,则需要手动调用,如下:
class person
{
  public:
  person(int m)
  {
      cout << "person()";
  }
  int a;
};
class student : public person
{
    public:
  student(int m)
    :person(m) //手动调用基类的构造函数
  {
      cout << "student()";
  }
  int b;
};
   

小知识:如果我们想手动调用基类的构造函数,需要写成这样person::~person,这里之所以要指定类域,原因是对于析构函数,编译器统一将名字改为destructor,而相同的名字便会被派生类的析构函数隐藏。

  1. 而像拷贝构造,赋值运算符重载则需要手动调用。

五、继承和友元

一句话:父亲的朋友不是我的朋友

六、继承和静态成员

基类定义了静态成员变量,则整个继承体系里只有一个这样的成员。
也就是说所有的基类对象和派生类对象共用一个。

class A
{
public:
	A()
	{
		cout++;
	}
	int id;
	static int cout;
};
int A::cout = 0;
class a : public A
{ 
public:
	int id;
};
int main(void)
{
	a a1[100];
	cout << A::cout; //cout等于100
	return 0;
}

六、多继承导致的菱形继承

一个派生类是只能有一个基类?
c++之父当年在思考这个问题时,认为一个派生类可以有多个基类。毕竟,在现实中,一个人身份是程序员,也是一个父亲。它当然具备程序员类的特征和父亲类的特征,这是多么符合逻辑呀。但代价是什么呢?

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。如下
在这里插入图片描述

class Person
{
public :
 string _name ; // 姓名
};
class Student : public Person
{
protected :
 int _num ; //学号
};
class Teacher : public Person
{
protected :
 int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
 string _majorCourse ; // 主修课程
};
void Test ()
{
 // 这样会有二义性无法明确知道访问的是哪一个
 Assistant a ;
a._name = "peter";//报错
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
 a.Student::_name = "xxx";
 a.Teacher::_name = "yyy";
 }

这就是问题,student和teather中都有继承自person的_name,因此在Assistant中会有两个_name.编译器不知道你的_name指的是哪个_name;这就导致很多问题。为了打补丁,c++在随后一个版本中添加了虚拟继承

虚拟继承:派生类 :virtual 继承方式 基类
class B : virtual public A

没使用虚拟继承的情况:

class A
{
public:
	int a = 1;
};
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 d1;
	return 0;
}

内存情况:
在这里插入图片描述
使用了虚拟继承

class A
{
public:
	int a = 1;
};
class B : virtual public A
{
public:
	int b = 2; //学号
};
class C : virtual public A
{
public:
	int c = 3; // 职工编号
};
class D : public B, public C
{
public:
	int d = 4;
};
int main()
{
	D d1;
	return 0;
}

内存情况:
在这里插入图片描述

问题:为什么指针不直接存储a的地址,而是指向一块空间,那块空间存储指针到a的偏移量?
答:如果A类中还有其他成员变量,即公有的除了a还有其他变量。那一个指针怎么存储多个变量的值呢?正是考虑到这一点,指针并没有直接存储a变量的地址,而是指向一块空间,存储a的偏移量。
在这里插入图片描述
这只是其中一个原因。


七、继承总结

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱
    形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设
    计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的编程语言都没有多继承,如Java。
  3. 继承和组合

public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
优先使用对象组合,而不是类继承 。

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
大的影响。派生类和基类间的依赖关系很强,耦合度高。

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。

实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。

// Car和BMW Car和Benz构成is-a的关系
   class Car{
   protected:
   string _colour = "白色"; // 颜色
   string _num = "陕ABIT00"; // 车牌号
   };
   
   class BMW : public Car{
   public:
   void Drive() {cout << "好开-操控" << endl;}
   };
   
   class Benz : public Car{
   public:
   void Drive() {cout << "好坐-舒适" << endl;}
   };
   
   // Tire和Car构成has-a的关系
   
   class Tire{
   protected:
       string _brand = "Michelin";  // 品牌
       size_t _size = 17;         // 尺寸
   
   };
   
   class Car{
   protected:
   string _colour = "白色"; // 颜色
   string _num = "陕ABIT00"; // 车牌号
    Tire _t; // 轮胎
   };
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值