目录
- 1. 定义抽象数据类型
1. 定义抽象数据类型
类可以定义自己的数据类型
通过定义新的类型来反映待解决问题中的各种概念
数据抽象能帮助我们将 对象的具体实现 与 对象所能执行的操作分离 开来
1.1 设计 Sales_data 类
Sales_data的接口应该包含以下操作:
- 一个 isbn 成员函数,用于返回对象 (这里指某类书) 的 ISBN 编号
- 一个 combine 成员函数,用于将一个 Sales_data 对象加到另一个对象上
(同一种书,将其数量,价格等相加) - 一个名为 add 的函数,执行两个 Sales_data 对象的加法
- 一个 read 函数,将数据从 istream (输入流)读入到Sales_data对象中
- 一个 print 函数,将Sales_data对象的值输出到 ostream (输出流)
1.1.0 使用改进的 Sales_data 类
1.从标准输入读取对象保存的数据写成 read 函数
2.将相同种类书的数据对应相加的操作(total += trans),写成一个combine函数
3.标准输出对象保存的数据写成 print 函数
Sales_data total; //对象total保存销售记录(isbn书号、销售数量、单价...)
if(read(cin, total)) //从标准输入cin读取对象total保存的数据
{
Sales_data trans; //对象trans保存销售记录
while(read(cin, trans)) //从标准输入cin读取对象trans保存的数据
{
if(total.isbn() == trans.isbn()) //如果对象total和对象trans保存的isbn书号一致
{
total.combine(trans); //将对象trans保存的数据与对象total保存的数据对应相加
}else{
print(cout, total) << endl; //书号不一致,输出当前对象保存的数据
total = trans; //将此书的数据移到对象total中,以便下一次循环对比
}
}
print(cout, total) << endl;//输出最后一条销售记录
}else{
cerr << "No data?!" << endl; //未读取到数据
}
1.2 定义改进的 Sales_data 类
struct Sales_data{
//定义在类内部的函数是隐式的内联函数(编译时直接展开函数内部的东西)
//成员函数:Sales_data对象的操作
//isbn函数返回类型为string
std::string isbn() const {return bookNo}; //isbn()后的const是修饰隐式this指针的
//combine函数返回类型为引用类型
Sales_data& combine(const Sales_data&);
double avg_price() const;平均价格函数,const是修饰隐式this指针的
//数据成员(对象所含有的全部属性)
std::string bookNo; //书号
unsigned units_sold = 0; //销售数量
double revenue = 0.0; //销售单价
}; //类定义别忘了最后加分号;
// Sales_data 的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&); //执行两个 Sales_data 对象的加法
//当函数返回引用类型时,没有复制返回值,相反,返回的是对象本身
//如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象
std::ostream &print(std::ostream&, const Sales_data&); //将Sales_data对象的值输出到 ostream (输出流)
std::istream &read(std::istream&, Sales_data&); //将数据从 istream (输入流)读入到Sales_data对象中
1.2.0 定义成员函数
所有成员必须在类内部声明
成员函数体既可在类内,也可在类外
成员函数isbn
//返回 Sales_data 对象的bookNo数据成员
std::string isbn() const { return bookNo; } //const的作用是修饰隐式this指针的类型
isbn是如何返回 Sales_data 对象的bookNo数据成员?
1.2.1 引入 this
对成员函数isbn的调用
total.isbn(); //对象total调用(.)成员函数isbn,返回对象total的bookNo
编译器负责把对象total的地址传递给成员函数isbn的隐式形参this
等价地认为编译器将该调用重写成如下形式:
//伪代码,用于说明调用成员函数的实际执行过程
//类名::成员函数(传入对象地址)
作用域运算符(::)说明成员函数isbn()被声明在Sales_data类的作用域内
Sales_data::isbn(&total);//伪代码,隐式形参this指针存着对象的地址
还能把isbn函数定义成如下形式:
std::string isbn() const { return this -> bookNo; } //const的作用是修饰隐式this指针的类型
//等价于return (*this).bookNo
//this指针存着对象total的地址,对this解引用获得对象total
//等价于return total.bookNo
因为 this 的目的总是指向 “这个” 对象,所以 this是一个常量指针(所存的地址不能被改变),我们不允许改变this中保存的地址
1.2.2 引入 const 成员函数
默认情况下,this的类型是指向类类型非常量版本的常量指针
例如:Sales_data成员函数中,this的类型(常量指针)是Sales_data *const
不能把this绑定到一个常量对象上,这样我们不能在一个常量对象上调用普通成员函数 (个人理解:常量对象的属性(数据成员)都固定,而成员函数是对对象的属性进行操作)
如果isbn是一个普通函数且this是一个普通指针参数,我们应把this声明成const Sales_data *const
第一个const:在isbn函数体内不会改变this所指的对象。
第二个const:this指针所存地址不能被改变,即所指对象不能改变
紧跟在参数列表后的const表示this是一个指向常量的指针,这样使用const的成员函数被称为 常量成员函数
//伪代码,说明隐式的this指针是如何使用的
//下面代码非法,因为不能显式地定义自己的this指针
//返回类型String,作用域运算符(::)说明成员函数isbn()被声明在Sales_data类的作用域内
std::string Sales_data::isbn(const Sales_data *const this){ //非法:不能显式地定义自己的this指针
return this->bookNo; //等价于(*this).bookNo等价于total.bookNo
}
this是指向常量的指针(上述形参中第一个const),所以常量成员函数不能改变调用它的对象的内容
1.2.3 类作用域和成员函数
当在类外部定义成员函数时,成员函数的定义必须与成员函数声明(类内部)的返回类型、参数列表和函数名完全一致
//作用域运算符(::)说明成员函数avg_price()被声明在Sales_data类的作用域内
//形参列表后const的作用是修饰隐式this指针的类型
double Sales_data::avg_price() const {
if(units_sold)//如果售出数量非0
return revenue/units_sold; //隐式this指针将指向对象的revenue和units_sold(Sales_data的数据成员),计算完成后返回给对象
else //售出数量为0
return 0; //结束函数
}
1.2.4 定义一个返回 this 对象的函数
//当函数返回引用类型时,没有复制返回值,相反,返回的是对象本身
//如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象
//作用域运算符(::)说明成员函数combine()被声明在Sales_data类的作用域内
//lhs左侧运算对象 rhs右侧运算对象
Sales_data& Sales_data::combine(const Sales_data &rhs){
units_sold += rhs.units_sold; //把rhs的数据成员(对象的某一属性)加到this对象的数据成员上(对象的某一属性)
revenue += rhs.revenue;
return *this; //this存着所指对象的地址,解引用获得对象,该函数返回类型为引用
//返回对象的引用
//使用this来把对象当成一个整体访问,而非直接访问对象的某个数据成员
}
调用函数
//total为combine函数隐式形参this所指的对象,trans为右侧运算对象
total.combine(trans);
解释上述调用combine
//rhs绑定到trans上
//combine函数的隐式参数:this指针(存着所指对象的地址)
Sales_data& Sales_data::combine(const Sales_data &rhs){
//左侧为this所指对象的数据成员(对象的某一属性)
//右侧为传入的对象,该对象调用数据成员(对象的某一属性)
units_sold += rhs.units_sold;
//等价于total.units_sold = total.units_sold + rhs.units_sold;
revenue += rhs.revenue;
//等价于total.revenue = total.revenue + rhs.revenue
return *this; //this存着所指对象的地址,解引用获得对象,该函数返回类型为引用
//返回对象的引用
//使用this来把对象当成一个整体访问,而非直接访问对象的某个数据成员
}
1.3 定义类相关的非成员函数
类的作者需要定义一些辅助函数,如add、read、print
尽管这些函数定义从概念上属于类的接口的组成部分,但它们实际不属于类本身
定义非成员函数和其他函数一样,将函数的声明和定义分离开来
一般来说,如果非成员函数是类接口的组成部分,则这些非成员函数的声明应该与类在同一个头文件中
1.3.0 定义 read 和 print 函数
输入的交易信息包括ISBN、售出总数、售出价格
//IO类属于不可拷贝类型,所以只能通过引用来传递它们
//read函数从给定流(istream)中将数据读到给定的对象里
//将item绑定到传来的对象上
istream &read(istream &is, Sales_data &item)
{
double price = 0;
//给定流is读取对象的数据成员(对象的属性)
is >> item.bookNo >> item.units_sold >> price;
//价格*单价的结果赋值给对象的总价
item.revenue = price * item.units_sold;
return is; //返回is流的引用
}
//print函数负责将给定对象的内容打印到给定的流中(ostream)
//将item绑定到传来的对象上
ostream &print(ostream &os, const Sales_data &item)
{ //将对象的内容传到os流中
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os; //返回os流的引用
}
1.3.1 定义 add 函数
//lhs左侧运算对象 rhs右侧运算对象
//实参传入两个对象给形参
//lhs绑定到传入的左侧实参上,rhs绑定到传入的右侧实参上
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; //把lhs的数据成员拷贝给sum
sum.combine(rhs); //把rhs的数据成员加到sum中
//lhs.combine(rhs)
return sum; //返回两个对象的和(售出数量的和和价格总和,具体见combine函数体内的内容)
//返回类型为非引用类型,所以返回sum的副本
}
1.4 构造函数(constructor)
类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫构造函数
构造函数的任务:
初始化类对象的数据成员(个人将数据成员理解为对象的属性)只要类的某一对象被创建,就会自动执行构造函数
构造函数的特点:
- 名字与类名相同
- 无返回类型
- 不能被声明成const
当创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其"常量"属性。因此,构造函数在const对象的构造过程中可向其写值
(构造过程中,属性还未被const修饰,所以此过程中可向对象的属性进行写值,一旦构造完成,所有属性均不可修改)
1.4.0 合成的默认构造函数 ( 编译器创建的构造函数 )
类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数(default constructor) 默认构造函数无须任何实参
如果类没有显式地定义构造函数,那编译器就会为我们隐式地定义一个默认构造函数,编译器创建的构造函数又被称为合成的默认构造函数(synthesized default constructor)
对大多数类,这个合成的默认构造函数将按照如下规则初始化类的数据成员(对象的属性):
- 如果存在类内的初始值,用已有初始值来初始化对象的数据成员
- 否则,默认初始化该对象的数据成员
1.4.1 某些类不能依赖于合成的默认构造函数
合成的默认构造函数只适合非常简单的类
对于一个普通的类来说,必须定义它自己的默认构造函数
1.4.2 定义 Sales_data 的构造函数
对于 Sales_data 类,使用下面参数定义4个不同的构造函数
- istream& :从中读取一条交易信息
- const string& :表示ISBN编号
- unsigned :表示售出的图书数量
- double :表示图书的售出价格
- const string& :表示ISBN编号;编译器将赋予其他成员默认值
- 空参数列表(即默认构造函数)
struct Sales_data{
//相比之前新增的构造函数
//希望构造函数等同于合成默认构造函数,则在构造函数的参数列表后添加 = default
Sales_data() = default;
//下面两个构造函数的 {函数体} 为空,因为构造函数的唯一目的就是为数据成员赋初值,如果没有其他任务需要执行,函数体也就为空了
//构造函数初始值列表(冒号:和花括号{函数体}之间的代码)
Sales_data(const std::string &s) : bookNo(s) { }
//类名(形参列表): 构造函数初始值列表 { }
//构造函数初始值列表为新建对象的数据成员赋初值
//没有出现在构造函数初始值列表的数据成员,将通过类内定义的初始值进行初始化
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p*n) { }
//如果是只接受一个string参数的构造函数,则可以写为:
//Sales_data(const std::string &s) : bookNo(s), units_sold(0), revenue(0) { }
Sales_data(std::istream &);
//原来已有的其他成员
//isbn()后的const是修饰隐式this指针的
std::string isbn() const { return bookNo; }
//当函数返回引用类型时,没有复制返回值,相反,返回的是对象本身
//如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象
Sales_data& combine(const Sales_data&);
//avg_price() 后的const是修饰隐式this指针的
double avg_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
}; //勿忘此处分号
1.4.3 =default 的含义
//希望构造函数等同于合成默认构造函数,则在构造函数的参数列表后添加 = default
Sales_data() = default;
如果=default在类的内部,则默认构造函数是内联的
如果=default在类的外部,则该成员默认情况下不是内联的
1.4.4 构造函数初始值列表
类名(形参列表): 构造函数初始值列表 { }
//下面两个构造函数的 {函数体} 为空,因为构造函数的唯一目的就是为数据成员赋初值,如果没有其他任务需要执行,函数体也就为空了
//构造函数初始值列表(冒号:和花括号{函数体}之间的代码)
Sales_data(const std::string &s) : bookNo(s) { }
//类名(形参列表): 构造函数初始值列表 { }
//构造函数初始值列表为新建对象的数据成员赋初值
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p*n) { }
//如果是只接受一个string参数的构造函数,则可以写为:
Sales_data(const std::string &s) : bookNo(s), units_sold(0), revenue(0) { }
1.4.5 在类的外部定义构造函数
//::作用域运算符表明构造函数Sales_data(std::istream &is)在类Sales_data的作用域内
//构造函数的名字与类名一致
//这里没有构造函数初始值列表对对象的数据成员进行初始化,但由于执行了构造函数的 {函数体} ,所以对象的数据成员(属性)仍能被初始化
Sales_data::Sales_data(std::istream &is){
read(is,*this); //read函数的作用是从is(输入流)中读取一条交易信息然后存入this所指的对象中
//this指针存着对象的地址,解引用this获得对象,使用this来把对象当成一个整体访问,而非直接访问对象的某个数据成员
}
1.5 拷贝、赋值和析构
类需要控制拷贝、赋值以及销毁对象过程中发生的行为
1.对象在何种情况下会被拷贝:
- 初始化变量
- 以值的方式传递
- 返回一个对象
2.使用赋值运算符时会发生对象的赋值操作
total = trans; //将对象trans赋值给对象total(连同对象的成员一并赋值)
//等价于
//对象.对象的数据成员
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;
3.当对象不存在时执行销毁操作,例如:
- 局部对象会在创建它的块结束时被销毁
- vector对象(或数组)销毁时存储在其中的对象也会被销毁
1.5.0 某些类不能依赖于合成的版本(编译器代替人工合成)
如果我们不主动定义拷贝、赋值、销毁这些操作,则编译器将替我们合成它们。尽管编译器能替我们合成这些操作,但要清楚一点,对于某些类来说合成的版本无法正常工作。特别是,当类需要分配类对象之外的资源时,合成的版本常常会失效
例如:管理动态内存的类通常不能依赖于上述操作的合成版本
如果类包含vector或string成员,则其拷贝、赋值、销毁的合成版本能够正常工作
当我们对含有vector成员的对象执行拷贝或赋值操作时,vector类会设法拷贝或赋值成员中的元素
当这样的对象被销毁时,将销毁vector对象,也就是一次销毁vector中的每一个元素