C++草原三剑客之一:继承

为王的诞生献上礼炮吧!       

目录

1 继承的概念及其定义

    1.1 继承的概念

    1.2 继承的定义

       1.2.1 定义格式

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

    1.3 继承类模板

2 基类和派生类之间的转换

3 继承中的作用域

    3.1 隐藏规则

    3.2 两道考察继承作用的相关易错题

4 派生类的默认成员函数

    4.1 4个常见的默认成员函数

    4.2 实现一个不能被继承的类

5 继承与友元

6 继承与静态成员

7 多继承及其菱形继承的问题

    7.1 概念介绍

       7.1.1 单继承

       7.1.2 多继承

       7.1.3 菱形继承

    7.2 虚函数

8 继承和组合


1 继承的概念及其定义

    1.1 继承的概念

       继承机制是面向对象程序设计使得代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称之为是派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触到的是函数层次的复用,而我们这里的继承是类设计层次的复用,我们接下来通过一段代码来说明一下这个继承知识的意义(原因):(我们这里写一个老师和学生的相关信息及其活动等操作)

        好了,我们这里将代码做成图片的形式来看效果会更好一些,通过我们这里上述所写的两个类来看的话,这两个类中有许多相似的地方,就比如说,都有identity()这个成员函数,都含有成员变量_name、_address、_tel、_age,每次定义类声明时,都要写这几个成员,这些相同的成员它们设计到两个类中明显有点冗余,我们在这里可以有继承来解决,将这两个类里均相同的成员全部统一放到一个类中,将不同的成员分别放在各自的类中不动,用继承去复用那两个类中都含所有的成员。

class person//我们这个写一个类,这个类中的所有成员均是上面student和teacher的两个类中都含有的成员
{
public:
	void identity()
	{
		cout << "void identity()" << endl;
	}
protected:
	string _name;
	string _address;
	string _tel;
private:
	int _age;//这里我们的这个_age成员变量使用private访问限定符来限制,后面会用到
};
class student:public person//这个意思就相当于是说student这个类它以public的方式继承了person类
{
public:
	void study()
	{  }
protected:
	int _stuid;
};
class teacher :public person
{
public:
	void teaching()
	{  }
protected:
	int _title;
};
int main()
{
	student s;
	teacher t;
	s.identity();//void identity()
	t.identity();//void identity();当我们某一个类继承了另一个类之后,我们也就可以通过student这个类类型的对象s去访问被继承的哪个类中的1成员了,这样一来,就极大地解决了上述相同代码冗余的问题
	return 0;
}

    1.2 继承的定义

       1.2.1 定义格式

class student:public person
{ };//student就是派生类,也被称为是叫作父类。student则是派生类,也被称作是叫作子类。

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

       在真正开始讲解之前,我们在这里先来看一下继承方式都有哪些:

                {  public  继承方式                                                     {   public  访问

继承方式 {  protected   继承方式                          访问限定符  {   protected  访问

                {  private   继承方式                                                   {   private  访问

       接下来,我们来通过一个表格来看一下继承父类成员访问方式的变化。

———————————————————————————————————————————

类成员 / 继承方式       |        public继承方式       |       protected继承方式       |      private继承方式

———————————————————————————————————————————

基类的public成员        |   派生类的public成员     |   派生类的protected成员   |   派生类的private成员

———————————————————————————————————————————

基类的protected成员  | 派生类的protected成员  |   派生类的protected成员   |  派生类的private成员

———————————————————————————————————————————

基类的private成员      |     在派生类中不可见      |      在派生类中不可见        |    在派生类中不可见

———————————————————————————————————————————

       1>.基类的private成员在派生类中无论以什么样的方式去继承,它们在派生类中其实都是不可见的。这里的不可见指的是基类的私有成员它还是被继承到了派生类的对象中,但是在语法上限制了派生类对象不管是在派生类里面还是在派生类外面都不可以去访问基类中用private访问限定符限定的成员的,只可以在基类中自己使用。例:

class person
{
public:
	void identity() 
	{
		cout << _identity << endl;
	}
private:
	int _identity = 23;
};
class student :public person
{
public:
	void studen()
	{
		cout << _identity << endl;
	}//我们这里这样写一般环境下编译器是不会报错的,根据我们所讲的关于按需实例化相关的知识可知。
};
int main()
{
	person p;
	cout << sizeof(p) << endl;//4;输出p这个person类类型对象的大小
	student s;
	cout << sizeof(s) << endl;//4;如果我们这里不看我们这部分的知识的话,按照我们前面所学的知识可知,这里应该是输出1,但是这里输出的是4,就足以说明这里student确实是将person类中的_iddentity这个成员变量给继承过来了。
	s.studen();//这里编译器会报错,不可以去访问_identity这个基类中的私有成员变量。
	s.identity();//23;student这个类将person这个类中的identity()这个成员函数给继承过来了,可以在s这个对象中去调用identity()这个函数,在基类这个类自己中可以使用被访问限定符限制的_identity这个成员变量。
	return 0;
}

       2>.基类private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但是需要在派生类中去被访问的话,我们就将这个成员的访问限定符定义为protected。只有这样才可以在派生类中去访问基类中的成员变量,从这里就可以看出来protected(保护限定符)其实就是为了继承才设计出来的。例:

class person
{
protected:
	int _identity = 23;
};
class student :public person
{
public:
	void studen()
	{
		cout << _identity << endl;
	}//我们这里这样写一般环境下编译器是不会报错的,根据我们所讲的关于按需实例化相关的知识可知。
};
int main()
{
	student s;
	s.studen();//23;这次就输出了23这个值了,根据这个输出结果可知,在基类中被protected访问限定符限定的成员变量在派生类中是可以被访问的。
	return 0;
}

       3>.我们来对上面的表格总结会发现,基类的私有成员在派生类中,不管以何种方式去继承,那么这些私有成员都是不可访问的,基类的其他成员在派生类中的访问方式其实就是派生类的访问方式和该成员在基类中的访问方式中权限最小的那个方式,就比如说前面代码中的_identity这个成员变量,它在基类中是被protected访问限定符限制的,而派生类在这里是以public继承方式去继承基类的,public和protected两个中protected权限最小,因此_identity这个基类中的成员变量在派生类中的访问权限就是protected,就相当于是被protected的访问限定符限制在派生类中。例:

class person
{
public:
	int _person_public = 11;
private:
	int _person_private = 22;
protected:
	int _person_protected = 33;
};
class student1 :public person//以public的继承方式去继承person类。
{
public:
	void print1()
	{
		cout << _person_public << endl;
	}
	void print2()
	{
		cout << _person_private << endl;
	}
	void print3()
	{
		cout << _person_protected << endl;
	}
};
class student2 :protected person//以protected的继承方式去继承person类。
{
public:
	void print4()
	{
		cout << _person_public << endl;
	}
	void print5()
	{
		cout << _person_private << endl;
	}
	void print6()
	{
		cout << _person_protected << endl;
	}
};//这里我们只简单讲解一下以public和private的继承方式去继承,private以此类推就可以了。
int main()
{
	student1 s1;
	s1.print1();//11;
	cout << s1._person_public << endl;//student1是以public的继承方式去继承person的,而_person_public在person类中是被public访问限定符限定的,因此在student1类中也就相当于是被public限定的。
	s1.print3();//33;student1是以public的继承方式去继承的,而_person_protected在person类中是被protected访问限定符限定的。因此在student1类中也就相当于是被protected限定的,只能在student1这个类里去使用,无法在student1这个类的外面使用。
	s1.print2();//编译器在这里会报错,_person_private在person类中是被private访问限定符限定的,只能在person类中被访问,其余地方均不能被访问。
	student2 s2;
	s2.print4();//11;
	s2.print5();//编译器在这里会报错。
	s2.print6();//33;
	cout << s2._person_public << endl;//编译器在这里会报错,经过上述我们所学的知识,我们可以得知,_person_public在student2这个类中就相当于是被protected这个访问限定符限定的,因此无法在student2这个类的类外被访问。
	return 0;
}

       4>.使用关键词class时默认的继承方式是private,使用struct关键字是默认的继承方式是public,不过最好还是显示的写出继承方式,以增加代码的可读性。例:

class person
{
public:
	int _a = 10;
};
class student1 : person
//class student1 : private person
{  };
struct student2 : person
//struct student2 : public person
{  };
int main()
{
	student1 s1;
	cout << s1._a << endl;//编译器在这里会报错,_a这个成员变量在student1这个类中是被private这个访问限定符限定的,因此不可以被访问。
	student2 s2;
	cout << s2._a << endl;//10;_a这个成员变量在student2这个类中是被public这个访问限定符限定的,因此,我们在类外也可以访问。
	return 0;
}

       5>.我们在实际运用中一般都是用public的继承方式去继承的,几乎很少看见使用protected / private这两种继承方式去继承的,因为protected / private继承下来的成员都只能在派生类里面去使用,实际中扩展维护性也不强。

    1.3 继承类模板

    (这以部分的内容我们结合代码来说明)

//我们前面在实现栈这个结构的时候,在其内部是通过组合的方式嵌套了一个vector类类型的对象去实现的,这种实现方式叫组合,其实,要想实现栈这个结构,不只有组合这一种实现方式,我们其实还可以借助我们这里学习的这个继承的这个知识点,我们可以通过继承一个vector类模板来实现栈这个结构。
template <class T>
class stack :public vector<T>//继承一个vector类型的模板
{
public:
	void push(const T& x)
	{
		//push_back(x);//按照我们前面所学到的知识可以知道,如果这里我们直接去调用模板中的push_back函数的话,编译器在这里会报错,讲解如下:我们在main函数中定义了一个stack<int>类型的一个对象,那么就是对这个stack模板进行了实例化操作,同时也就间接的对vector进行了实例化操作,我们这里需要注意一点,就是,这是我们这里所的实例化的stack和vector并不是将这两个模板中所有的成员函数全部都进行实例化操作,而这里只是实例化了stack和vector这两个模板中的默认构造函数,其余成员函数均未进行实例化操作,我们定义好后执行下一条代码,是调用push函数插入一个元素,也就相当于是调用vector中的push_back函数,但是我们这里的调用vector中的push_back这个函数是不能直接去调用的,如果写成我们这句代码的话,那么编译器就会报错,因为它找不到相应的函数,我们这里的这个push_back这个函数它没有进行实例化操作,也就是说编译器它在执行到这句代码的时候是到模板中去找vector<int>::push_back实例化后函数的地址,找不到就报错,因此要在前面加上实例化操作。
		vector<int>::push_back(x);//我们只有在前面加上一个实例化操作,去对vector中的push_back进行初始化操作,这样才可以,编译器在进行对push实例化时会根据模板构造出一个push函数(这个push函数中所有的T全部都被换成了int),这样就能对push_back这个函数进行实例化操作了。
	}
	void pop()
	{
		vector<int>::pop_back();//这个pop_back函数也需要在这里进行实例化操作,理由和上面类似。
	}
	//这里我们再对上述push_back函数实例化再做一个简单的解释,如果我们在这里不加上前面那个实例化操作的话,编译器它就会到stack类中去找相对应的函数,找不到,那么接下来就会到基类中去找,而基类(vector)中是没有实例化出相应对应的push_back函数的,因此编译器没有找到就会报错,当我们前面指定了实例化的操作后,编译器就知是从vector<int>中去找push_back函数,就会把vector<int>中的push_back实例化一份int的出来供我们使用。
};

       (这里我建议大家将上述的哪个代码中的解释全部看完,这样的话会对大家有很大的班助的,这样的话,大家对于实例化部分的理解就会跟上一层楼了)

2 基类和派生类之间的转换

       1>.public继承的派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫做切片或者是切割。寓意把派生类中基类的那一部分切割出来,基类指针或引用指向的是派生类中切出来的基类那部分。

class person
{
protected:
	string _name;
	string _tel;
	string _address;
	int _age;
};
class student :public person
{
public:
	int _no;//学号
};

(接下来,我们就以上面这个基类和派生类为例来展开讲解一下这里的这部分知识)

int main()
{
	student s;
	person p = s;//子类对象可以赋值给父类的对象,调用父类中的拷贝构造函数即可实现。
	return 0;
}

int main()
{
	student s;
	person* pp = &s;//子类对象可以赋值给父类指针,就相当于是把子类中父类的那部分切割出来,pp指针就指向切割出来的那部分的地址,大家如果对这块部分不是很明白的话,可以这么理解。
	return 0;
}

int main()
{
	student s;
	person& p = s;//子类对象可以赋值给父类的引用,就相当于是把子类中父类的那一部分切割出来,p它所引用的就是从子类中切割出来的那一部分。
	return 0;
}

        如果我们大家仔细去看的话,会发现这里是一个特殊处理的过程,按照我们前面所学的这个类型转换方面的知识来讲的话,s它会转换成一个person类型的临时对象,而临时对象具有常性,必须要用const修饰的person类型的对象去引用它,而我们这里没有加const修饰,然而编译器它在这里实际上也没有报错,由此我们便可以推断这里是进行了特殊处理。

//s=p;//编译器在这里会报错,在继承这里我们不允许将父类对象赋值给子类对象,类型转换也不行。

        3>.父类的指针或者引用是可以通过强制类型转换赋值给子类的指针或引用的。但是有一个要求,必须是父类的指针是指向子类对象时才是安全的。这里父类如果是多态类型,可以使用RTT(Run_Time  Type  Information)的dynamic_cast来进行识别后进行安全转移。(这个我们在后面的类型转换章节再来单独专门讲解,这里我们暂时先简单的提一下)

//由于某种原因,我们这里就不写一个类继承另一个类的代码了,就以前面那个继承的代码为例来讲解这里的这个问题。
int main()
{
	person p;
	student s;
	person* pp = &s;
	student* ps = (student*)pp;//这个是可以的,编译器它在运行到这里时其实并不会报错,为什么呢?通过我们上面所执行的程序,我们可以知道,一个父类类型的指针(pp)指向的是student类类型的对象中父类(person)的那一部分,将它转换成student不是很正常吗?
	person& rp = s;
	student& rs = (student&)rp;//这个也是可以的编译器也不会报错,原因与上述原因是类似的。
	return 0;
}

3 继承中的作用域

    3.1 隐藏规则

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

       2>.派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况其实就叫隐藏。(在派生类的成员函数中,可以使用基类::基类成员显示访问)

       3>.需要我们注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏,只有基类和派生类之间的同名函数才能构成隐藏。

class person
{
public:
	void print()
	{
		cout << "class person" << endl;
	}
	int _num = 10;
};
class student :public person
{
public:
	void print()
	{
		cout << "class student::public person" << endl;
	}
	double _num = 1.1;
};//ok,根据我们上述所学的知识可以得知,print函数构成隐藏,person类中的哪个print被隐藏了。
int main()
{
	student s;
	s.print();//class student::public person;从输出的结果我们不难可以看出person类中的print函数确实是被隐藏了。
	s.person::print();//class person;
	//当然,不仅仅成员函数是这样,成员变量一样也可以构成隐藏,和成员函数的隐藏相同,成员变量这里也是只要求名称相同就可以构成隐藏。
	cout << s._num << endl;//1.1;_num这个成员变量在这里构成了隐藏,person类中的那个_num变量被隐藏了。
	cout << s.person::_num << endl;//10;我们这里也可以通过基类::基类成员这种方式去访问基类中的_num这个成员变量。
	return 0;
}

      4>.注意实际中在继承体系里面最好不要定义同名的成员。

    3.2 两道考察继承作用的相关易错题

       代码如下:

class A
{
public:
	void func()
	{
		cout << "func()" << endl;
	}
};
class B :public A
{
public:
	void func(int i)
	{
		cout << "func(int i)" << endl;
	}
};
int main()
{
	B b;
	b.func(10);
	b.func();
	return 0;
}

       1>.A和B类中的两个func构成什么关系:A和B类中的两个func函数它们构成隐藏关系;根据我们前面所学的知识可以得知,这里还是需要注意一下,我第一次在做这个题的时候是认为这两个func函数构成重载关系的,这里我们来说一下,只有在同一域中的同名函数才会构成重载,注意:同一域,这里的A类和B类中的两个func函数是在不同的域,不在同一域,因此这里是构成隐藏,而不是构成重载。

       2>.main函数中程序的运行结果是什么:结果是运行报错,报错的位置是在调用b.func( )这个函数的时候报错的,通过我们这里所学的知识可知,A类中的那个func函数被隐藏了,无法通过B类类型的对象去调用这个函数,因此才会报错,结合我们前面所学过的编译和链接那块的知识我们可知,真正去执行代码其实是在最后的链接中去执行代码的,去调用各个函数接口,由此观之,这个错误它是属于运行错误。

4 派生类的默认成员函数

    4.1 4个常见的默认成员函数

       1>.派生类的构造函数必须调用基类的构造函数初始化基类那一部分的成员,如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

class person//基类
{
public:
	person(const char* name = "xxx")//构造函数
		:_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)//如果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,const char* address)
		//:_name(name)//我们这一步操作的目的是给父类初始化,但是给父类初始化这里不允许我们直接去给父类中的元素进行初始化操作,对于给父类成员进行初始化这个操作,要将继承父类成员看作是一个整体的对象,要求调用父类的构造函数去进行初始化操作。
		:person(name)//这样写就相当于是显示调用父类的构造函数,如果父类没有默认构造函数的话,就需要我们在这里进行显示调用父类的构造函数了。
	    ,_num(num)
		,_address(address)
	{  }
protected:
	int _num;
	string _address;
};
int main()
{
	student s("zyb", 1, "yunchengshi");//创建一个student类类型的对象,并显示传值对这个对象进行初始化操作,对于父类成员,这里则会调用父类的构造函数去进行初始化操作。
	//这里再来说一个简单的小知识点,就是我们这里student是在初始化列表中进行初始化操作的,初始化列表中对于各个成员变量的初始化顺序是和声明的顺序是一摸一样的(前面讲过这个知识点),我们如果让代码一句一句走的话,就能沟发现在初始化列表中,是先调用父类的构造函数,然后是_num,最后才是_address成员变量,是因为student将person类继承过来,是将person作为一个整体继承了过来,因此,person类是首先被声明的。
	return 0;
}

       2>.派生类对象初始化时先调用基类构造,再调用派生类构造函数。

       3>.派生类对象的拷贝构造函数必须调用基类的拷贝构造完成拷贝初始化。

class person//基类
{
public:
	person(const char* name = "xxx")//构造函数
		:_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)//如果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, const char* address)
		:person(name)
		, _num(num)
		, _address(address)
	{   }
	student(const student& s)//拷贝构造函数它也是一个特殊的构造函数,它也有初始化列表,因此,我们这个拷贝构造也可以在初始化列表中去进行我们想要的操作,如果我们要实现对资源的创建和释放的话,就要我们自己去实现。
		:person(s)//对父类成员进行拷贝操作,这里用到了基类和派生类之间转换的知识,如果我们这里不传这个s过去的话,它会默认传过调用person中的默认构造函数。
        ,_num(s._num)
		,_address(s._address)
	{   /*深拷贝,也就是有一些资源的开创和释放*/  }
	//这里有一个知识点,需要我们去注意一下,就是这个拷贝构造,如果是在初始化列表中实现的话,如果我们这里不写调用基类的拷贝构造的话,它是会默认去调用基类的构造函数的(因为初始化列表它默认对于自定义变量和基类是去调用它们各自对应的默认构造函数的,没有默认构造函数的话,编译器它就会报错),这样的话就无法达到我们想要的那个结果
protected:
	int _num;
	string _address;
};
int main()
{
	student s1("zyb", 1, "yunchengshi");
	student s2(s1);//调用拷贝构造函数;s2:zyb 1 yunchengshi
	return 0;
}

       4>.派生类的operator=()必须要去调用基类的operator=()来完成基类成员的赋值操作。需要注意的是派生类的operator=()函数隐藏了基类的operator=()函数,所以需要我们去显示调用基类的operator=()函数,就必须要去指定基类的作用域,只有这样才能访问基类中的那个operator=()赋值构造函数。

student& operator=(const student& s)
{
	if (this != &s)//如果是自己给自己赋值的话,就直接返回自己就可以了,有效地节省了时间,大大地提高了运行效率。
	{
		person::operator=(s);//调用父类的operator=()函数去给父类中的那一部分成员变量进行赋值操作,这里我们必须要去注意在调用父类中的operator=()函数去执行操作时,我们这里一定要在operator=()函数前面加上父类的作用域,否则系统会崩溃,原因是调用函数的次数太多,导致栈溢出了。这里之所以会报出这个错误,其主要的原因就是因为子类中的operator=()函数把父类中的operator=()函数给隐藏了,如果不加作用域限定的话,那么它就会一直调用student类中的operator=()函数,久而久之,栈就溢出了。
		_num = s._num;
		_address = s._address;
	}
	return *this;
}//严格来说,student赋值重载函数默认生成的就已经够用了,如果有需要进行深拷贝的地方,才需要我们自己去写。

       5>.派生类对象析构清理先调用派生类析构然后再去调用基类的析构函数。(先构造的后析构)

       6>.派生类的析构函数会在被调用完成后会自动去调用基类的析构函数去清理基类成员。因为只有这样才能保证派生类对象先清理派生类成员再去清理基类成员的顺序。

       7>.因为多态中一些场景导致析构函数需要重写,而重写的条件之一就是函数名相同(这个写一篇博客会讲到,这里我们只需要明白析构函数会被进行特殊处理即可),那么编译器在这里会对析构函数进行一个特殊处理的操作,将析构函数名处理成destructor(),所以基类的析构函数在不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。

~student()
{
	_num = 0;
	_address=nullptr;
	person::~person();//我们在去调用父类的析构函数去清理父类中的成员变量时,我们一定要在这里指定作用域,因为析构函数在最后它都会被处理成desructor()这个函数,进而会构成隐藏。
}//我们在显示调用父类的析构函数时,一定要注意要等到将子类中的成员变量全部清理完了以后,再去调用父类的析构函数去清理父类中的那一部分成员变量,通过我们前面所学过的知识可知,先构造的后析构,由于person是被继承过来的,就相当于是先有person,再有student,因此后析构person(也就是person)。

       严格来说,我们上写的这4中默认成员函数我们不写其实就可以,编译器默认生成的函数其实就够我们使用了,如果有用到深浅拷贝的资源的话,那么就需要我们自己去写了,通过上述的讲解,我们在理解时,完全可以将父类当成一个自定义对象去看待就可以了,这样子理解起来就显得简单得多了。

    4.2 实现一个不能被继承的类

       1>.基类的构造函数私有,派生类的构造必须调用基类的构造函数,但是基类的构造函数私有化以后,派生类看不见就自然不能去调用了,进而就无法实例化出对象了。

       2>.C++11中增加了一个final关键字,final修改了基类后,派生类就不可以继承基类了。

       当person被final被修改了,那么person这个基类就不能被继承了,否则编译器会报错。

5 继承与友元

       友元关系不能被继承,也就是说基类友元不能访问派生类的私有和保护成员,换句话说,就是基类的 " 朋友 " 不是派生类的 " 朋友 " 。

class student;//先声明一下student类,否则编译器会报错。
class person
{
	friend void display(const person& p, const student& s);//display这个函数是person这个类的友元。
protected:
	int _age;
};
class student :public person
{
protected:
	int _num;
};
void display(const person& p, const student& s)
{
	cout << p._age << endl;
	cout << s._num << endl;//当我们写到这里时,编译器它在这里其实就会显示报错,不可访问s对象中的保护成员变量,就是因为person和display的友元关系没有被继承下来,因此编译器在这里会报错。
}

6 继承与静态成员

       基类定义了一个static静态成员变量,则整个继承体系里面只有一个这样的成员。无论派生出多少个派生类,都是只有一个static成员实例。

class person
{
public:
	static int _num;
};
int person::_num = 0;
class student :public person
{
public:
	void print()
	{
		cout << ++_num << endl;
		//让_num变量++,通过++的变化来观察static修饰的变量的变化。
	}
};
int main()
{
	student s;
	person p;
	cout << &s._num << endl;//00007FF6C3C4E1B0
	cout << &p._num << endl;//00007FF6C3C4E1B0;输出的两个地址是一样的,就足以说明在基类中定义了一个static修饰的变量,那一整个的继承体系中就只有这一个这样的成员。
	s.print();//1;在print函数中,_num这个变量+1,按照我们前面在这里所讲的知识来说的话,p这个对象中的_num这个变量也就变成了1。
	cout << p._num << endl;
	return 0;
}

7 多继承及其菱形继承的问题

    7.1 概念介绍

       7.1.1 单继承

       一个派生类只有一个直接基类时称这个继承关系为单继承。

       7.1.2 多继承

       一个派生类有大于两个直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是:先继承的基类在前面,后面继承的基类在后面,派生类成员放到最后面。

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//assistant这个类同时继承了一个student类和teacher类,继承的父类之间分别用",",隔开,assistant这个类先继承的是student类,后继承的是teacher类。
{
protected:
	string _course;//主修课程
	//接下来,我们根据我们上面所学的知识来简单地看一下assistant这个类地内存空间地存放(看下图):
};

       7.1.3 菱形继承

       菱形继承是多继承的一种特殊情况。菱形继承的问题,从7.1.2中的模型构造上我们其实就是可以知道的,从中可以看出菱形继承有数据冗余和二义性的问题,在assistant的对象中person成员会有两份,这样的话,会大大增加空间的消耗,比如:person类中有一个关于性别的成员变量,这个变量其实存在一次就可以了(因为一个人不可能有两个性别),存两次显然就有点浪费了,这就是数据冗余的问题,二义性就是说,比如:student类和teacher类它们都继承了person类,这两个类中都含有_name这个成员变量,但是_name中存放的数据是不同的,我定义了一个assistant类类型的对象a,我想输出a对象中的_name的内容," cout << a._name << endl; " ,编译器在这里会报错,因为它不知道该输出哪个_name。但是我们可以通过指定作用域来输出就i可以了,例输出student类中的_name,代码可以写成 " cout << a._name << endl; " ,紧接着,我们来看一下菱形继承的模型(前一个代码为例):

        通过我们上面对菱形继承的介绍和分析,我们这里知道了一旦使用了多继承,就不可避免地会出现菱形继承这个大问题,其中就会造成数据冗余这样地一个问题,为了解决这里的这个数据冗余从而造成的空间浪费的问题,C++因此引入了一个叫虚函数的概念。

    7.2 虚函数

       通过我们前面的代码对多继承的分析,我们可以得知,只要我们使用继承,就不可避免地会出现菱形继承这个问题,它会造成空间地大量浪费,使得空间利用率大幅度下降,为了解决这个空间利用的问题,我们C++设计了一个虚函数这个东西,虚函数在这里就非常有效地解决了这个问题。我们接下来来看一下它的使用:

class person
{
public:
	string _name;
};
class student :virtual public person//这样写就是student类虚继承了person类,要在被继承的类前面加上一个virtual关键字就可以了,就说明子类虚继承了父类。
{
protected:
	int _num;
};
class teacher :virtual public person//teacher类虚继承了person类。
{
protected:
	int _id;
};
class assistant :public student, public teacher
{
protected:
	int _course;
};
//以上就是我们这里所写的虚继承了,接下来,我们来具体地看一下这个assistant类内部的存放方式(看下图)。

int main()
{
	assistant a;
	a._name = "xuanyaunyaun";//我们通过上述图中的内存存储的模型可以知道,person它不在student或者是teacher这两个类的任意一个类的内部,它在一个公共的地方,通过上述的结构来看,就相当于是assistant也继承了一个person类,因此我们可以通过assistant类类型的对象直接去访问person类中的成员变量。
	cout << a._name << endl;//xuanyaunyaun;
	a.student::_name = "xuanyuan";//我们也可以通过作用域去访问person中的_name成员变量,由于person是公共的,所以我们通过student这个作用域修改了_name之后,通过teacher作用域访问的_name也是"xuanyaun"。
	cout << a.student::_name << endl;//xuanyuan;
	cout << a.teacher::_name << endl;//xuanyuan;
	a.teacher::_name = "yuanyuan";
	cout << a.student::_name << endl;//yuanyuan;
	cout << a.teacher::_name << endl;//yuanyuan;无论是用哪个作用域去访问_name这个成员变量,最终得到的结果都是"yuanyuan"。
	return 0;
}

我们这个虚继承这里只讲解虚继承的用法,不讲解它的底层,只因它的底层在这里过于复杂,是我们现在所掌握的知识所接受不了的。而且虚继承它一般情况下达不到我们这里想要的效果,而且一旦使用菱形虚拟继承,就会是无论是使用还是底层它都会复杂好多,当然有多继承语法在这里支持,就一定会在这里设计出类型继承,想Java是不支持多继承的,自然就避开了菱形继承。最后,给大家说一个忠告:我们可以使用多继承,但是尽量不要设计出菱形继承,如果以后碰到了查查资料其实就可以了,否则会有很多复杂的问题。

       很多人都说c++的语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有了虚拟菱形继承,底层实现就会很复杂,性能方面就也会有一些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之一,后来的一些编程语言中基本上都没有多继承。

8 继承和组合

       1>.public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。
       2>.组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
       3>.继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤(这里既然说到了这个白盒测试和黑盒测试,我们先来简单地了解一下相关的知识:1 黑盒测试:不了解底层实现,是从功能的角度进行测试的,2 白盒测试(相对于黑盒测试更难):了解底层实现(代码实现),从代码运行的逻辑角度进行测试)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可⻅ 。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度⾼。
       4>.对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
       5>.优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。

       OK,今天我们就先讲到这里了,那么,我们下一篇再见,谢谢大家的支持!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/927414.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Crash-SQLiteDiskIOException

目录 相关问题 日志信息 可能原因 问题排查 相关问题 蓝牙wifi无法使用 日志信息 可能原因 磁盘空间不足&#xff1a;当设备上的可用存储空间不足时&#xff0c;SQLite无法完成磁盘I/O操作&#xff0c;从而导致SQLiteDiskIOException。 数据库文件损坏&#xff1a;如果数…

6.824/6.5840 Lab 1: Lab 3: Raft

漆昼中温柔的不像话 静守着他的遗憾啊 旧的摇椅吱吱呀呀停不下 风卷走了满院的落叶落花 ——暮色回响 完整代码见&#xff1a; https://github.com/SnowLegend-star/6.824 在完成Lab之前&#xff0c;务必把论文多读几遍&#xff0c;力求完全理解Leader选举、log日志等过程。 …

【C++动态规划 BFS 博弈】3283. 吃掉所有兵需要的最多移动次数|2473

本文涉及知识点 C动态规划 CBFS算法 数学 博弈 LeetCode3283. 吃掉所有兵需要的最多移动次数 给你一个 50 x 50 的国际象棋棋盘&#xff0c;棋盘上有 一个 马和一些兵。给你两个整数 kx 和 ky &#xff0c;其中 (kx, ky) 表示马所在的位置&#xff0c;同时还有一个二维数组 …

6.824/6.5840 Lab 2: Key/Value Server

故事里能毁坏的只有风景 谁也摧毁不了我们的梦境 弦月旁的流星划过了天际 我许下 的愿望 该向谁 去说明 ——我是如此相信 完整代码见&#xff1a; https://github.com/SnowLegend-star/6.824 还是那句话&#xff0c;尽量只是参考思路而不是照抄 先阅读几遍实验说明的Introd…

Linux-异步IO和存储映射IO

异步IO 在 I/O 多路复用中&#xff0c;进程通过系统调用 select()或 poll()来主动查询文件描述符上是否可以执行 I/O 操作。而在异步 I/O 中&#xff0c;当文件描述符上可以执行 I/O 操作时&#xff0c;进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其它的任务…

R 语言科研绘图第 1 期 --- 折线图-基础

在发表科研论文的过程中&#xff0c;科研绘图是必不可少的&#xff0c;一张好看的图形会是文章很大的加分项。 为了便于使用&#xff0c;本系列文章介绍的所有绘图都已收录到了 sciRplot 项目中&#xff0c;获取方式&#xff1a; R 语言科研绘图模板 --- sciRplothttps://mp.…

企业中数据防泄漏如何防范?有哪些防泄密措施?

企业数据不仅是业务运营的核心&#xff0c;也是企业竞争力的关键所在。 然而&#xff0c;随着信息技术的快速发展&#xff0c;数据泄露的风险也随之增加。 数据一旦泄露&#xff0c;不仅可能导致企业经济损失&#xff0c;还可能损害企业声誉&#xff0c;甚至引发法律纠纷。 …

汽车控制软件下载移动管家手机控车一键启动app

移动管家手机控制汽车系统是一款实现车辆远程智能控制的应用程序‌。通过下载并安装特定的APP&#xff0c;用户可以轻松实现以下功能&#xff1a;‌远程启动与熄火‌&#xff1a;无论身处何地&#xff0c;只要有网络&#xff0c;即可远程启动或熄火车辆&#xff0c;提前预冷或预…

基于事件驱动构建 AI 原生应用

作者&#xff1a;寒斜 AI 应用在商业化服务的阶段会面临诸多挑战&#xff0c;比如更快的服务交付速度&#xff0c;更实时、精准的结果以及更人性化的体验等&#xff0c;传统架构限制于同步交互&#xff0c;无法满足上述需求&#xff0c;本篇文章给大家分享一下如何基于事件驱动…

如何查看阿里云ddos供给量

要查看阿里云上的 DDoS 攻击量&#xff0c;你可以通过阿里云的 云盾 DDoS 防护 服务来进行监控和查看攻击数据。阿里云提供了详细的流量监控、攻击日志以及攻击趋势分析工具&#xff0c;帮助用户实时了解 DDoS 攻击的情况。以下是九河云总结的查看 DDoS 攻击量的步骤&#xff1…

华为HarmonyOS 让应用快速拥有账号能力 - 获取用户手机号

场景介绍 当应用对获取的手机号时效性要求不高时&#xff0c;可使用Account Kit提供的手机号授权与快速验证能力&#xff0c;向用户发起手机号授权申请&#xff0c;经用户同意授权后&#xff0c;获取到手机号并为用户提供相应服务。以下只针对Account kit提供的手机号授权与快…

React 的学习记录一:与 Vue 的相同点和区别

目录 一、学习目标 二、学习内容1️⃣——React的特点 1.组件化设计 2.单向数据流 3.声明式 UI 4.虚拟 DOM 5.Hooks 6.JSX 7.React Native 三、React与vue的比较总结 四、总结 一、学习目标 时间&#xff1a;两周 内容&#xff1a; React的特点React的入门React的…

使用epoll监测定时器是否到达指定时间,并执行回调函数

总览&#xff1a;Linux提供了定时器&#xff0c;暴露出来了文件描述符&#xff0c;所以我们使用epoll帮助我们监测&#xff0c;时间到达后&#xff0c;epoll_wait返回&#xff0c;于是我们根据fd&#xff0c;找到对应的回调函数&#xff0c;然后执行。从而达到定时执行函数的目…

鸿蒙征文|鸿蒙技术分享:使用到的开发框架和技术概览

目录 每日一句正能量前言正文1. 开发环境搭建关键技术&#xff1a;2. 用户界面开发关键技术&#xff1a;3. 应用逻辑开发关键技术&#xff1a;4. 应用测试关键技术&#xff1a;5. 应用签名和打包关键技术&#xff1a;6. 上架流程关键技术&#xff1a;7. 后续维护和更新关键技术…

【MIT-OS6.S081笔记0.5】xv6 gdb调试环境搭建

补充一下xv6 gdb调试环境的搭建&#xff0c;我这里装的是最新的15.2的gdb的版本。我下载的是下面的第二个xz后缀的文件&#xff1a; 配置最详细的步骤可以参考下面的文章&#xff1a; [MIT 6.S081] Lab 0: 实验配置, 调试及测试 这里记录一下踩过的一些报错&#xff1a; 文…

Python和Java后端开发技术对比

在当今互联网技术飞速发展的时代&#xff0c;后端开发扮演着至关重要的角色。Python和Java作为两大主流的后端开发语言&#xff0c;各自具备独特的优势和应用场景。让我们深入了解这两种技术的特点和选择建议。 Java后端开发一直是企业级应用的首选方案。它以强大的类型系统、…

1.2.3 逻辑代数与运算

逻辑代数与运算 基本的逻辑运算常用逻辑公式 基本的逻辑运算 基本逻辑运算非常简单&#xff0c;只包含与、或、非、异或这4种。 这里主要留意对基本逻辑运算的不同叫法&#xff0c;符号表示。逻辑表达式、真值表概念。 与&#xff1a;A和B都为真时&#xff0c;结果才为真或…

解析生成对抗网络(GAN):原理与应用

目录 一、引言 二、生成对抗网络原理 &#xff08;一&#xff09;基本架构 &#xff08;二&#xff09;训练过程 三、生成对抗网络的应用 &#xff08;一&#xff09;图像生成 无条件图像生成&#xff1a; &#xff08;二&#xff09;数据增强 &#xff08;三&#xff…

零售餐饮收银台源码

收银系统早已成为门店经营的必备软件工具&#xff0c;因为各个连锁品牌有自己的经营模式&#xff0c;自然对收银系统需求各有不同&#xff0c;需要有相应的功能模块来实现其商业模式。 1. 适用行业 收银系统源码适用于零售、餐饮等行业门店&#xff0c;如商超便利店、水果生鲜…

我的第一个创作纪念日 —— 梦开始的地方

前言 时光荏苒&#xff0c;转眼间&#xff0c;我已经在CSDN这片技术沃土上耕耘了365天 今天&#xff0c;我迎来了自己在CSDN的第1个创作纪念日&#xff0c;这个特殊的日子不仅是对我过去努力的肯定&#xff0c;更是对未来持续创作的激励 机缘 回想起初次接触CSDN&#xff0c;那…