第十三章 结构体和类
一、结构体(struct):结构体,原手册中翻译为架构,意思一样,但不是通用的术语。就是把一堆相互关联、属于同一个整体的多条数据,打包装成一个新的自定义的数据类型。 结构体与前面说过的枚举enum有些相同的特点。首先它也允许你自创新的结构体数据类型,并自己给它起个名字,然后结构体也分自带的和你自己创建的,最后是结构体内部也可以写数量不限的多条内容。 但结构体与枚举最明显的不同是,枚举提供多条选项,你只能选一个,其他选项作废。结构体提供多条内容,你可以全用上,也可以用一部分甚至只用其一条,当你使用结构体的部分数据时,不影响其他数据继续被调用。 结构体与枚举最大的不同是,枚举内部的数据本质上都是标记,选择了某个标记,再转给相对应的函数去运行。而结构体的本质是一堆带名字的数据集合,它里面的数据可以是前面说过的任意类型的数据,甚至还可以是结构提本身,也就是说,结构体可以嵌套结构体。 下面是我自己的趋势分析代码中的写的一个结构体,可以用来展示结构体长什么样子。 struct STrendSegment // STrendSegment是我自己编的结构体名字,S代表这是个结构体,TrendSegment就是趋势细节的意思 { // -------- 基本标识 -------- int direction; // 整数型,方向, 用整数数字代表两种方向,比如1=多头,-1=空头 int segmentId; // 整数型,标记编号,给划分出来的趋势段唯一编号(从0开始递增的正整数)
// -------- 起止信息 -------- datetime startTime; // 日期时间型,起点K线时间(交叉发生时刻) datetime endTime; // 日期时间型,终点K线时间(反向交叉发生时刻) double startPrice; // 双精度浮点型,起点价格(起点K线的开盘价) double endPrice; // 双精度浮点型,终点价格(终点K线的收盘价) int startIndex; // 整数型,起点K线在rates数组(自己定义的储存K线参数的数组)中的索引 int endIndex; // 整数型,终点K线在rates数组(自己定义的储存K线参数的数组)中的索引
// -------- 极值信息 -------- double highestPrice; // 双精度浮点型,该趋势段内的最高价 double lowestPrice; // 双精度浮点型,该趋势段内的最低价 datetime highestTime; // 日期时间型,最高价出现的时间 datetime lowestTime; // 日期时间型,最低价出现的时间
// -------- 统计信息 -------- int barCount; // 整数型,总共包含的K线根数(不含起点K线) double priceChange; // 双精度浮点型,价格变动幅度(endPrice - startPrice) double priceChangePercent; // 双精度浮点型,价格变动百分比
// -------- 波段点记录 -------- double swingHighs[]; // 双精度浮点型,波段高点价格列表,是一个数组 datetime swingHighTimes[]; // 日期时间型,波段高点对应时间,是一个数组 double swingLows[]; // 双精度浮点型,波段低点价格列表,是一个数组 datetime swingLowTimes[]; // 日期时间型,波段低点对应时间,是一个数组 int swingHighCount; // 整数型,波段高点数量 int swingLowCount; // 整数型,波段低点数量 }; 这就是个参数列表集合。你想定义一个趋势,必须要描述它是什么样子的,具备什么特征,这些特征有很多,种类也有很多。但所有的趋势段的参数都是一样的,不存在有的趋势有最高价,有的趋势没有最高价。所以你就可以定义这么一个参数集合,只要是符合你标准的趋势,都有这些参数,这些参数如果觉得不合适,还可以改。 系统自带的结构体也是这样。比如MQL5 执行交易最核心的系统内置结构体MqlTradeRequest,就是专门用来存放下单、挂单、平仓、修改订单的全部请求参数。因为你每次下单,都是在处理这些参数,不存在哪次下单不需要这些参数或者需要新的其他参数。所以系统把这些参数固定下来,形成一个结构体,它完整的内容是下面这样的:struct MqlTradeRequest { ENUM_TRADE_ACTION action; //交易动作:做什么操作,就是前面在枚举章节举的例子 ulong magic; //EA魔术数,区分本EA的订单 ENUM_ORDER_TYPE type; //订单类型:市价买、市价卖、限价、止损 ENUM_ORDER_TYPE_FILLING type_filling; //成交模式 ENUM_ORDER_TYPE_TIME type_time; //订单有效期模式 datetime expiration; //挂单过期时间(仅有效期模式生效) double volume; //交易手数 double price; //挂单价格,市价单可以填0 double stoploss; //止损价,不需要填0 double takeprofit; //止盈价,不需要填0 ulong position; //持仓ID,平仓/修改持仓时用 ulong position_by; //对冲模式下反向持仓ID string symbol; //交易品种名称 ulong comment; //注意!不是字符串,是注释ID,几乎不用 ulong deviation; //滑点,点为单位 }; 本质上,再复杂再高级的策略分析程序,不过是给这个结构体提供数据罢了。它把你几千几万行代码运行出来的结果,转化成十四个数字+一条品种名称文字的集合,整个交给OrderSend,剩下的事情,系统会自己完成。 如果把最终成交的单子比喻成射出去的子弹,OrderSend就是MT5平台为程序保留的扳机,而MqlTradeRequest结构体就是扣动这个扳机的手指头,给OrderSend十五个数,子弹就能射出去。二、结构体的格式:
struct structure_name //struct+空格+自定义结构体名称
{
elements_description //结构体各元素的描述,都是 数据类型+空格+变量名称+分号,最后一个元素也要带分号
};
三、访问结构体成员使用点运算符 . 来访问结构体成员。比如: struct TickInfo //创建一个名叫TickInfo的结构体数据类型 { datetime time; //自创结构体的成员1 double bid; //自创结构体的成员2 double ask; //自创结构体的成员3 }; TickInfo tick; // 用结构体TickInfo创建一个变量tick tick.bid = 1.234; // 通过点 . 访问结构体成员(操作点进入) 并给它赋值1.234 Print(tick.ask); //最后你看到的输出结果,就是1.234,相当于程序到这一步,就记住了成员bid就等于1.234 四、DLL 交互与结构体内存对齐注意:99% 的MQL5 开发者一辈子不用碰 DLL 交互。结构体(struct)如只在程序内部使用,不用考虑这项内容。以下只做知识性了解。 MQL5 有一定的局限性,很难做复杂数学运算、调用 GPU、读写特殊格式文件、调用 Windows 系统底层功能、做加密解密、运行自己写的复杂算法等。这时可以把这部分逻辑写成 C++(也可以用其他语言),编译成 dll 文件;然后让 EA(MQL5)去调用这个 dll 里面的函数,这就叫 DLL 交互。 两种语言的不同编译器,对结构体内存排布处理不一样。看上去完全相同的一个结构体,在C++中和在MQL5中,内存排布处理是不一样的。在C++ 端可以用#pragma pack(1)指令,控制 C++ 侧的内存布局。但MQL5这边没有这个命令,只能手动添加filler 占位字节,人工补齐空洞,让 MQL5 的内存偏移跟 C++ 完全对齐。 说白了,要做DLL 交互,需要跨语言传递结构体,内存布局必须 100% 一致,否则数据错位,系统无法正常读取数据。 在这种情况下,MQL5编的结构体名称,也不能跟C++语言的关键字重叠,否则会报错。 原手册中关于“简单构造结构体”的介绍,以及后面的“'pack'用于对齐架构和类字段”,也是DLL交互的内容,建议忽略。 五、关于类类(class)是数据和函数的结合体。它与结构体形式上类似,但本质上完全是两个东西。 结构体就好像是一个填数据的表格,你写啥,它保存啥,擦了重写也行。但类(class)自带处理函数,以及数据输入和输出结构。它自己接收数据,自己处理,处理完的数据放在自己的私有存储中,不允许外部随意写入或者调用。 MQL5语言被称作面向对象的编程语言,类(class)就是设计和创造对象的主要手段。如果你的策略比较简单,一般用不到(class),稍微上点强度的策略,还是用(class)来设计比较方便。 另外,因为类的概念涉及到函数、指针、对象等等比较复杂的其他概念,在此我不展开有关类的内容了,各位先了个大概,等我们写完了其他概念的内容,再回头详细研究类。 下面仅就类的格式做一下说明:(1)class类:不但装数据,还可以装处理这份数据的函数,还支持权限控制、继承、多态。 (2)类的内容,不完全是公开的,有三种访问权限控制类型: private私有:只能类内部自己的函数读取,外部代码不能直接访问。
public公开:外部可以通过.直接读写、调用。
protected保护:仅本类和子类可以访问。 class 的成员默认就是 private(私有的),而 struct 的成员默认是公开的。可以保护内部数据,不让外部随意乱改,只能通过你提供的函数修改数据,避免非法数值。 (3)构造函数:名字和类名一模一样,对象被创建的时候自动执行,用来初始化、给成员赋初始值,防止类的成员残留了之前运行的一些垃圾值。 关于聚合初始化:结构体和类(仅限于公开成员)内部可以写构造函数,也可以不写构造函数。当没有构造函数时,可以通过大括号{}来直接给内部成员赋值,实现初始化,这叫聚合初始化。当写了构造函数时,就不允许通过大括号{}来直接赋值的操作了。 (4)析构函数:名字前面加~;对象销毁、释放的时候自动执行,用来做清理工作(关闭文件、释放资源)。 (5)私有函数:private:下面写的函数,也叫内部工具函数。只供类内部别的接口函数调用,不给外部使用,不属于对外接口。 (6)接口函数:类的接口函数就是放置在 public 下的成员函数,是外部代码和对象交互的唯一通道。分为三类: 写入(输入)接口:接收外部传入的数据,内部可做校验过滤,存入私有成员;
读取(输出)接口:把内部私有数据(或计算加工后的结果)返回给外部代码;
动作接口:下发指令,让对象内部执行清空、计算等业务动作。 接口函数最大意义:保护私有数据,做数据校验,隐藏内部实现细节。内部逻辑修改时,只要接口不变,外部业务代码无需改动。 |