<C++ Primer 4th>读书笔记
在 C++ 中,用类来定义自己的抽象数据类型(abstract data types)。通过定义类型来对应所要解决的问题中的各种概念。最简单地说,类就是定义了一个新的类型和一个新作用域。
所有成员必须在类的内部声明,一旦类定义完成后,就没有任何方式可以增加成员了。
构造函数一般就使用一个构造函数初始化列表,来初始化对象的数据成员:
// default constructor needed to initialize members of built-in type
Sales_item(): units_sold(0), revenue(0.0) { }
在类内部,声明成员函数是必需的,而定义成员函数则是可选的。在类内部定义的函数默认为 inline。在类外部定义的成员函数必须指明它们是在类的作用域中。成员函数有一个附加的隐含实参this,将函数绑定到调用函数的对象。将关键字 const 加在形参表之后,就可以将成员函数声明为常量。 const 成员函数不能改变其所操作的对象的数据成员。const 必须同时出现在声明和定义中。
类背后蕴涵的基本思想是数据抽象和封装。
数据抽象是一种依赖于接口和实现分离的编程(和设计)技术。类设计者必须关心类是如何实现的,但使用该类的程序员不必了解这些细节。相反,使用一个类型的程序员仅需了解类型的接口,他们可以抽象地考虑该类型做什么,而不必具体地考虑该类型如何工作。
封装是一项低层次的元素组合起来的形成新的、高层次实体珠技术。函数是封装的一种形式:函数所执行的细节行为被封装在函数本身这个更大的实体中。被封装的元素隐藏了它们的实现细节——可以调用一个函数但不能访问它所执
行的语句。同样地,类也是一个封装的实体:它代表若干成员的聚焦,大多数(良好设计的)类类型隐藏了实现该类型的成员。
在 C++ 中,使用访问标号来定义类的抽象接口和实施封装。类型的数据抽象视图由其 public 成员定义。private 封装了类型的实现细节。
可以在任意的访问标号出现之前定义类成员。在类的左花括号之后、第一个访问标号之前定义成员的访问级别,其值依赖于类是如何定义的。如果类是用struct 关键字定义的,则在第一个访问标号之前的成员是公有的;如果类是用
class 关键字是定义的,则这些成员是私有的。
并非所有类型都必须是抽象的。标准库中的 pair 类就是一个实用的、设计良好的具体类而不是抽象类。具体类会暴露而非隐藏其实现细节。
编程角色的不同类别
类也类似:类的设计者为类的“用户”设计并实现类。在这种情况下,“用户”是程序员,而不是应用程序的最终
用户。好的类设计者会定义直观和易用的类接口,而使用者只关心类中影响他们使用的部分实现。设计类的接口时,设计者应该考虑的是如何方便类的使用;使用类的时候,设计者就不应该考虑类如何工作。
关键概念:数据抽象和封装的好处
数据抽象和封装提供了两个重要优点:
• 避免类内部出现无意的、可能破坏对象状态的用户级错误。
• 随时间推移可以根据需求改变或缺陷(bug)报告来完美类实现,而无须改变用户级代码。
仅在类的私有部分定义数据成员,类的设计者就可以自由地修改数据。
如果实现改变了,那么只需检查类代码来了解此变化可能造成的影响。如果数据为仅有的,则任何直接访问原有数据成员的函数都可能遭到破坏。在程序可重新使用之前,有必要定位和重写依赖原有表示的那部分代码。
同样地,如果类的内部状态是私有的,则数据成员的改变只可能在有限的地方发生。避免数据中出现用户可能引入的错误。如果有缺陷会破坏对象的状态,就在局部位置搜寻缺陷:如果数据是私有的,那么只有成员函数可能对该错误负责。对错误的搜寻是有限的,从而大大方便了程序的维护和修正。
如果数据是私有的并且没有改变成员函数的接口,则操纵类对象的用户函数无须改变。
改变头文件中的类定义可有效地改变包含该头文件的每个源文件的程序文本,所以,当类发生改变时,使用该类的代码必须重新编译。
在类内部定义的成员函数,将自动作为inline 处理。也就是说,当它们被调用时,编译器将试图在同一行内扩展该函数。也可以显式地将成员函数声明为 inline。可以在类定义体内部指定一个成员为inline,作为其声明的一部分。或者,也可以在类定义外部的函数定义上指定 inline。在声明和定义处指定 inline都是合法的。
一旦定义了类,那以我们就知道了所有的类成员,以及存储该类的对象所需的存储空间。如果在多个文件中定义一个类,那么每个文件中的定义必须是完全相同的。
可以声明一个类而不定义它:
class Screen; // declaration of the Screen class
这个声明,有时称为前向声明(forward declaraton)类 Screen 是一个不完全类型(incompete type),即已知 Screen 是一个类型,但不知道包含哪些成员。不完全类型(incomplete type)只能以有限方式使用。不能定义该类型的对象。不完全类型只能用于定义指向该类型的指针及引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。
在创建类的对象之前,必须完整地定义该类。必须定义类,而不只是声明类,这样,编译器就会给类的对象预定相应的存储空间。
因为只有当类定义体完成后才能定义类,因此类不能具有自身类型的数据成员。然而,只要类名一出现就可以认为该类已声明。因此,类的数据成员可以是指向自身类型的指针或引用:
class LinkScreen { Screen window; LinkScreen *next; LinkScreen *prev; };
函数返回对调用该函数的对象的引用时可以在该函数中使用 this。
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& Screen::move(index r, index c) { index row = r * width; // row location cursor = row + c; return *this; }
在普通的非 const 成员函数中,this 的类型是一个指向类类型的 const指针。可以改变 this 所指向的值,但不能改变 this 所保存的地址。在 const 成员函数中,this 的类型是一个指向 const 类类型对象的const 指针。既不能改变 this 所指向的对象,也不能改变 this 所保存的地址。不能从 const 成员函数返回指向类对象的普通引用。const 成员函数只能返回 *this 作为一个 const 引用。
基于成员函数是否为 const,可以重载一个成员函数;同样地,基于一个指针形参是否指向 const,可以重载一个函数。
class Screen { public: // interface member functions // display overloaded on whether the object is const or not Screen& display(std::ostream &os) { do_display(os); return *this; } const Screen& display(std::ostream &os) const { do_display(os); return *this; } private: // single function to do the work of displaying a Screen, // will be called by the display operations void do_display(std::ostream &os) const { os << contents; } // as before };
可变数据成员
有时(但不是很经常),我们希望类的数据成员(甚至在 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 }; void Screen::do_display(std::ostream& os) const { ++access_ctr; // keep count of calls to any member function os << contents; }
在类作用域之外,成员只能通过对象或指针分别使用成员访问操作符 . 或-> 来访问。
返回类型出现在成员名字前面。如果函数在类定义体之外定义,则用于返回类型的名字在类作用域之外。如果返回类型使用由类定义的类型,则必须使用完全限定名。
class Screen { public: typedef std::string::size_type index; index get_cursor() const; }; inline Screen::index Screen::get_cursor() const { return cursor; }
类定义实际上是在两个阶段中处理:
1. 首先,编译成员声明;
2. 只有在所有成员出现之后,才编译它们的定义本身。
使用了相同的名字来表示形参和成员,这是通常应该避免的。尽管类的成员被屏蔽了,但仍然可以通过用类名来限定成员名或显式使用 this 指针来使用它。
// bad practice: Names local to member functions shouldn't hide member names void dummy_fcn(index height) { cursor = width * this->height; // member height // alternative way to indicate the member cursor = width * Screen::height; // member height }
构造函数分两个阶段执行:(1)初始化阶段;(2)普通的计算阶段。计算阶段由构造函数函数体中的所有语句组成。使用构造函数初始化列表的版本初始化数据成员,没有定义初始化列表的构造函数版本在构造函数函数体中对数据成员赋值。这个区别的重要性取决于数据成员的类型。
不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。
有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数函数体中对它们赋值不起作用。没有默认构造函数的类类型的成员,以及 const 或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。
class ConstRef { public: ConstRef(int ii); private: int i; const int ci; int &ri; }; // no explicit constructor initializer: error ri is uninitialized ConstRef::ConstRef(int ii) { // assignments: i = ii; // ok ci = ii; // error: cannot assign to a const ri = i; // assigns to ri which was not bound to an object } // ok: explicitly initialize reference and const members ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }
成员初始化的次序
构造函数初始化列表仅指定用于初始化成员的值,并不指定这些初始化执行的次序。成员被初始化的次序就是定义成员的次序。
初始化的次序常常无关紧要。然而,如果一个成员是根据其他成员而初始化,则成员初始化的次序是至关重要的。
按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。
初始化类类型的成员时,要指定实参并传递给成员类型的一个构造函数。可以使用该类型的任意构造函数。
// alternative definition for Sales_item default constructor Sales_item(): isbn(10, '9'), units_sold(0), revenue(0.0) {}
这个初始化式使用 string 构造函数,接受一个计数值和一个字符,并生成一个 string,来保存重复指定次数的字符。
默认构造函数
只要定义一个对象时没有提供初始化式,就使用默认构造函数。为所有形参提供默认实参的构造函数也定义了默认构造函数。
只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。
一个类哪怕只定义了一个构造函数,编译器也不会再生成默认构造函数。这条规则的根据是,如果一个类在某种情况下需要控制对象初始化,则该类很可能在所有情况下都需要控制。
如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员。
实际上,如果定义了其他构造函数,则提供一个默认构造函数几乎总是对的。通常,在默认构造函数中给成员提供的初始值应该指出该对象是“空”的。如果一个类(比如NoDefault)没有默认构造函数,意味着:
1. 具有 NoDefault 成员的每个类的每个构造函数,必须通过传递一个初始的 string 值给 NoDefault 构造函数来显式地初始化 NoDefault 成员。
2. 编译器将不会为具有 NoDefault 类型成员的类合成默认构造函数。如果这样的类希望提供默认构造函数,就必须显式地定义,并且默认构造函数必须显式地初始化其 NoDefault 成员。
3. NoDefault 类型不能用作动态分配数组的元素类型。
4. NoDefault 类型的静态分配数组必须为每个元素提供一个显式的初始化式。
5. 如果有一个保存 NoDefault 对象的容器,例如 vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。
使用默认构造函数定义一个对象的正确方式是去掉最后的空括号:
// ok: defines a class object ... Sales_item myobj;
另一方面,下面这段代码也是正确的:
// ok: create an unnamed, empty Sales_itemand use to initialize myobj Sales_item myobj = Sales_item();
在这里,我们创建并初始化一个 Sales_item 对象,然后用它来按值初始化myobj。编译器通过运行 Sales_item 的默认构造函数来按值初始化一个Sales_item。
隐式类类型转换
为了定义到类类型的隐式转换,需要定义合适的构造函数。可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。
抑制由构造函数定义的隐式转换
可以通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数:
class Sales_item { public: // default argument for book is the empty string explicit Sales_item(const std::string &book = ""): isbn(book), units_sold(0), revenue(0.0) { } explicit Sales_item(std::istream &is); // as before };
explicit 关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它:
通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。将构造函数设置为explicit 可以避免错误,并且当转换有用时,用户可以显式地构造对象。
对于没有定义构造函数并且其全体数据成员均为 public 的类,可以采用与初始化数组元素相同的方式初始化其成员,根据数据成员的声明次序来使用初始化式:
struct Data { int ival; char *ptr; }; // val1.ival = 0; val1.ptr = 0 Data val1 = { 0, 0 }; // val2.ival = 1024; // val2.ptr = "Anna Livia Plurabelle" Data val2 = { 1024, "Anna Livia Plurabelle" };
显式初始化类类型对象的成员有三个重大的缺点。
1. 要求类的全体数据成员都是 public。
2. 将初始化每个对象的每个成员的负担放在程序员身上。这样的初始化是乏味且易于出错的,因为容易遗忘初始化式或提供不适当的初始化式。
3. 如果增加或删除一个成员,必须找到所有的初始化并正确更新。
定义和使用构造函数几乎总是较好的。当我们为自己定义的类型提供一个默认构造函数时,允许编译器自动运
行那个构造函数,以保证每个类对象在初次使用之前正确地初始化。
友元
友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。友元的声明以关键字 friend 开始。它只能出现在类定义的内部。
通常,将友元声明成组地放在类定义的开始或结尾是个好主意。
友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。将一个类设为友元,友元类的所有成员函数都可以访问授予友元关系的那个类的非公有成员。
使其他类的成员函数成为友元
class Screen { // Window_Mgrmust be defined before class Screen friend Window_Mgr& Window_Mgr::relocate(Window_Mgr::index, Window_Mgr::index, Screen&); // ...restofthe Screen class };
当我们将成员函数声明为友元时,函数名必须用该函数所属的类名字加以限定。
一般地讲,必须先定义包含成员函数的类,才能将成员函数设为友元。另一方面,不必预先声明类和非成员函数来将它们设为友元。友元声明将已命名的类或非成员函数引入到外围作用域中。此外,友元函数可以在类的内部定义,该函数的作用域扩展到包围该类定义的作用域。
用友元引入的类名和函数(定义或声明),可以像预先声明的一样使用:
class X { friend class Y; friend void f() { /* ok to define friend function in the class body */ } }; class Z { Y *ymem; // ok: declaration for class Y introduced by friend in X void g() { return ::f(); } // ok: declaration of f introduced by X };
重载函数与友元关系
类必须将重载函数集中每一个希望设为友元的函数都声明为友元
static 类成员
对于特定类类型的全体对象而言,访问一个全局对象有时是必要的。
通常,非 static 数据成员存在于类类型的每个对象中。不像普通的数据成员,static 数据成员独立于该类的任意对象而存在;每个 static 数据成员是与类关联的对象,并不与该类的对象相关联。
正如类可以定义共享的 static 数据成员一样,类也可以定义 static 成员函数。static 成员函数没有 this 形参,它可以直接访问所属类的 static 成员,但不能直接使用非 static 成员。
使用类的 static 成员
可以通过作用域操作符从类直接调用 static 成员,或者通过对象、引用或指向该类类型对象的指针间接调用(C#中是不可以的)。
当我们在类的外部定义 static 成员时,无须重复指定 static 保留字,该保留字只出现在类定义体内部的声明处。
static 数据成员必须在类定义体的外部定义(正好一次)。不像普通数据成员,static 成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。
保证对象正好定义一次的最好办法,就是将 static 数据成员的定义放在包含类非内联成员函数定义的文件中。
一般而言,类的 static 成员,像普通数据成员一样,不能在类的定义体中初始化。相反,static 数据成员通常在定义时才初始化。
这个规则的一个例外是,只要初始化式是一个常量表达式,整型 const static 数据成员就可以在类的定义体中进行初始化:
class Account { public: static double rate() { return interestRate; } static void rate(double); // sets a new rate private: static const int period = 30; // interest posted every 30 days double daily_tbl[period]; // ok: period is constant expression };
const static 数据成员在类的定义体中初始化时,该数据成员仍必须在类的定义体之外进行定义。在类内部提供初始化式时,成员的定义不必再指定初始值:
// definition of static member with no initializer; // the initial value is specified inside the class definition const int Account::period;
static 数据成员不是任何对象的组成部分,所以它们的使用方式对于非 static 数据成员而言是不合法的。
static 数据成员的类型可以是该成员所属的类类型。非 static 成员被限定声明为其自身类对象的指针或引用:
class Bar { public: // ... private: static Bar mem1; // ok Bar *mem2; // ok Bar mem3; // error };
static 数据成员可用作默认实参:
class Screen { public: // bkground refers to the static member // declared later in the class definition Screen& clear(char = bkground); private: static const char bkground = '#'; };
静态局部变量 (reference:http://www.blogjava.net/faintbear/archive/2010/01/06/308408.html)
在局部变量前加上“static”关键字,就成了静态局部变量。静态局部变量存放在内存的全局数据区。函数结束时,静态局部变量不会消失,每次该函数调用 时,也不会为其重新分配空间。它始终驻留在全局数据区,直到程序运行结束。静态局部变量的初始化与全局变量类似.如果不为其显式初始化,则C++自动为其 初始化为0。
静态局部变量与全局变量共享全局数据区,但静态局部变量只在定义它的函数中可见。静态局部变量与局部变量在存储位置上不同,使得其存在的时限也不同,导致对这两者操作 的运行结果也不同。
class Singleton { public: static Singleton& Instance() { static Singleton singleton; return singleton; } private: Singleton() { }; };
对静态局部变量的说明:
(1) 静态局部变量在静态存储区内分配存储单元。在程序整个运行期间都不释放。而自动变量(即动态局部变量)属于动态存储类别,存储在动态存储区空间(而不是静态存储区空间),函数调用结束后即释放。
(2) 为静态局部变量赋初值是在编译时进行值的,即只赋初值一次,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的 值。而为自动变量赋初值,不是在编译时进行的,而是在函数调用时进行,每调用一次函数重新给一次初值,相当于执行一次赋值语句。
(3) 如果在定义局部变量时不赋初值的话,对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符(对字符型变量)。而对自动变量来说,如果不赋初 值,则它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另分配存储单元,而所分配的单元中的值是不确定的。
(4) 虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的,也就是说,在其他函数中它是“不可见”的。