7.1 定义抽象数据类型
7.1.1 设计Sales_data类
7.1.2 定义改进的Sales_data类
成员函数(member function) 的声明必须在类的内部,定义则既可以在类的内部也可以在类的外部。作为接口组成部分的非成员函数,它们的定义和声明都在类的外部。
struct Sales_data{
string isbn ( ) const { return bookNo; }
Sales_data & combine ( const Sales_data& ) ;
double avg_price ( ) const ;
string bookNo;
unsigned unit_sold = 0 ;
double revenue = 0.0 ;
} ;
Sales_data add ( const Sales_data& , const Sales_data& ) ;
ostream & print ( ostream& , const Sales_data& ) ;
istream & read ( istream& , Sales_data& ) ;
成员函数通过一个名为 this 的隐式额外参数来访问调用它的对象。this 参数是一个常量指针,被初始化为调用该函数的对象地址,不允许改变 this 中保存的地址。
std:: string isbn ( ) const { return this - > bookNo; }
默认情况下,this 的类型是指向类类型非常量版本的常量指针(即Sales_data *const)。this 也遵循初始化规则,所以默认不能把 this 绑定到一个常量对象上,即不能在常量对象上调用普通的成员函数。C++允许在成员函数的参数列表后面添加关键字 const,表示 this 是一个指向常量的指针。使用关键字 const 的成员函数被称作常量成员函数 (const member function)。
std:: string Sales_data:: isbn ( const Sales_data * const this )
{ return this - > isbn; }
常量对象和指向常量对象的引用或指针都只能调用常量成员函数。isbn可以读取调用它的对象的数据成员,但是不能写入新值。
类本身就是一个作用域,成员函数的定义嵌套在类的作用域之内。
编译器处理类时器分两步处理类: (1)首先会是编译成员声明 (2)然后才轮到编译成员函数体(如果有的话) 因此,成员函数可以随意使用类的其他成员而无须在意这些成员的出现顺序。
在类的外部定义成员函数时,成员函数的定义必须与它的声明相匹配。也就是说,返回类型、参数列表和函数名都得与类内部的声明保持一致。如果成员函数被声明为常量成员函数,那么它的定义也必须在参数列表后面指定 const 属性。同时,类外部定义的成员名字必须包含它所属的类名。
double Sales_data:: avg_price ( ) const
{
if ( units_sold)
return revenue / units_sold;
else
return 0 ;
}
可以定义返回 this 对象的成员函数。
Sales_data& Sales_data:: combine ( const Sales_data & rhs)
{
units_sold + = rhs. units_sold;
revenue + = rhs. revenue;
return * this ;
}
7.1.3 定义类相关的非成员函数
类的作者通常会定义一些辅助函数,尽管这些函数从概念上来说属于类接口的组成部分,但实际上它们并不属于类本身。如果非成员函数是类接口的组成部分,则这些函数的声明应该与类放在同一个头文件中。
istream & read ( istream & is, Sales_data & item)
{
double price = 0 ;
is >> item. bookNo >> item. units_sold >> price;
item. revenue = price * item. units_sold;
return is;
}
ostream & print ( ostream & os, const Sales_data & item)
{
os << item. isbn ( ) << " " << item. units_sold << " "
<< item. revenue << " " << item. avg_price ( ) ;
return os;
}
Sales_data add ( const Sales_data & lhs, const Sales_data & rhs)
{
Sales_data sum = lhs;
sum. combine ( rhs) ;
return sum;
}
read和print分别接受一个各自IO类型的引用作为参数,这是因为IO类属于不能拷贝的类型,因此我们只能通过引用来传递它们,而且因为读取和写入会改变流的内容,所以两个函数接受的都是普通引用,而非常量引用。 print函数不负责换行。一般来说,执行输出任务的函数应该尽量减少对格式的控制,这样可以确保由用户代码来决定是否换行。
7.1.4 构造函数
类通过一个或几个特殊的成员函数来控制其对象的初始化操作,这些函数被称作构造函数(constructor) 。构造函数的任务是初始化类对象的数据成员,只要类的对象被创建,就会执行构造函数。 构造函数的名字和类名相同,和其他函数不一样的是,构造函数没有返回类型,且不能被声明为 const 函数。 构造函数在 const 对象的构造过程中可以向其写值。 类通过一个特殊的构造函数来控制默认初始化过料, 这个函数叫做默认构造函数(default constructor) 。默认构造函数无须任何实参。 如果类包含有内置类型或者复合类型的成员,则只有当这些成员全都被赋予了类内初始值时,这个类才适合于使用合成的默认构造函数。如数组和指针在块中的默认初始化将是未定义的。
struct Sales_data
{
Sales_data ( ) = default ;
Sales_data ( const string & s) : bookNo ( s) { }
Sales_data ( const string & s, unsigned n, double p) :
bookNo ( s) , units_sold ( n) , revenue ( p* n) { }
Sales_data ( istream & ) ;
} ;
在C++11中,如果类需要默认的函数行为,可以通过在参数列表后面添加 =default 来要求编译器生成构造函数。其中 =default 既可以和函数声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果 =default 在类的内部,则默认构造函数是内联的。
Sales_data ( ) = default ;
构造函数初始值列表(constructor initializer list) : 负责为新创建对象的一个或几个数据成员赋初始值。形式是每个成员名字后面紧跟括号括起来的(或者在花括号内的)成员初始值,不同成员的初始值通过逗号分隔。
当某个数据成员被构造函数初始值列表忽略时,它会以与合成默认构造函数相同的方式隐式初始化。 构造函数不应该轻易覆盖掉类内初始值,除非新值与原值不同。如果编译器不支持类内初始值,则所有构造函数都应该显式初始化每个内置类型的成员。 两个构造函数的函数体都是空的,这是因为这些构造函数的唯一目的就是为了数据成员的赋初值,一旦没有其他任务执行,函数体也就为空了。
使用 this 来把对象当成一个整体访问,而非直接访问对象的某个成员。
Sales_data:: Sales_data ( std:: istream & is)
{
read ( is, * this ) ;
}
7.1.5 拷贝、赋值和析构
编译器能合成拷贝、赋值和析构函数,但是对于某些类来说,合成的版本无法正常工作。特别是,当类需要分配类对象之外的资源时,合成的版本通常会失效。 使用 vector 或者 string 的类能避免分配和释放内存带来的复杂性。
7.2 访问控制与封装
class Sales_data
{
public :
Sales_data ( ) = default ;
Sales_data ( const string & s, unsigned n, double p) :
bookNo ( s) , units_sold ( n) , revenue ( p* n) { }
Sales_data ( const string & s) : bookNo ( s) { }
Sales_data ( istream & ) ;
std:: string isbn ( ) const { return bookNo; }
Sales_data & combine ( const Sales_data& ) ;
private :
double avg_price ( ) const
{ return units_sold ? revenue/ units_sold : 0 ; }
std:: string bookNo;
unsigned units_sold = 0 ;
double revenue = 0.0 ;
} ;
使用关键字 struct 定义类时,定义在第一个访问说明符之前的成员是public 的; 而使用关键字 class 时,这些成员是 private 的。 二者唯一的区别就是默认访问权限不同。
7.2.1 友元
类可以允许其他类或函数访问它的非公有成员,方法是使用关键字 friend 将其他类或函数声明为它的友元(friend)。
class Sales_data
{
friend Sales_data add ( const Sales_data& , const Sales_data& ) ;
friend std:: istream & read ( std:: istream& , Sales_data& ) ;
friend std:: ostream & print ( std:: ostream& , const Sales_data& ) ;
public :
Sales_data ( ) = default ;
Sales_data ( const std:: string & s, unsigned n, double p) :
bookNo ( s) , units_sold ( n) , revenue ( p* n) { }
Sales_data ( const std:: string & s) : bookNo ( s) { }
Sales_data ( std:: istream& ) ;
std:: string isbn ( ) const { return bookNo; }
Sales_data & combine ( const Sales_data& ) ;
private :
std:: string bookNo;
unsigned units_sold = 0 ;
double revenue = 0.0 ;
} ;
Sales_data add ( const Sales_data& , const Sales_data& ) ;
std:: istream & read ( std:: istream& , Sales_data& ) ;
std:: ostream & print ( std:: ostream& , const Sales_data& ) ;
通常情况下,最好在类定义开始或结束前的位置集中声明友元。 封装的好处:
确保用户代码不会无意间破坏封装对象的状态。 被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。
友元声明仅仅指定了访问权限,而并非一个通常意义上的函数声明。如果希望类的用户能调用某个友元函数,就必须在友元声明之外再专门对函数进行一次声明(部分编译器没有该限制)。
7.3 类的其他特性
7.3.1 类成员再探
由类定义的类型名字和其他成员一样存在访问限制,可以是 public 或 private 中的一种。
class Screen
{
public :
typedef std:: string:: size_type pos;
private :
pos cursor = 0 ;
pos height = 0 , width = 0 ;
std:: string contents;
public :
using pos = std:: string:: size_type;
} ;
与普通成员不同,用来定义类型的成员必须先定义后使用。类型成员通常位于类起始处。 如果需要显式声明内联成员函数,建议只在类外部定义的位置说明 inline。 和我们在头文件中定义 inline 函数的原因一样,inline 成员函数也该与类定义在同一个头文件中。 使用关键字 mutable 可以声明可变数据成员(mutable data member) 。可变数据成员永远不会是 const 的,即使它在 const 对象内。因此 const 成员函数可以修改可变成员的值。
class Screen
{
public :
void some_member ( ) const ;
private :
mutable size_t access_ctr;
} ;
void Screen:: some_member ( ) const
{
++ access_ctr;
}
提供类内初始值时,必须使用 = 或花括号形式。
7.3.2 返回*this的成员函数
当函数返回值是引用类型时,返回的是对象本身而不是对象的副本,可以直接对其进行操作,如果返回的不是引用,由于返回的是一个右值,则不能对其进行操作。
myScreen. move ( 4 , 0 ) . set ( '#' ) ;
myScreen. move ( 4 , 0 ) ;
myScreen. set ( '#' ) ;
Screen. temp = myScreen. move ( 4 , 0 ) ;
temp. set ( '#' ) ;
从const成员函数返回*this,返回的是常量对象无法对其进行修改操作 通过区分成员函数是否为 const 的,可以对其进行重载。因为非常量版本的函数对于常量对象是不可用的,在常量对象上只能调用 const 版本的函数;在非常量对象上,尽管两个版本都能调用,但显然会选择非常量版本,因为是一个更好的匹配。
class Screen
{
public :
Screen & display ( std:: ostream & os)
{ do_display ( os) ; return * this ; }
const Screen & display ( std:: ostream & os) const
{ do_display ( os) ; return * this ; }
private :
void do_display ( std:: ostream & os) const
{ os << contents; }
} ;
Screen myScreen ( 5 , 3 ) ;
const Screen blank ( 5 , 3 ) ;
myScreen. set ( '#' ) . display ( cout) ;
blank. display ( cout) ;
注意这里多设计了一个do_display函数,因为由很多接口成员函数都需要进行相同的操作,利用do_dispaly函数可以减少代码量,便于修改
7.3.3 类类型
每个类定义了唯一的类型。即使两个类的成员列表完全一致,它们也是不同的类型。 我们可以把类名作为类型的名字使用,从而直接指向类类型。或者,也可以把类名跟在关键字 class 或 struct 后面:
Sales_data iteml;
class Sales_data iteml;
可以仅仅声明一个类而暂时不定义它。这种声明被称作前向声明 (forward declaration),用于引入类的名字。在类声明之后定义之前都是一个不完全类型 (incomplete type)。
class Screen ;
不完全类型只能在非常有限的情景下使用:
可以定义指向不完全类型的指针或引用 也可以声明(不能定义)以不完全类型作为参数或返回类型的函数
必须首先完成类的定义,然后编译器才能知道存储该数据成员需要多少空间。因为只有当类全部完成后才算被定义,所以一个类的成员类型不能是该类本身。但是一旦类的名字出现,就可以被认为是声明过了,因此类可以包含指向它自身类型的引用或指针。
class Link_screen
{
Screen window;
Link_screen * next;
Link_screen * prev;
} ;
7.3.4 友元再探
除了普通函数,类还可以把其他类或其他类的成员函数声明为友元。此外, 友元函数能定义在类的内部, 这样的函数是隐式内联的。
class Screen
{
friend class Window_mgr ;
} ;
把其他类的成员函数声明为友元时,必须明确指定该函数所属的类名。
class Screen {
friend void Window_mgr:: clear ( ScreenIndex) ;
} ;
首先定义Window_mgr类,其中声明clear函数,但是不能定义它。在clear使用Screen的成员之前必须先声明Screen。(这里好像是先定义Screen不是声明) 接下来定义Screen,包括对于clear的友元声明 最后定义clear,此时他才可以使用Screen的成员
友元函数可以直接定义在类的内部,这种函数是隐式内联的。但是必须在类外部提供相应声明令函数可见。
struct X
{
friend void f ( ) { }
X ( ) { f ( ) ; }
void g ( ) ;
void h ( ) ;
} ;
void X:: g ( ) { return f ( ) ; }
void f ( ) ;
void X:: h ( ) { return f ( ) ; }
注意真正理解友元的声明的作用是影响访问权限,它本身并非普通意义上的声明。 如果类想把一组重载函数声明为友元,需要对这组函数中的每一个分别声明。
7.4 类的作用域
当成员函数定义在类外时,返回类型中使用的名字位于类的作用域之外,此时返回类型必须指明它是哪个类的成员。
class Window_mgr
{
public :
ScreenIndex addScreen ( const Screen& ) ;
} ;
Window_mgr:: ScreenIndex
Window_mgr:: addScreen ( const Screen & s)
{
screens. push_back ( s) ;
return screens. size ( ) - 1 ;
}
7.4.1 名字查找与类的作用域
1. 用于类成员声明的名字查找
编译器处理完类中的全部声明后才会处理成员函数的定义。 成员函数体直到整个类可见后才会被处理,因此它能使用类中定义的任何名字。 声明中使用的名字,包括返回类型或参数列表,都必须确保使用前可见。
如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找 如果类的成员使用了外层作用域的某个名字,而该名字表示一种类型,则类不能在之后重新定义该名字。
typedef double Money;
class Account
{
public :
Money balance ( ) { return bal; }
private :
typedef double Money;
Money bal;
} ;
类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后
2. 成员函数中名字的解析顺序
在成员函数内查找该名字的声明,只有在函数使用之前出现的声明才会被考虑。 如果在成员函数内没有找到,则会在类内继续查找,这时会考虑类的所有成员。 如果类内也没有找到,会在成员函数定义之前的作用域查找。 尽管类的成员被隐藏了,但我们仍然可以通过加上类的名字或显式地使用 this 指针来强制访问成员。
void Screen:: dummy_fcn ( pos height)
{
cursor = width * this - > height;
cursor = width * Screen:: height;
}
void Screen:: dummy_fcn ( pos ht)
{
cursor = width * height;
}
7.5 构造函数再探
7.5.1 构造函数初始值列表
不适用初始值列表而在函数体内使用赋值操作可能会出错 如果成员是 const、引用,或者是某种未定义默认构造函数的类类型,必须在初始值列表中将其初始化。 当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化。
class ConstRef
{
public :
ConstRef ( int ii) ;
private :
int i;
const int ci;
int & ri;
} ;
ConstRef:: ConstRef ( int ii) : i ( ii) , ci ( ii) , ri ( i) { }
成员初始化顺序与它们在类定义中的出现顺序一致,而构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。最好令构造函数初始值的顺序与成员声明的顺序保持一致。如果可能的话尽量避免使用某些成员初始化其他成员。
class X {
int i;
int j;
public :
X ( int val) : j ( val) , i ( j) { }
}
如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
7.5.2 委托构造函数
C++11扩展了构造函数初始值功能,可以定义 委托构造函数(delegating constructor)。委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。
class Sales_data
{
public :
Sales_data ( std:: string s, unsigned cnt, double price) :
bookNo ( s) , units_sold ( cnt) , revenue ( cnt* price) { }
Sales_data ( ) : Sales data ( "" , 0 , 0 ) { }
Sales_data ( std:: string s) : Sales_data ( s, 0 , 0 ) { }
Sales_data ( std:: istream & is) : Sales data ( ) { read ( is, * this ) ; }
}
7.5.3 默认构造函数的作用
在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。
7.5.4 隐式的类类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。这种构造函数被称为转换构造函数 (converting constructor)。 能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。 在Sales_data类中,接受string的构造函数和接受istream的构造函数分别定义了从这两种类型向Sales_data隐式转换的规则,也就是说,在使用Sales_data类的地方,可以使用string或者istream作为代替。
string null_book = "9-999-99999-9" ;
item. combine ( null_book) ;
注意只允许一步类类型转换
item. combine ( "9-999-99999-9" ) ;
item. combine ( string ( "9-999-99999-9" ) ) ;
item. combine ( Sales_data ( "9-999-99999-9" ) ) ;
在要求隐式转换的程序上下文中,可以通过将构造函数声明为 explicit 的加以阻止。
class Sales_data
{
public :
Sales_data ( ) = default ;
Sales_data ( const std:: string & s, unsigned n, double p) :
bookNo ( s) , units_sold ( n) , revenue ( p* n) { }
explicit Sales_data ( const std:: string & s) : bookNo ( s) { }
explicit Sales_data ( std:: istream& ) ;
} ;
explicit 关键字只对接受一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为 explicit 的。只能在类内声明构造函数时使用 explicit 关键字,在类外定义时不能重复。 执行拷贝初始化时(使用 =)会发生隐式转换,所以 explicit 构造函数只能用于直接初始化。
Sales_data item1 ( null_book) ;
Sales_data item2 = null_book;
尽管编译器不会将 explicit 的构造函数用于隐式转换过程,可以使用 explicit 构造函数显式地强制转换类型。
item. combine ( Sales_data ( null_book) ) ;
item. combine ( static_cast < Sales_data> ( cin) ) ;
7.5.5 聚合类
聚合类满足如下条件:
没有定义任何构造函数。 没有类内初始值。 没有基类。 没有虚函数。
下面是一个聚合类:可以使用一个用花括号包围的成员初始值列表初始化聚合类的数据成员。初始值顺序必须与声明顺序一致。如果初始值列表中的元素个数少于类的成员个数,则靠后的成员被值初始化。
struct Data
{
int ival;
string s;
} ;
Data val1 = { 0 , "Anna" } ;
Data va12 = { "Anna" , 1024 } ;
7.5.6 字面值常量
数据成员都是字面值类型的聚合类是字面值常量类。或者一个类不是聚合类,但符合下列条件,则也是字面值常量类:
数据成员都是字面值类型。 类至少含有一个 constexpr 构造函数。 如果数据成员含有类内初始值,则内置类型成员的初始值必须是常量表达式。如果成员属于类类型,则初始值必须使用成员自己的 constexpr 构造函数。 类必须使用析构函数的默认定义。
字面值常量类必须至少提供一个constexpr构造函数 constexpr 构造函数用于生成 constexpr 对象以及 constexpr 函数的参数或返回类型。 constexpr 构造函数必须初始化所有数据成员,初始值使用 constexpr 构造函数或常量表达式。
7.6 类的静态成员
使用关键字 static 可以声明类的静态成员,可以是 public 的或 private 的。
class Account
{
public :
void calculate ( ) { amount + = amount * interestRate; }
static double rate ( ) { return interestRate; }
static void rate ( double ) ;
private :
std:: string owner;
double amount;
static double interestRate;
static double initRate ( ) ;
} ;
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。类似的, 静态成员函数也不与任何对象绑定在一起。 由于静态成员不与任何对象绑定,因此静态成员函数不能声明为 const 的,也不能在静态成员函数内使用 this 指针。 用户代码可以使用作用域运算符访问静态成员,也可以通过类对象、引用或指针访问。类的成员函数可以直接访问静态成员。
double r;
r = Account:: rate ( ) ;
Account ac1;
Account * ac2 = & ac1;
r = ac1. rate ( ) ;
r = ac2- > rate ( ) ;
class Account
{
public :
void calculate ( ) { amount + = amount * interestRate; }
private :
static double interestRate;
} ;
在类外部定义静态成员时,不能重复 static 关键字,其只能用于类内部的声明语句。和类的所有成员一样,当我们指向类外部的静态成员时,必须指明成员所属的类名。static 关键字则只出现在类内部的声明语句中。 由于静态数据成员不属于类的任何一个对象,因此它们并不是在创建类对象时被定义的。通常情况下,不应该在类内部初始化静态成员。而必须在类外部定义并初始化每个静态成员。一个静态成员只能被定义一次。一旦它被定义,就会一直存在于程序的整个生命周期中。
double Account:: interestRate = initRate ( ) ;
建议把静态数据成员的定义与其他非内联函数的定义放在同一个源文件中,这样可以确保对象只被定义一次。
初始值必须是常量表达式,因为这些成员本身就是常量表达式, 所以它们能用在所有适合于常量表达式的地方。
class Account
{
public :
static double rate ( ) { return interestRate; }
static void rate ( double ) ;
private :
static constexpr int period = 30 ;
double daily_tbl[ period] ;
} ;
constexpr int Account:: period ;
即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员。
静态数据成员的类型可以就是它所属的类类型。而非静态数据成员则受到限制,只能声明成它所属类的指针或引用:
class Bar
{
public :
private :
static Bar mem1;
Bar * mem2;
Bar mem3;
} ;
静态成员和普通成员的另外一个区别是我们可以使用静态成员作为默认实参。
class Screen
{
public :
Screen& clear ( char = bkground) ;
private :
static const char bkground;
} ;