Chapter 4 类和对象
利用类可以实现数据的封装、隐藏、继承和派生
封装
抽象
抽象的实现:通过类的声明
封装
将抽象出的数据成员、代码成员相结合,将它们视为一个整体。
实现封装:类声明中的{}
- 注意:数据成员可以是指向自身类的指针(由于指针和引用所占存储空间大小与类型无关,所以编译器可以计算分配空间),但不能是自身类的对象(会出现无限初始化的问题)。也不能是auto,extern,和register类型。
为什么不能是auto, extern, register类型:
auto/register/extern表示的是变量的存储位置和作用域。auto变量存储在函数的堆栈空间,register存储在寄存器,extern表示这里没有新定义变量,只是扩展了一个已有全局变量的作用域。类和结构体中的变量是成员变量,其存储位置和作用域由定义对象的函数决定,不由这些对象自己决定。
例如,你定义了一个类,complex,然后你在函数main中有如下语句:
1 | auto complex c; |
这就表示c这个对象在函数堆栈中存储。那么,c中的real和imaginary自然也在堆栈中了。同时,他们的生存期也仅限于函数内部。因此,你不需要,也不能单独给对象的成员变量规定存储位置和作用域。
this指针:
编译器改变每个类成员函数的调用,加上一个额外的实参,即被调用对象的地址。
访问控制
private
在类内部可以任意访问。外界只能通过对象的接口(具有public属性的成员函数)来访问。
protected
在派生类中,具有私有的或者保护的访问属性(private成分在派生类中是不可访问的)。
public
外界可以访问。
创建一个对象指针:
1 | Clock *p = new Clock; |
成员函数在内存中的存储方式
用类去定义对象时,系统会为每一个对象分配存储空间。如果一个类包括了数据和函数,要分别为数据和函数的代码分配存储空间。按理说,如果用同一个类定义了10个对象,那么就需要分别为10个对象的数据和函数代码分配存储单元。 能否只用一段空间来存放这个共同的函数代码段,在调用各对象的函数时,都去调用这个公用的函数代码? 显然,这样做会大大节约存储空间。C++编译系统正是这样做的,因此每个对象所占用的存储空间只是该对象的数据部分(虚函数指针和虚基类指针也属于数据部分)所占用的存储空间,而不包括函数代码所占用的存储空间。
看如下测试代码:
1 | class D |
问题:以上代码的输出结果是什么?
C++程序的内存格局通常分为四个区:全局数据区(data area),代码区(code area),栈区(stack area),堆区(heap area)(即自由存储区)。全局数据区存放全局变量,静态数据和常量;所有类成员函数和非成员函数代码存放在代码区;为运行函数而分配的局部变量、函数参数、返回数据、返回地址等存放在栈区;余下的空间都被称为堆区。根据这个解释,我们可以得知在类的定义时,类成员函数是被放在代码区,而类的静态成员变量在类定义时就已经在全局数据区分配了内存,因而它是属于类的。对于非静态成员变量,我们是在类的实例化过程中(构造对象时)才在栈区或者堆区为其分配内存,是为每个对象生成一个拷贝,所以它是属于对象的。
应当说明,常说的“某某对象的成员函数”,是从逻辑的角度而言的,而成员函数的存储方式,是从物理的角度而言的,二者是不矛盾的。
下面我们再来讨论下类的静态成员函数和非静态成员函数的区别:静态成员函数和非静态成员函数都是在类的定义时放在内存的代码区的,因而可以说它们都是属于类的,但是类为什么只能直接调用静态类成员函数,而非静态类成员函数(即使函数没有参数)只有类对象才能调用呢?原因是类的非静态类成员函数其实都内含了一个指向类对象的指针型参数(即this指针),因而只有类对象才能调用(此时this指针有实值)。
回答开头的问题,答案是输出“printA”后,程序崩溃。printA是成员函数,存放在代码段(.text),所以没有实例化类的时候仍然可以调用。printB是虚函数,关系到虚函数表和虚函数指针,虚函数指针存放在实例化的对象中,所以,未实例化对象时,不存在虚函数指针,所以调用虚函数会报错。类中包括成员变量和成员函数。new出来的只是成员变量,成员函数始终存在,所以如果成员函数未使用任何成员变量的话,不管是不是static的,都能正常工作。需要注意的是,虽然调用不同对象的成员函数时都是执行同一段函数代码,但是执行结果一般是不相同的。不同的对象使用的是同一个函数代码段,它怎么能够分别对不同对象中的数据进行操作呢?原来C++为此专门设立了一个名为this的指针,用来指向不同的对象。
需要说明,不论成员函数在类内定义还是在类外定义,成员函数的代码段都用同一种方式存储。不要将成员函数的这种存储方式和inline(内联)函数的概念混淆。不要误以为用inline声明(或默认为inline)的成员函数,其代码段占用对象的存储空间,而不用inline声明的成员函数,其代码段不占用对象的存储空间。不论是否用inline声明(或默认为inline),成员函数的代码段都不占用对象的存储空间。用inline声明的作用是在调用该函数时,将函数的代码段复制插人到函数调用点,而若不用inline声明,在调用该函数时,流程转去函数代码段的入口地址,在执行完该函数代码段后,流程返回函数调用点。inline与成员函数是否占用对象的存储空间无关,它们不属于同一个问題,不应搞混。
构造函数和析构函数
由系统自动执行,在程序中不可以显示地调用它们。
定义数组时,需要使用无参的构造函数。
拷贝构造函数
拷贝构造函数的参数常常是const类型本类对象的引用。当类具有指针类型的数据成员时,默认拷贝构造函数就可能产生指针悬挂问题,需要提供显示的拷贝构造函数。
析构函数
析构函数不可以重载,也就是每个类只有一个析构函数。当数据成员有指针或者指针数组时,在析构函数中应当将new出来的空间delete掉。
类的组合
前向引用声明
前向声明的类属于一个incompete type只能用于指针和引用传递,或者声明该类型为函数形参或者返回值。在提供一个完整的类声明之前,不能声明该类的对象,也不能在内联成员函数中使用该类的对象。
1 | class Fred; //前向引用声明 |
数据的共享与保护
- 这种生存期与程序的运行期相同。
- 在文件作用域中声明的对象具有这种生存期。
- 在函数内部声明静态生存期对象,要冠以关键字static。
类的静态成员
包括静态数据成员(类中所有对象间的数据共享)和静态成员函数(操作类的静态数据的函数)。
对public的静态成员的访问:
可以通过类Point::showCount();
也可以通过任何一个对象a.showCount();
。
对普通成员(非静态成员)的访问需要当前对象(*this),对静态成员的访问不需要当前对象。静态数据成员不是由构造函数创建的。