继承的概念和定义
继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
简单地举个例子,比如图书馆或者大学刷脸进出校园,人固有的相同属性:名字 年龄 电话号码 住址等。
如果要分别定义不同类去存储共有特征,是不是太浪费空间了,在大学教师和学生区别 顶多是学号和教职工号的区别。
最快成为富二代的办法是什么?是继承他爸爸的财产,是最轻松、最容易的,因此这种方法叫继承
在国外父类也叫基类 子类叫做派生类。
class Person
{
public:
void print()
{
cout << _name << _age<< endl;
}
protected:
string _name="张麻子";姓名
int _age = 30;年龄
};
class Student :public Person
{
protected:
int _stuNum;学号
};
class Teacher :public Person
{
protected:
int _jobid;教师工号
};
int main()
{
Student s;
s.print();
Teacher t;
t.print();
return 0;
}
Student实际有三个成员变量 一个是自己独有的学号和从父类继承来的名字和年龄,而且也有成员函数(从父类继承来的),同理Teacher这个类也是一样的。
通过调试方式和运行结果,更能直观感到继承的魅力。
继承的定义
定义格式
继承关系和访问限定符
继承基类成员访问方式的变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类private成员 |
基类的protected成员 | 派生类的public成员 | 派生类的protected成员 | 派生类private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结 :
1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
什么意思呢?看代码说话
class Person
{
public:
void print()
{
cout << _name << _age<< endl;
}
//protected:
private:
string _name="张麻子";//姓名
int _age = 30;//年龄
};
class Student :public Person
{
public:
void f()
{
cout << _name << _age << endl;
}
protected:
int _stuNum;//学号
};
int main()
{
Student s;
s.print();
return 0;
}
会报错,因为父类的成员变量变成了私有,子类在自己类中访问不了。
相当于你要买房子,首付要50w,你爸爸立马给了40w,剩下的10w,让你自己去想办法。
但子类可以间接去访问父类的私有成员变量
这又是为什么呢?因为你发现爸爸除了正常工资还有一部分隐藏花销,供着两大爱好:抽烟和钓鱼,是什么支撑这两大情怀呢?当然money的魔力了,你发现爸爸还有私房钱,但他只会供自己的爱好去消费,但突然有一天,你说你也想去钓鱼,那么他会帮你准备好鱼竿、鱼饵,还有上好的烟。
class Person
{
public:
void print()
{
cout << _name << _age<< endl;
}
//protected:
private:
string _name="张麻子";//姓名
int _age = 30;//年龄
};
class Student :public Person
{
public:
/*void f()
{
cout << _name << _age << endl;
}*/
protected:
int _stuNum;//学号
};
int main()
{
Student s;
s.print();
return 0;
}
2.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
如果想在子类可以访问和使用就用protected或者public即可
class Person
{
public:
void print()
{
cout << _name << _age<< endl;
}
protected:
//private:
string _name="张麻子";//姓名
int _age = 30;//年龄
};
class Student :public Person
{
public:
void f()
{
cout << _name <<" "<< _age << endl;
}
protected:
int _stuNum;//学号
};
int main()
{
Student s;
s.print();
s.f();
return 0;
}
3.实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public >protected> private.
class Person
{
public:
void print()
{
cout << _name << _age<< endl;
}
protected:
//private:
string _name="张麻子";//姓名
int _age = 30;//年龄
};
class Student :protected Person
{
public:
void f()
{
cout << _name <<" "<< _age << endl;
}
protected:
int _stuNum;//学号
};
int main()
{
Student s;
s.print();
return 0;
}
因为父类的成员是protected Student类的继承方式是protected 两个protected相遇是protected
父类的公有部分遇子类的继承是protected public>protected 变protected 所以在类外访问不了。
为什么teacher可以在类外访问呢?
protected遇见public 变protected public遇public还是public 所以Teacher可以在类外访问。
4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式。
5.在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
一般都是父类成员 公有/保护 子类继承 用公有继承
基类和派生类对象赋值转换
这样为什么会报错呢?因为在int到double的隐式类型转换中,会产生一个临时变量,临时变量具有常属性,而直接引用相当于权限的放大,为了编译时候语法不报错,要在double前面加上const,形成权限的平移。
派生类对象 可以赋值给 基类的对象/基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
基类对象不能赋值给派生类对象。
class Person
{
public:
void print()
{
cout << _name << endl;
}
protected:
string _name;
private:
int _age;
};
class Student :public Person
{
protected:
int _stuNum;
};
int main()
{
Student s;
Person p = s;
是子类继承父类 切割父类那一部分的引用
Person& pp = s;
指针指向的是子类继承父类的那一部分
Person* ptr = &s;
s = p;//error
return 0;
}
但要注意的是,在派生类把父类切割的那部分赋值过去时,无论是赋值还是引用、指针都不会产生临时变量,是直接赋值过去,中间没有其他特殊情况。
切割或切片 子类->父类
赋值兼容转换 相当于语法特殊处理 中间转换没有产生临时变量。
继承中的作用域
在继承体系中基类和派生类都有独立的作用域
你觉得打印函数会打印出输的_id?是父类还是子类?
class Person
{
public:
void print()
{
cout <<_name << endl;
}
protected:
string _name;
int _id = 24;
};
class Student :public Person
{
public:
void print()
{
cout << _id << endl;
}
protected:
int _stuNum;
int _id = 12;
};
int main()
{
Student s;
s.print();
return 0;
}
优先打印子类_id 就近原则
打印的是子类的_id,这表明了父类和子类各种有独立的作用域。
那怎么打印父类的_id呢?前面加个父类名+域作用限定符就可以了。
class Person
{
public:
void print()
{
cout <<_name << endl;
}
protected:
string _name;
int _id = 24;
};
class Student :public Person
{
public:
void print()
{
cout << _id << endl;
cout << Person::_id << endl;
}
protected:
int _stuNum;
int _id = 12;
};
int main()
{
Student s;
s.print();
return 0;
}
隐藏关系
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)->" << i << endl;
}
};
int main()
{
B b;
b.fun();
return 0;
}
这里为什么会报错呢?因为子类和父类如果存在同名成员函数,那么父类的同名函数就会被隐藏,编译器只能看到子类的同名成员函数,所以只能传参使用。
注意只要是成员函数名相同即可,返回值和参数可以不相同。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)->" << i << endl;
}
};
int main()
{
B b;
//b.fun();//被隐藏
b.fun(5);
b.A::fun();
return 0;
}
总结:子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)。
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
注意在实际中在继承体系里面最好不要定义同名的成员。
派生类的默认成员函数
其实跟之前学过的类和对象类似,也有构造 析构 拷贝...那么这些函数在基类与派生类之间的关系又是怎么样的呢?让我用代码来解释下。
class Person
{
public:
Person(const char*name="张麻子")
:_name(name)
{
cout << " Person(const char*name)" << endl;
}
Person(const Person& p)
:_name(p._name)
{
cout << "Person(const char& p) " << endl;
}
Person& operator=(const Person& pp)
{
if (this != &pp)
{
_name = pp._name;
}
cout << " Person& operator=(const Person& pp)" << endl;
return *this;
}
~Person()
{
cout << "~Person() " << endl;
}
protected:
string _name;
};
class Student :public Person
{
protected:
int _stuNum;
};
int main()
{
Student s;
Student s1(s);
s1 = s;
return 0;
}
当派生类没有自己写的构造 拷贝等等函数时,它会自动去调用默认生成的函数(基类的函数),很合理吧,因为他是他儿子,使用爸爸的钱很合情合理。
那么为什么拷贝构造和赋值重载的参数是const Person&p 和&pp呢,前面讲过了,赋值还是引用、指针,都是派生类继承父类那一部分的切割 因此作为函数参数很合理。
那么如果子类想自己写这些默认成员函数该怎么写呢?
子类构造函数
class Student :public Person
{
public:
Student(const char*name,int num)
:_name(name)
,_stuNum(num)
{}
protected:
int _stuNum;
};
int main()
{
Student s;
return 0;
}
但是报错了,这是为什么呢?因为父类的构造函数被隐藏了,那怎么解决呢?你们可能会说前面不是说加上域作用限定符就行了吗?但这里不只是那么简单,加上父类+域作用限定符不能解决这个问题,甚至我们去掉这个_name构造都能编译通过。
为什么Student实例化时_name给的是张三,但在调试时为什么_name是张麻子呢?因为我们在父类构造函数时,_name给的参数是缺省参数,如果去掉父类的缺省参数就会报错。
class Person
{
public:
//Person(const char*name="张麻子")
Person(const char* name)
:_name(name)
{
cout << " Person(const char*name)" << endl;
}
Person(const Person& p)
:_name(p._name)
{
cout << "Person(const char& p) " << endl;
}
Person& operator=(const Person& pp)
{
if (this != &pp)
{
_name = pp._name;
}
cout << " Person& operator=(const Person& pp)" << endl;
return *this;
}
~Person()
{
cout << "~Person() " << endl;
}
protected:
string _name;
};
class Student :public Person
{
public:
Student(const char*name,int num)
//:_name(name)
:_stuNum(num)
{}
protected:
int _stuNum;
};
int main()
{
Student s("张三",20);
return 0;
}
那么怎么解决这个问题呢?最好的方法就是显示调用父类。
class Person
{
public:
//Person(const char*name="张麻子")
Person(const char* name)
:_name(name)
{
cout << " Person(const char*name)" << endl;
}
Person(const Person& p)
:_name(p._name)
{
cout << "Person(const char& p) " << endl;
}
Person& operator=(const Person& pp)
{
if (this != &pp)
{
_name = pp._name;
}
cout << " Person& operator=(const Person& pp)" << endl;
return *this;
}
~Person()
{
cout << "~Person() " << endl;
}
protected:
string _name;
};
class Student :public Person
{
public:
Student(const char*name,int num)
//:_name(name)
:_stuNum(num)
//显示调用父类
,Person(name)
{}
protected:
int _stuNum;
};
int main()
{
Student s("张三",20);
return 0;
}
但在调试时发现,无论初始化顺序怎么写,都是先初始化父类,再初始化子类
初始化父类->子类
相当于先声明先初始化,因为Student继承是Person 相当于声明 就先初始化父类咯。
一般情况下使用父类的拷贝构造就可以,如果Student想自己深拷贝该怎么写呢?
这里有个问题就是怎么把person里面student继承的那部分调出来呢?
前面讲过&s就是student继承person切割出来的那部分 所以&s就是person里面的_name
子类深拷贝构造
class Person
{
public:
//Person(const char*name="张麻子")
Person(const char* name)
:_name(name)
{
cout << " Person(const char*name)" << endl;
}
Person(const Person& p)
:_name(p._name)
{
cout << "Person(const char& p) " << endl;
}
Person& operator=(const Person& pp)
{
if (this != &pp)
{
_name = pp._name;
}
cout << " Person& operator=(const Person& pp)" << endl;
return *this;
}
~Person()
{
cout << "~Person() " << endl;
}
protected:
string _name;
};
class Student :public Person
{
public:
Student(const char*name,int num)
//:_name(name)
:_stuNum(num)
//显示调用父类
,Person(name)
{
cout << " Student(const char*name,int num)" << endl;
}
//深拷贝构造
Student(const Student& s)
:Person(s)
,_stuNum(s._stuNum)
{
cout << " Student(const Student& s)" << endl;
}
protected:
int _stuNum;
};
int main()
{
Student s("张三",20);
Student s1(s);
return 0;
}
子类的赋值运算符重载
运行时可以运行,但程序结束时返回的代码为负数,就说明有问题,那么到底出了什么问题?
因为派生类和基类的函数名相同,构成了基类函数隐藏,调用不到基类的赋值。所以赋值函数一直在派生类调用自己的赋值,为了解决这个问题,要声明基类的类名+域作用限定符即可。
子类的析构函数
为什么报错了呢?因为又再次的构成了隐藏,子类和父类的析构构成了隐藏。
由于后面多态的原因,析构函数被特殊处理,后面函数名都会被处理成destruction()。
为什么父类析构了两次?
而屏蔽掉之后就刚刚好?
因为为了先析构子类再析构父类,一般都是先析构子类,编译器再自动去调用父类的析构,与构造相反。
为什么要先析构子类再析构父类呢?
因为如果先析构父类再去析构子类,会存在安全隐患。
例如 父类是指针 先析构了父 释放了那段空间 指针变成了野指针,后面子类需要去访问父类数据时 去访问的空间就变成了野指针,可能父类资源已经释放处理了 ,子类析构函数去访问 存在野指针等安全风险。
在类和对象中 默认生成构造函数中 对内置类型不处理 自定义类型去调用它的默认构造
在派生类中也适用: 派生类去调用基类的构造函数。
总结:派生类默认成员函数规则,其实跟之前类似,唯一不同的是,不管是构造初始化/拷贝/析构,多了父类那一部分,原则:父类那部分调用父类的对应函数完成。
继承和友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
相当于你爸爸的朋友并不是你的朋友。
class Student;
class Person
{
public:
friend void reveal(const Person& p, const Student& s);
protected:
string _name;
};
class Student :public Person
{
protected:
int _age;
};
void reveal(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._age << endl;
}
int main()
{
Person p;
Student s;
return 0;
}
友元函数相当于爸爸的朋友,如果你想认识爸爸的朋友,就得爸爸的引荐。
class Student;
class Person
{
public:
friend void reveal(const Person& p, const Student& s);
protected:
string _name;
};
class Student :public Person
{
public:
friend void reveal(const Person& p, const Student& s);
protected:
int _age;
};
void reveal(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._age << endl;
}
int main()
{
Person p;
Student s;
return 0;
}
继承和静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。 因为static静态变量在静态区(全局变量,程序结束才销毁)。
class Person
{
public:
Person()
{
++_count;
}
protected:
string _name;
public:
static int _count;
};
int Person::_count = 0;
class Student :public Person
{
protected:
int _stuNum;
};
int main()
{
Person p;
Student s;
cout << p.Person::_count << endl;
cout << s.Person::_count << endl;
return 0;
}
复杂的菱形继承及菱形虚拟继承
单继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
多继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承 。
菱形继承
菱形继承:菱形继承是多继承的一种特殊情况。
class Person
{
public:
string _name;
};
class Student :public Person
{
protected:
int _stuNum;
};
class Teacher :public Person
{
protected:
int _jobid;
};
class Assistant :public Student, public Teacher
{
protected:
int _majorCourse;
};
int main()
{
Assistant as;
as._name="张麻子";
return 0;
}
报错了,为什么呢?因为存在二义性。
菱形继承会有数据冗余和二义性,因为里面有两份Person_name)(浪费空间),当指定_name时就会存在二义性,不知道应该指向student还是teacher(访问不明确)。
解决办法:就是类名+域作用限定符
这样写确实解决了二义性问题,但数据冗余还没有得到解决,属于是捡了芝麻丢了西瓜。
最好的办法就是在继承方式面前加virtual,变成虚继承。
class Person
{
public:
string _name;
};
class Student :virtual public Person
{
protected:
int _stuNum;
};
class Teacher :virtual public Person
{
protected:
int _jobid;
};
class Assistant :public Student, public Teacher
{
protected:
int _majorCourse;
};
int main()
{
Assistant as;
//as._name="张麻子";
as.Student::_name = "张麻子";
as.Teacher::_name = "鹅城";
return 0;
}
不是所有继承类加上virtual,是直接继承父类的子类 子类的继承方式前面才加virtual。
虚拟继承解决数据冗余和二义性的原理。
虚拟继承原理
为了研究虚拟继承的原理,我们给出一个简单菱形继承继承体系,再借助内存窗口观察对象成员的模型。
先来看正常的菱形继承(无virtual)
class A
{
public:
int _a;
};
class B :public A
{
public:
int _b;
};
class C :public A
{
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
int main()
{
D dd;
dd.B::_a = 1;
dd.C::_a = 2;
dd._b = 3;
dd._c = 4;
dd._d = 5;
return 0;
}
加上virtual以后
class A
{
public:
int _a;
};
class B :virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
int main()
{
D dd;
dd.B::_a = 1;
dd.C::_a = 2;
dd._b = 3;
dd._c = 4;
dd._d = 5;
return 0;
}
虚继承的内存窗口,大家有没有发现什么猫腻?
首先因为虚继承的缘故,不会出现数据冗余,因此1和2是在同一个内存地址存储的,第二个就是B和C有一段内存非常相似,因为VS2019的内存存储是小端存储,因此B的内存地址应该0x007f7bdc
C的内存地址是0x007f7be4
这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指 向的一张表。这两个指针0x007f7bdc 0x007f7be4 叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量 可以找到下面的A。
内存存储是16进制,C——14 D——12
0x003DF73C + 14 =0x003DF750 刚好是A的位置 B->A
0X003DF744 + 12 = 0x003DF750 又刚好是A的位置 C->A
里面存的是偏移量,B到A的偏移量,C到A的偏移量
什么时候会用到这个偏移量呢?
在切割对象时就可以通过偏移量去找到A,因为虚继承的缘故,B对象或者C对象并没有直接存A
而是把A放到一个公共区域,pb对象可能会指向B对象或者D对象的切割。
运行时,通过偏移量区找A的数据,pb->c pb->d 都可以找到A。
再比如结构体访问数据时,对象中多个成员的存储顺序是按照声明顺序确定。
比如结构体中有3个不同类型数据,p->a,p->b,p->c,通过对象顺序和内存对齐规则,在编译时计算好内存位置,进行访问。
大多数代码是编译时就计算好了内存的位置的偏移量。只有虚继承才在运行时去偏移量区找数据。
因此我们不要去写菱形虚拟继承,我们把握不住!要进过不断地看书学习,才能有能力把握菱形虚拟继承。
我们经常使用的i/o(输入和输出)流也是菱形虚拟继承。