前言:不要做代码观察师!让代码跑起来,学习更简单
设计的小实验涉及大量代码,喜欢无码的不建议食用 (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指针则指向头部
如果有什么想法的,或者想喷我的(有人喷也会有成长),欢迎评论区留言