第7章 类
我们使用类定义自己的数据类型,通过定义新的类型来反映待解决问题中的各种概念,可以使得更容易编写和调试程序。 类的基本思想是数据抽象 和封装 。 数据抽象是一种依赖于接口 和实现 分离的编程技术。 类要想实现数据抽象的封装,需要首先定义一个抽象数据类型 。
7.1 定义抽象数据类型
Sales_item是一个抽象数据类型,Sale_data类不是一个抽象数据类型,我们需要定义一些操作供用户使用,并封装它的成员函数。
7.1.1 设计Sales_data类
Sales_data的接口应该包含以下操作:
一个isbn成员函数,用于返回对象的ISBN编号。 一个combine成员函数,用于将一个Sales_data对象加到另一个对象上。 一个名为add的函数,执行两个Sales_data的加法。 一个read函数,将数据从istream读入到Sales_data对象中。 一个print函数,将Sales_data对象的值输出到ostream。 使用改进的Sales_data类。
Sales_data total;
if ( read ( cin, total) )
{
Sales_data trans;
while ( read ( cin, trans) )
{
if ( total. isbn ( ) == trans. isbn ( ) )
{
total. combine ( trans) ;
}
else
{
print ( cout, total) << endl;
total = trans;
}
print ( cout, total) << endl;
}
else
{
err << "No data?!" << endl;
}
}
7.1.2 定义改进的Sales_data类
数据成员包括:
bookNo,string类型,表示ISBN编号。 units_sold,unsigned类型,表示某本书的销量。 revenue,double类型,表示这本书的总销售额。 成员函数包括:
combine ISBN avg_price,返回售出书籍的平均价格。 定义在类内部的函数是隐式的inline函数。
struct Sales_data
{
std:: string isbn ( ) const { return bookNo; }
Sales_data & combine ( const Sales_data & ) ;
double avg_price ( ) const ;
std:: string bookNo;
unsigned units_sold = 0 ;
double revenue = 0.0 ;
}
Sales_data add ( const Sales_data & , const Sales_data & ) ;
std:: ostream & print ( std:: ostream & , const Sales_data & ) ;
std:: istream & read ( std:: istram & , Sales_data & ) ;
所有成员都必须在类的内部声明,但是成员函数体可以定义在类内也可以定义在类外。 std::string isbn() const { return bookNo; }
定义在类内,所以内联。total
是一个Sales_data的实例,观察total.isbn()
的调用,实际上成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。
Sales_data:: isbn ( & total)
然后对类成员的直接访问都被看作this的隐式引用。
std:: string isbn ( ) const { return this - > bookNo; }
isbn函数的另外一个关键之处是紧随参数列表之后的const关键词,这里的作用是修改隐式this指针的类型。 默认情况下,this的类型是指向类类型非常量版本的常量指针,this的类型是Sales_data *const。 可以把isbn的函数体想象成如下的形式:
std:: string Sales_data:: isbn ( const Sales_data * const this ) { return this - > isbn; }
类本身就是一个作用域。 编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体。 当我们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。
double Sales_data:: avg_price ( ) const
{
if ( units_sold)
{
return revenue / units_sold;
}
else
{
return 0 ;
}
}
Sale_data & Sales_data:: combine ( const Sales_data & rhs)
{
units_sold + = rhs. units_sold;
revenue + = rhs. revenue;
return * this ;
}
7.1.3 定义类相关的非成员函数
类常常需要定义一些辅助函数,尽管这些函数定义的操作从概念上来说属于类的接口的组成部分,但是它们实际上并不属于类本身。 一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。 定义read和print函数。
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;
}
7.1.4 构造函数
每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数 。 构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。 构造函数不能被声明成const。 现在的Sales_data类并没有定义任何构造函数,类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数 ,默认构造函数无需任何实参。 编译器创建的构造函数又被称为合成的默认构造函数 ,会按照如下规则初始化类的数据成员:
如果存在类内的初始值,用它来初始化成员。 否则,默认初始化该成员。 合成的默认构造函数只适合非常简单的类,对于一个普通的类来说,必须定义它自己的默认构造函数,原因有三个:
只有在类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数,一旦自己定义了一些其它的有参构造,那么除非我们生成一个默认的构造函数,否则类将没有默认构造函数。 合成的默认构造函数可能执行错误的操作,定义在块中的内置类型或者复合类型对象被默认初始化,这它们的值将是未定义的。 编译器不能为某些类合成默认的构造函数,如一个类中包含一个其它类类型的成员且这个成员的类型没有默认构造函数,则编译将无法初始化改成员。 定义Sales_data的构造函数
一个istream&,从中读取一个交易信息。 一个const string&,表示ISBN编号;一个unsigned,表示售出的图书数量;以及一个double,表示图书的售出价格。 一个const string&,表示ISBN编号;编译器将赋予其他成员默认值。 一个空参数列表(及默认构造函数),定义了其他构造函数,那么必须定义一个默认构造函数。
struct Sales_data
{
Sales_data ( ) = default ;
Sales_data ( const std:: string & s) : bookNo ( s) { }
Sales_data ( const std:: string & s, unsigned n, double p) : bookNo ( s) , units_sold ( n) , revenue ( p * n) { }
Sales_data ( std:: istream & ) ;
std:: string isbn ( ) const { return bookNo; }
Sales_data & combine ( const Sales_data & ) ;
double avg_price ( ) const ;
std:: string bookNo;
unsigned units_sold = 0 ;
double revenue = 0.0 ;
}
C++11标准中,=defalut
来要求编译器生成构造函数,可以声明在类内(内联),也可以声明在类外(不内联)。 如果编译器不支持类内初始值,那默认构造函数就应该使用构造函数初始值列表。 以下两个定义出现的新的部分,即冒号和花括号之间的代码称为构造函数初始值列表 。
Sales_data ( const std:: string & s) : bookNo ( s) { }
Sales_data ( const std:: string & s, unsigned n, double p) : bookNo ( s) , units_sold ( n) , revenue ( p* n) { }
在类的外部定义构造函数,构造函数初始值列表为空,但是执行了构造函数体。
Sales_data:: Sales_data ( std:: istream & is)
{
read ( is, * this ) ;
}
7.1.5 拷贝、赋值和析构
类还需要控制拷贝、赋值和销毁对象时发生的行为,如果我们不主动定义这些操作,则编译器将替我们合成它们。 尽管编译器能替我们合成拷贝、赋值和销毁的操作,但是对于某些类来说合成的版本无法正常工作。
7.2 访问控制与封装
使用访问说明符 加强类的封装性:
定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,隐藏了类的实现细节。
class Sales_data
{
public :
Sales_data ( ) = default ;
Sales_data ( const std:: string & s) : bookNo ( s) { }
Sales_data ( const std:: string & s, unsigned n, double p) : bookNo ( s) , units_sold ( n) , revenue ( p * n) { }
Sales_data ( std:: 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 ;
}
使用了class代替struct,它们的默认权限不一样,class关键词默认成员是private,struct关键词默认成员是public。
7.2.1 友元
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元 。 如果类想把一个函数作为它的友元,只需要增加一条以friend关键字 开始的函数声明语句即可。 友元声明只能出现类定义的内部,但是在类内出现的具体位置不限。 友元不是类的成员也不受它所在区域访问控制级别的约束。
class Sales_data
{
friend Sales_data add ( const Sales_data & , const Sales_data & ) ;
friend std:: ostream & print ( std:: ostream & , const Sales_data & ) ;
friend std:: istream & read ( std:: istram & , Sales_data & ) ;
public :
Sales_data ( ) = default ;
Sales_data ( const std:: string & s) : bookNo ( s) { }
Sales_data ( const std:: string & s, unsigned n, double p) : bookNo ( s) , units_sold ( n) , revenue ( p * n) { }
Sales_data ( std:: 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 ;
}
Sales_data add ( const Sales_data & , const Sales_data & ) ;
std:: ostream & print ( std:: ostream & , const Sales_data & ) ;
std:: istream & read ( std:: istram & , Sales_data & ) ;
7.3 类的其他特性
包括:类型成员、类的成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回*this、关于如何定义并使用类类型及友元类的更多知识。
7.3.1 类成员再探
定义一对相互关联的类分别是Screen和Window_mgr。 Screen表示显示器中的一个窗口。
class Screen
{
public :
typedef std:: string:: size_type pos;
private :
pos cursor = 0 ;
pos height = 0 , width = 0 ;
std:: string contents;
} ;
添加一个构造函数令用户能够定义屏幕的尺寸和内容,以及其他两个成员,分别负责移动光标和读取给定位置的字符。 在类中,常有一些规模较小的函数适合于被声明成内联函数 。
class Screen
{
public :
typedef std:: string:: size_type pos;
Screen ( ) = default ;
Screen ( pos ht, pos wd, char c) : height ( ht) , width ( wd) , contents ( ht * wd, c) { }
char get ( ) const { return contents[ cursor] ; }
inline char get ( pos ht, pos wd) const ;
Screen & move ( pos r, pos c) ;
private :
pos cursor = 0 ;
pos height = 0 , width = 0 ;
std:: string contents;
} ;
inline Screen & Screen:: move ( pos r, pos c)
{
pos row = r * width;
cursor = row + c;
return * this ;
}
char Screen:: get ( pos r, pos c) const
{
pos row = r * width;
return contents[ row + c] ;
}
screen myscreen;
char ch = myscreen. get ( ) ;
ch = myscreen. get ( 0 , 0 ) ;
有时希望能修改类的某个数据成员,即使式在一个const成员函数内,可以在变量的声明中加入mutable关键词成为一个可变数据成员 。
class Screen
{
public :
void some_member ( ) const ;
private :
mutable size_t access_ctr;
} ;
void Screen:: some_member ( ) const
{
++ access_ctr;
}
在C++ll新标准中,最好的方式就是把这个默认值声明成一个类内初始值。
class Window_mgr
{
private :
std:: vector< Screen> screens{ Screen ( 24 , 80 , ' ' ) } ;
}
7.3.2 返回*this的成员函数
继续添加一些函数,它们负责设置光标所在位置的字符或者其他任一给定位置的字符。
class Screen
{
public :
Screen & set ( char ) ;
Screen & set ( pos, pos, char ) ;
} ;
inline Screen & Screen:: set ( char c)
{
contents[ cursor] = c;
return * this ;
}
inline Screen & Screen:: set ( pos r, pos col, char ch)
{
contents[ r * width + col] = ch;
return * this ;
}
myScreen. move ( 4 , 0 ) . set ( '#' ) ;
myScreen. move ( 4 , 0 ) ;
myScreen. set ( '#' ) ;
假如当初我们定义的返回类型不是引用,则move的返回值将是*this的副本,因此调用set只能改变临时副本,而不能改变myScreen的值。
Screen temp = myScreen. move ( 4 , 0 ) ;
temp. set ( '#' ) ;
当一个成员调用另外一个成员时,this指针在其中隐式地传递。 当display的非常量版本调用do_display时,它的this指针将隐式地从指向非常量的指针转换成指向常量的指针。
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 ( ste:: ostream & os) const { os << contents; }
} ;
当do_display完成后,display函数各自返回解引用this所得的对象。 在非常量版本中,this指向一个非常量对象,因此display返回一个普通的(非常量)引用,而const成员则返回一个常量引用。
Screen myScreen ( 5 , 3 ) ;
const Screen blank ( 5 , 3 ) ;
myScreen. set ( '#' ) . display ( cout) ;
blank. display ( cout) ;
7.3.3 类类型
每个类定义了唯一的类型,对于两个类来说,即使它们的成员完全一样,这两个类也是两个不同的类型。
Struct First
{
int memi;
int getMem ( ) ;
} ;
Struct Second
{
int memi;
int getMem ( ) ;
} ;
First obj1;
Second obj2= obj1;
就像可以把函数的声明和定义分离开来一样,也能仅仅声明类而暂时不定义它,如class Screen;
,这种声明被称作前向声明 。 对于类型Screen来说,在它声明之后定义之前是一个不完全类型 ,也就是说,此时我们已知Screen是一个类类型,但是不清楚它到底包含哪些成员。
7.3.4 友元再探
关于类之间的友元关系,假设需要为Window_mgr添加一个名为clear的成员,它负责把一个指定的Screen的内容都设为空白。
class Screen
{
friend class Window_mgr ;
} ;
如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
class Window_mgr
{
public :
using ScreenIndex = std:: vector< Screen> :: size_type;
void clear ( ScreenIndex) ;
private :
std:: vector< Screen> screens{ Screen ( 24 , 80 , ' ' ) } ;
} ;
void Window_mgr:: clear ( ScreenIndex i)
{
Screen & s = screens[ i] ;
s. contents = string ( s. height * s. width, ' ' ) ;
}
除了令整个Window_mgr作为友元之外,Screen还可以只为clear提供访问权限。 关于令某个成员函数作为友元:
首先定义Window_mgr类,其中声明clear函数,但是不能定义它,在clear使用Screen的成员之前必须先声明Screen。 接下来定义Screen,包括对于clear的友元声明。 最后定义clear,此时它才可以使用Screen的成员。
class Screen
{
friend void Window_mgr:: clear ( ScreenIndex) ;
} ;
尽管重载函数的名字相同,但它们仍然是不同的函数,因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明。
extern std:: ostream& storeOn ( std:: ostream & , Screen & ) ;
extern BitMap& storeOn ( BitMap & , Screen & ) ;
class Screen
{
friend std:: ostream & storeOn ( std:: ostream & , 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 类的作用域
每个类都会定义它自己的作用域。 在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问。
Screen:: pos ht = 24 , wd = 80 ;
Screen scr ( ht, wd, ' ' ) ;
Screen * p = & scr;
char c = scr. get ( ) ;
c = p- > get ( ) ;
在类的外部,成员的名字被隐藏起来了。 因为编译器在处理参数列表之前已经明确了我们当前正位于某一个类的作用域中,所以不必再专门说明这个类成员是这个类定义的。 但是函数的返回类型通常出现在函数名之前,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外,这时,返回类型必须指明它是哪个类的成员。
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 名字查找与类的作用域
编译器处理完类中的全部声明后才会处理成员函数的定义。 名字查找 的过程:
首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明。 如果没找到,继续查找外层作用域。 如果最终没有找到匹配的声明,则程序报错。 类的定义分两步处理。
首先,编译成员的声明。 直到类全部可见后才编译函数体。 这种两阶段的处理方式只适用于成员函数中使用的名字。 声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。
typedef double Money;
string bal;
class Account
{
public :
Money balance ( ) { return bal; }
private :
Money bal;
} ;
一般来说,内层作用域可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过。 然而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
typedef double Money;
class Account
{
public :
Money balance ( ) { return bal; }
private :
typedef double Money;
Money bal;
} ;
类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。 成员函数中使用的名字按照如下方式解析:
首先,在成员函数内查找该名字的声明,和前面一样,只有在函数使用之前出现的声明才被考虑。 如果在成员函数内没有找到,则在类内继续查找,这时类的所有成员都可以被考虑。 如果类内也没找到该名字的声明,在成员函数定义之前的作用域内继续查找。 成员定义中的普通块作用域的名字查找,在此例中,height参数隐藏了同名的成员。
int height;
class Screen
{
public :
typedef std:: string:: size_type pos;
void dummy_fun ( pos height)
{
cursor = width * height;
}
private :
pos cursor = 0 ;
pos height = 0 , width = 0 ;
} ;
void Screen:: dummy_fun ( pos height)
{
cursor = width * this - > height;
cursor = width * Screen:: height;
}
其实最好的确保我们使用height成员的方法是给参数起个其他名字:
void Screen:: dummy_fun ( pos ht)
{
cursor = width * height;
}
void Screen:: dummy_fun ( pos height)
{
cursor = width * :: height;
}
当成员定义在类的外部时,名字查找的第三步不仅要考虑类定义之前的全局作用域中的声明,还需要考虑在成员函数定义之前的全局作用域中的声明。
int height;
class Screen
{
public :
typedef std:: string:: size_type pos;
void setHeight ( pos) ;
pos height = 0 ;
} ;
Screen:: pos verify ( Screen:: pos) ;
void Screen:: setHeight ( pos var)
{
height = verify ( var) ;
}
7.5 构造函数再探
对于任何C++的类来说,构造函数都是其中重要的组成部分。
7.5.1 构造函数初始值列表
当定义变量时习惯于立即对其进行初始化,而非先定义、再赋值:
string foo = "Hello World!" ;
string bar;
bar = "Hello World!" ;
如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。
Sales_data:: Sales_data ( const string & s, unsigned cnt, double price)
{
bookNo = s;
units_sold = cnt;
revenue = cnt * price;
}
如果成员是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;
}
初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值。
ConstRef:: ConstRef ( int ii) : i ( ii) , ci ( ii) , ri ( i) { }
构造函数的初始值列表效率更高,因为这是初始化不是赋值。 构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。
class X
{
int i;
int j;
public :
X ( int val) : j ( val) , i ( j) { }
}
最好令构造函数初始值的顺序与成员声明的顺序保持一致。尽量避免使用某些成员初始化其他成员。
X ( int val) : i ( val) , j ( val) { }
默认实参指的是当函数调用中省略了实参时,自动使用一个值。
class Sales_data
{
public :
Sales_data ( std:: string s = " " ) : bookNo ( s) { }
Sales_data ( std:: string s, unsigned cnt, double rev) : bookNo ( s) , units_sold ( cnt) , revenue ( rev * cnt) { }
Sales_data ( std:: istream & is) { read ( is, * this ) ; }
} ;
7.5.2 委托构造函数
C++11新标准扩展了构造函数初始值的功能,使得可以定义所谓的委托构造函数 。 当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。
class Sales_data
{
public :
Sales_data ( std:: string s, unsigned cnt, double rev) : bookNo ( s) , units_sold ( cnt) , revenue ( rev * cnt) { }
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 隐式的类类型转换
C++在内置类型之间定义了几种自动转换规则。 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数 。 书上例子看吐了,换个例子理解。 值得注意的是转换构造函数只接受一个参数,如果有提供默认参数的也可以,将一般参数类型隐式转为类类型。
class a
{
public :
int x, y;
a ( int i, int j = 2 ) : x ( i) , y ( j)
{
}
} ;
int main ( )
{
a t1 ( 4 ) ;
a t2 = 3 ;
return 0 ;
}
在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为explicit 加以阻止:
class a
{
public :
int x, y;
explicit a ( int i, int j = 2 ) : x ( i) , y ( j)
{
}
} ;
int main ( )
{
a t1 ( 4 ) ;
a t2 = 3 ;
return 0 ;
}
尽管编译器不会将explicit的构造函数用于隐式转换过程,但是可以将构造函数显式地强制进行转换:
int main ( )
{
a t1 ( 4 ) ;
a t2 = a ( 3 ) ;
a t3 = static_cast < a> ( 3 ) ;
return 0 ;
}
7.5.5 聚合类
聚合类 使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。
所有成员都是public的。 没有定义任何构造函数。 没有类内初始值。 没有基类,也没有virtual函数。
struct Data
{
int ival;
string s;
} ;
可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员。
Data val1 = { 0 , "Anna" } ;
Data val1 = { "Anna" , 0 } ;
显式地初始化类的对象的成员存在三个明显的缺点:
要求类的所有成员都是public的。 将正确初始化每个对象的每个成员的重任交给了类的用户。因为用户很容易忘掉某个初始值,或者提供一个不恰当的初始值,所以这样的初始化过程冗长乏味且容易出错。 添加或删除一个成员之后,所有的初始化语句都需要更新。
7.5.6 字面值常量类
但它符合下述要求,则它是一个字面值常量类:
数据成员都必须是字面值类型。 类必须至少含有一个constexpr构造函数。 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式,或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
class Debug
{
public :
constexpr Debug ( bool b = true ) : hw ( b) , io ( b) , other ( b) { }
constexpr Debug ( bool h, bool i, bool o) : hw ( h) , io ( i) , other ( o) { }
constexpr bool any ( ) { return hw || io || other; }
void set_io ( bool b) { io = b; }
void set_hw ( bool b) { hw = b; }
void set_other ( bool b) { hw = b; }
private :
bool hw;
bool io;
bool other;
}
constexpr Debug io_sub ( false , true , false ) ;
if ( io_sub. any ( ) )
{
cerr << "print appropriate error messages" << endl;
}
constexpr Debug prod ( false ) ;
if ( prod. any ( ) )
{
cerr << "print an error message" << endl;
}
7.6 类的静态成员
有的时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。 通过成员的声明之前加上关键词static 使得其与类关联在一起。并且可以有访问保护,可以是常量、引用、指针、类类型。
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 ( ) ;
} ;
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。 使用类的静态成员。
double r;
r = Account:: rate ( ) ;
虽然静态成员不属于类的某个对象,但是我们仍然可以使用类的对象、引用或者指针来访问静态成员。
Account ac1;
Account * ac2 = & ac1;
r = ac1. rate ( ) ;
r = ac2- > rate ( ) ;
成员函数不用通过作用域运算符就能直接使用静态成员:
void Account:: rate ( double newRate)
{
interestRate = newRate;
}
和类的所有成员一样,当我们指向类外部的静态成员时,必须指明成员所属的类名,static关键字则只出现在类内部的声明语句中。 不是由类的构造函数初始化的,类似于全局变量,静态数据成员定义在任何函数之外,因此一旦被定义,就将一直存在于程序的整个生命周期中。 的静态成员不应该在类的内部初始化,但是我们可以为静态成员提供const整数类型的类内初始值,静态成员必须是字面值常量类型的constexpr的常量表达式。
class Account
{
public :
static double rate ( ) { return interestRate; }
static void rate ( double ) ;
private :
static constexpr int period = 30 ;
double daily_tbl[ period] ;
} ;
静态成员独立于任何对象,因此,在某些非静态数据成员可能非法的场合,静态成员却可以正常地使用。 静态数据成员可以是不完全类型。
class Bar
{
public :
private :
static Bar mem1;
Bar * mem2;
Bar mem3;
}
class Screen
{
public :
Screen & clear ( char = bkground) ;
private :
static const char bkground;
}