目录
第七章:类
类的基本思想是数据抽象( data abstraction) 和封装(encapsulation)。 数据抽象是一种依赖于接口(interface) 和实现( implementation)分离的编程(以及设计)技术。
类的接口包括用户所能执行的操作:类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。
封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节
类要想实现数据抽象和封装,需要首先定义一个抽象数据类型( abstract data type)。在抽象数据类型中,由类的设计者负责考虑类的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么,而无须了解类型的工作细节。
7.1定义抽象数据类型
struct Sales_data {
//新成员:关于Sales_data对象的操作
std::string isbn() const { return bookNo; }
Sales_data& combine (const Sales_data&) ;
double avg_price() const;
//数据成员和2.6.1节(第64页)相比没有改变
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// Sales_data的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&) ;
std::ostream &print (std::ostream&, const Sales_data&) ;
std::istream &read(std:: istream&, Sales_data&) ;
定义成员函数:实现可以在类内 ,也可以在类外
引入this
成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。
total.isbn ()
则编译器负责把total的地址传递给isbn的隐式形参this
//伪代码,用于说明调用成员函数的实际执行过程
Sales_data::isbn (&total)
this 形参是隐式定义的。实际上,任何自定义名为this的参数或变量的行为都是非法的。我们可以在成员函数体内部使用this, 因此尽管没有必要,但我们还是能把isbn定义成如下的形式:
std::string isbn() const { return this->bookNo; }
引入const成员函数
isbn函数的另一个关键之处是紧随参数列表之后的const 关键字,这里,const的作用是修改隐式this指针的类型。
默认情况下,this的类型是指向类类型非常量版本的常量指针。
this是隐式的并且不会出现在参数列表中,所以在哪儿将this声明成指向常量的指针就成为我们必须面对的问题.
C++语言的做法是允许把const关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的const表示this是一个指向常量的指针。 像这样使用const的成员函数被称作常量成员函数( const member function)。
伪代码,说明隐式的this指针是如何使用的
//下面的代码是非法的:因为我们不能显式地定义自己的this指针
//谨记此处的this是一个指向常量的指针,因为isbn是一个常量成员
std::string Sales_data::isbn (const Sales_data *const this)
{ return this->isbn; }
因为this是指向常量的指针,所以常量成员函数不能改变调用它的对象的内容
类作用域和成员函数
编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体(如果有的话)。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。
在类的外部定义成员函数
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; //把rhs的成员加到this对象的成员上
revenue += rhs.revenue ;
return *this; //返回调用该函数的对象
}
当我们的交易处理程序调用如下的函数时:
total.combine (trans) ;//更新变量total当前的值
total的地址被绑定到隐式的this参数上,而rhs绑定到了tr
rans. 上。因此,当combine执行下面的语句时:
units_ sold += rhs.units_ sold;//把rhs的成员添加到this对象的成员中
效果等同于求total.units_sold 和trans.unit_sold 的和,然后把结果保存到
total.units_sold 中。
return *this; // 返回调用该函数的对象
7.1.3定义类相关的非成员函数
定义read和print函数
//输入的交易信息包括ISBN、售出总数和售出价格
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 ;
}
第一点, read 和print分别接受一个各自IO类型的引用作为其参数,这是因为IO类属于不能被拷贝的类型,因此我们
只能通过引用来传递它们。而且,因为读取和写入的操作会改变流的内容,所以两个函数接受的都是普通引用,而非对常量的引用。
第二点,print函数不负责换行。一般来说,执行输出任务的函数应该尽量减少对格式的控制,这样可以确保由用户代码来决定是否换行。
定义add函数
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs ; //把lhs的数据成员拷贝给sum
sum.combine (rhs) ; //把rhs的数据成员加到sum当中
return sum;
}
//函数返回sum的副本。
7.1.4构造函数
每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数( constructor)。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
构造函数的名字和类名相同。构造函数没有返回类型;除此之外类似于其他的函数,构造函数也有一个(可能为空的)参数列表和一个(可能为空的)函数体。类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数数量或参数类型上有所区别。
不同于其他成员函数,构造函数不能被声明成const的。当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。
合成的默认构造函数
类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数(default constructor)。默认构造函数无须任何实参。
编译器创建的构造函数又被称为合成的默认构造函数( synthesized default constructor )。对于大多数类来说,这个合成的默认构造函数将按照如下规则初始化类的数据成员:
●如果存在类内的初始值,用它来初始化成员。
●否则,默认初始化该成员。
某些类不能依赖于合成的默认构造函数
对于一个普通的类来说,必须定义它自己的默认构造函数,原因有三:第一个原因也是最容易理解的一个原因就是编译器只有在发现类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数。一旦我们定义了一些其他的构造函数,那么除非我们再定义一个默认的构造函数,否则类将没有默认构造函数。这条规则的依据是,如果一个类在某种情况下需要控制对象初始化,那么该类很可能在所有情况下都需要控制。
第二个原因是对于某些类来说,合成的默认构造函数可能执行错误的操作。回忆我们之前介绍过的,如果定义在块中的内置类型或复合类型(比如数组和指针)的对象被默认初始化,则它们的值将是未定义的。该准则同样适用于默认初始化的内置类型成员。因此,含有内置类型或复合类型成员的类应该在类的内部初始化这些成员,或者定义一个自己的默认构造函数。否则,用户在创建类的对象时就可能得到未定义的值。
第三个原因是有的时候编译器不能为某些类合成默认的构造函数。例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。对于这样的类来说,我们必须自定义默认构造函数,否则该类将没有可用的默认构造函数。
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;
};
= default的含义 c++11
首先请明确一点: 因为该构造函数不接受任何实参,所以它是一个默认构造函数。我们定义这个构造函数的目的仅仅是因为我们既需要其他形式的构造函数,也需要默认的构造函数。我们希望这个函数的作用完全等同于之前使用的合成默认构造函数。
在C++11 新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上= default来要求编译器生成构造函数。其中,= default 既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样, 如果=default 在类的内部,则默认构造函数是内联的;如果它在类的外部,则该成员默认情况下不是内联的。
构造函数初始值列表( constructor initialize list)
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 (const std: :string &s) :
bookNo(s),units_ sold(0) ,revenue(0) { }
以与合成默认构造函数相同的方式隐式初始化
在类的外部定义构造函数
Sales_data::Sales_data(std::istream &is)
{
read (is, *this); //read函数的作用是从is中读取一条交易信息然后
//存入this对象中
}
没有出现在构造函数初始值列表中的成员将通过相应的类内初始值(如果存在的话)
初始化,或者执行默认初始化。对于Sales_data来说,这意味着一旦函数开始执行,
则bookNo将被初始化成空string对象,而units_sold 和revenue将是0。
//习题:7.15
class Person
{
private:
string strName; //姓名
string strAddress; // 地址
public:
Person() = default;
Person(const string &name, const string &add)
{
strName = name;
strAddress = add;
}
Person(std::istream &is) { is >> *this; }
public:
string getName() const { return strName; } //返回姓名
string getAddress() const { return strAddress; } // 返回地址
};
7.1.5 拷贝、赋值和析构
除了定义类的对象如何初始化之外,类还需要控制拷贝、赋值和销毁对象时发生的行
为。对象在几种情况下会被拷贝,如我们初始化变量以及以值的方式传递或返回一个对象等。当我们使用了赋值运算符时会发生对象的赋值操作。当对象不再存在时执行销毁的操作,比如一个局部对象会在创建它的块结束时被销毁,当vector 对象(或者数组)销毁时存储在其中的对象也会被销毁。
如果我们不主动定义这些操作,则编译器将替我们合成它们。一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁操作。
某些类不能依赖于合成的版本
管理动态内存的类通常不能依赖于上述操作的合成版本。
7.2访问控制与封装
在C++语言中, 我们使用访问说明符( access specifiers) 加强类的封装性:
●定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。
●定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了(即隐藏了)类的实现细节。
使用class或struct关键字
当我们希望定义的类的所有成员是public 的时,使用struct;
如果希望成员是private的,使用class。
7.2.1友元
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend)。 如果类想把一个 函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可:
友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员也不受它所在区域访问控制级别的约束。
友元的声明
为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中(类的外部)。
练习7.20:友元在什么时候有用?请分别列举出使用友元的利弊。
[出题思路]
友元为类的非成员接口函数提供了访问其私有成员的能力,这种能力的提升利弊共存。
[解答]
当非成员函数确实需要访问类的私有成员时,我们可以把它声明成该类的友元。此时,友元可以“工作在类的内部”,像类的成员一样访问类的所有数据和函数。但是一旦使用不慎(比如随意设定友元),就有可能破坏类的封装性。
7.3类的其他特性
类型成员、类的成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回*this、关于如何定义并使用类类型及友元类的更多知识。
7.3.1类成员再探
定义一个类型成员
用来定义类型的成员必须先定义后使用
class Screen {
public:
typedef std::string::size_type pos;
private :
pos cursor = 0;
pos height = 0,width = 0;
std::string contents ;
};
class Screen{
public:
//使用类型别名等价地声明一个类型名字
using pos = std::string::size_type;//其他成员与之前的版本一致
};
Screen类的成员函数
class Screen {
public:
typedef std::string::size_type pos;
Screen() = default; //因为Screen有另一个构造函数,
//所以本函数是必需的
// cursor被其类内初始值初始化为0
Screen(pos ht, pos wa, char c) : height (ht),width (wa) ,
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;
};
因为我们已经提供了一个构造函数,所以编译器将不会自动生成默认的构造函数。如果我们的类需要默认构造函数,必须显式地把它声明出来。在此例中,我们使用=default告诉编译器为我们合成默认的构造函数。
需要指出的是,第二个构造函数(接受三个参数)为cursor成员隐式地使用了类内初始值。如果类中不存在cursor的类内初始值,我们就需要像其他成员一样显式地初始化cursor了。
令成员作为内联函数
inline //可以在函数的定义处指定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 //在类的内部声明成inline
{
pos row = r * width; //计算行的位置
return contents[row + c] ;//返回给定列的字符
}
在类中,常有一些规模较小的函数适合于被声明成内联函数。如我们之前所见的,定义在类内部的成员函数是自动inline的。因此,Screen 的构造函数和返回光标所指字符的get函数默认是inline函数。
我们可以在类的内部把inline作为声明的一部分显式地声明成员函数,同样的,也能在类的外部用inline关键字修饰函数的定义
重载成员函数
可变数据成员
一个可变数据成员(mutable data member)永远不会是const,即使它是const对象的成员。因此,一个const 成员函数可以改变一个可变成员的值。举个例子,我们将给Screen添加一个名为access_ctr 的可变成员,通过它我们可以追踪每个Screen的成员函数被调用了多少次:mutable
class Screen {
public:
void some_member() const ;
private :
mutable size_t access_ctr; //即使在一个const对象内也能被修改
//其他成员与之前的版本一致
};
void Screen::some_ member() const
{
++access_ctr ;
//保存一个计数值,用于记录成员函数被调用的次数
//该成员需要完成的其他工作
}
类数据成员的初始值
class Window_mgr {
private:
//这个window_mgr追踪的Screen
//默认情况下,一个window_mgr包含一个标准尺寸的空白Screen
std::vector<Screen> screens { Screen(24,80,' ')};
};
在C++11新标准中,最好的方式就是把这个默认值声明成一个类内初始值
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; //将this对象作为左值返回
}
inline Screen &Screen::set(pos r, pos col, char ch)
{
contents[r*width + col] = ch; //设置给定位置的新值
return *this;//将this对象作为左值返回
}
//把光标移动到一个指定的位置,然后设置该位置的字符值
myScreen.move(4,0).set(' #') ;
假如当初我们定义的返回类型不是引用,则move的返回值将是*this的副本因此调用set只能改变临时副本,而不能改变myScreen的值。
//如果move返回Screen而非Screen&
Screen temp = myScreen. move(4,0) ;//对返回值进行拷贝
temp.set('#') ;//不会改变myScreen的contents
从const成员函数返回*this
我们继续添加一个名为diplay的操作,它负责打印Screen的内容。我们希望这个函数能和move以及set出现在同一序列中,因此类似于move和set,diplay函数也应该返回执行它的对象的引用。
从逻辑上来说,显示一个Screen并不需要改变它的内容,因此我们令diplay为一个const成员,此时,this 将是一个指向const的指针而*this是const对象。由此推断,display的返回类型应该是const Sales_ data&然而,如果真的令diplay返回一个const的引用,则我们将不能把display嵌入到一组动作的序列中去:
Screen myScreen;//如果display返回常量引用,则调用set将引发错误
myScreen.display(cout).set('*');
即使myScreen是个非常量对象,对set的调用也无法通过编译。问题在于display
的const版本返回的是常量引用,而我们显然无权set一个常量对象。
基于const的重载
因为非常量版本的函数对于常量对象是不可用的,所以我们只能在一个常量对象上调用const成员函数。另一方面,虽然可以在非常量对象上调用常量版本或非常量版本,但显然此时非常量版本是一个更好的匹配。
class Screen {
public:
//根据对象是否是const重载了display函数
Screen &display (std::ostream &os)
{ do_display(os); return *this; }
const Screen &display (std::ostream &os) const
{ do_display(os); return *this; }
private:
//该函数负责显示Screen的内容
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) ;//调用常量版本
#include <iostream>
#include <string>
using namespace std;
class Screen
{
private:
unsigned height = 0,width = 0;
unsigned cursor = 0;
string contents;
public:
Screen() = default; //默认构造函数
Screen(unsigned ht, unsigned wd) : height(ht) , width(wd),
contents(ht * wd, ' ') { }
Screen(unsigned ht, unsigned wd, char c)
: height(ht),width(wd),contents(ht * wd, c) { }
public:
Screen& move(unsigned r, unsigned c)
{
cursor = r*width + c;
return *this;
}
Screen& set(char ch)
{
contents[cursor] = ch;
return *this;
}
Screen& set(unsigned r ,unsigned c ,char ch)
{
contents[ r * width + c] = ch;
return *this;
}
Screen& display()
{
cout << contents;
return *this;
}
};
int main()
{
Screen myScreen(5 , 5,'X');
myScreen.move(4, 0).set('#').display( );
cout << "\n";
myScreen.display( );
cout << "\n";
}
7.3.3类类型

Sales_ data iteml ;//默认初始化Sales_ data类型的对象 c++
class Sales_ data item1 ;//一条等价的声明 c
类的声明
class Screen; // Screen 类的声明
这种声明有时被称作前向声明( forward declaration), 它向程序中引入了名字Screen并且指明Screen是一种类类型。对于类型Screen来说,在它声明之后定义之前是一个不完全类型( incomplete type),也就是说,此时我们已知Screen是一个类类型,但是不清楚它到底包含哪些成员。
不完全类型只能在非常有限的情景下使用:可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
7.3.4友元再探
类之间的友元关系
class Screen{
// Window_ mgr的成员可以访问 Screen类的私有部分
friend class Window_mgr;
// Screen类的剩余部分
};
class Window_mgr {
public:
//窗口中每个屏幕的编号
using ScreenIndex = std::vector<Screen>::size_type;
//按照编号将指定的Screen重置为空白
void clear (ScreenIndex) ;
private:
std::vector<Screen> screens{Screen(24, 80, ' ')};
};
void Window_mgr::clear (ScreenIndex i)
// s是一个Screen的引用,指向我们想清空的那个屏幕
Screen &s = screens [i] ;
//将那个选定的Screen重置为空白
s. contents = string(s.height * s.width,' ') ;
令成员函数作为友元
class Screen {
// Window_mgr::clear必须在Screen类之前被声明
friend void Window_mgr::clear(ScreenIndex) ;
// Screen 类的剩余部分
};
要想令某个成员函数作为友元,我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系。在这个例子中,我们必须按照如下方式设计程序:
●首先定义window_mgr类,其中声明clear函数,但是不能定义它。在clear使用Screen的成员之前必须先声明Screen。
●接下来定义Screen, 包括对于clear的友元声明。
●最后定义clear,此时它才可以使用Screen的成员。
函数重载和友元
尽管重载函数的名字相同,但它们仍然是不同的函数。因此,如果一个类想把一 组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明
//重载的storeOn函数
extern std::ostream& store0n (std::ostream &,Screen &) ;
extern BitMap& storeOn (BitMap &,Screen &) ;
class Screen {
// store0n的ostream版本能访问Screen对象的私有部分
friend std::ostream& store0n (std::ostream &,Screen &) ;
// ...
};
友元声明和作用域
甚至就算在类的内部定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见。换句话说,即使我们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的:
struct X {
friend void f() { /*友元函数可以定义在类的内部*/ }
X() { f(); }//错误: f还没有被声明
void g() ;
void h() ;
};
void X::g() { return f() ; }//错误: f还没有被声明
void f() ; //声明那个定义在X中的函数
void X::h() { return f(); } //正确:现在f的声明在作用城中了
关于这段代码最重要的是理解友元声明的作用是影响访问权限,它本身并非普通意义上的声明。
#include <iostream>
#include <string>
using namespace std;
class Window_mgr
{
public:
void clear();
};
class Screen
{
friend void Window_mgr::clear();
private:
unsigned height = 0,width = 0;
unsigned cursor = 0;
string contents;
public:
Screen() = default; //默认构造函数
Screen(unsigned ht, unsigned wd , char c)
: height(ht), width(wd), contents(ht * wd, c) { }
};
void Window_mgr::clear()
{
Screen myScreen(10, 20, 'X');
cout << "清理之前myScreen的内容是: " << endl;
cout << myScreen.contents << endl;
myScreen.contents = " ";
cout << "清理之后myScreen的内容是:"<< endl;
cout << myScreen.contents << endl;
}
int main()
{
Window_mgr w;
w.clear();
return 0;
}
7.4类的作用域
每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问。对于类类型成员则使用作用域运算符访问。
Screen::pos ht = 24,wd = 80;//使用Screen定义的pos类型
Screen scr (ht,wd,' ') ;
Screen *p = &scr;
char c = scr.get() ;//访问scr对象的get成员
c = p->get() ;//访问p所指对象的get成员
作用域和定义在类外部的成员
函数的返回类型通常出现在函数名之前。因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。这时,返回类型必须指明它是哪个类的成员。
class Window_mgr {
public:
//向窗口添加一个Screen,返回它的编号
ScreenIndex addScreen (const Screen&) ;
//其他成员与之前的版本一致
public:
//窗口中每个屏幕的编号
using ScreenIndex = std::vector<Screen>::size_type;
//按照编号将指定的Screen重置为空白
void clear(ScreenIndex) ;
private :
std::vector<Screen> screens{Screen(24, 80, ' ')};
};
//首先处理返回类型,之后我们才进入window_mgr的作用域
Window_mgr::ScreenIndex
Window_mgr::addScreen (const Screen &s)
{
screens.push_back(s) ;
return screens.size() - 1;
}
7.4.1名字查找与类的作用域
名字查找(name lookup) (寻找与所用名字最匹配的声明的过程)的过程比较直截了当:
●首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明。
●如果没找到,继续查找外层作用域。
●如果最终没有找到匹配的声明,则程序报错。
对于定义在类内部的成员函数来说,解析其中名字的方式与上述的查找规则有所区别,不过在当前的这个例子中体现得不太明显。类的定义分两步处理:
●首先,编译成员的声明。
●直到类全部可见后才编译函数体。
用于类成员声明的名字查找
这种两阶段的处理方式只适用于成员函数中使用的名字。声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找。编译器会接着到外层作用域中查找.
typedef double Money;
string bal;
class Account {
public:
Money balance() { return bal; }
private:
Money bal;
//....
};
当编译器看到balance函数的声明语句时,它将在Account类的范围内寻找对Money的声明。编译器只考虑Account中在使用Money前出现的声明,因为没找到匹配的成员,所以编译器会接着到Account的外层作用域中查找。
类型名要特殊处理
一般来说,内层作用域可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过。然而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字:
typedef double Money;
class Account
{
public:
Money balance() { return bal; } //使用外层作用域的Money
private:
typedef double Money ; //错误:不能重新定义Money
Money bal ;
// ...
};
需要特别注意的是,即使Account中定义的Money类型与外层作用域一致,上述代码仍然是错误的。
尽管重新定义类型名字是一种错误的行为,但是编译器并不为此负责。一些编译器仍将顺利通过这样的代码,而忽略代码有错的事实。
成员定义中的普通块作用域的名字查找
成员函数中使用的名字按照如下方式解析:
●首先,在成员函数内查找该名字的声明。和前面一 样,只有在函数使用之前出现的声明才被考虑。
●如果在成员函数内没有找到,则在类内继续查找,这时类的所有成员都可以被考虑。
●如果类内也没找到该名字的声明,在成员函数定义之前的作用域内继续查找。
类作用域之后,在外围的作用域中查找
在文件中名字的出现处对其进行解析
当成员定义在类的外部时,名字查找的第三步不仅要考虑类定义之前的全局作用域中的声明,还需要考虑在成员函数定义之前的全局作用域中的声明。例如:
int height; //定义了一个名字,稍后将在Screen中使用
class Screen {
public:
typedef std::string::size_type pos;
void setHeight (pos) ;
pos height = 0;//隐藏了外层作用域中的height
};
Screen::pos verify (Screen::pos) ;
void Screen::setHeight (pos var){
// var:参数
// height: 类的成员
// verify: 全局函数
height = verify (var) ;
}
请注意,全局函数verify的声明在Screen类的定义之前是不可见的。然而,名字查找的第三步包括了成员函数出现之前的全局作用域。在此例中,verify的声明位于setHeight的定义之前,因此可以被正常使用。
7.5构造函数再探
7.5.1构造函数初始值列表
习惯于立即对其进行初始化,而非先定义、再赋值
// Sales_data构造函数的一种写法, 虽然合法但比较草率: 没有使用构造函数初始值
Sales_data::Sales_data (const string &s,
unsigned cnt,double price)
{
bookNo = s;
units_sold = cnt;
revenue = cnt * price;
}
构造函数的初始值有时必不可少
class ConstRef {
public:
ConstRef (int ii) ;
private :
int i;
const int ci;
int &ri;
};
//错误:ci和ri必须被初始化
ConstRef::ConstRef(int ii)
{ //赋值:
i=ii;//正确
ci = ii;//错误:不能给const赋值
ri=i;//错误:ri没被初始化
}
//正确:显式地初始化引用和const成员
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) { }
成员初始化的顺序
成员的初始化顺序与它们在类定义中的出现顺序一致: 第一个成员先被初始化,然后第二个,以此类推。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
默认实参和构造函数
class Sales_ data {
public: //定义默认构造函数,令其与只接受一个string实参的构造函数功能相同
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) ; }
//其他成员与之前的版本一致
};
当没有给定实参,或者给定了一个string实参时,两个版本的类创建了相同的对象。因为我们不提供实参也能调用上述的构造函数,所以该构造函数实际上为我们的类提供了默认构造函数。
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); }
// 其他成员与之前的版本一致
};
在这个sales_data 类中,除了一个构造函数外其他的都委托了它们的工作。第一个构造函数接受三个实参,使用这些实参初始化数据成员,然后结束工作。我们定义默认构造函数令其使用三参数的构造函数完成初始化过程,它也无须执行其他任务,这一点从空的构造函数体能看得出来。接受一个string的构造函数同样委托给了三参数的版本。
接受istream&的构造函数也是委托构造函数,它委托给了默认构造函数,默认构造函数又接着委托给三参数构造函数。当这些受委托的构造函数执行完后,接着执行istream&构造函数体的内容。它的构造函数体调用read函数读取给定的istream
当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。在Sales_data 类中,受委托的构造函数体恰好是空的。假如函数体包含有代码的话,将先执行这些代码,然后控制权才会交还给委托者的函数体。
#include <iostream>
#include <string>
using namespace std;
class Sales_data {
friend std::istream &read(std::istream &is, Sales_data &item);
//friend std::ostream &pri(std::ostream &os ,Sales_data &item);
public:
Sales_data(const string &book, unsigned num, double sellp, double salep)
:bookNo(book), units_sold(num), sellingprice(sellp), saleprice(salep)
{
if (sellingprice)
discount = saleprice / sellingprice;
cout << "该构造函数接受书号、销售量、原价、实际售价四个信息" << endl;
}
//委托构造函数
Sales_data() :Sales_data(" ", 0, 0, 0)
{
cout << "该构造函数,无需接受任何信息" << endl;
}
Sales_data(const string &book) : Sales_data(book,0, 0,0)
{
cout << "该构造函数接受书号信息" << endl;
}
Sales_data(std::istream & is) : Sales_data()
{
read(is, *this);
cout << " 该构造函数接受用户输入的信息" << endl;
}
private:
std::string bookNo; //书籍编号,隐式初始化为空串
unsigned units_sold = 0; //销售量,显示初始化为0
double sellingprice = 0.0; //原始价格,显示初始化为0.0
double saleprice = 0.0; //实售价格,显示初始化为0.0
double discount = 0.0; //折扣,显示初始化为0.0
};
std::istream & read(std::istream & is, Sales_data &item)
{
is >> item.bookNo >> item.units_sold >> item.sellingprice >>
item.saleprice;
return is;
}
//std::ostream &pri(std::ostream & os, const Sales_data &item)
//{
// os << item.bookNo << item.units_sold << item.sellingprice <<
// item.saleprice;
// return os;
//}
int main()
{
Sales_data fist("978-7-121-15535-2",85,128, 109);
Sales_data second;
Sales_data third("978-7-121-15535-2");
Sales_data last(cin);
return 0;
}
7.5.3默认构造函数的作用
●当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时。
●当一个类本身含有类类型的成员且使用合成的默认构造函数时。
●当类类型的成员没有在构造函数初始值列表中显式地初始化时。值初始化在以下情况下发生:
●在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时。
●当我们不使用初始值定义一个局部静态变量时。
●当我们通过书写形如T( )的表达式显式地请求值初始化时,其中T是类型名(vector的一个构造函数只接受一个实参用于说明vector大小,它就是使用一个这种形式的实参来对它的元素初始化器进行值初始化)。
使用默认构造函数
7.5.4隐式的类类型转换
C++语言在内置类型之间定义了几种自动转换规则。同样的,我们也能为类定义隐式转换规则。如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数(converting constructor)。
string null_book = "9-999-99999-9";
//构造一个临时的Sales_data对象
//该对象的units_sold和revenue等于0,bookNo等于null_book
item.combine(null_book) ;
在这里我们用一个string实参调用了Sales_data 的combine成员。该调用是合法的,
编译器用给定的string自动创建了一个Sales_data对象。新生成的这个(临时)
Sales_data 对象被传递给combine。因为combine的参数是一个常量引用,所以我
们可以给该参数传递一个临时量。
只允许一步类类型转换
//错误:需要用户定义的两种转换:
// (1) 把“9-999-99999-9”转换成string
// (2) 再把这个(临时的) string 转换成Sales_data
item.combine ("9-999-99999-9") ;
如果我们想完成上述调用,可以显式地把字符串转换成string或者Sales_data
对象:
//正确:显式地转换成string, 隐式地转换成Sales_data
item. combine (string ("9-999-99999-9"));
//正确:隐式地转换成string, 显式地转换成Sales_data
item. combine (Sales_data("9-999-99999-9"));
类类型转换不是总有效
是否需要从string到Sales_ data的转换依赖于我们对用户使用该转换的看法。在此例中,这种转换可能是对的。null_ book中的string可能表示了一个不存在的ISBN编号。
另一个是从istream到Sales_data 的转换:
//使用istream构造函数创建一个函数传递给combine
item. combine (cin);
这段代码隐式地把cin 转换成Sales_data,这个转换执行了接受一个istream 的Sales_data 构造函数。该构造函数通过读取标准输入创建了一个(临时的)Sales_data对象,随后将得到的对象传递给combine.
Sales_data 对象是个临时量,一旦combine完成我们就不能再访问它了。实际上,我们构建了一个对象,先将它的值加到item中,随后将其丢弃。
抑制构造函数定义的隐式转换
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&);
//其他成员与之前的版本一致
};
item.combine (null_book); //错误: string构造函数是explicit的
item.combine (cin); //错误: istream构造函数是explicit的
关键字explicit 只对一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为explicit 的。只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复:
//错误: explicit关键字只允许出现在类内的构造函数声明处
explicit Sales_data::Sales_data (istream& is)
{
read(is,*this) ;
}
explicit构造函数只能用于直接初始化
发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用=)。我们只能使用直接初始化而不能使用explicit构造函数:
Sales_data iteml (null_book) ; // 正确:直接初始化
//错误:不能将explicit构造函数用于拷贝形式的初始化过程
Sales_data item2 = null_book;
为转换显式地使用构造函数
尽管编译器不会将explicit的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数显式地强制进行转换:
//正确:实参是一个显式构造的Sales_data对象
item.combine (Sales_data(null_book)) ;
//正确: static_ cast可以使用explicit的构造函数
item.combine (static_cast<Sales_data>(cin)) ;
在第一个调用中,我们直接使用Sales_data 的构造函数,
该调用通过接受string的构造函数创建了一个临时的Sales_data对象。
在第二个调用中,我们使用static_cast执行了显式的而非隐式的转换。
其中,static_cast 使用istream构造函数创建了一个临时的Sales_data 对象。
标准库中含有显式构造函数的类
●接受一个单参数的const char*的string构造函数不是explicit的。
●接受一个容量参数的vector构造函数是explicit的。
7.5.5聚合类
聚合类( aggregate class)使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。当一个类满足如下条件时,我们说它是聚合的:
●所有成员都是public的。
●没有定义任何构造函数。
●没有类内初始值。
●没有基类,也没有virtual函数。
struct Data {
int ival ;
string s;
};
我们可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员:
// val1.ival = 0; val1.s = string ("Anna");
Dataval1={0,"Anna"};
初始值的顺序必须与声明的顺序一致, 也就是说,第一个成员的初始值要放在第一个, 然
后是第二个,以此类推。下 面的例子是错误的:
//错误:不能使用"Anna"初始化ival, 也不能使用1024初始化s
Dataval2={"Anna",1024};
如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化。初始值列表的元素个数绝对不能超过类的成员数量。
显式地初始化类的对象的成员存在三个明显的缺点:
●要求类的所有成员都是public的。
●将正确初始化每个对象的每个成员的重任交给了类的用户(而非类的作者)。因为用户很容易忘掉某个初始值,或者提供一个不恰当的初始值,所以这样的初始化过程冗长乏味且容易出错。
●添加或删除-个成员之后,所有的初始化语句都需要更新。
7.5.6字面值常量类
constexpr函数的参数和返回值必须是字面值类型。
除了算术类型、引用和指针外,某些类也是字面值类型。和其他类不同,字面值类型的类可能含有constexpr函数成员。这样的成员必须符合constexpr函数的所有要求,它们是隐式const的。
数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合类,但它符合下述要求,则它也是一个字面值常量类:
●数据成员都必须是字面值类型。
●类必须至少含有一个constexpr构造函数。
●如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
●类必须使用析构函数的默认定义,该成员负责销毁类的对象。
constexpr构造函数
尽管构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr函数。事实上,一个字面值常量类必须至少提供一个constexpr构造函数。
constexpr构造函数可以声明成=default 的形式(或者是删除函数的形式)。否则,constexpr构造函数就必须既符合构造函数的要求(意味着不能包含返回语句),又符合constexpr函数的要求(意味着它能拥有的唯一可执行语 句就是返回语句)。综合这两点可知,constexpr构造函数体一般来说应该是空的。我们通过前置关键字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;//硬件错误,而非IO错误
bool io;// IO错误
bool other;//其他错误
};
constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式。
constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型:
constexpr Debug io_sub (false, true, false) ;//调试IO
if (io_sub.any())//等价于if(true)
cerr << "print appropriate error messages" << endl ;
constexpr Debug prod (false) ;//无调试
if (prod.any())//等价于if(false)
cerr << "print an error message" << endl ;
7.6 类的静态成员
声明静态成员
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() ;
};
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。因此,每个Account对象将包含两个数据成员:owner和amount。只存在一个interestRate对象而且它被所有Account对象共享。
静态成员函数也不与任何对象绑定在一起, 它们不包含this指针。作为结果,静态成员函数不能声明成const的,而且我们也不能在static函数体内使用this指针。这一限制既适用于this的显式使用,也对调用非静态成员的隐式使用有效。
使用类的静态成员
double r;
r= Account::rate() ;//使用作用城运算符访问静态成员
虽然静态成员不属于类的某个对象,但是我们仍然可以使用类的对象、引用或者指针来访
问静态成员:
Account acl;
Account *ac2 = &acl;//调用静态成员函数rate的等价形式
r = acl.rate() ;//通过Account的对象或引用
r = ac2->rate() ;//通过指向Account对象的指针
成员函数不用通过作用域运算符就能直接使用静态成员:
class Account {
public:
void calculate() { amount += amount*interestRate; }
private:
static double interestRate; //其他成员与之前的版本一致
};
定义静态成员
既可以在类的内部也可以在类的外部定义静态成员函数。当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句:
void Account::rate (double newRate )
{
interestRate = newRate ;
}
因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的。这意味着它们不是由类的构造函数初始化的。而且一般来说,我们不能在类的内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员。和其他对象一样,一个静态数据成员只能定义一次。
//定义并初始化一个静态成员
double Account::interestRate = initRate() ;
静态成员的类内初始化
通常情况下,类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式,因为这些成员本身就是常量表达式,所以它们能用在所有适合于常量表达式的地方。例如,我们可以用一个初始化了的静态数据成员指定数组成员的维度:
class Account
{
public:
static double rate() { return interestRate; }
static void rate (double) ;
private:
static constexpr int period = 30;// period 是常量表达式
double daily_tbl [period] ;
};
如果在类的内部提供了一个初始值,则成员的定义不能再指定一个初始值了:
//一个不带初始值的静态成员的定义
constexpr int Account::period; //初始值在类的定义内提供
静态成员能用于某些场景,而普通成员不能
静态成员独立于任何对象。因此,在某些非静态数据成员可能非法的场合,静态成员却可以正常地使用。举个例子,静态数据成员可以是不完全类型
class Bar {
public:
// ...
private:
static Bar meml ;//正确:静态成员可以是不完全类型
Bar * mem2 ;//正确:指针成员可以是不完全类型
Bar mem3 ;//错误:数据成员必须是完全类型
};
静态成员和普通成员的另外一个区别是:可以使用静态成员作为默认实参
class Screen {
public:
// bkground 表示一个在类中稍后定义的静态成员
Screen& clear (char = bkground) ;
private:
static const char bkground;
};