C++的多态

虽然没有学过Java,但是很喜欢Java的干净、统一,没有特别多的意外情况。因为Java里“一切皆是对象”。所有的类型都继承自Object类型,所以能写出一个适用于一切类型的函数。这是一个良好的实践,符合设计模式的基本特征。

所以当我们看许多C++框架的时候,这个框架都会有一个Object类,框架里所有的类都继承自这个类——这也算是自成一个小系统了。

但是,在写C++程序时,我时常会遇到一个问题——并不是一切皆是Object。当我们费尽心思写出一个能够处理所有类型的函数时,譬如:

template<class T> void f(T t)
{
第一步,调用t的构造函数
第二步,XXXX
}

我们发现,写函数的时候,都会对参数有某些假设,譬如,所有的参数都可以加减乘除,所有的参数都可以调用构造函数。没错,如果所有的类型都由我们自己来定义,确实没问题。但是——C++有自己的原生类型,C++的POD(Plain Old Data),也就是标量类型或传统类型。

当我们写出一个函数时,世界一片和谐一片大好。但是,突然发现,有那么一点不和谐。有一点东西就是纳入不到我们的框架之内,这时候我们需要单独地、特别地来处理这些不和谐的因素。

上述所说的“一切皆对象”是所有面向对象编程语言都提供的东西。通过继承来实现运行时多态。但是C++提供了函数重载和模板偏特化这两种工具来实现编译时多态。相比于运行时多态,编译器多态比较麻烦,但是随度更快。

1. 函数重载

如果同一作用域内的几个函数名字相同但形参列表不同,我们称为重载函数(overloaded)。
而掌握函数重载的关键是掌握函数匹配的原则。

1.1 函数匹配

  1. 第一步是选定本次调用对应的重载函数集,也就是候选函数。候选函数有两个要求:一是与被调用函数同名;二是其声明在调用点可见。
  2. 根据实参,从候选函数中选择可行函数。可行函数有两个特征:一是形参数量与本次调用提供的实参数量相等;二是每个实参的类型与对应形参类型相同,或者是能转换成形参的类型
  3. 寻找最佳匹配,其基本思想是:实参类型与形参类型越接近,匹配得越好。而这就是函数重载发挥作用的地方。
  4. 如果找不到唯一的最佳匹配,则产生二义性编译错误。

而函数重载的难点在于,实参类型可以转换成形参类型时,如何确定最佳匹配。

1.2 C++中的类型转换

  • 由const引起的类型转换。
    • 对于顶层const来说,譬如const int,与int类型编译器同等对待,两者等价;
    • 对于底层const来说,其实主要指的是指向常量的指针对常量的引用,允许指向非常量指针/对非常量的引用转换为指向常量的指针/对常量的引用。
  • 由于继承引起的类型转换。
  • 算术转换,也就是算术类型之间的转换。
    • 整型提升。把小整数类型转换为较大的整数类型。
    • 其他算术类型转换。
  • 数组与指针的等价性
  • C++规定的指针转换方式:nullptr可以转为任意指针类型;而任意指针可以转为const void*,任意指向非常量的指针可以转为void*。
  • 类类型转换。这是由重载类型转换运算符来完成的。编译器每次只能执行一次。可以置于上述标准的类型转换之前和之后。

1.3 函数匹配的最佳匹配等级

  1. 精确匹配:分情况:
    • 实参类型与形参类型相同;
    • 实参从数组类型或函数类型转换成指针类型
    • 实参添加/删除顶层const。
  2. 通过const转换实现的匹配;
  3. 通过类型提升实现的匹配(有int型形参,有short形参,则char直接提升为int)
  4. 通过算术类型转换实现的匹配,所有转换等级都一样;
  5. 通过类类型转换实现的匹配。

1.4 补充说明

当函数重载与类这两个概念发生联系的时候,我们需要有几点问题需要注意。

void f(int);
class A
{
void g(){f(3);}
void f(double);
}

上述情况下,成员函数g中调用的f是哪一个?

名字查找(name lookup):

  • 在名字所在块中寻找其声明语句,只考虑在名字的使用之前出现的声明;
  • 如果没找到,继续查找外层作用域;
  • 最终没找到,程序报错。

这就牵扯到了类中名字查找的一些特点:

类作用域的特点是:编译器分两步来处理:首先编译成员的声明,然后才轮到成员函数体。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。

成员函数中使用的名字解析方式如下:

  • 在成员函数内查找该名字的声明。和前面一样,只有在函数使用之前出现的声明才被考虑。
  • 如果成员函数内没有找到,则在类内继续查找;
  • 如果类内也没有找到,在成员函数定义之前的作用域内继续查找。

第二个问题是由运算符重载引起的。

a sym b

可能调用的是:

a.operatorsym(b);//a 的成员函数
operatorsym(a, b);//普通函数

当在表达式中使用重载的运算符时,要考虑上面两种情况。

2. 模板

2.1 模板与函数重载

就像引入类会给函数重载带来一些新的规则;引入模板也会给函数重载带来一些新的规则:

  • 对于一个调用,其候选函数包括所有的模板参数推断成功的函数模板实例;
  • 如果同样好的函数中只有一个是非模板函数,则选择此函数;
  • 如果同样好的函数中没有非模板参数,而有很多函数模板,且其中一个模板比其他模板更特例化,则选择此模板。(没有他的话,本质上(const T&)可以用于任何类型);
  • 否则,程序报错

ps. [具体模板参数推断、类型转换等等细节,不在此讨论]

2.2 函数模板的特例化

通用模板:

template<typename T> int compare(const T&, const T&);

特例化示例:

template<>
int compare(const char* const &p1, const char* const &p2)
{
    return strcmp(p1, p2);
}

一个特例化版本本质上是一个实例,而非函数名的一个重载版本。因此,特例化不影响函数匹配

2.3 类模板特例化(全特例化)

通用情况:

template<typename T>
class hash
{
}

特例化:

template<>
class hash<SaleData>
{
}

2.4 类模板部分特例化

不指定所有模板参数实参。只提供一部分模板参数或是参数的一部分而非全部特性。

通用版本:

tempalate<typename T> class remove_reference
{
typedef T type;
}

部分特例化版本:

template<typename T> class remove_reference<T&> //左值引用
{
typedef T type;
}

3. 应用

参考侯捷的《STL源码剖析》。

3.1 迭代器设计

现在我们需要这么一个功能,对于函数f,输入为迭代器,输出为迭代器的值。
这在C++11中很简单:

1 template<typename It>
2 auto f(It beg, It end) ->decltype(*beg)
3 {
4     return *beg;
5 }

但是在这之前,没有auto与decltype的时候,应该怎么实现呢?
我们假设所有的迭代器都有:

template<class T> class Iter
{
typedef T value_type;
}

那么就可以实现:

template<class It>
typename It::value_type f(It it)
{
return *it;
}

那么问题来了,我们的程序基于一个假设:所有的迭代器有value_type。
然而C++原生迭代器——指针——就不符合这个假定。
怎么办?需要使用特例化:

template<class T> T f(T* it) //迭代器是指针的情况
{
    return *it;
}

3.2 POD类型处理

现在需要一个功能,将迭代器[first, last)的内容填充为类型为T的val。
我们打算对于类型T调用构造函数;
然而,问题是int,float等等类型不需要构造函数,所以我们需要根据val来判断类型——而这正是C++所欠缺的。
那么我们可以针对这些POD类型进行特例化:

struct true_type{};
struct false_type{};

对于所有的类型有:

template<class Type>
struct type_trait
{
typedef false_type Is_POD;
}

针对char,int等POD类型有:

template<>
struct type_trait<char>
{
typedef true_type Is_POD;
}

那么,可以实现函数:

template<class It, class T>
void fill(It first, It last, const T& val)
{
fill_aux(first, last, val, value_type(first));
}
template<class It, class T, class T1>
void fill_aux(It first, It last, const T& val, T1*)
{
typedef typename type_trait<T1>::Is_POD is_POD;
fill_aux2(first, last, val, is_POD());
}
template<class It, class T>
void fill_aux2(It first, It last, const T& val, true_type)
{
for(auto it=first;it!=last;++it) *it =val;
}

template<class It, class T>
void fill_aux2(It first, It last, const T& val, false_type)
{
for(auto it=first;it!=last;++it) construct(&*it,val);
}
【为什么还需要学习C++?】 你是否接触很多语言,但从来没有了解过编程语言的本质?你是否想成为一名资深开发人员,想开发别人做不了的高性能程序?你是否经常想要窥探大型企业级开发工程的思路,但苦于没有基础只能望洋兴叹? 那么C++就是你个人能力提升,职业之路进阶的不二之选。【课程特色】 1.课程共19大章节,239课时内容,涵盖数据结构、函数、类、指针、标准库全部知识体系。2.带你从知识与思想的层面从0构建C++知识框架,分析大型项目实践思路,为你打下坚实的基础。3.李宁老师结合4大国外顶级C++著作的精华为大家推出的《征服C++11》课程。【学完后我将达到什么水平?】 1.对C++的各个知识能够熟练配置、开发、部署;2.吊打一切关于C++的笔试面试题;3.面向物联网的“嵌入式”和面向大型化的“分布式”开发,掌握职业钥匙,把握行业先机。【面向人群】 1.希望一站式快速入门的C++初学者; 2.希望快速学习 C++、掌握编程要义、修炼内功的开发者; 3.有志于挑战更高级的开发项目,成为资深开发的工程师。 【课程设计】 本课程包含3大模块基础篇本篇主要讲解c++的基础概念,包含数据类型、运算符等基本语法,数组、指针、字符串等基本词法,循环、函数、类等基本句法等。进阶篇本篇主要讲解编程中常用的一些技能,包含类的高级技术、类的继承、编译链接和命名空间等。提升篇:本篇可以帮助学员更加高效的进行c++开发,其中包含类型转换、文件操作、异常处理、代码重用等内容。
C++中的多态(Polymorphism)是指在父类和子类之间的相互转换,以及在不同对象之间的相互转换。 C++中的多态性有两种:静态多态和动态多态。 1. 静态多态 静态多态是指在编译时就已经确定了函数的调用,也称为编译时多态C++中实现静态多态的方式主要有函数重载和运算符重载。 函数重载是指在同一作用域内定义多个同名函数,但它们的参数列表不同。编译器根据传递给函数的参数类型和数量来确定调用哪个函数。例如: ```c++ void print(int num) { std::cout << "This is an integer: " << num << std::endl; } void print(double num) { std::cout << "This is a double: " << num << std::endl; } int main() { int a = 10; double b = 3.14; print(a); // 调用第一个print函数 print(b); // 调用第二个print函数 } ``` 运算符重载是指对C++中的运算符进行重新定义,使其能够用于自定义的数据类型。例如: ```c++ class Complex { public: Complex(double real, double imag) : m_real(real), m_imag(imag) {} Complex operator+(const Complex& other) const { return Complex(m_real + other.m_real, m_imag + other.m_imag); } private: double m_real; double m_imag; }; int main() { Complex a(1.0, 2.0); Complex b(3.0, 4.0); Complex c = a + b; // 调用Complex类中重载的+运算符 } ``` 2. 动态多态 动态多态是指在运行时根据对象的实际类型来确定调用哪个函数,也称为运行时多态C++中实现动态多态的方式主要有虚函数和纯虚函数。 虚函数是在父类中定义的可以被子类重写的函数,使用virtual关键字声明。当一个对象的指针或引用指向一个子类对象时,调用虚函数时会根据实际的对象类型来确定调用哪个函数。例如: ```c++ class Shape { public: virtual void draw() { std::cout << "Drawing a shape." << std::endl; } }; class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle." << std::endl; } }; int main() { Shape* shape_ptr = new Circle(); shape_ptr->draw(); // 调用Circle类中重写的draw函数 } ``` 纯虚函数是在父类中定义的没有实现的虚函数,使用纯虚函数声明(如virtual void func() = 0;)。父类中包含纯虚函数的类称为抽象类,抽象类不能被实例化,只能作为基类来派生子类。子类必须实现父类的纯虚函数才能实例化。例如: ```c++ class Shape { public: virtual void draw() = 0; }; class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle." << std::endl; } }; int main() { Shape* shape_ptr = new Circle(); shape_ptr->draw(); // 调用Circle类中重写的draw函数 } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值