第12章 类
12.1 类的定义和声明
12.1.1 类定义
12.1.2 数据抽象和封装
数据抽象 是一种 接口和实现分离 的编程技术,类的设计者必须关心类是如何实现的,类的使用者仅需了解类的接口,考虑的是类可以做什么而不是如何做;封装 是一项将低层次的元素组合起来形成新的、高层次实体的技术,函数是封装的一种形式,类也是一个封装的实体,它代表若干成员的聚集
在C++中,使用 访问标号 来定义抽象接口和实施封装,一个类可以没有标号,也可以包含多个访问标号
C++程序员经常会将 应用程序的用户 和 类的使用者 都称为“用户”,改变头文件中的类定义,使用该类的代码必须重新编译
不在类定义体内定义的 inline 成员函数,其定义通常应放在 有类定义的 同一头文件中
12.1.4 类声明与类定义
12.2 隐含的this指针
成员函数具有一个附加的隐含指针,即指向该类对象的一个指针,这个隐含形参命名为this,与调用成员函数的对象绑定在一起,成员函数体可以显式使用this指针
从 const成员函数 不能返回指向类对象的普通引用,const成员函数只能返回 *this 作为一个 const引用,基于成员函数是否为const ,可以重载一个成员函数
可变数据成员 永远都不能为const,甚至当它是const对象的成员时也是如此,因此const成员函数可以改变mutable成员,要将数据成员声明为可变的,必须将关键字mutable放在成员声明之前
12.3 类作用域
每个类,都定义了自己的新作用域 和 唯一的类型,在类定义体内 声明类成员,将成员名引入类的作用域,成员函数的形参表和函数体处于类作用域中,尽管成员是在类的定义体之外定义的,但成员定义就好像它们是在类的作用域中一样
成员函数返回类型不一定在类作用域中,与形参相比,返回类型出现在成员名字前面,如果函数在类定义体之外定义,则用于返回类型的名字在类作用域之外,如果返回类型使用由类定义的类型,则必须使用完全限定名
#include <iostream> using namespace std; class A { public: typedef size_t index; //index 类内定义的类型 index getX(); //类内声明成员函数,返回类型 index 为类定义的类型 A(index x):x(x) { } private: index x; }; A::index A:: getX() { return x; } //类外定义 A::index int main() { A a(128); A::index x = a.getX(); cout << x; return 0; }
12.4 构造函数
构造函数是特殊的成员函数,只要创建 类类型的 新对象,都要执行构造函数,构造函数的工作是保证每个对象的 数据成员具有合适的初始值
构造函数的工作是初始化对象,不管对象是否为const,都用一个构造函数来初始化对象,构造函数不能声明为const
12.4.1 构造函数的初始化式
构造函数可以包含一个构造函数初始化列表,列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式,初始化式可以是任意复杂的表达式,构造函数的初始化式 只在构造函数的定义中 而不是声明中 指定
在构造函数,初始化列表中,没有显式提及的每个成员:类类型的数据成员,(编译器会隐式的使用)该类型的默认构造函数来初始化,内置或复合类型的成员 的初始化依赖于对象的作用域,即 局部作用域中,这些成员不被初始化,全局作用域中它们被初始化为0
没有默认构造函数的类类型的成员,以及const或引用类型的成员,必须在构造函数 初始化列表中 进行初始化
构造函数初始化列表并不指定这些初始化执行的次序,成员被初始化的次序就是定义成员的次序
12.4.3 默认构造函数
只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数,该合成的默认构造函数使用与变量初始化相同的规则来初始化数据成员:具有类类型的成员通过运行各自的默认构造函数来进行初始化,内置和复合类型的成员 如指针和数组,只对定义在全局作用域中的对象才初始化,当对象定义在局部作用域中时,内置或复合类型的成员不进行初始化
如果定义了其他构造函数,则提供一个默认构造函数几乎总是对的,通常,在默认构造函数中给成员提供的初始值应该指出该对象是“空”的
12.4.4 隐式类类型转换
可以定义如何将其他类型的对象隐式转换为我们的类类型,或将我们的类类型的对象隐式转换为其他类型(14.9),可以用 单个实参调用的构造函数,定义从形参到该类类型的一个隐式转换,当构造函数被声明为explicit时,编译器将不使用它作为转换操作符,explicit关键字只能用于类内部的构造函数声明上,在类的定义体外部所做的定义上不再重复它
#include <iostream> using namespace std; class A{ //public: int a; public: A():a(0) { } explicit A(int a):a(a) { } }; int main() { A a; a = A(11); //a = 11; 隐式转换错误 //cout << a.a; return 0; }
通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为explicit,将构造函数设置为explicit可以避免错误,并且 当需要转换时,用户可以显式地构造对象
12.4.5 类对象的显式初始化
尽管大多数对象可以通过运行适当的构造函数进行初始化,但是 直接初始化 简单的非抽象类 的数据成员 仍是可能的,对于没有定义构造函数 并且 其全体成员均为public的类,可以采用与初始化数组元素相同的方式初始化其成员
显式初始化从C继承而来,支持与C程序兼容,显式初始化类类型对象的成员有 三个重大的缺陷:要求类的 全体数据成员都是public;将初始化每个对象的每个成员的负担 放在程序员身上; 如果增加或删除一个成员,必须找到所有的初始化并正确更新
12.5 友元
友元机制,允许一个类,将对其非共有成员的访问权授予 指定的函数或类,友元的声明以关键字 friend 开始,只能出现在 类定义的内部,可以出现在类的任何地方,不受访问控制影响
12.6 static类成员
static 数据成员,独立于该类的任意对象而存在,每个static数据成员是与类关联的对象,并不与该类的对象相关联;static成员函数,没有this形参,它可以直接访问所属类的static成员,但不能直接使用非static成员
static 成员遵循正常的公有/私有访问规则,可以通过作用域操作符从类 直接调用 static成员,或者通过对象、引用或指向该类类型对象的指针间接调用
12.6.1 static成员函数
static成员函数不能被声明为const,不能声明为虚函数
12.6.2 static 数据成员
static 数据成员可以声明为任意类型
#include <iostream> using namespace std; class A{ private: static int x; //static数据成员的声明 static int setX(); public: static int getX(){ return x; } }; int A:: x = setX(); //一旦成员名出现,static成员的定义就是在类作用域中,因此可以访问该类的私有成员 //static数据成员的定义,放在包含类的非内联函数定义的文件中 int A:: setX(){return 3; } int main() { cout << A::getX(); return 0; }