类的基本思想是数据抽象和封装。
数据抽象是一种依赖于接口和实现分离的技术。
类的接口包括用户所能执行的操作;类的实现包括类的数据成员,负责接口实现的函数体以及定义类所需的各种私有函数。
封装实现了类的接口和实现分离。要实现数据抽象和封装,首先要定义一个抽象数据类型。类的设计者考虑类的实现过程,调用者只需要思考类做了什么,无需知道细节。
7.1定义抽象数据类型
定义一个Sales_data类
struct Sales_data{
//成员函数声明在内部,定义既可以内部也可以外部
std::string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);
double avg_price() const;
std::string bookNo;
unsigned unit_sold = 0;
double revenue = 0.0;
}
//作为接口组成部分的非成员函数,定义和声明在外部
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
调用时使用点运算符来访问total对象的isbn成员,然后调用。
成员函数通过this的隐式参数来访问调用对象。
total.isbn();
可以等价认为
Sales_data::isbn(&total);
当isbn使用bookNo时,隐式使用this指向的成员
return this->bookNo;
this总是指向“这个”对象,因此是常量指针。
可以把isbn的函数体想象成如下的
//此代码非法,因为无法定义this
std::string Sales_data::isbn(const Sales_data *const this)
{
return this->isbn;
}
类作用域和成员函数
类本身是一个作用域,类的成员函数嵌套在作用域中。
即使bookNo定义在isbn之后,isbn也能使用bookNo。
编译器两步处理类: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 &rsh)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
total.combine(trans);
total的地址被绑定到隐式的this上,rhs绑定到trans上。因此结果保存到了total.units_sold中。
调用结果返回total的引用。
定义类相关的非成员函数
有些函数定义的操作从概念上属于类的接口组成部分,实际不属于类本身,如add、read;
通常把声明和定义分开。如果函数在概念上属于类但不定义在类中,一般和声明放在同一个头文件。
//从给定流中将数据读到给的对象
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.untis_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
构造函数
类通过一个或几个特殊的成员函数来控制其对象的初始化过程,叫构造函数。
目的:初始化类对象的数据成员。只有类被创建就会执行构造函数。
构造函数名字和类名相同,没有返回类型。
构造函数不能被声明成const。
如果没有,会执行一个默认构造函数,无任何实参。
某些类不能依赖默认构造函数
1、定义了其他构造函数
2、含有内置类型复合类型成员的类应该在内部初始化,或者定义一个默认的构造函数。否则在创建类的对象时会得到未定义的值
3、编译器不能为某些类合成默认的构造函数。例:类中包含一个其他类类型的成员并且这个成员的类型没有默认构造函数。
定义Sales_data的构造函数
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){}
}
=defaule代表默认构造函数,目的是因为定义了其他的构造函数,还需要默认的构造函数。
新的那部分成为构造函数初始值列表,为新创建的对象成员赋值。
类的外部定义构造函数
Sales_data::Sales_data(std::istream &is)
{
read(is, *this);
}
这个构造函数没有初始值列表,但是执行了函数体。
拷贝、赋值和析构
拷贝:初始化变量以及以值的方式传递或返回一个对象
赋值:使用了赋值运算符时
析构:销毁对象。
如果不主动定义,编译器都自动合成这些操作。
某些类不能依赖于合成的版本
某些类编译器合成的版本无法正常工作。
特别是当类需要分配类对象之外的资源时,合成版本常常失效。如管理动态内存的类。
7.2访问控制和封装
为了限制访问使用封装,用访问说明符。
public:成员在整个程序都能访问。public定义了类的接口
private:只能被类成员函数访问,隐藏了类的实现细节。
再次定义Sale_datas类
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和struct只有默认访问权限不一样。
struct定义在第一个访问说明符之前的时public,class则时private。
一般情况下如果希望类的所有成员都是public,就用struct,其他情况用class
友元
类可以允许其他类或函数访问非共有成员,使用friend关键字。
友元声明只能出现在类定义的内部。最好都在类开始或结束集中声明。
封装的好处
1、确保用户代码不会无意间破坏封装对象的状态。
2、被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
友元的声明
友元声明仅仅指定了访问权,不是通常意义的声明。如果需要调整某个友元函数,必须再次声明。
通常把友元声明和类本身放在同一个头文件中。
7.3类的其他特性
定义类型成员
class Screen{
public:
typedef std::string::size_type pos;
//等价写法
using pos = std::string::size_type;
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的构造函数和返回光标所指字符的get函数默认是inline函数。
也可以在类内部显式声明成员函数,或者在类外部用inline修饰函数定义。
无需在声明和定义同时inline,不过最好在类外部定义的地方说明inline。
重载成员函数
成员函数的重载和匹配和非成员函数类似。
可变数据成员
希望能修改类的某个数据成员,即使是const成员函数;可以用mutable。
一个可变数据成员用于不会const。
class Screen()
{
public:
void some_member() const;
private:
mutable size_t access_ctr;
}
void Screen::some_member() const
{
++access_ctr; //记录被调用的次数
}
尽管some_member是const成员函数,但是还能改变access_ctr的值。
类数据成员的初始值
希望一个类开始时总是有一个默认初始化的值,最好的方式是把这个默认值声明成一个类内初始值
class Window_mgr{
private:
std::vector<Screen> screens{Screen(24, 80, '')};
}
//创建一个单元素vector对象,传递给vector<Screen>构造函数
返回*this的成员函数
inline Screen &Screen::set(char c)
{
contents[cursor] = c;
return *this;
}
set返回值是调用set的对象的引用,是左值的,返回的是对象本身而不是副本。
myScreen.move(4,0).set('#');
//先移动,然后设置成员。
如果令函数返回的是Screen而非Screen&,则上述等价为
Screen temp = myScreen.move(4,0); //是返回值的拷贝
temp.set('#')
如果不是引用,函数调用的对象就不改变本身的值,只是生成一个结果的拷贝。
从const成员函数返回*this
如果要定义一个const的display函数,那么this将是一个指向const的指针,*this是const对象。因此display返回的类型应该是const Sales_data&。
那么display将不能嵌入到动作序列,因为它的返回是常量引用。
myScreen.display(cout).ser('*');//错误,是常量引用,无法再调用set
基于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;}
}
当display调用do_display是,this指针会隐式传递。如果是非常量版本,this将隐式转换成指向常量的指针。
调用完成后,display会各自解引用,如果是非常量版本,this指向一个非常量对象,返回一个普通引用。
因此当调用display时,会根据对象是否时const决定调用哪个
myScreen.ser('#').display(cout); //调用非常量
blank.display(cout); //调用常量
建议:对于公共代码使用私有功能函数
原因1:避免多处使用相同代码
2:display函数可能会很复杂,这样把相应操作写在移除更好
3:可能会添加调试信息,在do_displayu一处添加更容易
4:额外的函数调用不会增加任何开销
类类型
每个类定义了唯一类型,即使成员都一样。
类名可以作为类型名直接指向类类型,也可以跟在class和struct后
Sales_data item1;
class Sales_data item1; //等价,从C继承过来的
类的声明
类也可以声明而暂时不定义,成为前向声明。
这个类是一个不完全类型。
对于一个类,在创建前必须被定义,而不能只被声明。
不过有一种特例:正常来说直到类定义之后数据才能被声明成这种类型,所以一个类的成员不能是自己,然后一旦一个类名字出现后,就被认为是声明过了(但未定义),因此类允许包含指向它自身类型的引用或指针。
class Link_screen
{
Screen window;
Link_screen *next;
Link_screen *prev; //只可以引用或指针
}
类友元
除了把普通的非成员函数定义成友元,也可以把类定义成友元,或者其他类的成员函数定义成友元。
友元函数可以定义在类内部,这样的函数是隐式内联的。
//Window_mgr类向访问Screen的私有成员,就可以用友元
class Screen{
//Window_mgr的成员可以访问Screen类的私有部分
friend class Window_mgr;
}
友元没有传递性,如果Window_mgr也有友元,这些友元无法访问Screen。
每个类负责控制自己的友元类或友元函数。
成员函数友元
除了令整个类作友元,还可以只为类的某一个函数提供访问权限
class Screen{
friend void Window_mgr::clear(ScreenIndex);
}
函数重载和友元
如果一个类想把一组重载函数声明成友元,需要每一个分别声明
友元声明和作用域
类和成员函数的声明不一样要在友元声明之前,友元声明只是影响访问权限,不是普通意义的声明。
7.4类的作用域
每个类都会定义自己的作用域,在类的作用域之外,普通的数据和函数只能由对象、引用或指针使用成员访问符来访问。
对于类类型成员使用作用域运算符。
Screen::pos ht = 24, wd = 80; //使用Screen定义的pos类型
Screen scr(ht , wd, '');
char c = scr.get();
作用域和定义在类外部的成员
一个类就是一个作用域。一旦遇到类名,定义的剩余部分都在类作用域内。
void Window_mgr::clear(ScreenIndex i) //ScreenIndex不必专门说明
而返回类型通常出现在函数名之前,因此需要指明作用域
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s_){}
名字查找和类的作用域
名字查找是寻找与所用名字最匹配的声明,过程一般是
1:在名字所在块中寻找声明,只考虑名字使用之前出现的声明。
2:如果没有,则查找外层作用域
3:如果最终没有找到匹配声明,则报错
对于定义在类内部的成员函数来说解析名字有所区别,类的定义分两步处理
1:编译成员声明
2:直到类全部可见后才编译函数体
用于类成员声明的名字查找
两阶段处理方式只适用于成员函数中使用的名字。
声明中用的名字,包括返回类型和参数列表中的,都必须在使用前可见。
如果类中没出现,就会在该类的作用域中继续查找。
类型名要特殊处理
一般来说内层作用域可以重新定义外层作用域中的名字。
但是在类中,如果成员使用了外层作用域中的名字,则在之后不能再次定义。
typedef double Money;
class Accout {
public:
Money balance() {return bal;} //使用外层Money
private:
typedef double Money; //不能重新定义,即便是一样的
}
成员定义中的普通块作用域的名字查找
成员函数中使用名字的解析过程:
1、函数内查找,只在函数使用前出现的声明才被考虑
2、类内继续查找,所有的成员都可以考虑
3、在成员函数定义之前的作用域内继续查找
一般清空不要使用其他成员的名字作为某个函数的参数。
如果要绕开规则,需要使用this来强制访问
void Screen::dummy_fun(pos height)
{
cursor = width * height; //参数height
cursor = width * this->height; //成员height
cursor = width * Screen::height; //成员height
}
类作用域之后,在外围作用域查找
使用作用域运算符访问,尽量不要重名
int height;
void Screen::dummy_fun(pos height)
{
cursor = width * ::height; //全局的height
}
文件中名字的出现处进行解析
名字查找的第三步不仅考虑类定义之前的声明,还有考虑在成员函数定义之前的全局作用域中的声明
class Screen(){
public:
typedef std::string::size_type pos;
void setHeight(pos);
pos height = 0;
}
Screen::pos verify(Screen::pos);
void Screen::set::setHeight(pos var)
{
//verify是全局函数
//height是类的成员
height = verify(var); //全局函数verify的声明虽然类定义之前不可见,可以正常使用
}
7.5构造函数再探
构造函数初始值列表
构造函数如果没有在初始值列表显式初始化,则会先默认初始化,然后可以再赋值
Sales_data::Sales_data(const string&s, unsigned cnt, double price)
{
bookNo = s;
units_sold = cnt;
revenue = ct * price;
}
初始值有时必不可少
如果成员是const或者引用,必须初始化。
当成员属于某种类类型且该类没有定义默认构造函数,也必须初始化。
随着构造函数体开始执行,初始化就完成了。
初始化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){} //报错,试图用未定义的值j初始化i
}
尽量避免在初始化是使用同一个对象的其他成员
默认实参和构造函数
Sales_data(std::string s = ""):bookNo(s){}
如果不提供实参也能调用这个构造函数,该构造函数为类提供了默认构造函数
委托构造函数
c++11扩展了构造函数初始值的功能,可以定义委托构造函数。
使用它所属类的其他构造函数执行它自己的初始化过程,或者把自己的一些职责委托给了其他构造函数。
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 obj; //obj是个默认初始化对象
Sales_data obj2(); //声明了一个函数obj2,返回值为Sales_data
隐式的类类型转换
string null_book = "9-999-99999-9";
//构造了一个临时的Sales_data对象
//该对象units_sold和revenue = 0; bookNo = null_book
item.combine(null_book);
只允许一步类类型转换
编译器只会自动执行一步类型转换
item.combine("9-999-99999"); //错误的
item.combine(string("9-999-99999")); //显式转换成string,隐式转换成Sales_data;
item.combine(Sales_data(9-999-99999")) //隐式转换string,显式转换成Sales_data
抑制构造函数定义的隐式转换
在要求隐式转换的程序上下文中,可以将构造函数声明为explicit加以阻止
class Sales_data{
public:
Sales_data() = default;
explicit Sales_data(std::istream&);
}
item.combine(cin); //错误,istream构造函数式explicit的
explicit只对一个实参的构造函数有效。需要多个实参的构造函数无法执行隐式转换,所以无须使用explicit。
只能在类内声明构造函数时使用explicit,在外部定义时不应重复。
explicit只能用于直接初始化
执行拷贝形式的初始化时,不能使用explicit构造函数
Sales_data item1(null_book); //正确
Sales_data item2 = null_book; //错误
为转换显示地使用构造函数
可以显式地强制转换
item.combine(Sales_data(null_book));
item.combine(static_cast<Sales_data>(cin)); //使用static_cast执行显式转换
标准库中含有显式构造函数的类
1:接受一个单参数const char*的string构造函数不是explicit的
2:接受一个容量参数的vector构造函数是explicit的
聚合类
使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。
当一个类满足以下条件,它是聚合的:
1、所有成员都是public
2、没有定义任何构造函数
3、没有类内初始值
4、没有基类,也没有virtual函数
例如
struct Data{
int ival;
string s;
}
我们可以提供一个花括号括起来的成员初始值列表,并初始化聚合类的数据成员
Data vall = {0, "Anna"};
//等价于
vall.ival = 0;
vall.s = string("Anna");
初始值的顺序必须和声明顺序一样
如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化。
初始值列表的元素个数不能超过类的成员数量。
显式的初始化类的对象成员有三个缺点:
1、所以类成员都是public
2、把正确初始化的任务交给用户(而非类的作者),容易忘掉或提供不适合的值,所以这样的初始化过程冗长乏味且容易出错。
3、在添加或删除一个成员后,所有的初始化语句都要更新
字面值常量类
constexpr函数的参数和返回值必须是字面值类型。
字面值类型的类可能含有constexpr函数成员,这样的成员必须符合constexpr函数的所有要求,它们是隐式const的。
数据成员都是字面值类型的聚合类式字面值常量。
如果一个类不是聚合类,满足以下要求也是字面值常量类:
1、数据成员都是字面值类型
2、类至少有一个constexpr构造函数
3、如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是常量表达式;如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
4、类必须使用析构函数的默认定义,该成员负责销毁类的对象
constexpr构造函数
构造函数不能是const的,但字面值常量类的构造函数可以是constexpr的。
constexpr构造函数可以声明成=default的形式,否则
1、必须符合构造函数的要求
2、符合constexpr的要求(唯一可执行语句式return语句)。
因此一般constexpr构造函数体一般为空
class Debug{
public:
constexpr Debug(bool b = true): hw(b), io(b), other(b){}
consterpr 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){other = b;}
private:
bool hw;
bool io;
bool other;
}
constexpr构造函数必须初始化所有的数据成员。
constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型:
constexpr Debug io_sub(false, fasle, true);
if(io_sub.any()) //等价于if(true)
cerr << "print appropriate error messages" >> endl;
7.6类的静态成员
有些成员希望和类关联,而非与具体的对象关联。
声明静态成员
使用static关键字。
静态成员可以是public也可以是private。
静态数据成员的类型可以是常量、引用、指针、类类型。
//银行账户记录
class Accout{
public:
void calculate(){ amout += 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
使用类的静态成员
可以直接使用作用域运算符直接访问
double r;
r = Account::rate();
静态成员不属于类的某个对象,但是可以通过类的对象、引用或指针来访问
Account ac1;
Account *ac2 = &ac1;
r = ac1.rate();
r = ac2->rate();
定义静态成员
在类外部定义静态成员时,不能重复static关键字。
类似全局变量,类的静态变量一旦定义,就一直存在程序的整个生命周期。
double Account::interestRate = initRate(); //定义了interestRate的对象,该对象时Account的静态成员。
静态成员的类内初始化
通常情况静态成员不应该在类的内部初始化。但是可以为它提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式。
class Account{
public:
static double rate() { return interestRate; }
static void rate(double);
private:
static constexpr int period = 30;
double daily_tbl[period];
}
静态成员用于某些场景
1:静态数据成员的类型可以是它所属的类类型,可以是不完全类型;非静态数据成员则不行。
class Bar{
private:
static Bar mem1; //正确
Bar *mem2; //正确
Bar mem3; //错误
}
2:可以使用静态成员作为默认实参,非静态成员不行
class Screen{
public:
Screen& clear(char = bkground);
private:
static const char bkground;
}