【C++】:继承全面解析
希望文章能对你有所帮助,有不足的地方请在评论区留言指正,一起交流学习!
目录
1.引入
2.继承的概念
3.继承的定义
3.1.继承的语法
3.2.继承的方式和访问限定符
4.基类和派生类的赋值转换
5.继承当中的作用域
5.1.父子类中的同名成员变量
5.2.同名函数
6.派生类中的默认成员函数
7.继承和友元
8.继承和静态成员
1.引入
C++是面向对象的语言,我们定义两个类一个是student类,一个是teacher类便于创建对象的操作。
class student
{
public:string name; // 姓名int age; // 年龄string email; // 邮箱string gender; // 性别string phone; // 电话//特有的信息string major; // 专业string class_id; // 班级int admission_year; // 入学年份string student_id; // 学号
private:
};
class teacher
{
public:string name; // 姓名int age; // 年龄string email; // 邮箱string phone; // 电话string gender; // 性别//特有的信息string department; // 所在部门int hire_year; // 入职年份string teacher_id; // 工号string course; // 教授课程
private:
};
对student的类和teacher的类进行了分析,我们发现二者有需要的成员变量存在相同的部分。因此就可以将二者的相同的部分提取出来创建一个新的类person,再由student和teacher继承person的成员变量,这样减少代码重复与冗余。
定义一个person的类
class person
{
public:string name; // 姓名int age; // 年龄string email; // 邮箱string phone; // 电话string gender; // 性别
};
那么如何继承呢? public是继承的方式,下面我们会提到的
然后我们创建一个对象,看看student是否继承了person的成员变量。
调出去局部变量窗口观测 student创建的st对象是否继承,可以看到st的成员变量完全继承了person的成员变量。
至此就是继承的基本使用方式,下面将详细介绍继承。
2.继承的概念
继承是一种允许我们基于一个已有的类(基类 / 父类,Base Class)来定义一个新的类(派生类 / 子类,Derived Class)的机制。
核心特征
- 代码的复用:子类自动获得父类的属性(成员变量)和行为(成员函数),无需重新编写。
- 行为扩展:子类可以在父类的基础上添加新的成员变量和成员函数,以满足新的需求。
class person
{
public:void print(){cout << "姓名:" << _name << endl;}
protected:string _name = "小六子"; };
class student : public person
{
protected:int _stuid; // 学号
};int main()
{student st;st.print();return 0;
}
注意点:
- 派生类和基类是两个类,但是不是孤立存在的,派生类的定义依赖基类,它在和基类的 共性基础上添加了个性内容,本质是 基于基类扩展而来的新类。
- 继承不等于代码复制,派生类 “继承” 基类的成员函数,本质上是获得了调用这些函数的权限(在访问控制允许的前提下),而不是在内存中再复制一份相同的函数代码。
内存中实际情况
- 成员变量:派生类对象会包含基类的成员变量(包括私有成员),这些变量在对象内存布局中是真实存在的。
- 成员函数:所有类的成员函数(静态 / 非静态)都放在代码区,多个对象共享同一份函数代码,不随对象数量增加而复制。对于派生类也是如此,继承基类的使用权限,并没有得到真正的奥秘(俗称留一手)
之前的知识,复习一下
在编译后,类的成员函数只有一份代码段,存放在程序的代码区,不会因为继承而在派生类中复制一份。
以上述例子看一下 ,派生类的内存的使用情况
成员变量
成员变量确实是复制了, 真实存在的,但是在实际的内存中是没有person的壳子的,内存中_name和_stuid按照先后顺序直接排布在内存中。
成员函数 person的对象和student创建的对象调用的函数是否是一样的。
int main()
{student st;person p;p.print();st.print();return 0;
}
转到反汇编之后,可以看出链接时候,调用的函数地址是一样的,也就是同一个函数。
证明了基类的成员函数没有被复制,派生类只是继承了基类函数的使用权。
3.继承的定义
3.1.继承的语法
如何继承 继承的语法,
3.2.继承的方式和访问限定符
继承方式决定了派生类如何继承基类成员的访问权限。 分为三种 公有继承 (public)保护继承(protected)私有继承(privite)
访问限定符分为三种 public(公有)protected保护 privite私有
在 C++ 中,继承方式和访问限定符是控制类成员访问权限的两大核心机制,二者相互作用决定了基类成员在派生类中的可见性。
类成员/继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private 成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private 成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
1.之前我们当private和protected的区别是一样的,但是在这里就有了区别,基类中private成员,派生类是不可见的,如果基类中的成员指向在派生类中访问模式用protected定义
2.基类的private成员在派生类中是不可以见的(类内类外都不可以使用);但是派生类还是继承了
3.除了基类的private成员,基类的其他成员在子类中的访问方式,Min(成员在基类的访问限定符,继承方式)
4.默认继承 使用关键字class创建类,默认继承方式是private ;使用关键字sruct创建类,默认继承方式是public。
5.实际应用中,继承方式一般使用 public ,基类中的成员访问限定文为 public private
4.基类和派生类的赋值转换
之前知识的复习
1. 不同的对象之间赋值是不允许的,如果允许,就是类型转化 ?
2.不同类型之间的赋值,无论是隐式还是强制类型转换,底层都会先产生一个目标类型的临时对象,然后再把临时对象的值赋给左操作数。
double a = 2.33;
int b = a;
cout << b << endl;
上述代码存在隐式类型转换,产生中间临时变量(常性)。
那么派生类对象可以赋值给基类的对象吗? 可以的,派生类赋值给基类属于隐式类型转换,但是没有中间变量。
但是基类对象不能赋值给派生类 ,子类中有的成员变量基类没有,所以基类不能给派生类赋值,强制转换也是不可以的,语法直接禁止此行为。
class person
{
public:void print(){cout << "姓名:" << _name << endl;}
protected:string _name = "小六子";
};class student : public person
{
protected:int _stuid = 1; // 学号
};
int main()
{student s;person p = s;return 0;
}
证明没有中间变量,使用引用的权限的特性.
int main()
{double a = 2.33;const int& b = a;cout << b << endl;student s;person p;p = s;person& p1 = s;person* p2 = &s;p2->print();//通过结构体指针调用函数p1._name = "王六";p1.print();//通过引用调用函数return 0;}
当 b为整型引用的,a给其赋值,会产生中间变量(常性),int& 的权限大于常量,因此需要加上const缩小权限。
而 student的对象赋值给s,中间不会产生中间变量,上述的代码看出,派生类可与给基类的引用或者指针赋值。
对象的赋值 子类中将属于父类的部分切出来给父类赋值
引用赋值 子类对象中属于父类的哪部分的别名
指针赋值 指针指向子类子类对象中属于父类的哪部分
总结:
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割(赋值兼容转换)。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
5.继承当中的作用域
定义一个类就会有一个类域,基类和派生类就会有自己的类域,自己的类域如何说明的呢?
1.在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。4. 注意在实际中在继承体系里面最好不要定义同名的成员。
5.1.父子类中的同名成员变量
class Person
{
protected:string _name = "小六子"; int _num =89;
};
class Student : public Person
{
public:void Print(){cout << " 学号:" << _num << endl;}
protected:int _num = 139;
};
int main()
{Student s1;s1.Print();
}
同名的成员父子类也是可以的定义的,因为二者根本不属于同一个作用域;子类中将父类中的童名成员隐藏了。
因此,上述代码输出的结果是139,查找就近原则先查找子类的作用域,没有才会取父类中查找想用父类的成员变量,需要使用类域特别指定(不是真的隐藏,指定线索也是可以找的)
域的本质是在编译的时候指导编译器查找的规则 局部域 全局域 类域 命名空间域
5.2.同名函数
class A
{
public:void fun(){cout << "func()" << endl;}
};
class B : public A
{
public:void fun(int i){A::fun();cout << "func(int i)->" << i << endl;}
};
int main()
{B b;b.fun(10);
}
上述同名函数构成了隐藏/重定义;C++ 编译器默认会对几乎所有函数进行名字修饰,以便支持重载、命名空间、模板等特性。
func()触发了编译器的语法检查
那么如何访问父类中的成员函数,指定了还是可以访问的。
子可以调用父类,父类不能调用子类的。
6.派生类中的默认成员函数
在类中由六个默认成员函数,这里我讲解常用的四个默认成员函数,构造函数、拷贝构造函数、析构函数、赋值重载函数。
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。3. 派生类对象初始化先调用基类构造再调派生类构造。
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): _num(num){cout << "Student()" << endl;}
protected:int _num; //学号
};
我们以上述的persom和student为例子
(1)构造函数
使用上述代码定义对象的效果,可知,创建派生类对象,将调用基类的默认构造函数
使用上述代码定义对象的效果
上述程序基类中没有默认构造函数
根据上述所讲述的派生类的初始化必须嗲用基类的构造函数,有默认构造可以不用在初始化列表显示调用,但是没有默认构造必须显示调用初始化列表。父类没有默认构造,定义一个匿名对象一样。
使用上述代码定义对象的效果
上述两个构造函数是一样的,说明派生类的构造函数会自动调用基类的默认构造函数,如果基类没有默认构造函数,需要在初始化列表中初始化。类似自定义类型的对象,将父类看成了对象,自动调用一般调用默认的成员
初始化的顺序
初始化的顺序和声明的顺序 是一样的 ,继承父类的成员变量放在子类前面的。父类对象看作为整体对象。person先初始化,在初始化student。
(2)拷贝构造
和构造函数一样,写拷贝构造 ,可以直接初始化基类的成员只能调用其拷贝构造函数
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
输出的结果
没有调用拷贝构造的话
将会去调用构造函数
(3)赋值重载
派生类的operator=必须要调用基类的operator=完成基类的复制。
因为基类和派生列的operator=名字一样构成了隐藏/重定义,因此必须指定类域调用基类中的operator=函数。
(4)析构函数
- 由于因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
因此派生类中的析构函数只要销毁派生类中的资源和成员变量就可以了,编译器会自动调用基类的析构函数
当派生类的析构函数有基类的析构函数时候
输出的结果
每个对象调用两次析构函数,所以是错误的写法。
显示调用父类析构函数,无法保证先子后父,子类析构无法完成就调用父类析构自动调用基类析构的好处保证先子后父, 保证派生类对象先清理派生类成员再清理基类成员的顺序。
假如子类中可能用到父类的成员的,父类仙溪沟必定影响子类;子类析构了不会影响父类 子可以使用父类 ,父不能用子类的成员比那辆。
7.继承和友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
上述程序中的基类中的友元函数,想要访问派生类的成员函数,但是会报错的
解决方式,将这个函数变为派生类的友元
8.继承和静态成员
静态成员是否可以继承 辩证的看待 ,
class Person
{
public:Person() { ++_count; }string _name;
public:static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:int _stuNum;
};
class Graduate : public Student
{
protected:string _seminarCourse;
};
int main()
{Person p;Student s;cout << &p._name << endl;cout << &s._name << endl;cout << &p._count << endl;cout << &s._count << endl;cout << &Person::_count << endl;cout << &Person::_count << endl;return 0;
}
两个_name不是一个_name 对象实例化一部分 ,静态成员变量可以使用对象去访问也可以类名访问;静态成员继承的是使用权,静态成员属于父类和派生类,在派生列不会拷贝一份,继承的是使用权。