【第十一章】 掌握数据抽象

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_34536551/article/details/78311671

隐藏数据抽象的细节


● 在程序员创建类的对象之前,c++ 编译器要知道该类所有成员(私有、保护、公有)的细节, 因为编译器必须为对象分配足够的内存。 对象通常由客户创建,编译器在编译客户的程序时要知道类的大小而客户只使用类的接口文件。

因此, 编译器必须能仅通过类的头文件即可确定任何类对象的大小。这就是在类的头文件中必须显示类的私有成员和保护成员的唯一原因。

在头文件中声明类的所有数据成员还有一个缺点,与编译程序的开销有关。 如果改变了类的大小, 该类的所有客户都必须用新的类头文件重新编译它们的程序。 在类中添加或移除数据成员时,类的大小会发生改变。


现在的问题是我们写完一个类后,在交付这个类后, 我们可能决定修改类的实现, 让它高效。 我们已经知道, 修改类的实现不会对类的接口有任何影响,因此,修改实现并不会影响客户。


假如现在我们在原来的类中添加了一些新的成员, 现在,每个使用这个类的客户必须重新编译他们的代码。 然而,强迫客户重新编译代码可不是好策略。 在改变类的实现时, 应该尽量不要强迫客户重新编译它们的代码。


数据抽象的好处之一,便是可以自由地修改实现。 如果仅仅因为修改实现可能导致对象大小的改变, 就放弃修改, 那我们就失去了数据抽象的主要优势之一, 需要想办法解决这个问题。


办法就是使用单独的实现类。 与其在头文件中保留类的所有数据成员, 不如只保留一个指向类对象实现的指针。 这个指针也称为句柄。 实现的细节置于其他类中。 通常,实现类的名称有后辍 Impl(或者甚至 Implementation).


原来的实现的类

#include<iostream>
using namespace std;

class TString
{
public:
    TString();
    TString(const char *s);
    TString(char aChar);

private:
    unsigned _strCapacity; //是指向缓冲区所能容纳的最大字符数
    unsigned _length; // 是储存在对象中的字符数目
    char *str;//指向字符的指针, _str 指向的内存至少要有 _length+1 长度,
};

下面,将使用单独的实现类,修改 TString 类

#include<iostream>
using namespace std;
class TStringImpl;  //前置声明- 这是一个实现类

class TString
{
public:
    TString();
    TString(const char *s);
    TString(char aChar);

private:
    TStringImpl *_handle;
};

class TStringImpl
{
public:
    unsigned _strCapacity; //是指向缓冲区所能容纳的最大字符数
    unsigned _length; // 是储存在对象中的字符数目
    char *_str;//指向字符的指针, _str 指向的内存至少要有 _length+1 长度,
};


// TString类的构造函数将创建TStringImpl类的对象, 并让 _handel指向它
TString::TString(const char *arg)
{
    _handle = new TStringImpl;  //创建一个新的实现类对象

    if (arg && *arg) // 指针不是0, 它指向非零字符
    {
        _handle->_length = strlen(arg);
        _handle->_str = new char[_handle->_length + 1];
        _handle->_strCapacity = _handle->_length;
        strcpy(_handle->_str, arg);
    }
    else
    {
        _handle->_str = 0;
        _handle->_length = 0;
        _handle->_strCapacity = 0;
    }
}

利用这个实现, 改动TStringImpl类不会影响TString 主类的大小。TString类一直包含一个单独的指针, 它指向一个TStringImpl类对象。 因此,TString 类对象的大小可以保持不变。

TString类的客户对TStringImpl类一无所知。 TString类的实现要知道TStringImpl类的细节, 但是TString类的接口不用。 编译器只需知道TStringImpl 是一个单独的类。


使用句柄的优点


这里写图片描述


使用句柄的缺点


TString类的所有成员函数都不能是内联函数, 因为TString的所有成员函数必须访问存储在TStringImpl类内部的信息。 但是, 在编译客户代码时, 编译器并不知道TStringImpl类的细节。 因此,TString类的内联函数将无法展开内联。

class TString
{
public:
    TString();
    TString(const char *s);
    TString(char aChar);
    int size()const
    {
        _handle->_length;  //不能编译,因为不知道TStringImpl类的细节
    }
private:
    TStringImpl *_handle;
};

class TStringImpl
{
public:
    unsigned _strCapacity; //是指向缓冲区所能容纳的最大字符数
    unsigned _length; // 是储存在对象中的字符数目
    char *_str;//指向字符的指针, _str 指向的内存至少要有 _length+1 长度,
};


TString::TString(const char *arg)
{
    _handle = new TStringImpl;  //创建一个新的实现类对象

    if (arg && *arg) // 指针不是0, 它指向非零字符
    {
        _handle->_length = strlen(arg);
        _handle->_str = new char[_handle->_length + 1];
        _handle->_strCapacity = _handle->_length;
        strcpy(_handle->_str, arg);
    }
    else
    {
        _handle->_str = 0;
        _handle->_length = 0;
        _handle->_strCapacity = 0;
    }
}

那么在定义TString 之前完整地定义TStringImpl类就可以在TString类中使用内联函数,例如下面的代码可以编译成功:

class TStringImpl
{
public:
    unsigned _strCapacity; //是指向缓冲区所能容纳的最大字符数
    unsigned _length; // 是储存在对象中的字符数目
    char *_str;//指向字符的指针, _str 指向的内存至少要有 _length+1 长度,
};

class TString
{
public:
    TString();
    TString(const char *s);
    TString(char aChar);
    int size()const
    {
        _handle->_length;  //可以编译,因为TStringImpl在前面已经完全定义
    }
private:
    TStringImpl *_handle;
};

TString::TString(const char *arg)
{
    _handle = new TStringImpl;  //创建一个新的实现类对象

    if (arg && *arg) // 指针不是0, 它指向非零字符
    {
        _handle->_length = strlen(arg);
        _handle->_str = new char[_handle->_length + 1];
        _handle->_strCapacity = _handle->_length;
        strcpy(_handle->_str, arg);
    }
    else
    {
        _handle->_str = 0;
        _handle->_length = 0;
        _handle->_strCapacity = 0;
    }
}

重新编译与重新链接的开销比较


将源代码转换成可执行程序, 需要经过两个步骤: 第一个步骤是编译, 第二个步骤是链接

● 编译程序时, 编译器检查语法和语义错误,然后生成一个目标文件。 在生成的目标文件中, 要调用许多函数。 其中一些函数可能在目标模块中, 而另一些可能尚未解析,对库函数的调用肯定还未解析。


只允许使用new() 操作符创建对象


● 在这里, 只允许用户使用new() 操作符创建对象客户无法在运行时栈上直接实例化对象。 你可能会想到,让构造函数私有就能解决这个问题。 但是这可不行, 因为new() 操作符也要访问构造函数

现在我们需要一种解决方案, 即使构造函数为公有, 也不能在栈上创建对象(自动对象)。

在C++中, 当用户试图在栈上创建对象时, 编译器会查找匹配且可以访问的构造函数和析构函数。 在创建对象时使用构造函数, 在离开作用域时用析构函数销毁对象。 如果其中的一个(构造函数和析构函数) 在调用时无法访问, 则会出现编译错误。 我们已经知道, 即使是new() 操作符, 也需要调用匹配的公有构造函数。 因此 ,我们仍然提供公有构造函数, 这里用到的是技巧是, 让析构函数成为私有,代码如下:

class X
{
public:
    X();

private:
    ~X();   //私有析构函数
};

void ff()
{
    X s;  //不能通过编译, 因为析构函数为私有,如果想通过编译,析构函数变公有
    X *ptr = new X;
}

但是,我们如何删除ptr 指向的对象, 如果我们使用 delete ptr;,将会编译错误, 因为delete 试图调用X 类的析构函数,因为它是私有,所以错误。

我们可以提供另外一个成员函数,该成员函数的的功能就是调用私有的析构函数,代码如下:

class X
{
public:
    X();
    void Delete()
    {
        delete this;
    }
private:
    ~X();   //私有析构函数
}; 

void ff()
{
    X s;  //不能通过编译, 因为析构函数为私有,如果想通过编译,析构函数变公有
    X *ptr = new X;

    ptr->Delete(); //   如果写语句 delete ptr; 编译错误, 因为delete 试图调用X 类的析构函数,因为它是私有,所以错误
}

Delete 成员函数中, 我们只需调用delete 操作符, 指向待删除对象的指针是this, 我们只用删除它即可。接着,将调用析构函数, 因为Delete () 是 X 类的成员函数, 因此它也可以访问私有成员函数

注意: 不要在调用Delete 之后使用this指针, 使用这种技巧时必须十分谨慎。 删除this 指针很不安全。 因为它有删除对象的副作用。 ptr 指向的对象将不再可用。 删除this 指针几乎是“自杀” 行为。

注意: 使用私有析构函数, 可防止在栈上创建对象(自动对象)


防止使用new() 操作符创建对象


在这里我们禁止使用new() 操作符创建对象, 只允许在栈上创建对象。 只需要一个不可访问的new() 的操作符, 代码如下:


class Y
{
public:
    Y();
private:
    void *operator new(size_t size);
    void operator delete(void *address);
};

void hh()
{
    Y stackObject; //运行正常,调用公有构造函数
    Y *ptr = new Y; // 不能编译, new操作符为私有
}

hh() 函数中的代码无法运行, 因为编译器试图调用类特殊的new()操作符, 但它是私有的。 delete()操作符的情况也是一样的


使用指针和引用代替内嵌对象


● 如果数据成员必须表现为多态性,就不能使用内嵌对象, 我们必须使用指针或引用(推荐使用指针)如果不要求多态性,使用内嵌对象也是个不错的选择。


避免用大型数组作为自动变量(或数据成员)


● 在函数中创建数组作为局部变量时, 将为其在运行时栈上分配。 在运行时栈上创建大型数组并不安全, 运行时栈通常有预设的大小, 而且依平台而异。 在栈上创建大型数组会导致运行时崩溃, 如果创建大型数组所在的函数是地规定额,问题会更加严重。

使用大型数组作为数据成员也不安全, 在栈上创建包含大型数组的对象时,也不是一个好的习惯。

● 注意: 当使用数组作为数据成员时, 应考虑用指针代替数组。

class X
{
public:
    X()
    {
        _parray = new int[1024];
    }
private:
    int *_parray; // 指向整数数组的指针
    int _arraySize;
    //int _array[1024]; 不推荐
};

在创建X 的对象时, 将在堆(而不是在栈上)上分配内存。 而且, X类的构造函数可以在运行时确定数组的大小。 并不是所有的X 类对象都需要1024 大小的数组。 动态数组更加具有灵活性。


使用对象数组和对象指针数组


● 注意: 在C++中处理对象时,我们通常避免创建对象数组。

class TrangeInt
{
public:
    static const int eLow = 0;
    static const int eHigh = 255;

    TrangeInt(int vlow, int vhigh, int val);
    TrangeInt(int val = 0);

private:
    int _value;
    int _low, _high;
};
int main()
{
    TrangeInt intAarray[10];
    TrangeInt intAarray[10] = { 101, //调用TrangeInt(int)
        TrangeInt(5,20,25 )};  //调用TrangeInt(int,int ,int)
    system("pause");
    return 0;
}

在创建数组的元素时使用特定构造函数, 也没有太多的灵活性


推荐使用指向对象的指针数组,利用指向对象的指针数组,用户完全可以自由地使用特定构造函数创建对象 而且, 并不是数组的所有元素都要包含对象。 我们可以在需要时才创建对象, 再将其放入数组中,

TrangeInt *intAarray[10];  //TrangeInt 是一个包含10个指针的数组,每个指针都指向一个TrangeInt 类对象。

通常, 在使用数组的元素之前,我们会将所有指针初始化为0,这样可以检查数组中特定位置是否包含有效地址。

for(int i=0;i<10;++i)

{
  intAarray[i]=0; //将每个地址初始化为0
}

这个数组的每个元素都可以储存TRangeInt 类对象的地址。 动态对象和静态对象的地址都可以放在该数组中

TRangeInt ri(100); // 栈上的一个TRangeInt类对象

intAarray[0] =new intAarray(10,100,55); // 将动态对象储存在下标为0的位置
intAarray[1]= &ri;

保存在数组中的地址, 其对象的生存期由程序员或编译器控制,而不是由数组控制。 我们还可以用新地址灵活地替换特定数组下标中的地址。

注意: 用指针数组代替对象数组,更具有灵活性。 但也需要格外谨慎,因为指针数组中的位置可能并不指向对象


用对象代替基本类型指针作为数据成员和成员函数的返回值


● 一般而言, 如果使用基本类型(如 char*), 则由实现者负责正确地管理它们。 然而, 使用对象减少了实现者的负担, 因为对象可以自我管理, 而且在许多情况下, 也减少了客户的负担。

用对象代替基本类型指针作为数据成员和成员函数的返回值的缺点: 为该类对象调用构造函数和析构函数, 增加了一些开销。 该类对象要比先前的大,因为包含两个对象成员, 而不是简单的字符指针

用对象代替基本类型指针作为数据成员和成员函数的返回值的优点: 比较对象更容易些。

● 尽可能使用对象代替基本类型指针


避免临时对象


● 每个对象都必须由构造函数创建, 与其他任何函数一样, 运行构造函数也要花费时间。 如果将对象作为常量使用, 最好是创建一次, 然后重复使用它。 例如:

void f(const TString &x); //某函数接受一个对象TString 类对象的引用

下面是对f() 的调用:

void g()
{
  f(TString("Hello There")); //开销不小
}

说明: 每次调用g() 时, 都会创建一个临时的TString 对象, 然后在从g() 退出时销毁它。 但是要注意, 这个临时对象的内容不变。最好只创建这个对象一次,然后重复使用它。

TString HelloObject("Hello There");
void g()
{
  f(HelloObject);
}

另外一种方法是,在函数内部将HelloObject 创建为静态对象。

void g()

{
  static TString HelloObject("Hello There");
  f(HelloObject);
}

● 注意: 避免创建临时对象——注意一些可能会创建临时对象的语句

猜你喜欢

转载自blog.csdn.net/qq_34536551/article/details/78311671