首页 > 编程语言 >C++【全特化】【半特化】【继承方式权限】【继承使用】【菱形继承的探究】【组合与继承的讨论】

C++【全特化】【半特化】【继承方式权限】【继承使用】【菱形继承的探究】【组合与继承的讨论】

时间:2024-09-15 21:21:04浏览次数:13  
标签:const 继承 子类 C++ int 父类 public 特化

目录

类模板的特化

全特化

偏特化

特化部分参数

对参数类型进行一定的限制

关于*&的讨论

特化的优先级

类模板的声明和定义分离

​编辑

继承初学

继承概念理解

继承方式

继承权限

继承切割与切片

继承的作用域

继承的默认构造成员函数

继承的默认构造

继承的拷贝构造

继承的赋值重载

继承的析构

复杂的菱形继承及菱形虚拟继承

菱形继承的底层探究

探究有无virtual修饰的内存空间结构

组合和继承

  


类模板的特化

全特化

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

相关文章

  • C++入门基础知识69(高级)——【关于C++ 动态内存】
    成长路上不孤单......
  • C++入门基础知识68(高级)——【关于C++ 异常处理】
    成长路上不孤单......
  • C++ decltype 类型知识符
    希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。decltype(f())sum=x;//sum的类型就是函数f的返回类型编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。decltype处理顶层const和引用的方式与auto有些许不同......
  • Java入门:09.Java中三大特性(封装、继承、多态)01
    1封装面向对象的三大特征:封装,继承,多态。封装可以从三个层面理解将属性和方法组合在一起(封闭在一起)将属性隐藏起来,对外提供可以间接操作属性的方法。(提高程序设计安全性)目前我们都是属性私有化,并提供与之对应的get和set方法封装应用工具,为其他的程序员提供......
  • C++ auto 类型推断注意的地方
    inti=0,&r=i;autoa=r;//a是一个整数(r是i的别名,而i是一个整数)intaauto一般会忽略掉顶层const(参见2.4.3节,第57页),同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:constintci=i,&cr=ci;autob=ci;//b......
  • C++的习题
    C++的习题类与对象习题1:(const成员函数)假设AA是一个类,AA*abc()const是该类的一个成员函数的原型。若该函数返回this值,当用x.abc()调用该成员函数后,x的值是()A.可能被改变B.已经被改变C.受到函数调用的影响D.不变A.此成员函数被定义为const常方法,代表在......
  • C++中对象的延迟构造
    本文并不讨论“延迟初始化”或者是“懒加载的单例”那样的东西,本文要讨论的是分配某一类型所需的空间后不对类型进行构造(即对象的lifetime没有开始),更通俗点说,就是跳过对象的构造函数执行。使用场景我们知道,不管是定义某个类型的对象还是用operatornew申请内存,对象的构造函数都......
  • C++ typedef 类型别名注意的地方
    指针、常量和类型别名如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。例如下面的声明语句用到了类型pstring,它实际上是类型char*的别名typedefchar*pstring;constpstringcstr=0;//cstr是指向char的常量指针constpstring*ps;......
  • 矩阵连乘(动态规划)(C/C++)最详尽代码注释
    写在所有的前面:本文采用C/C++实现代码目录写在所有的前面:题目说明题目题目出处题目描述Description输入Input输出Output样例Sample限制Hint解答说明方案1:最优分隔点法(动态规划)解题思路代码实现c语言头文件:c++头文件主代码部分:(详尽版本1)主代码部分(题目对应版本)其他解......
  • C++链接的那些事
    接上文OK!Rightnow!  Let's go!今天我们来谈谈链接,什么是链接,C++链接实际上做什么的?链接是一个过程,当我们从源C++文件转到实际的可执行文件(二进制文件)。第一阶段是编译源文件,一旦我们把文件编译好,就需要通过一个叫做链接的过程,现在链接的主要工作是找到每个符号和......