我已经看到它多次断言C ++标准不允许以下代码:

int array[5];
int *array_begin = &array[0];
int *array_end = &array[5];
在这种情况下是否合法的C ++代码?

如果可能的话,我希望参考标准。

知道它是否符合C标准也很有趣。如果不是标准C ++,为什么要决定将其与&array[5]array + 5区别对待?

评论

@Brian:是的,如果需要运行时才能捕获错误,则只需要进行边界检查。为了避免这种情况,该标准可以简单地说“不允许”。最好的情况是未定义的行为。不允许这样做,也不需要运行时和编译器告诉您是否执行此操作。

好的,请澄清一下,因为标题误导了我:指向数组末尾的指针不会超出范围。通常不允许使用超出范围的指针,但是标准的“过去一末端”指针要宽松得多。如果您专门询问最后一指针,则可能需要编辑标题。如果您想大致了解越界指针,则应编辑示例。 ;)

通常,他并不是在问指针。他正在询问使用&运算符获取指针。

@Matthew:但是答案取决于指针指向的位置。允许您采用过去最后一次的地址,但不允许超出范围。

5.3.1.1节一元运算符“ *”:“结果是引用对象或函数的左值”。 5.2.1节下标表达式E1 [E2](通过定义)与*((E1)+(E2))相同。通过我在这里阅读标准。不会取消结果指针的引用。 (请参阅下面的完整说明)

#1 楼

您的示例是合法的,但这仅是因为您实际上并未使用界外指针。

首先让我们处理界外指针(因为这是我最初解释您的问题的方式,然后我才注意到该示例改为使用一个过去的指针):

通常,甚至不允许您创建越界指针。指针必须指向数组中的一个元素,或者指向末尾的一个元素。

甚至不允许指针存在,这显然也不允许您取消引用它。

这是标准中必须说明的主题:

5.7:5:


当将具有整数
类型的表达式添加到
指针或从中减去时,结果的类型为
指针操作数。如果指针
操作数指向
数组对象的元素,并且数组足够大
,则结果指向从原始
偏移的
元素元素,使得结果数组元素和
原始数组元素的下标之差等于
整数表达式。换句话说,如果表达式P指向数组对象的第i个
元素,则
表达式(P)+ N(相当于,
N +(P) )和(P)-N(其中N具有
值n)分别指向
数组对象的第i + n个元素和第i-n个元素,
此外,如果表达式P指向数组的最后一个元素
,则表达式(P)+1指向数组的最后一个元素一个数组
对象,如果表达式Q指向
数组的最后一个元素之后
,则表达式(Q)-1指向
该数组的最后一个元素数组对象。
如果指针操作数和
结果都指向同一
数组对象的元素,或者指向最后一个元素
数组对象的元素,
求值不应产生
溢出;否则,行为是未定义的。


(强调我的)

当然,这是针对operator +的。因此,可以肯定的是,这是标准关于数组下标的内容:

5.2.1:1:


表达式E1[E2]是相同的(按定义)到*((E1)+(E2))


当然,有一个明显的警告:您的示例实际上并未显示越界指针。它使用“结束后一个”指针,这是不同的。允许指针存在(如上所述),但是据我所知,标准没有提及取消引用。我能找到的最接近的是3.9.2:3:


[注意:例如,将数组末尾的地址(5.7)视为
指向数组元素类型的不相关对象,该对象可能位于该地址。 —尾注]


在我看来,是的,您可以合法地取消引用它,但未指定读取或写入该位置的结果。

感谢ilproxyil在这里纠正了最后一点,回答了问题的最后一部分:



array + 5实际上并没有取消引用任何东西,它只是简单地
创建指向array末尾的指针。

&array[4] + 1取消引用
array+4(非常安全),
采用该地址左值,并向该地址添加一个
,该地址
导致最后一个指针
(但该指针永远不会被取消引用。

&array[5]取消引用array + 5
(据我所知,这是合法的,
导致“数组元素类型的不相关对象
”,作为
以上所述),然后采用该元素的
地址,这也很合法。

因此,他们做的事情并不完全相同,尽管在这种情况下,最终结果是相同的。

评论


&array [5]指向过去。但是,这不是获得该地址的合法方法。

–马修·弗拉申(Matthew Flaschen)
09年6月12日在18:27

最后一句不正确。 “ array + 5”和“&array [4] + 1”不会在末尾取消引用,而array [5]会取消引用。 (我还假设您的意思是&array [5],但该注释仍然有效)。前两个只是指向终点。

–user83255
09年12月12日在18:52

@jalf注意-我认为如果b在数组“ a”之后直接分配,则“ a + sizeof a”与“&b”同等有效,并且得出的地址同样“指向”相同的内容目的。不多。请记住,所有注释都是信息性的(非规范性的):如果要陈述这样的基本重要事实,例如数组对象之后的对象位于过去的末尾,则需要使该规则具有规范性

– Johannes Schaub-小人
09年6月12日在22:21

就ANSI C(C89 / C90)而言,这是正确的答案。如果您遵循字母的标准,则&array [5]在技术上是无效的,而array + 5是有效的,即使几乎每个编译器都会为两个表达式生成相同的代码。 C99更新了标准以明确允许&array [5]。请参阅我的答案以获取全部详细信息。

–亚当·罗森菲尔德
09年6月13日在1:56

@jalf,注释中的整个文本也以“如果类型T的对象位于地址A ...开头” <-表示“以下文本假定地址A处有一个对象。”因此,您的报价单没有(而且在这种情况下也不能)说在地址A处始终有一个对象。

– Johannes Schaub-小人
09年6月13日在13:06

#2 楼

是的,这是合法的。根据C99标准草案:

第6.5.2.1节第2段:


后缀表达式和方括号[]中的表达式是带下标的
数组对象元素的指定。下标运算符[]
的定义是E1[E2](*((E1)+(E2)))相同。由于
适用于二进制+运算符的转换规则,如果E1是数组对象(相当于指向数组对象的
初始元素的指针),而E2是整数,则E1[E2]指定E2 -th
E1的元素(从零开始计数)。


第6.5.3.2节,第3段(重点是我的):


一元&运算符产生其操作数的地址。如果操作数的类型为“类型”,
结果的类型为“类型的指针”。如果操作数是一元*运算符的结果,则
不对该运算符和&运算符进行求值,并且结果似乎都被省略了,只是对运算符的约束仍然适用并且结果不是
左值。同样,如果操作数是[]运算符的结果,则不会对&运算符或*所隐含的一元[]求值,并且结果就像删除了&运算符
并更改了[]运算符一样给+运算符。否则,结果是
指向由其操作数指定的对象或函数的指针。


第6.5.6节第8段:


将具有整数类型的表达式添加到指针或从指针中减去时,
结果具有指针操作数的类型。如果指针操作数指向
数组对象的元素,并且数组足够大,则结果指向距
的元素偏移量原始元素,以使结果数组元素和原始
数组元素的下标之差等于整数表达式。换句话说,如果表达式P指向数组对象的第i个元素,则表达式(P)+N(等效为N+(P))和
(P)-N(其中N具有值n)分别指向,
数组对象的第i+n个元素和第i−n个元素(如果存在)。此外,如果表达式P指向数组对象的last (P)+1指向一个数组对象的最后一个元素,如果表达式Q指向一个数组对象的最后一个元素。一个数组对象,表达式(Q)-1指向数组对象的最后一个元素。如果指针
操作数和结果都指向同一数组对象的元素,或者指向数组对象的最后一个
元素之后,则评估不应产生溢出;否则,
行为是不确定的。如果结果指向数组对象的最后一个元素,则它
不能用作被评估的一元*运算符的操作数。


如果未取消引用,则标准明确允许指针指向数组末尾的一个元素。在6.5.2.1和6.5.3.2中,表达式&array[5]等效于&*(array + 5),等效于(array+5),后者指向数组末尾。这不会导致取消引用(按6.5.3.2),因此是合法的。

评论


他明确询问了C ++。这是在两者之间移植时不能依靠的细微差别。

–马修·弗拉申(Matthew Flaschen)
09年6月12日在21:34

他问了两个问题:“知道它是否符合C标准也很有趣。”

– CB Bailey
09年6月12日在21:42

@Matthew Flaschen:C ++标准通过引用合并了C标准。附件C.2包含更改列表(ISO C和ISO C ++之间的不兼容性),并且所有更改均与这些条款无关。因此,&array [5]在C和C ++中是合法的。

–亚当·罗森菲尔德
09年6月12日在22:00

C标准是C ++标准中的规范性参考。这意味着C ++标准引用的C标准条款是C ++标准的一部分。这并不意味着C标准中的所有内容都适用。特别是附件C是提供信息的,不是规范性的,因此,仅因为本节中未突出显示差异并不意味着C“版本”适用于C ++。

– CB Bailey
09年6月12日在22:20

@亚当:谢谢你指出这一点。我从未完全确定C的历史。关于C ++ 0x,指的是C99,我对C99的更改知之甚少,我很确定C ++会继续参考C89 / 90,并且会逐案挑选C99的“理想”更改。这个问题/答案就是一个很好的例子。我想说C ++将继续使用“从左值到右值”(novalue-to-rvalue),因此没有未定义的行为,而不是集成“&* == no-op”的措辞。

–理查德·科登(Richard Corden)
09年6月17日在7:49

#3 楼

这是合法的。

根据C ++的gcc文档,&array[5]是合法的。在C ++和C语言中,您都可以安全地在数组末尾寻址该元素-您将获得一个有效的指针。因此,将&array[5]作为表达式是合法的。因此,即使指针本身有效,尝试取消引用由该表达式生成的指针仍然是未定义的行为(即非法)。

实际上,我认为它通常不会导致崩溃。 br />
编辑:顺便说一句,这通常是实现STL容器的end()迭代器的方式(作为指向过去的指针),因此这是对实践的很好证明是合法的。

编辑:哦,现在我看到您不是真正在问持有指向该地址的指针是否合法,但是获取指针的确切方法是否合法。我会请其他回答者来回答。

评论


我会说,如果且仅当C ++规范没有说&*必须被视为无操作时,您才是正确的。我想它可能不会这么说。

–泰勒·麦克亨利(Tyler McHenry)
09年6月12日在18:33

您引用的页面(正确)说,指向终点是合法的。 &array [5],从技术上讲,首先要取消引用(数组+ 5),然后再次引用它。因此,从技术上讲,它是这样的:(&*(array + 5))。幸运的是,编译器足够聪明,可以知道&*可以归零。但是,他们不必这样做,因此,我要说的是UB。

–埃文·特兰(Evan Teran)
09年6月12日在18:34

@Evan:还有更多。请查看核心问题232的最后一行:std.dkuug.dk/JTC1/SC22/WG21/docs/cwg_active.html#232。那里的最后一个示例看起来是错误的-但它们清楚地说明了区别是在“左值到右值”转换上,在这种情况下不会发生。

–理查德·科登(Richard Corden)
09年6月12日在18:44

@Richard:有趣的是,似乎对此话题有些争论。我什至同意应该允许它:-P。

–埃文·特兰(Evan Teran)
09年6月12日在18:46

这与人们一直在讨论的“对NULL的引用”是一样的未定义行为,而且似乎所有人都投票赞成“这是未定义行为”的答案。

– Johannes Schaub-小人
09年6月12日在19:21

#4 楼

我认为这是合法的,并且取决于发生的“从左值到右值”的转换。最后一行核心问题232具有以下内容:我们同意该标准中的方法似乎还可以:p = 0; * p;本质上不是错误。从左值到右值的转换会带来不确定的行为


尽管这是一个稍有不同的示例,但它确实表明,'*'不会导致左值到右值的转换,并且因此,假设表达式是'&'的直接操作数,且期望该值是一个左值,则定义行为。

评论


+1为有趣的链接。我仍然不确定我是否同意p = 0; * p;定义得很好,因为我不认为'*'是针对其值不是指向实际对象的指针的定义良好的。

– CB Bailey
09年6月12日在19:25

作为表达的陈述是合法的,并且意味着评估该表达。 * p是一个可调用未定义行为的表达式,因此该实现所执行的所有操作均符合标准(包括向老板发送电子邮件或下载棒球统计信息)。

– David Thornley
09年6月12日在22:01

请注意,该问题的状态仍然是“起草”,并且尚未成为标准(尚未),至少我可以找到那些C ++ 11和C ++ 14的草案版本。

–音乐爱好者
15年7月2日在23:32

“空的左值”提案从未被任何已发布的标准采用

– M.M
1月13日4:36

问题中的注释显式涉及&* a [n]问题:“类似地,只要不使用该值,就应允许取消引用指向数组末尾的指针”。不幸的是,自2003年以来,这个问题一直没有解决,标准中还没有解决方案。

– Pablo Halpern
8月11日21:18

#5 楼

我不认为这是非法的,但我确实认为&array [5]的行为未定义。


5.2.1 [expr.sub] E1 [E2]是(根据定义)与*((E1)+(E2))相同。
5.3.1 [expr.unary.op]一元*运算符...结果是一个左值,它引用对象或函数。表达式点。

此时,您有未定义的行为,因为表达式((E1)+(E2))并未实际指向对象,并且标准确实指出了结果应该是什么,除非它

1.3.12 [defns.undefined]当本国际标准省略任何对行为的明确定义的描述时,也可能会出现未定义的行为。

如其他地方所述,array + 5&array[0] + 5是获得数组末尾一个指针的有效且定义明确的方法。

评论


关键点是:“'*'的结果是左值”。据我所知,仅当您对该结果进行从左值到右值的转换时,它才变成UB。

–理查德·科登(Richard Corden)
09年6月12日在18:41

我认为,“ *”的结果仅根据应用了运算符的表达式所定义的对象来定义,因此,如果没有表达式,则它是不确定的-省略-结果是什么实际引用对象的值。但是,这还远远不够。

– CB Bailey
09年12月12日在18:52

#6 楼

除了上述答案外,我还指出了可以将operator&覆盖为类。因此,即使它对POD有效,对您知道无效的对象也不是一个好主意(就像首先覆盖了operator&()一样。)。

评论


即使操作员建议不要覆盖它,因为某些STL容器依赖它将指针返回到元素中,因此即使讨论该问题,+ 1也是讨论的重点。这是他们在不了解之前就已经成为标准的事情之一。

–DavidRodríguez-dribeas
09年6月12日在19:28

#7 楼

这是合法的:

int array[5];
int *array_begin = &array[0];
int *array_end = &array[5];



第5.2.1节下标表达式E1 [E2](通过定义)与*((E1)+( E2))


因此,我们可以说array_end也等效:

int *array_end = &(*((array) + 5)); // or &(*(array + 5))



5.3节.1.1一元运算符'*':一元*运算符执行间接操作:对其应用的表达式应为指向对象类型的指针,或为
指向函数类型的指针,并且结果为引用的左值表达式指向的对象或函数。
如果表达式的类型为“指向T的指针”,则结果的类型为“ T”。 [注意:指向不完整类型(cv void以外的
)的指针可以被取消引用。这样获得的左值可以以有限的方式使用(以初始化引用为例)。此左值不能转换为右值,请参见4.1。 —结束语]


上面重要的部分:


'结果是引用对象或函数的左值'。 br />

一元运算符'*'返回一个引用int的左值(无反引用)。一元运算符'&'然后获取左值的地址。

,只要不取消对超出范围的指针的引用,该操作就被标准完全覆盖,并且所有行为定义。因此,根据我的阅读,以上内容完全合法。

许多STL算法都依赖于行为定义良好的事实,这暗示着标准委员会已经做到了这一点,并且我肯定有一个明确地涵盖了这一点的东西。

下面的注释部分提出了两个参数:

(请阅读:但是它很长,我们俩都最终trollish)

参数1

由于第5.7条第5款的规定,这是非法的


当将具有整数类型的表达式添加到指针或从指针中减去时,结果将具有指针操作数的类型。如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素偏移的元素,以使结果数组元素和原始数组元素的下标之差等于整数表达式。换句话说,如果表达式P指向数组对象的第i个元素,则表达式(P)+ N(相当于N +(P))和(P)-N(其中N的值为n)到数组对象的第i + n个元素和第i-n个元素,只要它们存在。此外,如果表达式P指向数组对象的最后一个元素,则表达式(P)+1指向数组对象的最后一个元素之后,如果表达式Q指向数组对象的最后一个元素之后,表达式(Q)-1指向数组对象的最后一个元素。如果指针操作数和结果都指向同一数组对象的元素,或者指向数组对象的最后一个元素,则求值不会产生溢出;


并且尽管该节是相关的;否则,该行为是不确定的。它不会显示未定义的行为。我们正在讨论的数组中的所有元素都在数组中,或者在末尾(在上面的段落中已定义)。

论点2:

下面显示的第二个参数是:*是取消引用运算符。
尽管这是用于描述'*'运算符的常用术语;在标准中故意避免使用此术语,因为术语“取消引用”在语言以及对底层硬件的含义方面定义不充分。

尽管访问超出数组末尾的内存绝对是未定义的行为。我不相信unary * operator在这种情况下(不是按照标准定义的方式)访问内存(对内存的读/写)。在这种情况下(如标准所定义(请参阅5.3.1.1)),unary * operator返回lvalue referring to the object。在我对语言的理解中,这不是访问基础内存。然后,该表达式的结果立即由unary & operator运算符使用,该运算符返回lvalue referring to the object所引用的对象的地址。

还提供了许多其他有关Wikipedia和非规范资源的参考。我发现所有这些都不相关。 C ++是由标准定义的。 。以下提供了NON。如果您给我看一个显示这是UB的标准参考书。我会


留下答案。
大写这是愚蠢的,我所有人都读错了。参数:


并非全世界都由C ++标准定义。敞开心mind。


评论


根据谁?根据哪个段落? *执行取消引用。它是取消引用运算符。这就是它的作用。可以争辩的是,随后获得一个指向结果值的新指针(使用&)是无关紧要的。您不能仅仅给出一个评估序列,给出最终表达的语义,并假装没有发生中间步骤(或者该语言的规则不适用于每个规则)。

–轨道轻赛
13年8月6日在11:58



我引用同一段话:结果是一个左值,表示表达式所指向的对象或函数。显然,如果不存在这样的对象,则不会为此操作符定义任何行为。您的后续语句返回一个指向int的左值(不进行反引用),这对我来说毫无意义。您为什么认为这不是取消引用?

–轨道轻赛
2013年8月6日12:00



它返回对所指向内容的引用。如果不取消引用,这是什么?这段话说*执行间接操作,而从指针到指针的间接操作称为解引用。您的论点实质上断言指针和引用是同一件事,或者至少是隐式链接,这根本不是事实。 int x = 0; int * ptr =&x; int&y = * x;在此我取消引用x。我不需要使用y就可以了。

–轨道轻赛
13年8月6日在12:04



@LokiAstari:我有一个问题,如果不“调用返回返回指向表达式所指向对象的左值的一元*运算符”,您认为“取消引用”意味着什么? (请注意,该标准的后续句子确实将此过程称为C ++ 11规范中的取消引用)

–鸭鸭
13年8月6日在17:25



@LokiAstari:我几天前给你看过参考资料;您出于某种原因拒绝承认它们的存在。如何证明这种行为的合理性超出了我的范围,但您必须是唯一的巨魔。

–轨道轻赛
13年8月10日在1:11



#8 楼

工作草案(n2798):“一元&运算符的结果是一个指向其操作数的指针。操作数
应为左值或限定值-id。
在第一种情况下,如果
表达式的类型为“ T”,则
结果的类型为“指向T的指针”。(第103页)


array [5]并不是我所能分辨的合格ID(列表在第87页);最接近的似乎是标识符,但是当array是标识符时,array [5]不是。它不是左值,因为“左值是指对象或函数。”(第76页)。 array [5]显然不是函数,并且不能保证引用有效对象(因为array + 5在最后分配的数组元素之后)。

显然,它在某些情况下可能有效,但它不是有效的C ++或安全代码。

注意:合法的是通过数组加一(p。113):


表达式P [指针]
指向数组的最后一个元素
,表达式(P)+1指向
指向数组的最后一个元素
,并且如果表达式Q指向
数组的最后一个元素
,则表达式(Q)-1指向
数组对象的最后一个元素。
如果指针操作数和
结果都指向同一个
数组对象的元素,或者指向数组对象的最后一个
元素之后的元素,则
求值不应产生
溢出”


但是使用&。这样做是不合法的。

评论


我赞成你,因为你是对的。没有任何对象可以保证位于过去的位置。打败您的人可能会误解您(您听起来好像说任何array-index-op都根本没有指向任何对象)。我认为这是一件有趣的事情:它是一个左值,但也没有引用对象。因此,这与标准所说的是矛盾的。因此,这会产生未定义的行为:)这也与此相关:open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#232

– Johannes Schaub-小人
09年6月12日在19:16

@jalf,注释说“可能位于该地址”。不保证有一个位于:)

– Johannes Schaub-小人
09年6月12日在19:28

该标准说op *的结果必须是一个左值,但是仅说明如果操作数是实际上指向对象的指针,那么左值是什么。这暗示着(粗俗地)如果到最后没有碰巧指向一个合适的对象,则实现必须从其他地方找到合适的左值并使用它。那真的会弄乱&array [sizeof array]!

– CB Bailey
09年6月12日在21:49

“但是,我仍然认为它不是左值。因为没有对象可以保证在array [5]处,所以array [5]不能合法地引用对象。” <-这就是我认为这是未定义行为的原因:它依赖于标准未明确指定的某些行为,因此在1.3.12 [defns.undefined]之内

– Johannes Schaub-小人
09年6月12日在21:58

小东西,还算公平。假设它不是/绝对是左值,因此绝对/不是100%安全。

–马修·弗拉申(Matthew Flaschen)
09年6月12日在22:48

#9 楼

即使合法,为什么还要违背约定?数组+ 5无论如何都更短,而且我认为可读性更高。

编辑:如果希望通过对称方式实现,则可以编写

int* array_begin = array; 
int* array_end = array + 5;


评论


我认为我在问题中使用的样式看起来更加对称:数组声明和begin / end指针,或者有时我将它们直接传递给STL函数。这就是为什么我使用它而不是较短的版本。

–赞·山猫
09年12月12日23:45

为了对称,我认为它必须是array_begin = array + 0; array_end =数组+ 5;长时间延迟的评论回复如何?

–赞·山猫
10 Mar 16 '10在18:44

可能是世界纪录:)

– rlbond
10 Mar 16 '10在20:56

#10 楼

出于以下原因,它应该是未定义的行为:


尝试访问越界元素会导致未定义的行为。因此,在这种情况下,标准不禁止实现抛出异常(即,在访问元素之前检查边界的实现)。如果将& (array[size])定义为begin (array) + size,则在越界访问的情况下抛出异常的实现将不再符合标准。
如果array不是数组而是一个数组,则无法产生end (array)的产量。任意集合类型。


#11 楼

C ++标准5.19,第4段:

地址常量表达式是指向左值的指针...。该指针应使用一元&运算符或使用数组(4.2)...类型下标运算符[] ...可以用于创建地址常量表达式,但是不能通过使用这些运算符来访问对象的值。如果使用下标运算符,则其操作数之一应为整数常量表达式。

对我来说,&array [5]是合法的C ++,是地址常量表达式。

评论


我不确定原来的问题一定是在谈论带有静态存储的数组。即使我想知道&array [5]是否不是因为不指向指定对象的左值的地址常量表达式吗?

– CB Bailey
09年6月12日在22:28

我认为数组是静态的还是堆栈分配的都不重要。

– David Thornley
09年6月13日在15:40

如果您引用的是5.19,则可以。您省略的部分说:“……指定静态存储持续时间的对象,字符串文字或函数。……”。这意味着,如果您的表达式包含分配给堆栈的数组,则不能使用5.19推理这些表达式的有效性。

– CB Bailey
09年6月13日在19:52

您的报价是说,如果&array [5]是合法的,并且引用了静态存储期限数组,那么它将是一个地址常量。 (例如,与&array [99]比较,本段中没有文字可区分这两种情况)。

– M.M
16年2月13日在0:06

#12 楼

如果您的示例不是一般情况,而是特定情况,则可以使用。您可以合法地通过AFAIK将内存移动到分配的内存块之外。
它不适用于一般情况,例如,当您尝试访问距离数组结尾较远1的元素时。

仅搜索C-Faq:链接文本

评论


最重要的答案是“合法”,我也说同样的话。那么为什么要下投票:)。我的答案有问题吗?

– Aditya Sehgal
09年6月12日在18:40

#13 楼

这是完全合法的。

当调用myVec.end()时,来自stl的vector <>模板类正是这样做的:它为您提供了一个指针(在此为迭代器),该指针指向数组末尾的一个元素。

评论


但这是通过指针算法实现的,而不是通过形成对过去的引用,然后将地址运算符应用于该左值。

– Ben Voigt
2014年10月10日,下午1:54