C++“接口“与“实现“分离的两种方法


C++"接口"与"实现"分离的两种方法

原文链接:https://blog.csdn.net/TAOKONG1017/article/details/79561856

接口需求

在软件开发这个行业中,一个较大的软件项目,一般由几个小组共同开发完成,为了将小组之间的影响降低到最低,定义好接口势在必行,如若要求短时间开发完成,定义好接口更是如此。或者说你的客户要求为其提供实现某个功能的接口,然后再在这些接口的基础上进行二次开发,如何定义才能定义好的接口呢? 第一,接口名字和实际的功能相符合;第二、接口要对数据进行封装,不允许客户直接操作接口之下的数据,尤其是使用new和delete在堆上操作内存数据。因为客户很容易由于操作不当造成错误,误以为是设计的接口有问题。

接口与实现分离

c++中实现对接口与实现进行分离有两种方法,一种是将对象的实现细目隐藏于指针背后,简单的说就是将其分成两个类,一个类只提供接口,另一个负责实现该接口,这种设计手法常称为Pimpl Idiom(pointer to implementation)。
另一种方法就是将接口定义为抽象类,接口全被定义为纯虚函数(纯虚函数没有具体的实现方法),派生类的成员函数负责实现这些接口。这种设计手法称为Object Interface。千万不要忘记把抽象接口类的析构函数定义为virtual函数,可能会造成内存泄漏。

Pimpl Idiom手法

下面举个简单的例子,要求实现一个Person接口,其要包含如下四个函数:
string& getName() const;
void setName(string& name);
int getAge() const;
void setAge(int age);
它们的功能是设置获取名字和年龄。其声明在Person.h文件中,具体接口如下:

#include<string>
class PersonImpl;
using namespace std;

class Person {
public:
    Person(string& name, int age);
    virtual ~Person();

    string& getName() const;
    void setName(string& name);
    int getAge() const;
    void setAge(int age);

private:
    PersonImpl *mPersonImpl;
};

Person.cpp文件中定义了具体函数接口,其内容如下:

#include "Person.h"
#include "PersonImpl.h"

Person::Person(string& name, int age):
    mPersonImpl(new PersonImpl(name, age))
{
    std::cout << "construct Person" << std::endl;
}

Person::~Person() {
    delete mPersonImpl;
    std::cout << "deconstruct Person" << std::endl;
}

string& Person::getName() const {
    return mPersonImpl->getName();
}

void Person::setName(string& name) {
    mPersonImpl->setName(name);
}

int Person::getAge() const {
    return mPersonImpl->getAge();
}

void Person::setAge(int age) {
    mPersonImpl->setAge(age);
}

PersonImpl.h声明了实现接口背后所需细目的函数接口,其内容如下:

#include<string>
#include <iostream>
using namespace std;

class PersonImpl {
public:
    PersonImpl(string& name, int age);
    virtual ~PersonImpl();

    string& getName() const;
    void setName(string& name);
    int getAge() const;
    void setAge(int age);

private:
    string& mName;
    int mAge;
};

PersonImpl.cpp中负责实现这些接口背后的细目函数,其内容如下:

PersonImpl::PersonImpl(string& name, int age):
    mName(name),
    mAge(age)
{

}

PersonImpl::~PersonImpl() {

}

string& PersonImpl::getName() const {
    return mName;
}

void PersonImpl::setName(string& name) {
    mName = name;
}

int PersonImpl::getAge() const {
    return mAge;
}

void PersonImpl::setAge(int age) {
    mAge = age;
}

从上面的例子中可以发现,在对外提供的接口函数中,只包含操作背后细目数据的接口方法,致使客户无法直接操作接口背后的细目数据,因此最大限度地降低了客户错误使用的可能性。

Object Interface手法

同样我们参照上面那个例子,要求实现一个Animal接口,其由如下四个接口组成:
string& getName() const;
void setName(string& name);
int getAge() const;
void setAge(int age);
它们的功能也是设置和获取名字和年龄,不同的是类不一样罢了,其声明在Animal.h文件中,具体接口如下:

#include <string>
using namespace std;

class Animal {
public:
    Animal(){};
    virtual ~Animal(){};

    virtual string& getName() const = 0;
    virtual void setName(string& name) = 0;
    virtual int getAge() const = 0;
    virtual void setAge(int age) = 0;
};

Animal* creat(string& name, int age);

真正实现Animal类声明的接口函数,声明在RealAnimal.h中,具体细节如下:

#include "Animal.h"

class RealAnimal: public Animal {
public:
    RealAnimal(string& name, int age);
    virtual ~RealAnimal();

    string& getName() const;
    void setName(string& name);
    int getAge() const;
    void setAge(int age);

private:
    friend Animal* creat(string& name, int age);

private:
    string& mName;
    int mAge;
};

在RealAnimal类中,除了继承的接口函数的声明之外,还多了一个友元函数,其有点类似于工厂函数,其作用就是实例化一个对象。下面看一下接口真正的实现细节,具体如下:

#include "RealAnimal.h"

RealAnimal::RealAnimal(string& name, int age):
    mName(name),
    mAge(age)
{

}

RealAnimal::~RealAnimal()
{

}

string& RealAnimal::getName() const {
    return mName;
}

void RealAnimal::setName(string& name){
    mName = name;
}

int RealAnimal::getAge() const{
    return mAge;
}

void RealAnimal::setAge(int age){
    mAge = age;
}

Animal* creat(string& name, int age) {
    return new RealAnimal(name, age);
}

如前面所说,Animal* creat(string& name, int age)确实只是实例化一个RealAnimal对象,返回的却是Animal接口对象,所以必须将类Animal 的析构函数声明为虚函数,不然会造成内存泄漏。

总结

无论是Impl Idiom手法,还是Object Interface手法都实现了同样的接口,而且它们有一个共同的目的,降低用户(被提供接口的小组也称为客户)直接操作数据造成不必要错误的可能性。其实它们有一个重要的优点就是将模块的依赖性降到了最低,举个例子吧,假如客户在使用这些接口的时候,如果这些接口内部的实现细目变更了,客户也不需要再重新编译自己的代码,因为客户只依赖接口声明的头文件。如果客户依赖接口的代码量非常大,那么,这个时候,这样定义接口就非常有必要了,毕竟客户在不修改自己代码的前提下,不需要重新编译自己的代码,这样可以提高客户的效率。

其实,这样来设计接口还是有缺点的,虽然接口定义在一个类中,但是真正实例化接口类的过程中,编译器会自动替我们生成必需的成员函数(比如构造函数、拷贝构造函数等),显然Animal也不例外。虽然有这样的缺点,但还是瑕不掩瑜。


C++之善用PIMPL技巧


https://blog.csdn.net/caoshangpa/article/details/78590826

PIMPL(Pointer to Implementation)这个手法可以解決/改善C++编码时常碰到的2大问题。

1.class增加private/protected成员时,使用此class的相关 .cpp(s) 需要重新编译。
2.定义冲突与跨平台编译

Q1.class增加private/protected成员时,使用此class的相关 .cpp(s) 需要重新编译
假设我们有一个A.h(class A),並且有A/B/C/D 4個.cpp引用他,他们的关系如下图:
在这里插入图片描述
如果A class增加了private/protected成员,A/B/C/D .cpp全部都要重新编译。因为make是用文件的时间戳记录来判断是否要从新编译,当make发现A.h比A/B/C/D .cpp4个文件新时,就会通知compiler重新编译他们,就算你的C++ compiler非常聪明,知道B/C/D文件只能存取A class public成员,make还是要通知compiler起来检查。三个文件也许还好,那五十个,一百个呢?
解決方法:

//a.h
#ifndef A_H
#define A_H
 
#include <memory>
 
class A
{
public:
    A();
    ~A();
     
    void doSomething();
     
private:    
      struct Impl;
      std::auto_ptr<impl> m_impl;
};
 
#endif

有一定C++基础的人都知道,使用前置声明(forward declaration)可以减少编译依赖,这个技巧告诉compile指向 class/struct的指针,而不用暴露struct/class的实现。在这里我们把原本的private成员封裝到struct A::Impl里,用一个不透明的指针(m_impl)指向他,auto_ptr是个smart pointer(from STL),会在A class object销毁时连带将资源销毁还给系统。
a.cpp 如下:

//a.cpp
#include <stdio.h>
#include "a.h"
 
struct A::Impl
{
    int m_count;
    Impl();
    ~Impl();
    void doPrivateThing();
};  
 
A::Impl::Impl():
    m_count(0)
{
}
 
A::Impl::~Impl()
{
}          
 
void A::Impl::doPrivateThing()
{
    printf("count = %d\n", ++m_count);
}    
 
A::A():m_impl(new Impl)
{
}      
 
A::~A()
{
} 
 
void A::doSomething()
{
    m_impl->doPrivateThing();    
}    

上面我们可以看到A private数据成员和成员函数全部被封裝到struct A::Impl里,如此一来无论private成员如何改变都只会重新编译A.cpp,而不会影响B/C/D.cpp,当然有时会有例外,不过大部分情况下还是能节约大量编译时间,项目越大越明显。

Q2.定义冲突与跨平台编译
如果你运气很好公司配給你8 cores CPU、SSD、32G DDRAM,会觉得PIMPL是多此一举。
但定定义冲突与跨平台编译问题不是电脑牛叉能够解決的,举个例子,你想在Windows上使用framework(例如 Qt)不具备的功能,你大概会这样做:

//foo.h
#ifndef FOO_H
#define FOO_H
 
#include <windows.h>
 
class Foo
{
 
public:
    Foo();
    ~Foo();
    void doSomething();
     
private:
    HANDLE m_handle;
     
};
 
#endif

Foo private数据成员: m_handle和系统相关,某天你想把Foo移植到Linux,应为Linux是用int来作为file descriptor,为了与Windows相区分,最直接的方法是用宏:

//foo.h
#ifndef FOO_H
#define FOO_H
 
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#endif
 
class Foo
{
 
public:
    Foo();
    ~Foo();
    void doSomething();
     
private:
 
#ifdef _WIN32    
    HANDLE m_handle;
#else
    int m_handle;
#endif    
     
};
 
#endif

这样做会有什么问题?
1.windows.h是个巨大的header file,有可能会增加引用此header file的其他.cpp(s)编译时间,而实际上这些.cpp並不需要windows.h里面的内容。
2.windows.h会与framework冲突,虽然大部分的framework极力避免发生这种事情,但往往项目变得越来越大后常常出现这类编译错误,(Linux也可能发生)。
3.对于Linux用户,Windows那些header file是多余的,对于Windows用户Linux header files是多余的,沒必要也不该知道这些细节。


C+±–pimpl技法


原文链接:https://blog.csdn.net/www_dong/article/details/118682882

1. 概念

pimpl(Private Implementation 或 Pointer to Implementation)是通过一个私有的成员指针,将指针所指向的类的内部实现数据进行隐藏。

2. 优点

1)降低模块的耦合;

2)降低编译依赖,提高编译速度;

3)接口与实现分离,提高接口的稳定性;

3. 实现原则

1. 暴露的接口里面不要有虚函数,要显式声明构造函数、析构函数,并且不能inline。

sizeof(Graphics) == sizeeof(Graphics::Impl*)

class Graphics
{
public:
    Graphics();
    ~Graphics();
 
    void drawLine(int x0, int y0, int x1, int y1);
 
    void drawArc(int x, int y, int r);
 
private:
    class Impl; //头文件只放声明
    std::shared_ptr<Impl> impl = nullptr;
}

2. 在库的实现中把调用转发(forward)给实现(Graphics::Impl),这部分代码位于.so/.dll中,随库的升级一起变化。

#include <Graphics.h>
 
Graphics::Graphics()
{
    impl = std::make_shared<Impl>();
}
 
Graphics::~Graphics()
{
    
}
 
void Graphics::drawLine(int x0, int y0, int x1, int y1)
{
    if(nullptr != impl->get())
    {
        impl->get()->drawLine(x0, y0, x1, y1);
    }
}
 
void Graphics::drawArc(int x, int y, int r)
{
    if(nullptr != impl.get())
    {
        impl.get()->drawArc(x, y, r);
    }
}

3. 如果要加入新的功能,不必通过继承来扩展,可以原地修改,且很容易保持二进制兼容性。

先动头文件:

class Graphics
{
public:
    Graphics();
    ~Graphics();
 
    void drawLine(int x0, int y0, int x1, int y1);
    void drawLine(double x0, double y0, double x1, double y1);  //add
 
    void drawArc(int x, int y, int r);
    void drawArc(double x, double y, double r);  //add
 
private:
    class Impl; 
    std::shared_ptr<Impl> impl = nullptr;
}

在实现的文件例增加forward,这么做不会破坏二进制兼容性,因为增加non-virtual函数不影响现有的可执行文件。

#include <Graphics.h>
 
Graphics::Graphics()
{
    impl = std::make_shared<Impl>();
}
 
Graphics::~Graphics()
{
    
}
 
void Graphics::drawLine(int x0, int y0, int x1, int y1)
{
    if(nullptr != impl->get())
    {
        impl->get()->drawLine(x0, y0, x1, y1);
    }
}
 
void Graphics::drawLine(double x0, double y0, double x1, double y1)
{
    if(nullptr != impl->get())
    {
        impl->get()->drawLine(x0, y0, x1, y1);
    }
}
 
void Graphics::drawArc(int x, int y, int r)
{
    if(nullptr != impl.get())
    {
        impl.get()->drawArc(x, y, r);
    }
}
 
void Graphics::drawArc(double x, double y, double r)
{
    if(nullptr != impl.get())
    {
        impl.get()->drawArc(x, y, r);
    }
}

采用pimpl多了一道explicit forward的手续,带来的好处是可扩展性和二进制兼容性。起到了防火墙的作用。


C++惯用法全!最后一谈pImpl


原文链接:https://blog.csdn.net/guangcheng0312q/article/details/103556062

二进制兼容性

开发库时,可以在不破坏与客户端的二进制兼容性的情况下向XImpl添加/修改字段(这将导致崩溃!)。由于在向Ximpl类添加新字段时X类的二进制布局不会更改,因此可以安全地在次要版本更新中向库添加新功能。

当然,您也可以在不破坏二进制兼容性的情况下向X / XImpl添加新的公共/私有非虚拟方法,但这与标准的标头/实现技术相当。

数据隐藏

如果您正在开发一个库,尤其是专有库,则可能不希望公开用于实现库公共接口的其他库/实现技术。要么是由于知识产权问题,要么是因为您认为用户可能会被诱使对实现进行危险的假设,或者只是通过使用可怕的转换技巧来破坏封装。PIMPL解决/缓解了这一难题。

编译时间

编译时间减少了,因为当您向XImpl类添加/删除字段和/或方法时(仅映射到标准技术中添加私有字段/方法的情况),仅需要重建X的源(实现)文件。实际上,这是一种常见的操作。

使用标准的标头/实现技术(没有PIMPL),当您向X添加新字段时,曾经重新分配X(在堆栈或堆上)的每个客户端都需要重新编译,因为它必须调整分配的大小 。好吧,每个从未分配X的客户端也都需要重新编译,但这只是开销(客户端上的结果代码是相同的)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值