适合具备 C 语言基础的 C++ 入门教程(十)

前言

在上一则教程中,叙述了抽象类以及动态链接库的相关内容,本节来叙述一下抽象类界面的相关内容,以及本节即将引入一个新的概念,模板。

抽象类界面

何为抽象类界面呢?要说清楚这个概念,需要回顾上一则教程中所述的类编程应用编程两个概念,为了实现应用编程和类编程,引入了动态链接库的概念,要达到的效果就是当更改类的代码的时候,而不更改应用程序的代码的时候,只需要重新生成动态链接库,而不需要重新生成可执行文件。那么我们回顾之前的代码,看应用编程里面的内容,也就是主函数里面的内容:

#include "Human.h"
#include "Englishman.h"
#include "Chinese.h"

void test_eating(Human *h)
{
	h->eating();
}


int main(int argc, char **argv)
{
	Englishman e;
	Chinese c;

	Human* h[2] = {&e, &c};
	int i;
	for (i = 0; i < 2; i++)
		test_eating(h[i]);

	return 0;
}

在上述代码中,我们看到第一行代码和第二行代码包含了头文件Englishman.hChinese.h,那么这个时候,如果更改了类中代码,比如说我们更改了Englishman.h或者是Chinese.h的代码,这个时候在编译的时候,如果只编译动态链接库,而不编译应用程序,那么必然会导致程序出现问题。那要如何解决这个问题呢,所采取的一种思路便是使用抽象类界面的思路来进行解决。

下面是抽象类界面的一个示意图:

image-20210224101410895

通过这张示意图也可以明白,这个时候,APP也就是应用程序的代码只和Human.h相关,而 Human.h又和EnglishmanChinese有关,这样一来,如果改变的是Englishman或者是Chinese类的代码,那么就不会影响到应用程序,仍然只需要重新编译动态链接库就好。

说了那么多,该如何做呢,我们先从主函数看起,下面是更改之后的主函数:

#include "Human.h"

void test_eating(Human *h)
{
	h->eating();
}


int main(int argc, char **argv)
{
	Human& e = CreateEnglishman("Bill", 10, "sfwqerfsdfas");
	Human& c = CreateChinese("zhangsan", 11, "beijing");

	Human* h[2] = {&e, &c};
	int i;
	for (i = 0; i < 2; i++)
		test_eating(h[i]);

	return 0;
}

看到上述代码,第一,头文件中,Englishman.hChinese.h不见了,只剩下一个Human.h,正如上面所说,APP的代码只和Human.h有关联;第二,之前有EnglishmanChinese的实例化对象,现在改为了使用函数调用生成Human类的引用,来替代之前的实例化对象。

那自然,这两个函数调用是在Human.h中声明的了,Human.h的代码如下所示:

#ifndef _HUMAN_H
#define _HUMAN_H

#include <iostream>
#include <string.h>
#include <unistd.h>

using namespace std;

class Human {
private:
	char *name;

public:
	void setName(char *name);
	char *getName(void);
	virtual void eating(void) = 0;
	virtual void wearing(void) = 0;
	virtual void driving(void) = 0;
	
};

Human& CreateEnglishman(char *name, int age, char *address);
Human& CreateChinese(char *name, int age, char *address);

#endif

为了使得应用编程和类编程相互分离,那么这两个函数的定义自然分别为了EnglishmanChinese了,代码分别如下所示:

/* 当前处于Englishman.cpp中 */
Human& CreateEnglishman(char *name, int age, char *address)
{
	return *(new Englishman(name, age, address));
}

下面是CreateChinese的函数定义,代码如下所示:

Human& CreateChinese(char *name, int age, char *address)
{
	return *(new Chinese(name, age, address));
}

这样一来,就实现了抽象类界面,在更改EnglishmanChinese的代码的时候,不需要重新生成可执行文件,只需要重新生成动态链接库就可以了。

模板

C++中的模板定义中,模板有两类,一个是函数模板,一个是类模板,在本节的教程中,主要是讲述函数模板的相关内容。

函数模板的引入

为什么要引入函数模板呢,我们来看一下如下所示的代码:

int& max(int& a, int& b)
{
	return (a < b)? b : a;
}

double& max(double& a, double& b)
{
	return (a < b)? b : a;
}

float& max(float& a, float& b)
{
	return (a < b)? b : a;
}

上述的代码是max函数的一个重载,观察这个重载函数,可见,每个重载函数的两个形参是相同的,并且形参和返回值一样,基于此,我们也就可以定义一个函数模板来替代这些函数重载,函数模板定义如下:

template<typename T>
T& max(T&a,t&B)
{
   return (a < b)? b : a;
}

如何理解上述模板函数呢,实际上也就是说,把类型用T来替换了

基于模板函数,我们再来实现上述使用重载而实现的功能,代码如下所示:

#include <iostream>
#include <string.h>
#include <unistd.h>

using namespace std;

template<typename T>
T& mymax(T& a, T& b)
{
	cout<<__PRETTY_FUNCTION__<<endl;
	return (a < b)? b : a;
}

int main(int argc, char **argv)
{
	int ia = 1, ib = 2;
	float fa = 1, fb = 2;
	double da = 1, db = 2;
	
	mymax(ia, ib);
	mymax(fa, fb);
	mymax(da, db);

	return 0;
}

上述代码执行的结果如下所示:

image-20210224135710100

可见上述的运行结果显示,虽然是使用的一个函数模板,但是在执行的时候,mymax(ia, ib);mymax(fa, fb);以及mymax(da, db);实际上是执行了三个不同的函数,这也正是函数模板执行的一个机制,函数模板其特点主要是以下两点:

  • 函数模板只是编译指令,一般写在头文件中;
  • 编译程序的时候,编译器根据函数的参数来“推导”模板的参数;然后生成具体的模板函数

模板函数参数推导过程

模板函数参数的推导过程是一个重要的内容,它主要可以分为如下几个方面:

  • 有限的类型转换
    • 函数模板只支持两种隐式转换
      • const 转换:函数参数为非 const 引用/指针,它可以隐式地转换为const引用/指针
      • 数组或者函数指针转换:
        • 数组可以隐式的转换为”指向第一个元素的指针“
        • 参数为”函数的名字“,它隐式地转化为函数指针
    • 其他隐式转换都不支持
  • 苛刻的类型匹配
    • 参数类型必须完全匹配;如果不能直接匹配,则可以进行”有限的类型转换“,如果还是不匹配,那么就推导失败

基于上述所述的这些特点,接下来通过实例进行阐述,现在基于刚才那个函数模板,我们来编写下面的例子:

using namespace std;

int main(int argc,char **argv)
{
    int a = 1;
    double b = 2.1;
    mymax(a,b);
    
    return 0;
}

代码编译结果如下所示:

image-20210224142730144

通过上述错误信息,可以看到所给出的信息是没有匹配的函数,只是因为我们传入的参数是intdouble,传入这两个参数是函数模板是无法进行推导的,无法进行隐式转换。

针对于上述来讲,函数模板只支持两种隐式转换,那分别是哪两种呢,我们来看具体的例子,我们将函数模板也进行一些更改,更改之后的代码如下所示:

#include <iostream>
#include <string.h>
#include <unistd.h>

using namespace std;

template<typename T>
const T& mymax(const T& a, const T& b)
{
	cout<<__PRETTY_FUNCTION__<<endl;
	return (a < b)? b : a;
}

int main(int argc, char **argv)
{
	int ia = 1;
	int ib = 2;
    
	mymax(ia, ib);

	return 0;
}

当前这个函数是可以执行通过的,也就是说当函数模板中的形参和返回值带有const的时候,那么对于实参是可以不含const修饰的,也就是说可变的参数可以传入到形参不可变的函数里,但是反过来是不行的,除非两个传进去的变量都是const的。比如如下所示的代码:

#include <iostream>
#include <string.h>
#include <unistd.h>

using namespace std;

template<typename T>
T& mymax(T& a, T& b)
{
	cout<<__PRETTY_FUNCTION__<<endl;
	return (a < b)? b : a;
}

int main(int argc,char** argv)
{
    int ia = 1;
    const int ib = 2;
    mymax(ia,ib);    /* 错误,const 不能隐式转换为非 const */
    
    const int isa = 1;
    const int isb = 2;
    mymax(isa,isb);  /* 正确 */
    
    return 0;
}

除了上述的 非constconst的例子以外,还有一个是数组和指针的隐式转换,数组可以隐式地转换为“指向第一个元素的指针”,下面是一个关于数组和指针的代码:

#include <iostream>
#include <string.h>
#include <unistd.h>

using namespace std;

template<typename T>
const T& mymax(const T& a, const T& b)
{
	cout<<__PRETTY_FUNCTION__<<endl;
	return (a < b)? b : a;
}

template<typename T>
const T* mymax2(const T* a, const T* b)
{
	cout<<__PRETTY_FUNCTION__<<endl;
	return (a < b)? b : a;
}

基于上述编写的两个模板函数,我们依次测试上述所说的指针和数组之间的隐式转换,代码如下所示:

int main(int argc, char** argv)
{
    char a[] = "ab";
    char b[] = "cd";
    
    mymax(a,b);
    mymax2(a,b);
    
    return 0;
}

下面是代码执行的结果:

image-20210224145329671

注:cout<<__PRETTY_FUNCTION__<<endl 可以在函数模板内打印匹配结果

根据打印出来的匹配结果,可以看到mymax匹配的T是数组类型,而mymax2匹配的Tchar类型,这也证实了上述所说的指针和数组之间的隐式转换。

基于上述的函数模板,我们继续来看一个例子:

int main(int argc, char** argv)
{
    char a2[] = "abc";
    char b2[] = "cd";
    
    mymax(a2, b2); /* mymax(char[4], char[3]),无法推导出T:mymax(char& [4], char& [3]),因为两个参数不一样*/
    mymax(a2, b2); /* mymax2(char[4], char[3]),可以推导出T:mymax2(const char*,const char*) */
    
    return 0;
}

通过上述的注释我们可以知道,第6行代码是不能编译通过的,但是第七行代码可以编译通过,因为它使用的模板的参数是指针,而对于数组来说,可以隐式转换为指针,数组名可以隐式转换为指向第一个元素的指针。

上述的几个例子已经说了const非const的,以及数组和指针的,接下来就是说的是函数指针的,且看下面这个例子:

template<typename T>
void test_func(T f)
{
    cout<<__PRETTY_FUNCTION__<<endl
}

void f1(int a, int b)
{
    return 0;
}

int main(int argc, char **argv)
{
    test_func(f1);
    test_func(&f1);
    
    return 0;
}

代码执行结果如下所示:

image-20210224151130109

可见对于函数名称来说,上述的两种传入方式都是将 T推导为函数指针的形式。

小结

上述就是本期分享的内容,涉及的代码可以通过百度云链接的方式获取到:

链接:https://pan.baidu.com/s/13_g0L9KBTSVJWDvOrksrfQ
提取码:gfsb

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值