《C++面向对象高效编程(第2版)》——2.12 类中的访问区域

    xiaoxiao2024-01-23  158

    本节书摘来自异步社区出版社《C++面向对象高效编程(第2版)》一书中的第章,第2.12节,作者: 【美】Kayshav Dattatri,更多章节内容可以访问云栖社区“异步社区”公众号查看。

    2.12 类中的访问区域

    C++面向对象高效编程(第2版)每个类都有3个不同的访问区域。在我使用过的所有OOP语言中,只有C++精心设计了这3个区域。

    public区域是最重要的区域,为类的用户指定了类的接口。任何客户都可以访问public区域。

    本书的源代码样式

    (1)所有的类名都以T开始。类的行为与在语言中添加的新类型类似,因此使用前缀T 1。但是,在表示实现这些类的文件名时,去掉T。例如,包含TInt接口的文件名是Int.h。

    (2)所有的成员函数名,首字母大写(如Add()、FlipSlip()等)。

    (3)所有的枚举(enum)都以E开始,枚举中的成员名以e开始。

    (4)函数中的所有局部(自动)变量名,首字母小写。

    (5)所有的常量都用大写字母和数字表示。例如:

    const unsigned DAYS_IN_WEEK = 7(6)下划线(_)用于命名常量(如上DAYS_IN_WEEK所示)。

    (7)头文件中的注释置于声明之前,简短注释可与代码位于同一行。

    (8)所有的数据成员(实例变量)都以下划线(_)开始。

    (9)全局变量和全局常量通常以g开始。

    例如:

    // 改变数字的符号 void FlipSign(); TInt operator+(...) const; // 加法 TDate gEpoch(“1-1-1970”);```` 作为public区域的对立面,private区域是任何客户都不能直接访问的区域,只供实现使用。换言之,只有类的实现才能访问private区域。 第3个区域是protected区域,用于代码的扩展和复用(继承)。后面的章节中将介绍相关内容。 在一个类中,可以声明多个这些区域(public,protected和private),编译器将负责合并。 理解代码 (1)构造函数(constructor):所有与类名(本例为TInt)相同的该类成员函数都称为构造函数,它们用于创建和初始化新对象。为什么我们需要构造函数? 声明语言定义类型(如int)时,该语言(或编译器)会创建相应类型的变量(本例为int),初始值(如果指定)将储存于int变量中。例如: int i; // 无初始值 int j = 10; // 创建初始值为10的变量j 该例中,编译器负责为i和j分配(或预留)内存,并初始化j为10。与此类似,如果是创建对象,则需要一个创建对象的工具,并且能在数据成员中储存适当的值。如在TInt类的例子中,用户希望为TInt类对象提供某些初始值。这些都可以利用构造函数来完成,一个类可以提供若干重载构造函数。在C++中,创建(或初始化)对象的唯一途径是调用类提供的构造函数。在TInt类中,

    TInt(int value);TInt(long mostSignificant, unsigned long leastSignificant);TInt(long value);TInt(short value);TInt();TInt(const TInt& arg);`以上都是构造函数,它们的行为与重载函数(编译器将它们作为函数名不同的重载函数来实现)类似。欲了解更多构造函数相关内容,请参阅第3章。

    (2)析构函数(destructor):名称与类名相同,且带前缀~的成员函数称为析构函数。在C++中,其他类型(或声明)名称都不会有~。当某个类对象不再处于程序段的作用域内时,该函数负责清理工作。与构造函数不同,一个类只能有一个析构函数。析构函数在无用单元收集(garbage collection)中非常有用。

    从一个函数(或块)中退出时,编译器将自动销毁在该函数(或块)中创建的对象。但是,对象可能已经聚集了资源(动态内存、磁盘块、网络连接等),这些资源储存在对象的数据成员中,由成员函数操控。由于对象被销毁(退出函数)后不可再用,因此,必须释放该对象储存的资源。但是,编译器并不知道这些资源(它们可能由动态分配),因此,对象必须负责释放它们。为了帮助对象完成这些工作,在退出函数(或块)时,所有在该函数(或块)中静态创建(即不使用new()操作符创建)的对象都将调用析构函数。析构函数将释放对象储存的所有资源。换言之,析构函数提供了一种机制,即对象在被销毁前可自行处理掉自身储存的资源。参见下面复制构造函数的例子。析构函数的相关内容将在第3章中详细讨论。

    构造函数和析构函数都是特殊的成员函数。在声明中,它们无任何返回值类型,这表明它们不能返回任何值2。

    (3)复制构造函数(copy constructor):这是一个特殊的构造函数,用于通过现有对象创建新对象,因而称为复制构造函数。复制构造函数有独特的函数原型(或签名)3,很容易识别。当内置数据类型变量(如int和char)从一个函数按值传递至另一个函数时,由编译器负责复制该变量,并将其副本传递给被调函数(called function)。当对象按值传递给一个函数时,该对象的副本必须像内置类型那样被复制。然而,对象不是简单变量,它们是由程序员实现的复杂实体。因此,编译器在复制对象时需要帮助。逻辑上,应该由对象的实现者负责复制对象(内置类型的实现者是编译器)。复制构造函数就提供了这样的帮助。无论何时需要对象的副本,编译器都会调用复制构造函数来复制对象。特别是,当类在它的实现中使用动态内存时,复制构造函数必不可少。如果类的实现者不提供复制构造函数,编译器将会自动生成一个复制构造函数。至于这个生成的复制构造函数是否满足类的要求,那完全是另一个问题,在此暂不作赘述。注意,复制构造函数是一个特殊语义的构造函数。我们在创建和初始化一个新对象(从无到有地创建)时调用普通构造函数,在通过现有对象创建一个新对象时才调用复制构造函数。这是复制构造函数与其他构造函数的主要区别。出现下列情况时,将调用复制构造函数:

    对象从一个函数按值传递至另一个函数时; 对象从函数按值返回时; 通过现有对象初始化一个新对象时。 void f() {   TInt x(100);  // 创建一个TInt类型的对象x   // g() 是一个接受TInt值,且无返回值的函数   void g(TInt); // g()函数的原型   // ...   g(x); } void g(TInt y) {   // ...代码... }``` 当f()函数调用带参数x的g()函数时,复制构造函数构造了一个x的副本,该副本作为实参提供给g()的形参y。从g()退出时,对象y(该函数的形参)被销毁,即对象y调用了析构函数。不用担心,在g()内操作对象y不会影响f()内的对象x(这正是按值传递语义所期望的)。 相关内容详见第3章和第4章。 (4)赋值操作符(assignment operator):复制构造函数用于通过现有对象创建新对象,而赋值操作符用于将现有对象显式赋值给另一现有对象。赋值是用户显式完成的操作。例如:

    TInt a;TInt b(100);// ...a = b;   // 将b赋值给a,a和b都已存在。`注意,源对象是b,目的对象4是a。两个对象都是程序员使用的现存对象。如果类的实现未提供赋值操作符,在需要时编译器会为类生成一个赋值操作符。与复制构造函数的情况相同,编译器生成的赋值操作符是否满足类对象的要求是另一个问题,在此暂不作赘述。编译器生成的赋值操作符相当简单,通常用处不大。详见第3章。

    (5)其他函数都是类的普通成员函数,它们通过TInt类的对象来操作。只有类的现有对象才能使用该类的成员函数(更精确地说是非静态成员函数)。

    (6)操作符函数(operator function):TInt是我们定义的新类型,我们希望提供一些操作符(如同+和-)让该类型更有用。也就是说,应该可以像整数(int)那样使用TInt类对象。例如:

    TInt a(10); TInt b(20); TInt c; int i = 100; int j = 200; int k; c = a + b; // a与b相加,并将结果储存至c中。 k = i + j; // 整数i和j相加,并将结果储存至k中。``` 使用+操作符将i与j相加,是相当简单的操作,+操作符由语言提供。换言之,语言知道如何相加两个整数,因为语言提供了int的实现。如果语言知道如何实现int,它当然明白如何将它们相加。 回到TInt例中,该抽象数据类型的实现由实现者提供。怎样才能让语言明白如何相加两个TInt类对象?如果我们为TInt类型提供实现,就必须负责提供+操作符的实现(如果需要的话)。但是,并不是所有的语言都允许自由实现操作符。万幸的是,C++允许抽象数据类型的实现者为该类型提供合理的操作符。我们需要做的是,将+操作符的含义扩展至TInt类型。这就是操作符重载(operator overloading)的概念。操作符函数是用于实现特定操作符的函数,其定义方式与定义其他成员函数类似。但是,操作符函数的名称前必须保留关键字operator,后面紧跟重载的操作符。相关内容详见第8章。 注意,操作符重载是非常强大的机制,很容易被滥用。类的实现者必须为实现的类谨慎选择合适的操作符。类只能使用它所支持(即实现)的操作符(赋值操作符,即=操作符除外,如果类的实现者未提供该操作符的实现,需要时编译器将自动生成一个)。      注意: 严格意义上说,类似+、-、*、/等操作符应作为非成员函数(或友元函数)实现。本例中的类接口如下所示:

    // TInt类型的操作符TInt operator+(const TInt& operand1, const TInt& operand2);TInt operator-(const TInt& operand1, const TInt& operand2);// 诸如此类... 已略去`为尽可能简化示例,略去若干细节。欲了解友元函数的概念,详见第7章。

    (7)成员函数声明中const的意义:在以上示例的头文件中,许多函数后都缀有const关键字。例如:

    void Print() const;这个const应用于函数,而非任何参数。这样的函数称为const成员函数,只有成员函数(非普通函数)可以声明为const。类的数据成员、函数的参数、对象、普通变量等也都可以声明为const。const成员函数保证在被调用期间,不会改变调用对象的状态。该例中,Print()函数确保在被调用期间,不会修改调用对象的数据成员。这样的函数称为选择器(selector),它只能从对象中读取数据,不能在对象的数据成员中写入(修改)数据。因此,

    TInt aInt; aInt.Print();``` 无论如何都无法修改aInt。在这些const成员函数中,编译器通过禁止给对象的数据成员赋值来确保对象的这种常量性(`constantness`)。详见第3章。 了解抽象数据类型的知识后,我们的影碟播放机抽象可以这样声明(在C++中):

    typedef short ErrorCode;class TLaserDiscPlayer {    public:     // 操作     ErrorCode Play(unsigned atChapter=0);     ErrorCode  Stop(void);     ErrorCode SearchFor(unsigned chapter);     ErrorCode OpenTray();     ErrorCode CloseTray();     void  PowerOn();     void  PowerOff();     ErrorCode Pause();     // 构造函数     TLaserDiscPlayer();     // 析构函数     ~TLaserDiscPlayer();     // 略去其他函数    private:     enum ETrayStatus { eClosed, eOpen };     enum EPowerStatus { eOff, eOn };     enum EplayerMode { ePlay, eSearch, ePause, eStop };     ETrayStatus  _trayStatus;  // 打开或关闭     EPowerStatus _powerStatus; // 开机或关机     EPlayerMode  _playerMode;  // 播放、查找等     // 略去其他成员};`以上的类声明未揭示任何关于如何实现接口函数(或简称接口)的细节。在C++中,这样的类声明称为接口文件(interface file)或类头文件(class header file),常后缀.h扩展名。因此,以上示例的文件表示为Int.h。

    相关资源:Visual.Basic.2010.&.NET4.高级编程(第6版)-文字版.pdf
    最新回复(0)