类的基本思想是数据抽象和封装。类的抽象是一种依赖接口和实现分离的编程技术。
封装实现了类的接口和实现的分离.封装后的类隐藏了它的实现细节。
实现类的数据抽象和封装,需先定义一个抽象数据类型。
1.定义抽象数据类型
在本章我们引用和设计Sales_data类来理解我们对C++中的抽象数据类型。
当然,Sales_data类并不是一个抽象数据类型,它允许类的用户直接访问它的数据成员,而抽象数据类型的数据成员是封装好的。
1.1 设计Sales_data 类
Sales_data类的接口包括一些成员函数(isbn成员函数、combine函数、read函数等);
类的设计者为其用户设计并实现类,类的用户是程序员,而非应用程序的最终使用者。
1.2 定义改进的Sales_data类
成员函数的声明必须在类的内部,它的定义既可以在类内部,也可以在类的外部。
作为接口组成部分的非成员函数的定义和声明都在类的外部。
定义在类内部的函数都是隐式的inline函数。
引入this
当我们调用成员函数时,实际上是在替某个对象调用它。
例如,Sales_data类的isbn成员函数
total.isbn()
当isbn指向Sales_data类的成员(bookNo),则isbn隐式地指向调用该函数的对象(total)的成员(bookNo)。
成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。
当我们调用一个成员函数时,用请求该函数的对象地址初始化this。
由于有了this参数,在成员内部,我们可以使用调用该函数的对象的成员,而无须通过成员访问运算符来实现该功能。
this形参是隐式定义的。任何自定义名为this的参数或变量或显式地定义this指针的行为都是非法的。
this是一个常量指针,总是指向调用该函数的对象,不允许改变this中保存的地址。
std::string isbn() const { return bookNo; }
函数的参数列表之后的const关键字的作用是修改隐式this指针的类型。
默认情况下,this的类型是指向类类型非常量版本的常量指针。
通过const将this声明为指向常量的指针,这样使用const的成员函数称为常量成员函数。
常量对象,以及常量对象的引用或指针只能调用常量成员函数。
编译器先编译成员的声明,才轮到成员函数体。因此,成员函数可以随意使用类中的其他成员而无需在意这些成员的出现次序。
当我们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。
double Sales_data::avg_Price() const { //明确指定const属性
...... //类外部定义的成员必须显式地指出包含它所属的类名
}
当我们想返回调用该函数的对象时,直接把调用该函数的对象当成一个整体来使用。
return *this;
1.3 定义类相关的非成员函数
非成员函数在概念上属于类的组成部分,但实际上并不属于类本身。
非成员函数通常在类中声明,但没有在类中定义。
默认情况下,拷贝类的对象其实拷贝的是对象的数据成员。
1.4 构造函数
构造函数用来控制类对象的初始化过程,初始化类对象的数据成员。
构造函数的名字和类名相同,函数没有返回类型;
类可以包含多个构造函数,不同的构造函数之间必须在参数数量或参数类型上有所区别。
构造函数不能被声明成const的。
当我们创建类的一个const对象时,直到构造函数完成初始化过程后,对象才真正取得常量属性。
只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。
默认构造函数并不是万能的,对于一些复合类型(数组或指针)的对象进行初始化,它们的值是未定义的;
如果类中包含其他类类型的成员且该成员没有默认构造函数,那么默认构造函数无法初始化该成员
在C++新标准中,我们可以通过default来要求编译器生成构造函数。
Sales_data() = default;
Sales_data(const std::string &s) : bookNo(s) {}
冒号以及冒号和花括号之间的部分被称为构造函数初始值列表。
构造函数初始值列表负责为新创建的对象的一个或多个数据成员赋初值。
若某个数据成员被构造函数初始值列表忽略时,它将被合成默认构造函数隐式默认初始化。
若编译器支持类内初始值机制,构造函数可以使用类内初始值来给数据成员赋予正确的值。
Sales_data(const std::string &s):bookNo(s),units_sold(0) {}
当我们在类的外部定义构造函数时,必须指明该构造函数所属哪个类。
1.5 拷贝、赋值和析构
类不仅要定义类的对象如何初始化,而且还需要控制拷贝、赋值和销毁对象时的行为。
若我们没有定义这些操作,编译器会自动合成这些操作,但某些类不能依赖于合成的版本。
对于使用vector或string的类能在合成版操作下正常工作,避免了分配和释放内存带来的复杂性。
2.访问控制与封装
在C++中,我们使用访问说明符来加强类的封装性;
定义在public说明符之后的成员在整个程序内可被访问;
定义在private说明符之后的成员可被类的成员函数访问,但不能被使用该类的代码访问。
构造函数和部分成员函数在public说明符之后,数据成员和实现细节函数在private之后。
一个类可以包含0~n个访问说明符。
使用struct关键字,则定义在第一个访问说明符之前的成员是public的,class关键字则相反。
2.1 友元
通过令其他类或函数成为类的友元,则其他类或函数可访问它的非公有成员。
class Sales_data
{ //在类内,加一条以friend关键字开头的声明语句
friend Sales_data add(const Sales_data&, const Sales_data&);
......
}
友元声明只能出现在类定义的内部,但不限制在类内出现的具体位置。
封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
友元声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。
当我们想在类中调用某个友元函数,必须在友元声明之外再对函数进行一次函数声明。
每个友元函数必须提供独立的声明。
3. 类的其他特性
3.1 类成员再探
类除了定义数据和函数成员,还可以自定义某种类型在类中的别名。
class Sales_data
{
public:
typedef std::string::size_type pos;
using pos = std::string::size_type; //等价于上式
private:
pos cursor = 0;
}
若我们已经提供构造函数,且类需要默认构造函数,必须显式地把default声明出来。
成员函数可以被重载,只要在参数的数量或类型上有所区别。
mutable关键字:可以修改类的某个数据成员,即便在一个const成员函数中。
mutable修改的对象不是const,但调用它的函数是const。
3.2 返回*this的成员函数
return *this;
this指针解引用后是成员函数,指针是类型别名 。
myScreen.move(4,0).set('#'); //运算顺序遵循左结合律
一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。
通过区分成员函数是否是const的,我们可以对其函数进行重载。
3.3 类类型
每个类定义了唯一的类型,即使两个类的成员完全相同,这两个类是不同的类型。
我们可以通过把类名作为类型的名字使用,从而直接指向了类类型。
C++允许我们仅声明而暂时不定义它,是一种不完全类型,这种声明称为前向声明。
不完全类型只能在两种情况下使用:
1、定义指向这种类型的指针或引用; 2、声明以不完全类型作为参数或返回类型的函数
类必须被定义过,才能用指针或引用访问其成员。
3.4 友元再探
友元函数能定义在类的内部,这样的函数是隐式内联的。
如果一个类指定了友元类,则友元类的成员函数可访问该类的所有成员。
友元关系不存在传递性。
当把一个成员函数声明为友元时,我们必须明确指出该成员函数属于哪个类。
class screen
{
friend void Window_mgr::clear(Screenindex);
};
类和非成员函数的声明不是必须在它们的友元声明之前。
4. 类的作用域
我们在类的外部定义成员函数时必须同时提供类名和函数名。
在类的外部时,一旦遇到类名,定义的参数列表和函数体就在类的作用域内,函数可直接使用类的其他成员。
当成员函数定义在类的外部时,返回类型中使用的名字位于类的作用域之外。
void Window_mgr::clear(Screenindex i) {}
4.1 名字查找与类的作用域
类的名字查找从内向外,逐层地匹配,若最终没有匹配成功则报错。
编译器处理完类中的全部声明后才会处理成员函数的定义。
类型名的定义通常出现在类的开始处,这样就能确保使用该类型的成员都出现在类名的定义之后
尽管类的成员被隐藏了,我们仍然可以通过加上类名或显式地使用this指针来强制访问。
尽管外层的对象被隐藏掉了,但我们仍然可以用作用域运算符访问它。
void Screen::dummy(pos height)
{
cursor = width * :: height;
}
5. 构造函数再探
5.1 构造函数初始值列表
如果没有在构造函数的初始值列表中初始化成员,则该成员将在构造函数体之前执行默认初始化.
构造函数可以通过赋值操作来初始化数据成员。
Sales_data::Sales_data(double price)
{
units_sold = price;
}
构造函数的初始值列表必不可少的两种情景:
1、成员是const或者是引用时,必须初始化 。
2、当成员属于某种类类型且该类没有默认构造函数时,也必须将该成员初始化。
构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。
成员初始化与它们在类定义中的出现顺序一致,因此,尽量避免使用某些成员初始化其他成员。
如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
5.2 委托构造函数
C++新标准允许我们定义委托构造函数,委托构造函数可以委托其他构造函数来帮它完成一些对象成员的初始化操作。
被委托的构造函数在委托构造函数的初始化列表里被调用。
委托构造函数的初始值列表中,只允许出现被委托的构造函数,而不能出现给成员变量进行初始化的操作。
委托函数的执行顺序:先执行被委托构造函数的初始化列表,然后执行被委托构造函数的函数体,最后返回执行委托构造函数的函数体。
class Sales_data
{
//有完整参数初始化的构造函数
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) {}
//把缺少参数初始值的函数通过委托给有完整参数的函数来进行初始化
};
5.3 默认构造函数的作用
当类类型的成员没有在构造函数初始值列表中显式地初始化时,成员将被默认初始化。
在实际上,如果定义了其他构造函数,最好也提供一个默认构造函数。
定义一个使用默认构造函数进行初始化的对象:
Sales_data obj; //obj是个默认初始化的对象
5.4 隐式的类类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。
string nubook = "999999999";
Sales_data& combine(Sales_data &a) {......}
item.combine(nubook);
//编译器自动给string对象创建一个临时的Sales_data对象,并传递给combine函数
编译器只会自动地执行一步类型转换;例如上式的string转换成Sales_data类型,完成调用。
类类型转换不是总有效,这依赖于我们对用户使用该转换的看法。
通过将构造函数声明为explicit能抑制构造函数定义的隐式转换。
explicit Sales_data(const std::string &s): bookNo(s) {}
关键之explicit只对一个实参的构造函数有效,且只允许出现在类内的构造函数声明处。
当我们用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用。
我们可以通过显式构造和static_cast来强制转换explicit的构造函数;
item.combine(Sales_data(nubook));
item.combine(static_cast<Sales_data>(nubook));
5.5 聚合类
聚合类使得用户可以直接访问其成员,且具有特殊的初始化语法形式。
满足以下条件的类,它是聚合的;
1、所有成员都是public的 2、没有定义构造函数 3、没有类内初始值 4、没有基类或virtual函数
struct data
{
int ival;
string s;
};
初始化聚合类的数据成员的初始值顺序必须与声明的顺序一致。
如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化。
5.6 字面值常量类
数据成员都是字面值类型的聚合类是字面值常量类。
非聚合类,但满足以下条件的类,它是字面值常量类;
1、数据成员都是字面值类型。 2、类中至少含有一个constexpr构造函数
3、如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式。
4、类必须使用析构函数的默认定义,该成员负责销毁类的对象。
constexpr构造函数体一般来说是空的,且必须初始化所有数据成员。
constexpr debug(bool h, bool o): hw(h),io(o) { }
6. 类的静态成员
在成员声明前加上关键字static,使得该成员与类本身关联,未与类的各个对象保持关联。
静态数据成员的类型可以是常量、引用、指针、类类型等。
class account{
static void rate(double);
private:
static double interestrate; //静态数据成员
};
静态成员存在于任何对象之外,静态成员是唯一的,被所有的类对象共享。
静态成员函数也不与任何对象绑定在一起,它们不包含this指针,也不能声明为const的。
account a;
account *b = &a;
double r = account::rate(); //使用作用域运算符访问
double r = a.rate(); //通过account的对象或引用
double r = b->rate(); //通过指向account对象的指针
成员函数不用通过作用域运算符就能直接使用静态成员。
当我们指向类外部的静态成员时,必须指明成员所属的类名。
当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句中。
由于静态数据成员不属于类的任何一个对象,这意味着它们不是由类的构造函数初始化的。
静态成员必须在类的外部定义和初始化,且在类中声明该静态成员。
定义静态成员需要指定对象的类型名、类名、作用域运算符以及成员本身名称。
double account::interestrate = initrate();
特殊情况下,静态成员可以在类内部初始化,但要求成员必须是字面值常量类型的constexpr。
static constexpr int price = 30;
即使一个常量静态数据成员在类内部被初始化了,但也应该在外部声明该成员。
静态数据成员可以是不完全类型,类型可以是它所属的类类型。
静态成员可以作为类成员函数的默认实参,类似于全局变量。