IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> Essential C++ Lippman 第4章 基于对象的编程风格 摘录 -> 正文阅读

[C++知识库]Essential C++ Lippman 第4章 基于对象的编程风格 摘录

一般而言,class由两部分组成:一组公开的(public)操作函数和运算符,以及一组私有的(private)实现细节。这些操作函数和运算符称为class的member function(成员函数),并代表这个class的公开接口。

身为class的用户,只能访问其公开接口。

Class的private实现细节可由member function的定义以及与此class相关的任何数据组成。

Class用户通常不会关心此等实现细节。身为一个用户,我们只利用其公开接口来进行编程。这种情形下,只要接口没有更改,即使实现细节重新打造,所有的应用程序代码也不需要变动。

4.1 如何实现一个Class

一般来说,我们会从所谓的抽象(abstraction)开始。比如Stack后进先出,push叠放数据,pop取出最后一个数据。查询是否为空,已满,或者查询元素个数,还需提供查看最后一个元素数值。

我们应该让Stack存放哪一类型的元素呢? 通用型的Stack应该可以存放各种类型元素。如果将其定义为class template,便可以达到这个目的。

class的声明以关键字class开始,其后接一个class名称。

class Stack;

此句只是作为Stack class的前置声明(forward declaration),将class名称告诉编译器,并未提供此class的任何信息(像是class支持的操作行为及所包含的data member等)。前置声明使得我们得以进行类指针(class pointer)的定义,或以此class作为数据类型。

接下来,在定义实际的Stack class object或访问Stack的任何一个member之前,必须先定义class本身。class定义的骨干看起来是这样:

class Stack
{
private:
    ...

public:
    ...

};

Class定义由两部分组成:class的声明,以及紧接在声明之后的主体。主体部分由一对大括号括住,并以分号结尾。主体内的两个关键字public和private,用来标示每个块的“member访问权限”。public?member可以在程序的任何地方被访问,private member只能在member function或是class friend内被访问。以下是Stack class的起始定义:

class Stack
{
public:
    bool push(const string&);
    bool pop(string& elem);
    bool peek(string& elem);
    
    bool empty();
    bool full();

    int size() { return _stack.size(); }


private:
    vector<string> _stack;

};

我的编码习惯是在data member之前加上下划线。

所有的member function都必须在class主体内声明。至于是否要同时进行定义,可自由决定。如果要在class主体内定义,这个member function会自动地被视为inline函数。若在class主体外定义member function,必须使用特殊语法。目的是为了分辨该函数是属于哪一个class。如果希望该函数为inline,应该在最前面指定关键字inline。

inline bool
Stack::empty()
{ return _stack.empty(); }

上述代码中的Stack::empty()告诉编译器说,empty()是Stack class的一个member。class名称之后的两个冒号(Stack::)即所谓的class scope resolution(类作用域解析)运算符。

然而,带有inline说明符的函数通常放在包含函数声明的文件中。

non-inline member function应该在程序代码文件中定义,该文件通常与class同名,其后接着扩展名。

4.2 什么是构造函数和析构函数

这些data member如何被初始化呢? 编译器不会自动为我们处理。如果我们提供一个或多个特别的初始化函数,编译器就会在每次class object被定义出来时,调用适当的函数加以处理。这些特别的初始化函数称为constructor(构造函数)。

constructor的函数名称必须与class名称相同。语法规定,constructor不应指定返回类型,亦不用返回任何值。它可以被重载(overload)。

class object定义出来后,编译器便自动根据获得的参数,挑选出应被调用的constructor。

最简单的constructor是所谓的default constructor。它不需要任何参数(argument)。这意味着两种情况,第一种,它不接受任何参数。第二种,它为每个参数提供了默认值。

Member initialization List(成员初始化列表)

Triangular::Triangular(const Triangular& rhs) : _length(rhs._length),         
                                                _beg_pos(rhs._beg_Pos), _next(rhs._beg_pos - 1){}

Member initialization list紧接着参数列表最后的冒号后面,是以逗号分隔的列表。其中,欲赋值给member的数值被放在member名称后面的小括号中;这使得他们看起来像是在调用constructor。

Member initialization list主要是将参数传递给member class object的constructor。

和constructor对立的是destructor。所谓destructor乃是用户自定义的一个class member。Destructor主要用来释放在constructor中或对象生命周期中分配的资源。

class Matrix
{
public:
    Matrix(int row, int col) : _row(row), _col(col) { _pmat = new double[row * col]; }
    ~Matrix() { delete[] _pmat;}

private:
    int _row, _col;
    double* _pmat;

};

Destructor的名称也有严格规定:class名称再加上~前缀。它绝对不会有返回值,也没有任何参数。由于其参数列表是空的,所以也绝不可能被重载(overload)。

编译器将会在Matrix对象被定义出来的那一刻,暗暗应用Matrix类的constructor。而在mat对象被清除的那一刻,编译器又会暗暗调用Matrix类的destructor,于是释放_pmat所指的内存。

事实上,C++编程的最难一部分之一,便是了解何时需要定义destructor而何时不需要。

Memberwise initialization(成员逐一初始化)

Matrix mat(4, 4);

Matrix mat2 = mat;

其中,default memberwise initialization会将mat2的_pmat设为mat的_pmat的值。这会使得两个对象的_pmat都知道heap内的同一数组。当Matrix destructor应用在mat2上,该数组空间被释放。不幸的是,此时mat的_pmat仍指向那个数组,而你知道,对空间已被释放的数组进行操作,是非常严重的错误行为。

这个问题可以通过提供另一个copy constructor达到目的。这个copy constructor,其唯一参数就是一个const reference,指向一个Matrix object。

Matrix::Matrix(const Matrix& rhs) : _row(rhs._row), _col(rhs._col)
{
    int elem_cnt = _row * _col;
    _pmat = new double[ele_cnt];

    for (int ix = 0; ix < elem_cnt; ++ix)
        _pmat[ix] = rhs._pmat[ix];

}

当我们设计class时,必须先问问自己,在此class进行成员逐一初始化的行为模式是否适当?如果不适当,那么需要另行定义copy constructor和copy assignment operator。

4.3 何为mutable和const

int sum(const Triangular& trian);

trian只是个const reference参数,因此编译器必须保证trian在sum()之中不会被修改。但是,sum()所调用的任何member function都有可能改变trian的值。因此class的设计者必须在member function身上标注const,以此告诉编译器:这个member function不会更改class object的内容。

class Triangular
{
public:
    int length() const { return _length; }
    int beg_pos() const { return _beg_pos; }
    int elem(int pos) const;

    bool next(int& val);
    void next_reset() { _next = _begin_pos - 1;}
// ...
private:
    int _length;
    int _beg_pos;
    int _next;
    static vector<int> _elems;
};

const修饰符紧接于函数参数列表之后。凡是在class主体以外定义者,如果它是一个const member function,那么就必须同时在声明和定义中指定const。例如:

int Triangular::elem(int pos) const
{
    return _elems[pos - 1];
}

但是如果const member function返回一个non-const reference指向member data,实际上是将member data开放出去,允许程序在其他地方加以修改。由于member function可以根据const与否而重载,因此有个方法可以解决这个问题:提供两份定义,一为const版本,以为non-const版本。例如:

const int& beg_pos() const { return _beg_pos; }
int& beg_pos() { return _beg_pos; }

non-const class object会调用non-const版,const class object调用const版。

设计class时,鉴定其const member function是一件很重要的事情。如果忘了做,要知道,没有一个const reference class参数可以调用公开接口中的non-const成分(但目前许多编译器对此情况都只给警告)。

mutable data member(可变的数据成员)

以下是sum()的一种形式:

int sum(const Triangular& trian)
{
    if (!triangu.length())
        return 0;

    int val, sum = 0;
    trian.next_reset(); // 改变了next值
    while (trian.next(val))    // 改变了next值
        sum += val;

    return sum;
}

在sum函数中,trian是个const object,而next_reset()和next()都会改变_next值,它们都不是const member function。但是他们却被trian调用,于是造成错误。

如果希望采用sum()的这份实现,则next()和next_reset()势必得改为const。但它们真的改变了_next的值呀!

_length和_beg_pos提供了数列的抽象属性。如果我们改变trian的长度或起始位置,形同改变其性质。然而,_next只是让我们得以实现iterator机制,它本身不属于数列抽象概念上的一环。改变_next的值,从意义上说,不能视为改变class object的状态,或者说不算破坏了对象的常量性(constness)。关键字mutable可以让我们做出这样的声明。只要将_next标示为mutable,我们就可以宣称:对_next做出的该改变并不会破坏class object的常量性。

class Triangular
{
...
    bool next(int& val) const;
    void next_reset() const { _next = _begin_pos - 1;}
private:
...
    mutable int _next;
    static vector<int> _elems;
};

现在,next()和next_reset()即可以修改_next的值,又可以被声明为const member function。这样一来,上述的sum()实现就没得问题了。

4.4 什么是this指针

我们设计一个copy函数时,一个可能的实现是:

Triangular& Triangular::copy(const Triangular& rhs)
{
    _length = rhs._length;
    _beg_pos = rhs._beg_pos;
    _next = rhs._beg_pos - 1;
    
    return ???
};

Triangular tr1;
Triangular tr2(8, 9);

tr1.copy(tr2);

上述的赋值操作_length指向tr1内相应的成员。不过我们还需要一个可以指向tr1整个对象的方法。所谓的this的指针扮演了这样角色。

this指针在member function内用来指向其调用者(一个对象)。

在编译器工作过程内,this指针被自动加到每一个member function的参数列表内;

Triangular& Triangular::copy(const Triangular& rhs)
{
    if (this != &rhs)
? ? {
        this->_length = rhs._length;
? ?     this->_beg_pos = rhs._beg_pos;
? ?     this->_next = rhs._beg_pos - 1;
? ??
? ?     return *this
    }
};

在member function内,this指针可以让我们访问其调用者的一切。

4.5 静态类成员

static data member用来表示唯一的,可共享的member。它在同一类的所有对象中被访问。

class Triangular
{
...
    static vector<int> _elems;
};

对class而言,static data member 只有唯一的一份实体,因此我们必须在程序代码中提供其清楚的定义。这样看起来很像全局对象(global object)的定义,其名称必须附上class scope运算符。

vector<int> Triangular::elems = {...};

如果要在class member function内访问static data member,其方式有如访问一般(non-static)数据成员。

像const static int data member,可以在声明时为其明确指定初值。

static member function(静态成员函数)

若member function并未访问任何non-static data member。它的工作与任何对象都没有任何关联,那么可以将其声明为static member function。声明方式实在声明之前加上关键字static。

class Triangular
{
...
    static bool is_elem(int);
};

当我们在class外部进行member function的定义时,无需重复加上关键字static。(这个规则也适用于static data member)。

4.6 打造一个Iterator Class

Triangular trian(1, 8);
Triangular::iterator it = trian.begin(), end_it = trian.end();

while (it != end_it)
{
    cout << *it << ' ';
    ++it;

}

为上述代码得以工作,我们必须为iterator class定义!=、*、++等运算符。我们可以像定义member function那样来定义运算符。运算符函数看起来很像普通函数,唯一差别是它不用指定名称,只需在运算符前加上关键字operator即可。例如:

class Triangular_iterator
{
public:
    Triangular_iterator(int index) : _index(index - 1) {}
    bool operator==(const Triangular_iterator&) const;
    bool operator!=(const Triangular_iterator&) const;
    int operator*() const;
    Triangular_iterator& operator++();
    Triangular_iterator operator++(int);
private:
    void check_integrity() const;
    int _index;
};

inline bool Triangular_iterator::
operator==(const Triangular_iterator& rhs) const
{ return _index ==  rhs._index; }



inline bool Triangular_iterator::
operator!=(const Triangular_iterator&) const;
{ return !(*this == rhs); }

Triangular_iterator维护一个用来索引Triangular中存储数列元素的那个static data member,也就是_elems。所以必须赋予Triangular_iterator的member function的特殊访问权限。

任何运算符如果与另一个运算符性质相反,我们通常会以后者实现出前者,取反即可。

运算符重载的规则:

????????不可以引入新的运算符。出来./.*/::/?:四个运算符,其余运算符皆可重载。

? ? ? ? 运算符的操作数不可改变。

? ? ? ? 运算符的优先级不可改变;

? ? ? ? 运算符函数的参数列表内,必须至少有一个参数为class类型。

运算符的定义方式,可以像member function一样,也可以像non-member function一样。

non-member运算符的参数列表中,一定会比相应的member运算符多出一个参数,也就是this指针。

接下来我们需要做的,便是为Triangular提供一组begin()/end() member function,并支持前述iterator定义。这需要用到nested type。

class Triangular
{
public:
    typedef Triangular_iterator iterator;

    Triangular_iterator begin() const { return Triangular_iterator(_beg_pos); }
    Triangular_iterator end() const { return Triangular_iterator(_beg_pos + _length); }
...
};

嵌套类型(nested type)

typedef可以为某个类型设定另一个不同的名称。 其通用形式;

typedef existing_type new_name;

4.7 合作关系必须建立在友谊的基础上

任何class都可以将其他function或class指定为friend。而所谓的friend,具备了与class member function相同的访问权限,可以访问class中的private member。

只需要在某个函数原型(prototype)前加上关键字friend,就可以将它声明为某个class的friend。

class Triangular
{
...
friend int Triangular_iterator::operator*();
friend void Triangular_iterator::check_integrity();
...
};

为了让定义成功通过编译,我们必须在上述两行前,先提供Triangular_iterator的定义让Triangular知道。否则编译器就没有足够的消息可以确定上述两个函数原型是否正确,也无法确定它们是否的确是Triangular_iterator的member function。

也可以令class A与class B建立友元关系,借此让class A的所有member function都成为class B的friend。例如:

class Triangular
{
...
friend class Triangular_iterator;
...
};

如果是class之间建立友元,就不需要再友谊声明之前先显现class的定义了。

友谊的考虑,通常是为了效率。如果只是为了进行某个data member的读取和写入,那么,为它提供具有public范文权限的inline函数,就是建立友谊之外的一个替代权限。

4.8 实现一个copy assignment operator

对于default memberwise copy不适用的class,我们需要显示定义一个copy constructor和一个copy assignment operator。

4.9 实现一个function object

所谓的function object乃是一个“提供有function call运算符”的class。

例如,当编译器遇到函数调用时:

lt(ival);

lt可能是函数名称,可能是函数指针,也可能是提供了function call运算符的function object。如果lt是个class object,则编译器会在内部将此语句转换为lt.operator(ival)。

function call运算符可接受任意个数的参数:零个,一个或更多。举个例子,它可以被用来支持Matrix下多维度下标(subscripting index)的操作,因为语言所提供的subscript运算符仅能接受一个参数。

下面是一个例子:

class LessThan
{
public:
    LessThan(int val) : _val(val) {}
    int comp_val() const { return _val; }
    void comp_val(int nval) { _val = naal;}

    bool operator()(int _value) const;

private:
    int _val;
};

inline bool LessThan::
operator()(int value) const
{
    return _val < value;
}

通常我们会把function object当作参数传递给泛型算法。

4.10 重载iostream运算符

ostream& operator<<(ostream& os, const Triangular& rhs);

其中ostream对象并未声明为const,因为每个output操作都会更改ostream对象的内部状态。为什么不把output运算符设计成member function?因为member function的左操作数必须是隶属于同一个class的对象。如果设计成member function,那么class object就必须放在output运算符的左侧了。这种形式对于class用户必定感到十分疑惑。

4.11 指针,指向Class Member Function

pointer to member function在声明时,必须指明其所属类。

// 数据类型名(类名::*指针变量名)(参数列表);
void (num_sequence::*pm)(int) = 0;

上式便是将pm声明为一个指针,指向num_sequence的member function,后者的返回类型必须是void,且只能接受单一参数。等于0,只是说明该指针目前不指向任何函数。

给该指针赋值;

// 指针变量名 = &类名::成员函数名;
pm = &num_sequence::function_name;

// 要定义一个指向成员函数的函数指针
pm ptmf = &num_sequence::function_name;

由函数指针引出的.*运算符:pointer to member selection运算符,只针对class object工作。我们必须为它加上小括号才能正确工作。

num_sequence ns;
int pos;
(ns.*pm)(pos);

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-11-24 07:46:15  更:2021-11-24 07:47:58 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 9:04:50-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码