这是一次。。无聊的笔记
我我我要学C++!
可是我我我只会C。。
所以,这篇笔记从一个C初学者的角度。。。试着理解C++(他在狂笑)
概念
- 类(class):就是C里面的结构体,但是面向对象的类定义更灵活,比如,在类里面可以写方法(函数)而结构体不可以
- 对象 (object):类是多个相似对象的总称,对象是类的实际表示,也叫对象化实例,简称实例。这样说吧,C里面int就是一个类,然后你把它实例化,写了个int a=b; b就是个对象了。当然可以更宏观的理解,我在看《JavaScript DOM编程艺术》里有一句话——万物皆是对象。虽然这个是对于java(额对,但这是一本JavaScript书)说的,但是面向对象更多的是一种思维方式,重新构建理解世界的另一种方法。(后面写一个我看到的面向对象解八皇后问题。。。真的服了,这是人类的思想吗?)
- 属性(property):每个对象所拥有的变量,如car对象,我们可以定义car对象的几个属性:char color;Int year;等
- 继承(inheritance):当我们想在一个类里面添加属性或方法时,我们可以用继承的方法来避免重复代码
- 多态 (ploymophism):编译器根据函数实参的类型(可能会进行隐式类型的转换),推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
- 泛型 :顾名思义,就是针对广泛类型的编程方式
额具体的概念理解还是放在确定的情景下吧。
慢着,首先还有一个问题。。。
为什么要用面向对象编程?
所以为什么要有面向对象编程?
当然,我们写一个hello world算一个2*3是不需要面向对象的,但是当软件年得越来越大,越来越多的代码量的时候。。问题就来了:
举个例子,我要做FIFA游戏,额这个游戏已经足够大了对吧,这是我们就遇到了很多传统面向过程编程的问题:
- 命名冲突,英文单词也就那么几个,可能写着写着取名时就没合适的短词用了,为了避免冲突,只能把函数名取得越来越长。
- 代码重复,我们有好多好多球员,球员每个都有自己的属性,那我该怎么办呢?每个人我都去写一个串代码?哪怕可以复制粘贴,那也很麻烦啊。。。而且,这样并不利于修复和扩展啊
所以,面向对象思想应运而生
面向对象有的一大特性:抽象(abstract)在这个环境下就好理解了
抽象,就是为后来的程序员提供规范和限制,比如我只需要做好一个类来描述球员,他有各种属性,速度、射门、盘带等等,我就在属性里面一个一个定义,每个球员有复杂的物理引擎,我们也一个一个定义,
然后我们就可以在更低的抽象层次把这些一一实现,然后每一个球员可以从实现好类里面继承出来一个具体的球员,而这些球员都是一个独立封装的对象,对外提供各种物理引擎的接口,然后这些每一个程序组成的对象就能够在物理引擎的作用下开心的踢球了
所以,面向对象有一个很棒棒的特性,就是责任,面向对象用责任来描述行为,每一个球员都有自己的责任去找到,自己怎么去过人,怎么去防人,而不受上层对象的干扰。
总而言之,我们可以在这里看到,面向对象易于修复和扩展,有很强的封装性,对象只提供给外界自己想给的内容。
开始实现
开始用C++实现面向对象
下面借鉴清华大学出版社《C++面向对象程序设计》里面的案例来阐释C++里面的一些语法特性
- “简单的航空货物价格计算系统”
题目是这样的,对于乘客的货物,我们将15公斤以下的不收费,1525,每公斤12元,2545,每公斤15元;45~80,每公斤20;80公斤以上的货物,按每公斤30元收费。
其中还要考虑一定的乘客信息。
首先明确,面向对象语言的优势在于他是一个设计语言,设计时我们要力求面面俱到。、
1先划定我们要讨论的类——货物(Cargo);
2.类的属性与方法的确定
owner,weight,destination,content,charge,同时要保存分段收费标准,一边今后的扩展,就要定义weightRange[],chargeRange[]两个数组为了显示货物是属于哪个乘客的,就要定义一个成员函数getOwner()来获得对象的值;
同理,要取得其他货物信息,要定义成员函数
最后,当然还要预留一个析构函数进行事后处理。
3.根据需要,设定数据成员和函数的类型
比如,对于一个货物,我们要知道他的主人(owner);重量(weight);目的地(destination);内容(content);价格(charge);以及上面说了为了方便修改我们要把几个分段作为数组存进去;最后再划定他们所有的方法(C里面的函数)。类的定义就很像C里面结构体的定义,只是C++语言里面可以给类添加方法。
就像这样:
#include<iostream>
#include<stdlib.h>
#define MIN 10
#define MAXITEM 5
using namespace std;
class Cargo
{
char owner[40];
float weight;
char destination[100];
char content[200];
float charge;
float weightRange[MAXITEM];
float chargeRange[MAXITEM];
//Cargo(char *owner, float weight, char *destination, char *content);
//~Cargo() {};
float calcCharge();
char *getOwner();
float getWeight();
char *getDestination();
char *getContent();
float getCharge();
};
这就是一个基本的货物类的创立;
一如C里面的结构体定义只是确定数据类型,但是编译器并不会为你设定的类留出内存空间,只有你在main函数里面定义一个Cargo类的对象时才会分配内存。我们可以在类外用一个完全新的方法来专门给所有的成员赋值。就像这样:
void Cargo::init(char *owner,float weight,char *destination,char *content){
//为每个成员赋值
}
其中的双冒号::是类作用域符。我们在类里面定义了某方法,但要是在类里面全部实现方法,未免会破坏类定义的完整性(比如我把定义和方法内容写一块,就会导致我在类里面就写了很长一串,然后其他方法的定义就湮灭在密密麻麻的代码里面了),于是C++提供了::来扩大类的作用域,让我们可以在类外继续实现方法。
但是这样做依然有瑕疵。这需要我们在每次创建对象的时候都要调用init函数。一旦忘记了就会导致数据未初始化而无法预知结果。
C++为我们提供了一种叫做构造函数的东西,可以在每次创建对象时自动调用。
就像这样:
(将前面类定义的注释给去掉)
构造函数的函数名与类名相同,具体实现构造函数的时候不带任何返回类型,包括void也不用。
其中的Cargo是基于一种对称思想出现的。有初始化成员的就会有自动释放结束生命历程成员的函数,就是图中的Cargo函数。学名叫做析构函数。
但是到这里我们类的定义并没有完全结束。
因为任何抽象都伴随着信息的隐藏,面向对象编程有一个优势或者说特点就在于它强大的信息隐藏能力,
举一个例子,在本项目里面,我们希望其他拿到这个类的对象无法获知
具体的乘客信息,否则会出现信息泄露。所以C提供了private、protect、public三种访问限定符。
如果开始做一些简单的小项目,就会对信息隐藏体会的更深刻。比如微信小程序前端需要获取用户信息,那么你就要把代码内容放在后端,而只给前端提供可以调用的接口,这也是一种封装性的体现
所以在赋予了访问限定权利之后就是这样:
class Cargo
{
protect:
char owner[40];
float weight;
char destination[100];
char content[200];
float charge;
float weightRange[MAXITEM];
float chargeRange[MAXITEM];
//Cargo(char *owner, float weight, char *destination, char *content);
//~Cargo() {};
public:
float calcCharge();
char *getOwner();
float getWeight();
char *getDestination();
char *getContent();
float getCharge();
};
之后我们依次实现上面定义好的方法
//初始化数据成员
Cargo::Cargo(char *owner, float weight, char *destination, char *content)
{
strcpy_s(this->owner, owner);
this->weight = weight;
strcpy_s(this->destination, destination);
strcpy_s(this->content, content);
}
// 计算货物运费
float Cargo::calcCharge()
{
int i;
float tw = weight;
float rc = 0.0f;//后面加一个f是告诉编译器这是一个float型的数据
float pw = 0.0f;
for (i = 0; i < MAXITEM; i++)
{
for (i = 0; i < MAXITEM; i++)
{
if (tw > (weightRange[i] - pw))
{
rc = rc + (weightRange[i] - pw)*chargeRange[i];
tw = weight - weightRange[i];
pw = weightRange[i];
}
else
{
rc = rc + tw * chargeRange[i];
charge = rc;
return charge;
}
charge = rc + tw * chargeRange[MAXITEM - 1];
return charge;
}
}
}
//取得货物信息:
char *Cargo::getOwner()
{
return owner;
}
char *Cargo::getDestination()
{
return destination;
}
char *Cargo::getContent()
{
return content;
}
float Cargo::getCharge()
{
return charge;
}
float Cargo::getWeight()
{
return weight;
//——————Cargo类定义结束
运行出来是这样的:
现在我们已经基本完成了一个简单的交互程序。
我们继续在这上面加条件!
从航空旅行的常识我们知道,航空公司会对旅客所携带的危险品货物进行付佳佳收费,因此,对于相同重量的货物而言,对危险瓶的收费当然要高于普通货物的收费。
假设每公斤需要再附加收费5元,该如何修改呢?
最先也最自然的思路就是直接在原类里面添加成员。但是这样实际上改变了原先定义的类,不利于对象的独立和封装。
如果新建一个新的类,原类大量的代码又是重复的。面向对象语言提供了一种技术叫继承。建立一个新的类,是从原来类派生出来的。原类叫做基类,派生类叫做子类。
//对危险品的新类:
class AirCargo : public Cargo
{
private:
int dangerLevel;
char usage[100];
public:
AirCargo(char *owner, float weight, char *destination, char *content, int dangerLevel, char *usage);
AirCargo() :Cargo (" c",0.0f," c"," c" )
{
}
~AirCargo() {};
float calcAirCharge();
int getDangerLevel();
char *getUsage();
void init(char *owner, float weight, char *destination, char *content, int dangerLevel, char *usage);
};
AirCargo::AirCargo(char *owner, float weight, char *destination, char *content, int dangerLevel, char *usage)
:Cargo(owner, weight, destination, content)
{
this->dangerLevel = dangerLevel;
strcpy_s(this->usage, usage);
}
float AirCargo::calcAirCharge()
{
float fee;
fee = calcCharge();
if (dangerLevel == 1)
fee = fee + getWeight() * 5;
charge = fee;
return fee;
void AirCargo::init(char *owner, float weight, char *destination, char *content, int dangerLevel, char *usage)
{
strcpy_s(this->owner, owner);
this->weight = weight;
strcpy_s(this->destination, destination);
strcpy_s(this->content, content);
this->dangerLevel - dangerLevel;
strcpy_s(this->usage, usage);
}
int AirCargo::getDangerLevel()
{
return dangerLevel;
}
char *AirCargo::getUsage()
{
return usage;
}
对代码里面的一些语法做一些说明:
1.class AirCargo:public Cargo就是一个定义新类继承的语法。表示AirCargo是从Cargo类继承来的。关键字public是对原类成员访问权限在新类里面的新阐释。public、protect、private的访问限定程度从小到大,定义继承语句里面的访问限定符表示原成员在该访问权限限定程度及以上的权限全部转化为该权限。
举个例子,如果是private访问区分符,在基类中的所有private成员任然是基类的private成员,在派生类中不能直接访问积累中的这些private成员,基类所有的protect成员变成派生类的private成员,在派生类中可以直接访问这些成员;基类所有的public成员变成派生类的private成员,在派生类中可以直接访问这些成员。
2. 关于子类的构造函数
AirCargo::AirCargo(char *owner, float weight, char *destination, char *content, int dangerLevel, char *usage)
:Cargo(owner, weight, destination, content)
{
this->dangerLevel = dangerLevel;
strcpy_s(this->usage, usage);
}
这一部分即为子类的构造函数。
子类一共有六个参数,其中有四个都是Cargo类继承得来的,用“:”+Cargo(参数列表)来表示,新的两个参数直接在类定义里面赋值就是了(篇幅不长可以直接考虑在类里面赋值或完成方法,以免在类外过于累赘)。
至此,我们就基本(强行)完成了一个C++实现的管理系统了,主要是为了熟悉语法知识。
其他的高阶概念。。我在后面的笔记添吧。。。。
参考资料:面向对象编程导论(Timothy A.Budd)机械工业出版社
C++面向对象程序设计 清华大学出版社
个人初学者,水平有限,还望指正。