`
qqsunkist
  • 浏览: 33095 次
  • 性别: Icon_minigender_1
  • 来自: 大连
社区版块
存档分类
最新评论

有效创建一个类(二)

    博客分类:
  • C++
阅读更多
上一篇记录了在创建一个类时,首先要考虑这个类的构造函数、拷贝构造函数、拷贝赋值操作、以及析构函数的声明及定义;那么本篇主要说明的是有关类成员的声明及定义;有关类成员声明的工作实际上大多数时候都是在决定类构造函数、拷贝函数及析构函数之前需要考虑的。那么为什么我要把构造函数等作为创建类考虑的第一个因素呢?因为在大多数软件设计的情况下,无论这个软件是一个大型的应用程序还是其中的微小组件,都是先进行概要设计再进行详细设计。而概要设计的核心工作就是给出组件完成什么功能,为了完成目标功能如何与其他组件协同工作,遵守什么样的协定。详细设计才会根据功能以及组件间的协定给出类定义。那么这就意味着概要设计完成后,类的设计者应该已经对需要定义的类与类之间的松耦合关系、类层次结构甚至类应该拥有一些什么样的成员有了一个大致蓝图。而根据上述这三个因素,构造函数、拷贝函数、以及析构函数可以优先考虑。当然不能否认,在具体的类成员(尤其下述的前两种)出来后,可能对构造函数等一族需要进行进一步修改。


这部分工作虽然看上去简单,但是如果视同创建一个类与创建一个类型相同的话,那么这部分工作就变得不是那么简单了。

大致类成员分成四种:

1. 类常量
2. 类变量
3. 类成员函数(读取第2种的)
4. 类成员函数(改变第2种的)

下面我们针对每一种类成员分别说明。

1. 类常量

作为类专属常量,为了将常量的作用域限制于类内,必须让它成为类的一个成员;而为确保此常量至多只有一份实体,你必须让它成为一个static成员:

class Cache {
private:
    static const int BUFSIZE = 4196;
    char buffer[BUFSIZE];
    // ...
}; 

上述只是声明式而不是定义式,如果要取某个类专属常量的地址甚至即使不取其地址时,C++编译器却坚持要看到一个定义式,所以我们必须提供定义式如下:

const int Cache::BUFSIZE;

这个定义式请放入实现的文件中而非头文件中。因为声明时,类常量获得初始值 ,因此定义时不可以再设初始值。顺带一提,宏定义#define无法创建一个类专属常量,因为#define不重视作用域。一旦宏被定义,它就在其后的编译过程中有效。这表示不仅不能定义类专属常量,而且不能提供任何封装性。 老的编译器也许不支持上述语法,它们不允许static成员在其声明式上获得初始值,另外所谓的"in-class初值设定";也只允许对整数常量进行。那么怎么办呢?可以通过下述的方式进行:

class Cache {
private:
   static const int BUFSIZE;
   char buffer[BUFSIZE];
   // ... ... 
};
const int Cache::BUFSIZE=4196; 


假如在编译期间需要一个类常量值,例如上述的Cache::buffer的数组声明式中,编译器坚持必须在编译期间知道数组的大小。这时候万一编译器不允许“staic整数型类常量完成in class初值设定”,可使用the enum hack补偿。
class Cache {
private:
  enum { BUFSIZE=4196};
  char buffer[BUFSIZE];
  // ... ...
};

关于enum hack我会详细介绍。

除了类常量外,还有一种是non-static的const成员,这种用const修饰的成员向编译器表达了一个语义约束,表示这个成员不该被改动。当然这个语义约束理解起来并不困难,而编译器会强制实施这项约束。如果类中成员有这样的约束事实存在,那么请一定清晰的告诉编译器,以获得它的帮助。

const可谓多才多艺。它可以用来修饰
(1)global或者namespace作用域中的常量
(2)文件、函数、或者block scope中被声明为static的对象
(3)类内部的static或者non-static的成员变量
(4)指针本身,指针所指对象;

char greeting[] = "Hello";  
char *p = greeting;         //non-const ptr, non-const data
const char *p = greeting;   //non-const ptr, const data
char* const p = greeting;   //const ptr, non-const data
const char* const p = greeting; //const ptr, const data


有人发明的一种指针的读法比较有助于记忆和识别,这种指针读法就是从右往左念。
例如,最后一个p是常量指针指向字符常量;另外,在《The C++ Programming Language》一书中,曾经提及过“引用”可以理解为常量指针,一旦被初始化或者赋值,其指针地址不可更改。

下面要知道const修饰的标识符什么时候被初始化?
实际上const修饰的标识符有两种,一种称为编译器const对象;另一种称为运行时const对象(函数参数为主);编译期const对象是针对编译器而言,如果用于初始化const对象的值在编译期即被确定,则通过类型检查后用这个初始值代替这个const对象本身(听起来好像跟宏#define相似啊):)而对于运行时const对象,其初始化时机和对象本身被创建的时机相同。作为函数参数的const对象(包括任何引用类型)在参数传递生成参数时同时初始化。

2. 类变量

类变量感觉上好像没什么可说的,但是这部分涉及到了OO的三大特性之一——封装。
类变量也称为数据成员,那么在一个类中的数据成员可以用public, protected, private修饰。这也是OO的封装级别,public意味着完全不需要封装,protected意味着派生类可以访问,但并不比public更具有封装性,private表示只有类成员函数以及友元类函数可以访问。原则上,类变量要求用private修饰。

在具体谈到某个数据成员的封装级别之前,我们应该首先考虑这个数据成员是否有必要被封装;换句话说,如果没必要封装,就表示它可以不是该类的数据成员。按照开闭原则和里氏替换原则来说的话,被封装的数据应该是那些变化的数据,而不是那些不变化的数据。

一旦决定某数据是需要被封装在类中的以后,那么就是决定封装级别的时候了。考虑封装级别的时候,就参考下面的引用:
引用

封装的重要性比你最初见到它时还重要。如果你对客户隐藏成员变量(也就是封装它们),你可以确保class的约束条件总是会获得维护,因为只有成员函数可以影响它们。犹有进者,你保留了日后变更实现的权利。如果你不隐藏它们,你很快会发现,即使拥有class源代码,改变任何public事物的能力还是极端受到束缚,因为那会破坏太多客户代码。Public以为不封装,而几乎可以说,不封装以为不可以改变,特别是对被广泛使用的classes而言。被广泛使用的classes,是最需要封装的一个族群,因为它们最能够从“改采用一个较佳实现版本”中获益。“封装性与当期内容改变时可能造成的代码破坏量成反比” -- 参考《EFFECTIVE C++》条款22, 23.


另外需要考虑成员变量的声明通过采用外覆类型(wrapper types)可以使得用户不易误用。例如:(这个例子摘自《Effective C++》)
class Date{
public:
  Date(int month, int day, int year);
private:
  int m, d, y;
};

乍看之下,这个类变量的声明看上去挺合理的。但是Date的客户却不像想象中的那么合理使用这个类;例如,欧洲的客户很容易输入错误的次序传递参数: Date d(30, 12, 2010); 更有可能输入错误的日期Date d(2, 30, 2010);那么怎么防范呢?很多人第一反应是,应该在所有的接口函数加上一些判断语句就可以了。如
Date::Date(int month, int day, int year){
 if(month>=1 && month <=12)
     m = month;
 else
     throw bad_date();
 //...
};

这样,虽然能达到目的,但是不觉得这样一个构造函数已经很丑陋了吗?上面的代码还没有写出可以解决客户容易误用的第二个错误的判断语句。如果再加上那样的判断语句,估计会更丑陋的。那么还有什么更好的方法看上去不那么丑陋吗?
struct Day{
 explicit Day(int d):val(d){}
 int val;
};

struct Month{
 explicit Month(int m):val(m){}
 int val;
};

struct Year{
  explicit Year(int y):val(y){}
  int val;
};

class Date{
public:
  Date(const Month& m, const Day& d, const Year& y);
private:
  Year y;
  Month m;
  Day d;
};

Date d(30, 12, 2010) // error! wrong type
Date d(Day(30), Month(12), Year(2010)); // error! wrong type
Date d(Month(12), Day(30), Year(2010)); // correct!


针对第二种容易误用的解决方案,我想可以通过ENUM+外覆类型可以得到更好地解决;
那么类变量在声明时,除了考虑其封装性外,还需要考虑其合理范围,尽量避免误用。

3. 成员函数(读取第2种的)

设计这种成员函数时,在C++语言中需要注意和理解三个事项;
(1)const 修饰符
(2)inline 的里里外外
(3)避免返回handler指向对象内部成分

(1)如上所述,const多才多艺,但const最具威力的用法就是面对函数声明时的应用;针对一个函数的声明式,const可以和函数的返回值、各参数、函数自身产生关联。但是针对第3种const成员函数而言,主要说明下const主要跟函数返回值和函数本身的两种关联的意义。

I. 令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。
II. 将const实施于成员函数的目的是为了确认该成员函数可作用于const对象身上。这样一来,使得class接口比较容易被理解,二来呢,它们使“操作const对象”成为可能,对于编写高效率代码是个关键,也很重要。(例如,pass-by-reference to const 这一技术的前提是,我们有const成员函数可用来处理取得的const对象)注意,C++成员函数只因constness不同,可以被重载。这是个非常重要的特性。

(2)inline修饰符语义是对inline函数的每一次调用都以函数本体替换之。那这个语义跟宏的函数定义不一样吗?语义上一样,但是执行上不一样,要比宏更好。为什么呢?很显然这样每一次调用它们时,由于事实上函数已经被函数本体替换,所以不需要蒙受函数调用所招致的额外开销。当然,至此宏也能这样做到;但是inline函数实际上只是向编译器发出这样一个申请,但是申请的结果完全取决于编译器优化的结果。这就好像,你申请美国的过境签证一样,即使万事俱备,也未必会得到审批。

由于inline的语义,有足够的理由可以相信,这样会导致程序产生的目标执行代码会膨胀。如果在一台资源,尤其是内存吃紧的机器上运行目标代码时,这样的代码膨胀会导致你的程序招致内存换页所引起的开销,降低cache命中率。但是如果inline函数本体很小,替换后的结果如果比函数调用更小,那么我们也有足够的理由相信产生的目标代码更小,当然程序执行效率也会很高,也提高了cache的命中率。

那么什么样的函数本体算很小,可以比函数调用更小呢?
至少函数体包含循环语句,或者调用virtual函数,再或者利用函数指针调用都会使得inline的申请遭到拒绝。但是仅仅列出这两个标准,似乎并不是让人很满意的答案。幸运的是,现代编译器大多数都提供了一个诊断级别:如果无法将被申请函数inline化,会发出警告信息。

另一个慎重使用inline便是由于其语义而导致的debug困难。

(3)
摘自《Effective C++》- 条款28
class Point {
public:
  Point(int x, int y);
  ...
  void setX(int newVal);
  void setY(int newVal);
  ...
};

struct RectData {
  Point ulhc; //upper left hand corner
  Point lrhc; //lower right hand corner
};

class Rectangle {
public:
  Point& upperLeft() const { return pData->ulhc; }
  Point& lowerRight() const { return pData->lrhc; }
...
private:
  std::tr1::shared_ptr<RectData> pData;
};

虽然这样可以通过编译,但是却是个逻辑上自相矛盾的错误。一方面upperLeft()和lowerRight()被声明为const成员函数,因为它们的目的只是为客户提供一个得知Rectangle相关坐标点的方法,而不是让客户修改Rectangle。另一方面,两个函数都返回引用指向private数据,使得private的封装形同虚设。

因此:
(一)、成员变量的封装性最多只等于“返回其reference”的函数的访问级别。
(二)、const成员函数返回一个引用且引向数据与对象自身有关联,那么函数调用者有机会更改对象内部数据。
分享到:
评论

相关推荐

    android 二级分类列表 listview

    在创建二级分类列表时,我们通常会自定义一个Adapter,它负责解析数据并生成列表项视图。对于二级分类,我们可以将一级分类视为父项,二级分类视为子项。在Adapter中,我们需要处理父项与子项的点击事件,以便展开或...

    ABAQUS中二维粘结单元的创建_二维cohesiveelement创建_

    1. 准备模型:首先,我们需要一个合适的几何模型,这个模型应该包含粘结区域。可以使用ABAQUS的建模工具创建或导入几何模型。 2. 创建单元类型:在ABAQUS中,二维粘结单元主要有COH2D4和COH2D8两种类型。COH2D4是四...

    在ASP.NET 2.0中操作数据之二:创建一个业务逻辑层

    - 首先,为BLL创建一个或多个类,每个类通常对应一个数据表或一组相关的数据操作。 - 通常会在项目中创建一个BLL文件夹,用于存放这些业务逻辑类。 - 使用Visual Studio的解决方案资源管理器,可以快速创建这些类...

    Android二级分类列表ListView GirdView in ViewPager

    在这种情况下,"Android二级分类列表ListView GridView in ViewPager" 的实现方式是一个重要的知识点。这个主题主要涉及如何在一个ViewPager中嵌套ListView和GridView,以便用户可以水平滑动切换不同的分类,同时每...

    java等级分类例子

    类的继承允许我们创建一个新类(子类),它扩展或基于已存在的类(父类)。这种方式有助于代码重用,使得我们可以构建更加复杂且层次分明的系统。在这个例子中,“一二级分类”可能代表了不同层次的类结构。 首先,...

    Accp6.0 使用Java实现面向对象编程 第二章

    继承是面向对象编程中的一个核心概念,它允许创建一个新的类(子类),这个新类继承了现有类(父类)的所有属性和方法。通过继承机制,可以有效地减少代码重复,提高代码的可读性和可维护性。此外,继承还使得类的...

    线程信息输出类

    总之,“线程信息输出类”是MFC多线程编程中的一个重要工具,它通过线程同步和消息传递机制,保证了子线程与主线程之间的有效通信,从而实现数据安全地更新到用户界面。理解和熟练使用这样的类,对于编写高效、稳定...

    一个生成xls二进制文件的类

    标题中的“一个生成xls二进制文件的类”指的是在编程中创建能够生成Microsoft Excel的XLS文件格式的代码类。XLS是Excel早期版本(97-2003)使用的二进制文件格式,用于存储表格数据、计算公式和其他相关数据。这种类...

    C++创建window服务

    文章强调了一个关键概念——使用一个特定的C++类来简化服务程序的开发过程。这种方法的核心优势在于,开发者只需要重写几个基本类中的虚拟函数就能实现自己的服务程序。此外,文中提供了三个示例源代码: - **NT...

    Scott Mitchell 的ASP_NET 2_0数据教程之二:创建一个业务逻辑层

    【ASP.NET 2.0 数据教程之二:创建一个业务逻辑层】 在ASP.NET 2.0中,数据访问和业务逻辑的分离是构建高效、可维护的应用程序的关键。本教程将详细介绍如何创建一个业务逻辑层(Business Logic Layer,BLL),以便...

    Eclipse中设置在创建新类时自动生成注释

    在模板编辑器中,可以看到一个名为`"${filecomment}"`的占位符,这是我们自定义文件级注释的关键位置。将光标置于该行后,可以插入自定义的注释模板。例如,参考提供的部分代码: ``` /** * @author E-mail: * @...

    Java实验二类与对象

    实验内容不仅限于这两个类,还可能包括一个圆类(Circle),该类通常会包含半径(radius)属性,以及计算面积(使用πr²)和周长(2πr)的方法。不过这部分代码在提供的内容中没有给出。 通过这样的实验,学生...

    jsp编程技巧 创建表示层之类

    例如,创建一个Servlet或Filter来处理请求,然后通过RequestDispatcher将控制权传递给JSP页面,以展示结果。 五、使用EL表达式和JSTL Expression Language (EL)和JavaServer Pages Standard Tag Library (JSTL)是...

    jsp计算一元二次方程的根

    1. 创建一个Java Bean类,用来封装一元二次方程的系数和根。在Java中,一个Bean类通常指遵循一定命名规则的简单Java类,能够被序列化并具有无参构造器和获取/设置(getter和setter)属性的方法。本实验中的Java Bean...

    二进制与字符串之间的转换类CBinary

    例如,一个字节的二进制值`01001000`对应ASCII编码中的字符'H'。 2. **字符串转二进制**:将字符串转换为二进制数据。这个过程是上面操作的逆过程,它将字符串中的每个字符根据选定的编码转换为其对应的二进制表示...

    nextdate函数有效等价类实训

    "有效等价类"是一个测试术语,它指的是一个测试用例集合,其中每个用例都能反映出程序的一个不同行为。在设计nextdate函数的测试时,我们需要考虑各种有效日期(如2月28日、2月29日)、边界条件(如12月31日之后的...

    SQL实验:创建表并实施完整性,使用规则和缺省,更新表中数据.doc ) 您可以上传小于60MB的文件

    在SQL中,创建一个基本表的基本语法如下: ```sql CREATE TABLE 表名 ( 列名1 数据类型 约束, 列名2 数据类型 约束, ... ); ``` 其中,“表名”是您要创建的表的名称;“列名”表示表中的每一列的名称;“数据...

    递归的方式创建二叉树

    1. 定义二叉树节点类:首先,我们需要定义一个表示二叉树节点的类,包含数据字段和指向左右子节点的引用。 ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self....

    java内部类的讲解

    一个成员内部类的实例总是与外部类的一个实例相关联,因此,你不能在没有外部类实例的情况下创建成员内部类的实例。 3. **本地内部类(Local Inner Classes)**:这些内部类是在方法体或初始化块中定义的。它们可以...

    C#创建多线程应用程序

    - 使用Thread类的构造函数,传入一个委托(代表线程要执行的方法)。 ```csharp Thread thread = new Thread(new ThreadStart(MyMethod)); ``` - 使用Lambda表达式简化代码。 ```csharp Thread thread = new ...

Global site tag (gtag.js) - Google Analytics