C++ Primer第七章①
C++ Primer
第七章 类
这一章是C++ 面向对象的重头戏,首先我来说一下我对面向对象的理解,对象就是生活中的一些事物,比如汽车,电脑等,面向对象的意思是,我们把这些汽车等对象做成一个类,这个类很好地描述了汽车这个对象的特征,那我们就可以去实例化这个类,从而产生对象,就是一辆辆的汽车,而汽车的使用者不需要关心汽车是怎么造出来的,只要会开车,是老司机就好了,以后要转Java或者C#的同学这一章要格外认真地看,因为如果你完全搞定了C++里面的类,其他的也就不在话下了,因为毕竟它是最烦的。 类的基本思想是数据抽象和封装:
- 数据抽象: 这是最难的部分,因为你要设计出符合项目的一些类是比较难的,这是从无到有的过程,要多多练习才行。数据抽象有一些基本的概念了解下:
- 接口:用户所能执行的操作
- 类的实现:包括类的数据成员、负责接口实现的函数以及定义类所需的各种函数
- 封装:实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,类的用户只能使用接口而无法访问实现部分;说人话:我们来实现一个类给人家用,就相当于我们造了一个汽车模板来生产汽车,人家只需要知道怎么开车,不需要知道也不能知道这个车怎么造。
定义抽象数据类型
书店老板现在有一个任务给我们,完成后奖励2000块。写一个程序来自动计算算每一本书的总销售额、售出册数和平均售价。
设计类
设计部分我就直接写出来了: 这个类命名为Sales_data,它的接口要包含以下操作
- isbn成员函数,返回对象的ISBN编号(用来确认是不是同一种书)
- combine成员函数,用来相加(两本书相加的时候保证是它们的售价相加)
- read函数,将数据从istream读入到Sales_data对象中
- print函数,将Sales_data对象输出ostream
设计类的一个很好的窍门是,你先假装它实现了,去用它,然后再去实现它: 接下来的代码注意啦,第一次使用类,来看看有多好用(假如你没有类的话,你可以用C写写试试):
Sales_data current; //保存当前求和结果的变量
if(read(cin, current)) //如果有交易输入
{
Sales_data next; //用于保存下一条交易数据
while(read(cin, next))
{
if(current.isbn() == next.isbn()) //如果是同一种书
{
current.combine(next); //加上这个交易额和交易数量
}
else
{
print(cout, current) << endl; //输出目前这种书的结果
current = next; //更新书本
}
}
print(cout, current) << endl; //输出最后一条
}
else
{
cout << "没输入啊老板" << endl; //提醒用户没输入
}
这个程序要求书本按照书籍种类输入,就是说ABA这样输入会打印出ABA,最好是AAB输入,这样A就被加和了。 总之,使用类可以让我们专注在业务逻辑上,不用管类的实现,上面的程序基本是人去做这个事情的顺序,我们现在用这个小程序就代替了,当然,我们还没实现类呢。
定义类
如下代码,我还加了一些使用的函数定义:
struct Sales_data
{
//数据成员
string bookNo; //书号
unsigned units_sold = 0; //售出册数
double revenue = 0; //总销售收入
//成员函数
string isbn() const //返回书本isbn号,这里的const待会解释
{
return bookNo;
}
Sales_data& combine(const Sales_data&); //函数声明
double avg_price() const; //返回售出书籍的均价,这里的const也待会解释
}
定义成员函数
我们来看一下isbn函数,你别看这个函数简单,细细推导的话,我们可以从这个函数中学到很多东西。 我们先来看一下我们是怎么调用这个函数的:
current.isbn()
我们是用点运算符来访问current对象的isbn成员来调用的,想想是谁在调用成员函数呢?是这个类的对象在调用,所以isbn函数返回的bookNo是current这个对象的bookNo,所以实际的过程是这样的:
Sales_data::isbn(¤t); //调用的时候传入了current的地址
而在类内部,任何对类成员的直接访问都被看作this的隐式引用,所以,我们也可以把isbn函数写成:
string isbn() const
{
return this->bookNo;
}
因为this总是指向调用它的这个对象,所以C++把this看作常量指针,意思是指针本身是常量,我们不允许改变this中保存的地址。 第二个可以从isbn函数中学习的就是const,我们先来看没有const的情况:
string isbn()
{
return this->bookNo;
}
那我们来调用试试:
const Sales_data test;
test.isbn(); //这里会报错
为什么会报错呢?我们来分析下,上面这句调用的话用this写出来的话等价于:
Sales_data::isbn(&test);
//在类内部执行的时候是这样的
string isbn()
{
return this->bookNo;
}
问题就出在this指针上,因为this是指针常量,它只是本身值不变,但它可以改变它所指对象的值(它是顶层const),而它所指向的test对象是const的,所以引发了矛盾。 也就是说,我们如果不加const的话,这个函数只能被非const对象调用。 现在,我们知道了const的作用了:修改隐式this指针的类型,使它成为顶层和底层都是const的类型。 本质上isbn的函数是这样的:
string isbn(const Sales_data *const this)
{
return this->bookNo;
}
怎么样,还是有点复杂吧,然而你知其所以然之后,你就可以一直写成最简单的形式了啊:
string isbn() const //返回书本isbn号,这里的const待会解释
{
return bookNo;
}
C++官方说法:this是隐式地,没办法直接将它声明成指向常量的指针,于是设计者们就想出了这样的办法:把const放在参数列表之后表示this是一个指向常量的指针(它本身也是个常量哦)。 像这样使用const的成员函数被称作常量成员函数。
graph LR
常量成员函数-->常量和非常量对象都可调用
graph LR
普通成员函数-->非常量对象才能调用
一定要理解上面的图,不理解我说了那么多就白说了。
编译器对类的处理顺序
编译器分两步处理类:
- 首先编译成员变量的声明
- 再编译成员函数 所以啊,成员函数可以随意使用类中的其他成员变量不用管它们出现的次序。
在类的外面也可以定义其成员函数
我们来定义一下类中声明的avg_price函数,只要在函数名前面加一个作用域说明符::就行了:
double Sales_data::avg_price() const
{
if(units_sold>0) //有卖出书的话
{
return revenue/units_sold;
}
else
{
return 0;
}
}
编译器看到 avg_price 这个函数是在 Sales_data 中,就会把它当成在类内部了。
下面我们来看一下combine函数的定义:
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this; //返回调用该函数的对象
}
上面的&都是引用,不是取地址啊,不要连这个都搞错了。 这个函数值得介绍的是返回值,为什么要这样返回呢?为了可以这样调用: a.combine(b).combine(c) 至少我觉得是这样。
定义类相关的非成员函数
类的设计者常常需要定义一些辅助函数,我们接下来就会以Sales_data类为例来介绍一些函数;尽管这些函数定义的操作从概念上来说属于类的接口组成部分,但它们其实不属于类本身。
定义read和print函数
我们来定义一下前面用过的这两个函数:
//输入的信息包括ISBN、售出总数和售出价格
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;
}
//print自己写
这个函数就解释一个地方,返回类型是istream,因为IO类不能被拷贝。 这样2000块差不多到手了,之后的章节会介绍更多细节。
#C++工程师#