C++超详细讲解隐藏私有属性和方法的两种实现方式

目录
  • 例子
  • 用抽象类解决问题
  • 用Pimpl风格解决问题
  • 总结
  • 参考

在我们编写程序的时候,会将程序模块化,常见的就是用动态链接库的方式,然后导出函数接口或者类。而对于导出类的方式,作为模块的实现者,不论是给第三方使用或者自己的项目使用,应该都不太愿意暴露自己的私有属性和方法,个人碰到的主要有以下两个常见原因:

  • 通过隐藏私有属性和方法,让被调用者猜不到其实现方式
  • 私有方法中或者属性中,可能会存在一些第三方的头文件或者库的依赖,而对于被调用方来说不应该直接依赖

本文将介绍两种方式来满足以上的需求,一种是抽象类,另一种是pimpl风格. 在找到解决方法的时候,你会发现这样的方式不仅仅满足了原先的需求,还买一赠一地带来了其他的优点。

例子

假设我们有一个DataAcquirer封装为一个动态链接库,用来获取数据的:那么以下代码有几个问题:

  • 其只需要暴露GetData这个方法给调用方,但是文件中还包含了头文件HttpClient.h 这个是调用方其实并不需要关心的,这就导致调用方还需要配置头文件的目录,有时候甚至还要配置这个间接依赖的库。那么就给调用方带来了不必要的依赖。
  • 有时候想要隐藏类的内部实现细节,但这里通过HttpClient m_pHttpClient私有属性和HttpResponseCode HttpDataGet()私有方法,那么调用方就可能猜到这个数据其实是通过http协议来获取的。
#include <string>
#include "HttpClient.h"
#ifdef DATA_ACQUIRER_DLL_EXPORT
#define DATA_ACQUIRER_DECL __declspec(dllexport)
#else
#define DATA_ACQUIRER_DECL __declspec(dllimport)
#endif
class DATA_ACQUIRER_DECL DataAcquirer
{
public:
	DataAcquirer();
	~DataAcquirer();
public:
	const std::string GetData();
private:
	HttpResponseCode HttpDataGet();
	HttpClient m_pHttpClient;
};

用抽象类解决问题

如果你知道依赖倒置原则(Dependence Inversion Principle, DIP), 那应该知道,提供给调用方的时候高层模块依赖其抽象。 在软件编写的时候,抽象是必不可少的,他可以降低我们依赖,也能够让我们更加清晰的定义更友好的接口。这个样例中,我们只需要提供GetData的方法/接口,那我们面向接口的设计如下面类图所示:

解释下上述的类图:

  • 调用者client操作的是DataAcquirerAbstract作为抽象类,利用多态实际的对象指向的是DataAcquirer
  • DataAcquirer通过工厂方法DataAcquirerFactory进行生产

DataAcquirerAbstract.h的内容如下, 声明抽象类:

#pragma once
#include <string>
class DataAcquirerAbstract
{
public:
	virtual const std::string GetData() = 0;
};

DataAcquirer.h的内容如下, 声明DataAcquirer :

#pragma once
#include <string>
#include "HttpClient.h"
#include "DataAcquirerAbstract.h"
class DataAcquirer : public DataAcquirerAbstract
{
public:
	DataAcquirer();
	~DataAcquirer();
public:
	virtual const std::string GetData();
private:
	HttpResponseCode HttpDataGet();
	HttpClient m_pHttpClient;
};

工厂方法部分用于生产DataAcquirer,下面是DataAcquirerFactory .h文件:

#pragma once
#include <memory>
#include "Factory.h"
#include "DataAcquirerAbstract.h"
#ifdef DATA_ACQUIRER_DLL_EXPORT
#define DATA_ACQUIRER_DECL __declspec(dllexport)
#else
#define DATA_ACQUIRER_DECL __declspec(dllimport)
#endif
class DATA_ACQUIRER_DECL DataAcquirerFactory : public Factory
{
public:
	virtual std::unique_ptr<DataAcquirerAbstract> CreateDataAcquirer();
};

最后调用者只需要引用DataAcquirerAbstract和DataAcquirerFactory ,如下所示, DataAcquirer对于调用者来说是不可见的。

#include <string>
#include <memory>
#include "DataAcquirerAbstract.h"
#include "DataAcquirerFactory.h"
int main()
{
	std::unique_ptr<Factory> factory = std::make_unique<DataAcquirerFactory>();
	std::unique_ptr<DataAcquirerAbstract> pObj = factory->CreateDataAcquirer();
	std::string strData = pObj->GetData();
	//...	Do something else
	return 0;
}

用Pimpl风格解决问题

Pimpl实际的解决方法也比较简单,将Private/Protected属性和方法放到另一个类中,这个类只需要进行声明,然后通过成员指针的方式,进行属性或者方法的访问。用pimpl改造后的类图如下:

DataAcquirer只给调用者暴露了GetData()方法和m_pImpl未知细节的指针,而这个未知细节的指针,在cpp文件中将含有一些私有的方法和属性,也提供一个相应的GetData()的public方法。

DataAcquirer.h文件实现如下:

#pragma once
#include <string>
#include "HttpClient.h"
#ifdef DATA_ACQUIRER_DLL_EXPORT
#define DATA_ACQUIRER_DECL __declspec(dllexport)
#else
#define DATA_ACQUIRER_DECL __declspec(dllimport)
#endif
class DATA_ACQUIRER_DECL DataAcquirer
{
public:
	DataAcquirer();
	~DataAcquirer();
public:
	const std::string GetData();
private:
	class DataAcquirerImpl;
	std::unique_ptr<DataAcquirerImpl> m_pImpl;
};

DataAcquirerImpl的具体实现放在DataAcquirer.cpp中:

#include "DataAcquirer.h"
class DataAcquirer::DataAcquirerImpl
{
public:
	DataAcquirerImpl() {};
	const std::string GetData() { return ""; };
private:
	HttpResponseCode HttpDataGet() { return m_pHttpClient.Get(); };
	HttpClient m_pHttpClient;
};
DataAcquirer::DataAcquirer() : m_pImpl(new DataAcquirerImpl())
{
}
DataAcquirer::~DataAcquirer()
{
}
const std::string DataAcquirer::GetData()
{
	return m_pImpl->GetData();
}

总结

无论是抽象类的方式还是Pimpl风格都达成了接口与实现的分离,并且降低了编译时候的依赖。

以上所说的两种方式,在从无到有编写代码的时候,可以完整的使用这个模式,可是有时候,你需要去维护已有的代码,在原先的导出类中进行一些修改,想要去降低这些依赖,个人认为用Pimpl此时就更适合去做这种扩展修改了。

参考

抽象类方法和Pimpl均在<<Effective C++>> 条款31中提到,只是本人的实现方式会有小小的区别。

另外参考了微软文档<<Pimpl For Compile-Time Encapsulation (Modern C++)>>

到此这篇关于C++超详细讲解隐藏私有属性和方法的两种实现方式的文章就介绍到这了,更多相关C++隐藏私有属性内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入理解C++中变量的存储类别和属性

    C++变量的存储类别(动态存储.静态存储.自动变量.寄存器变量.外部变量) 动态存储方式与静态存储方式 我们已经了解了变量的作用域.作用域是从空间的角度来分析的,分为全局变量和局部变量. 变量还有另一种属性--存储期(storage duration,也称生命期).存储期是指变量在内存中的存在期间.这是从变量值存在的时间角度来分析的.存储期可以分为静态存储期(static storage duration)和动态存储期(dynamic storage duration).这是由变量的静态存储方式

  • 将 C++ 类型属性暴露给 QML

    目录 一.数据类型处理和所有权 1.1.暴露属性 1.2.使用通知信号的注意事项 1.3.具有对象类型的属性 1.4.具有对象列表类型的属性 1.5.分组属性 二.暴露方法 三.暴露信号 可以使用 C++ 代码中定义的功能轻松扩展 QML.由于 QML 引擎与 Qt 元对象系统的紧密集成,QObject 派生类公开的任何功能都可以从 QML 代码访问.这使得 C++ 数据和函数可以直接从 QML 访问,通常几乎不需要修改. QML 引擎能够通过元对象系统反射 QObject 实例.这意味着任何

  • C++超详细讲解隐藏私有属性和方法的两种实现方式

    目录 例子 用抽象类解决问题 用Pimpl风格解决问题 总结 参考 在我们编写程序的时候,会将程序模块化,常见的就是用动态链接库的方式,然后导出函数接口或者类.而对于导出类的方式,作为模块的实现者,不论是给第三方使用或者自己的项目使用,应该都不太愿意暴露自己的私有属性和方法,个人碰到的主要有以下两个常见原因: 通过隐藏私有属性和方法,让被调用者猜不到其实现方式 私有方法中或者属性中,可能会存在一些第三方的头文件或者库的依赖,而对于被调用方来说不应该直接依赖 本文将介绍两种方式来满足以上的需求,一

  • Android 超详细讲解fitsSystemWindows属性的使用

    对于android:fitsSystemWindows这个属性你是否感觉又熟悉又陌生呢? 熟悉是因为大概知道它可以用来实现沉浸式状态栏的效果,陌生是因为对它好像又不够了解,这个属性经常时灵时不灵的. 其实对于android:fitsSystemWindows属性我也是一知半解,包括我在写<第一行代码>的时候对这部分知识的讲解也算不上精准.但是由于当时的理解对于我来说已经够用了,所以也就没再花时间继续深入研究. 而最近因为工作的原因,我又碰上了android:fitsSystemWindows这

  • Java超详细讲解三大特性之一的继承

    目录 继承的概念 方法的重写 super关键字的使用 super调用构造器 总结 继承的概念 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为. 通过继承可以快速创建新的类,实现代码的重用,提高程序的可维护性,节省大量创建新类的时间,提高开发效率和开发质量. 继承性的好处: 减少代码的重复 提高代码复用性 便于功能拓展 继承性的格式:class A extends B{} A:子类,派生类,subclass,B: 父类

  • java反射超详细讲解

    目录 Java反射超详解✌ 1.反射基础 1.1Class类 1.2类加载 2.反射的使用 2.1Class对象的获取 2.2Constructor类及其用法 2.4Method类及其用法 Java反射超详解✌ 1.反射基础 Java反射机制是在程序的运行过程中,对于任何一个类,都能够知道它的所有属性和方法:对于任意一个对象,都能够知道它的任意属性和方法,这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制. Java反射机制主要提供以下这几个功能: 在运行时判断任意一个对象所属

  • 超详细讲解Java秒杀项目登陆模块的实现

    目录 一.项目前准备 1.新建项目 2.导入依赖 3.执行sql脚本 4.配置yml文件 5.在启动类加入注解 6.自动生成器 二.前端构建 1.导入layui 2.将界面放到template 3.在js目录下新建目录project 4.新建controller类 三.MD5加密 1.导入帮助包与exception包 2.新建vo类 3.登录方法: 4.密码加密 四. 全局异常抓获 1.给实体类userVo加入注解 2.导入帮助包validate,异常抓获 3.在UserController类方

  • C语言函数超详细讲解下篇

    目录 前言 函数的声明和定义 函数声明 函数定义 举例 简单的求和函数 把加法单独改写成函数 添加函数声明 带头文件和函数声明 静态库(.lib)的生成 静态库文件的使用方法 函数递归 什么是递归? 递归的两个必要条件 练习1 一般方法 递归的方法 练习2 一般方法 递归方法 练习3 一般方法 递归方法 练习4 一般方法 递归方法 递归与迭代 递归隐藏的问题 如何改进 选递归还是迭代 总结 前言 紧接上文,继续学习函数相关内容. 函数的声明和定义 函数声明 告诉编译器有一个函数叫什么,参数是什么

  • Java超详细讲解三大特性之一的封装

    目录 封装 封装的概念 Java中的包 java中类的成员-构造器 java中的this关键字 总结 说到面向对象则不得不提面向对象的三大特征:封装,继承,多态.那么今天就和大家先来介绍什么是封装. 封装 封装的概念 将类的某些信息隐藏在类的内部,不允许外部程序直接访问,而是通过该类提供的方法来对隐藏的信息进行操作和访问. 为什么需要封装? 当我们创建一个类的对象后,我们可以通过“对象.属性”的方式,对对象的属性进行赋值.这里赋值操作要受到 属性的数据类型和存储范围的制约.除此之外,没有其他制约

  • MyBatis插件机制超详细讲解

    目录 MyBatis的插件机制 InterceptorChain MyBatis中的Plugin MyBatis插件开发 总结 MyBatis的插件机制 MyBatis 允许在已映射语句执行过程中的某一点进行拦截调用.默认情况下,MyBatis 允许使用插件来拦截的方法调用包括: Executor(update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) ParameterHandler(

  • Java Scala数据类型与变量常量及类和对象超详细讲解

    目录 一.数据类型 二.变量和常量 三.类和对象 3.1 类 3.2 对象 3.3 伴生类和伴生对象 3.4 Scala的main函数只能写在object里 总结 一.数据类型 简记: 所有基础类型基本与Java的包装类等同,唯一有不同的Int(Scala),Integer(Java),注意这个就好 Unit, Null, Nothing, Any, AnyRef, AnyVal,这几个除了Null乍一眼会有些陌生,不要怕,上总结: 首先是层级图: 然后是表格: 补丁: 记住Any是所有类型的超

  • 超详细讲解Linux C++多线程同步的方式

    目录 一.互斥锁 1.互斥锁的初始化 2.互斥锁的相关属性及分类 3,测试加锁函数 二.条件变量 1.条件变量的相关函数 1)初始化的销毁读写锁 2)以写的方式获取锁,以读的方式获取锁,释放读写锁 四.信号量 1)信号量初始化 2)信号量值的加减 3)对信号量进行清理 背景问题:在特定的应用场景下,多线程不进行同步会造成什么问题? 通过多线程模拟多窗口售票为例: #include <iostream> #include<pthread.h> #include<stdio.h&

随机推荐