C++ 继承

前言:不要做代码观察师!让代码跑起来,学习更简单

设计的小实验涉及大量代码,喜欢无码的不建议食用 (virtual关键字不细说)

如果有错误,还请大佬指正

继承

为了提高代码的复用性,继承应运而生。
继承允许我们沿用基类中定义的变量和函数,这是继承最主要的目的。

一些常出现的名词:

	父类:基类
	子类:派生类
	People 派生 出Student, Student 继承 自People

继承代码怎么写

class People{};
class Student:People {};

子类使用 : 指定父类,简单的继承就完成了

这里People是父类(基类),Student是子类(派生类)


继承有什么用?
我们从下面两个小实验要说明继承有什么用
1. 复用父类变量

class People {
public:
    string name = "人";
    int age = 18;
};

class Student : People {
public:
    void printInfo() {
        cout << "name:\t" << name << endl;
        cout << "age:\t" << age <<endl;
    }
};

int main(){
	Student st;
	st.pritInfo();
}
//输出结果:
//name:   人
//age:    18

这就完成了对父类定义的变量的复用

类中写了一个public,关于访问权限,通俗地说:

private:	只能在 1.该类中使用、			 2.其友元函数使用。
protected:	可以在 1.该类中使用、2.子类中使用、3.其友元函数使用。
public:	可以在 1.该类中使用、2.子类中使用、3.其友元函数使用、4.类外使用。

而class的默认访问权限是private的,如果没有显式声明访问权限,则所有成员都是private

而显示声明后,作用范围是从这个声明后到下个声明前,如果没有下个声明,则作用范围从当前声明开始到函数结束

class A{
public:
	作用范围
private:
	作用范围
}

2. 复用父类函数

class People {
public:
    string name = "人";
    int age = 18;
    //将函数移动到People中
    void printInfo() {
        cout << "name:\t" << name << endl;
        cout << "age:\t" << age << endl;
    }
};

class Student : public People {
};

int main(){
	Student st;
	st.pritInfo();
}
//输出结果:
//name:   人
//age:    18

这完成了对父类定义的函数的复用

类的访问权限

这里写了class Student : public People,是告诉编译器,当把父类中的成员抄写到子类时,子类中对应的抄写成员的访问权限

对应的表查看下方:在这里插入图片描述

权限:public > protect > private (为了方便理解,将private定义为低权限)
两种访问权限声明取较低权限的一项作为继承成员的访问权限

默认的继承方式是private,如果上面的代码写成class Student : People那么编译器会报错,提示People::printInfo()不可访问。因为继承的时候将访问权限设置为了private,private时不能在类外使用。


内存分布

父类会被抄写到子类的上方,并将this指针指向最上方的内存。如果类中存在变量则会指向类的第一个变量,如果有virtual函数的声明则this指向虚函数表(因为虚函数表放在第一个)。

在这里插入图片描述


我们如何验证这种内存分布呢

class People {
public:
    int i1 = 1;
    int i2 = 2;
};

class Student : public People {
public:
    int i3 = 3;
    int i4 = 4;
    Student* getThis(){ return this; }
};
int main(){
	Student st;
	int* pi = (int*)st.getThis();//将指针pi指向st的this
	/* 这句代码等价于上面那一句,但为了严谨,我写出了上面那一句
	 * 为了方便,从下面开始,我将写成下面那种形式
	 * int* pi = (int*)(&st);*/
    for (int i = 0; i < 4; i++) {
        cout << *pi << endl;
        pi++;
    }
}
//输出结果:(这样的打印结果我会写作一排,下面也会如此)
//1 2 3 4

请忘掉之前学地那些规矩,否则这个代码一定会使你倍感痛苦

(int*)st.getThis()这个代码的意思是,我将采用读取 int[ ] 的方式来读取这个类的空间中的内容

在这里插入图片描述


这种方法也能访问到私有变量,我们可以用这个办法验证私有变量也会被继承

class People {
private: 	int i1 = 1;
protected: 	int i2 = 2;
public: 	int i3 = 3;
};
class Student : public People {
private: 	int i4 = 4;
protected: 	int i5 = 5;
public:		int i6 = 6;
};
int main(){
	Student st;
    int* pi = (int*)&st;
    for (int i = 0; i < 6; i++) {
        cout << *pi << endl;
        pi++;
    }
}
//输出结果:
//1 2 3 4 5 6

这段代码我们证明了即使时私有成员,也会被继承下来,并放入当变量空间中,这比普通验证方式更加直接,因为我们验证了它确实被放入类的空间中了

编译器不让我们直接访问私有变量,但我们可以通过一些特殊手段进行访问(但并不建议这么做,这破坏了类的封装性)

在这里插入图片描述


成员函数的本质其实是在类作用域范围之内的普通函数

class People {
public:
    void func() { cout << endl; }//写一些内容防止被优化没了
};

class Student : public People {
public:
    void func1() { cout << endl; }
};
int main(){
	Student st;
    char* pi = (int*)&st; //取byte*,因为当类为空时,大小为1
    cout << sizeof(st) << endl; //如果成员函数放在类空间里,那么一个函数指针的空间时4或8
    cout << hex << *((int*)pi) << endl; //读取一个DWORD的内容
    cout << hex << *((int*)(++pi)) << endl;//尝试向下取一个地址的内容
    printf("%p\n", &People::func); //这里不能写cout << hex << &People::func << endl;
    printf("%p\n", &Student::func); //否则会打印1
    printf("%p\n", &Student::func1);
}
//输出结果:
//1
//cccccccc
//cccccccc
//006815C8
//006815C8
//006815B9

这里可以看到,成员函数并不是直接放在类空间里的。这和我们定义的普通函数一样,时随意找一个空白地方放的。

(cout << hex : 以十六进制的形式输出)


同名变量

我们将验证:父类同名变量将被隐藏(hiding),而不是直接覆盖掉

class People {
public:
    int i1 = 1;
    int i2 = 2;
};

class Student : public People {
public:
    int i1 = 3;
    int i2 = 4;
};

int main() {
    int main() {
    Student st;
    cout << st.i1 << endl;
    cout << st.i2 << endl;
    int* pi = (int*)&(st);
    cout << *pi << endl;
    cout << *(++pi) << endl;
    //cout << ((People)st).i1 << endl; //这种写法也能达到上面的效果
    //cout << ((People)st).i2 << endl;
}
}
//输出结果:
//3 4  1 2

这证明同名变量会 被隐藏,但不会被覆盖掉,父类的变量被完整地保留下来了(private的也一样会被保留下来,可以自己试试,都写出来篇幅太大了)


同名函数 (hiding)

中文名:隐藏
原来的函数并没有被覆盖掉,它依然存在,只是被隐藏了。
为什么不是重载?重载的定义是在同一作用域下,对同名的函数,写出不同的参数的版本
顺便一提,重写:虚函数才有的定义,父类的同名虚函数被丢弃了,重新写一个

class People {
public:
    void func() { cout << "people func" << endl; }
};

class Student : public People {
public:
    void func() { cout << "student func" << endl; }
};
int main() {
    Student st;
    st.func();
    ((People)st).func();
}
//输出结果:
//student func
//people func

优先调用了当前实例声明的类型(Student)的func
(int i那么当前声明的类型就是int)


这里有个坑:

同名函数会隐藏所有父类的同名函数,即使子类只有一个同名函数

class A {
public:
    void func(int i) { cout << "A func1" << endl; }
    void func(int i, int j) { cout << "A func2" << endl; }
};
class B : public A{
public:
    void func(int i) { cout << "B func1" << endl; }
    //void func(int i, int j) { cout << "B func2" << endl; }//这个我们不声明
};
void main() {
    B b;
    b.func(1);
    //b.func(12, 23);//这居然是错的,因为这个函数没有声明
}

编译器在查找func的时候,当前作用域(当前类)有,编译器直接摆烂,不找了。

所以一定要小心,当出现子类出现和父类同名的函数的时候,子类会隐藏所有父类的同名函数。

解决办法:

//我们可以在调用时显式写出作用域
int main(){
	......
	b.A::func(12, 23);
}
//或者在class B中写上这个,表示我们要在B使用A::func函数
class B : public A{
	......
    using A::func;
};
//b.func(1,2);//这时就能正确调用了

构造函数和析构函数

class People {
public:
    People() { cout << "People 构造函数" << endl; }
    ~People() { cout << "People 析构函数" << endl; }
};
//记得后序的类也要写 继承方式,否则将以private方式继承
class Student : public People, public Body {
public:
    Student() { cout << "Student 构造函数" << endl; }
    ~Student() { cout << "Student 析构函数" << endl; }
};

int main(){
	Student st;
}
//输出结果:
//People 构造函数
//Student 构造函数
//Student 析构函数
//People 析构函数

这里可以看到,构造函数先打印People后打印Student。但实际上是先调用了子类的构造函数,在子类构造函数执行前调用了父类地构造函数。

析构刚好相反,析构函数先打印Student后打印People。但实际上是先调用了父类的析构函数,在父类析构函数执行前调用了子类的析构函数。

具体细节可以在调用时使用断点查看调用堆栈


虽说子类会调用父类的构造函数,但是也仅是会自动调用父类的默认构造函数罢了

若是使用了其他版本的构造函数,调用的也是父类的默认构造函数。

int index = 0;//加入index看得更清晰
class People {
public:
    People()            { cout << index << ": P0" << endl; }
    People(People& p)   { cout << index << ": P1" << endl; }
};

class Student : public People {
public:
    Student()           { cout << index << ": S0" << endl; }
    Student(Student& s) { cout << index << ": S1" << endl; }
    Student(int i)      { cout << index << ": S2" << endl; }
};
  
void test33() {
    Student s1;
    index++;
    Student s2(s1);
    index++;
    Student s3(3);
}
//打印结果: 0:P0 S0   1: P0 S1  2: P0 S2

所以想要在子类构造函数里 调用父类构造函数的其他版本,就得显式地写出来

Student(Student& s) : People(s){ //正确的写法应当写在这里
   cout << index << ": S1" << endl;
   //People::People(s); //错误的写法,在此之前还是调用了People的默认构造函数
}

用父类的类型 存放 子类实例

补充一点内容:

将上面代码的mian函数稍作修改 (如果全复制过来的话有点多余)

void main() {
    People* stu = new Student;
    delete stu;
}
//输出结果:
//People 构造函数
//Student 构造函数
//People 析构函数

这时我们会发现,Student的析构函数没有被调用

这时什么情况呢?
再看一个例子

class People {
public:
    int i = 1234;
};

class Student : public People {
public:
    int j = 1111;
};
void test() {
    People* stu = new Student;
    stu->i; //正确,People能够管理变量 i
    //stu->j; //错误,class People没有成员 j
    cout << sizeof(People) << endl;
    cout << sizeof(stu) << endl;
    cout << sizeof(Student) << endl;
    delete stu;
}
//输出结果:
//4 4 8

这里stu的空间为4,而Student类空间为8,请记住这点

还记得我们之前说过的空间分布吗?People会写在最上面,而子类会写在下面。如果我们使用这种方式来创建一个People,那么People能管理到的空间只有4,而实际上我们能用的地址有8。如果使用cout << ((Student*)stu)->j << endl;这样的方法来打印j,那么依旧能够打印出1111

这也就是说,People摆烂了,儿子的东西他不管了。儿子免费啦,但儿子的东西仍然存在。
理所当然的,父亲没有义务帮儿子打扫房间里的垃圾。

  • 父类不能管理子类的空间
  • 父类不会调用子类的析构函数,因为那段空间根本不归父类管。

编译器很难知道儿子到底能有多少东西,与其乱管,不如不管。

那么有什么办法可以然父类帮忙调用子类的析构函数,实现帮儿子扫房间呢?

儿子都求我我,就勉为其难地答应了吧
当然是有的,就是在析构函数之前加virtual关键字,这里仅仅提一下,在一下篇C++多态中,会进行详细讲解。
不用多态的情况下,应该没有什么理由写出这样的代码,申请更多的空间,但并不去使用它。就像是我买了个盆吃饭,但我每次只盛一勺,多余的空间都浪费掉了,况且盆吃饭并不方便。


再补充亿点:

之前说过,对于同名函数,总是会优先调用实例当前声明的类型的同名函数

class People {
public:
    void func() { cout << "people func" << endl; }
};

class Student : public People {
public:
    void func() { cout << "student func" << endl; }
};
int main() {
    People* stu = new Student;
    stu->func();
    ((Student*)stu)->func();
    //stu->Student::func(); //错误,作用域仅可指定父类成员,父类对子类无效
}
//输出结果:
//people func
//student func

请务必注意这一点,否则程序很有可能会出错


多继承

多继承也很简单
只要在 : 后使用 , 隔开每一个继承的类即可
例如 class Student : public People, Body,这里,后没有写public,这将会使得Body是以private的方式继承

class People {
public:
    People() { cout << "People 构造函数" << endl; }
    ~People() { cout << "People 析构函数" << endl; }
};

class Body {
public:
    Body() { cout << "Body 构造函数" << endl; }
    ~Body() { cout << "Body 析构函数" << endl; }
};

class Student : public People, public Body { //多继承
public:
    Student() { cout << "Student 构造函数" << endl; }
    ~Student() { cout << "Student 析构函数" << endl; }
};

int main(){
	Student st;
}
//输出结果:
//People 构造函数
//Body 构造函数
//Student 构造函数
//Student 析构函数
//Body 析构函数
//People 析构函数

多继承空间分布

class People {
public:
    int i1 = 1;
    int i2 = 2;
};

class Body {
public:
    int i5 = 5;
    int i6 = 6;
};

class Student : public People, Body {
public:
    int i3 = 3;
    int i4 = 4;
};
int main(){
	Student st;
    int* pi = (int*)&(st);
    for (int i = 0; i < 6; i++) {
        cout << *pi << endl;
        pi++;
    }
}
//输出结果:
//1 2  5 6  3 4

可以画出空间分布图:
在这里插入图片描述
以此类推,如果还有更多的继承类,那么按照从左到右的顺序抄写到内存中,最后再写入子类。

但一般不建议写出多继承
至于为什么,请听下回分解(C++ 多态)

总结

  • 继承是为了实现代码的复用
  • private和protected的成员和public的成员一样,会被子类继承
  • 一般我们使用public的方式继承,但默认的继承方式是private (不同继承方式的差别请查看图表)
  • 如果有同名的方法或属性,则优先使用当前类型的方法或属性(当前类型就是写在最前面的,声明的这个变量的类型),但这不是直接覆盖掉了原有的方法或属性,只是将其隐藏(暂时不考虑virtual关键字)
  • 继承类的空间分布:继承列表中的类从左到右分别依次抄写到子类的上方,最下方才写入子类,this指针则指向头部

如果有什么想法的,或者想喷我的(有人喷也会有成长),欢迎评论区留言

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值