在cocos2d-x中的Sequence是一种特殊的动作,它可以表示一个动作序列。创建该动作通常使用这样的代码:
Action* sequence = Sequence::create(acttion1, action2, action3/*, action4, action5...*/, nullptr);
其中括号里的参数个数事实上是不限制的,可以是3个,也可以有更多。
那么这是如何实现的?
显然不应该是用重载函数的方式,不会有人想把1~9999个参数的函数都写一遍。
通过查看cocos2d-x中该函数的源码可以发现,该函数的参数中有这样的参数:
static Sequence* create(FiniteTimeAction *action1, ...) ;
这个省略号…就是实现的关键,这个函数就可以支持写不限个数的参数。
那么函数内部要如何使用那若干个参数呢?
函数的实参列表中并不包含那若干个参数,只是有一个…。
在调用该函数的时候,那些参数真的传进来吗?如果进来了,又被保存在哪里呢?
调用函数是一个入栈的过程,函数的参数也会依次入栈,包括那些被…涵盖的参数。
所以只需要知道…所代表的若干参数的首地址,就可以读到这些参数,而且…参数的使用有要求,其后不能再跟其它形参了,所以获取参数首地址后就能够不多不少地读取所有参数数据了。
也就是说参数是进来了,但是栈中的参数数据并不包含其数据类型,而函数形参也没有说明进来了哪些类型的数据,为了能够正确读取这些数据,只能靠程序员说明是什么类型了。
所以具体会有这些步骤:
- 指定…所覆盖的实参首地址,并以此获取所有实参的数据;
- 依次说明参数类型,依次按照该数据类型读取数据;
- 读取完毕,释放…所覆盖的实参内存;
具体实现方法
步骤1通过va_start宏实现,需要两个参数,第一个参数是用来指向…所覆盖参数数据的指针,通常是一个va_list类型的变量(其实就是一个char*),第二个参数是…左边第一个参数,相当于提供了…所覆盖参数的首地址。
步骤2通过va_arg宏实现,也需要两个参数,第一个参数是上面说的指向参数数据的指针,第二个是要取出参数的类型。有一个返回值,即当前读取出的一个参数。一次只能读一个参数,要是想继续读就要继续调用该宏。
步骤3通过va_end实现,传入指向参数数据的指针即可。
这里给出cocos2d-x中的实现代码:
Sequence* Sequence::create(FiniteTimeAction *action1, ...)
{
va_list params;
va_start(params, action1);//步骤1
Sequence *ret = Sequence::createWithVariableList(action1, params);//步骤2
va_end(params);//步骤3
return ret;
}
/************************************步骤2详细************************************/
Sequence* Sequence::createWithVariableList(FiniteTimeAction *action1, va_list args)
{
FiniteTimeAction *now;
FiniteTimeAction *prev = action1;
bool bOneAction = true;
while (action1)
{
now = va_arg(args, FiniteTimeAction*);//指定数据类型为 FiniteTimeAction*
if (now)//如果读到了
{
prev = createWithTwoActions(prev, now);
bOneAction = false;
}
else//如果读完了
{
// If only one action is added to Sequence, make up a Sequence by adding a simplest finite time action.
if (bOneAction)
{
prev = createWithTwoActions(prev, ExtraAction::create());
}
break;
}
}
return ((Sequence*)prev);
}
几个小问题
- 上面说的va_start需要提供…左边第一个参数,是否是不能有纯…作为参数的函数,比如像这样的:
void fun(...);
但是这样写编译是通过的,那va_start第2个参数要写什么呢?如何才能正确获取参数数据呢?这种情况参数的首地址应该就是…所覆盖参数首地址,但这该如何说明呢? - 如果只看上面的代码来理解va_arg的用法可能会产生偏差。我一开始就以为如果参数读取完的话就会返回null(其实这种想法本身就不合理)。后来才知道原来va_arg本身不能告诉你参数是不是读完了,所以需要手动告知有几个参数。这有两个办法,一个是添加加一个参数用来说明…中参数的数量。另一个我觉得更好一些的办法是事先约定一个结束标志,然后在调用该函数时,一定以该结束标志作为最后一个实参。这样当va_arg读取到结束标志时,就可以知道读取结束了。cocos2d-x就是用的这种办法,在使用 Sequence::create()方法时需要最后一个参数必须为nullptr。
总结
…说实话使用起来感觉非常原始和不安全,还是要慎重使用。
笔者许多理解可能不当或者错误,欢迎批评指正。