目录
一.模版
1.模版的基本概念
a.函数模版
b.类模板
c.模版的实例化
d.class 和 typename的区别
e.非类型模版参数
2.模版的特化
a.全特化 —— 参数类型全部特殊化处理
b.半特化 —— 部分参数特殊化处理
c.偏特化——对某些类型的进一步限制(实例化时传的也是指针类型)
3.类模版内函数的声明和定义分离
二.继承
1.继承的基本知识
a.继承的方式
b.构造函数和析构函数
2.基类和派生类对象的赋值转换(前提:公有继承)
a.基类对象不能赋值给派生类对象
b. 派生类对象可以赋值给基类对象(切片)
c.使用动态类型转换
3.隐藏/重定义
a.成员函数隐藏
b.成员变量隐藏
三.多继承(重难点)
1.菱形继承
2.虚继承
a.虚继承的底层原理
b.虚基表和虚基表指针
3.继承和组合
一.模版
1.模版的基本概念
C++模板分为函数模板和类模板两种。
a.函数模版
函数模板:定义一个函数,该函数的参数可以是任意类型,当编译器调用该函数时,会根据提供的参数类型自动生成对应的函数实例。
b.类模板
类模板:定义一个类,我们可以向这个类传递任意类型,编译器在创建类的实例时,会根据提供的类型自动生成对应的类定义。
c.模版的实例化
当编译器遇到模板的使用时,它会根据提供的类型参数生成一个特定类型的模板实例,这个过程称为模板的实例化。
例如,在上面的main函数中,当使用Stack<int>时,编译器会生成一个Stack类的实例,其中T被替换为 int。同样地,当使用Stack<string>时,编译器会生成另一个Stack类的实例,其中T被替换为string。
注意:普通类的类名和类型相同,但是对类模版而言,类名是类名,类型是类型,域作用符使用的是类型!!
函数模版实例化如下:
显示实例化
d.class 和 typename的区别
在C++中,class 和 typename 都可以用于在模板定义中指定类型参数,但它们之间实际上没有功能性的区别。这两个关键字在模板参数列表中是可以互换使用的,用来指示随后的标识符是一个类型名称。
但是,当我们使用域作用访问限定符去访问类中的某个数据类型时,typename 的作用就显现出来了,示例如下:
只要使用没被实例化的模板类里面的typedef后的数据类型,都要在前面加一个typename,否则编译器报错!!
e.非类型模版参数
非类型模板参数指的是模板参数列表中不是类型参数的部分,它们可以是整数常量表达式、指针类型(指向对象或函数的指针,但通常指向对象的指针更常用)以及引用类型。
如图:
注意:
①非类型模板参数必须是编译时常量。
②对于整数类型的非类型模板参数,它们可以是有符号或无符号的整数类型,包括char、short、int、long以及它们的无符号版本。
③指针和引用类型的非类型模板参数必须指向或引用有效的对象或函数,并且这些对象或函数在模板实例化时必须是可访问的。
2.模版的特化
模板特化是一种对模板进行定制化的技术,它允许我们为模板的某个特定类型或值提供专门的实现。模板特化分为完全特化和半特化,但注意,对于函数模板来说,只有完全特化,没有半特化。
a.全特化 —— 参数类型全部特殊化处理
当模板实例化时,如果使用了与特化相匹配的类型或值,则编译器会使用特化的版本而不是通用的模板版本。
b.半特化 —— 部分参数特殊化处理
部分特化(仅适用于类模板)允许我们为模板的某些类型参数提供特定的实现,同时保留其他类型参数的通用性。
c.偏特化——对某些类型的进一步限制(实例化时传的也是指针类型)
3.类模版内函数的声明和定义分离
情景:我们在vector.h文件内对类模板内成员函数做出声明,在vector.cc文件内对模版类内函数做出定义,在test.cc文件内对类模版进行实例化,编译时出现链接报错,为什么??
---由于编译器对各源文件的编译在“链接”前都是无交织状态的,导致类内的模版函数在其他外部文件内定义时无法收到来自源文件传来的模版参数,无法完成模版参数的实例化,而涉及模版的东西,都是完成实例化后才能生成地址,所以在“链接”时,其它源文件就找不到模版函数定义的位置,最终报错!!
解决方法:
a.显示实例化——在其它源文件内声明 template class stack<int>; //但是这种方法限制了模版的作用,传入int类型,就会限制其它类型数据对该类的使用
b.将模版函数的定义放在类的同一个文件内。
c.将模版函数定义所在的文件,包含在使用该类的源文件内。
二.继承
1.继承的基本知识
情景:结构体和类里面的属性数据都是一样的,而在某些特定的场景下,需要所有人既要有这些共性属性,又要有一些个性化的属性。如:同一个班级内的学生,它们的:学校名、学院名、专业名、班级名...都是一样的,但像:学号、姓名、身份证号...等信息又都是不一样的。在这样的场景下,就不能用一个共有的结构体去描述学生的属性数据,这时,我们可以创建一个描述公共属性数据的结构体(基类),然后用个性化的结构体(派生类)去继承它。
什么是继承?--- 继承是一种面向对象编程的特性,它允许一个类(称为派生类或子类)继承另一个类(称为基类或父类)的属性和方法。通过这种方式,派生类可以重用基类的代码,并且可以添加新的属性和方法或者覆盖基类的方法。
a.继承的方式
公共继承:基类的公共成员在派生类中也是公共的,基类的保护成员在派生类中也是保护的。
保护继承:基类的公共成员在派生类中变为保护的,基类的保护成员在派生类中仍然是保护的。
私有继承:基类的公共成员和保护成员在派生类中都是私有的。
注意:①基类的私有成员在派生类中都是不可见的;②基类的公有成员在派生类中的权限类型是由继承的方式决定的。
b.构造函数和析构函数
构造函数:当创建派生类的对象时,首先会调用基类的构造函数。因此,如果基类有构造函数,那么派生类也需要显式地调用它(通过初始化列表)。
析构函数:当派生类的对象被销毁时,首先会调用派生类的析构函数,然后是基类的析构函数。
继承到的基类成员变量的声明是在派生类成员变量之前的,即基类的成员变量在初始化列表要先于派生类成员变量完成初始化!
派生类的拷贝构造给基类成员变成初始化时,若不显示传参,则调用基类的默认构造;若显示传参,则调用基类的拷贝构造。
派生类的赋值重载内,对基类成员变量完成赋值时,需要显示调用基类的赋值重载,这里需要注意“由于函数名相同,导致隐藏问题”,需指定类域!
注意:继承时,若不指定继承方式,则默认是私有继承!!
2.基类和派生类对象的赋值转换(前提:公有继承)
a.基类对象不能赋值给派生类对象
派生类对象包含了基类对象的所有成员,但还可能包含额外的成员。因此,将一个基类对象赋值给一个派生类对象会导致信息丢失或不一致,这是不允许的。
b. 派生类对象可以赋值给基类对象(切片)
派生类对象可以被赋值给基类对象,但这种情况下会发生对象切片,即派生类对象中特有的成员会被丢弃,只保留基类部分的成员。
注意切片和强制类型转换的区别:
与其它不同类型对象间的赋值规则(强制类型转换)不一样,派生类对象赋值给基类对象的过程,*不会产生临时对象,而是“赋值兼容转换”,类似“切割,切片”,即将子类对象中与父类对象重合的数据直接拷贝赋值给父类对象。
c.使用动态类型转换
在运行时,如果需要安全地将基类指针或引用转换为派生类指针或引用,可以使用dynamic_cast。如果转换失败,dynamic_cast会返回nullptr(对于指针)或抛出异常(对于引用)。
3.隐藏/重定义
"隐藏"通常指的是在继承关系中,派生类的成员函数或变量与基类中的同名成员发生冲突时,导致基类中的成员被隐藏。在派生类内,使用相同的成员名时,默认访问的是派生类自己的成员数据,而若想指定使用基类的成员,则需使用域作用访问限定符!
隐藏分为两种:成员函数隐藏和成员变量隐藏。
a.成员函数隐藏
如果派生类中定义了一个与基类成员函数同名的成员函数(即使参数列表不同),基类的成员函数在派生类的作用域内会被隐藏。
对成员函数而言,只要基类和派生类的成员函数名相同,无论参数如何,二者不构成函数重载,而构成“隐藏”关系!!
b.成员变量隐藏
如果派生类中定义了一个与基类成员变量同名的成员变量,基类的成员变量在派生类的作用域内会被隐藏。
注意隐藏成员的访问格式:d.Base::func(); d.Base::value;
由于多态,析构函数的函数名都会被特殊处理,统一处理成了destructor,导致基类的析构与派生类的析构形成“隐藏”关系,所以在派生类的析构内必须指定类域去调用基类的析构函数,但是为了保证析构顺序“先子后父”,编译器会特殊处理基类成员变量的析构,即调用完派生类的析构后,编译器会自动帮我们调用基类的析构,这样一来,我们对基类成员的消耗不必理会即可。
注意:①基类的友元函数不能继承给派生类;②派生类能够继承基类中的静态成员,与基类“共享”静态成员。
三.多继承(重难点)
一个子类继承两个或两个以上的父类时,这个继承关系称为“多继承”。如:class Assistant : public Student, public Teacher {}
示例如下:
1.菱形继承
菱形继承是指一个类从多个基类继承,而这些基类又共同继承自另一个基类。这可能导致数据冗余和二义性问题,因为派生类会有多个路径访问同一个基类成员。
上述的D类相当于间接的继承了两次A类,导致_d中含有两个_a成员变量,那么,我们该如何解决这种问题呢?换句话说,菱形继承所带来的数据二义性该如何解决?--- 虚继承!!
2.虚继承
通过在中间基类(即那些被多个基类继承的基类)的继承声明中使用 virtual 关键字,可以确保派生类只继承一个基类实例。这样,无论派生类通过哪条路径继承,它都只会看到一个基类实例。
a.虚继承的底层原理
虚继承的底层原理,菱形虚拟继承中各成员变量在内存中的排布:
好的,现在让我们来解释一下虚继承的底层原理,以及B和C中的那两个指针又是怎么回事儿~~
在编译器编译时,对于虚继承的派生类(如类B和类C),编译器会在其对象中添加一个指向虚基类实例的指针(虚基表指针),这个指针用于指向虚基类的唯一实例。
在派生类中,对于虚基类的成员访问都通过这个虚基类指针进行。这样,无论派生类通过哪个路径继承虚基类,都可以通过这个指针访问到虚基类的唯一实例,从而避免了菱形继承中的二义性问题。
b.虚基表和虚基表指针
虚基表指针通常在虚继承类成员变量的前面,指向类的虚基表,虚基表是因虚继承产生的,表内存放的是“虚基表指针的地址”与派生类继承的“基类成员变量”的偏移量,可通过 虚基表指针+表内偏移量 的方式找到派生类所继承的基类成员变量的位置!
多个虚继承类对象共用一个偏移量表(虚基表),偏移量的存在保证了“切割”时对数据的成功读取。
虚继承的类,其成员变量在内存中的存储也会发生改变,所继承的基类成员变量在内存中,以指针的形式指向一个偏移量表,其内记录了基类成员在内存中与该指针的相对偏移位置。
注意:成员变量在内存地址方面的存储,基类成员且先继承的在最前面!虚拟继承对基类成员的初始化的顺序是由类声明的顺序决定的!
3.继承和组合
继承是面向对象编程中的一个基本概念,它允许一个类继承另一个类的属性和方法。继承主要用于表示“是一种”(is-a)的关系。
组合是一种将一个类的对象作为另一个类的成员的方式。组合用于表示“有一个”(has-a)的关系。
继承:class D : public C{}; 组合: class E { private : C _cc; };
在继承方式中,基类的内部细节对派生类可见,继承在一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响,派生类与基类间耦合度较高。
而基类中protected的成员仍能被派生类继承,但无法被组合类使用,这在一定程度上降低了基类与组合类间的耦合度。
而组合类之间没有很强的依赖关系,耦合度低,优先使用对象组合有助于我们保持每个类被封装。