继承和派生

目录

一.继承的概念

​1.2.继承的格式及访问方式权限的变化

1.3.继承中的作用域及隐藏

1.4.子类如何初始化父类的内容

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

1.6.继承里析构函数相关问题

​编辑1.7.派生类拷贝构造

1.8.operator=,赋值重载

1.9.继承和友元

继承和静态成员以及静态成员函数

 二.多继承与菱形继承

多继承概念

多继承的内存分布

菱形继承

虚继承 

三.继承和组合


一.继承的概念

继承机制是面向对象程序设计使代码可以复用的重要手段,它允许程序员在保持原有类基础上进行扩展,增加功能,产生新类,称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知的过程。以前接触的复用都是函数复用,比如重载,而继承是类设计层次的复用。

     继承的类与被继承的类也被叫做子类与父类或者派生类与基类,继承这一概念用子类与父类来通俗一点来讲的话就是儿子继承父亲的财产,儿子可以用父亲的东西比如公司,但是儿子在继承之前也许创业开了新公司,这个新公司与父亲没有什么关系。子类与父类联系之处就在于儿子继承的那一部分财产,但是儿子也可以有自己的新财产。继承并不是通过复制父类对象来实现的,而是通过共享父类定义的属性和方法(在子类对象中的内存布局上)来实现的。子类对象可以直接访问和操作这些继承自父类的成员,同时它也可以定义自己的成员,这些成员在子类对象中是与继承自父类的成员并存的。

举个例子B继承A,然后实例化对象b,它里面不只包含自己的成员pre,还有整个A类的成员ptr

 1.2.继承的格式及访问方式权限的变化

继承方式分为public公有继承,protected保护继承,private私有继承

类里面也有三种访问规则的访问限定符,public访问,protected访问,private访问,其实就是之前常见的东西,类里面函数经常放public公有,成员变量经常放private。

从继承的概念可以知道子类继承父类,拥有了访问父类成员的权限,对于子类自己的东西原先定义了public和private现在依旧是一样的访问限定符和权限。可是继承父类所得到的东西却不是原先在父类是什么权限现在依旧是什么权限,而是要根据继承方式的不同重新划分在子类的权限

继承基类成员访问方式的变化

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的proected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见(不能访问)在派生类中不可见(不能访问)在派生类中不可见(不能访问)

总结:

1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制了派生类对象不管在类里面还是类外面都不能访问它(但是在类外子类对象可以通过继承父类而来public的函数间接访问它)

 在类外子类对象可以通过继承父类而来public的函数间接访问它

2.基类private成员在派生类中是不能被访问的,如果父类基类private成员不想在类外直接通过子类实例化的对象访问,但是在子类的类内部能访问,就定义为protected。可以看出保护限定符是因为继承才出现的,别的地方其实不怎么常用

改成保护就可以使用了

3.基类的私有成员在子类中都是不可访问的。基类的其他成员在子类的访问方式==Min(成员在基类的访问限定符,继承方式),public>protected>private

首先用私有继承来举例,原先在父类中的public成员函数因为私有继承小于在父类中public的访问限定符,所以它在子类中访问权限取private。而类的私有成员可以在类里面访问,可是不能在类外面访问,所以printf1函数在子类外不能访问到

4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式

5.在实际运用中一般使用都是public继承,几乎很少用protected/private继承,也不提倡使用protected/private继承,因为protected/private继承下来的成员只能在派生类的类里面使用,实际扩展性不强

1.3.继承中的作用域及隐藏

1.在继承体系中基类和派生类都有独立的作用域

2.子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义(在子类成员函数中,可以使用基类::基类成员显示访问)。如果是成员函数的隐藏,只需要函数名相同就构成隐藏。在实际中继承体系里面最好不要定义同名的成员,避免麻烦和不必要的麻烦

同样上面的例子,因为子类和父类都有独立的作用域,因此在构成隐藏的情况下可以通过类作用符指定去调父类的_name把小张打印出来

同样成员函数如果名字相同,不指定类域的话默认调子类的成员函数,依旧构成隐藏,但是请注意隐藏不是重载。首先子类和父类不在同一个作用域,他们有各自的作用域,而重载必须在同一作用域。其次重载要满足函数名相同,参数类型,数量,顺序不同,而隐藏只要函数名相同就构成隐藏了。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

class person
{
public:
	void printf()
	{
		cout <<_name<< endl;
	}
protected:
	string _name="小张";
	string sex;
};

class teacher:public person
{
public:
	void printf()
	{
		cout << _name<< endl;
	}
private:
	string _name="小李";
	int age=18;
};

class doctor :public person
{
public:
	void printf()
	{
		cout << "doctor" << endl;
	}
private:
	int age = 20;
};
int main()
{
	teacher B;
	B.printf();//不指定类域,函数名相同默认调子类的函数,父类的函数构成隐藏
	B.person::printf();//隐藏情况下指定父类的类名还是可以去调父类的函数的
}

1.4.子类如何初始化父类的内容

对于一个类来说默认构造不会处理内置类型(有些编译器会处理会),而自定义类型成员会去调自定义类型自己的默认构造函数。而子类继承父类,当中除了内置类型和自定义类型还会有父类的东西,子类里面父类的东西与处理自定义类型类似,会去自动调父类的默认构造函数,如果没有默认构造函数必须写明构造函数并且在子类初始化类别必须显示调用父类构造函数,否则就会报错。

#include<iostream>
using namespace std;

class person
{
public:
	person()
	{
		cout << "父类构造函数" << endl;
	}
	void printf()
	{
		cout <<_name<< endl;
	}
protected:
	string _name="小张";
	string sex;
};

class teacher:public person
{
public:
	teacher(int _age=20)
		:age(_age)//继承父类的部分会自动去调父类的默认构造函数
	{
		cout << "子类构造函数" << endl;
	}
		void prinf()
	{
		cout << _name<< endl;
	}
private:
	int age=18;
};

int main()
{
	teacher B(20);
}

显示初始化列表初始化继承过来的父类成员 

#include<iostream>
using namespace std;

class person
{
public:
	person(string name="小张",string _sex="男")
		:_name(name)
		,sex(_sex)
	{
		cout << "父类构造函数" << endl;
	}
	void printf()
	{
		cout <<_name<< endl;
	}
protected:
	string _name;
	string sex;
};

class teacher:public person
{
public:
	teacher(int _age,string name, string _sex)
		:age(_age)
		,person(name,_sex)//继承父类的部分显示调用父类的构造函数
	{
		cout << "子类构造函数" << endl;
	}
		void printf()
	{
		cout << _name<< endl;
		cout << age<< endl;
		cout << sex<< endl;
	}
private:
	int age;
};

为什么要一定要先构造父类然后去构造子类呢,如果先构造子类然后构造父类,此时在类里面因为继承的原因,子类是可以直接访问父类的成员的,如果子类的成员依赖父类的成员值来初始化,比如name(person::name+'x);那么此时父类还没初始化构造子类得到的只能是随机值 

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

派生类对象可以赋值给基类对象/基类指针/基类的引用。也就是说把派生类中父类的那部分切出来赋值过去

同样的例子teacher实例化的对象B赋值给A,把张三,女等原本属于B中继承过来的成员的值赋值过去了

一般来说成员初始化的顺序是成员声明的顺序,但是在继承里有个例外,就是不管你成员声明还是定义顺序以及也不管初始化列表的顺序,默认的都会先去调子类继承父类的成员的构造函数,也就是说无论怎样都会先初始化子类继承父类的部分。这是因为派生类依赖于基类,所以基类必须首先被正确地构造。如果基类有多个,则按照它们在派生类定义中出现的顺序进行构造

写一个简单个例子,base和pro类,执行完构造函数会发现是先基类调构造函数完成构造然后才是调子类的构造函数

同样用父类指针指向子类或者用父类对象引用子类对象也不会报错,因为他们指向和引用的是子类当中属于父类的那一部分东西,本来就是同一类型当然不会报错

反过来基类对象不能赋值给派生类对象,因为派生类对象不只有继承基类的东西,还有自己的东西,这些自己的东西得不到赋值

基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但是必须是基类的指针是指向派生类对象时才是安全的

1.6.继承里析构函数相关问题

派生类对象析构清理先调用派生类析构再调用基类的析构,先构造的后析构,基类先构造的所有最后析构。但是不要显式调用父类的析构函数,因为这样就压根没法保证先析构派生类然后再析构基类了

即使要显式调用基类的析构函数也不要~person这样调用,因为后续多态的原因需要构成重写,所以所有的析构函数名都被编译器特殊处理成destrutor();所以你如果直接这样调父类的析构函数~person(),实际上有可能会构成无限析构函数递归,编译器为了避免这种情况就直接不允许这样做了

1.7.派生类拷贝构造

正常的拷贝构造是使用同类对象初始化创建对象,内置类型默认是值拷贝,自定义类型是会调用同时要考虑深浅拷贝问题。

派生类在内置类型和自定义类型拷贝的基础上还得考虑派生类在父类哪里继承过来的东西也得拷贝过去,所以也得调用父类的拷贝构造函数。这时候要考虑的是调用拷贝构造函数,必须传一个已经实例化出来的对象过去才能拷贝构造成功,子类对象里可没有包含父类的对象。所以采用之前说过的赋值转换,利用切片操作把子类当中属于父类的成员传给父类的拷贝构造函数完成拷贝构造

但是值得注意的是拷贝构造函数也是构造函数,C++不允许在构造函数内显式直接调用另一个构造函数,,原因在于如果在构造函数体内调用基类的构造函数,就无法保证基类在派生类的数据成员之前被正确初始化,所以最好还是在初始化列表里进行传参初始化子类里面父类的东西,否则就会报很奇怪的错误。

正确做法在于在初始化列表里传子类要拷贝的对象过去,这样会把子类里属于父类的部分切出去完成拷贝构造

完整示例代码如下

#include<iostream>
using namespace std;

class person
{
public:
	person(string name="小张",string _sex="男")
		:_name(name)
		,sex(_sex)
	{
		/*cout << "父类构造函数" << endl;*/
	}
	void printf()
	{
		cout << _name << endl;
		cout << sex << endl;
	}
	~person()
	{
		/*cout << "person子类析构" << endl;*/
	}
	person(const person& tamp)
	{
		_name = tamp._name;
		sex = tamp.sex;
	}
protected:
	string _name;
	string sex;
};

class teacher:public person
{
public:
	teacher(int _age,string name, string _sex)
		:age(_age)
		,person(name,_sex)//继承父类的部分显示调用父类的构造函数
	{
	/*	cout << "子类构造函数" << endl;*/
	}
		void printf()
	 {
		cout << _name<<" ";
		cout << age<<" ";
		cout << sex<<" ";
	 }

	teacher(const teacher& tamp)
		:person(tamp)//子类里父类的部分直接通过初始化列表完成拷贝构造
	{
		age = tamp.age;//这个同样也可以写在初始化列表里
	}
private:
	int age;
};

int main()
{
	teacher B(20,"张三","女");
	teacher A(B);//拷贝构造
	B.printf();
	cout << endl;
	A.printf();
}

如果没有手动在堆上分配空间,比如malloc或者new等,其实用默认拷贝构造也可以完成拷贝构造(前提在于没有不是默认构造函数的构造函数,默认构造函数包含全缺省的,无参的,以及不写构造函数编译器自动生成的)。但是一旦设计到深拷贝,一定要显式分配空间,显式写拷贝构造函数

1.8.operator=,赋值重载

    person& operator=(const  person& tamp1)
    {
        _name=tamp1._name;
        sex = tamp1.sex;
        return *this;//确保连续赋值
    }

    teacher& operator=(const teacher& tamp)
    {
        person::operator=(tamp);//子类里父类的东西切片调用父类的赋值重载完成赋值
        age = tamp.age;
        return *this;
    }//确保连续赋值 

赋值与拷贝构造类似,都是子类里父类的东西切片调用父类的赋值重载完成赋值,自定义类型调他们自己的赋值重载。值得注意的是父类与子类operator=也算函数名相同,虽然返回值不同,但依旧会构成隐藏,直接这样operator=(tamp);,其实调的还是子类的赋值重载函数。所以要加person::类域去指定访问父类的operator=函数。

1.9.继承和友元

 继承和友元可以直接下结论,父类的友元函数不是子类的友元函数,同样子类的友元也不是父类的友元函数,即使他们是继承关系。用现实生活来举例,父亲的朋友不一定是儿子的朋友,同样儿子朋友也不一定是父亲的朋友

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

class friend1
{
public:
	friend class father;//father类为友元类
	void printf1()
	{
		cout <<name<< endl;
	}
private:
	string name = "张三";
		int age = 21;
};

class father
{
public:
	void printf(const friend1 &B)
	{
		cout << B.age << endl;//因为father类是B对像类的友元函数,所以可以在father类里面直接访问B的私有成员
		cout << B.name << endl;
	}
private:
	int age;
};

class son :public father
{
public:
	void printf(const friend1& B)
	{
		cout << B.age << endl;//友元关系不能继承,所以不能访问
		cout << B.name << endl;
	}
private:
	int age;
};

int main()
{
	father lisi;
	friend1 B; son xiao;
	lisi.printf(B);
	xiao.printf(B);
}

 而把子类注释掉,父类依旧可以访问friend1的私有成员

友元函数的例子

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;


class son;
class father
{
public:
    friend void show(const father& A, const son& B); // 设置友元函数
    void printf()
    {
        cout << "father" << endl;
    }
private:
    int age = 45;
    string name = "张三";
};


class son : public father
{
public:
    void printf()
    {
        cout << "son" << endl;
    }
protected:
    int grade = 55;
};


void show(const father& A, const son& B)
{
	cout << A.name << " " << A.age << endl;
    cout << B.gade << endl;//友元不能继承
}
int main()
{
    father lisi;
    son liwu;
    show(lisi, liwu);
    return 0;
}

为什么友元不能继承呢,友元关系并不是类之间的一种继承或包含关系,而是基于访问权限的特别授予。子类通过继承从父类那里获得了属性和方法(取决于继承的类型:公有继承、保护继承或私有继承),友元只是一个声明在类里面没有具体实现,同时不是类的成员,所以当然不能继承。继承关系确保了子类可以重用父类的代码,并且可以添加或修改特定的功能。但是,这种继承是关于类之间结构和功能的共享,并不涉及访问权限的直接传递。


继承和静态成员以及静态成员函数

没有继承知识之前的静态成员在类里面是单独一份的,无论实例化出多少个对象,都是只有一个静态成员。而继承关系里,子类和父类也是在继承体系中只有一个这样的静态成员。

可以打印一下地址来查看一下

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;


class son;
class father
{
public:
    friend void show(const father& A, const son& B); // 设置友元函数
    void printf()
    {
        cout << "father" << endl;
    }
    static string sex;
private:
    int age = 45;
    string name = "张三";//静态成员函数不能通过构造函数初始化列表初始化,只能类外初始化

};

string father::sex = "男";

class son : public father
{
public:
    void printf()
    {
        cout << "son" << endl;
    }
protected:
    int grade = 55;
};


int main()
{
    father lisi;
    son liwu;
    printf("%p\n",&lisi.sex);
    printf("%p\n", &liwu.sex);//因为继承,所以子类也能直接访问父类的东西(除了隐藏)
    return 0;
}

 二.多继承与菱形继承

多继承概念

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承

一个子类有两个或以上直接父类时称这个继承关系为多继承

多继承的内存分布

先来看一道题

#include<iostream>
using namespace std;

class Base1 { public:int _b1; };
class Base2 { public:int _b2; };
class Derive :public Base1, public Base2 { public: int _d; };
int main()
{
    Derive d;
    Base1* p1 = &d;
    Base2* p2 = &d;
    Derive* p3 = &d;
    printf("%p\n", p1);
    printf("%p\n", p2);
    printf("%p\n", p3);
    return 0;
}

A: p1==p2==p3   B:p1<p2<p3   C:p1==p3!=p2  D:p1!=p2!=p3

 答案是C,这是因为继承体系下内存分布最先分配内存的一定是子类当中基类的东西,然后才是子类自己的东西(成员变量等)。另一个原则是多继承时的基类之间的内存分布顺序主要取决于基类在派生类中的声明顺序,比如class Derive :public Base1, public Base2,先继承的Base1所以先给Base1分配空间,然后才是Base2。所以最开始最上面的内存一定是p1,栈帧空间分配空间时可以按照数组来理解,先给类的对象分配一大块内存,然后按照基类和声明顺序给各自成员变量分配划分好内存的空间。指针变量只会记录最开始的地址,而此时p1指向的空间是最先分配的,p1保存的是最开始的地址,而p3指向的是整体类的对象分配的一大块内存,保存的也是最上面最开始空间的地址,所以p3=p1

菱形继承

菱形继承是多继承的一种特殊情况

通俗一点来说,它指的是两个子类都继承自同一个父类,并且还有一个子类同时继承这两个子类,从而在继承关系图上形成了一个类似菱形的结构

菱形继承会导致二义性和数据冗余,比如说上图Assistant继承了Student和Teacher,而Student和Teacher又都继承了Person,Student和Teacher都有一份Person的东西,这就导致了继承他们两的Assistant间接有了两份Person的东西这就是数据冗余

_name 是 person 类的一个公有成员。当 student 和 teacher 类都继承自 person 类时,它们各自都会继承 _name 成员。由于 Assistant 类同时继承自 student 和 teacher,它实际上有两个 _name 成员:一个通过 student 继承,另一个通过 teacher 继承。但是,由于 C++ 的类成员隐藏规则,Assistant 类内部只能直接访问通过其直接基类(student 和 teacher)继承的 _name 成员,而这些成员在 Assistant 的作用域内是不可见的,因为它们被隐藏了(但这里说的是从 Assistant 的视角看,它们不是直接可见的,而不是说它们不存在)。

然而,更关键的问题是,在 Assistant 类或其外部尝试访问 _name 时,会出现二义性,因为编译器不知道应该使用哪个继承路径上的 _name。这就是菱形继承中的典型问题。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class person
{
public:
	string _name;//姓名
};

class student :public person
{
protected:
	int _num;//学号
};

class teacher :public person
{
protected:
	int _id;//职工编号
};

class Assistant :public student, public teacher
{
protected:
	string _majorCourse;//主修课程
};

void Test()
{
	Assistant a;
	a._name = "peter";//菱形继承产生歧义
}

再来一个简单的例子

从vs监视里可以看出来B和C都有一份A的东西

#include<iostream>
using namespace std;

class A
{
public:
	int a = 4;
};

class B:public A
{
public:
	int b = 5;
};

class C:public A
{
public:
	int c = 6;
};

class D:public B,public C
{
public:
	int d = 7;
};

int main()
{
	D d;
}

 

如果贸然用b去访问a是会报错的,解决办法一个是通过类域指定访问的是B里的a还是C里的a,另一个办法是虚继承

 

虚继承 

虚继承是为了解决菱形继承二义性提出来的,正常的继承父类的东西在子类哪里会有一份副本,但是菱形继承会产生歧义,因为这样在最底层会有两份一模一样的东西。

虚继承具体来说是当使用虚继承时,编译器会在派生类中插入一个隐藏的“虚基类表指针”(通常简称为vbptr),这个指针指向一个虚基类表(virtual base table,简称vbtable),该表中记录了虚基类相对于派生类实例的偏移量。这样,无论派生类如何扩展或继承,只要它虚继承了一个基类,就只会在该派生类中保存一份该基类的实例,并通过虚基类表指针和偏移量来访问这个实例。

通过vs内存监控可以查看一下

第一步.调出内存窗口,取d的地址(我是64位,一个地址占8个字节)

此时d对应的内容 

可以拿之前没有虚继承内存的情况对比一下

可以看出来B除了存自己的数字5之外还存了一个很奇怪的东西,00 00 7f f7 24 7d ac 48(小端存储,所以是倒着来的),同样c也存了一个相接近的00 00 7f f7 24 7d ac 58。以B保存的00 00 7f f7 24 7d ac 48来举例,这个其实就是虚基表的地址,可以在内存栏直接输入查看一下究竟存了什么东西 

可以看出来从DAC48到DAC4C只存了一个28,再回过头来看B在C内存里的情况,地址0x000000E18499FCD8(存的是上面虚基表的地址),0x000000E18499FCE0(存B成员int b=5的数值),再看看最下面存A的东西的地址0x000000E18499FD00,A的地址减去B最开始存东西的地址0x000000E18499FCD8(十六进栈减法)就等于28

 所以再回过头就可以理解了编译器会在派生类中插入一个隐藏的“虚基类表指针”(通常简称为vbptr),这个指针指向一个虚基类表(virtual base table,简称vbtable),该表中记录了虚基类相对于派生类实例的偏移量。

三.继承和组合

继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象

而组合是has-a的关系。假设B组合了A,每一个B对象中都有一个对象,强调了一种“整体-部分”的关系,比如车和轮子的东西,车里面包含四个轮子,车是整体,轮子是部分

组合和内部类的区别在于,内部类直接定义在一个类的内部,而组合定义在外部,但是实例化出来的对象在内部

请注意,优先使用组合而不是继承

继承在类之间创建了紧密的耦合关系。子类依赖于父类的实现细节,这限制了子类的灵活性和可维护性。一旦父类发生变化,所有子类都可能受到影响。相反,组合通过将对象作为另一个对象的属性来工作,这减少了类之间的直接依赖,使得系统更加灵活和可维护。同时继承的复用主要体现在代码的重用上,但这种方式是静态的,即在编译时就确定了。组合则提供了更灵活的复用方式,因为它允许在运行时动态地改变对象的行为。通过组合,可以将多个不同的组件组合在一起来创建新的功能,这比通过继承来扩展类要灵活得多。

同时多继承会使类之间的依赖性太强,耦合度太高,组合则避免了这个问题,因为它允许我们以更简单、更直观的方式组织代码。

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
为白箱复用 (white-box reuse) 。术语 白箱 是相对可视性而言:在继承方式中,基类的
内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
(black-box reuse) ,因为对象的内部细节是不可见的。对象只以 黑箱 的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装

白盒和黑盒是软件工程的概念,不用了解太多,白盒注重内部逻辑结构,而黑盒注重功能,白盒耦合度高。所以继承耦合度高,组合耦合度低 

  • 11
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值