C++继承

本文详细介绍了面向对象编程中的继承概念,包括继承的原理、使用场景、访问权限控制、构造函数和析构函数的行为,以及如何处理静态成员和菱形继承带来的问题。重点讲解了如何通过virtual关键字解决多继承中的二义性和数据浪费问题。
摘要由CSDN通过智能技术生成

继承的概念

        继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象 程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用, 承是类设计层次的复用。
        需求:许多子类想共用父类的成员时,为了简化代码(不让每个子类写同样的成员),就有了继承,通过类访问限定符对成员的划分,我们可以将父类的一些成员(成员变量+成员函数)继承到子类,使得子类也可以使用。
        例子:对于学校中的学生信息与老师信息,我们需要进行管理,创建Student类管理学生,Teacher类管理老师,对于这两个公有的成员比如学生与老师的姓名,年龄等,我们将其写在另外一个类Member类中作为父类,使得子类可以使用父类的成员,而对于老师的工号与学生的学号不同,我们把它们分别写在各自的类中。
父类/基类
class Member
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}

protected:
	string _name = "XiaoMing";//姓名
	int _age = 18;//年龄
};

子类/派生类

class Teacher : public Member
{
protected:
	int _jobid;//工号
};

子类/派生类

class Student : public Member
{
protected:
	int _stuid; // 学号
};
父类Member的成员(成员函数+成员变量)都会变成子类的一部分。这里Student和Teacher复用了Person的成员。

继承的定义

定义格式

基类与普通类的格式一致。

派生类与普通类稍有差别,主要是类名后面加上了继承方式和父类,具体定义格式如下。

 继承关系与访问限定符

        对于派生类能访问基类的哪些成员,实际上需要上面的继承方式访问限定符的组合,3×3=9种可能的情况。

        这里给出一个顺序:public > protected > private

        对于派生类中能否访问基类成员,我们给出这样一条规律:子类继承方式父类访问限定符较小的那一个作为访问方式。

        注意:所有的成员都可以被继承,但是继承后的访问关系是有规则的。

例子:

理解:

  • 对于基类的private成员,我们在派生类中无论以什么方式继承都是不可见的(可以被继承但是不能被访问)。
  • 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出protected成员限定符是因继承才出现的

总结:

        在实际应用中,用的最多的是基类访问限定符是public,protected,派生类的继承方式是protected方式。

基类和派生类对象赋值转换

        派生类对象 可以赋值给 基类的对象/指针/引用,反过来不行 
Student s;
Member m1 = s;
Member* m2 = &s;
Member& m3 = s;
        这里有个形象的说法叫切片或者切割。意为把派生类中基类那部分切来赋值过去。

它的原理是这样的。

        对于不同类型的变量赋值时会进行类型转换,产生临时变量。 而对于派生类对象赋值给基类对象时不会产生临时对象,通过切片的方式直接进行赋值。
int i=10;
double d = i;//产生了临时拷贝,进行类型转换

Student s;
Member m=s;//没有产生临时拷贝
//通过切片的方式把s对象中含有的基类成员变量直接赋值给基类对象m

继承中的作用域

        在继承体系中基类 派生类 都有 独立的作用域,就是它们类名形成的作用域 。 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫 隐藏 也叫 重定义
        在子类成员函数中,可以使用 基类::基类成员 显示访问。
例子
class Member
{
protected:
	string _name = "XiaoMing";//姓名
	int _age = 18;//年龄
	int _id = 123;
};

class Student : public Member
{
public:
	void Print()
	{
		cout << "_id:" << _id << endl;
		cout << "Member::_id:" << Member::_id << endl;//通过指定类域访问基类变量
	}
protected:
	int _id = 1111; // 学号
};

int main()
{
	Student s;
	s.Print();
	return 0;
}
结果:
注意:如果是成员函数的隐藏,只需要函数名相同就构成隐藏。 在实际中在继承体系里 面最好 不要定义同名的成员

派生类的默认成员函数

构造函数

默认的构造函数

        派生类的成员:1.内置类型不做处理 2.自定义类型去调他的构造函数(和普通的类一样!)

        派生类中的基类成员:去调基类的构造函数(与普通类的区别!)

例子:

class Member
{
protected:
	string _name;//姓名
	int _age;//年龄
};

class Student : public Member
{
public:

private:
	int _id;
};

int main()
{
	Student s;
	return 0;
}

        构造Student对象,对于他自己(派生类)的成员,调用其默认的构造函数,即1.内置类型不做处理(int类型的_id初始化为随机值) 2.自定义类型去调他的构造函数(此处没有)

        对于基类的成员(string类型的_name和int类型的_age),去调其基类的构造函数(此处父类仍是默认构造函数)。

显示的构造函数

        在初始化基类成员时不能在派生类中单独初始化,需要在父类构造函数中初始化。

显示调用初始化函数

class Member
{
public:
	Member(const char* name = "XiaoMing", int age = 10)
		:_name(name)
		,_age(age)
	{
        cout << "Member()" << endl;
    }
protected:
	string _name;//姓名
	int _age;//年龄
};

class Student : public Member
{
public:
	Student(const char* name, int age ,int id = 10)
		:Member(name, age)
		,_id(id)
	{
        cout << "Student()" << endl;
	}

private:
	int _id;
};

int main()
{
	Student s("XiaoFang",10,100);
	return 0;
}

        进行对象实例化,结果为

        无论初始化列表顺序如何改变,构造函数构造顺序都是先父后子。

        理解:如果子类初始化时需要父类继承的成员变量时,此时若父类成员还未初始化,则子类初始化出现错误,所以构造顺序一定是先父类后子类。

构造函数构造的顺序:先父后子。

析构函数

class Member
{
public:
	Member(const char* name = "XiaoMing", int age = 10)
		:_name(name)
		,_age(age)
	{
         cout << "Member()" << endl;
    }

    ~Member()
    {
        cout << "~Member()" << endl;
    };
protected:
	string _name;//姓名
	int _age;//年龄
};

class Student : public Member
{
public:
	Student(const char* name, int age ,int id = 10)
		:Member(name, age)
		,_id(id)
	{
         cout << "Student()" << endl;
    }

    ~Student()
    {
        //~Member();错误写法,原因如下
        //子类的析构函数和父类的析构函数构成隐藏关系
		//由于多态原因,析构函数被特殊处理,函数名都被处理成destructor(),同名函数形成隐藏
        
        Member::~Member();//要指定类域;
        cout << "~Student()" << endl;
    };

private:
	int _id;
};

int main()
{
	Student s("XiaoFang",10,100);
	return 0;
}

我们创建一个对象,观察一下

        此处我们发现Member被析构两次,原因是父类析构函数会在子类析构后自动调用。

        我们在~Student()中显示调用~Member(),在子类自己析构后又自动调用一次父类的析构函数,从而导致析构两次,所以这种析构函数的写法是错误的。

        总结:对于父类的析构函数不用再子类的析构函数中显示调用,子类析构函数结束后父类析构函数自动调用!

        继承体系中正确析构函数写法:

class Member
{
public:
	Member(const char* name = "XiaoMing", int age = 10)
		:_name(name)
		,_age(age)
	{
         cout << "Member()" << endl;
    }

    ~Member()
    {
        cout << "~Member()" << endl;
    };
protected:
	string _name;//姓名
	int _age;//年龄
};

class Student : public Member
{
public:
	Student(const char* name, int age ,int id = 10)
		:Member(name, age)
		,_id(id)
	{
         cout << "Student()" << endl;
    }

    ~Student()
    {
        cout << "~Student()" << endl;
    };

private:
	int _id;
};

int main()
{
	Student s("XiaoFang",10,100);
	return 0;
}

        观察结果我们发现析构顺序与构造顺序相反,析构顺序是先子类后父类。

        理解:假设析构先父后子,会存在安全隐患,可能父类成员的资源已经清理,派生类再去访问可能会找不到,或者出现野指针等问题。 

析构函数析构的顺序:先子后父

拷贝构造函数

        利用切片原则,先将子类赋值给父类,然后再单独赋值子类剩下的函数。

例子:

class Member
{
public:
	Member(const char* name = "XiaoMing", int age = 10)
		:_name(name)
		,_age(age)
	{}
    ~Member()
    {
        cout << "~Member()" << endl;
    };
protected:
	string _name;
	int _age;
};

class Student : public Member
{
public:
	Student(const char* name, int age ,int id = 10)
		:Member(name, age)
		,_id(id)
	{

	}
//----------------------------------------------------------------------------
	Student(const Student& s)
		:Member(s)//利用赋值转换的原则(切片原则)将派生类成员切片给基类进行赋值
		,_id(s._id)
	{}
//----------------------------------------------------------------------------
    ~Student()
    {
        cout << "~Student()" << endl;
    };

private:
	int _id;
};

int main()
{
	Student s("XiaoFang",10,100);
	Student s1(s);
	return 0;
}

赋值重载函数

class Member
{
public:
	Member(const char* name = "XiaoMing", int age = 10)
		:_name(name)
		,_age(age)
	{}
//-----------------------------------------------------------
	Member& operator=(const Member& p)
	{
		if (this != &p)
		{
			_name = p._name;
			_age = p._age;
		}
		return *this;
	}
//-----------------------------------------------------------
    ~Member()
    {
        cout << "~Member()" << endl;
    };
protected:
	string _name;
	int _age;
};

class Student : public Member
{
public:
	Student(const char* name, int age ,int id)
		:Member(name, age)
		,_id(id)
	{

	}
	Student(const Student& s)
		:Member(s)//利用赋值转换的原则(切片原则)将派生类成员切片给基类进行赋值
		,_id(s._id)
	{}
//----------------------------------------------------------
    Student& operator=(const Student& s) 
	{
		if (this != &s)
		{
			Member::operator=(s);
			_id = s._id;
		}

		return *this;
	}
//----------------------------------------------------------
    ~Student()
    {
        cout << "~Student()" << endl;
    };

private:
	int _id = 10;
};

int main()
{
	Student s("XiaoFang",10,100);
	Student s1 = s;
	return 0;
}

总结:派生类的默认成员函数规则跟普通类的规则一致,唯一不同的是,不管是构造/析构/拷贝,多的是基类那一部分,基类部分调用基类那一部分对应函数去完成

继承与友元

友元关系不能继承,基类友元不能访问子类私有和保护成员。(爸爸的朋友不是孩子的朋友)

class Student;
class Member
{
public:
	//friend void print(const Member& m, const Student& s);
    //友元不能继承,无法获取子类的成员
protected:
	string _name;
	int _age;
};

class Student:public Member
{
public:
    friend void print(const Member& m, const Student& s);
private:
	int _id;
};
void print(const Member& m,const Student& s)
{
	cout << m._name << endl;
	cout << s._id << endl;
}

int main()
{
    print();
    return 0;
}

继承与静态成员

基类定义了static静态成员,则整个继承体系中只有一个这样的成员,子类只能继承访问权。(子类中不会再产生一份拷贝,原因是static静态成员不是在对象中的,是在静态区的)

菱形继承

单继承:一个子类只有一个直接父类。

多继承:一个子类有两个或两个以上的直接父类。

菱形继承:在多继承的基础上,被同一个类继承的两个类,他们又继承自同一对象。

有了多继承就可能出现菱形继承,菱形继承会产生二义性,空间浪费

        产生的问题:Student类和Teacher类继承Member类的成员,具有相同的成员(作用域不同),Assistant类又继承Student类和Teacher类,这样Assistant类就有两个相同的成员(但是作用域不同,所以可以通过不同的作用域访问这另个成员),造成了变量二义性,数据浪费。

        为了解决这个问题,引入关键字virtual加在产生同一个成员变量的类中,这个时候B对象实例化时没有保存A类成员,而是通过一个机制指向A类成员

 例子:

深入探究一下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 = 3;
	dd._d = 3;
	return 0;
}

 该代码描述的是这样的场景:

我们观察其内存地址: 

观察发现其确实多储存了两份_a(B::_a与C::_a)。

        在B类与C类中加入virtual关键字。 

我们观察其内存地址:  

        在两个蓝色框中我们发现下一行分别存储B类_b和C类_c的值,而上一行储存的是地址,由于我的是小端机,则_b存储的地址是0x001c7bdc,_c存储的地址是0x001c7be4 ,找到这两个地址,我们发现它们下一个地址存储了一个十六进制的值,这个值是偏移量。

        再将这个偏移量加回原来存储地址的地址,我们发现它们的结果都指向了A类的地址。

        B类和C类都通过偏移量表去找到公共的A类。这个时候B或C对象实例化时没有保存A类成员,而是通过一个机制指向A类成员。

都指向A有什么用?

//例
B* ph = &dd;
ph->_a++;

//例
B bb;
bb._a=10;

这样在B对象通过偏移量表就可以访问A的成员。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘子13

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值