[CPP] Object Based Class
前言
几年前接触到一款很好玩的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;
你也许会有疑问,依照我们先看函数内是否使用了局部变量的原则,这里的返回值是可以使用引用的,而且应该使用引用。嗯,看起来确实如此,这是编写标准库的大佬们写出的代码,他们也会犯错,或者说也会写出不是最优的代码。当然了,这样的写法没有错,只是不是最优的写法,标准库并不是圣经,你可以去研究她,发现她的亮点和不足。
总结
设计类是最基本的技能,也是决定代码效率的关键一环,以下是我的认为设计类需要考虑的问题:
- 设计一个类,首先考虑有什么数据
- 考虑数据的类型,是public,还是private,还是friend
- 构造函数:是否有默认参数;有没有使用初始化列表
- 函数参数:首先考虑传引用;要不要加const;使用者会不会创建一个const的对象!
- 函数返回:是不是适合传引用?局部变量不适合传引用!
- 函数重载:使用者以不同的方式进行操作,比如double+complex complex+double
- cout重载:应该放在类外面,不适合做成员函数,因为没有人会倒着写
- this指针:调用成员函数都会隐藏的加上this指针,除了static
- static变量:必须在类的外面进行定义!类里面只是声明!声明没有内存!
第一篇攻略就先到这了,我们下次再见。
Reference
C++面向对象高级编程, 侯捷.
[CPP] Object Based Class的更多相关文章
- [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 ...
- Poco::JSON::Array 中object 设置preserveInsertionOrder 时,stringify出错-->深入解析
在使用poco version 1.6.0时 Poco::JSON::Array 在object 设置preserveInsertionOrder =true 时 调用 array.stringif ...
- Page Object Model (Selenium, Python)
时间 2015-06-15 00:11:56 Qxf2 blog 原文 http://qxf2.com/blog/page-object-model-selenium-python/ 主题 Sel ...
- 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 ...
- [TypeScript] Shallow copy object by using spread opreator
For example we have an object: const todo = { text: "Water the flowers", completed: false, ...
- 译:Boost Property Maps
传送门:Boost Graph Library 快速入门 原文:Boost Property Map 图的抽象数学性质与它们被用来解决具体问题之间的主要联系就是被附加在图的顶点和边上的属性(prope ...
- 【转】cocos2d-x Lua
Call custom c++ from Lua cocos2d-x lua binds c++ class, class functions ,enum and some global functi ...
- cocod2d-x 之 CCDirector、CCScene、CCSprite
CCDirector是控制游戏流程的主要组件. typedef enum { /// sets a 2D projection (orthogonal projection)2D投机模式 kCCDir ...
- GDI+ 摘要: 保存图像文件
要保存图像文件,必须先获得图像的编码格式信息.可是GDI+没有直接提供这个函数:GetEncoderClsid(const WCHAR* format, CLSID* pClsid) 因此须要我们自己 ...
随机推荐
- django model中的save()方法
Model.save(force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS, update_fields=None) id和pk ...
- java环境和Tomcat环境
这些变量名是一样的,变量的值需要自己根据自己的安装位置来确定 JAVA_HOME C:\Program Files\Java\jdk1.8.0_151 CATALINA_HOME(这个可能不需要) D ...
- 类似gitlab代码提交的热力图怎么做?
本文由 网易云发布. 作者:张淞(本篇文章仅限知乎内部分享,如需转载,请取得作者同意授权.) 昨夜,网易有数产品经理路过开发的显示屏前见到了类型这样的一张图: 于是想到有数能不能做出这样的图来?作为 ...
- CF1109DSasha and Interesting Fact from Graph Theory(数数)
题面 传送门 前置芝士 Prufer codes与Generalized Cayley's Formula 题解 不行了脑子已经咕咕了连这么简单的数数题都不会了-- 首先这两个特殊点到底是啥并没有影响 ...
- Kali Linux来袭~老司机带你进击
Kali是BackTrackLinux完全遵循Debian开发标准彻底的完全重建.全新的目录框架,复查并打包所有工具,我们还为VCS建立了Git树. 本次推荐内容主要介绍Kali-Linux的安装,包 ...
- SAE实践——创建新应用开启MySQL服务
1. 创建SAE应用 当创建完成SAE账户之后,即可创建SAE应用.点击创建新应用按钮,创建一个新的SAE 应用 阅读提示信息,等待五秒,点继续创建. 填写应用信息完成创建.可选择PHP5.3和空应用 ...
- 如何使用robots禁止各大搜索引擎爬虫爬取网站
ps:由于公司网站配置的测试环境被百度爬虫抓取,干扰了线上正常环境的使用,刚好看到每次搜索淘宝时,都会有一句由于robots.txt文件存在限制指令无法提供内容描述,于是便去学习了一波 1.原来一般来 ...
- C++基础知识:成员函数、对象拷贝、私有成员
一.综述 类是我们自己定义的数据类型(新类型) 设计类时要考虑的角度: (1)站在设计和实现者的角度来考虑 (2)站在使用者的角度来考虑 (3)父类,子类 二.类基础 (1)一个类就是一个用户自己定义 ...
- leetcode-867-Transpose Matrix(矩阵由按行存储变成按列存储)
题目描述: Given a matrix A, return the transpose of A. The transpose of a matrix is the matrix flipped o ...
- javascript如何阻止事件冒泡和默认行为
阻止冒泡: 冒泡简单的举例来说,儿子知道了一个秘密消息,它告诉了爸爸,爸爸知道了又告诉了爷爷,一级级传递从而以引起事件的混乱,而阻止冒泡就是不让儿子告诉爸爸,爸爸自然不会告诉爷爷.下面的demo ...