【C++】继承(上) 继承的基本概念 | 子类的默认成员函数

一、继承

概念

继承(inheritance)是一种面向对象编程的概念,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的特征和行为。子类可以获得父类的成员函数和变量,而不需要重新编写它们。子类还可以添加自己的成员函数和变量,以扩展其功能。

可以说,继承机制 是在面向对象的程序设计中,使代码可以复用的最重要的手段。以前我们接触的复用都是函数复用,而继承是类设计层次的复用。

我们通过下面的例子来说明什么是继承。假设要录入全校师生的信息,那要根据学校中的不同角色,涉及不同的类,如学生、老师、职工……

#include<iostream>
using namespace std;
class Student
{
    string name;
    int age;
    string tele;
    string StudentID;
};
class Teacher
{
    string name;
    int age;
    string tele;
    string TeacherID;
};
class Staff
{
    string name;
    int age;
    string tele;
    string StaffID;
};
int main() {
    
    return 0;
}

但实际上,我们会发现,这些类的基本成员(姓名、年龄、电话)是一致的,仅仅是ID不一样。那这样写,是否会造成代码的冗余呢?

Yes。为了避免这种冗余,C++中用“继承”的机制来复用类中的代码。

我们先定义出一个包含基本成员的Person类,然后复用这个Person类,让各个类都能继承Person类中的基本成员:

#include<iostream>
using namespace std;
class Person
{
    string name;
    int age;
    string tele;
};
class Student:public Person   //让Student继承Person类
{
    string StudentID;
};
class Teacher :public Person
{
    string TeacherID;
};
class Staff :public Person
{
    string StaffID;
};
int main() {
    Student s;
    return 0;
}

这就是继承,Student类继承了Person类中的所有成员。我们打开监视窗口看看s的当前成员:

可见,Perosn的成员已经被Student对象包含进来了。

格式

格式:class 子类名 : 继承方式 父类名

在下面的例子中,Person是父类,也称作基类。Student是子类,也称作派生类

Student和Person是“子承父业”的关系,子类在获得父类全部成员的基础上,又拓展了自己的属性,添加了“学号”“专业”这种 自己的成员变量。

访问限定符protected

之前学访问限定符时,就剩了个protected没提。因为protected是为继承而设计的。

三种访问方式:

public:公有,类外可直接访问

private:私有,仅类里能访问,类外不可访问

protected:保护。通常用于父类中,子类能访问父类的protected成员,而类外不能访问。

举个栗子,如果我们把父类成员设为private,那类外和子类都被拦在类域之外,子类无法访问父类成员:

#include<iostream>
using namespace std;
class Person       //把Person成员设为private
{
private:
    string name="zhangsan";
    int age=20;
    string tele="1234567";
};
​
class Student:public Person
{
public:
    void Print() {
        cout << name << endl;  //访问父类成员
        cout << age << endl;
        cout << tele << endl;
    }
    string StudentID;
};
​
int main() {
    Student s;
    s.Print();
    return 0;
}

这种情况下,Student是访问不了Person成员的:

如果我们设父类成员为protected,那就对子类开放了访问窗口(类外仍不可访问):

class Person
{
protected:
    string name="zhangsan";
    int age=20
    string tele="1234567";
};
class Student:public Person
{
    ……
};
int main() {
    Student s;
    s.Print();
    return 0;
}

这就是protected访问方式。

其实,public、protected、private不仅是三种访问方式,也可以是三种继承方式。

三种继承方式

继承方式,分public、protected、private这三种情况。

3种父类的访问方式,与3种子类的继承方式,结合一下就是9种情况:

这个表老师一般都会要求背诵,我们不要死记硬背,这张表其实是很有规律的:两个权限结合,我们取权限更小的那个。从权限来看,public>protected>private,如果是protected继承与基类的private成员,那继承到派生类中的权限就是private(小的那个)。

几点说明:

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

2.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。

3.在实际运用中,绝大部分情况都用public继承,很少使用protetced/private继承,也不提倡使用protetced/private继承,protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

4.如果不指定继承方式,那默认为private。

子类和父类对象的赋值转换

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

#include<iostream>
using namespace std;
class Person
{
public:
    string name="zhangsan";
    int age=20;
    string tele="1234567";
};
class Student:public Person
{
public:
    string StudentID;
};
int main() {
    Student s;
    Person p;
    p = s;   //子类对象赋值给父类对象
    return 0;
}

2.父类对象不能赋值给子类对象。

3.当父类指针指向子类对象时,如何避免切片现象?

刚刚我们说了,子类对象可以赋值给父类的指针。但当子类对象的大小大于父类对象的大小时,则会切片,使子类对象的部分信息丢失。

这种情况下,为了避免切片现象,处理方法是:将父类指针强制转换为子类指针。

示例:

class A
{
public:
    int _a=10;
};
class B:public A
{
public:
    int _b=20;
};
int main() {
    B b;
    A* pa = &b;
    B* pb = (B*)pa;  //强转
    return 0;
}

隐藏(重定义)

在继承体系中子类和父类都有独立的作用域。

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

例:

#include<iostream>
using namespace std;
class Person
{
public:
    string name="zhangsan";
    int age=20;
    string tele="Person中的tele";
};
class Student:public Person
{
public:
    void Print() {
        cout << tele << endl;
    }
    string tele="Student中的tele";
};
int main() {
    Student s;
    s.Print();  //此时从Person中继承来的tele被隐藏
    return 0;
}

隐藏并不是删除,从父类那里继承来的同名成员 依然存在于子类中。在子类的成员函数中,可以使用 “父类::父类成员”的方式访问。我们来访问一下看看:

class Person
{
public:
    ……
    string tele="Person中的tele";
};
class Student:public Person
{
public:
    void Print() {
        cout << Person::tele << endl;
    }
    string tele="Student中的tele";
};
int main() {
    Student s;
    s.Print();  
    return 0;
}

如果是成员函数的隐藏,只需要函数名相同就构成隐藏。不过,在实际中,继承体系里面最好不要定义同名的成员。

区分隐藏与函数重载

小练习:A和B的func构成什么关系? A、重载 B、重定义 C、重写

class A {
public:
    void func() {
        cout << "func()" << endl;
    }
};
 
class B : public A {
public:
    void func(int i) {
        A::func();
        cout << "func(int i) -> " << i << endl;
    }
};
 
int main(void) {
    B b;
    b.func(10);
}

answer:B重定义,即隐藏。这里很容易误选A。函数重载的前提是,得在同一个作用域里。而子类和父类是两个不同的类域,所以这俩类域中相同的函数名构成隐藏,而非函数重载。

继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。

class Student;
class Person
{
    friend void Print(Person& p, Student& s);
protected:
    string _name;
};
class Student :public Person
{
protected:
    string _ID;
};
void Print(Person& p, Student& s) {
    cout << p._name << endl;     //可以访问
    cout << s._ID << endl;     //不可访问
}
int main() {
    Person p;
    Student s;
    Print(p, s);
    return 0;
}

基类与静态成员

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

#include<iostream>
using namespace std;
class Student;
class Person
{
public:
    static int count;
    Person()
        :_name("")
    {
        count++;
    }
protected:
    string _name;
};
int Person::count = 0;
​
class Student :public Person
{
protected:
    string _ID;
};
int main() {
    Person p1;
    Student s;
    cout << Person::count <<" "<< Student::count << endl;   //原本count为2
    Person::count = 0;             //将Person的count置空
    cout << Student::count << endl;   //发现Student的count也被清空了,说明这俩count就是同一个
    return 0;
}

静态成员始终是放在静态区的,而不是储存在对象的内存分配中。静态成员是一定不被包含在对象中的。

 判断正误

1.基类的所有成员变量都会被子类继承。

√,包括静态成员变量,都会被子类继承。

2.基类对象包含了所有的基类成员变量。

×,静态成员变量跟普通变量存放的位置不一样,前者存放在静态区,后者存放在对象的内存空间中。所以不包含。

3.子类对象中不仅包含了所有基类成员变量,也包含了所有子类成员变量。

×,静态成员变量就不被包含

二、子类的默认成员函数

构造函数

当实例化出子类对象时,会先调用父类的构造函数,再调用子类的构造函数。

示例:

#include<iostream>
using namespace std;
class Person
{
public:
    Person(string n="",int a=0,string tel="")
        :name(n)
        , age(a)
        , tele(tel)
    {
        cout << "调用了基类的构造" << endl;
    }
​
    string name;
    int age;
    string tele;
};
​
class Student :public Person
{
public:
    Student(string n="", int a=0, string tel="", string teleNum="")
        :tele(teleNum)     //这里没有显示调用父类的构造函数,但编译器会自觉调用父类的默认构造函数
    {
        cout << "调用了派生类的构造" << endl;
    }
    string tele;
};
​
int main() {
    Student s;
    return 0;
}

子类的构造函数必须调用 父类的构造函数 初始化父类的那一部分成员。那假如父类没有默认构造函数,要怎么办呢?

class Person
{
public:
    Person(string n,int a,string tel)  //父类的构造函数
        :name(n)
        , age(a)
        , tele(tel)
    {
        cout << "调用了基类的构造" << endl;
    }
    string name;
    int age;
    string tele;
};

这时,得在子类的构造函数中,显示调用父类的构造函数:

class Student:public Person
{
public:
    Student(string n, int a, string tel,string teleNum)
        :tele(teleNum)
        ,Person(n,a,tel)   //需显式调用
    {
        cout << "调用了派生类的构造" << endl;
    }
    string tele;
};

总之,父类的构造函数是一定要调用 并且是先调用的。假如你不想让某类被继承,那就把它的构造函数私有化。

析构函数

析构的顺序是固定的:先析构子类对象,再析构父类对象。为了保证一定是这个析构顺序,子类的析构函数在调用完成后,会自动调用父类的析构函数。

#include<iostream>
using namespace std;
class Person
{
public:
    ~Person() {
        cout << "析构父类" << endl;
    }
    string name;
    int age;
    string tele;
};
class Student:public Person
{
public:
    ~Student() {
        cout << "析构子类" << endl;
    }
    string tele;
};
int main() {
    Student s;
    return 0;
}

要注意,析构函数不要再显示调用了!来看下面这个错误示例:

class Person
{
public:
    ~Person() {
        cout << "析构父类" << endl;
    }
    ……
};
class Student:public Person
{
public:
    ~Student() {
        Person::~Person();   //在析构函数中显示调用基类的析构
        cout << "析构子类" << endl;
    }
    string tele;
};
int main() {
    Student s;
    return 0;
}

可见,这样会导致基类对象被析构两次。

因为父类的析构会被自动调用,所以不要再显示调用父类的析构函数了。不然如果有指针变量,析构两次就造成释放野指针的问题。

拷贝构造函数

子类的拷贝构造函数中,初始化 从父类那儿继承来的成员 时,必须得调用父类的拷贝构造函数。

#include<iostream>
using namespace std;
class Person
{
public:
    Person(string n="",int a=0,string tel="")
        :name(n)
        , age(a)
        , tele(tel)
    {
        cout << "调用了基类的构造" << endl;
    }
​
    //拷贝构造
    Person(const Person& p)
        :name(p.name)
        ,age(p.age)
        ,tele(p.tele)
    {
        cout << "调用了父类的拷贝构造" << endl;
    }
    string name;
    int age;
    string tele;
};
​
class Student :public Person
{
public:
    Student(string n = "", int a = 0, string tel = "",string teleNum ="")
        :tele(teleNum)
        ,Person(n,a,tel)
    {
        cout << "调用了派生类的构造" << endl;
    }
​
    //拷贝构造
    Student(const Student& s)
        :Person(s)   //s中,属于Person的那部分会“切片”,拷贝过去
        ,tele("")
    {
        cout << "调用了zi类的拷贝构造" << endl;
    }
    string tele;
};
  
int main() {
    Student s1("zhangsan",20,"111","");
    Student s2(s1);
    return 0;
}

赋值重载operator=

子类的operator=必须调用父类的operator=,完成父类成员的赋值。

#include<iostream>
using namespace std;
class Person
{
public:
    Person(const char* name="")
        :_name(name)
    {}
protected:
    string _name;
};
class Student :public Person
{
public:
    Student(const char* name="",string ID = "")
        :_ID(ID)
        ,Person(name)
    {}
    Student& operator=(const Student& s) {
        if (this != &s) {
            Person::operator=(s);   //调用父类的operator=
            _ID = s._ID;
        }
        return *this;
    }
​
protected:
    string _ID;
};
int main() {
    Student s1("111","000");
    Student s2;
    s2 = s1;
    return 0;
}

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目录 1、 C++对C的扩展 1 1简单的C++程序 1 1.1求圆的周长和面积 1 1.2初学者易犯错误模型 3 2程序设计方法的发展历程 4 3 C语言C++语言关系 6 4 C++对C的加强 6 4.1 namespace命名空间 6 4.2 “实用性”增加 6 4.3 register关键字增强 6 4.4变量检测增强 6 4.5 struct类型加强 6 4.6 C++中所有的变量和函数都必须有类型 6 4.7新增Bool类型关键字 6 4.8三目运算符功能增强 6 5 C/C++中的const 6 1 const基础知识(用法、含义、好处) 6 2 C中“冒牌货” 6 3 const和#define相同之处 6 4 const和#define的区别 6 5 结论 6 6引用专题讲座 6 1引用(普通引用) 6 2常引用 6 3 const引用结论 6 4const修饰类 6 5综合练习 6 7C++对C的函数扩展 6 1 inline内联函数 6 2 默认参数 6 3 函数占位参数 6 4 默认参数和占位参数 6 5 函数重载(Overroad) 6 8附录 6 附录1:C++语言对C语言扩充和增强的几点具体体现 6 附录2:C语言register关键字—最快的关键字 6 一、皇帝身边的小太监----寄存器 6 2、类和对象 6 1前言 6 2类和对象 6 2.1 基本概念 6 2.2类的封装 6 2.3 C++面向对象程序设计举例 6 2.4 作业 6 3对象的构造和析构 6 3.1构造和析构函数 6 3.2构造函数的分类及调用 6 3.3构造函数调用规则研究 6 3.4深拷贝和浅拷贝 6 3.5多个对象构造和析构 6 3.6构造函数和析构函数的调用顺序研究 6 3.7构造函数和析构函数综合练习 6 3.8 对象的动态建立和释放 6 4静态成员变量成员函数 6 4.1静态成员变量 6 4.2静态成员函数 6 4.3综合训练 6 5 C++面向对象模型初探 6 5.1基础知识 6 5.2编译器对属性和方法的处理机制 6 5.3总结 6 5.4 this指针 6 5.5全局函数PK成员函数 6 6 6友元 6 6.1友元函数 6 6.2友元类 6 7强化训练 6 1 static关键字强化训练题 6 2 数组类封装 6 3小结 6 8运算符重载 6 8.1概念 6 8.2运算符重载的限制 6 8.3运算符重载编程基础 6 8.4运算符重载提高 6 8.5运算符重载在项目开发中的应用 6 8.7附录:运算符和结合性 6 2、 继承和派生 6 3.1继承概念 6 3.1.1类之间的关系 6 3.1.2继承关系举例 6 3.1.3 继承相关概念 6 3.1.4 派生类的定义 6 3.1.5 继承重要说明 6 3.2派生类的访问控制 6 3.2.1单个类的访问控制 6 3.2.2不同的继承方式会改变继承成员的访问属性 6 3.2.3“三看”原则 6 3.2.3派生类类成员访问级别设置的原则 6 3.2.4综合训练 6 3.3继承中的构造和析构 6 3.3.1类型兼容性原则 6 3.3.2继承中的对象模型 6 3.3.3继承中的构造析构调用原则 6 3.3.4继承与组合混搭情况下,构造和析构调用原则 6 3.3.5继承中的同名成员变量处理方法 6 3.3.6派生类中的static关键字 6 3.4多继承 6 3.4.1多继承的应用 6 3.4.2虚继承 6 3.5继承总结 6 4、多态 6 4.1多态 6 4.1.1问题引出 6 4.1.2面向对象新需求 6 4.1.3解决方案 6 4.1.4多态实例 6 4.1.5多态工程意义 6 4.1.6多态成立的条件 6 4.1.7多态的理论基础 6 4.2多态相关面试题 6 面试题1:请谈谈你对多态的理解 6 面试题2:谈谈C++编译器是如何实现多态 6 面试题3:谈谈你对重写,重载理解 6 #include <cstdlib> 6 #include <iostream> 6 using namespace std; 6 class Parent01 6 { 6 public: 6 Parent01() 6 { 6 cout<<"Parent01:printf()..do"<<endl; 6 } 6 public: 6 virtual void func() 6 { 6 cout<<"Parent01:void func()"<<endl; 6 } 6 virtual void func(int i) 6 { 6 cout<<"Parent:void func(int i)"<<endl; 6 } 6 virtual void func(int i, int j) 6 { 6 cout<<"Parent:void func(int i, int j)"<<endl; 6 } 6 }; 6 class Child01 : public Parent01 6 { 6 public: 6 //此处2个参数,和子类func函数是什么关系 6 void func(int i, int j) 6 { 6 cout<<"Child:void func(int i, int j)"<<" "<<i + j<<endl; 6 } 6 //此处3个参数的,和子类func函数是什么关系 6 void func(int i, int j, int k) 6 { 6 cout<<"Child:void func(int i, int j, int k)"<<" "<<i + j + k<<endl; 6 } 6 }; 6 void run01(Parent01* p) 6 { 6 p->func(1, 2); 6 } 6 int main() 6 { 6 Parent01 p; 6 p.func(); 6 p.func(1); 6 p.func(1, 2); 6 Child01 c; 6 //c.func(); //问题1 6 c.Parent01::func(); 6 c.func(1, 2); 6 run01(&p); 6 run01(&c); 6 system("pause"); 6 return 0; 6 } 6 //问题1:child对象继承父类对象的func,请问这句话能运行吗?why 6 //c.func(); //因为名称覆盖,C++编译器不会去父类中寻找0个参数的func函数,只会在子类中找func函数。 6 //1子类里面的func无法重载父类里面的func 6 //2当父类和子类有相同的函数名、变量名出现,发生名称覆盖(子类的函数名,覆盖了父类的函数名。) 6 //3//c.Parent::func(); 6 //问题2 子类的两个func和父类里的三个func函数是什么关系? 6 面试题4:是否可类的每个成员函数都声明为虚函数,为什么。 c++编译器多态实现原理 6 面试题5:构造函数中调用虚函数能实现多态吗?为什么? c++编译器多态实现原理 6 面试题6:虚函数表指针(VPTR)被编译器初始化的过程,你是如何理解的? 6 面试题7:父类的构造函数中调用虚函数,能发生多态吗? c++编译器多态实现原理 6 面试题8:为什么要定义虚析构函数? 6 其他 6 4.3多态原理探究 6 4.3.1 多态的实现原理 6 4.3.2如何证明vptr指针的存在 6 4.3.3构造函数中能调用虚函数,实现多态吗 6 5、纯虚函数和抽象类 6 5.1基本概念 6 5.2抽象类案例 6 5.3抽象类在多继承中的应用 6 5.3.1有关多继承的说明 6 5.3.2多继承的应用场景 6 5.4抽象类知识点强化 6 5.5面向抽象类编程思想强化 6 5.4.1案例:socket库c++模型设计和实现 6 5.4.2案例:计算员工工资 6 5.4.3案例:计算几何体的表面积和体积 6 5.6 C面向接口编程和C多态 6 5.6.1函数类型语法基础 6 5.6.2函数指针做函数参数 6 5.6.3函数指针正向调用 6 5.6.4函数指针反向调用 6 5.6.5.C动态库升级成框架案例 6 5.6.6附录:诸葛亮的锦囊妙计 6

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值