查看原文
其他

啥是封装?来看C++这个小技巧

嵌入式ARM 2021-01-31

来源 :今日头条@小智雅汇


将程序中重复出现的代码块提炼成一个函数,也可以理解为一种封装。如果将一个事物相关的数据及对这些数据的处理封装起来,这就是类的概念。

毫无疑问,封装可以实现代码的重用、任务的分治、实现(implementaion)和接口(interfavce)的分离以及提高代码的安全性。


1 封装类更易于使用,并降低程序的复杂性。


封装将对象实现的细节隐藏在对象的用户之外。相反,对象的用户通过公共接口访问对象。这样,用户就可以使用对象,而不必了解它是如何实现的。简单说,类由设计者完成实现,而类的使用者只需了解接口即可拿过来使用(实现和接口的分离)。正因为如此,我们才可以在编程语言中使用函数库或类库,而不是每一个程序都要从0开始。

对于完全封装的类,您只需要知道哪些成员函数可以公开使用该类,它们采用什么参数,以及返回什么值。类是如何在内部实现的并不重要。例如,包含一系列名字的类可以使用C风格字符串的动态数组、std::vector、std::map、std::list或许多其他数据结构中的一个来实现。为了使用这个类,您不需要知道(或关心)某个是如何实现的。这大大降低了程序的复杂性,也减少了错误。这比任何其他原因都重要,这是封装的关键优势。


2 封装类有助于保护数据并防止误用


Global variables are dangerous because you don’t have strict control over who has access to the global variable, or how they use it. Classes with public members suffer from the same problem, just on a smaller scale.

全局变量是危险的,因为您无法严格控制谁可以访问全局变量,或如何使用全局变量。有公共成员的类也有同样的问题,只是规模较小。

例如,假设我们正在编写一个字符串类。我们可以这样开始:

class MyString{ char *m_string; // we'll dynamically allocate our string here int m_length; // we need to keep track of the string length};

这两个变量有一个内在的连接:m_length应该始终等于m_string持有的字符串的长度(此连接称为不变量)。如果m_length是公共的,任何人都可以在不更改m_string的情况下更改字符串的长度(反之亦然)。这会使类处于不一致的状态,这可能会导致各种奇怪的问题。通过将m_length_和m_string设置为私有,用户被迫使用任何可用的公共成员函数来处理类(这些成员函数可以确保m_length_和m_string始终被适当设置)。

我们还可以帮助保护用户避免在使用我们的类时出错。考虑一个具有公共数组成员变量的类:

class IntArray{public: int m_array[10];};

如果用户可以直接访问数组,则可以使用无效索引下标数组,从而产生意外结果:

int main(){ IntArray array; array.m_array[16] = 2; // invalid array index, now we overwrote memory that we don't own}

但是,如果我们将数组设为私有,则可以强制用户使用一个函数,该函数首先验证索引是否有效:

class IntArray{private: int m_array[10]; // user can not access this directly any more
public: void setValue(int index, int value) { // If the index is invalid, do nothing if (index < 0 || index >= 10) return;
m_array[index] = value; }};

这样,我们就保护了程序的完整性。顺便说一下,std::array和std::vector的at()函数做了一些非常相似的事情!另外C风格字符串封装为string类,裸指针封装为智能指针都体现了同样的思想。


3 封装类更易于更改


举个简单的例子:

#include <iostream> class Something{public: int m_value1; int m_value2; int m_value3;}; int main(){ Something something; something.m_value1 = 5; std::cout << something.m_value1 << '\n';}

虽然这个程序运行良好,但如果我们决定重命名m_value1或更改其类型,会发生什么情况?我们不仅会破坏这个程序,而且很可能大多数使用y类的程序也一样!
封装使我们能够在不破坏所有使用类的程序的情况下更改类的实现方式。
下面是这个类的封装版本,它使用函数访问m_value1:

#include <iostream> class Something{private: int m_value1; int m_value2; int m_value3; public: void setValue1(int value) { m_value1 = value; } int getValue1() { return m_value1; }}; int main(){ Something something; something.setValue1(5); std::cout << something.getValue1() << '\n';}

现在,让我们更改类的实现:

#include <iostream> class Something{private: int m_value[3]; // note: we changed the implementation of this class! public: // We have to update any member functions to reflect the new implementation void setValue1(int value) { m_value[0] = value; } int getValue1() { return m_value[0]; }}; int main(){ // But our program still works just fine! Something something; something.setValue1(5); std::cout << something.getValue1() << '\n';}

请注意,因为我们没有更改类的公共接口中任何函数的原型,所以使用该类的程序将继续工作,而不会发生任何更改。


4 封装类更易于调试


最后,封装有助于在出现问题时调试程序。通常当程序不能正常工作时,是因为我们的一个成员变量的值不正确。如果每个人都可以直接访问该变量,那么跟踪修改该变量的代码片段可能会很困难(可能是其中的任何一个,您需要对它们进行断点操作才能确定是哪一个)。但是,如果每个人都必须调用同一个公共函数来修改一个值,那么您可以简单地中断该函数并观察每个调用方更改该值,直到您看到它出错的地方。


-END-


推荐阅读

【01】每个工程师都应该了解的一些 C++ 特性【02】C++中是如何实现智能指针的?【03】C++ 救不了程序员!【04】现在市场上,C++ 主要用来做什么?【05】C++虚函数的深入理解



免责声明:整理文章为传播相关技术,版权归原作者所有,如有侵权,请联系删除

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存