最重要的OOP特性:
抽象
封装和数据隐藏
多态
继承
代码的可重用性
本章:
首先,介绍类
解释抽象、封装、数据隐藏,并演示类是如何实现这些特性的
如何定义类、如何为类提供公有部分和私有部分以及如何创建使用类数据的成员函数
构造函数和析构函数
this指针
运算符重载(另一种多态)和继承
10.1 过程性编程和面向对象编程
采用OOP,首先从用户角度考虑对象--描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。最后,使用新的设计方案创建出程序。
10.2 抽象和类
10.2.1 类型是什么
指定基本类型完成了三项工作:
决定数据对象需要的内存数量;
决定如何解释内存中的位(long和float在内存中占用的位数相同,但将它们转换为数值的方法不同);
决定可使用数据对象执行的操作或方法
对于内置类型来说,有关操作的信息被内置到编译器中。但在C++定义用户自定义类型时,必须自己提供这些信息。
10.2.2 C++中的类
定义类,类规范由两个部分组成:
类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口;
类方法定义:描述如何实现类成员函数;
简明的说,类声明提供了类的蓝图,而方法定义则提供了细节。
C++程序员将接口(类定义)放在头文件中,并使用了#ifndef等来防止多次包含同一个文件,并将实现(类方法的代码)放在源代码文件中。
遵循一种常见但不通用的约定--类名首字母大写。
类的成员函数可以就地定义,也可以使用原型表示;
1、访问控制
public和private,使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数(或友元函数)来访问对象的私有成员。
因此,公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。
类设计尽可能将公有接口与实现细节分开:
将实现细节放在一起并将它们与抽象分开被称为封装;
数据隐藏是一种封装;
将实现的细节隐藏在私有部分中,也是一种封装;
将类函数定义和类声明放在不同的文件中,也是一种封装;
2、控制对成员的访问:公有还是私有
由于隐藏数据是OOP主要的目标之一,因此:
数据项通常放在私有部分;
组成类接口的成员函数放在公有部分,否则,就无法从程序中调用这些函数;
也可以把成员函数放在私有部分中,不能直接从程序中调用这种函数,但公有方法却可以使用它们。通常,程序员使用私有成员函数来处理不属于公有接口的实现细节。
不必在类声明中使用关键字private,因为默认访问控制;
类和结构:
唯一区别:结构的默认访问类型是public,而类为private;
C++通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象(常被称为普通老式数据)
10.2.3 实现类成员函数
成员函数定义与常规函数定义非常相似,两个特殊的特征:
定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类;
类方法可以访问类的private组建;
实现类方法,将它们放在一个独立的实现文件中,因此需要包含头文件stock00.h,让编译器能够访问类定义。
1、成员函数说明
4个成员函数设置或重新设置了total_val成员值,并非将计算代码编写4次,而是让每个函数都调用set_tot()函数。
由于set_tot()只是实现代码的一种方式,而不是公有接口的组成部分,因此这个类将其声明为私有成员函数。
2、内联方法
其定义位于类声明中函数都将自动成为内联函数,因此set_tot是一个内联函数,类声明常将短小的成员函数作为内联函数。
如果愿意,也可以在类声明之外定义成员函数,并使其成为内联函数。为此,只需在类实现部分定义函数时使用inline限定符即可。
class Stock{ private: set_tot(); public:}inline void Stock::set_tot() { total_val = shares * share_val;}
内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。确保内联定义对多个文件程序中的所有文件都可用的、最简便的方法是:
将内联定义放在类的头文件中。
根据改写规则,在类声明定义方法等同于用原型替换方法定义,然后再类声明的后面将定义改写为内联函数。这与上述的代码是等价的。
3、方法使用哪个对象
如何创建对象-->声明类变量:Stock kate, joe;
如何使用对象的成员函数。和使用结构成员一样,通过成员运算符:
kate.show();joe.show();
注意:调用成员函数时,它将使用被用来调用它的对象的数据成员。
同样,函数调用kate.sell()在调用set_tot()函数时。相当于调用kate.set_tot(),这样该函数将使用kate对象的数据。
所创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法,即每种方法只有一个副本。
10.2.4 使用类
程序清单10.3提供了一个使用上述接口和实现文件的程序,它创建了一个名为fluffy_the_cat的Stock对象。
要编译该程序,可使用用于多文件程序的方法。具体地说,将其与stock00.cpp一起编译,并确保stock00.h位于当前文件夹中。
stock00.h#ifndef SOCK00_H_#define SOCK00_H_#includeclass Stock { 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(); };#endif=====================stock00.cpp#include #include "stock00.h"void Stock::acquire(const std:: string &co, long n, double pr) { company = co; if(n < 0) { std::cout << company << "set shares to 0"<< std::endl; shares = 0; }else shares = n; share_val = pr; set_tot();}void Stock::buy(long num, double price) { if (num < 0) { std::cout << "can't be negative. " << std::endl; } else { shares += num; share_val = price; set_tot(); }}void Stock::sell(long num, double price) { using std::cout; using std::endl; if (num < 0) cout << "can't be negative" << endl; else if(num > shares) cout << "can't ge shares'" << endl; else { shares -= num; share_val = price; set_tot(); }}void Stock::update(double price) { share_val = price; set_tot();}void Stock::show() { using namespace std; cout << "company:" << company << endl; cout << "shares:" << shares << endl; cout << "share_val:" << share_val << endl; cout << "total_val:" << total_val << endl;}====================usestack00.cpp#include #include "stack00.h"int main(){ Stock fluffy_the_cat; fluffy_the_cat.acquire("", 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(30000, 40.125); fluffy_the_cat.show(); fluffy_the_cat.sell(30000, 0.125); fluffy_the_cat.show(); return 0;}
10.2.5 修改实现
前面的程序中,想修改数字的格式,现在可以改进实现,但保持接口不变。
std::cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
这设置了cout对象的一个标记,命令cout使用定点表示法。
下面的语句导致cout在使用定点表示法时,显示三位小数:std::cout.precision(3);
修改show(),修改方法实现时,不应影响客户程序的其他部分,上述格式修改将一直有效,直到您再次修改,因此它们可能影响客户程序中的后续输出。因此,show()应重置格式信息,使其恢复到自己被调用前的状态。
std::streamsize prec = std::cout.precision(3);...std::cout.precision(prec);// store original flagsstd::ios_base::fmtflags orig = std::cout.setf(std::ios_base::fixed);...// reset to stored valuesstd::cout.setf(orig, std::ios_base::floatfield);
ios_base类是在名称空间std中定义的,fmtflags是在ios_base类中定义的一种类型。
其次,orig存储了所有的标记,而重置语句使用这些信息来重置floatfield。而floatfiled包含定点表示法标记和科学表示法标记。
void Stock::show() { using std::cout; using std::endl; using std::ios_base; cout << "begin -----------------------------" << endl; cout << "company:" << company << endl; cout << "shares:" << shares << endl; cout << "share_val:" << share_val << endl; cout << "total_val:" << total_val << endl; // set format to #.### ios_base::fmtflags orig = cout.setf(ios_base::fixed, ios_base::floatfield); std::streamsize prec = cout.precision(3); cout << "company: " << company << endl; cout << "shares: " << shares<< endl; // set format to #.## cout.precision(2); cout << "share price: " << total_val<< endl; // restore original format cout.setf(orig, ios_base::floatfield); cout.precision(prec); cout << "end -----------------------------" << endl;}
10.2.6
指定类设计的第二步是实现类成员函数。可在类声明中提供完整的函数定义,而不是函数原型,但是通常的做法是单独提供函数定义(除非函数很小。
在这种情况下,需要使用作用域解析运算符来指出成员函数是属于哪个类。
eg: Bozo::Retort(),而名称Retort()是限定名的缩写,只能在某些特定的环境中使用,如类方法的代码中。
10.3 类的构造函数和析构函数
C++的目标之一是让使用类对象就像使用标准类型一样,然而,到现在为止,本章提供的代码还不能让您像初始化int或结构那样来初始化Stock对象。
eg:结构 thing
thing amabob = {"aa", -23};Stock hot = {"", 200, 50.25};
不能像上面这样初始化Stock对象的原因在于,数据部分的访问状态是私有的,这意味着程序不能直接访问数据成员。您已经看到,程序只能通过成员函数来访问数据成员,因此需要设计合适的成员函数,才能成功地将对象初始化。
一般来说,最好是在创建对象时对它进行初始化。
C++提供了类构造函数,专门用于构造新对象。
10.3.1 声明和定义构造函数
为Stock对象提供3个参数,有两个参数为默认:
Stock(const string & co, long n=0, double pr = 0.0)
注意:没有返回类型,原型位于类声明的公有部分。
下面是构造函数的一种可能定义:
Stock::Stock(const string & co, long n = 0, double pr = 0.0) { company = co; if(n < 0) { std::cout << company << "set shares to 0"<< std::endl; shares = 0; }else shares = n; share_val = pr; set_tot();}
上述代码和acquire相同,区别在于,程序声明对象时,将主动调用构造函数。
成员函数和参数,
构造函数的参数名和类成员不能相同,否则,会shares = shares;
为避免这种混乱,可采取:
* 数据成员名使用m_前缀;
* 数据成员名使用后缀_;
10.3.2 使用构造函数
C++提供两种使用构造函数来初始化对象的方式,
* 显示地调用构造函数:Stock food = Stock("", 250, 1.25);
* 隐式地调用构造函数:Stock garment = Stock("", 50, 2.5);这种方式更紧凑,与此等价:Stock garment = Stock("", 50, 2.5);
每次创建类对象(甚至使用new动态分配内存)时,C++都是用类构造函数,下面是将构造函数与new一起使用的方法:
Stock *pstock = new Stock("", 50, 2.5);
在此情况下,对象没有名称,但可以使用指针来管理该对象,在11章进一步讨论对象指针。
构造函数不同于其他类方法。
无法使用对象来调用构造函数,因此在构造函数构造出对象之前,对象是不存在的,因此构造函数被用来创建对象,而不能通过对象来调用。
10.3.3 默认构造函数
默认构造函数时在未提供显式初始值时,用来创建对象的构造函数。用于下面这种声明的构造函数:
Stock fluffy_the_car;
若没有提供任何构造函数,C++将自动提供默认构造函数,它是默认构造函数的隐式版本,不做任何工作。eg:
Stock::Stock(){}
默认构造函数没有参数,因此声明中不包含值。
当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。为类定义了构造函数后,程序员就必须为它提供默认构造函数。
这么做的原因可能是禁止创建未初始化的对象。
若要创建对象,而不显式的初始化,则必须定义一个不接受任何参数的默认构造函数。
定义默认构造函数的方式:
* 给已有构造函数的所有参数提供默认值Stock(cosnt string & co = "Error", int n = 0, double prt = 0.0)
* 通过函数重载来定义另一个构造函数--没有参数的构造函数Stock()
由于只能有一个默认构造函数,因此不要同时采用这两种方式。
实际上,通常应初始化所有的对象,以确保所有成员一开始就有已知的合理值。
因此,用户定义的默认构造函数通常给所有成员提供隐式初始值。
使用上述任何一种方式创建了默认构造函数后,便可以声明对象变量,而不对它们进行显式初始化:
Stock first;Stock first = Stock();Stock *prelief = new Stock();
10.3.4 析构函数
析构函数完成清理工作。比如:
* 构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存
构造函数名称:在类名前加上~,没有返回值和声明类型。
~Stock();
什么时候应调用析构函数呢?由编译器决定,通常不应在代码中显式地调用析构函数(例外情况查看12章)。
* 如果创建的是静态存储类对象,则其析构函数将在程序结束时自动被调用。
* 如果创建的是自动存储类对象,则其析构函数将在程序执行完代码块时自动被调用。
* 如果对象是通过new创建的,则它将驻留在栈内存或自由存储区中,当使用delete来释放内存时,其析构函数将自动被调用。
最后,程序可以创建临时对象来完成特定的操作,这种情况下,程序将在结束对该对象的使用时自动调用其析构函数。
若程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。
10.3.5 改进Stock类
改造stock00.h stock00.cpp和usestock00.cpp
1、头文件
将构造函数和析构函数的原型加入到原来的类声明中。
2、实现文件
3、客户文件
stock10.h#ifndef SOCK00_H_#define SOCK00_H_#include#include class Stock { private: std::string company; long shares; double share_val; double total_val; void set_tot() { total_val = shares * share_val;} public: Stock(const std::string & co, long n = 0, double pr = 0.0); Stock(); ~Stock(); void buy(long num, double price); void sell(long num, double price); void update(double price); void show(); };#endif=================stock10.cpp#include #include "stock10.h"Stock::Stock() { std::cout << "Default constructor called. " << std::endl; company = "no name"; shares = 0; share_val = 0.0; total_val = 0.0;}Stock::~Stock(){ std::cout << company << "Bye" << std::endl;}void Stock::buy(long num, double price) { if (num < 0) { std::cout << "can't be negative. " << std::endl; } else { shares += num; share_val = price; set_tot(); }}void Stock::sell(long num, double price) { using std::cout; using std::endl; if (num < 0) cout << "can't be negative" << endl; else if(num > shares) cout << "can't ge shares'" << endl; else { shares -= num; share_val = price; set_tot(); }}void Stock::update(double price) { share_val = price; set_tot();}void Stock::show() { using std::cout; using std::endl; using std::ios_base; cout << "begin -----------------------------" << endl; cout << "company:" << company << endl; cout << "shares:" << shares << endl; cout << "share_val:" << share_val << endl; cout << "total_val:" << total_val << endl; // set format to #.### ios_base::fmtflags orig = cout.setf(ios_base::fixed, ios_base::floatfield); std::streamsize prec = cout.precision(3); cout << "company: " << company << endl; cout << "shares: " << shares<< endl; // set format to #.## cout.precision(2); cout << "share price: " << total_val<< endl; // restore original format cout.setf(orig, ios_base::floatfield); cout.precision(prec); cout << "end -----------------------------" << endl;}Stock::Stock(const std::string & co, long n, double pr) { company = co; if(n < 0) { std::cout << company << "set shares to 0"<< std::endl; shares = 0; }else shares = n; share_val = pr; set_tot();}=============usestock10.cpp#include #include "stock10.h"int main(){ { using std::cout; using std::endl; cout << "Using constructors to create new object." << endl; Stock stock1("NanoSmart", 12, 20.0); stock1.show(); Stock stock2("Boffo objects", 2, 2.0); stock2.show(); cout << "Assigning stock1 to stock2." << endl; stock2 = stock1; cout << "Listening stock1 and stock2." << endl; stock1.show(); stock2.show(); cout << "Using a constructor to reset an object" << endl; stock1 = Stock("", 10, 50.0); cout << "Revised stock1." < << "Done" << endl; } return 0;}
4、程序说明
Stock stock1("NanoSmart", 12, 20.0);
Stock stock2 = Stock("Boffo objects", 2, 2.0)
C++标准允许编译器使用两种方式来执行第二种语法。
使其行为和第一种语法完全相同。
允许构造函数来创建一个临时对象,然后将该临时对象复制到stock2中,并丢弃它。如果这种方式,则将为临时对象调用西沟函数.
生成上述输出的编译器可能立即删除临时对象,但也可能会等一段时间,在这种情况下,析构函数的消息会过一段时间才显示。
下面的语句讲一个对象赋给同类型的另一个对象:
stock2 = stock1;
与给结构赋值一样,在默认情况下,给类对象赋值时,将把一个对象的成员复制给另一个。
构造函数不仅仅可用于初始化新对象,还可以将新值赋给它:
stock1 = Stock("Nifty Poods", 10, 50.0);
这通过让构造程序创建一个新的、临时的对象,然后将其内容复制给stock1来实现的。随后程序调用析构函数,以删除该临时对象。
main结束,局部变量stock1和stock2将消失。由于这种自动变量被放在栈中,因此最后创建的对象将最先被删除。
输出表明,下面两条语句有根本性的差别:
Stock stock2 = Stock("Boffo Objects", 2, 2,0);stock1 = Stock("Nifty Foods", 10, 50.0);
第一条语句是初始化,它创建有指定值的对象,可能会创建临时对象(也可能不会);
第二条语句是赋值。像这样在赋值语句中使用构造函数总会导致在赋值前创建一个临时对象。
提示:如果既可以通过初始化,也可以通过赋值来设置对象的值,则应采用初始化方式。通常这种方式的效率更高。
5、C++11列表初始化
6、const成员函数
请看:
const Stock land = Stock("aaa");land.show();
对于当前的C++来说,编译器将拒绝第二行。
为什么呢?show()的代码无法确保调用对象不被修改--调用对象和const一样,不应被修改。
需要一种新的语法--保证函数不会修改调用对象。
解决办法:将const关键字放在函数的括号后面。
void show() const;
同样,函数定义的开头应像这样:
void stock::show() const
以这种方式声明和定义的类函数被称为const成员函数。就像应尽可能将const引用和指针用作函数形参一样,只要类方法不修改调用对象,就应将其声明为const。从现在开始,我们将遵守这一规则。