欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C++面向对象高效编程:数据抽象

程序员文章站 2022-09-28 14:27:39
数据抽象 数据抽象是一种依赖于接口和实现分离的编程(以及设计)技术。即定义抽象数据类型以及一组操作并隐藏实现的过程 接口和实现的分离 接口的含义 类的接口包括用户所...

数据抽象

数据抽象是一种依赖于接口和实现分离的编程(以及设计)技术。即定义抽象数据类型以及一组操作并隐藏实现的过程

接口和实现的分离

接口的含义

类的接口包括用户所能执行的操作 用户观察到的对象视图 告诉Client“可以做什么”

实现的含义

类的实现包括类的数据成员、负责接口实现的函数体以及定义类所需要的各种私有函数。 负责“如何做”

保护实现——数据封装

1.通过数据封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节。

2.数据被封装后,客户无法直接访问,更不能修改,只有接口函数才可以访问和修改封装的信息。接口用户完全不知道描述该接口的函数如何使用被封装的信息,而且类的用户也对此毫无兴趣。

3.封装另外一个优点是实现独立,由于类用户无法查看封装的数据,他们也不会注意到封装数据的存在,因此改动封装的数据和信息不会影响用户缩减的接口。即客户使用的接口与支持接口的实现彼此独立。

需要封装的内容

某项对用户理解类毫无帮助,或者从接口中移除该项根本不会减少类的作用 某项包含有敏感数据,为了不让用户直接访问的 某项有着潜在的危险,并且要求用户掌握特殊技能才能操作的 类为了自我管理而使用的某些元素,且对接口意义不大

抽象数据类型

为了实现数据抽象和封装,首先需要先定义一个抽象数据类型。定义抽象类型将直接应用数据抽象的概念.

Cpp 与数据抽象

在C++中,抽象的基本单元是类,类只不过是功能增强的C结构。

下面以一个整数栈TintStack类为例

class TintStack{
    public:
        //member functions
        TintStack(unsigned int StackSize = DEFAULT_SIZE);   //defualt constructor
        TintStack(const TintStack& that);                   //copy constructor
        TintStack& operator= (const TintStack &that);       //assignment operators
        ~TintStack();                                      //destructor

        void push(int thisValue);
        int pop();
        unsigned int howmany() const;
        //more...
    private:
        int * _sp;
        unsigned _count;
        unsigned _size;
};

访问区域

访问说明符控制派生类基类继承而来的成员是否对派生类用户可见。

public区域:
该区域看做是通用公共的接口,没有任何的保护,是类限制最少的区域。 private区域:
成员函数的实现可以访问在此区域的所有成员。但是类的用户无法操控private区域。 protected区域
protected区域的限制比private区域要宽松一点,但比public要严格。 该区域用户派生类(通过继承)使用。派生类的成员函数友元可以访问该区域内的所有成员,但是派生类的用户不能访问。 派生类的成员函数友元只能通过派生类对象访问基类protected区域的成员。派生类对于一个基类对象中的受保护成员没有任何访问特权

构造函数

考虑下面的创建对象实例

TintStack mystack;
TintStack s1(100);
TintStack s2 = s1;
TintStack *dsp = new TintStack(200);
TintStack s3 = TintStack(250);

对象只能用构造函数创建。

(1)TintStack mystack;

这里我们创建了一个TinStack类的对象mystack,因为该声明没有带有任何的参数,所以这个语句将调用默认构造函数。不带任何参数调用的构造函数即是默认构造函数

在mystack 类中TintStack(unsigned int StackSize = DEFAULT_SIZE);就是一个默认构造函数,接受一个unsigned int 参数,并且有一个默认值DEFAULT_SIZE。

(2)TintStack s1(100)

这条语句创建TintStack类的对象s1,其栈的大小为100个元素。这类的语法看起来像是函数调用。这里调用了TinStack类的构造函数。

(3)TinStack s2 = s1;

从语法上来说,这条语句将通过s1创建s2,编译器此时会调用复制构造函数

(4)TinStack *dsp = new TintStack (200)

在前面(1)创建对象中,编译器负责运行栈对象mystack分配内存,分配的内存包括有:数据成员、编译器需要的其他内部信息。此时编译器控制这种对象的分配和生存期。

而在这条语句中,使用了new()操作符,表明dsp所指向的对象通过动态分配内存,在运行堆上创建,这种动态分配对象的生存期应该由程序员控制,而非编译器。

(5)TinStack s3 = TinStack(250)

该语句等同于 TinStack temp (250); TinStack s3 = temp;,但是编译器优化可能会改变这个声明的实现,原来的声明表明我们正在请求的创建一个对象s3,而且为了初始化s3,必须创建一个包含250个元素的临时temp类对象,临时对象将在s3创建之后消失。

因此,我们不需要创建这样一个临时对象,并把它赋值给s3,这样很浪费时间。大多数编译器会选择直接创建一个大小为250的对象的s3.

创建对象时发生了以下三个步骤:

编译器需要获得对象所需的内存数量。

获得的原始内存被转换成一个对象,涉及将对象的数据成员防止在正确的位置,还有可能建立成员函数指针表等

最后,在前两部完成后,编译器通过新创建的对象调用构造函数。

动态分配相关与析构函数

(1)对于下列的函数

void printStack(TintStack thisOne)
{
    unsigned i =thisOne.howmany();
    for (unsigned j = 0; j < i; j++)
        cout << thisOne.pop() << " ";
    cout << endl;
}

当调用printStack()函数的时候,该函数接受一个对象的副本,编译器就会调用对象所属类的复制构造函数。从而使得函数获得一个原始对象的副本,用来初始化形参thisOne。

离开printStack()函数的时候,对象thisOne会发生什么情况?在运行时栈分配的一切都被清楚,而且编译器将回收它们所占用的内存。所有语言定义类型都是这样。

因此,对于局部变量i和j不在作用域内,编译器将回收之前被它们所占用的空间。与此类似,局部对象thisOne不在作用域内,因此也应该回收它所占用的内存空间。

对于对象的本身的大小,编译器很清楚。但是它并不知道对象中的指针_sp所指向的某些动态分配的内存。这时,析构函数就会排上用场。

无论何时对象离开作用域,编译器真正回收该对象所占用的内存之前,都会通过调用对象的析构函数,释放对象获得的任何资源。

换句话说,只有当对象在作用域中即将不再可见时,在函数返回之前或离开代码块之前,编译器才会调用析构函数。

(2)当我们使用动态分配时,就必须在使用之后手动使用delete释放资源。
调用delete操作符时,发生以下两个步骤:

若指针是一个指向类的指针,则通过调用该类的析构函数。

回收指针所引用的内存

赋值操作符

实现赋值操作符重载的时候必须注意:

确保对象没有自我赋值

复用被赋值对象(目的对象)的资源或者销毁它

从元对象中将带赋值内容赋值到目的对象

最后,返回目的对象的引用

类的接口

当类的用户查看类的时候,最关心的是类中的声明内容。通过观察类的共有成员函数,客户可获知对类对象进行的绝大多数操作。因此一个好的类,必须对类的接口有着良好的设计和注释。

类通常被用户用来创建对象或者通过继承创建其他类,而成员函数则被这些对象调用,我们要为类和成员函数提供有意义的名称。

此外,我们还应该为成员函数的参数使用合适的名称。这样用户可以清楚地知道某个参数的用途。

而在大多数情况下,仅通过查看函数、参数名称,无法清楚地了解类及其成员函数的用途,必须提供详尽的文档,其内容包括:

类的用途 预定用户 所依赖的类 类的限制是什么

参数的传递模式

每种成员函数都应该清楚地指明参数的传递模式,参数可以按照值传递、引用传递、指针传。与const联合使用,参数会更加的安全可靠。

每个参数的传递模式都给客户传达特定的含义,此外,有时还需准许你一些经长时间验证的有效规则。为参数选择合适的类型非常重要。

下面几个例子说明参数不同的传递模式设置:
注:主调函数指的是g()函数(或者main()函数),它调用另外一个函数f(),此时,f()是被调函数。

若有两个类T、X 以及X类的成员函数f()

(1)void X::f(T arg)
(2)void X::f(const T arg)
(3)void X::f(T& arg)
(4)void X::f(const T& arg)
(5)void X::f(T* argp)
(6)void X::f(const T*arg)
(7)void X::f(T* const argp)
(8)void X::f(const T* const argp)

为参数选择正确的模式:

函数的返回值