C++学习记录——십칠 继承


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。但继承不可少,面向对象语言的三大特性中就有继承,其中的多态也是基于继承而存在的。

在这里插入图片描述

结束。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值