一个类:
class Sales_item { public://程序的所有部分都可以访问带有 public 标号的成员。类型的数据抽象视图由其 public 成员定义。 // operations on Sales_item objects double avg_price() const; bool same_isbn(const Sales_item &rhs) const//const 成员不能改变其所操作的对象的数据成员。 { return isbn == rhs.isbn; } //构造函数一般就使用一个构造函数初始化列表,来初始化对象的数据成员
Sales_item(): units_sold(0), revenue(0.0) { } private://使用类的代码不可以访问带有 private 标号的成员。private 封装了类型的实现细节。 std::string isbn; unsigned units_sold; double revenue; }; double Sales_item::avg_price() const { if (units_sold) return revenue/units_sold; else return 0; }
类背后蕴涵的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程(和设计)技术。类设计者必须关心类是如何实现的,但使用该类的程序员不必了解这些细节。相反,使用一个类型的程序员仅需了解类型的接口,他们可以抽象地考虑该类型做什么,而不必具体地考虑该类型如何工作。
函数是封装的一种形式:函数所执行的细节行为被封装在函数本身这个更大的实体中。被封装的元素隐藏了它们的实现细节——可以调用一个函数但不能访问它所执行的语句。同样地,类也是一个封装的实体:它代表若干成员的聚焦,大多数(良好设计的)类类型隐藏了实现该类型的成员。
class和struct的区别:如果类是用 struct 关键字定义的,则在第一个访问标号之前的成员是公有的;如果类是用 class 关键字是定义的,则这些成员是私有的。
并非所有类型都必须是抽象的。标准库中的 pair 类就是一个实用的、设计良好的具体类而不是抽象类。具体类会暴露而非隐藏其实现细节。一些类,例如 pair,确实没有抽象接口。pair 类型只是将两个数据成员捆绑成单个对象。在这种情况下,隐藏数据成员没有必要也没有明显的好处。在像 pair 这样的类中隐藏数据成员只会造成类型使用的复杂化。尽管如此,这样的类型通常还是有成员函数。特别地,如果类具有内置类型或复合类型数据成员,那么定义构造函数来初始化这些成员就是一个好主意。类的使用都也可以初始化或赋值数据成员,但由类来做更不易出错。
class Screen { public: typedef std::string::size_type index; // return character at the cursor or at a given position char get() const { return contents[cursor]; } char get(index ht, index wd) const; // remaining members private: std::string contents; index cursor; index height, width; };
与任意的重载函数一样,给指定的函数调用提供适当数目和/或类型的实参来选择运行哪个版本:
Screen myscreen; char ch = myscreen.get();// calls Screen::get() ch = myscreen.get(0,0); // calls Screen::get(index, index)
在类内部定义的成员函数,例如不接受实参的 get 成员,将自动作为 inline 处理。也就是说,当它们被调用时,编译器将试图在同一行内扩展该函数。也可以显式地将成员函数声明为 inline:
class Screen { public: typedef std::string::size_type index; // implicitly inline when defined inside the class declaration char get() const { return contents[cursor]; } // explicitly declared as inline; will be defined outside the class declaration inline char get(index ht, index wd) const; // inline not specified in class declaration, but can be defined inline later index get_cursor() const; // ... }; // inline declared in the class declaration; no need to repeat on the definition char Screen::get(index r, index c) const { index row = r * width; // compute the row location return contents[row + c]; // offset by c to fetch specified character } // not declared as inline in the class declaration, but ok to make inline in definition inline Screen::index Screen::get_cursor() const { return cursor; }
可以在类定义体内部指定一个成员为inline,作为其声明的一部分。或者,也可以在类定义外部的函数定义上指定 inline。在声明和定义处指定 inline 都是合法的。在类的外部定义 inline 的一个好处是可以使得类比较容易阅读。
像其他 inline 一样,inline 成员函数的定义必须在调用该函数的每个源文件中是可见的。不在类定义体内定义的 inline 成员函数,其定义通常应放在有类定义的同一头文件中。
一旦遇到右花括号,类的定义就结束了。并且一旦定义了类,那以我们就知道了所有的类成员,以及存储该类的对象所需的存储空间。可以声明一个类而不定义它:
class Screen; // declaration of the Screen class
这个声明,有时称为前向声明(forward declaraton),在程序中引入了类类型的 Screen。在声明之后、定义之前,类 Screen 是一个不完全类型(incompete type),即已知 Screen 是一个类型,但不知道包含哪些成员。不完全类型(incomplete type)只能以有限方式使用。不能定义该类型的对象。不完全类型只能用于定义指向该类型的指针及引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。在创建类的对象之前,必须完整地定义该类。必须定义类,而不只是声明类,这样,编译器就会给类的对象预定相应的存储空间。同样地,在使用引用或指针访问类的成员之前,必须已经定义类。
只有当类定义已经在前面出现过,数据成员才能被指定为该类类型。如果该类型是不完全类型,那么数据成员只能是指向该类类型的指针或引用。
因为只有当类定义体完成后才能定义类,因此类不能具有自身类型的数据成员。然而,只要类名一出现就可以认为该类已声明。因此,类的数据成员可以是指向自身类型的指针或引用:
class LinkScreen { Screen window; LinkScreen *next;//类大小不知道,所以不能定义类的对象;但一个指针的大小是固定的,所以可以定义不完全类的指针 LinkScreen *prev; };
类的前身声明一般用来编写相互依赖的类。
尽管在成员函数内部显式引用 this 通常是不必要的,但有一种情况下必须这样做:当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。最常见的情况是在这样的函数中使用 this:该函数返回对调用该函数的对象的引用。
某种类可能具有某些操作,这些操作应该返回引用(即调用这个成员函数的对象本身),比如Screen 类只有一对 get 操作。逻辑上,我们可以添加下面的操作。
一对 set 操作,将特定字符或光标指向的字符设置为给定值。一个 move 操作,给定两个 index 值,将光标移至新位置。
理想情况下,希望用户能够将这些操作的序列连接成一个单独的表达式:
// move cursor to given position, and set that character myScreen.move(4,0).set('#');
这个语句等价于:
myScreen.move(4,0); myScreen.set('#');
在单个表达式中调用 move 和 set 操作时,每个操作必须返回一个引用,该引用指向执行操作的那个对象:
class Screen { public: // interface member functions Screen& move(index r, index c); Screen& set(char); Screen& set(index, index, char); // other members as before };
注意,这些函数的返回类型是 Screen&,指明该成员函数返回对其自身类类型的对象的引用。每个函数都返回调用自己的那个对象。使用 this 指针来访问该对象。下面是对两个新成员的实现:
Screen& Screen::set(char c) { contents[cursor] = c; return *this; } Screen& Screen::move(index r, index c) { index row = r * width; // row location cursor = row + c; return *this; }
函数中唯一需要关注的部分是 return 语句。在这两个操作中,每个函数都返回 *this。在这些函数中,this 是一个指向非常量 Screen 的指针。如同任意的指针一样,可以通过对 this 指针解引用来访问 this 指向的对象。
在普通的非 const 成员函数中,this 的类型是一个指向类类型的 const 指针。可以改变 this 所指向的值,但不能改变 this 所保存的地址。在 const 成员函数中,this 的类型是一个指向 const 类类型对象的 const 指针。既不能改变 this 所指向的对象,也不能改变 this 所保存的地址。
不能从 const 成员函数返回指向类对象的普通引用。const 成员函数只能返回 *this 作为一个 const 引用。
例如,我们可以给 Screen 类增加一个 display 操作。这个函数应该在给定的 ostream 上打印 contents。逻辑上,这个操作应该是一个 const 成员。打印 contents 不会改变对象。如果将 display 作为 Screen 的 const 成员,则 display 内部的 this 指针将是一个 const Screen* 型的 const。
如果 display 是一个 const 成员,且它的返回类型是 const Screen&。这个设计存在一个问题。如果将 display 定义为 const 成员,就可以在非 const 对象上调用 display,但不能将对 display 的调用嵌入到一个长表达式中。下面的代码将是非法的:
Screen myScreen; // this code fails if display is a const member function // display return a const reference; we cannot call set on a const myScreen.display().set('*');
问题在于这个表达式是在由 display 返回的对象上运行 set。该对象是 const,因为 display 将其对象作为 const 返回。我们不能在 const 对象上调用 set。
而这样则是可行的:
// move cursor to given position, set that character and display the screen myScreen.move(4,0).set('#').display(cout);
为了解决这个问题,我们必须定义两个 display 操作:一个是 const,另一个不是 const。基于成员函数是否为 const,可以重载一个成员函数;同样地,基于一个指针形参是否指向 const(而非指针本身是否是const),可以重载一个函数。const 对象只能使用 const 成员(因为,在使用非const成员函数时,隐含的传递了一个参数this,它是一个指向非const对象的const指针,而当你定义一个const对象,调用一个非const成员函数时,这个成员函数的this指针指向的就是一个const对象,const对象要求不可以被改变,而this指针又是指向非const对象的指针,可以改变它所指向的对象,矛盾出现了。所以,如果const对象使用的是const成员函数,那么隐含传递的是一个指向const对象的const指针,这个指向const对象的const指针接收的就是一个const对象,会正常执行,所以,const对象不能调用非const成员函数)。非 const 对象可以使用任一成员,但非 const 版本是一个更好的匹配。
有时(但不是很经常),我们希望类的数据成员(甚至在 const 成员函数内)可以修改。这可以通过将它们声明为 mutable 来实现。可变数据成员(mutable data member)永远都不能为 const,甚至当它是 const 对象的成员时也如此。因此,const 成员函数可以改变 mutable 成员。要将数据成员声明为可变的,必须将关键字 mutable 放在成员声明之前:
class Screen { public: // interface member functions private: mutable size_t access_ctr; // may change in a const members // other data members as before };
我们给 Screen 添加了一个新的可变数据成员 access_ctr。使用 access_ctr 来跟踪调用 Screen 成员函数的频繁程度:
void Screen::do_display(std::ostream& os) const { ++access_ctr; // keep count of calls to any member function os << contents; }
尽管 do_display 是 const,它也可以增加 access_ctr。该成员是可变成员,所以,任意成员函数,包括 const 函数,都可以改变 access_ctr 的值。因此,mutable主要是用来在const函数中使用的、可以改变的数据成员。