对象和类
主要内容:
- 面向对象编程与过程性编程
- 类的定义和实现
- 公有类和私有类
- 类的数据成员
- 类方法
- 创建和使用类对象
- 类的构造函数和析构函数
- const成员函数
- this指针
- 创建对象数组
- 类作用域
- 抽象数据类型
面对对象编程(oop)的特性:
- 抽象
- 封装和数据隐藏
- 多态
- 继承
- 代码的可重用性
面对对象编程
采用OOP的方法,首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。最后,使用新的设计方案创建出程序。
抽象和类
在计算中,为了根据信息与用户之间的接口来表示它,抽象是至关重要的。也就是说,将问题的本质特征抽象出来,并根据特征来描述解决方案。抽象是通往用户定义类型的捷径,在C++中,用户定义类型指的是实现抽象接口的类设计。
类型
指定基本类型完成了三项工作:
- 决定数据对象需要的内存数量;
- 决定如何解释内存中的位(long和float在内存中占用的位数相同,但将它们转换为数值的方法不同);
- 决定可使用数据对象执行的操作或方法。
C++中的类
类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包。例如一个股票的类。
可对股票进行的操作:1.获得股票 2.增持 3.卖出股票 4.更新股票价格 5.显示关于所持股票的信息。
存储的股票信息:1.公司名称 2.所持股票的数量 3.每股的价格 4.股票总值。
然后就可以定义类了,一般来说,类规范由两个部分组成:
1.类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口。
2.类方法定义:描述如何实现类成员函数。
简单的说,类声明提供了类的蓝图,而方法定义则提供了细节。
为开发一个类并编写使用它的程序,需要完成多个步骤。通常,C++程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。
常用的方法是将类名首字母大写。下面是一个类定义的例子:
// stock00.h -- Stock class interface
// version 00
#ifdef STOCK00_H_
#define STOCK00_H_
#include <string>
class Stock // class declaration
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot() {total_val = shares * share_val; }
public:
void acquire(const std::string & co, long n, double pr);
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
}; // note semicolon at the end
#endif
创建两个对象(实例):
Stock sally;
stock solly;
1.访问控制
关键字private和public描述了对类成员的访问控制。使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数(或友元函数)来访问对象的私有成员。例如,要修改Stock类的shares成员,只能通过Stock的成员函数。因此,公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。防止程序直接访问数据被称为数据隐藏。C++还提供了第三个访问控制关键字protected。
类设计尽可能将公有接口与实现细节分开。公有接口表示设计的抽象组件。将实现细节放在一起并将它们与抽象分开被称为封装。数据隐藏(将数据放在类的私有部分中)是一种封装,将实现的细节隐藏在私有部分中,就像Stock类对set_tot()所做的那样,也是一种封装。封装的另一个例子是,将类函数定义和类声明放在不同的文件中。
数据隐藏不仅可以防止直接访问数据,还让开发者(类的用户)无需了解数据是如何被表示的。我们只需要知道成员函数接受什么样的参数以及返回什么样类型的值。原则是将实现细节从接口设计中分离出来。如果以后找到了更好的、实现数据表示或成员函数细节的方法,可以对这些细节进行修改,而无需修改程序接口,这使程序维护起来更容易。
2.控制对成员的访问:公有还是私有
无论类成员是数据成员还是成员函数,都可以在类的共有部分或私有部分中声明它。但由于隐藏数据是OOP主要的目标之一,因此数据项通常放在私有部分,组成类接口的成员函数放在公有部分;否则,就无法从程序中调用这些函数。正如Stock声明所表明的,也可以把成员函数放在私有部分中。不能直接从程序中调用这种函数,但公有方法却可以使用它们。通常,程序员使用私有成员函数来处理不属于公有接口的实现细节。
不必在类声明中使用关键字private,因为这是类对象的默认访问控制:
class World
{
float mass; // private by default
char name[20]; // private by default
public:
void tellall(void);
...
}
但是有时为了强调数据隐藏的概念,也会显示使用private。
实现类成员函数
还需要创建类描述的第二部分:为那些由类声明中的原型表示的成员函数提供代码。成员函数定义与常规函数定义非常相似,它们有函数头和函数体,也可以有返回类型和参数。但是它们还有两个特殊的特征:
- 定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类;
- 类方法可以访问类的private组件。
首先,成员函数的函数头使用作用域运算符解析(::)来指出函数所属的类。例如,update()成员函数的函数头如下:
void Stock::update(double price)
这种表示法意味着我们定义的update()函数时Stock类的成员。这不仅将update()标识为成员函数,还以为这我们可以将另一个类的成员函数也命名为update()。作用域解析运算符确定了方法定义对应的类的身份。我们说,标识符update()具有类作用域,Stock类的其它成员函数不必使用作用域解析运算符,就可以使用update()方法,这是因为它们属于用一个类,因此update()是可见的。然而,在类声明和方法定义之外使用update()时,需要采取特殊的措施。
下面是类方法的实现:
// stock00.cpp -- impementing the Stock class
// version 00
#include <iostream>
#include "stock00.h"
void Stock::acquire(const str::string & co, long n, double pr)
{
company = co;
if (n < 0)
{
std::cout << "Number of shares can't be negative;"
<< company << "shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
void Stock::buy(long num, double price)
{
...
}
void Stock::sell(long num, double price)
{
...
}
void Stock::update(double price)
{
...
}
void Stock::show()
{
...
}
内联方法:
其定义位于类声明中的函数都将自动成为内联函数,因此Stock::set_tot()是一个内联函数。类声明常将短小的成员函数作为内联函数。如果愿意,也可以在类声明之外定义成员函数,并使其成为内联函数。为此,只需要在类实现部分中定义函数时使用inline限定符即可:
class Stock
{
private:
...
void set_tot();
public:
...
}
inline void Stock::set_tot()
{
total_val = shares * share_val;
}
方法使用哪个对象
如何将类方法应用于对象。首先看看如何创建对象:
Stock kate, joe;
这将创建两个Stock类对象,一个为kate,一个为joe。
使用对象的成员函数:
kate.show(); // the kate object calls the member function
joe.show(); // the joe object calls the member function
第一条语句调用kate对象的show()成员,这意味着show()方法将把shares解释为kate.shares,将share_val解释为kate.share_val。同样,函数调用joe.show()使show()方法将shares和share_val分别解释为joe.share和joe.share_val。
注意: 调用成员函数时,它将使用被用来调用它的对象的数据成员。
所创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享一组类方法,即每种方法只有一个副本。例如,假设kate和joe都是Stock对象,则kate.shares将占据一个内存块,而joe.shares占用另一个内存块,但kate.show()和joe.show()都调用同一个方法,也就是说,它们将执行同一个代码块,只是将这些代码块用于不同的数据。在OOP中,调用成员函数被称为发送消息,因此将同样的消息发送给两个不同的对象将调用同一个方法,但该方法被用于两个不同的对象。
使用类
知道如何定义类及其方法后,创建一个程序来使用类,它创建并使用类对象。C++的目标是使得使用类与使用基本的内置类型(如int和char)尽可能相同。要创建类对象,可以声明类变量,也可以使用new为类对象分配存储空间。可以将对象作为函数的参数和返回值,也可以将一个对象赋给另一个。C++提供了一些工具,可用于初始化对象、让cin和cout识别对象,甚至在相似的类对象之间进行自动类型转换。
下面时一个类使用的示例程序:
// usestock0.cpp -- the client program
// compile with stock00.cpp
#include <iostream>
#include "stock00.h"
int main()
{
Stock fluffy_the_cat;
fluffy_the_cat.acquire("NanoSmart", 20, 12.50);
fluffy_the_cat.show();
fluffy_the_cat.buy(15, 18.125);
fluffy_the_cat.show();
fluffy_the_cat.sell(400, 20.00);
fluffy_the_cat.show();
fluffy_the_cat.buy(300000, 40.125);
fluffy_the_cat.show();
fluffy_the_cat.sell(300000, 0.125);
fluffy_the_cat.show();
return 0;
}
要使用新类型,最关键的是要了解成员函数的功能,而不必考虑其实现细节。
修改类时一定要注意避免修改接口,只对成员函数内部进行修改,不要修改接口定义。
类的构造函数和析构函数
对于Stock类,还有一些其它工作要做。应为类提供被称为构造函数和析构函数的标准函数。
C++的目标之一就是让使用类对象就像使用标准类型一样,所以还缺少一个对类对象进行初始化的过程,而常规的初始化语法不适用于类,因为类的数据部分的访问状态是私有的,所以程序不能直接访问数据成员。程序只能通过成员函数来访问数据成员,因此需要设计合适的成员函数,才能成功的将对象初始化。
一般来说,最好是在创建对象时对它进行初始化。C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值赋给它们的数据成员。更准确的说,C++为这个成员函数提供了名称和使用语法,而程序员需要提供方法定义。名称与类名相同。例如,Stock类一个可能的构造函数是名为Stock()的成员函数。构造函数的原型和函数头有一个有趣的特征——虽然没有返回值,但没有被声明为void类型。实际上,构造函数没有声明类型。
声明和定义构造函数
下面创建Stock的构造函数。由于需要为Stock对象提供3个值,因此应为构造函数提供3个参数。(第4个值,total_val成员,是根据shares和share_val计算得到,所以不需要)程序员可能只想设置company成员,而将其它值设置为0;这可以使用默认参数来完成。因此原型如下:
// constructor prototype with some default arguments
Stock(const string & co, long n = 0, double pr = 0.0);
下面个是构造函数的另一种可能定义:
// constructor definition
Stock::Stock(const string & co, long n, double pr)
{
company = co;
if (n < 0)
{
std::cerr << "Numer of share can't be negative;"
<< company << " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
上述代码和前面的acquire()函数相同。区别在于,程序声明对象时,将自动调用构造函数。
注意:
不可以将类成员名称作为构造函数的参数名,为了避免这种混乱,一种常见的做法是在数据成员名前使用m_前缀:
class Stock
{
private:
string m_company;
long m_shares;
...
}
另一种常见的做法是,在成员名种使用后缀_:
class Stock
{
private:
string company_;
long shares_;
...
}
使用构造函数
C++提供了两种使用构造函数来初始化对象的方式。第一种方式是显式的调用构造函数:
Stock food = Stock("World Cabbage", 250, 1.25);
另一种是隐式的调用构造函数:
Stock garment("Furry Mason", 50, 2.5);
这种格式更紧凑,它与下面的显示调用等价:
Stock garment = Stock("Furry Mason",50, 2.5 );
每次创建类对象(甚至使用new动态分配内存)时,C++都使用类构造函数。下面是将构造函数与new一起使用的方法:
Stock *pstock = Stock("Electroshock Games", 18, 19.0);
这条语句创建一个Stock对象,将其初始化为参数提供的值,并将该对象的地址赋给pstock指针。在这种情况下,对象没有名称,但可以使用指针来管理该对象。
构造函数的使用方式不同于其它类方法,它无法被对象调用,因为在构造函数构造出对象之前,对象是不存在的。因此构造函数被用来创建对象, 而不能通过对象来调用。
默认构造函数
默认构造函数是在未提供显式初始值时,用来创建对象的构造函数,也就是说,它是用于下面这种声明的构造函数:
Stock fluffy_the_cat; // use the default constructor
也就是说,如果没有提供任何构造函数,则C++将自动提供默认构造函数。定义默认构造函数有两种方法。
第一种:给已有构造函数的所有参数提供默认值:
Stock(const string & co = "Error", int n = 0, double pr = 0.0);
第二种:通过函数重载来定义另一个构造函数——一个没有任何参数的构造函数:
Stock();
// definition
Stock::Stock()
{
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
声明对象时有三种形式使用默认构造函数:
1. Stock first; // calls the default constructor implicity
2. Stock first = Stock(); // call it explicity
3. Stock *prelief = new Stock; // calls it implicity
析构函数
用构造函数创建对象后,程序员负责跟踪该对象,知道其过期为止。对象过期时,程序将自动调用一个特殊的成员函数——析构函数。析构函数的名称是在类名前加上~。因此,Stock类的析构函数为~Stock()。另外,和构造函数一样,析构函数也可以没有返回值和声明类型。与构造函数不同的是,析构函数没有参数,因此Stock析构函数的原型必须是这样的:
~Stock();
由于Stock的析构函数不承担任何重要的工作,因此可以将它编写为不执行任何操作的函数:
Stock::~Stock()
{
}
如果程序员没有提供析构函数,编译器将隐式的声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。
如果构造函数使用了new,那么必须提供使用delete的析构函数。