在C++中,我们可以使用类定义自己的数据类型,用新的类型反映程序中的概念,从而我们更容易编写、调试和修改程序。
类的基本思想是数据抽象
和封装
。数据抽象能够帮助我们将对象的具体实现和对象所执行的操作分离开来,是一种依赖于接口和实现分离的编程技术。封装后的类隐藏了实现细节,类的使用者只需要使用接口而不需要关心具体的实现,降低程序的耦合性。
定义抽象数据类型
成功的应用程序,作者必须充分了解并实现用户的需求。所以,优秀的类设计者也应该密切关注那些使用该类的程序员的需求。作为一个设计良好的类,既要有直观而且易于使用的接口,也要具备高效的实现过程。
定义在类内部的函数是隐式的inline函数。
引入this
当我们调用成员函数时,实际上是在替某个对象调用它。成员函数通过一个名为this的隐式参数来访问调用它的对象。当我们调用一个成员函数时,用调用该函数对象的地址初始化this。例如,调用total.isbn()
,则编译器负责把total的地址传递isbn的隐式参数this,等价于编译器将调用重写成如下形式:
1 | //伪代码,用于说明调用成员函数的实际执行过程 |
在成员函数内部,因为this都指向该对象,所以可以直接调用该对象的其他成员函数。
this形参是隐式定义的。所以,任何自定义名为this的参数或成员变量都是非法的。另外,我们可以在成员函数内部使用this。因为this的目的总是指向“这个”对象,所以this是一个常量指针。
引入const成员函数
默认情况下,this是一个指向非常量的常量指针。C++用成员函数参数列表之后的const关键字修饰隐式的this指针,表示this是一个指向常量的常量指针,const修饰后的this指针具体类型等价于const Sales_data* const this;
。
1 | struct Sales_data { |
像这样使用const的成员函数称为常量成员函数
,常量成员函数不能改变调用它的对象的内容。
常量对象,以及常量对象的引用或者指针都只能调用常量成员函数。
定义类相关的非成员函数
如果普通函数在概念上属于类但是不定义在类中,则它应该与类声明在同一个头文件中,从而用户使用时只需引入一个头文件。
构造函数
构造函数控制对象的初始化过程,其任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
构造函数不能被声明为const,当我们创建类的一个const对象时,直到构造函数完成初始化,对象才真正取得其“常量”属性。
默认构造函数无任何实参。只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数。
如果我们的类没有显式定义构造函数,那么编译器会为我们隐式地定义一个默认的构造函数,称为合成默认构造函数。合成默认构造函数初始化类的数据成员规则:
如果存在类内初始值,使用它来初始化成员 C++11新标准规定,可以为数据成员提供类内初始值,放在花括号或者等号右边,不能使用圆括号
否则,默认初始化该成员
=default的含义
在C++11新标准中,如果我们需要默认的行为,可以通过在参数列表后面写上= default
要求编译器生成构造函数。
让编译器生成的默认构造函数有效,需要我们为内置类型提供类内初始值。
如果编译器不支持类内初始值,默认构造函数应该使用构造函数初始值列表
来初始化类的每个成员。
构造函数不应该轻易覆盖类内初始值,除非新的值与原值不同。
访问控制和封装
使用struct时,默认访问权限为public;使用class的默认访问权限是private。
友元
将其他的类,类的成员函数或者函数使用friend关键字声明为类的友元,就可以访问该类的非公有成员。
一般来说,最好在类定义开始或者结束的位置集中声明友元。
友元的声明仅仅指定了访问权限,而非通常意义上的函数声明。所以我们在类的外部再提供独立的函数声明。
使用封装的优点:
用户代码不会无意间破坏封装对象的状态,提高内聚
被封装的类的具体实现细节可以随时改变,而调用者不需要做任何修改,降低耦合
类的其他特性
类成员再探
可以在类中自定义类型别名。由类定义的类型名字和其他成员一样存在访问限制,可以是public和private的一种。
在类中,规模较小的函数适合声明成内联函数。定义在类内部的成员函数是自动inline的。
另外,一般只在类外部定义的地方声明inline,同样地,inline成员函数也应该位于类定义的同一个头文件中。
有时我们希望在一个const成员函数内能够修改某个数据成员,我们可以通过在变量的声明中加入mutable
关键字变成可变数据成员
。
返回*this
的成员函数
通过*this
返回调用对象的引用,返回引用意味着函数返回的是对象本身而非对象的副本,在链式操作中,所有操作将在同一个对象上执行。
一个const成员函数如果以引用的形式返回*this
,那么它的返回类型是常量引用。
通过区分成员函数是否是const,我们可以对其进行重载。在一个常量对象上只能调用const成员函数,在非常量对象上既可以调用常量版本也可以调用非常量版本,但非常量版本更匹配。
当const版本和non-cast版本有这等价的实现时,可以将公共代码实现成const的私有成员函数或者令non-const版本调用const版本从而避免重复代码。
类类型
对于一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明。否则,编译器无法了解这样的对象需要多少存储空间。
但我们可以仅声明类而暂时不定义它,这种声明称作前向声明(forward declaration)
。对于类类型,在声明之后,定义之前是一个不完全类型(incomplete type)
,也就是说我们知道Screen是一个类类型,但不清楚它到底包含什么成员。
1 | class Screen; //Screen类的声明 |
不完全类型的使用场景:
定义指向这种类型的指针或引用,因此,一个类的成员类型不能是类自己,但类可以包含指向自身类型的指针或者引用
声明(不能是定义)以不完全类型作为参数或者返回类型的函数
友元再探
除了类,普通函数,我们也可以将类的成员函数作为友元,但我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系,在将Window_mgr::clear成员函数声明为Screen类的友元例子中,我们必须按照以下方式设计程序:
首先定义Window_mgr类,其中声明clear函数,但是不能定义。
接下来定义Screen,包括对于clear的友元声明
最后定义clear,此时它才可以使用Screen的成员
具体的示例程序如下所示:
1 | class Window_mgr |
类的作用域
在类的外部,成员名字被隐藏了,因此在类的外部定义成员函数时需要同时提供类名和函数名。一旦遇到类名,定义的剩余部分,包括形参列表和函数体就可以直接使用类的其他成员。
但函数的返回类型出现在类名之前,因此返回类型中使用的名字位于类的作用域之外。所以,返回类型必须使用作用域操作符::
指明是哪个类的成员。
成员函数中使用名字解析规则:
首先,在成员函数内部查找该名字的声明
如果在成员函数内没有找到,则在类内继续查找
如果类内也没有找到该名字的声明,在成员函数定义之前的作用域内继续查找
构造函数再探
如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。随着构造函数体一开始执行,初始化就完成了。因此我们初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值列表
。
如果成员是const、引用或者某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初始值。
最好令构造函数初始值的顺序与成员声明顺序保持一致,如果可能的话,尽量避免使用某些成员初始化其他成员。
委托构造函数
C++11新标准提供委托构造函数
,它能够使用所属类的其他构造函数执行它自己的初始化过程,也就是说它把它自己的一些或者全部职责委托给其他构造函数。
1 | class Sales_data |
接受istream&构造函数委托给默认构造函数,默认构造函数又委托给三参数构造函数。当这些受委托的构造函数执行完毕后,控制权才会交还给委托的函数体。
在实际中,如果定义了其他的构造函数,那么最好也提供一个默认构造函数。
隐式的类类型转换
如果构造函数只接受一个实参,则实际上定义了转换为此类类型的隐式转换机制。编译器只会自动地执行一步类型转换
。另外,有时隐式类型转换是我们不希望的,我们可以通过将构造函数声明为explicit
加以阻止。
1 | class Sales_data |
关键字explicit只对一个实参的构造函数有效。
只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应该重复。
当我们用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用。
为转换显式地使用构造函数:
1 | std::string null_book("999-999-999"); |
类的静态成员
在成员的声明之前加上static关键字将成员与类本身直接关联,而不是与类的各个对象相关联。
静态成员函数不与任何对象绑定在一起,也不包含this指针。因此,静态成员函数不能声明成const的,而且我们也不能在static函数体内使用this指针或者调用非静态成员(隐式需要this指针)。
使用作用域运算符直接访问静态成员。
既可以在类的内部也可以在类的外部定义静态成员函数,当在类的外部定义静态成员函数时,不能重复static关键字,static关键字只出现在类内部的声明语句中。
因为静态数据成员不属于类的任何一个对象,所以我们不能在类的内部初始化静态成员,必须在类的外部定义和初始化每个静态数据成员,一个静态数据成员只能定义一次。
静态数据成员定义方式:类型名,然后类名,作用域运算符以及成员名字。
1 | double Account::interestRate = initRate(); //initRate是static成员函数 |
为了保证对象只定义一次,最好的办法是把静态数据成员的定义和其他非内联函数的定义放在同一个文件中。
除了常量静态数据成员,其他静态数据成员不能在内的内部初始化。
即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义下该成员,但不需要再指定初始值了。