文章目录
1、概念和定义
继承是一种类层次的复用,继承中分为基类(父类)和子类(派生类),父类的一些属性,子类同样可以拥有。
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "zyd";
int _age = 21;
};
class Student : public Person
{
protected:
int _stuid;
};
class Teacher : public Person
{
protected:
int _jobid;
};
int main()
{
Student s;
s.Print();
return 0;
}
可以正常打印。此时s里面就有name, age,和未初始化是随机值的stuid变量。如果是用Teacher实例化出的对象,那就是name,age和未初始化是随机值的jobid变量。
继承有继承的方式,比如上面代码中的public继承
虽然基类的private成员派生类无法访问,但可以用基类中不是私有的函数来调用私有变量。
如果不写访问限定符,那就默认是私有继承。
2、基类和派生类的赋值转换
基类 对象 = 派生类对象(会变成公有继承)
Student s;
Person p = s;
Person& rp = s;
不会发生类型转换。p = s,就是把子类当中父类的部分给赋值过去;而引用,则是子类当中父类那一部分的引用,所以rp可以修改,访问。如果是指针,就指向子类中父类那一部分。
但是现阶段父类对象不能赋值给子类对象。
赋值兼容、切割、切片
B b = d;
B* ptrb = &d;
把子类对象赋值给父类对象,把子类中父类那一部分给过去。无论是引用还是指针还是赋值,都是子类中的父类部分给父类的那个对象。
3、继承中的作用域
看函数重定义
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "zyd";
int _age = 21;
};
class Student : public Person
{
public:
void Print()
{
cout << "重定义" << endl;
cout << _stuid << endl;
}
protected:
int _stuid;
};
int main()
{
Student s;
s.Print();
s.Person::Print();
//Person p = s;
///cout << p._stuid << endl;
return 0;
}
像上面代码那样显式调用父类的函数,而s.Print就是在调用子类中的同名函数,也就是被重定义成的那个函数。
父子类可以有同名成员,不过真实数据由子类决定。想访问父类的内容就得显式一下
cout << Person::_age << endl;
cout << _age << endl;
s.Person::Print();
父子类用重名内容时,就出现了隐藏的现象。
4、派生类中的默认成员函数
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num;
};
int main()
{
Student s;
return 0;
}
只是实例化,但是子类会自动调用父类的成员函数。
那子类自己写一个构造函数,调用自己的行不行?
public:
Student(const char* name, int num)
:_name(name)
, _num(num)
{}
不行。即使显式调用父类的_name,Person::_name也不行。父子类的规定就是父类的成员变量必须用父类的构造函数,可以在子类中写构造函数,但是里面这要有对父类成员变量的初始化就必须用父类的方法。
Student(const char* name, int num)
:Person(name)
,_num(num)
{}
int main()
{
Student s("asdas", 21);
return 0;
}
这时候父类的_name就变成了"asdas",而子类的_num就变成了21。父类的缺省参数也就用不上了。
如果在子类构造函数不写某一个变量的初始化也可以,只要父类能构造就行。
拷贝构造如果没写,那就调用父类的,写的话
Student(const Student& s)
:Person(s)
,_num(s._num)
{}
_num是子类变量。如果这样
Person p = s1;
会调用父类的拷贝构造。并且也只会初始化父类的_name,因为p1是父类对象。只有_name,那在子类中赋值重载一下
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
return &this;
}
所以可以看到,各分各的,有父类的那就调用父类函数,否则就调用的自己的。
现在的代码结果是这样
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(const char* name, int num)//构造
:Person(name)
,_num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)//拷贝构造
:Person(s)
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)//赋值构造
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
cout << "Student(const Student= s)" << endl;
return *this;
}
protected:
int _num;
};
int main()
{
Student s1("asdasd", 21);
Student s2(s1);
Person p = s1;
s1 = s2;
return 0;
}
写上析构函数
~Student()
{
Person::~Person();
cout << "~Student()" << endl;
}
Student s1("asdasd", 21);//只实例化一个对象
必须显式调用谁的析构,因为在C++的规则中,C++要处理多态的情况,析构函数会被处理成destructor,也就是说父类和子类都是destructor,会构成隐藏,所以要显式调用。
会发现多了一个~Person。如果去掉Person:: ~Peroson就正常了,析构函数不要显式调用,编译器会自动调用。
构造时会先构造父,不论什么构造。子类析构函数完成时,会自动调用父类析构函数,保证先子后父。子类有什么函数要运行时,会先进行父类构造,子类构造,然后执行子类的行为,最后子类析构,父类析构。
5、继承与友元
友元不能继承
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
如果要访问,就得再定义一个友元。
6、继承与静态成员
静态变量也不能继承,但由于是静态区的,哪里都可以访问。
class Person
{
public:
Person() { ++_count; }
string _name;
public:
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum;
};
class Graduate : public Student
{
protected:
string _seminarCourse;
};
int main()
{
Person p;
Student s;
cout << &(p._name) << endl;
cout << &(s._name) << endl;
cout << &(p._count) << endl;
cout << &(s._count) << endl;
}
也能从中看出来,虽然子类可以继承父类的公有成员,但是地址不一样。
7、复杂的菱形继承及菱形虚拟继承
单继承是一个子类只有一个直接父类。
多继承是一个子类有两个或以上父类。
菱形继承是多继承的一种特殊情况。
实际上,多继承变得更复杂了,会出现数据冗余和二义性(无法明确访问哪一个)的问题。
虚继承
监视窗口是经过优化的,为了更好的体现虚继承,我们要看内存窗口,并且用内置类型的例子会更适合。
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 d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
面对这样的多继承,我们调试起来,先走到D d这一行
1和2赋值进去后
剩下三个值也赋值进去
从赋值的顺序来看,其实上面两行是B的区域,中间两行是C的区域,最后一行是_d,整体是D的区域。接下来看一下引入虚继承后是什么样的
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
还是一样的区域划分,上两行是B,中两行是C,然后是_d,而A的_a放在一个,而且1呢?其实在调试过程中,1先出现在最后,然后再是2,也就是说无论是B::还是C::,_a的地址都一样。除此之外,B和C中的一些奇怪数字,比如40 ab 9d 00,它们不像是cc cc cc cc的随机数,而像是指针。
为什么会这样?虚继承到底做了什么?那两行地址到底是什么?我们现在再开两个内存窗口看一下它俩。由于我是小端机器,就应当输入0x009dab40和0x009dab48这两个地址,正好倒着输入。输入后前两行就是它们的内容,剩下的都是随机值,会发现这两个地址的第二行处写着14 00 00 00和0c 00 00 00,按照16进制转10进制,也就是20和12。中间相差8,再看一下我们这两个地址相差多少?
40 ab 9d 00
48 ab 9d 00
正好就是8!那么48这一行加上12是谁?也就是02 00 00 00那一行,也就是_a的地址,而40那一行加上20就是_a的地址,所以这就是偏移量!也叫相对距离。
_a既属于B也属于C,虚继承要解决数据冗余和二义性,就只设一个_a的地址。虚继承用过偏移量来辨别是属于那个类的,这也就能解决二义性的问题。这两个地址中,表示偏移量的放在第二行,是因为第一行是留给多态的。类与类之间还有别的问题需要解决,并不只有菱形继承等问题。
看这样的代码
D d;
B b = d;
B* ptrb = &d;
C* ptrc = &d;
这在上面写到过,其实也叫赋值兼容,切片,切割。要把d中B的部分给到b,但是这部分还有A,A在哪?程序需要找到A,这里就需要用着虚继承的偏移量。还有的切片是用指针指向d,也就是上面的后两行,这个和上面所写的一样,还是会把d中B的部分拿出来给到ptrb,但是也是一样的找A的问题,到了ptrc,会找到自己对应的C的那一部分,也需要找到A。
虚继承统一了其他问题。上面是B类的指针指向一个D的对象,然后内存图是整个D的内容。如果B类指针指向一个B的对象,内存图是什么样的?A又如何去找?
B b;
b._a = 10;
b._b = 20;
B* ptrb = &b;
ptrb->_a;
14还在里面,第三行0a也就是_a的地址。这时候A紧贴着B,在它下面。那再看一下第一行里面是什么。
偏移量是8,回到上一个图,0x004FFA04的地址+8就来到了_a的地址,而那个20偏移量还保存着。B没法指向C的对象,所以A要么紧贴着B,要么远离A,而两者的偏移量B都已经有了。
反汇编中也添加了很多指令。
就像这样,虽然指向的对象不同,但是不做区分,都是ptrb->_a++,依靠偏移量就可以区分。
毫无疑问,虚继承解决了二义性问题,那么数据冗余呢?其实也解决了。现在的例子中,新增了两个指针,也就是B和C中的第一行地址,但是只减少了一个A,如果A变得更大,那么增加几个指针就不是什么代价了。以前是每个类都得有个A,而现在只有一个A。那么新增的指针指向的空间算不算代价?也不是,如果是多个D的对象,对A的偏移量都相同,那么共用一块指向的空间就行。
如果A有多个成员变量,那么通过偏移量到达A的第一个变量后,如果不是要访问的,那就按照内存对齐去找要访问的。
写代码时最好不要写菱形继承,多继承可以写,但是菱形继承难以把握住。
8、组合
但如果C有保护或者私有成员,D就不能有了。D可以直接用C的公有成员,间接用保护或私有成员。耦合度来讲组合不如继承,但组合更自由,修改C的保护或者私有成员不会影响到D,修改A的成员有可能全部都影响到B。但继承不可少,面向对象语言的三大特性中就有继承,其中的多态也是基于继承而存在的。
结束。