定义抽象数据类型

目录

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中的每一个元素

上一篇:C语言编程笔试题(一)


下一篇:一个insert插入语句很慢的优化