C/C++复习 day2(模板,继承,多态)

C/C++复习 day2



前言

C/C++复习day02


一、模板

虽然模板的一些功能函数重载也可完成,但是。

  1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函
  2. 代码的可维护性比较低,一个出错可能所有的重载均出错。

为了实现泛型编程,因此引入模板。

1.模板的原理

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的参数类型来推演生成对应类型的函数。
也就是说,函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。

2.非类型模板参数

即在模板参数列表中,用一个常量作为类模板参数的值。

template<class T, size_t N = 10>
 class array
 {
 public:
 T& operator[](size_t index){return _array[index];}
 const T& operator[](size_t index)const{return _array[index];}
 
 size_t size()const{return _size;}
 bool empty()const{return 0 == _size;}
 
 private:
 T _array[N];
 size_t _size;
 }

注意

  1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
  2. 非类型的模板参数必须在编译期就能确认结果。

3.模板的特化

template<class T>
bool Less(T left, T right)
{
 return left < right;
}

比如这段代码,我们定义了一个比较函数。
当我们正常传值时会正常比较,但是如果我们传的是地址呢?
这个函数就会按照地址大小去比较,但我们希望按值去比。
因此,我们就需要对这些进行一个特殊处理。

a. 函数模板的特化

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
template<class T>
bool Less(T left, T right)
{
 return left < right;
}
template<>
bool Less<int*>(int* a,int* b)
{
	return *a<*b; 
}

b. 类模板的特化

1.全特化

即将类模板参数列表的所有参数都确定化。

template<class T1, class T2>
class Data
{
public:
 	Data() {cout<<"Data<T1, T2>" <<endl;}
private:
 	T1 _d1;
	 T2 _d2;
};
template<>
class Data<int, char>
{
public:
 	Data() {cout<<"Data<int, char>" <<endl;}
private:
 	int _d1;
 	char _d2;
}
2.偏特化

将第二个模板特化成int

template <class T1>
class Data<T1, int>
{
public:
 	Data() {cout<<"Data<T1, int>" <<endl;}
private:
	 T1 _d1;
 	int _d2;
};

除此之外还可进行进一步的限制,在此不过多赘述。

4.模板的分离编译

// a.h
template<class T>
T Add(const T& left, const T& right);

// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
 return left + right;
}

// main.cpp
#include"a.h"
int main()
{
 Add(1, 2);
 Add(1.0, 2.0);

 return 0;
}

以上场景,模板函数定义写在.h头文件中,实现写在a.cpp文件中,而调用又在另一文件中,这样就会出现问题,编译报错。

解决的方法:最好是将其声明和定义写到一个文件中(xxx.hpp或者xxx.h)

二、继承

1.继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
在这里插入图片描述

2.继承与派生类对象赋值转化

转化规则:

  1. 派生类对象可以赋值给基类的对象/指针/引用。这里有个形象的说法称为切片,比喻将派生类中父类的那一部分切出来,赋值给基类。
  2. 基类对象不能赋值给派生类。
  3. 基类的指针或者引用可以通过强制类型转化赋值给派生类的指针或者引用。但基类的指针或者引用必须是指向派生类对象才可以这样处理。
    可以使用RTTI(Run Time Type Information)的dynamic_cast识别后进行安全转化。

3.隐藏

1.成员变量的隐藏

子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)

class Person
{
protected :
 	string _name = "小李子"; // 姓名
 	int _num = 111;   // 身份证号
};
class Student : public Person
{
public:
 	void Print()
	 {
	 cout<<" 姓名:"<<_name<< endl;
	 cout<<" 身份证号:"<<Person::_num<< endl;
	 cout<<" 学号:"<<_num<<endl;
 }
protected:
 	int _num = 999; // 学号
};

这里Person类和Student类中 _num变量构成了隐藏。

2. 成员函数的隐藏

基类和子类中成员函数,只要函数名相同,并且不构成重写,则就构成隐藏。

B中的fun和A中的fun不是构成重载,因为不是在同一作用域
B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
	 void fun()
	 {
	 cout << "func()" << endl;
	 }
};
class B : public A
{
public:
	 void fun(int i)
	 {
	 A::fun();
	 cout << "func(int i)->" <<i<<endl;
	 }
};

4.继承中的友元

  1. 友元关系不可被继承:基类的友元函数不能直接访问派生类的私有和保护成员。
    例如,如果 class A 有一个友元函数 friend void func(A& a) ,那么这个友元函数不能直接访问从 A 派生的 class B 的私有和保护成员。
  2. 友元函数不能被派生类继承为友元关系:派生类不能自动继承基类友元的友元关系。
    假设 func 是 A 的友元,对于派生类 B 来说,func 不是 B 的友元。
  3. 友元关系的非传递性:如果 class C 是 class B 的友元,class B 是 class A 的友元,不能得出 class C 是 class A 的友元。
    这种特点保证了友元关系的明确性和安全性,防止了不必要的访问权限扩散。

5.继承与静态变量

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

6.多继承

在现实生活中,例如一位学生,他同时具有人的属性,也具有学生的属性。因此这名学生最起码有两个的直接父类。因此祖师爷引入了多继承这个概念来更好的面向对象编程。
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
在这里插入图片描述

1.菱形继承

在这里插入图片描述
如上图所示,Assistant的对象中会出现两份Person成员。
这就说明了菱形继承的两个问题:数据冗余和二义性

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";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	 a.Student::_name = "xxx";
	 a.Teacher::_name = "yyy";
}

由此可知,二义性我们可以通过显式指定访问来完成,但是数据冗余我们无法解决。

2.菱形虚拟继承

为了解决数据冗余问题,引入了菱形虚拟继承。
例如类A,B,C,D。B,C继承了A,D继承了B,C。
我们可以通过调试来看出在这里插入图片描述
上图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C。
那么B和C如何去找到公共的A呢?
这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的是偏移量。通过偏移量可以找到下面的A。

7.继承和组合

  1. public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
    组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  2. 优先使用对象组合,而不是类继承 。
  3. 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
    为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
    内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
    大的影响。派生类和基类间的依赖关系很强,耦合度高。
  4. 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
    来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
    用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
    组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
    封装。
  5. 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
    些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
    继承,可以用组合,就用组合。

三、多态

1.什么是多态?

1.静态多态(绑定)

静态多态即为函数重载。
底层是编译时通过在函数名添加参数类型来识别不同的函数重载。

2.动态多态(继承中的多态,动态绑定)

构成的条件
  1. 必须通过基类的指针或者引用调用虚函数。
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

2.虚函数

虚函数:即被virtual关键字修饰的函数即为虚函数。

class Person {
public:
	 virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

1.虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

class Person {
public:
	 virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	 virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

注意:派生类中的虚函数在重写时可以不加virtual关键字。但这种写法不太规范,不建议使用。

2.虚函数重写的意外

a.协变

当两个函数构成虚函数时,并且基类与子类的返回值类型分别对应一个基类和子类的指针或引用时,就构成了协变。

class A{};

class B : public A {};

class Person {
public:
	 virtual A* f() {return new A;}
};

class Student : public Person {
public:
	 virtual B* f() {return new B;}
};
b.析构函数的重写

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同。
虽然看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

class Person {
public:
	 virtual ~Person() {cout << "~Person()" << endl;}
};

class Student : public Person {
public:
	 virtual ~Student() { cout << "~Student()" << endl; }
};

3.final和override

a.final

final修饰虚函数,表示该虚函数不能被重写。

class Car
{
public:
 	virtual void Drive() final {}
};

class Benz :public Car
{
public:
 	virtual void Drive() {cout << "Benz-舒适" << endl;}
};

例如这样就会出现问题。

b.override

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car{
public:
 	virtual void Drive(){}
};

class Benz :public Car {
public:
 	virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

4.重载,重写(覆盖),隐藏(重定义)

在这里插入图片描述

3.抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
	virtual void Drive() = 0;
};

class Benz :public Car
{
public:
	 virtual void Drive()
	 {
	 cout << "Benz-舒适" << endl;
	 }
};

class BMW :public Car
{
public:
	 virtual void Drive()
	 {
	 cout << "BMW-操控" << endl;
	 }
};

4.多态的原理

虚函数表
总结:是通过存放虚函数表来实现的。
虚函数表是在编译阶段生成的,一般情况下存放在代码段(常量区)。

5.多态总结

内联函数不能是虚函数。
一个类中的不同对象共用同一张虚表。

总结

以上就是我的C/C++day2总结。
本人小白一枚,有问题还望各位大佬指正。

  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值