目录
类模板的特化
全特化
template<class T1,class T2>
class Date {
private:
T1 _d1;
T2 _d2;
public:
Date() {
cout << "Date(T1,T2)" << endl;
}
};
毫无压力的可以执行,但当我们想,给特定类型会有特定结果的时候我们应该怎么做
比如,我们给T1为int , T2为double,并且希望它两能跟常规的类模板有不一样的行为
template<>
class Date<int,double> {
private:
int _d1;
double _d2;
public:
Date() {
cout << "Date(int,double)" << endl;
}
};
给它来一下特化
偏特化
特化部分参数
只要我的一个参数是我要的类型,我就会走偏特化
//偏特化——特化部分参数
template<class T1>
class Date<T1, char> {
private:
T1 _d1;
char _d2;
public:
Date() {
cout << "Date(T1,char)" << endl;
}
};
例如上述代码,只要我的第二个参数是char,那么无论我的第一个参数是什么,我都会走这个偏特化
对参数类型进行一定的限制
偏特化——特化参数类型
template<class T1,class T2>
class Date<T1*, T2*> {
public:
Date() {
cout << "Date(T1*,T2*)" << endl;
}
};
template<class T1,class T2>
class Date<T1&, T2*> {
public:
Date() {
cout << "Date(T1&,T2*)" << endl;
}
};
当谁都匹配不上的时候,就用原模版
关于*&的讨论
在我们实现stack_queue的时候
template<class T>
class less {
public:
bool operator()(const T& x, const T& y) {
return x < y;
}
};
在有特定的类型之后,比如
template<class T>
class less<T*> {
public:
bool operator()(const T* x, const T* y) {
return *x < *y;
}
}
那为什么不能
template<class T>
class less<T*> {
public:
bool operator()(const T*& x, const T*& y) {
return *x < *y;
}
}
首先我们需要回顾一下,const修饰有指针的时候,const在*之前,修饰x指向的内容不能修改,const在*之后,即x所代表的指针不能够修改
Date* 和 int* 能够传给const T* x 和 const T* y ,因为只是涉及到了权限的缩小,允许通过
但是却不能传给第三种的const T*& x, const T*& y,为什么呢?
因为在本质上就是发生了类型转换,类型转换会发生临时变量,临时变量又是具有常性,可读不可写,又因为,注意啊,const 修饰的只是x 指向的内容不能够修改,并没有对引用进行限定,所以会出现权限放大的问题,因此这种行为是不被支持的
彼此之间的关系还可以转化为:
int* p1
const int* p2 = p1;
const int* &p3 = p1;
如果要改变其现状,我们需要
原来的
bool operator()(const T* x, const T* y){...}
变为
bool operator()(const T* const & x, const T* const & y){...}
我们在使用引用&的目的就是为了减少拷贝,减少开销,但是一个指针才4个或者8个字节,给你开了又怎么样嘛,所以还是不是很建议在这里实现引用
特化的优先级
那么,当我们既有偏特化,又有全特化的时候,它不会矛盾吗,它应该走谁呢?
模板的特化本质就是参数的匹配,它会优先匹配全特化,因为全特化不用去推演啊,有现成的给你谁还想去推
类模板的声明和定义分离
先给出三个文件,Stack.h , Stack.cpp , test.cpp
// Stack.h
#pragma once
#include<iostream>
using namespace std;
template<class T>
T add(const T& left, const T& right);
void func(int a,int b);
//Stack.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
template<class T>
T add(const T& left, const T& right) {
cout << "T add(const T& left, const T& right)" << endl;
return left + right;
}
void func(int a, int b) {
cout << "void func(int a, int b)" << endl;
}
//test.cpp
int main() {
add(1, 2);
func(1, 2);
}
编译运行的话,会发现编译错误
在预处理的时候,函数声明给出了空头支票
test.i 知道add 要实例化成int,但是test.i没有模板
stack.i有模板,但不知道要实例化成int,因为stack不知道要实例化成int,所以在接下来的汇编和链接的过程中,就不会形成int的函数表,然后test.o拿着空头支票去找stack.o来链接,发现根本找不到它的相关函数表
解决方法就是显示实例化
它跟特化比较像,不要混淆了
缺陷就是:使模板化显得更加麻烦,更加不便,因为当我更换不同的类型的时候,就又会不支持了,很呆很无力
实践当中我们都是从来不分离的,即不要定义和声明,直接定义在.h里
模板的本质就是本来应该由我写的多份类似代码,现在可以直接借助模板实例化,帮我们解决了
继承初学
继承概念理解
继承 (inheritance) 机制是面向对象程序设计 使代码可以复用 的最重要的手段,它允许程序员在 保 持原有类特性的基础上进行扩展 ,增加功能,这样产生新的类,称派生类。继承 呈现了面向对象 程序设计的层次结构 ,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用, 继 承是类设计层次的复用
比如,在设计一个教学系统的时候,我们分别会定义学生跟老师的属性
class Student {
private:
string name = "张三";
string gender = "男";
size_t age;
string classID;
};
class Teacher {
private:
string name;
string gender = "男";
size_t age;
};
我们会知道,这个两个类里有重复的信息,我们就可以通过父类的方式,将共有的信息提取出来,放在一个父类里,让这两个类继承父类就好了,自己只需要定义自己独有的成员即可
父类
class Person {
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
cout << "gender:" << _gender << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; //年龄
string _gender = "男";
};
继承方式
子类:继承方式 父类
class Student:public Person {
protected:
string _stuid; //学号
};
class Teacher:public Person {
protected:
string _jobid; //工号
};
私有成员在派生类不可见,这里的不可见是指用不到
继承权限
公有和保护成员在派生类,取权限小的
类成员 / 继承方式 | public 继承 | protected继承 | private继承 |
---|---|---|---|
基类的 public 成员 | 派生类的 public 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
基类的 protected 成员 | 派生类的 protected 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
基类的 private 成 员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可 见 |
将父类成员由 protected 改为 private后,子类便不能够访问,验证不可见
private后,就相当于私房钱
继承切割与切片
知识回顾
int i = 0; double d = i;
这两句话在类型转换的时候会产生临时变量,临时变量又是个常量
所以, double& r = i; 是行不通的,因为,i是int类型,转换为double类型的时候涉及到的是类型变化,产生了一个临时变量,临时变量又是个常量,只能读,而引用r又是可读可写,所以涉及到了权限放大,这是绝对不允许的
不同类型之间的相互比较:(以char 和 int)在汇编指令的过程中,比较一般都是通过调用cmp这个指令,但是cmp这个指令又是只能相同类型之间进行比较,所以char和int不能够直接比较
需要对char变量进行类型提升,但我们需要注意的是,我们并不是直接对char类型的这个变量直接提升(它一个字节就是一个字节,不可能将它变为4个字节),而是产生4个字节的临时变量,高位补0,最后与int比较。
子类对象给予父类可以称为切割或者切片
class Person {
public:
void func() {
}
protected:
string name; //姓名
private:
int _age;
};
class Student :public Person {
protected:
int _stunum; //学号
};
int main() {
Student s;
Person p = s;
return 0;
}
这段代码中,Person p = s; s(子类)会将 p (父类)没有的对象切走,剩余的拷贝给 p
Person& r = s;
它所代表的意义是,并不是将子类s当成是父类r 的别名,而是子类s切出来的跟父类成员一样的成员才是r的别名,它不产生临时变量
父类是不能给子类的(不是绝对)
继承的作用域
1. 在继承体系中 基类 和 派生类 都有 独立的作用域 。 2. 子类和父类中有同名成员, 子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。 (在子类成员函数中,可以 使用 基类 :: 基类成员 显示访问 ) 3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。 4. 注意在实际中在 继承体系里 面最好 不要定义同名的成员 。既然他们是各自独立的作用域,那么父类和子类能有能同名的成员?
可以有同名的成员
class Person {
public:
void Print() {
cout << _name << endl;
}
protected:
string _name; //姓名
int _id = 2;
private:
int _age;
};
class Student :public Person {
public:
void func() {
cout << _id << endl;
}
protected:
int _stunum; //学号
int _id = 1;
};
在protected中,父和子同时拥有_id 成员,那么在调用的时候是调用父类的对象还是调用子类的对象呢,我们可以进行测试,分别给子类的_id = 1,父类的_id = 2来进行验证,调用s.func();
可以看出访问的是子类,因为它会遵循一个原则,就是就近原则,先回去到子类里寻找,之后才会到父类中寻找
那如果我想要访问父类的这个对象呢?给_id加上说明,指定一下作用域
void func() {
cout << Person::_id << endl;
}
这种现象除了就近原则,同时也是隐藏作用,子类会隐藏父类的同名成员,但是一般不推荐用同名成员。
那如果子类和父类同时拥有相同的函数名,那么它们之间又会构成什么关系,类如
class A{
public:
void fun(){}
};
class B : public A{
public:
void fun(int i){}
};
此间,函数名相同,参数不同,肯定会觉得就是重载
但是重载的要求是什么?要在同一作用域啊!!!
所以它们绝对不可能是重载,父类和子类两个都是独立的作用域
那么它们之间的关系是什么呢?
如果是成员函数:函数名相同就能构成隐藏,且返回值和参数可以不相同,实践当中也不建议同名成员
那如果进行调用它会怎么调用呢?
父类的成员函数
void fun() {
cout << "I'm father's fun()" << endl;
}
子类的成员函数
void fun(int i) {
cout << "I'm child's fun()" << endl;
}
main函数
Student s;
s.fun(1);
那尝试一下调用父类对象,s.fun()
可以发现,编译器并没有找到父类的fun(),原因就是子类将其隐藏了
如果想要调用的话,那我又该如何 :加限定符
继承的默认构造成员函数
在生成基本构造函数的时候,子类不能够代替父类完成一些初始化,析构,拷贝等函数工作
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; // 姓名
};
现在调用一下子类,发现在调用时,子类会自己往上找父类的构造和析构
继承的默认构造
如果我要自己显示的去写子类的构造函数呢?
已经说的明明白白了,父类的_name不允许这样去初始化,就算直接指定作用域也没有办法,而且也没必要其指定作用域,因为指定作用域只跟它有没有隐藏有关,你都没有跟我同名,你隐藏我什么,对吧
派生类的构造只要求一条:自己管好自己就可以了,不能干涉父类的构造
如果要改变父类的话,也只能通过父类去改
Student(const char* name,int id)
:_id(id)
,Person(name)
{}
总结就是子类的这些默认成员函数的规则,跟我们以前学的默认函数规则类似,唯一不同的只是多了父类的那一类,父类那部分调用父类的函数
继承的拷贝构造
如果我们要自己写拷贝构造呢?
一样,子类不能帮父类完成,要将父类叫来让它自己完成
Student(const Student& s)
:Person(s)
,_id(s._id)
{}
其中,Person(s) 就是前面说的子类切割给父类
继承的赋值重载
子类的复制重载
Student& operator=(const Student& s) {
if (this != &s) {
operator=(s);
_id = s._id;
}
return *this;
}
如上述代码所示,运行一下
调用之后,代码会一直运行,理由就是父类的operator=被子类刚写的给隐藏住了,所以它只有调用子类的,一直掉一直循环,一直叠堆栈,所以一直走不出去,最终失败
子类和父类的operator=函数名相同,类域不同,形成隐藏关系
想要让其得到正确的赋值,应该给它找出来,即加上前缀,表明我要用父类的函数
Student& operator=(const Student& s) {
if (this != &s) {
Person::operator=(s);
_id = s._id;
}
return *this;
}
继承的析构
子类的析构函数和父类的析构函数构成隐藏关系,欸就会问,它两名字不是不一样吗,为什么还是隐藏关系
由于后来多态的原因,析构函数到后面都会被特殊处理,函数名都会被处理为destructor()
所以,它两就构成了隐藏关系
所以,想显示调用的话,就需要这么来 Person::~Person();指定一下
但为了保证先析构子类,即父类的析构在最后析构,父类的析构会在子类的析构后会自动调用
所以一般不去显示调用这个函数
为什么需要保证先子后父呢?
怕子类用了父类的成员,当父类调用析构的时候,子类析构又去访问父类的相关成员,最后可能会导致访问野指针的情况
析构函数的主要作用是清理资源
复杂的菱形继承及菱形虚拟继承
这种是属于单继承呢还是属于多继承呢?
此种现象是属于单继承,单继承是指一个子类只有一个直接父类的继承关系,注意是直接
这种才是属于多继承,一个子类有两个或多个直接父类时称这个继承关系为直接父类
菱形继承,Student 类和 Teacher类 都继承有Person类的 _name 成员,那么当助教类去多继承Student 类和 Teacher类时,同样也会继承它两的 _name 成员,产生的后果就是会继承冗余和二义性,我有两个_name,无法判断继承的是哪个_name
新加一下Teacher类 和 assistance 类
class Teacher : public Person
{
protected:
int _id;
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse;
};
创建assistance 类对象
解决办法就是,加上前缀声明
我们便知道了,他在学生类的对象里是叫做助教哥哥
在老师们眼里就是个打工仔
那么,他就是助教哥哥呢,还是打工仔呢,或者是他究竟自己叫什么名字,我们不知道哇,他已经继承了两个类,却无法真正做到独立成员,以后有电话号码或者是家庭住址的话,也同样需要标注这是Student类严重的电话和住址,同样需要标注这是Teacher类眼中的家庭电话和住址,显得及其麻烦且不适用,这就是菱形继承的大弊端
那么如何解决,让我这个助教类的成员不在受以后两个或者多个父类的影响呢,答案是加vitural关键字,且是在父类加
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
这种行为就是将 string _name 将其从类内拿了出来,单独放在里面
菱形继承并不是就是说非要菱形
上述也是菱形继承,且virtual需要加在BD位置
菱形继承的底层探究
探究有无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;
};
D dd;
dd.B::a = 1;
dd.C::a = 2;
dd.b = 3;
dd.c = 4;
dd.d = 5;
我们在监视窗口看不出它的空间结构变化,我们可以打开内存窗口看一下
可以看到 B类继承的a 与 B类的b 紧紧挨着一块,C类继承的a 与 C类的 c紧紧挨在一块,最后一行是d类自己的d ,值为5
现在添加虚继承看看效果
虚继承后的a的地址明显跟 B类和C类分开了
正好验证上图
那下面b和c的值为什么又会被其它值所阻挡,这些值又是什么呢?
这些值就是偏移值的指针,记得是指针,它指向的内容就是虚继承下来的a距离B类和C类有多远的数值。
(内存窗口一打开,或者在打了断电之后按下一步的时候是看不到我想要看到的内存,得在内存输入框中输入你想看的内存,再来按下一步)
为什么存着的是地址而不是偏移量,因为以后可能不止有他一个对象,还有其它对象
就是这是一张偏移量表,并不是就指向dd的偏移量,它还可能指向其他的偏移量值
组合和继承
继承和组合都是复用,组合是一种黑箱复用,继承是一种白箱复用,实践中用组合会更好一点,更适配于低耦合高内聚
组合的耦合度明显会比继承低,高耦合相连度高,互相影响的可能性大,低耦合就相反
就好比,我父类有100多个公有成员对象,我现在要修改99个,是不是其下的子类也要跟着修改。
而组合就直接把自己包装了就ok了
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用 (white-box reuse) 。术语 “ 白箱 ” 是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。 继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。 派生类和基类间的依赖关系很强,耦合度高。 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复 用 (black-box reuse) ,因为对象的内部细节是不可见的。 对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。 优先使用对象组合有助于你保持每个类被 封装。 实际尽量多去用组合。 组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,可以用组合,就用组合。以上就是本次博文的学习内容,如有错误,还请各位大佬指点,谢谢阅读!
标签:const,继承,子类,C++,int,父类,public,特化 From: https://blog.csdn.net/2301_76219154/article/details/141956541