C 学习笔记之 继承

本文介绍了C++中的继承概念,包括派生类、基类和继承方式。详细讨论了基类和派生类对象赋值转换、继承中的作用域、派生类的默认成员函数、继承与友元、静态成员的关系,以及复杂的菱形继承问题。重点解释了虚拟继承如何解决菱形继承的数据冗余和二义性问题,强调了虚继承在多继承中的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.继承的概念及定义

1.1继承的概念 

继承:允许程序员在保持原有类特性的基础上进行扩展,产生的新的类称为派生类,继承是类设计层次的复用,呈现了面向对象设计的层次结构。 

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
class person
{
public:
  void print()
  {
    cout<<"name:"<<" "<<endl;
    cout<<"age:"<<" "<<endl;
  }
protected:
  string name="peter";
  int age=20;
};
class student:public person
{
protected:
  int stunum;
};
class teacher:public person
{
protected:
  int jobid;
};
int main()
{
  teacher t1;
  student s2;
  t1.print();
  s2.print();
  return 0;
}

子类成员继承了父类的成员函数,继承后父类的成员都会变成子类的一部分。

1.2继承定义

 定义格式:class test : public person 。test:派生类    public:继承方式      person:基类   (基类就相当于父亲类,派生类相当于子类)

 继承方式-------public继承/protected继承/private继承  三种继承方式

访问限定符--------public访问/protected访问/private访问

private:0 不可访问  不可继承

protected:1 不可访问 可继承

public:2 可访问 可继承

派生类中继承来的元素是什么权限,取决于继承的方式和基类成员权限的较小值。

基类的private成员在派生类中不可访问。

基类的private成员在派生类中无论以什么方式继承都是不可见的,这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

2.基类和派生类对象赋值转换

  • 派生类对象可以赋值给基类的对象/基类的指针/基类的引用
  • 基类对象不能赋值给派生类对象
  • 基类的指针可以通过强制类型转换赋值给派生类的指针,但前提是基类的指针必须是指向派生类对象时才是安全的。

原因:若把父类指针强转为子类指针,有一篇空间不属于它 ,会发生越界访问。建议最好不要强制转换,但如果基态是多态类型可以使用dynamic cast等来进行转换。

#include<iostream>
#include<cstdio>
#include<string>
#include<algorithm>
using namespace std;
class person
{
public:
  string name;
  string sex;
  int age;
};
class student:public person
{
public:
  int no;
};
int main()
{
  student sobj;
  person pobj=sobj;//派生类的值赋值给基类的对象
  person* pp=&sobj;//派生类的值赋值给基类的指针
  person& rr=sobj;//派生类的值赋值给基类的引用
  //sobj=pobj;基类不能赋值给派生类
  pp=&sobj;
  //基类的指针通过强制类型转换赋值给派生类指针,
  //但是有时候会发生强制类型转换
  student* ps1=(student*)pp;
  ps1->no=10;
  return 0;
}

 3.继承中的作用域

  1. 继承体系中,基类和派生类都有独立的作用。
  2. 在基类和派生类中若有同名成员,派生类成员也就是子类成员将屏蔽掉基类(父类)对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
  3. 子类指针可以转化为父类指针,反之不行,原因是子类指针的作用范围比父类指针作用范围大。
#include<iostream>
#include<string>
#include<cstdio>
#include<algorithm>
using namespace std;
class father
{
protected:
  string name="peter";
  int num=11;
};
class son:public father
{
public:
  void print()
  {
    cout<<"name:"<<name<<endl;
    cout<<"idcard:"<<father::num<<endl;
    cout<<"stuid:"<<num<<endl;
  }
protected:
  int num=999;
};
int main()
{
  son test;
  test.print();
  return 0;
}

注意作用域的区别,若是子类函数中有和父类函数相同的,那么也将屏蔽掉父类对同名成员的直接访问。

4.派生类的默认成员函数

  1. 首先清楚六个默认成员函数是什么?初始化和清理:构造函数主要完成初始化工作,析构函数主要完成清理工作。拷贝复制:拷贝构造函数是使用类对象初始化创建对象,赋值重载主要是把一个对象赋值给另一个对象。取地址重载:主要是对普通对象和const对象取地址。
  2. (快速记忆:好的事情先父亲,坏的先自己(坏的就是指析构函数))。
  3. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认的构造函数,则必须在派生类初始化列表阶段显示调用。
  4. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  5. 派生类的赋值运算符算符重载函数(operator=)完成基类的复制。
  6. 派生类对象初始化先调用基类构造再调用派生类构造。
  7. 派生类对象析构清理先调用派生类析构再调用基类的析构。 
  8. 构造是先构造基类,再有子类,拷贝构造是先给基类拷贝再给子类拷贝,析构是先干掉子类,再干掉父类,赋值是先给基类赋值,再给自己赋值,好事先基类(父亲),坏事先自己。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
using namespace std;
class father
{
public:
  father(const char* name="peter")
  :m_name(name)
  {
    cout<<"father()"<<endl;
  }
  father(const father& p)
  :m_name(p.m_name)
  {
    cout<<"father(const father& p)"<<endl;
  }
  father& operator=(const father& p)
  {
    cout<<"father operator=(const father& p)"<<endl;
    if(this!=&p)
      m_name=p.m_name;
    return *this;
  }
  ~father()
  {
    cout<<"~father()"<<endl;
  }
protected:
  string m_name;
};
class son:public father
{
public:
  son(const char* name,int num)
  :father(name),stunum(num)
  {
    cout<<"son()"<<endl;
  }
  son(const son& s)
  :father(s)
   ,stunum(s.stunum)
   {
     cout<<"son(const son& s)"<<endl;
   }
   son& operator=(const son& s)
   {
     cout<<"son& operator=(const son& s)"<<endl;
     if(this!=&s)
     {
       father::operator=(s);
       stunum=s.stunum;
     }
     return *this;
   }
   ~son()
   {
     cout<<"~son()"<<endl;
   }
protected:
  int stunum;//学号
};
int main()
{
  son s1("jack",25);
  son s2(s1);
  son s3("rose",23);
  s1=s3;//把s3赋值给s1
  return 0;
}

5.继承和友元

友元关系不能继承也就是说基类友元不能访问子类私有和保护成员,其实这个很好理解,你父亲的朋友不是你的朋友。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
using namespace std;
class son;
class father
{
public:
  friend void display(const father& p,const son& s);
private:
  string name;
};
class son:public father
{
protected:
  int num;
};
void display(const father& p,const son& s)
{
  cout<<p.name<<endl;
  cout<<s.num<<endl;
};
int main()
{
  father f;
  son s;
  display(f,s);
  return 0;
}

 

这就说明基类友元不能访问子类私有和保护成员。

6.继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论派生出多少个子类 ,都只有一个static成员实例。

静态成员可以继承,但无论发生任何事,都只有一个拷贝。

7.复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承。

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。

菱形继承:菱形继承是多继承的一种特殊情况。

菱形继承的问题:菱形继承有数据冗余和二义性的问题。

#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
class person
{
public:
  string name;
};
class student :public person
{
protected:
  int num;
};
class teacher:public person
{
protected:
  int id;
};
class Assiant:public student,public teacher
{
protected:
  string majorcourse;
};
int main()
{
  Assiant a;
  //需要显式指定访问哪个父类的成员可以解决二义性问题,
  //但是数据冗余问题无法解决
  a.student::name="peter";
  a.teacher::name="john";
//菱形继承某个类的两个父类拥有一个相同的父类会产生二义性和冗余性的问题
  return 0;
}

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。

解决方法:使用virtual关键字。在父亲类中继承方式改为virtual public方式继承。 

#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 Test()
{
    D d;
    d.B::_a = 1;
    d.C::_a = 2;
}

int main()
{
    Test();
    return 0;
}

虚拟继承为什么会解决数据冗余和二义性问题?

虚基表中存的是偏移量,通过偏移变量能解决数据冗余问题。

含有一个虚基类指针,指向自己的基类,作用是可以描述自己的父类,当发现被继承的另一个父类中也有这么一个相同的虚基类时,两个基类就会合并,只保留一个。

(普通的继承只继承了爷爷的衣钵,不知道爷爷是谁,虚继承都知道,所以我的两个爸爸的爸爸是同一个人的时候,干脆只要一个爷爷.)

虚继承只用于菱形继承的情况,也可以解决菱形继承带来的问题。

多重继承:

  • 一个派生类可以来源于多个基类,多个基类间用逗号隔开。

  • 如果多个父类有重名的成员,那么会产生二义,必须父类名::的方式指明用谁的成员。

  • 如果继承中的多个父类中有多个虚表,那么子类将全部继承下来。如果子类出现了新的虚函数,那么会加在第一个虚表(第一个继承的父亲的虚表)的后面,如果多个父类中含有相同的虚函数,子类重写后,将会只出现一个虚函数。

注意点:

1.多重继承父类不会争夺是因为每一个父类都知道偏移量的值,根据偏移量的值进行取。

2.若两个父亲有同名变量,则会出现访问不明确。

3.若两个父亲有同名函数,子类也有,调用的是子类的(父类被隐藏)子类没有,则访问不明确

4.若父类有虚函数,子类无,则子类会继承虚表,若子类也有虚函数,则不会创建新的虚表,直接在父亲/后面继承

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值