【问题标题】:Why C has so many different types? [closed]为什么C有这么多不同的类型? [关闭]
【发布时间】:2015-07-01 09:12:09
【问题描述】:

我写了一个简单的定时器函数来计算startend之间经过的时间

double mytimer(struct timeval *start, struct timeval *end)
{
    return (end->tv_sec - start->tv_sec) + (end->tv_usec - start->tv_usec)*1e-6;
}  

gcc 给出以下警告:

警告:从“__suseconds_t”转换为“double”可能会改变其值
警告:从“__time_t”转换为“double”可能会改变其值

这里是timeval的定义:

struct timeval {
    time_t      tv_sec;     /* seconds */
    suseconds_t tv_usec;    /* microseconds */
};

所以我的问题是,为什么 C 定义了这么多 incompatible 类型,而不是简单地使用原始类型,例如 int short ...?这根本不是用户友好的。
我怎样才能对这些类型进行算术运算?

更新

你们中的大多数人似乎都忽略了我的第二个问题。添加time_tsuseconds_t等两种不同类型的标准方法是什么?

【问题讨论】:

  • suseconds_t 可能类似于 std::size_t .. 或其他无符号类型。
  • 正是为了防止用户朝自己的脚开枪,正如您很乐意尝试做的那样。从长远来看,它非常用户友好的,因为你花在调试上的时间要少得多:如果你的程序不正确,它甚至不会编译,编译器会告诉你原因。跨度>
  • 谢天谢地,您从未听说过 C# (.NET) 或 Haskell 之类的语言:}
  • "仅仅使用基本类型,例如int short ..." intshort 不一定大到足以存储实现的范围tv_sec 值。如果 tv_sec 是一个 64 位值,那么警告您在转换为 double 时可能会丢失精度是否合理且可能有用?而且它们不一定是“这么多不同的类型”——它们可能是typedefs 对于更简单、熟悉的类型,但编译器会显示typedef 名称,因为它通常提供更多信息。
  • the C standard 在哪里定义suseconds_t,作为不完整类型 还是其他?

标签: c type-conversion


【解决方案1】:

因为time_t 等包含的内容是实现定义的,所以没有什么说它们应该包含作为整数的秒数,就像代码中的注释所暗示的那样。原因是他们希望这些类型能够在不同系统之间移植。

在实践中,time.h 确实相当麻烦,所以大多数时候程序最终都会调用系统特定的函数。

【讨论】:

  • 你将如何编写 mytimer() 来消除警告?
【解决方案2】:

正如蝴蝶正确指出的那样,c++ 语言和标准库设计的目的是在编译时强制执行逻辑正确性。

这意味着我们的目标是这样一种情况,即在某些晦涩的情况下会做错事的程序根本无法编译(或将编译,但会警告您)。

这意味着您可以在您的软件甚至在您的测试工具中运行之前修复这些逻辑错误,更不用说在您的客户面前了。

这样做的结果是,正确编写的 c++ 代码可以在运行时之前大部分被证明是“正确的”,这意味着您可以花费更少的时间来跟踪原本会发现的晦涩错误。

c++ 的魔力在于它执行了这一令人难以置信的壮举,同时提供了出色的效率和代码优化。

注意:“正确”是指它会可靠地执行您认为您告诉它执行的操作。写出正确的逻辑还是要靠你!

关于问题:

所以我的问题是,为什么 C 定义了这么多不兼容的类型,而不是简单地使用原始类型,例如 int short ...?它根本不方便用户使用。

将它们设置为不兼容是为了故意阻止您将它们相互转换。它们代表不同的概念,就像速度和距离是不同的概念一样。它们之间没有直接转换。

我该如何对这些类型进行算术运算?

使算术的中间结果可以安全地转换为这些类型而不会丢失精度。在这种情况下,tv_sec 和 tv_usec 是整数类型,因此即使它们彼此不兼容,它们都可以单独转换为 double。

例如:

double mytimer(struct timeval *start, struct timeval *end)
{
    return double(end->tv_sec - start->tv_sec) + 
           double(end->tv_usec - start->tv_usec) * 1e-6;
}

【讨论】:

  • 您还没有真正回答 OP 的问题。你没有解释在他的情况下如何提高正确性,你也没有告诉他编写更安全和/或不会受到奇怪警告的代码。
  • @SteveSummit 感谢您强调史蒂夫 - 我已经更新了答案。
【解决方案3】:

为了避免警告

double mytimer(struct timeval * start, struct timeval * end) {
    long usec = end->tv_usec - start->tv_usec;
    long sec = end->tv_sec - start->tv_sec;
    return 1.0 * sec + 1e-6 * usec;
}

如果您看到<sys/types.h> 中定义的数据类型,您自然会发现

typedef long time_t;
...
typedef long suseconds_t;

struct timeval 没有特定的格式说明符,但结构成员的类型为 long。希望这能解决您的问题。

【讨论】:

  • 显式类型转换确实消除了警告。但是我怎么知道选角是 100% 正确的呢?铸造方式优雅吗?
  • @duleshi 另请阅读stackoverflow.com/questions/471248/…的答案
  • 有趣我也在读这个!但是在使用它们之前,我是否必须知道每种类型定义类型的详细信息?未来的实施可能会发生变化。我认为这不合理。
  • @duleshi 这主要是基于意见。如果您不想这样做,请迁移到 C++ 并使用来自std::chrono 的函数。
  • 使用 int 作为中间类型是一个非常糟糕的主意。此代码在 16 位机器上将完全失败。
【解决方案4】:

原因是像 int 这样的内置类型是平台相关的。因此,在一台计算机上,int 可能足以存储时间值,而在另一台计算机上则需要 long。为了允许人们编写在所有平台上运行的程序,引入了像 time_t 这样的类型,它们通常只是适用于该特定平台的某些原始类型的别名定义。这实际上在开始时需要更多的学习努力,但从长远来看,这种努力会带来巨大的回报。

有道理,不是吗?

[编辑]:至于奇怪的警告:编译器警告将 time_t 和 suseconds_t 转换为 double 可能会丢失一些信息。这是因为这两种类型都是整数类型,其位数比 double 的尾数部分多。在您的情况下,这仅适用于非常大的时间值,因此您可以简单地忽略这些警告。 (但是编译器如何知道 time_t 值通常适合双精度值?所以他发出了这个警告。)事实上,如果不使代码依赖于平台,你无能为力。

【讨论】:

  • 您还没有真正回答 OP 的问题。你没有解释在他的情况下如何提高正确性,你也没有告诉他编写更安全和/或不会受到奇怪警告的代码。
【解决方案5】:

[注:自从我第一次发布这个答案以来,我几乎完全重写了这个答案。]

第一个问题的答案是,C 有这么多类型,试图平衡支持所有不同字长的机器的需求,并具有合理的可移植性。事情变得更加复杂,因为希望也可以合理地支持诸如“数据结构的大小”、“文件中的偏移量”和“现实世界中的时间”之类的特殊数量,尽管有时这些特殊数量会结束up 不是由语言规范或编译器决定的,而是由底层操作系统决定的。

一般来说,从大整数类型转换为浮点类型时有两个问题:

  1. 浮点类型可能无法准确表示整数类型的所有有效数字

  2. 浮点类型甚至可能无法处理整数类型的范围

对于time_t,还有一个额外的问题:

  1. 类型 time_t 可能不是您可以有意义地减去的整数秒数

(不过,这些天来,针对这些担忧的“有用”编译器警告有时似乎有点像保姆,我和你一样担心。很难理解编译器实际上担心什么晦涩难懂的情况,并且很难看到如何在没有警告的情况下重写代码,并且很难确保您最终必须插入的任何强制转换最终不会使代码更不安全。)

如果您不担心问题 #3(如果您愿意假设 time_t 是整数秒),则可以通过先进行减法(在整数类型),然后转换:

return (sometype)(end->tv_sec - start->tv_sec) +
       (sometype)(end->tv_usec - start->tv_usec) / 1e6;

当然,最大的问题是,sometype 应该是什么?

我相信你最好的选择是在每个地方投到double。 C 中double 类型的保证范围和精度都相当大。因此,除非您操纵的时间差大于 1e50 年(除非有人将类型 subsec_t 实现为 266 位类型或其他类型),否则即使使用警告抑制强制转换,您的代码也应该是安全的,并且您可以插入对此发表评论。

如果您想了解这些问题在实践中的实际表现,很容易证明它们。试试这个:

float f = (float)2000000123L - (float)2000000000L;
printf("%f\n", f);

如果您有 64 位编译器,即使使用双精度也可以观察到精度损失:

double d = (double)9000000000000001234LL - (double)9000000000000000000LL;
printf("%f\n", d);

在我的机器上,这两个片段分别打印1281024

我不确定您的编译器试图警告您的三个问题中的哪一个。 #1 是最有可能的。如果在减法之后而不是之前进行转换,您可以看到精度损失是如何消失的:

f = 2000000123L - 2000000000L;
d = 9000000000000001234LL - 9000000000000000000LL;

f = (float)(2000000123L - 2000000000L);
d = (double)(9000000000000001234LL - 9000000000000000000LL);

当我们只有 32 位长整数和 64 位双精度数时,这在实践中并不是什么大问题(因为 IEEE 754 doubles 具有类似 52 位的精度)。现在 64 位类型变得司空见惯,这些类型的警告正变得越来越普遍。如果您对 double 类型具有足够的精度来满足您的所有时间减法需求感到满意,您可以使用适当的强制转换为 double 以使警告静音。 (同样,这里的“适当”是指“减法之后”。)如果你想更安全,你可以改为输入long double。如果你的编译器支持它,并且它确实比普通的double“更长”,它可以真正减少精度损失问题。 (这是前面使用long double的例子:

long double ld = (long double)9000000000000001234LL - (long double)9000000000000000000LL;
printf("%Lf\n", ld);

在我的系统上这个打印1234。)

但话虽如此,在这种情况下,如果您想真正让您的生活更轻松 - 顺便说一句,同时解决第 3 个问题 - 您可以并且可以说应该使用标准函数进行计算差异。减去两个time_t 值的标准函数是difftime。 (difftime 的工作是担心所有这些事情,包括time_t 不直接代表秒的可能性。)所以你可以写

return difftime(end->tv_sec - start->tv_sec) +
       (double)(end->tv_usec - start->tv_usec) / 1e6;

虽然当然还有亚秒级的问题。

最好的解决方案是一个预先编写的库函数来减去两个timeval,您可能需要花一些时间寻找其中一个。

【讨论】:

  • 感谢您真正理解我的问题。其他大多数人都在谈论类型安全,但事实上,不了解类型安全我并不是那么愚蠢。但是,我仍然对 C 哲学不满意。 1)需要显式转换的 API 很丑陋; 2)做铸造,我必须知道所涉及的类型的细节,这肯定不是一个好的设计。为什么用户必须了解你的结构的细节?我更喜欢 OOP 方式class timer{public: void start(); void end(); double get_diff();};
  • @duleshi:不客气。 (我想你知道类型安全。)关于你关于 OOPiness 的后续问题,显然 C 不是一种 OOP 语言,但是你的问题和我们在这里对它的分析已经证明我很满意我们绝对需要一个准-standard timevalsubtract 函数,如果还没有的话。 (也许我会尝试游说 GNU 人员添加它。)
  • 我快速搜索了任何标准时间值操作函数,到目前为止,我找到的只是this(在撰写本文时,它表示 GNU 没有)。
  • 请注意,具有微秒分辨率的struct timeval 已被 POSIX 有效弃用,应优先使用struct timespec(具有纳秒分辨率)。这意味着在struct timeval 上进行算术运算的函数将比在struct timespec 上工作的等价函数使用更少。也就是说,BSD 在/usr/include/sys/time.h 中提供了一些宏来处理struct timevaltimeraddtimersubtimercleartimerissettimercmp — 时间值操作)。在任何替代方案中效仿这些都是合理明智的。
  • @JonathanLeffler:感谢您对 BSD timeval 宏的提醒。不过,对于您的另一点,似乎并不是每个人都知道struct timeval 已“有效弃用”,正如我们在this other question 中了解到的那样。
【解决方案6】:

原因被称为类型安全。并非所有类型的表达式在工作程序中都有意义。而类型安全意味着编译器将拒绝允许不安全、无效或不适当的操作。

从长远来看,拒绝此类不良代码的编译器可以节省程序员的工作量,因为程序员在没有帮助的情况下检测问题比编译器花费更多的时间。

C 和 C++ 具有许多类型安全特性。其他编程语言有更多,其他更少。这仅仅意味着某些编程风格在不同的编程语言中更有效。

警告是一种不那么规范的类型安全形式——它们会导致编译器警告可疑的事情,而不是完全拒绝它们。

【讨论】:

  • 您还没有真正回答 OP 的问题。你没有解释在他的情况下如何提高正确性,你也没有告诉他编写更安全和/或不会受到奇怪警告的代码。
  • OP 没有询问是否改进了正确性。他问为什么编译器会抱怨它们之间的无效操作有多种类型。 “类型安全”是对此的简短回答。
  • 我只是在他添加了他的更新后才阅读这个问题。
【解决方案7】:

虽然这些类型通常使用标准整数类型实现,但您不能为了可移植性而假设这一点。标准库为转换等提供了一些方便的函数。例如,如果你想计算时间之间的延迟,你可以使用difftime,它返回一个double

【讨论】:

    猜你喜欢
    • 2023-03-14
    • 1970-01-01
    • 2013-04-22
    • 2022-01-21
    • 1970-01-01
    • 1970-01-01
    • 2023-03-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多