当前位置: 首页 > news >正文

【语法】C++的继承

目录

继承基本语法:

protected访问限定符:

子类和父类之间的赋值兼容规则:

重定义(隐藏): 

继承中的友元/继承中的静态成员: 

子类中的默认成员函数

构造函数/拷贝构造函数:

赋值重载函数:

析构函数: 

 菱形继承问题:

多继承:

解决:

原理:

继承和组合:


面向对象的三大特性分别是封装,继承和多态

而本篇的主要目标就是继承

如果要设计一个学生管理系统,肯定需要定义很多类,例如学生,老师等等

class student
{
public:string _name;int _age;int _score;int _num;
};class teacher
{
public:string _name;int _age;int _id;
}

可以看到student和teacher两个类中都有_name和_age字段,这不就重复了吗,这时就需要用到继承了

在继承中,有父类和子类(或叫基类和派生类)两个概念,我们可以先创建一个父类,用来存student和teacher类中都会用到的字段,再让子类(即student和teacher)去继承父类,就不需要像之前那样每个类都要定义一遍了

class Person
{
public:string _name;int _age;
};class student: public Person
{
public:int _score;int _num;
};class teacher: public Person
{
public:int _id;
}

如上述代码所示,student和teacher都以public的方式继承了Person类,那现在student和teacher里也都有_name和_age变量了。可以看出,继承的目的就是类之间的复用

那为什么前面还要加个public呢?

继承基本语法:

 这是继承的基本格式,也就是说,public是继承方式,那么除了public,就还有private和protected

这就是三种继承方式所对应的区别

也就是说,父类的成员的访问限定符是min(成员在基类的访问限定符,继承方式) (public > protected > private)

需要注意的是,class默认的继承方式是private,struct默认的继承方式是public

class Person
{
protected:string _name = "野兽先辈";int _age;
};class teacher:/*private*/ Person
{
public:teacher(){cout << _name << endl;}private:int _id;
};struct student: /*public*/ Person
{
public:student(){cout << _name << endl;}private:int _id;
};

上述代码中,teacher类是class定义,所以即使继承中不加继承方式,默认就是以private作为继承方式,而student类是struct定义,所以即使继承中不加继承方式,默认就是以public作为继承方式 

不过最好把继承方式写出来

protected访问限定符:

但无论是什么继承方式,父类的private成员在子类中都不可见

难道父类的成员就只能都设为public了吗?虽说设成public的确可以被子类访问,但这样的话不仅子类,类外都可以随意访问。

这时就要用到protected(保护)限定符了

在类中private和protected没有区别,但在继承的子类中有区别:

父类成员设为protected之后,仍可以确保private中在类外不能访问的特性,除此之外,还可以被继承的子类所访问到

因此,protected是专门因继承而出现的访问限定符

class Person
{
protected:string _name = "野兽先辈";int _age;
};class teacher: public Person
{
public:void print(){cout << _name << endl;}private:int _id;
};

这样就没有问题了

在实际运用中一般都是使用public继承,几乎很少用private/protected继承,也不提倡

子类和父类之间的赋值兼容规则:

在正常操作中,类之间的赋值都必须是相同类型,不同类型之间是不能赋值的

class Person
{
protected:string _name;int _age;
};class teacher
{
private:string _title;
};int main()
{teacher s1,s2;Person p1,p2;p1 = p2;//正确p1 = s1;//错误return 0;
}

在Person和teacher没有继承关系时,是不能将teacher的对象赋给Person的对象的

但如果teacher继承了Person呢?

class Person
{
protected:string _name;int _age;
};class teacher : public Person
{
private:string _title;
};int main()
{teacher s1,s2;Person p1,p2;p1 = p2;//正确p1 = s1;//正确return 0;
}   

此时,s1就可以赋给p1了

这就是子类和父类之间的赋值兼容规则

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

但反过来不行,即父类对象不能赋值给子类对象

如果这样可以的话,子类中多出来的成员要赋值成什么? 

有一种例外,前面说过,子类对象 可以赋值给 父类指针/引用

那指向子类对象的父类指针,就可以强转成子类指针后赋值给子类指针

teacher t;    
Person* pp = &t;//父类指针指向子类对象,那pp就只能访问到父类成员的那部分
teacher *pt = (teacher*)pp;//这样指向子类对象的父类指针就可以赋值给子类指针了,不过需要强转

引用也是一样

teacher t;    
Person& pp = t;//父类引用指向子类对象,那pp就只能访问到父类成员的那部分
teacher& pt1 = (teacher&)pp;//这样指向子类对象的父类引用就可以赋值给子类(引用)了,不过需要强转
teacher pt2 = (teacher&)pp;

重定义(隐藏): 

在继承体系中父类和子类不在一个作用域,他们都有独立的作用域

class Person
{
public:string _name;int _age = 114;
};class teacher : public Person
{
public:int _age = 514;string _title;
};

上述代码中,可以看到子类和父类都有个_age成员,那此时如果调用teacher实例化对象的_age会调用哪个呢?

答案是teacher中的_age

子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。 

 那如果就是要访问父类的同名成员要怎么办?

class Person
{
public:string _name;int _age = 114;
};class teacher : public Person
{
public:int _age = 514;string _title;void print(){cout << Person::_age << endl;//父类::父类成员}
};

如上述代码所示,只需要指定作用域就好了    父类::父类成员

成员函数只需要函数名相同就构成隐藏,所以如果父类和子类都有一个同名函数,且那个函数的返回值,参数不一样,千万不想错想成是重载!(重载的要求是必须在同一作用域)

继承中的友元/继承中的静态成员: 

 若父类中有友元函数,那继承的子类中不会将友元继承下来(了解即可)

若父类中有static成员,那整个继承体系中无论有多少个子类继承了父类,也都只会有一个唯一的static成员(了解即可)

子类中的默认成员函数

构造函数/拷贝构造函数:

子类的构造函数中会必须先调用父类的构造函数完成父类成员的初始化,再来完成子类成员的初始化

class Person
{
public:Person() { cout << "Person" << endl; }
};class Student : public Person
{
public:Student() :Person(){ cout << "Student" << endl; }
};int main()
{Student s;return 0;
}

输出结果:

那我如果就要在子类的构造函数中初始化父类成员呢?

class Person
{
public:Person() { cout << "Person" << endl; }
protected:string _name;
};class Student : public Person
{
public:Student() :_name("野兽先辈"){ cout << "Student" << endl; }
};int main()
{Student s;return 0;
}

上面代码中我试图用Student的构造函数初始化Person中的_name,但是运行不了

也就是说,要想初始化父类成员,就必须调用父类的构造函数来完成初始化

但好像这样又可以了?

不是不能在子类中给父类成员初始化吗?

确实不能,但上图中的_name = "野兽先辈"并不是初始化(initialization),而是赋值(assignment)

并且其实上图中在赋值之前也已经给_name初始化过了,只不过是隐式调用的Person的构造函数

 也就是说,即使你不写,编译器也会自动调用

对于拷贝构造而言,如果子类没有写拷贝构造函数,那么会自动调用父类的拷贝构造函数对父类成员进行初始化。但如果子类有拷贝构造函数了,那就不会调用父类的拷贝构造函数,而是调用父类的构造函数进行初始化

#include <iostream>struct base 
{base() { std::cout << "base default \n"; };base(const base&) { std::cout << "base copy \n"; }
};struct derived : base
{derived() { std::cout << "derived default \n"; };derived(const derived&) { std::cout << "derived copy \n"; }//有用户定义的拷贝构造,就只会调用父类的构造函数
};int main()
{derived a;std::cout << "\n";derived b(a);
}

输出:

#include <iostream>struct base 
{base() { std::cout << "base default \n"; };base(const base&) { std::cout << "base copy \n"; }
};struct derived : base
{derived() { std::cout << "derived default \n"; };//没有用户定义的拷贝构造,就会调用父类的拷贝构造函数//derived(const derived&) { std::cout << "derived copy \n"; }
};int main()
{derived a;std::cout << "\n";derived b(a);
}

赋值重载函数:

和上面一样,子类的operator=必须要调用父类的operator=完成基类的复制,而且必须显式调用(子类不会隐式调用父类operator=),如下所示

class Person
{
public:Person() { cout << "Person" << endl; }Person& operator=(const Person& p){if (this != &p)_name = p._name;return *this;}
protected:string _name;
};class Student : public Person
{
public:Student() { _name = "野兽先辈";cout << "Student" << endl; }Student& operator=(const Student& s){if (this != &s){Person::operator=(s);_age = s._age;}return *this;}
private:int _age;
};int main()
{Student s1,s2;s1 = s2;return 0;
}

但有些人可能发现,直接在子类的operator=中给父类成员赋值好像也没问题,这是为什么?

需要注意的是,这里并不是隐式调用了Person的operator=,因为子类不会隐式调用父类operator=

这里看起来没问题是因为我们的Person的operator=只完成了最简单的赋值操作,没有复杂操作(例如开辟空间),下面代码就不行了

class Person
{
public:Person(const char* name = ""): _name(new char[strlen(name) + 1]){strcpy(_name, name);cout << "Person constructed: " << _name << endl;}Person(const Person& p): _name(new char[strlen(p._name) + 1]){strcpy(_name, p._name);cout << "Person copy-constructed: " << _name << endl;}Person& operator=(const Person& p){if (this != &p){delete[] _name; // 释放旧资源_name = new char[strlen(p._name) + 1];strcpy(_name, p._name);cout << "Person assigned: " << _name << endl;}return *this;}virtual ~Person(){cout << "Person destructed: " << _name << endl;delete[] _name;}protected:char* _name;
};class Student : public Person
{
public:Student(const char* name = "", int age = 0): Person(name), _age(age){cout << "Student constructed: " << _name << ", Age: " << _age << endl;}Student(const Student& s): Person(s), _age(s._age){cout << "Student copy-constructed: " << _name << ", Age: " << _age << endl;}Student& operator=(const Student& s){if (this != &s){// 如果不调用基类的 operator=,基类的资源不会被正确处理_name = s._name; // 显式调用基类的赋值运算符_age = s._age;cout << "Student assigned: " << _name << ", Age: " << _age << endl;}return *this;}~Student(){cout << "Student destructed: " << _name << ", Age: " << _age << endl;}private:int _age;
};int main()
{Student s1("John", 20);Student s2("Doe", 25);cout << "Assigning s2 to s1..." << endl;s1 = s2; // 调用赋值运算符return 0;
}

如果运行的话,会报错

析构函数: 

如果像上面一样,那应该是要在子类的析构函数里面调用父类的析构函数

struct base {base() { std::cout << "base default \n"; }~base(){ std::cout << "~base \n";}
};struct derived : base{derived() { std::cout << "derived default \n"; }~derived(){ ~base();std::cout << "~derived \n";}
};

但这样却运行不了

 报错为找不到这个函数

实际上我们根本不需要这么做,因为在派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

在实际编程几乎很少会在子类析构函数中显示调用父类析构函数(可以这样来结束你使用placement new创建的对象的生命周期),那我如果非要调用,有什么方法吗?

先说一下为什么这里会报错

这里的报错和是不是继承没有关系,因为下面代码也会报同样的错误

class base
{~base(){cout << "~base \n";}void func(){~base();}
};

这段代码的执行顺序其实是先base(),再~

base()会先构造一个base的临时对象。这里其实不难理解,就例如int(),会调用int的默认构造函数创建临时的int变量,它的值是0

int main()
{cout << int() <<endl;return 0;
}

输出:0

所以base()也一样,会调用base的默认构造函数来构建一个临时的base对象

然后会试图调用base对象的operator~重载,如下图

#include <iostream>
using namespace std;
class base
{
public:base(){cout << "constructor base \n";}~base(){cout << "destructor base \n";}void operator~(){cout << "operator ~ \n";}
};class derived : public base
{
public:derived(){cout << "constructor derived \n";}~derived(){base::~base();cout << "destructor derived \n";}
};
int main()
{~base();cout <<"\n";base().operator~();//第一行就相当于这么调用return 0;
}

输出结果:

如上图,第一个和第一行和第三行的代码输出结果相同

所以报错的原因就是base类没有operator~重载

流程:先构造一个临时的base对象,再调用临时base对象的operator~运算符,最后析构这个临时的base对象

所以我们需要明确一点

base::~base();

在前面加上作用域限定符就好了,这样编译器就不会误认为是~运算符了

 菱形继承问题:

多继承:

上面所讲到的继承都叫单继承,并且实际中也一般只会用单继承

#include <iostream>using namespace std;
class base1
{
public:int a = 1;
};class base2 : public base1
{
public:int b = 2;
};class derived : public base2
{
public:int c = 3;
};
int main()
{derived d;return 0;
}

上面代码中,就是一个单继承

derived有base2的成员,base2中有base1的成员

而多继承是子类会同时继承多个父类

#include <iostream>using namespace std;
class base1
{
public:int a = 1;
};class base2
{
public:int b = 2;
};class derived : public base1, public base2
{
public:int c = 3;
};

上图代码,就是一个多继承

derived既有base1的元素,又有base2的元素,并且base1和base2没有联系 

多继承可以说是C++的一个万恶之源了,多继承本身没什么问题,但正是因为有多继承,才会有菱形继承问题

#include <iostream>using namespace std;class base0
{
public:int x = 0;
};
class base1 : public base0
{
public:int a = 1;
};class base2 : public base0
{
public:int b = 2;
};class derived : public base1, public base2
{
public:int c = 3;
};
int main()
{derived d;return 0;
}

这就是一个典型的菱形继承 

当然,也并非必须这样才算菱形继承

 这也是菱形继承

菱形继承会导致什么问题呢?

#include <iostream>using namespace std;class base0
{
public:int x = 0;
};
class base1 : public base0
{
public:int a = 1;
};class base2 : public base0
{
public:int b = 2;
};class derived : public base1, public base2
{
public:int c = 3;void test(){a = 10;b = 20;c = 30;x = 40;}
};

一眼望过去好像没什么问题,但它其实会报错

x是base0的成员,而base1和base2中也有x(继承了base0),而又因为derived继承了base1和base2,所以derived中有两个x,分别是base1和base2的x,如果像上图那样直接调用的话,编译器不知道你要调用哪个x,就会报错

所以需要在前面加上作用域限定符,如下所示

base1::x = 40;
base2::x = 50;

这样会让继承具有数据冗余(被定义多次)和二义性(意义不明确)

解决:

在base1和base2继承base0时加上virtual就可以了

class base0
{
public:int x = 0;
};
class base1 : virtual public base0
{
public:int a = 1;
};class base2 : virtual public base0
{
public:int b = 2;
};class derived : public base1, public base2
{
public:int c = 3;void test(){a = 10;b = 20;c = 30;x = 40;}
};

为什么加上virtual就行了呢?

在继承的前面加上virtual是虚继承的意思,简单来说就是x原来会存两个,但现在x只会存1个了。

原理:

class base0
{
public:int x;
};
class base1 : virtual public base0
{
public:int a;
};class base2 : virtual public base0
{
public:int b;
};class derived : public base1, public base2
{
public:int c;
};int main()
{derived d;d.a = 1;d.b = 2;d.c = 3;d.x = 4;return 0;
}

以上面代码为例,我们来调用调试看看d在内存中的分布

从上面不难看出,a,b,c,x 4个变量都在这里面,但是怎么还有两个看不出来是什么的值呢?

先来给这块内存分个区

可以看到现在x被直接放在子类中,而那两个虚继承了base0的类中,都多出了一个值

这其实是两个指针,叫做虚基表指针(虚拟-基类),而虚基表指针里存的是什么呢?

虚基表指针中存的是虚基表

分别找到这两个虚基表指针存的虚基表后,可以发现,每个虚基表中有8个字节,第一个虚基表存的是14,第二个虚基表中存的是0c,他们都是16进制,转换成10进制后是20和12,这两个值其实是他们各自所属的类(base1和base2)距离虚基类(x(base0))的偏移量

拿上面的图举例,x是在00D2F844的位置,base1是在00D2F830的位置,00D2F830+14 = 00D2F844,正好是x的地址;base2是在00D2F838的位置,00D2F838+0c = 00D2F844,正好是x的地址

问:为什么虚基表是8个字节?这不是只存了一个值吗?

答:上面的例子中只有一个虚基类,所以虚基表中只有一个值,如果子类中有多个虚基类,那虚基表中就会存多个值

继承和组合:

什么是组合?

class B
{class A;//A包含在B类中,是B的成员
}

这就是组合

可以发现继承和组合都可以完成类的复用,那到底是继承好还是组合好?

先说结论:组合更好

为什么?
继承是一种白箱复用,父类对于子类都是透明的,不管父类成员是public还是protected,在子类都可见,一定程度上破坏了父类的封装

组合是一种黑箱复用,父类对于子类不是透明的,父类public的成员,在子类依旧可见,但父类private/protected的成员,在子类就不可见了

因此组合的耦合度较低,而继承的耦合较高,所以组合更好

但并不是继承就没有用武之地:

构成is-a关系的用继承

构成has-a关系的用组合

class car
{
public:...
protected:...}class BWM : public car//宝马是车(is-a的关系用继承)
{
public:...
protected:...
}

上述代码中,车和宝马是is-a的关系,即“宝马车”

如果用组合的话,就成了“宝马车”,会非常别扭

class tire{};
class car : public tire 
{class tire;//车有轮胎(构成has-a关系)
}

上述代码中,轮胎和车是has-a的关系,即“车轮胎”

这时就可以用组合

当然,如果两者都可以用的话,肯定优先用组合

相关文章:

  • 篮球足球体育球员综合资讯网站模板
  • hutools工具类中isNotEmpty与isNotBlank区分
  • 关闭正点原子atk-qtapp-start.service
  • 企业办公协同平台安全一体化生态入住技术架构与接口标准分析报告
  • Day16(贪心算法)——LeetCode45.跳跃游戏II763.划分字母区间
  • 机器学习实操 第一部分 机器学习基础 第6章 决策树
  • 高定电视,一场关于生活方式的觉醒
  • 基于 ARM 的自动跟拍云台设计
  • 第六章 QT基础:7、Qt中多线程的使用
  • Vue常用的修饰符有哪些有什么应用场景(含deep seek讲解)
  • 嵌入式设备异常掉电怎么办?
  • 第三方软件测试报告如何凭借独立公正与专业权威发挥关键作用?
  • CA校验主辅小区配置及UE能力
  • 通过 Node.js 搭配 Nodemailer 实现邮箱验证码发送
  • 分治而不割裂—分治协同式敏捷工作模式
  • Plant Simulation MultiPortalCrane Store 小案例
  • 【网络编程】socket编程和TCP协议
  • 远程桌面导致Quartus 破解失效
  • 爬虫学习笔记(四)---request入门
  • npm如何安装pnpm
  • 南京航空航天大学启动扁平化改革:管理岗规模控制在20%,不再统一设科级机构
  • 习近平对辽宁辽阳市白塔区一饭店火灾事故作出重要指示
  • 王毅:为改革完善全球治理作出金砖贡献
  • 君亭酒店:2024年营业收入约6.76亿元, “酒店行业传统增长模式面临巨大挑战”
  • AI观察|算力饥渴与泡沫
  • 五一期间上海景观照明开启重大活动模式,外滩不展演光影秀