1-3 C++基础:类
7.1 定义抽象数据类型
1、类背后的基本思想:数据抽象(data abstraction)和封装(encapsulation)。数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程技术,类要想实现数据抽象和封装,需要首先定义一个抽象数据类型(abstract data type)。
2、Sales_item类和Sales_data类与抽象数据类型:Sales_item类:C++ primer第五版的书店程序,Sales_data类如下:
struct Sales_data{
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
其中在Sales_item 类是一个抽象数据类型,只能通过它的接口来使用一个sales_ item对象,而不能访问Sales_item对象的数据成员Sales_data类不是一个抽象数据类型。它允许类的用户直接访问它的数据成员,并且要求由用户来编写操作。
7.1.1 从设计Sales_data类开始学习类
最终目的希望和C++ primer第五版的书店程序 一样实现Sales_data,因此Sales_data的接口包含以下操作:
- 一个isbn成员函数,用于返回对象的ISBN编号
- 一个combine成员函数,用于将一个Sales_data对象加到另一个对象上
- 一个名为add的函数,执行两个Sales_data 对象的加法
- 一个read函数,将数据从istream读入到Sales_data对象中
- 一个print函数,将Sales_data 对象的值输出到ostream
因此现在的Sales_data的形式为:
struct Sales_data {
//成员函数:关于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的非成员接口函数
Sales_data add(const Sales_data&,const Sales_data&);
std::ostream &print (std::ostream&,const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
定义在类内部的函数是隐式的内联(inline)函数
成员函数的函数体也是一个块。
成员函数:成员函数是定义为类的一部分的函数, 有时也被称为方法(method)。我们通常以一个类对象的名义来调用成员函数形如:
类对象.成员函数()
7.1.2 this的概念
在Sales_data代码中,函数isbn是如何返回出bookNo的?
struct Sales_data {
//成员函数:关于Sales_ data对象的操作
std::string isbn() const {
return bookNo;
}
//数据成员
std::string bookNo;
};
例如我们在使用某个对象,如total调用isbn时候:total.isbn()
,此时编译器把total的地址传递给isbn的隐式形参this,可以等价地认为编译器将该调用重写成了如下的形式:
Sales_data::isbn (&total)
其中,调用Sales_data 的isbn成员时传入了total的地址。
在成员函数内部,可以直接使用调用该函数的 对象的成员,而无须通过成员访问运算符, 因为this所指的正是这个对象。任何对类成员的直接访问都被看作this的隐式引用,也就是说,当isbn使用bookNo时,它隐式地使用this指向的成员,就像我们书写了this->bookNo一样。
this 形参是隐式定义的,任何自定义名为this的参数或变量的行为都是非法的。我们可以在成员函数体内部使用this,如
std: :string isbn() const { return this->bookNo; }
this是一个常量指针不允许改变this中保存的地址。
7.1.3 成员函数参数列表后的const
1、此处const的作用:const的作用是修改隐式this指针的类型。
2、常量成员函数:C++语言的做法是允许把const关键字放在成员
函数的参数列表之后,此时,紧跟在参数列表后面的const表示this是一个指向常量,像这样使用const的成员函数被称作常量成员函数(const member function)。
// 可以把isbn的函数体想象成如下的形式的伪代码,说明隐式的this指针是如何使用的
// 下面的代码是非法的,因为不能显式地定义自己的this指针
// 谨记此处的this是一个指向常量的指针,因为isbn是一个常量成员
std::string Sales_data::isbn (const Sales_data *const this){
return this->isbn;}
因为this是指向常量的指针,所以 常量成员函数不能改变调用它的对象的内容 。即在上例中,isbn可以读取调用它的对象的数据成员,但是不能写入新值。
常量对象,以及常量对象的引用或指针都只能调用常量成员函数。
7.1.4 类的作用域和成员函数
1、成员函数的作用域:类本身就是一个作用域,成员函数的定义嵌套在类的作用域之内。
2、在类的外部定义的成员函数:在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。即返回类型、参数列表和函数名都得与类内部的声明保持一致。如果成员被声明成常量成员函数,那么它的定义也必须在参数列表后明确指定const属性。同时,类外部定义的成员的名字必须包含它所属的类名:
double Sales_data::avg_price() const {
if (units_sold)
return revenue/units_sold;
else
return 0;
}
后续还有介绍类的作用域的内容(7.4 类的作用域)
7.1.5 返回this的函数
1、combine函数的目的和实现:函数combine的设计初衷类似于复合赋值运算符+=,调用该函数的对象代表的是赋值运算符左侧的运算对象,右侧运算对象则通过显式的实参被传入函数:
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当前的值
2、返回this函数的返回值:如combine函数,该函数一个值得关注的部分是它的返回类型和返回语句。
- 一般来说,当我们定义的函数类似于某个内置运算符时,应该令该函数的行为尽量模仿这个运算符。 内置的赋值运算符把它的左侧运算对象当成左值返回,因此为了与它保持一致,combine函数必须返回引用类型。 因为此时的左侧运算对象是一个
Sales_data
的对象,所以返回类型应该是Sales_data&
。 - 如前所述,无须使用隐式的this指针访问函数调用者的某个具体成员,而是需要把调用函数的对象当成一个整体来访问:
return *this;//返回调用该函数的对象
。其中,return 语句解引用this指针以获得执行该函数的对象,换句话说,上面的这个调用返回total的引用。
7.1.5 类的非成员函数
1、非成员函数:类的作者常常需要定义一些辅助函数,比如add、read和print等。尽管这些函数定义的操作从概念上来说属于类的接口的组成部分,但它们实际上并不属于类本身,因此叫做非成员函数。
2、定义非成员函数:定义非成员函数的方式与定义其他函数一样,通常把函数的声明和定义分离开来,如果函数在概念上属于类但是不定义在类中,则一般 非成员函数应与类声明(而非定义)在同一个头文件内 。
3、以Sale_data为例的非成员函数:read函数从给定流中将数据读到给定的对象里,print函数则负责将给定对象的内容打印到给定的流中。
//输入的交易信息包括ISBN、售出总数和售出价格
struct Sales_data{
// ... ...
};
istream& read(istream&is,Sales_data& item);
ostream& print(ostream&os,const Sales_data& item);
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.unitssola<<" "<<item.revenue<<" "<<item.avg_price();
return os;
}
函数的声明和定义:函数的声明和函数的定义非常类似,唯一的区别是函数声明无须函数体,用一个分号替代即可。因为函数的声明不包含函数体,所以也就无须形参的名字。
和其他名字一样,函数的名字也必须在使用之前声明。类似于变量,函数只能定义一次,但可以声明多次。
7.1.6 构造函数
1、构造函数:每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数(constructor)。
2、构造函数的特点:构造函数与类名相同,构造函数也有一个(可能为空的)参数列表和一个(可能为空的)函数体,其具有以下几个特点:
- 构造函数没有返回类型,除此之外形式上类似于其他的函数。
- 类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数数量或参数类型上有所区别。
- 不同于其他成员函数,构造函数不能被声明成const的(原因:当创建类的一个 const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。)
3、默认构造函数:类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数(default constructor)。默认构造函数无须任何实参。
- 如果存在类内的初始值,用它来初始化成员;
- 否则,默认初始化该成员;
4、定义Sale_data的构造函数:对于Sale_data而言,其包含四个构造函数,分别实现功能有:①一个istream&,从中读取一条交易信息;②一个const string&,表示工SBN编号,一个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;
};
5、=default含义:对于上述Sale_data的构造函数代码:
struct Sales_data {
Sales_data() = default;
// ... ...
};
因为该构造函数不接受任何实参,所以它是一个默认构造函数。定义这个构造函数的目的仅仅是因为我们既需要其他形式的构造函数,也需要默认的构造函数。我们 希望这个函数的作用完全等同于之前使用的合成默认构造函数。其中,=default
既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样, 如果=default
在类的内部,则默认构造函数是内联的 ,如果它在类的外部,则该成员默认情况下不是内联的。
上面的默认构造函数之所以对 Sales_data有效,是因为我们为内置类型的数据成员提供了初始值。如果你的编译器不支持类内初始值,那么你的默认构造函数就应该使用构造函数初始值列表来初始化类的每个成员。
6、构造函数初始值列表:在Sales_data类中,有两个构造函数形如:
struct Sales_data {
//新增的构造函数
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){}
}
这两个定义中出现了新的部分,即冒号:
以及冒号和花括号{}
之间的代码,其中花括号{}
定义了(空的)函数体。我们把新出现的部分称为构造函数初始值列表(constructor initialize list),它负责为新创建的对象的一个或几个数据成员赋初值。构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来的(或者在花括号内的)成员初始值。不同成员的初始化通过逗号分隔开来。
7、在类外定义的构造函数:形如如下格式的构造函数:
struct Sales_data {
//新增的构造函数
Sales_data (std::istream &);
// ... ...
};
Sales_data::Sales_data(std::istream &is){
read(is, *this); // read函数的作用是从is中读取一条交易信息然后
//存入this对象中
}
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;
}
没有出现在构造函数初始值列表中的成员将通过相应的类内初始值(如果存在的话)初始化,或者执行默认初始化。
为了更好地理解调用函数read的意义,要特别注意read的第二个参数是一个Sales_data对象的引用。其中使用this 来把对象当成一个整体访问,而非直接访问对象的某个成员。因此在此例中,我们使用*this 将“this”对象作为实参传递给read函数。
7.1.7 拷贝、赋值和析构
- 对象在几种情况下会被拷贝,如我们初始化变量以及以值的方式传递或返回一个对象等;
- 当我们使用了赋值运算符时会发生对象的赋值操作;
- 当对象不再存在时执行销毁的操作,比如一个局部对象会在创建它的块结束时被销毁,当vector对象(或者数组)销毁时存储在其中的对象也会被销毁。
如果我们不主动定义这些操作,则编译器将替我们合成它们。一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁操作。例如书店程序中,当编译器执行如下赋值语句时,
total = trans; //处理下一本书的信息
//它的行为与下面的代码相同
// sales_data的默认赋值操作等价于:
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans. revenue;
7.2 访问、控制和封装
1、类的说明符:在C++语言中,使用访问说明符(access specifiers)加强类的封装性:
- 定义在
public
说明符之后的成员在整个程序内可被访问,public成员定义类的接口。 - 定义在
private
说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了(即隐藏了)类的实现细节。
封装的好处:
1、确保用户代码不会无意间破坏封装对象的状态。
2、被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。
3、能防止由于用户的原因造成数据被破坏。
2、新Sales_data代码:根据访问权限(暂时没有考虑非成员函数read和print的权限)设计的新Sales_data类(全代码)
#include<iostream>
using namespace std;
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&);
// 成员函数
string isbn() const { return bookNo;};
Sales_data &combine (const Sales_data&);
private:
double avg_price() const{return units_sold ? revenue/units_sold : 0; };
//成员变量
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
// 友元函数
friend istream &read(istream&,Sales_data&);
friend ostream &print(ostream&,const Sales_data&);
};
//输入的交易信息包括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;
}
3、class和struct关键字:可以使用这两个关键字中的任何一个定义类。唯一的一点区别是,struct和 class 的默认访问权限不一样(唯一区别)。
- 如果使用struct关键字,则定义在第一个访问说明符之前的成员是public的;
- 如果我们使用class关键字,则这些成员是private 的;
- 出于统一编程风格的考虑,当希望定义的类的所有成员是 public 的时,使用struct,反之,如果希望成员是private的,使用class。
7.2.1 友元函数
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend)。如果类想把一个函数作为它的友元,只需要增加一条以friend
关键字开始的函数声明语句即可(形如上述的Sales_data类):
class Sales_data{
// ... ...
// 友元函数
friend istream &read(istream&,Sales_data&);
friend ostream &print(ostream&,const Sales_data&);
};
友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限,即在任意说明符下都是可行的,因为友元不是类的成员也不受它所在区域访问控制级别的约束。
一般来说,最好在类定义开始或结束前的位置集中声明友元。
下面还有介绍友元函数的内容(7.3.4 友元再探)
7.3 类的其他特性
7.3.1 从设计Screen和Window_mgr类学习类的其他特性
1、Screen类的
1、成员函数作为内联函数:在类中,常有一些规模较小的函数适合于被声明成内联函数。如我们之前所见的,定义在类内部的成员函数是自动隐式inline的。我们可以在类的内部把inline作为声明的一部分显式地声明成员函数,同样的,也能在类的外部用inline关键字修饰函数的定义:
inline // 声明内联关键字
返回值 函数名(形参){
函数体
}
2、重载成员函数:和非成员函数一样,成员函数也可以被重载,只要函数之间在参数的数量或类型上有所区别就行。成员函数的函数匹配过程(同样与非成员函数非常类似。
3、可变数据成员:有时(但并不频繁)会发生这样一种情况,我们希望能修改类的某个数据成员,即使是在一个const 成员函数内。可以通过在变量的声明中加入mutable
关键字做到这一点。一个可变数据成员(mutable data member)永远不会是const,即使它是const对象的成员(即本身是const的对象在关键字mutable作用下都是可变的!)。因此,一个 const成员函数可以改变一个可变成员的值。
7.3.2 返回*this的成员函数
7.3.3 类类型
1、每个类定义了唯一的类型:对于两个类来说,即使它们的成员完全一样,这两个也是两个不同的类型。即使两个类的成员列表完全一致,它们也是不同的类型。对于一个类来说,它的成员和其他任何类(或者任何其他作用域)的成员都不是一回事儿。例如:
struct First{
int memi;
int getMem();
};
struct Second {
int memi;
int getMem();
};
First obj1;
Second obj2 = obj1;//错误:obj1和obj2的类型不同
2、类的声明:就像可以把函数的声明和定义分离开来一样,我们也能仅仅声明类而暂时不定义它:
class Screen;//Screen类的声明
这种声明有时被称作前向声明(forward declaration),它向程序中引入了名字Screen并且指明Screen是一种类类型。对于类型Screen来说,在它声明之后定义之前是一个不完全类型( incomplete type),也就是说,此时我们已知Screen是一个类类型,但是不清楚它到底包含哪些成员。
- 不完全类型只能在非常有限的情景下使用:可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
- 对于一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明。否则,编译器就无法了解这样的对象需要多少存储空间。类似的,类也必须首先被定义,然后才能用引用或者指针访问其成员。毕竟,如果类尚未定义,编译器也就不清楚该类到底有哪些成员。
必须首先完成类的定义,然后编译器才能知道存储该数据成员需要多少空间。因为只有当类全部完成后类才算被定义,所以一个类的成员类型不能是该类自己。然而,一旦一个类的名字出现后,它就被认为是声明过了(但尚未定义),因此类允许包含指向它自身类型的引用或指针(如树的定义中):
struct Tree{
int data;
Tree* left;
Tree* right;
};
7.3.4 友元再探
1、类之间的友元关系:在7.2.1的Sales_data类把三个普通的非成员函数定义成了友元。①类还可以把其他的类定义成友元,②也可以把其他类(之前已定义过的)的成员函数定义成友元。此外,友元函数能定义在类的内部,这样的函数是隐式内联的。
2、其他类定义成友元:如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。通过上面的声明,Window_mgr被指定为Screen 的友元,因此我们可以将Window_mgr的clear成员写成如下的形式:
必须要注意的一点是 友元关系不存在传递性。也就是说,如果 window_mgr有它自己的友元,则这些友元并不能理所当然地具有访问screen的特权。即每个类负责控制自己的友元类或友元函数。
3、成员函数作为友元:当把一个成员函数声明成友元时,必须明确指出该成员函数属于哪个类:
要想令某个成员函数作为友元,我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系。在这个例子中,我们必须按照如下方式设计程序:
- 首先定义Window_mgr类,其中声明clear函数,但是不能定义它。在clear使用Screen的成员之前必须先声明Screen。
- 接下来定义Screen,包括对于clear的友元声明。
- 最后定义clear,此时它才可以使用Screen的成员。
4、函数重载和友元:尽管重载函数的名字相同,但它们仍然是不同的函数。因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个都进行friend
的关键字声明。
5、友元声明和作用域:类和非成员函数的声明不是必须在它们的友元声明之前。 当一个名字第一次出现在一个友元声明中时,就隐式地假定该名字在当前作用域中是可见的。然而,友元本身不一定真的声明在当前作用域中。甚至就算在类的内部定义该函数,也必须在类的外部提供相应的声明从而使得函数可见。 换句话说,即使仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的:
7.4 类的作用域
7.5 构造函数再探
7.5.1 构造函数初始值列表
当我们定义变量时习惯于立即对其进行初始化,而非先定义、再赋值:
string foo = "Hello world ! "; //定义并初始化
就对象的数据成员而言,初始化和赋值也有类似的区别。如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。例如:
// Sales_data构造函数的一种写法,虽然合法但比较草率:没有使用构造函数初始值
Sales_data::Sales_data(const string &s,unsigned cnt,double price)
bookNo = s;
units_sold = cnt;
revenue = cnt * price;
}
1、构造函数初始值有时候必不可少:有时我们可以忽略数据成员初始化和赋值之间的差异,但并非总能这样。
- ①如果成员是const或者是引用的话,必须将其初始化。
- 类似的,②当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化。
2、成员初始化顺序:构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。成员的初始化顺序与它们在类定义中的出现顺序一致:第一个成员先被初始化,然后第二个,以此类推。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
一般来说,初始化的顺序没什么特别要求。不过如果一个成员是用另一个成员来初始化的,那么这两个成员的初始化顺序就很关键了。
区分本段中,构造函数初始化顺序和成员初始化顺序。
7.5.2 委托构造函数
1、委托构造函数:C++11新标准扩展了构造函数初始值的功能,使得可以定义所谓的委托构造函数( delegating constructor)。一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。和其他构造函数一样,一个委托构造函数也有一个成员初始值的列表和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。和其他成员初始值一样,类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中另外一个构造函数匹配。
7.5.3 默认构造函数的作用
1、默认构造函数作用:当对象被默认初始化或值初始化时自动执行默认构造函数。
2、默认初始化情况:默认初始化在以下情况下发生:
- 当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时。
- 当一个类本身含有类类型的成员且使用合成的默认构造函数时。
- 当类类型的成员没有在构造函数初始值列表中显式地初始化时。
3、值初始化情况:值初始化在以下情况下发生:
- 在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时。
- 当我们不使用初始值定义一个局部静态变量时。
- 当我们通过书写形如T( )的表达式显式地请求值初始化时,其中 T是类型名(如vector的一个构造函数只接受一个实参用于说明vector大小,它就是使用一个这种形式的实参来对它的元素初始化器进行值初始化)。
4、缺少默认构造函数的情况:类必须包含一个默认构造函数以便在上述情况下使用,其中的大多数情况非常容易判断。不那么明显的一种情况是类的某些数据成员缺少默认构造函数:
class NoDefault{
public:
NoDefault (const string&);
//还有其他成员,但是没有其他构造函数了
};
struct A {
//默认情况下my_mem是 public的(参见7.2节,第240页)
NoDefault my_mem;
};
A a; //错误:不能为A合成构造函数
struct B {
B(){} //错误:b _member没有初始值
NoDefault b_member;
};
7.5.4 隐式的类类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。这种构造函数又叫转换构造函数(converting constructor)。
编译器只会自动地执行仅一步类型转换。
抑制构造函数定义的隐式转换:
- 将构造函数声明为explicit加以阻止。
- explicit构造函数只能用于直接初始化,不能用于拷贝形式的初始化。
7.5.5 聚合类
满足以下所有条件:
- 所有成员都是public的。
- 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,也没有virtual函数。
可以使用一个花括号括起来的成员初始值列表,初始值的顺序必须和声明的顺序一致。
7.5.6 字面值常量类
constexp
r函数的参数和返回值必须是字面值。
字面值类型:除了算术类型、引用和指针外,某些类也是字面值类型。
数据成员都是字面值类型的聚合类是字面值常量类。
如果不是聚合类,则必须满足下面所有条件:
- 数据成员都必须是字面值类型。
- 类必须至少含有一个constexpr构造函数。
- 如果一个数据成员含有类内部初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
7.6 类的静态成员
非static数据成员存在于类类型的每个对象中。
static数据成员独立于该类的任意对象而存在。
每个static数据成员是与类关联的对象,并不与该类的对象相关联。
声明:声明之前加上关键词static。
使用:使用作用域运算符::直接访问静态成员:r = Account::rate();也可以使用对象访问:r = ac.rate();
定义:在类外部定义时不用加static。
初始化:通常不在类的内部初始化,而是在定义时进行初始化,如 double Account::interestRate = initRate();如果一定要在类内部定义,则要求必须是字面值常量类型的constexpr。
成员函数:可以基于是否含有const进行重载
非成员函数:
构造函数:
内联函数: