前言

几年前接触到一款很好玩的RPG游戏,叫作CPP。最近想着怀念一下,又不想干巴巴地去玩。于是乎,我打算写几篇攻略,主要是记录一下游戏中一些奇妙的点。游戏的第一章是面向对象程序设计,其中又分为基于对象(object-based)的关卡和面向对象(object-oriented)的关卡,而基于对象的关卡中又有两个BOSS,一个是无指针的类,另一个是有指针的类。今天就写写第一关基于对象的无指针的类吧。

基于对象的无指针类

首先介绍一个complex类,这个类是从标准库中提出出来的,有着大佬们的气息。source code

class complex {
public:
complex (double r = 0, double i = 0) : re(r), im(i) {}
complex& operator += (const complex&);
complex& operator -= (const complex&);
complex& operator *= (const complex&);
complex& operator /= (const complex&);
double real() const { return re; }
double imag() const { return im; }
private:
double re, im; friend complex& __doapl(complex *, const complex&);
friend complex& __doami(complex *, const complex&);
friend complex& __doaml(complex *, const complex&);
};

现在我们可以倒过来看,想想怎么设计一个表示复数的类呢?

首先,复数有实部和虚部,那么我们的类里一定有两个变量,我们先不考虑模版类,那么就是:

private:
double re, im;

想想如果不加上private可以吗?当然可以了,只是这样干也太懒了,太随意了,我好不容易弄出一个class,你在哪里都可以直接访问我的数据,还不如直接用回C的struct,怎么能这么干呢!这不符合封装的设计艺术,也不符合大佬的作风。private必须加!

接下来,一个类当然要有构造函数了。那么干脆直接不写了,直接使用默认构造函数吧!其实也不是不行,不过让人感觉很蠢,因为你怎么设值呢?当然也可以弄一些setter函数,不过也不够优雅。我们的使用者应该可以换不同的姿势来创建对象,比如:

complex c1;
complex c2(1);
complex c3(1,2);
complex *p = new complex();

这样一来,我们的构造函数这样设计:

public:
complex (double r = 0, double i = 0) : re(r), im(i) {}

我们的给实参写上默认值0,一个构造函数就可以应对上面4种写法。不过,我们为什么使用: re(r), im(i)这样奇怪的写法?而不是在函数题内直接赋值,

{
this->re = r;
this->im = i;
}

这样不是更清晰吗,清晰的代码不好吗?其实也可以,不过这样不够逼格!你可知道变量的生死是怎样的吗?变量需要经历两个阶段,首先初始化,其次赋值。而我们奇怪的初始化列表(initialization list)的写法,就让我们直接初始化就完事!效率UPUP!

有了成员变量,我们接下来应该考虑怎么让其他人来访问我的数据呢?当让是写getter了!

public:
double real() const { return re; }
double imag() const { return im; }

等等,函数名后面那个const是什么鬼!大佬们在写这个函数的时候,考虑到这个函数不会修改本身的数据,以及使用者会这样写:

const complex c4(1,2);
cout << c4.real() << " " << c4.imag() << endl;

这样一来,万一我们把函数名之后的const删掉了,编译器会认为你的函数可能会修改数据,因此就会报错,而使用者就会对你破口大骂!千万别这么干。。。

接下来,我们拿出中学数学课本,查了一下复数有四则运算,那么使用者可以这么用:

  cout << c1+c2 << endl;
cout << c1-c2 << endl;
cout << c1*c2 << endl;
cout << c1 / 2 << endl;
cout << (c1 += c2) << endl;
cout << (c1 == c2) << endl;
cout << (c1 != c2) << endl;
cout << +c2 << endl;
cout << -c2 << endl;
cout << (c2 - 2) << endl;
cout << (5 + c2) << endl;

聪明的你肯定立马反应到,操作符重载呗!没错,我们接下来就来看看操作符重载里有什么可以 dig dig!

public:
complex& operator += (const complex&);
complex& operator -= (const complex&);
complex& operator *= (const complex&);
complex& operator /= (const complex&);

注意到,我们是在类里面声明的操作符重载,那么可不可以在外面声明呢?其实也可以的,但是大佬们为什么这样写?

inline complex&
complex::operator += (const complex& r)
{
return __doapl (this, r);
} inline complex&
__doapl (complex* ths, const complex& r)
{
ths->re += r.re;
ths->im += r.im;
return *ths;
}

设计一个函数,我们必须考虑参数和返回值,我们可能会这样使用操作符:

c1 += c2;

这时候应该有两个参数,c1和c2。实际上编译器处理到这里,会这样调用函数:

complex& complex::operator += (complex *this, const complex &r);

注意到,第一个参数this就是左边的c1,第二个是右边的c2,至于为什么使用const,我想你已经知道了。我们知道编译器会这样调用函数,但是我们写的时候可不能这样写,否则会被骂哭。

接下来考虑返回值,为什么我们有时用引用,有时候不使用引用呢?其实使用引用还是为了提高效率,就跟C语言里面传指针4个字节的速度一样,引用的速度和传指针一样快。而使用引用和指针都需要考虑,返回值是不是局部变量(local object),如果不是,好的,使用引用吧!

这下我们知道了写成类成员函数的原因,c1的this指针默认的传入重载函数,c2的数据加到c1上,再返回c1自己,于是乎,使用者可以这样来用:

c1 += c2 += c3;

你可能会对函数friend complex& __doapl(complex *, const complex&);感到奇怪,这是个朋友,不过是我们单方面宣称的朋友。我们单方面认为他是我们的朋友,于是乎我们把他当自己人,朋友可以自由访问自己的私有数据。同样地,同一个类创建出来的对象互相都是自己人,自己人都可以访问自己的私有数据。

下面我们看一看声明在类外部的函数:

inline complex
operator + (const complex& x, const complex& y)
{
return complex (real (x) + real (y), imag (x) + imag (y));
}

这里我们的返回值变了,变成了complex,不是引用了。

complex (real (x) + real (y), imag (x) + imag (y));

这里我们函数里使用了临时对象,临时对象的生命在下一行代码就结束了。于是立即推出,不能使用引用,而直接创建一个临时对象,然后返回临时对象。注意到,临时对象的生命在下一行代码就结束了,返回值实际上就是一个临时对象,用过了,就死了。

最最最后,我们还可以花里胡哨地重载一下输出:

ostream&
operator << (ostream& os, const complex& x)
{
return os << '(' << real (x) << ',' << imag (x) << ')';
}

这里有几个问题,为什么这个函数定义在类外部呢?假如我们定义在类内部,那么使用者就该这么用:

c1 << cout;

emmmmmm...这一定会被喷死!那为什么我们的返回值不是void呢,返回void就行了啊?万一使用者这样使用:

cout << c1 << " love love";

第一个cout << c1返回值是void,编译器报错,GG。那么第一个参数为什么不添加const呢?const ostream& os

当然不能这样干,因为return os << '(' << real (x) << ',' << imag (x) << ')';已经修改了os的状态。

小问题

这里抛出一个问题,看看下面这个函数:

inline complex
operator + (const complex& x)
{
return x;
}

这个函数的意思是输出正的复数,比如:

cout << +c1;

你也许会有疑问,依照我们先看函数内是否使用了局部变量的原则,这里的返回值是可以使用引用的,而且应该使用引用。嗯,看起来确实如此,这是编写标准库的大佬们写出的代码,他们也会犯错,或者说也会写出不是最优的代码。当然了,这样的写法没有错,只是不是最优的写法,标准库并不是圣经,你可以去研究她,发现她的亮点和不足。

总结

设计类是最基本的技能,也是决定代码效率的关键一环,以下是我的认为设计类需要考虑的问题:

  1. 设计一个类,首先考虑有什么数据
  2. 考虑数据的类型,是public,还是private,还是friend
  3. 构造函数:是否有默认参数;有没有使用初始化列表
  4. 函数参数:首先考虑传引用;要不要加const;使用者会不会创建一个const的对象!
  5. 函数返回:是不是适合传引用?局部变量不适合传引用!
  6. 函数重载:使用者以不同的方式进行操作,比如double+complex complex+double
  7. cout重载:应该放在类外面,不适合做成员函数,因为没有人会倒着写
  8. this指针:调用成员函数都会隐藏的加上this指针,除了static
  9. static变量:必须在类的外面进行定义!类里面只是声明!声明没有内存!

第一篇攻略就先到这了,我们下次再见。

Reference

C++面向对象高级编程, 侯捷.

[CPP] Object Based Class的更多相关文章

  1. [Javascript] Different ways to create an new array/object based on existing array/object

    Array: 1. slice() const newAry = ary.slice() 2. concat const newAry = [].concat(ary) 3. spread oprea ...

  2. Poco::JSON::Array 中object 设置preserveInsertionOrder 时,stringify出错-->深入解析

    在使用poco version 1.6.0时 Poco::JSON::Array 在object  设置preserveInsertionOrder =true 时 调用 array.stringif ...

  3. Page Object Model (Selenium, Python)

    时间 2015-06-15 00:11:56  Qxf2 blog 原文  http://qxf2.com/blog/page-object-model-selenium-python/ 主题 Sel ...

  4. A Complete Tutorial on Tree Based Modeling from Scratch (in R & Python)

    A Complete Tutorial on Tree Based Modeling from Scratch (in R & Python) MACHINE LEARNING PYTHON  ...

  5. [TypeScript] Shallow copy object by using spread opreator

    For example we have an object: const todo = { text: "Water the flowers", completed: false, ...

  6. 译:Boost Property Maps

    传送门:Boost Graph Library 快速入门 原文:Boost Property Map 图的抽象数学性质与它们被用来解决具体问题之间的主要联系就是被附加在图的顶点和边上的属性(prope ...

  7. 【转】cocos2d-x Lua

    Call custom c++ from Lua cocos2d-x lua binds c++ class, class functions ,enum and some global functi ...

  8. cocod2d-x 之 CCDirector、CCScene、CCSprite

    CCDirector是控制游戏流程的主要组件. typedef enum { /// sets a 2D projection (orthogonal projection)2D投机模式 kCCDir ...

  9. GDI+ 摘要: 保存图像文件

    要保存图像文件,必须先获得图像的编码格式信息.可是GDI+没有直接提供这个函数:GetEncoderClsid(const WCHAR* format, CLSID* pClsid) 因此须要我们自己 ...

随机推荐

  1. sql server partition分区与group by 分组

    例子:在一个StudentScore表中,有序号ID,班级ClassId,学生姓名Name,性别Sex,语文成绩ChineseScore,数学成绩MathScore,平均成绩AverageScore等 ...

  2. you need to be root to perform this command

    在linux 终端执行某条命令时 提示一下错误 you need to be root to perform this command 是提示要获取root权限 输入su 回车输入密码 即可解决 参考 ...

  3. roadflow企业微信工作流程的配置与使用

    1.在您的微信后台添加应用 应用地址: 待办事项 :http://demo.roadflow.net/RoadFlowCore/Mobile/WaitTask 已办事项:http://demo.roa ...

  4. Sql语法高级应用之四:使用视图实现多表联合数据明细

    之前章节我们讲到:如果某个表的数据是多个表的联合,并且存在列与列的合并组成新列,用视图是最好的方案. 下面我分享两个个真实的SQL语句案例 USE Wot_Inventory GO FROM sys. ...

  5. NOI2009 区间

    题目链接:戳我 60分部分分还是很好拿的,排序(按照左端点为第一关键字,右端点为第二关键字)之后一个\(O(n^2)\),暴力判交,更新最小值,就可以水过前12个测试点. #include<io ...

  6. django系列8.2--django的中间件流程

    Django请求流程图 请求到达中间件之后,先按照正序执行每个注册中间件的process_reques方法,process_request方法返回的值是None,就依次执行,如果返回的值是HttpRe ...

  7. 【ocp-12c】最新Oracle OCP-071考试题库(37题)

    19.choose the best answer View the Exhibit and examine the structure of the PROMOTIONS table. Evalua ...

  8. 根据现有PDF模板填充信息(SpringBoot)

    根据现有PDF模板填充信息(SpringBoot+maven) 首先得有一个pdf模板,建立pdf模板需要下载工具 红色框为文本框,filename为域名.java需要根据域名赋值 pom 文件配置 ...

  9. Spring中applicationContext.xml详解

    <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.sp ...

  10. leetcode-733-Flood Fill

    题目描述: An image is represented by a 2-D array of integers, each integer representing the pixel value ...