C++ 定义和使用接口类


C++中如何定义接口类?

首先给接口类下了定义:接口类应该是只提供方法声明,而自身不提供方法定义的抽象类。接口类自身不能实例化,接口类的方法定义/实现只能由接口类的子类来完成。

而对于C++,其接口类一般具有以下特征:

  • 1.最好不要有成员变量,但可以有静态常量(static const或enum)
  • 2.要有纯虚接口方法
  • 3.要有虚析构函数,并提供默认实现
  • 4.不要声明构造函数

如下就是一个最简单的例子:

class Testable
{
public:
    static const int START = 1;  // #1
    static const int STOP = 2;

    virtual void test() = 0;  // #2: 接口方法

    virtual ~Testable() {};   // #3: 从C++11开始可以: virtual ~Testable() = default;
}

如果成员变量,尤其是可变的成员变量,定义在接口中,等于是把实现细节暴露出来了,不符合接口定义的要求,所以一般不在接口中定义可变的成员变量。

而常量可以定义在接口中,因为有时接口需要返回状态,而这些状态可以定义成常量放在接口中。


由于不能让接口类自身能够实例化,并且需要子类必须实现接口暴露的方法,所以接口方法都要声明成纯虚函数。

声明成纯虚函数意味着接口类自身不需要提供方法的定义,方法的定义需要由接口类的子类提供,并且接口类自身也因此变成了抽象类而不能被实例化。


a). 在使用接口类的指针访问接口类的子类的实例时,当对接口类的指针做delete时,如果接口类的析构函数不是虚析构函数的话,将只会调用接口类的析构函数,接口类的子类的析构函数将不会被调用,内存泄露将会产生,所以接口类的析构函数必须定义成虚析构函数。

b). 如果接口类的析构函数不提供默认实现,即如果接口类的析构函数是纯虚析构函数的话,接口类的子类将被迫必须提供析构函数的实现,这样对接口类的子类不友好。

c). 在C++11中也可以用: virtual ~Testable() = default; 替代 virtual ~Testable() {};


不要显式定义任何的构造函数,但也不要在接口中加入如下代码来禁止生成构造函数:

Testable() = delete;
Testable(const Testable&) = delete;

因为C++的调用机制要求子类的构造函数调用时一定会先调用父类的构造函数,如果禁止生成构造函数,代码编译时会报错。如果程序员不显式的提供构造函数,编译器也会隐式的加上构造函数的,虽然这些构造函数对于接口类来说实际没有什么意义。

C++中如何定义标识接口(marker interface)类?

标识接口是没有任何方法和属性的接口。这种接口在java中出现的较多,比如:java.io.Serializable、java.rmi.Remote、java.util.EventListener、java.util.RandomAccess
实现代码如下:

class Testable {
public:
    virtual ~Testable() = 0 {}; // #5
};

只要对纯虚析构函数提供一个默认实现就可以了。这种对纯虚函数提供实现的写法看似很奇怪,但C++的确是支持的。

C++:如何正确的使用接口类

提供接口与实现

首先,声明一个接口:

// circle.h
// 圆的接口类
class Circle {
public:
    virtual ~Circle() {};

    // 接口方法:面积
    virtual double area() = 0;
};

通过继承的方式实现这个接口:

// circle_impl.h
#include "circle.h"

// 圆的具体实现类
class CircleImpl : public Circle {

private:
    double radius;
public:
    CircleImpl(double radius);
    double area() override;
};
// circle_impl.cpp
#include <cmath>
#include "circle_impl.h"

inline double pi() {
    return std::atan(1) * 4;
};

CircleImpl::CircleImpl(double _radius) : radius(_radius) {
};

double CircleImpl::area() {
    return pi() * radius * radius;
};

最后,通过管理类创建接口派生类的实例,或者销毁接口派生类的实例:

// circle_manager.h
#include "circle.h"

// 圆的创建工厂类
class CircleManager {
public:
    static Circle* create(double radius);     // 创建circle实例
    static void destroy(Circle* circlePtr);   // 销毁circle实例
};
// circle_manager.cpp
#include "circle_manager.h"
#include "circle_impl.h"

Circle* CircleManager::create(double radius) {
    Circle* circlePtr = new CircleImpl(radius);

    return circlePtr;
};

void CircleManager::destroy(Circle* circlePtr) {
    delete circlePtr;
}; 

代码目录结构:

proj-+
     |-inc-+
     |     |-circle.h
     |     |-circle_manager.h
     |
     |-src-+
           |-circle_impl.h
           |-circle_impl.cpp
           |-circle_manager.cpp

其中inc目录用于存放Circle接口类和Circle管理类的声明,src目录中存放Circle实现类CircleImpl的声明和定义、Circle管理类CircleManager的定义。

然后,可以将以上代码编译成静态库circle.lib,并和inc目录中的头文件一起提供给外部调用:

如何使用静态库?

外部使用者编译时,需要做如下配置:

1). 把inc目录添加到“附加包含目录”中。

2). “附加依赖项”中添加circle.lib。

3). 把circle.lib所在目录的路径添加到“附加库目录”中。

外部使用者的代码如下:

// main.cpp
#include <iostream>
#include "circle_manager.h"
#include "circle.h"

int main() 
{
    Circle* circlePtr = CircleManager::create(3);
    cout << circlePtr->area() <<endl;
    CircleManager::destroy(circlePtr);

    system("pause");

    return 0;
}

以上代码只提供给外部circle的接口,circle的实现完全被隐藏了起来,外部将无从知晓,外部使用者只能通过circle管理类生成circle的派生类的实例。外部使用者得到circle派生类的实例后,除了能调用接口暴露的方法area()外,其它什么也做不了,这样就完全达到了使用接口的最终目标。

如何编译成动态库?

// dll_export.h 
// if windows .dll 
#ifdef _WINDLL 

#ifdef DLL_API_EXPORTS 
#define DLL_API __declspec(dllexport) 
#else 
#define DLL_API __declspec(dllimport) 
#endif 

// else if Linux or macOS .so 
#else 
#define DLL_API 
#endif

添加此头文件后,代码可以在windows、Linux下都可编译生成动态库,只需在编译时设置不同参数就行了。

windows: /D “DLL_API_EXPORTS” /D “_WINDLL”

Linux: 不用配置额外参数

circle.h和circle_manager.h也要做相应改动:

// circle.h 
#pragma once 
#include "dll_export.h" 

// 圆的接口类 
class DLL_API Circle 
{ 
public:        
    virtual ~Circle() {};

    // 接口方法:面积         
    virtual double area() = 0; 
};
// circle_manager.h 
#pragma once 

#include "circle.h" 
#include "dll_export.h"  

// 圆的创建工厂类 
class DLL_API CircleManager 
{ 
public: 
    static Circle* create(double radius); 
    static void destroy(Circle* circlePtr); 
};

编译完成后将生成”circle.lib“和”circle.dll“文件:

proj-+
     |-inc-+
     |     |-circle.h
     |     |-circle_manager.h
     |
     |-src-+
     |     |-circle_impl.h
     |     |-circle_impl.cpp
     |     |-circle_manager.cpp
     |
     |-bin-+
           |-circle.lib
           |-circlr.dll

如何使用动态库?

外部使用者编译时,需要做如下配置:

1). 代码中添加#pragma comment(lib,”circle.lib”), 这里是circle.lib,不是circle.dll。

2). 把inc目录添加到“附加包含目录”中。

3). “附加依赖项”中添加circle.lib,这里也是circle.lib,不是circle.dll。

4). 把bin目录所在路径添加到”附加库目录“中。

新的外部使用者的代码如下:

#include <iostream> 

#include "circle_manager.h" 
#include "circle.h" 

#pragma comment(lib,"circle.lib") 

int main() 
{
    Circle* circlePtr = CircleManager::create(3);
    cout << circlePtr->area() << endl;
    CircleManager::destroy(circlePtr);

    system("pause");

    return 0; 
}

总结

这里有几点需要说明一下:

  • 1、为什么CircleManager类即在提供创建实例的方法又要提供销毁实例的方法?
    由于编译器的实现方式不同,dll的堆空间可能跟调用方的堆空间不同,它可能是由dll自己单独管理的,所以从dll中创建的实例,最好还是在dll中销毁。

  • 2、对动态库的调用本文是通过隐式调用的方式完成的,对动态库的调用也可以使用显式调用的方式,但由于windows和Linux在使用显式调用时的API是不同的,不好提供统一的代码,所以本文没有举例,以后有机会再单独行文介绍。


文章作者: JoyTsing
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 JoyTsing !
评论
 上一篇
C++ “接口”与“实现”分离的两种方法 C++ “接口”与“实现”分离的两种方法
接口需求在软件开发这个行业中,一个较大的软件项目,一般由几个小组共同开发完成,为了将小组之间的影响降低到最低,定义好接口势在必行,如若要求短时间开发完成,定义好接口更是如此。或者说你的客户要求为其提供实现某个功能的接口,然后再在这些接口的基
2019-09-23
下一篇 
C++ virtual function分析 C++ virtual function分析
一、概述为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是如何实现动态绑定的。 二、类的虚表每个包含了虚函数的类都包含一个虚表。我们知道,当一个类(A)继承另一个类(B)时
2019-09-23
  目录