C++ Primer Plus学习笔记10-对象和类
1. 过程性编程和面向对象编程
采用OOP方法时,首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。最后,使用新的设计方案创建程序。
2. 抽象和类
将问题的本质特征抽象出来,并根据特征来描述解决方案。抽象是通往用户定义类型的捷径。
提供类声明(类似结构声明,包括数据成员和函数成员);实现类成员函数。
2.1 C++中的类
类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包。
一般来说,类规范由两部分组成:
- 类声明:以数据成员的方式描述数据部分,以成员函数(方法)的方式描述公有接口
- 类方法定义:描述如何实现类成员函数。
什么是接口
接口是一个共享框架,供两个系统交互时使用。对于类,称公共接口。公共指的是使用类的程序。接口让程序员能够编写与类对象交互的代码,从而让程序能够使用类对象。
访问控制
关键字private
和public
。使用类对象的程序都可以访问公有部分,但只能通过公有成员函数访问对象的私有成员。C++还提供了第三个访问控制关键字protected
因此公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。
类设计尽可能将公有接口与实现细节分开。数据隐藏不仅可以防止直接访问数u,还让开发者无需了解数据是如何被表示的。
数据项通常放在私有部分,组成类接口的成员函数放在公有部分。通常,程序员使用私有成员函数来处理不属于公有接口的实现细节。不必在类声明中使用关键字private
,这是类对象的默认访问控制。
只要类方法不修改调用对象,就应将其声明为const
void Stock::show() const // promises not to change invoking object
// stock00.h -- Stock class interface
// version 00
#ifndef STOCK00_H_
#define STOCK00_H_
#include <string>
class Stock // class definition
{
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
2.2 实现类成员函数
成员函数定义与常规函数定义相似,但是它们还有两个特殊的特征:
- 定义成员函数时,使用作用域解析运算符
::
来标识函数所属的类。 - 类方法可以访问类的
private
组件。
定义位于类声明中的函数都将自动称为内联函数。
调用成员函数时,它将使用被用来调用它的对象的数据成员。所创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员。但同一个类的所有对象共享一组类方法。
// stock00.cpp -- implementing the Stock class
// version 00
#include <iostream>
#include "stock00.h"
void Stock::acquire(const std::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){
if (num < 0)
{
std::cout << "Number of shares purchased can't be negative. "
<< "Transaction is aborted.\n";
}
else {
shares += num;
share_val = price;
set_tot();
}
}
void Stock::sell(long num, double price){
if (num < 0)
{
std::cout << "Number of shares sold can't be negative. "
<< "Transaction is aborted.\n";
}
else {
shares -= num;
share_val = price;
set_tot();
}
}
void Stock::update(double price){
share_val = price;
set_tot();
}
void Stock::show(){
std::cout << "Company: " << company << endl;
std::cout << "\tShares: " << shares << endl;
std::cout << "\tShare Price: $" << share_val
<< "\tTotal Worth: $" << total_val << endl;
}
2.3 使用类
C++的目标是使得使用类与使用基本的内置类型尽可能相同。
// usestck0.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();
return 0;
}
OOP程序员常依照客户/服务器模型来讨论程序设计。客户是使用类的程序;类声明构成服务器,它是程序可以使用的资源。客户只能通过以公有方式定义的接口使用服务器,这意味着客户唯一的责任是了解接口。服务器的责任是确保服务器根据该接口可靠并准确地运行。
2.4 修改实现
修改方法的实现时,不应影响客户程序的其他部分。
3. 类的构造函数和析构函数
常规的初始化语法不适用于类型,原因在于数据部分的访问状态是私有的。一般来说,最好在创建对象时对它进行初始化。为此,C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象并赋值,其名称与类名相同。构造函数实际上没有声明类型。
程序声明对象时,将自动调用构造函数。
构造函数的参数表示的不是类成员,而是赋给类成员的值。因此参数名不能与类成员相同。为避免以上混乱,一种常见的做法是在数据成员名中使用m_
前缀;另一种做法是在成员名中使用后缀_
。
3.1 声明和定义构造函数
// 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::cout << "Number of shares can't be negative; "
<< company << " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
3.2 使用构造函数
C++提供了两种使用构造函数初始化对象的方式:
Stock food = Stock("World Cabbage", 250, 1.25); // 显式调用
Stock garment("Furry Mason", 50, 2.5); // 隐式调用
Stock *pstock = new Stock("Electroshock Games", 18, 19.0);
每次创建类对象,C++都使用类构造函数。但是无法使用对象来调用构造函数。
3.3 默认构造函数
在未提供显示初始值时,用来创建对象的构造函数。当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。为类定义了构造函数后,程序员就必须为它提供默认构造函数。
3.4 析构函数
对象过期时,程序将自动调用一个特殊的成员函数——析构函数。它完成清理工作。
析构函数的名称是类名前加~
,它没有返回值和声明类型且没有参数。
什么时候调用析构函数由编译器决定,通常不应在代码中显式地调用析构函数。如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。
3.5 改进Stock
类
// stock10.h -- Stock Class definition with constructors, destructor added
#ifndef STOCK10_H_
#define STOCK10_H_
#include <string>
class Stock // class definition
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot() {total_val = shares * share_val;}
public:
// two constructors
Stock(); // default constructor
Stock(const std::string & co, long n=0, double pr=0.0);
~Stock(); // noisy destructor
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
}; // note semicolon at the end
#endif
// stock10.cpp -- Stock class with constructors, destructor added
#include <iostream>
#include "stock10.h"
// Constructors (verbose versions)
Stock::Stock() // default constructor
{
std::cout << "Default constructor called\n";
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
Stock::Stock(const std::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();
}
// class destructor
Stock::~Stock() // verbose class destructor
{
std::cout << "Bye, " << company << "!\n";
}
//other methods
void Stock::buy(long num, double price){
if (num < 0)
{
std::cout << "Number of shares purchased can't be negative. "
<< "Transaction is aborted.\n";
}
else {
shares += num;
share_val = price;
set_tot();
}
}
void Stock::sell(long num, double price){
if (num < 0)
{
std::cout << "Number of shares sold can't be negative. "
<< "Transaction is aborted.\n";
}
else {
shares -= num;
share_val = price;
set_tot();
}
}
void Stock::update(double price){
share_val = price;
set_tot();
}
void Stock::show(){
std::cout << "Company: " << company << endl;
std::cout << "\tShares: " << shares << endl;
std::cout << "\tShare Price: $" << share_val
<< "\tTotal Worth: $" << total_val << endl;
}
// usestok1.cpp -- using the Stock class
// compile with stock10.cpp
#include <iostream>
#include "stock10.h"
int main()
{
using std::cout;
cout << "Using constructors to create new objects\n";
Stock stock1("NanoSmart", 12, 20.0);
stock1.show();
Stock stock2 = Stock("Boffo Objects", 2, 2.0);
stock2.show()
Stock hot_tip = {"Derivatives Plus Plus", 100, 34.0};
cout << "Assigning stock1 to stock2:\n";
stock2 = stock1;
cout << "Listing stock1 and stock2:\n";
stock1.show();
stock2.show();
cout << "Using a constructor to reset an object\n";
stock1 = Stock("Nifty Foods", 10, 40.0); // temporary object
cout << "Revised stock1:\n";
stock1.show()
cout << "Done\n";
return 0;
}
4. this
指针
使用this
指针指向用来调用成员函数的对象(this
被作为隐藏参数传递给方法)。如果方法需要引用整个调用对象,则可以使用表达式*this
;在函数的括号后面使用const
限定符将this
限定为const
。
// method prototype
const Stock & topval(const Stock & s) const;
// method implementation
const Stock & Stock::topval(const Stock & s) const{
if (s.total_val > total_val)
return s;
else
return *this;
}
5. 对象数组
声明对象数组的方法与声明标准类型数组相同。
初始化对象数组的方案:首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。
6. 类作用域
在类中定义的名称的作用域都为整个类。类作用域意味着不能从外部直接访问类的成员,公有成员函数也是如此。要调用公有成员函数,必须通过对象。
6.1 作用域为类的常量
声明类只是描述了对象的形式,并没有创建对象。因此在创建对象前,没有用于存储值的空间。
- 在类中声明一个枚举,可以使用枚举为整形常量提供作用域为整个类的符号名称。以这种方式声明枚举并不会创建类数据成员。
- 使用关键字
static
class Bakery{
private:
enum {Months=12};
static const int Months2=12;
double costs[Months];
double costs2[Months2];
}
6.2 作用域内枚举(C++11)
传统枚举定义的枚举量可能发生冲突。为避免该问题,C++11提供一种新枚举,枚举量作用域为类。另外, C++11还提高了作用域内枚举的类型安全(不能隐式地转换成整型)。
enum egg {Small, Medium, Large, Jumbo};
egg choice = egg::Large //需要用枚举名限定枚举量
enum class : short pizza {Small, Medium, Large, Xlarge}; //底层类型指定
7. 抽象数据类型(ADT)
Abstract Data Type以通用的方式来描述数据类型。类概念非常适合与ADT方法。
//stack.h -- class definition for the stack ADT
#ifndef STACK_H_
#define STACK_H_
typedef unsigned long Item;
class Stack{
private:
enum {MAX = 10}; // constant specific to class
Item items[MAX]; // holds stack items
int top; // index for top stack item
public:
Stack();
bool isempty() const;
bool isfull() const;
// push() returns false if stack already is full, true otherwise
bool push(const Item &item); // add item to stack
// pop() returns false if stack already is empty, true otherwise
bool pop(Item & item); // pop top into item
};
#endif
// stack.cpp -- Stack member functions
#include "stack.h"
Stack::Stack(){ // create an empty stack
top = 0;
}
bool Stack::isempty() const {
return top == 0;
}
bool Stack::isfull() const {
return top == MAX;
}
bool Stack::push(const Item &item){
if (top < MAX){
items[top++] = item;
return true;
}
else
return false;
}
bool Stack::pop(Item &item){
if (top == 0)
return false;
else{
item = items[--top];
return true;
}
}
// stacker.cpp -- testing the Stack class
#include <iostream>
#include <cctype> // or ctype.h
#include <stack.h>
int main(){
using namespace std;
Stack st; // create an empty stack
char ch;
unsigned long po;
cout << "Please enter A to add a purchase order,\n"
<< "P to process a PO, or Q to quit.\n"
while(cin >> ch && toupper(ch) != 'Q'){
while(cin.get() != '\n')
continue;
if(!isalpha(ch)){
cout << '\a';
continue;
}
switch(ch){
case 'A':
case 'a': cout << "Enter a PO number to add: ";
cin >> po;
if (st.isfull())
cout << "stack already full\n";
else
st.push(po);
break;
case 'p':
case 'P': if (st.isempty()):
cout << "stack already empty\n";
else{
st.pop(po);
cout << "PO #" << po << " popped\n";
}
break;
}
cout << "Please enter A to add a purchase order.\n"
<< "P to process a PO, or Q to quit.\n";
}
cout << "Bye\n";
return 0;
}