类
C++中,我们使用类定义自己的数据结构,本章主要关注数据抽象的重要性。
1、定义抽象数据类型
通过2.6.1节定义的Salas_data类进行具体描述
#include<iostream>
#include<string>
#include<vector>
using namespace std;
struct Salas_data{
string bookNo; //定义书号
unsigned int book_num =0; //书本售出数量
double unit_sold = 0.0; //书本售出单价
double book_rev = book_num * unit_sold; //售出书本总收入
};
(1)经过前期对该类的使用,Salas_data类应包括以下接口(执行加法和IO的函数不作为Salas_data的成员,执行复合赋值运算的函数时成员函数,原因与重载运算符有关):
- 一个isbn成员函数,返回对象的ISBN编号
- 一个combine成员函数,将一个Salas_data对象加到另一个对象
- 一个add普通函数,执行两个Salas_data对象的加法
- 一个read普通函数,将数据从istream读入Salas_data对象中
- 一个print普通函数,将Salas_data对象值输出到ostream
(2)对函数的定义
1. 定义成员函数(combine和isbn)
成员函数的声明必须在类的内部,定义既可以在类内也可以在类外。
因为使用要求,在类中增加一个avg_pric成员函数计算平均售价,但因为并不通用,所以不作为接口使用。
- isbn成员函数:string isbn() const {return bookNo;}
Salas_data total.isbn()使用点运算符对其进行调用,在调用成员函数过程中,其实是替某个对象调用,如上述语句隐式地指向调用该函数的对象(total)的成员,当isbn成员函数返回bookNo时,其实隐式地返回的是total.bookNo。
ⅰ、引入this:其中固定名为this作为isbn的一个隐式形参,来访问调用它的对象,即用请求该函数的对象地址初始化this(*this = &total)。因此①在成员函数内部,可以直接调用该函数的对象的成员,不需成员访问运算符(isbn中隐式书写this->bookNo);②this形参是隐式定义,任何名为this的参数或变量都是非法的;③this是一个常量指针,总是指向“这个”对象,不允许改变this中的地址。
ⅱ、引入 const 成员函数:string isbn() const {return bookNo;}中的const关键字使用于修改隐式this指针的类型。默认情况,this是指向类类型非常量版本的常量指针,对于常量对象this无法绑定;添加const关键字后,this是一个指向常量的常量指针,此时可以在一个常量对象上调用普通的成员函数,增加了函数的灵活性。此时的成员函数称为常量成员函数。
ⅲ、类和成员函数:编译器分两步处理类:首先编译成员的声明,然后才是成员函数体,所以成员函数题可以随意使用类中的其他成员而无须在意这些成员出现的次序;在类的外部定义成员函数时,定义必须与声明匹配,且类外部定义的成员名必须包含它所属的类名。
- combine成员函数:Sala_data & combine(const Sala_data&);
类外定义,使用total.combine(trans);调用该函数,该函数设计初衷为+=复合赋值运算符,调用该函数的对象代表赋值运算符左侧对象,右侧运算对象通过显式实参传入。
Salas_data & Salas_data::combine(const Salas_data & rhs){
book_num += rhs.book_num;
book_rev += rhs.book_rev;
return *this; //返回调用该函数的对象
}
定义的函数类似于内置运算符时,应尽量模仿该运算符,因为内置的赋值运算符把它的左侧运算对象当成左值返回,而返回类型中只有引用是左值,所以combine函数必须返回引用类型。
本成员函数返回*this,因为我们无须使用隐式的this指针访问函数调用者的某个具体成员,而是需要把调用函数的对象当做整体访问,所以return语句解引用this指针以获得执行该函数的指针。
2. 定义非成员函数
作为接口的非成员函数,声明和定义都在类外部。
上述非成员函数的操作虽然从概念上是类的接口,但它们并不属于类;这些函数定义与普通函数相同,将声明与定义分离开来,且声明一般与类声明在同一个头文件中。
struct Salas_data; //只有在此处声明了类,下一句中read函数的实参类型才成立
istream &read(istream&, Sala_data&); //在开头处声明该函数,类内部定义的istream构造函数才可以调用read函数
ostream &print(ostream &, const Sala_data &);
Salas_data add(const Sala_data&, const Sala_data&);
//IO流不能拷贝,只能引用
istream &read(istream &is, Salas_data &item){
is >> item.bookNo>> item.book_num >> item.unit_sold;
item.book_rev = item.book_num*item.unit_sold;
return is;
}
ostream &print(ostream &os, const Salas_data &item){
os << "书号为:" << item.bookNo<< "的书册,售出" << item.book_num <<
"册,平均售出单价为:" << item.avg_price() << ",总收入为:" << item.book_rev << endl;
return os;
}
Salas_data add(const Salas_data& s1, const Salas_data& s2){
Salas_data s = s1;
s.combine(s2);
return s;
}
其中IO类不能拷贝,只能通过引用;print函数不负责换行。
3. 定义构造函数
类通过一个或几个特殊的成员函数控制其对象的初始化过程,这些函数称为构造函数;任务:初始化类对象的数据成员,只要类对象被创建就会执行构造函数。构造函数名与类名相同,没有返回类型,包括一个可能为空的参数列表和一个可能为空的函数体。且类可包含多个构造函数,不同构造函数必须在参数数量或类型上有所区别。构造函数不可声明为const,且构造函数在const对象的构造过程可向其写值。
- 合成的默认构造函数
类通过一个特殊的构造函数对对象执行默认初始化的过程,这个函数叫默认构造函数,而当类中没有显式地定义构造函数,编译器就会隐式的定义一个默认构造函数,称为合成的默认构造函数:如果存在类内初始值,用它初始化成员,否则执行默认初始化。
合成的默认构造函数只适用非常简单的类,有以下几种情况并不支持:
①、当类中定义了其他构造函数时,编译器将不会生成默认构造函数;
②、因为内置类型或复合类型被默认初始化后,它们的值时未定义的将引发错误,所以只有当这些成员全被赋予类内初始值时,才适合使用合成的默认构造函数;
③、当类中包含其他类类型的成员且该成员没有默认构造函数时,此时编译器不能合成默认构造函数。
Salas_data() = default;
Salas_data(const string& s) :bookNo(s){}
Salas_data(const string&s, unsigned int n, double u) :bookNo(s), book_num(n), unit_sold(u), book_rev(n*u){}
Salas_data(istream& is); //该构造函数需要一些实际的操作,在类外定义
其中可以再参数列表后写= default要求编译器生成构造函数,= default即可以出现在类内(内联)也可以出现在类外(不是内联)。
- 构造函数初始值列表
Salas_data(const string&s, unsigned int n, double u) :bookNo(s), book_num(n), unit_sold(u), book_rev(n*u){},构造函数中冒号以及冒号和花括号之间的部分称为构造函数初始值列表,每个名字后面紧跟括号括起来的成员初始值,不同成员的初始值通过逗号分隔。
当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。
istream为参数的构造函数需要执行实际任务,在类外进行定义,和成员函数一样,必须指明该构造函数是哪个类的成员。
Salas_data::Salas_data(istream& is){
read(is, *this);
}
(3)类中同样需要控制拷贝、赋值和销毁对象。
- 拷贝:初始化变量和以值的方式传递或返回一个对象等
- 赋值:使用了赋值运算符
- 销毁:对象不再存在时,
当我们不主动定义这些操作,编译器会替我们合成,编译器生成的版本对对象的每个成员执行拷贝、赋值和销毁操作。
但对于某些类来说,并不能依赖于合成版本:比如当类需要分配类对象之外的资源时、管理动态内存的类。
但很多需要动态内存的类能使用vector对象或者string对象管理必要的存储空间,而且使用vector或者string的类能避免分配和释放内存带来的复杂性(其合成版本可以正常工作)。
析构函数:用来完成对象被删除前的一些清理工作。
2、访问控制和封装
C++中使用访问说明符加强类的封装性。
(1)访问说明符(一个类可以包含0或多个访问说明符,每个访问说明符指定接下来成员的访问等级,有效范围知道出现下一个访问说明符或直至类的结尾处):
- public说明符:定义在其后的成员在整个程序内可被访问,public定义类的接口(Salas_data类中构造函数和部分成员函数【isbn和combine】);
- private说明符:其后的成员可以被类的成员函数访问,但不能被使用该类的代码访问,private部分封装了类的实现细节(Salas_data类中的数据成员和作为实现部分的函数)。
(2)定义类的class关键字和struct关键字(唯一区别:默认访问权限不太一样):
- class关键字:定义在第一个访问说明符前的成员是private的;
- struct关键字:定义在第一个访问说明符前的成员是public的。
(3)当类将其成员定义为private后,非类成员将无法访问,因此引入友元的概念。
- 类允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend关键字开头的函数声明语句),最好在类定义的开始或结束前的位置集中声明友元。 同时友元的声明仅仅指定了访问的权限,如需调用某个友元函数,就必须在友元声明之外再专门对函数进行一次声明。
#include <iostream>
#include <string>
using namespace std;
struct Sala_data; //只有在此处声明了类,下一句中read函数的实参类型才成立
//类外再次声明
istream &read(istream&, Sala_data&); //在开头处声明该函数,类内部定义的istream构造函数才可以调用read函数
ostream &print(ostream &, const Sala_data &);
Sala_data add(const Sala_data&, const Sala_data&);
struct Sala_data
{
//友元声明
friend istream &read(istream&, Sala_data&);
friend ostream &print(ostream &, const Sala_data &);
friend Sala_data add(const Sala_data& , const Sala_data& );
public:
//各种构造函数
Sala_data() = default;
Sala_data(const string& s) :isbn(s){}
Sala_data(const string&s, unsigned int n, double u) :isbn(s), book_num(n), unit_sold(u), book_rev(n*u){ cout << "自己构造函数" << endl; }
//Sala_data() :Sala_data(" ", 0, 0){ cout << "委托默认构造函数" << endl; } //输出"自己构造函数" "委托默认构造函数"
//Sala_data(const string& s) :Sala_data(s, 0, 0){ cout << "委托isbn构造函数" << endl; } //输出"自己构造函数""委托isbn构造函数"
//Sala_data(istream & is) :Sala_data(){ read(is, *this); cout << "读取委托构造函数,受委托默认构造函数" << endl; }//输出"自己构造函数" "委托默认构造函数""读取委托构造函数,受委托默认构造函数"
Sala_data(istream& is); //该构造函数需要一些实际的操作,在类外定义
//Sala_data(istream& is){ read(is, *this); }
//类成员函数
string Isbn() const{ return isbn; } // 隐式形参this,声明和定义都在类内部
Sala_data & combine(const Sala_data&); //在类内部声明
private:
double avg_price() const{
return unit_sold ? book_rev / book_num : 0;
}
string isbn; //定义书号
unsigned int book_num =0; //书本售出数量
double unit_sold = 0.0; //书本售出单价
double book_rev = book_num * unit_sold; //售出书本总收入
};
- 使用友元的利弊:
优点:①函数可以引用类中成员而不用显式的用类名作为前缀;②可以访问所有非公开成员;③类用户更易阅读。
缺点:①降低了封装,减少了可维护性;②代码冗长,类内外都需要声明。 - 类将其他类定义为友元:
需要注意的是:友元关系不存在传递性,如果Window_mgr有他自己的哟元,则这些友元并不能理所当然的具有访问Sceen的特权。
#include<iostream>
#include<string>
#include<vector>
using namespace std;
//满足声明Screen类,定义Window_mgr包括clear成员函数的声明,然后定义Screen类包括对clear的友元声明后才能对clear进行定义
class Screen;
class Window_mgr{
public:
using Screenindex = vector<Screen>::size_type;
//声明clear函数
void clear(Screenindex);
private:
vector<Screen> screens{ Screen(24, 80, ' ') }
};
class Screen{
friend class Window_mgr;
public:
typedef string::size_type pos;
Screen() = default;//默认的构造函数1
//构造函数,cursor类内初始化值初始化为0
Screen(pos h, pos w) :heigth(h), width(w), contents(h * w, ' '){} //2
Screen(pos h, pos w, char c) :heigth(h), width(w),contents(h * w, c){} //3
char get() const{ return contents[cursor]; } //返回光标处的字符
char get(pos h, pos w) const{ return contents[h*width + w]; } //返回计算后光标处的字符
Screen &move(pos r, pos w); //移动光标
Screen &set(char c);//设置光标所在处的字符
Screen &set(pos r, pos w, char c);//设置给定光标处的字符
Screen &display(ostream & os){
do_display(os);
return *this;
}
const Screen &display(ostream & os) const{
do_display(os);
return *this;
}
pos size() const;
private:
pos cursor = 0; //光标所在位置
pos heigth = 0, width = 0; //定义屏幕的长宽
string contents; //存放光标所在位置的字符
void do_display(ostream& os) const{
os << contents;
}
};
//定义clear
void Window_mgr::clear(Screenindex i){
if (i >= screens.size()) return;
Screen &s = screens[i]; //s是Screen的一个引用,指向需要修改的那个屏幕
s.contents = string(s.heigth * s.width, ' '); //选定的重置为空白
}
- 类将其它类的成员函数作为友元(必须指明该成员函数属于哪个类):
必须仔细组织程序结构以满足声明和定义的彼此依赖关系- 首先定义Window_mgr类,其中声明其成员函数,但不能定义。在成员函数使用Screen的成员之前声明Screen;
- 接下来定义Screen,包括对Window_mgr类成员函数的友元声明;
- 最后定义成员函数。
#include<iostream>
#include<string>
#include<vector>
using namespace std;
//满足声明Screen类,定义Window_mgr包括clear成员函数的声明,然后定义Screen类包括对clear的友元声明后才能对clear进行定义
class Screen;
class Window_mgr{
public:
using Screenindex = vector<Screen>::size_type;
//声明clear函数
void clear(Screenindex);
private:
vector<Screen> screens;
};
class Screen{
friend void Window_mgr::clear(Screenindex);
//friend class Window_mgr;
public:
typedef string::size_type pos;
Screen() = default;//默认的构造函数1
//构造函数,cursor类内初始化值初始化为0
Screen(pos h, pos w) :heigth(h), width(w), contents(h * w, ' '){} //2
Screen(pos h, pos w, char c) :heigth(h), width(w),contents(h * w, c){} //3
char get() const{ return contents[cursor]; } //返回光标处的字符
char get(pos h, pos w) const{ return contents[h*width + w]; } //返回计算后光标处的字符
Screen &move(pos r, pos w); //移动光标
Screen &set(char c);//设置光标所在处的字符
Screen &set(pos r, pos w, char c);//设置给定光标处的字符
Screen &display(ostream & os){
do_display(os);
return *this;
}
const Screen &display(ostream & os) const{
do_display(os);
return *this;
}
pos size() const;
private:
pos cursor = 0; //光标所在位置
pos heigth = 0, width = 0; //定义屏幕的长宽
string contents; //存放光标所在位置的字符
void do_display(ostream& os) const{
os << contents;
}
};
//定义clear
void Window_mgr::clear(Screenindex i){
if (i >= screens.size()) return;
Screen &s = screens[i]; //s是Screen的一个引用,指向需要修改的那个屏幕
s.contents = string(s.heigth * s.width, ' '); //选定的重置为空白
}
- 如果类想把一组重载函数声明为它的友元,需要对这组函数中的每一个分别声明。
- 类和非成员函数的声明不是必须在友元声明之前,当一个名字第一次出现在一个友元声明中时,我们隐式的假定该名字在当前作用域可见。友元本身不一定真的声明在当前作用域。
就算在类的内部定义该函数,也必须在类的外部提供相应的声明从而时函数可见(有的编译器并不强制执行,但最好如此以避免换到强制执行的编译器出错)。
struct X{
friend void f(){}//友元定义在类内部
X(){f();}//出错,f还没有声明
void g();
void h();
};
void X::g(){return f();} //错误,f还没有被声明
void f(); //声明f
void X::h(){return f();} //正确,现在f的声明在作用域中了
3、类的其他特性
类型成员、类的成员的类内初始值、可变数据成员、内联成员函从成员函数返回this,如何定义并使用类类型。
(1)类型成员:用来定义类型的成员必须先定义后使用,,因此类型成员通常出现在类开始的地方。常使用typedef或类型别名等价的声明一个类型别名。
//声明类型成员
typedef string::size_type pos;
using pos = std::string::size_type;
(2)一些规模较小的函数适合被声明为内联函数,可以在类内部把inline作为声明的一部分显式的声明成员函数,也可以在类的外部用inline关键字修饰函数的定义(类的内外同时说明inline是合法的,但最好只在类外部定义的地方说明,这样便于理解)。
(3)重载成员函数:成员函数的重载与匹配过程与非成员函数非常类似。
//类内定义
char get() const{ return contents[cursor]; } //返回光标处的字符
char get(pos h, pos w) const{ return contents[h*width + w]; } //返回计算后光标处的字符
//调用
Screen myscreen;
char ch = myscreen.get();
ch = myscreen.get(0,0);
(4)可变数据成员:永远不会是const,即使作为一个const对象的成员也可以改变,在变量的声明前加入mutable关键字使其成为可变数据成员。
//类内
mutable size_t access_art;
//类外
void Screen::some_member() const{
++access_art;
}
(5)类数据成员的初始值:初始化类类型的成员时,需要为构造函数传递一个符合成员类型的实参。提供一个类内初始值时,必须以符号=或花括号表示。
std::vector<Screen> screens{Screen(24,80,' ')};
(6)返回*this的成员函数:返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本。若返回类型不是引用,则返回值将是*this的副本。
//返回引用类型,返回对象本身
inline
Screen &Screen::move(pos r, pos w){
pos row = r * width;
cursor = r + w;
return *this;
}
//返回类型非引用,返回*this副本
inline Screen Screen::set(char c){
contents[cursor] = c;
return *this;
}
(7)从const成员函数返回this:**一个const成员函数如果以引用的形式返回this,那么它的返回类型是常量引用**。
const Screen &display(ostream & os) const{
do_display(os);
return *this;
}
(8)通过区分成员函数是否为const,可以对其进行重载(非常量对象-》常量形参/非常量形参,常量对象-》常量形参)。
(9)类类型:每个类定义了唯一的类型,即使成员完全一样,两个类也是不同的类型。可以把类名作为类型的名字使用,从而直接指向类类型,或者将类名跟在关键字class或struct后面使用。
(10)类的声明:可以仅声明类而暂时不定义,称为前向声明。
class Screen;
此时,对于类型Screen,在声明后定义前是一个不完全类型(仅知道是一个类类型,但不清楚包含哪些成员)。
不完全类型仅可以在有限情景下使用(即类仅声明未定义):可以定义指向这种类型的的指针或引用;可以声明(但不能定义)以不完全类型作为参数或者返回类型的函数;
类必须被定义后,才能用引用或指针访问其成员;
直到类被定义后数据成员才能被声明成这种类型,只有类全部完成后类才算被定义,所以一个类的成员不能是类自己(此时类未完成定义,编译器不知道存储该数据成员需要火多少空间),,但因为类名字出现即表示声明过了,所以类允许包含指向自身类型的引用和指针。
10的总结:
类允许包含指向自身类型的引用和指针(不完全类型应用场景);
一个类的成员不能是类自己(因为类必须被定义后,才能用引用或指针访问其成员)。
4、类的作用域
(1)一个类就是一个作用域,在类的作用域外,普通的数据和函数成员只能由对象、指针或引用使用成员访问运算符(->)来访问;对于类类型使用作用域运算符访问(::);在类外部定义成员函数时必须同时提供类名和函数名,一旦遇到类名,则可以使用类的其他成员而无需再次授权。
(2)返回类型中使用的名字始终在类作用域外,因此返回类型必须指明他是哪个函数。
(3)常见的名字查找规则:
- 首先,在名字所在的块中,查找名字使用之前的声明;
- 若没找到则继续查找外层作用域;
- 若过最终没有匹配的声明,则报错。
(4)类分两步进行定义:
- 首先编译成员的声明;
- 直到类全部可见后才编译函数体。
因此成员函数体可以使用类中定义的所有名字,但声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。
(5)一般 情况,内层作用与可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过,但如果成员使用了外层作用域中的名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
(6)成员定义中的普通块作用域的名字查找(函数体内使用的名字):
- 首先在成员函数内查找该名字的声明(只考虑在函数使用之前出现的声明);
- 若成员函数体内没找到,则类内继续查找,类的所有成员都可考虑;
- 若类内也没有找到,则在成员函数定义前的作用域内继续查找(成员定义在类外部时,查找不仅考虑类定义前全局作用域中的声明,还包括成员函数定义前的全局作用域中的声明)。
5、构造函数再探
最好养成使用构造函数初始值的习惯,可避免很多编译错误。
(1)如果没有在构造函数初始值列表中显示的初始化成员,则该成员将在构造函数体之前执行默认初始化。
(2)构造函数的初始值在以下几种情况必不可少(不可赋值):
- 成员为const;
- 成员为引用;
- 成员属于某种类类型且该类类型没有定义默认构造函数。
(3)成员初始化顺序与类中定义顺序一致,且构造函数初始值列表的初始值顺序并不影响实际初始化顺序;因此若前面定义的成员使用后定义成员来初始化,则会出错(最好避免使用同一对象的成员作为初始值)。
(4)如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
(5)委托构造函数:使用它所属类的其他构造函数初始化它自己的初始化过程。
- 一个委托构造函数拥有一个成员初始值的列表(唯一入口:类名,且必须与受委托构造函数的参数列表匹配)和一个函数体。
- 受委托的构造函数依次执行初始值列表、函数体,只有全部执行完毕控制权才会归还委托者的函数体。
(6)类必须包含一个默认构造函数,以供对象被默认初始化或值初始化时自动执行(如果类中定义了其他构造函数,则最好也提供一个默认构造函数)。
- 默认初始化的情况:
①在块作用域内不使用任何初始值定义一个非静态变量或者数组时;
②当类本身含有类类型成员且使用合成的默认构造函数时;
③当类类型的成员没有在构造函数初始值列表中显式地初始化时。 - 值初始化的情况:
①在数组的过程中提供的初始值数量少于数组的大小时;
②不使用初始值定义一个局部静态变量时;
③当我们书写形如T()的表达式显式地请求值初始化,T是类型名。
(7)转换构造函数:当构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换规则(比如Salas_data类中,定义的只接受string和istream的构造函数,可分别定义了string到Salas_data和istream到Salas_data的隐式转换规则,因此可以使用string和istream替代Salas_data)。
(8)隐式转换规则只允许一步类类型转换
item.combine("9-99-999-9"); //出错,使用了两种转换规则,9-99-999-9到string,然后才string到Salas_data。
item.combine(string("9-99-999-9"));//正确
(9)抑制构造函数定义的类类型转换:通过将构造函数声明为explicit阻止隐式转换。隐式转换规则只适用于一个实参的构造函数,因此关键字explicit(该关键字只能出现在类内)只对一个实参的构造函数有效。
explicit构造函数只能用于直接初始化,不能用于拷贝形式的初始化。
- 接受一个单参数的const char*的string构造函数不是explicit的;
- 接受一个容量参数的vector构造函数是explicit的。
(10)满足以下条件则称其为聚合类:
- 所有成员都是public的;
- 没有定义任何构造函数;
- 没有类内初始值;
- 没有基类,也没有virtual函数。
使用花括号括起来的成员初始值列表进行初始化,列表中顺序与声明顺序一致,列表元素少于类成员则余下的进行值初始化,且元素个数不能超过成员个数。
(11)显式地初始化类的对象的成员的缺点:
- 类所有成员都是public。
- 将正确初始化每个对象的每个成员的重任交给了类的用户。
- 添加或删除一个成员之后,所有初始化语句都需要更新。
(12)字面值常量类:数据成员都是字面值类型的聚合类是字面值常量类;若类不是聚合类,但满足下列要求,他也是字面值常量类:
- 数据成员都是字面值类型;
- 类必须至少含有一个constexpr构造函数;
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式,或如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数;
- 类必须使用析构函数(名字与类名相同,但前面加~,,没有参数和返回值)的默认定义,该成员负责销毁类的对象。
(13)一个字面值常量类必须至少提供一个constexpr构造函数,constexpr构造函数可以声明为=default形式或者删除函数的形式。constexpr构造函数函数体一般为空,通过前置关键字constexpr声明一个constexpr构造函数。
(14)constexpr构造函数必须初始化所有数据成员,使用constexpr构造函数或者是一条常量表达式。
6、类的静态成员
静态数据成员与类本身直接相关,不与类的各个对象保持关联。
- 声明静态成员
在成员声明前加static关键字将其与类关联在一起,所有对象共享一个静态数据成员。
静态数据成员类型可以是常量、引用、指针、类类型等。
静态成员函数不包含this指针,也不能声明为const,函数体内也不能使用this指针。 - 使用类的静态成员
通过使用作用域运算符(::)直接访问静态成员。
double r;
r = Accout::rate();
可以使用类的对象、引用或者指针访问静态成员。
Accout ac1;
Accout *ac1 = $ac2;
r = ac1.rate();
r = ac2->rate();
成员函数不用通过作用域运算符就能直接使用静态成员。
- 定义静态成员
指向类外部的静态成员时,必须指明成员所属的类名。
static关键字只能出现在类内部的声明语句中。
必须在类外部定义和初始化每个静态成员,和其他对象一样,一个静态数据成员只能定义一次。 - 静态成员的类内初始化
静态成员若是字面值常量类型的constexpr,则可以在类内进行初始化,但初始值必须是常量表达式或const整数类型的类内初始值。
若使用静态成员的场景中,编译器可以替换它的值,则一个初始化的const或constexpr static不需要分别定义,相反若值不可替换,则该成员必须有一条定义语句。
class Accout {
public:
static double rate(){return interestRate;}
static void rate(double);
private:
static constexpr int period = 30;
double daily_tbl[period];
};
constexpr int Acount::period;
即使一个常量静态数据成员在类内部被初始化了,通常情况下也应在类的外部定义一下该成员。
- 静态成员可用于场景而普通成员不可以
①静态成员可以是不完全类型,特别静态成员可以是它所属的类类型,而非静态数据成员受到限制,只能声明成它所属类的指针或引用;
②可以使用静态成员作为默认实参,非静态数据成员不能,因为它的值属于对象的一部分。