文章目录
前言
类的基础思想是数据结构和封装。数据抽象是一种依赖于接口和实现分离的编程技术。类的接口包括用户所能实执行的操作;类的实现则包括类的成员结构、负责接口实现的函数体以及定义类所需的各种私有函数。封装实现了类的接口和实现的分离。封装后隐藏了它的实现细节。
类想要实现数据抽象和封装,需要首先定义一个抽象数据类型。在抽象数据类型中,又累的设计者负责考虑类的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么,而无需了解类型的工作细节。
定义抽象类型数据
1.1. 设计Sales_data类
基于设计一个图书管理系统的类,所以Sales-data的接口应该包括一下操作:
- 一个isbn成员函数,用于返回对象的ISBN码
- 一个combine成员函数,用于将一个Sales-data对象加到另一个对象上
- 一个名为add的函数,执行两个Sales-data的相加
- 一个read函数,将数据从istream读入到Sales-data对象中
- 一个print函数,将Sales-data对象的值输出到ostream
使用Sales_data类
#include<iostream>
#include<string.h>
#include"Sales_data.h"
using namespace std;
int main()
{
Sales_data total;
if(read(cin,total))
{
Sales_data trans;
while(read(cin,trans))
{
if(total.isbn()==trans.isbn())
total.combine(trans);
else{
print(cout,total)<<endl;
total=trans;
}
}
print(cout,total)<<endl;
}
else{
cerr<<"No data?"<<endl;
}
1.2.定义Sales_data类
struct Sales_data
{
//关于Sales_data对象的操作
string isbn() const {return bookNo;}
Sales_data& combine(const Sales_data&);
double avg_price() const;//返回书籍的平均价格
//数据成员
string bookNo;
unsigned units_sold=0;
double revenue=0.0;
};
//Sales_data的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
ostream &print(ostream&,const Sales_data&);
istream &read(istream&,Sales_data&;
定义和声明成员函数的方式与普通函数差不多。成员函数的声明必须在类的内部,他的定义则既可以在类的内部,也可以在类的外部。作为接口组成部分的非成员函数,他们的定义和声明都在类的外部。
1.2.1引入this
在total.isbn()的调用中,实质上是编译器将total的地址传递给了isbn的隐式形参this上
伪代码如下:
total.isbn()
Sales_data::isbn(&total)
这两种意义相同。
我们还可以把isbn定义成如下形式:
string isbn() const {return this->bookNo;}
因为this是一个指针常量,我们不允许改变this中保存的地址。
1.2.2.引入const成员函数
默认情况下,this的类型是指向类类型非常量版本的常量指针,但是this仍需要遵循初始化规则,这就意味着我们不能把this绑定到一个常量对象上。使得我们不能调用普通的成员函数。
则我们将this的声明成const Sales_data *const。如此既不会改变this所指对象,等有助于提高函数的灵活性。
1.2.3. 在类的外部定义成员函数
像其他函数一样,当我们在类的外部定义成员函数时,成员函数的定义必须和他的声明匹配。
如下:
double Sales_data::avg_price() const
{
if(units_sold)
return revenue/units_sold;
else
return 0;
}
其中使用作用域运算符Sales_data::avg_price,所以当avg_price使用revenue和units_sold时,实际上隐式的使用了Sales_data的成员。
1.2.4定义一个返回this对象的函数
函数combine的设计初衷类似于复合赋值运算符+=,调用该函数的对象代表得是赋值运算符左侧的运算对象,右侧运算对象则通过显式的实参被传入函数:
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold+=rhs.units_sold;
revenue+=rhs.revenue;
return *this;
}
当我们的处理交易程序调用如下的函数时,
total.combine(trans);
此时total的地址被绑定在隐式的this参数上,而rhs绑定到了trans上,因此函数相当于:
units_sold+=rhs.units_sold;
最后返回total的引用。
1.3.定义类相关的非成员函数
设计一个类常常需要定义一些辅助函数,比如add、read和print等,虽然这些函数属于类的接口的组成部分,但他们实际上并不属于类。
1.3.1定义read和print函数
istream &read(istream &is,Sales_data &item)
{
double price=0;
is>>item.bookNo>>item.units_sold>>price;
item.revenue=price*item.units_sold;
return is;
}
ostream &price(ostream &os,const Sales_data &item)
{
os<<item.isbn()<<" "<<item units_sold >>" "
<<item.revenue<<" "<<item.avg_price();
return os;
}
两点值得注意:
1.read和print分别接受一个各自IO类型的引用作为参数,因为IO类属于不能拷贝类型,因此我们只能通过引用传递他们。因为都读入和写入的操作会改变流的内容,所以函数接收的都是普通引用。
2.print函数不负责换行,执行输出任务的函数应该尽量减少对格式的控制。
1.3.2定义add函数
Sales_data add(const Sales_data &lhs,const Sales_data &rhs)
{
Sales_data sum=lhs;
sum.combine(rhs);
return sum;
}
1.4.构造函数
每个类都分别定义了他的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象初始化的过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
1.4.1合成的默认的构造函数
类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。默认构造函数无需任何实参。如果我们的类没有显式的定义构造函数,那么编译器就会为我们隐式的定义一个默认构造函数。,编译器创建的构造函数又称为合成的默认构造函数,有一下规则:
1.如果存在类内的初始值,用它来初始化成员。
2.否则,默认初始化该成员。
1.4.2 定义Sales_data的构造函数
对于我们的Sales_data类来说,我们将使用下面的参数定义4个不同的构造函数;
- 一个istream& ,从中读取一条交易信息。
- 一个const string& ,表示ISBN编号;一个unsigned,表示售出的图书数量;以及一个double,表示图书的售出价格。
- 一个const string&,表示ISBN编号;编译器将赋予其他成员默认值。
- 一个空参数列表。
struct Sales_data {
Sales_data()=default;
Sales_data(const string &s):bookNo(s){}
Sales_data(const string &s,unsigned n.double p):
bookNo(s),units_sold(n),revenue(p*n){}
Sales_data(istream &);
string isbn() const {return bookNo;}
Sales_data& combine(const Sales_data&);
double avg_price() const;
string bookNo;
unsigned units_sold=0;
double revenue=0.0;
};
1.4.3=default的含义
如果我们既需要其他形式的构造函数,也需要默认的构造函数,那在c++11新标准中,如果我们需要这种默认行为,那么可以通过在参数列表后面写上=default来要求编译器生成构造函数。其中,=default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。