第10章 对象和类
思维导图
1.OOP特性和相关概念
1.1 接口
接口是一个共享框架,由编写类的人提供的方法组成,使得程序能够使用类对象。
1.2 抽象
类的公共接口就是对类的抽象。
1.3 用户定义类型
构成基本类型的要素:
决定数据对象需要的内存数量;存储
决定如何解释内存中的位;(long和float在内存中占用的位数相同,但是将其转化为数值的方法不同)
决定操作数据对象的方法;
C++中,用户定义类型就是指实现了抽象接口的类设计。
1.4 类规范
类声明:以数据成员的方式描述数据部分,以函数成员的方式描述公有接口。
类方法定义:描述如何实现类成员函数。
类声明提供了类的蓝图,方法定义提供了细节。
1.5 访问控制
关键字private、public描述了对于类成员的访问控制,类对象的默认访问控制是private。
使用类对象的程序都可以直接访问类的公有部分;使用类对象的程序必须通过类的公有部分间接访问类的私有成员。
类的公有部分包括公有成员函数或友元函数;类的公有部分可以直接访问类的私有成员。
类的公有部分构成了公共接口,是使用类的程序和私有成员之间的桥梁。
类的私有成员无法被程序直接访问,使得数据被隐藏。
1.6 数据隐藏
防止程序直接访问数据称为数据隐藏。
数据隐藏的优点:
1.保护数据,防止程序直接访问数据,对数据进行不适当的修改
2.方便复用,隐藏实现细节,使用时只需调用接口
3.便于维护,实现细节与接口分离,修改细节时无需改变接口,修改调用程序时无需改变实现细节
1.7 封装
将实现细节放在一起,并将它们与抽象分开称为封装。
数据隐藏将类的公共接口与私有成员相分离,是一种封装。
将类函数定义和类声明放在不同的文件中也是一种封装。
2.类与对象
2.1 类成员函数的特点
1.定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类。
Stock::update()
就是函数的限定名。名称update()
是限定名的缩写,只能在特定环境中使用。
2.成员函数可以访问类的私有成员。
3.成员函数被类对象调用,而不是直接函数调用。
2.2 内联函数
内联函数是指定义位于类声明中的函数。
如果要在类声明之外定义,则需要加上inline
限定符:
inline void Stock::set_tot()
{
...
}
内联函数的规则要求在每个使用它们的文件中都对其进行定义。最简单的方法就是将内联定义放在类的头文件中。
2.3 对象的存储
类是用户定义的类型,对象是类的实例。
每个对象都存储自己的数据,但是共享类方法。
2.4 典型的类声明格式
class className
{
private:
data member declarations
public:
member function prototypes
};
3.构造函数和析构函数
构造函数的必要性:
1.用户定义类型需要有与基本型类似的初始化方式
2.类的私有成员数据被隐藏,无法在类外被直接赋值
3.需要使用类的成员函数对对象初始化
析构函数的必要性:
清理过期的对象
3.1 构造函数
功能:创建新对象,并自动对其进行初始化。
特点:构造函数名称与类名相同,它没有返回值,也没有声明类型。
1.声明
通过函数重载,可以创建多个同名的构造函数,只要每个函数的参数列表不同。
Stock::Stock(const string & co, long n, double pr)
{
...
}
注意:在构造函数中,不能将类成员名称用作构造函数的参数名。
2.使用
//显式调用构造函数
Stock food = Stock("World Cabbage", 250, 1.25);
//隐式调用构造函数
Stock food("World Cabbage", 250, 1.25);
//构造函数与new一起使用
Stock *food = new Stock("World Cabbage", 250, 1.25);
3.默认构造函数
定义及作用:
默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。
如果没有定义任何构造函数,编译器就会提供默认构造函数。
意义:
默认构造函数的存在,使得Stock stock1;
这样的声明成为可能。否则,将会报错,原因是创建了一个没有初始化的对象。
定义默认构造函数的两种方式,两种方式不应该同时采用:
1.为已有构造函数的所有参数提供默认值
2.新创建一个没有参数的构造函数
区分各种调用方式:
Stock first; //隐式调用默认构造函数
Stock first = Stock(); //显式调用默认构造函数
Stock *first = new Stock; //隐式调用默认构造函数
Stock first("Concrete"); //调用非默认构造器
Stock second(); //声明一个返回值为Stock类型的函数
stock third; //调用默认构造器
3.2 析构函数
功能:清理过期的类对象
特点:
析构函数的名称是在类型前加上~,如Stock::~Stock()
。
析构函数没有返回值、声明类型、参数。
调用时机:
类对象过期时,会自动调用析构函数。
编译器决定在何时调用析构函数,通常不应该在代码中显式调用析构函数。
如果程序员没有提供析构函数,编译器将先隐式地声明一个默认析构函数,当发现导致对象删除的代码后,再提供析构函数的定义。
3.3 初始化与赋值中的构造与析构函数
1.初始化——隐式调用构造函数:
执行:创建一个名为stock1
的Stock对象,并将数据成员初始化为指定的值。
Stock stock1("NanoSmart", 12, 20.0);
2.初始化——显式调用构造函数:
编译器有两种方式执行下面的语法:
第一种是创建一个名为stock2
的Stock对象,并将数据成员初始化为指定的值。
第二种是使构造函数先创建一个临时对象,对临时对象初始化,然后将该临时对象相应成员的值复制到stock2
中。之后,编译器会为该临时对象调用析构函数。
Stock stock2 = Stock("Boffo Objects", 2, 20.0);
3.赋值——使用另一个对象:
默认情况下,将一个对象赋值给同一类型的另一个对象时,C++将源对象的每个数据成员复制到目标对象的相应数据成员中。
stock1 = stock2;
4.赋值——使用构造函数:
在对stock1
赋新值时,构造函数先创建一个临时对象,对临时对象初始化,然后将其复制给stock1
,随后对临时对象调用析构函数。
stock1 = Stock("Nifty Foods", 10, 50.0);
3.4 const成员函数
不修改调用对象的类方法,应该声明为const
。
//声明
void show() const;
//定义
void Stock::show() const
{
...
}
只有const
方法,才能被const
对象调用,以保证该方法不会改变调用对象的值:
const Stock land = Stock("KKK");
land.show();
4. this指针
1.必要性:
设计一个成员函数,用于查看当前对象和输入的另一个对象,并返回股价较高的那个对象的引用。
const Stock & Stock::topval( const Stock & s ) const;
如果要返回对另一个对象的引用,只需返回s
;
如何返回对当前对象的引用?
2.对该函数一些说明:
访问方式:该函数隐式的访问调用对象,显式的访问输入的另一个对象。
引用的作用:
传入参数为引用,可以提高效率。返回类型为引用,说明返回的是对象本身,而不是其副本。
const
的作用:
括号中的const
表明,该函数不会修改显式访问的对象;
括号后的const
表明,该函数不会修改隐式访问的对象;
由于该函数返回了两个const
对象之一的引用,因此函数返回类型也为const
引用。
3.this
指针的作用:
为了实现该函数,this
指针用于指向调用成员函数的对象。
this
指针中存储了对象的地址,所以*this
是调用对象本身,可以作为调用对象的别名。
每个成员函数都有一个this
指针,将其作为隐藏参数传递给方法。
4.该函数的实现:
const Stock & Stock::topval( const Stock & s ) const
{
if(s.total_val > total_val)
return s;
else
return *this;
}
5. 对象数组
Stock mystuff[4];
const int STKS = 10;
Stock stocks[STKS] = {
Stock("NanoSmart", 12.5, 20),
Stock(),
Stock("Mono", 130, 3.25)
};
上述的声明只初始化了部分元素,其余的元素将使用默认构造函数进行初始化。
初始化对象数组的方案是:
使用默认构造函数创建数组元素;
花括号中的构造函数将创建临时对象;
将临时对象的内容复制到数组元素中;
临时对象被析构函数删除。
如果要创建类对象数组,该类必须有默认构造函数。
例子:使用第4部分的函数,比较对象数组中值最大的元素:
const Stock *top = &stocks[0];
for(int i=1; i<STKS; i++)
top = &top->topval(&stocks[i]);
6. 类作用域
类成员名称的作用域都为整个类,只在该类内已知,在类外不可知。因此,可以在不同类中使用相同的类成员名而不引起冲突。
只有在类声明和成员函数定义中,可以使用非限定名。其它情况下,使用类成员名时,必须根据上下文使用直接成员运算符.
、间接成员运算符->
或作用域解析符::
。
6.1 作用域为类的常量
声明类只是描述了对象的形式,并没有创建对象。因此在创建对象前,将没有用于存储值的空间。
因此下面的做法并不可行:
private:
const int Months = 12;
double costs[Months];
两种可行的方法是:
enum{Months=12};
这种方式声明枚举目的是创建符号常量,并不会创建枚举类型的类数据成员。Months
只是一个符号名称,在作用域为整个类的代码中遇到它时,编译器将使用12替换它。
static const int Months = 12;
这将创建一个名为Months
的常量,该常量与其它静态变量存储在一起,而不存储在对象中。
7. 抽象数据类型
抽象数据类型(abstract data type,ADT)以通用的方式描述数据类型,而没有引入语言或实现细节。
抽象数据类型——栈的类声明
#ifndef STACK_H_
#define STACK_H_
typedef unsigned long Item;
class Stack
{
private:
enum {MAX=10}; //声明类作用域内的符号常量
Item items[MAX];
int top;
public:
Stack();
bool isempty() const;
bool isfull() const;
//返回值为false表示栈满,为true表示成功加入元素
bool push(const Item &item);
//返回值为false表示栈空,为true表示成功弹出元素
bool pop(Item & item);
}
#endif
私有部分指明了数据的存储方式。公有接口隐藏了数据的表示。私有部分数据存储方式的改变,不会影响到公有接口。
该类不是使用特定的类型来定义栈,而是使用通用的Item类型来描述。如果需要其它类型的栈,只需要修改typedef语句。而类声明和定义保持不变。
8. 练习题
8.1 什么是类?
类是用户定义的类型。
类声明指定了数据将如何存储,同时指定了用来访问和操作这些数据的方法。
8.2 类如何实现抽象、封装和数据隐藏?
通过类的公有接口可以操作类的对象,这就是抽象。
类可以分为公共接口和私有数据两部分。也就是说公共接口和实现细节是分开的,体现了封装。
私有数据无法直接被访问,只能通过公共接口间接访问,从而实现了数据的隐藏。
8.3 对象和类的关系是什么?
类是用户定义的类型,对象是类的一个实例。
8.4 除了是函数之外,类函数成员和类数据成员之间的区别是什么?
同一个类的多个对象实例, 每个对象都单独存储自己的数据成员,但是所有对象都共享一组成员函数。
8.5 类的构造函数在何时被调用?类的析构函数?
类的构造函数在创建对象或被显式调用时;
类的析构函数在对象过期时被调用。
8.6 什么是默认构造函数?使用默认构造函数有什么好处?
默认构造函数是没有参数或所有参数都有默认值的构造函数。
使用默认构造函数,可以使声明的对象被隐式初始化 ,使得声明数组成为可能。
8.7 this
和*this
是什么?
this
是调用对象的地址;*this
是调用对象本身, 是调用对象的别名。
只能通过公共接口间接访问,从而实现了数据的隐藏。