在C语言中,size_t是什么?

808

我对C语言中的size_t感到困惑。我知道它是由sizeof运算符返回的。但它究竟是什么?它是一种数据类型吗?

假设我有一个for循环:

for(i = 0; i < some_size; i++)

我应该使用int i;还是size_t i;


18
如果这是你唯一的选项,那么如果 some_size 是有符号的,使用 int,如果它是无符号的,使用 size_t - Nate
10
@Nate,那是不正确的。POSIX有一个ssize_t类型,但实际上使用的正确类型是ptrdiff_t。 - Molly Stewart-Gallus
6
在《低级编程:C、汇编和Intel® 64上的程序执行》一书中,答案并不像那么清晰。正如该书所述,使用索引“int i”可能无法足够地寻址一个巨大的数组。因此,通过使用“size_t i”,您可以寻址更多的索引,因此即使您有一个巨大的数组,也不应该成为问题。“size_t”是一个数据类型:通常是“无符号长整型”,但这取决于您的系统。 - bruno
15个回答

568

维基百科的定义如下:

根据1999年ISO C标准(C99),size_t是至少有16位无符号整数类型(参见7.17和7.18.3节)。

size_t是由多个C/C++标准定义的无符号数据类型,例如C99 ISO/IEC 9899标准,在stddef.h中定义。1 它可以通过包含stdlib.h来进一步导入,因为此文件在内部子包括了stddef.h

该类型用于表示对象的大小。接受或返回大小的库函数期望它们的类型为size_t或具有size_t作为返回类型。此外,最常使用的编译器运算符sizeof应该评估为与size_t兼容的常量值。

因此,size_t是一种保证能够容纳任何数组索引的类型。


7
库函数需要传入或返回大小时,要求使用类型为... size_t的变量。但是,stat()函数使用off_t类型来表示文件大小。 - Draemon
78
这条评论反映出一个基本的混淆。 size_t 用于内存中的对象。C标准甚至没有定义 stat()off_t(它们是POSIX定义),也没有与磁盘或文件系统有关的任何内容 - 它只停留在 FILE 流上。对于大小要求而言,虚拟内存管理与文件系统和文件管理完全不同,因此在这里提到 off_t 是不相关的。 - jw013
4
@jw013:我不会说这是一种根本的混淆,但你提出了一个有趣的观点。尽管如此,引用的文本并没有说“内存对象的大小”,而“offset”无论存储在何处,都不是一个好的大小类型名称。 Translated: @jw013: 我很难将其称为根本性的混淆,但你提出了一个有趣的观点。尽管如此,引用的文本并没有说“内存对象的大小”,而且“offset”无论存储在哪里,都不是一个好的大小类型名称。 - Draemon
47
@Draemon 提出了一个很好的观点。这个回答引用了维基百科,但在我看来,它并没有提供最好的解释。C标准本身更加清晰:它定义size_tsizeof操作符的结果类型(有关<stddef.h>的7.17p2)。第6.5节详细解释了C表达式的工作方式(sizeof的6.5.3.4)。由于你无法对磁盘文件应用sizeof(主要是因为C甚至未定义磁盘和文件的工作方式),因此不会产生混淆。换句话说,应该责备维基百科(以及这个回答引用了维基百科而非实际的C标准)。 - jw013
3
@Draemon - 我也同意“基本混淆”的评估。如果您没有阅读过C/C++标准,可能会认为“对象”指的是“面向对象编程”,但实际上并非如此。请阅读C标准,其中没有这些OOP对象,但是确实有对象,请查找一下。答案可能会让你惊讶! - Heath Hunnicutt
显示剩余11条评论

283

size_t是一种无符号类型。因此,它不能表示任何负值(<0)。您在计数时使用它,并确信它不可能为负数。例如,strlen()返回一个size_t,因为字符串的长度必须至少为0。

在您的示例中,如果您的循环索引始终大于0,则使用size_t或其他任何无符号数据类型可能是有意义的。

当您使用size_t对象时,您必须确保在包括算术在内的所有上下文中都想要非负值。例如,假设您有:

size_t s1 = strlen(str1);
size_t s2 = strlen(str2);

假设您想要找到str2str1的长度差异。您不能这样做:

int diff = s2 - s1; /* bad */

这是因为分配给变量diff的值始终是正数,即使s2 < s1也是如此,因为计算是使用无符号类型完成的。在这种情况下,根据您的使用情况,您可能最好使用int(或long long)来替代s1s2

C/POSIX中有一些函数可能/应该使用size_t,但由于历史原因而没有这样做。例如,fgets的第二个参数理想情况下应该是size_t,但实际上是int


20
@Alok: 两个问题:1)size_t的大小是多少?2)为什么我应该优先选择size_t而不是像unsigned int这样的东西? 回答:
  1. size_t的大小是根据系统架构来确定的,通常在32位系统上为4个字节,在64位系统上为8个字节。
  2. 应该优先选择size_t,因为它是专门用于表示对象大小或容器元素数量的类型,并且可以确保足够大以适应任何平台。相比之下,unsigned int具有固定的大小(通常为4个字节),可能无法足够大,而导致数据截断或溢出。
- Lazer
3
@Lazer: size_t 的大小为 sizeof(size_t)。C 标准保证 SIZE_MAX 至少为 65535。size_t 是由 sizeof 运算符返回的类型,并且在标准库中使用(例如 strlen 返回 size_t)。正如 Brendan 所说,size_t 不一定与 unsigned int 相同。 - Alok Singhal
5
是的,size_t被保证是无符号类型。 - Alok Singhal
6
请记住,在64位Linux上,int始终为32位,但size_t为64位。因此,size_t和int不可互换使用。 - dtoux
4
@JasonOster,C标准中没有要求使用二进制补码。如果s2-s1的值溢出了int类型,行为将是未定义的。 - Alok Singhal
显示剩余9条评论

107

size_t 是一种可以容纳任何数组索引的类型。

根据不同实现,它可能是以下任意一种:

unsigned char

unsigned short

unsigned int

unsigned long

unsigned long long

这是 stddef.h 中定义 size_t 的方式:

typedef unsigned long size_t;

5
确实,仅仅因为一个实现将其定义为这样,并不意味着所有实现都是如此。以64位Windows为例,unsigned long 是32位,而 size_t 则为64位。 - Tim Čas
1
在32位机器上,size_t是否总是32位,64位机器同理? - John Wu
根据1999年的ISO C标准(C99),size_t是至少16位的无符号整数类型(参见7.17和7.18.3节)。所以它不能是“unsigned char”吗? - jameshfisher
@jameshfisher 我不确定16位限制是否正确。 uint_least16_t 表示至少为16位。对于size_t,标准规定“无符号整型是sizeof运算符的结果”,并且“sizeof运算符返回其操作数的大小(以字节为单位)”。 - Arjun Sreedharan
5
@jameshfisher说无符号字符不能是16位的?! - Antti Haapala -- Слава Україні

95

如果您是实证主义者,

echo | gcc -E -xc -include 'stddef.h' - | grep size_t

Ubuntu 14.04 64位GCC 4.8的输出:

typedef long unsigned int size_t;

请注意,在GCC 4.2中,stddef.h由GCC提供而不是glibc,位于src/gcc/ginclude/stddef.h中。

C99的一些有趣表现

  • mallocsize_t作为参数,因此它确定可以分配的最大大小。

    由于它也是sizeof返回的值,我认为它限制了任何数组的最大大小。

    另请参见:C中数组的最大大小是多少?


第一个命令应该是带有空格的“-x c”吗?对于任何一开始不理解该命令的人,如果我理解正确,“-”参数从标准输入读取,因此需要使用echo管道来获取一个空的源文件。 - Danny

28

types.h 的 man页如下:

size_t应该是一个无符号整数类型。


26
为了解释为什么需要存在 size_t 并介绍我们是如何到达这里的:
从实用角度来看,size_tptrdiff_t 在 64 位实现中保证为 64 位宽,在 32 位实现中为 32 位宽等等。不可能强制任何现有类型在每个编译器上都具有这种意义,因为会破坏遗留代码。 size_tptrdiff_t 不一定与 intptr_tuintptr_t 相同。当在1980年代后期添加 size_tptrdiff_t 到标准中时,它们在某些仍在使用的体系结构上是不同的,并且在 C99 中添加了许多新类型但尚未消失(例如16位Windows)。在16位保护模式下的x86具有分段内存,最大可能的数组或结构的大小只能为65,536字节,但一个 far 指针需要32位宽,比寄存器宽。在这些体系结构上,intptr_t 可以是32位宽,但 size_tptrdiff_t 可以是16位宽并适合于寄存器中。而且谁知道将来会编写什么样的操作系统呢?理论上,i386体系结构提供了一个32位分段模型和48位指针,但没有任何操作系统实际使用过。
由于太多遗留代码假设 long 的精度为32位,因此内存偏移量的类型不能为 long 。 这个假设甚至内置于UNIX和Windows API中。 不幸的是,许多其他旧代码还假定 long 足以容纳指针,文件偏移量,自1970年以来经过的秒数等等。 POSIX现在提供了一种标准化的方法来强制后者成为真实情况,而不是前者,但这两种情况都不是可移植的假设。
它也不能是 int ,因为只有少数几个编译器在90年代时将 int 扩展到64位。 然后它们真正变得奇怪,仍然保持 long 32位宽。 标准的下一个版本宣布 int 超过 long 是非法的,但在大多数64位系统上, int 仍然是32位宽的。
它也不能是 long long int ,因为即使在32位系统上,它也至少要宽64位。
因此,需要一种新类型。 即使没有,所有这些其他类型也意味着除了数组或对象内的偏移量之外的其他含义。 如果从32位到64位的迁移惨败中有一个教训,那就是要具体说明类型需要具有的属性,并且不要使用在不同程序中具有不同含义的类型。

1
不同意 "size_tptrdiff_t 在 64 位实现中保证为 64 位宽度" 等说法。这种“保证”被夸大了。size_t 的范围主要受实现的内存容量驱动。"n 位实现" 主要是整数的本机处理器宽度。当然,许多实现使用类似大小的内存和处理器总线宽度,但是具有稀少内存或窄处理器的宽本机整数或具有大量内存的窄处理器确实存在,并且会使这两个实现属性分离。 - chux - Reinstate Monica
“不”。“一个n位的实现主要是整数的本机处理器宽度”这样说是完全错误的。在C编译器的上下文中,“n位实现”指的是操作系统上指针的宽度(或更正确地说,指软件意图运行的操作系统当前架构模式下指针的宽度,例如编译32位应用程序以在64位操作系统上使用32位兼容模式时),与硬件无关。 - Nathan Wiebe
64位的硬件已经存在很长时间了,但有些情况仍需要运行编译为操作系统32位兼容模式(也称为64位操作系统上的32位应用程序)的代码,甚至需要回退到32位操作系统。这并不罕见,因为许多传统应用程序尚未经过彻底的重构和重新测试,以确保指针操作永远不会做出任何32位的假设,从而截断指针的上32位(这曾经是常见的,但现在完全可以避免,感谢像size_t和uintptr_t这样的工具)。 - Nathan Wiebe

20

因为还没有人提到,所以size_t的主要语言意义是sizeof运算符返回该类型的值。同样,ptrdiff_t的主要意义是将一个指针从另一个指针中减去会产生该类型的值。接受它的库函数这样做是因为它将允许这些函数使用大小超过UINT_MAX的对象,在这些对象可能存在的系统上工作,而不会强制调用者浪费代码传递大于“unsigned int”的值,在大多数可能对象都适用于更大的类型的系统上。


我的问题一直是:如果没有sizeof,那么是否需要size_t? - Dean P
@DeanP:也许不是这样,但这会引出一个问题,即对于诸如malloc()之类的函数应该使用什么类型的参数。就我个人而言,我希望看到的版本可以接受intlonglong long类型的参数,并且一些实现可以提升较短的类型,而其他实现则可以实现例如lmalloc(long n) {return (n < 0 || n > 32767) ? 0 : imalloc(n);} [在某些平台上,调用imalloc(123)比调用lmalloc(123)更便宜,即使在size_t为16位的平台上,代码也可以分配由long值计算出的大小... - supercat
如果值大于分配器可以处理的值,应该能够依赖于分配失败。 - supercat

10

size_tint不能互换使用。例如,在64位Linux上,size_t的大小为64位(即sizeof(void*)),而int的大小为32位。

另外,请注意size_t是无符号的。如果您需要有符号版本,则某些平台上有ssize_t,它可能更适合您的示例。

作为一般规则,我建议在通用情况下使用int,只有在计算内存偏移量时(例如使用mmap())才使用size_t/ssize_t


9

size_t 是一种无符号整数数据类型,只能赋值为0和大于0的整数值。它用于测量任何对象的大小(以字节为单位),并由 sizeof 运算符返回。

constsize_t 的语法表示,但如果没有 const,程序也可以运行。

const size_t number;

size_t通常用于数组索引和循环计数。如果编译器是32位,则它将使用unsigned int,如果编译器是64位,则还将使用unsigned long long int。因此,size_t的最大大小取决于编译器类型。

size_t已经定义在<stdio.h>头文件中,但也可以由<stddef.h><stdlib.h><string.h><time.h><wchar.h>头文件定义。

示例(带有const

#include <stdio.h>

int main()
{
    const size_t value = 200;
    size_t i;
    int arr[value];

    for (i = 0 ; i < value ; ++i)
    {
        arr[i] = i;
    }

    size_t size = sizeof(arr);
    printf("size = %zu\n", size);
}

输出: 大小 = 800


例子(不带const

#include <stdio.h>

int main()
{
    size_t value = 200;
    size_t i;
    int arr[value];

    for (i = 0; i < value; ++i)
    {
        arr[i] = i;
    }

    size_t size = sizeof(arr);
    printf("size = %zu\n", size);
}

输出: 大小 = 800


7

size_t 是一个类型定义(typedef),用于以字节表示任何对象的大小。(类型定义用于为另一种数据类型创建附加的名称/别名,但不创建新类型。)

stddef.h 中可以找到如下定义:

typedef unsigned long long size_t;

size_t 也在 <stdio.h> 中定义。

size_t 作为 sizeof 运算符的返回类型。

使用 size_t 结合 sizeof 来定义数组大小参数的数据类型,示例如下:

#include <stdio.h>

void disp_ary(int *ary, size_t ary_size)
{
    for (int i = 0; i < ary_size; i++)
    {
        printf("%d ", ary[i]);
    }
}
 
int main(void)
{
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
    int ary_size = sizeof(arr)/sizeof(int);
    disp_ary(arr, ary_size);
    return 0;
}

size_t保证足够大以容纳主机系统可以处理的最大对象的大小。

请注意,数组的大小限制实际上是编译和执行此代码所在系统的栈大小限制的因素。您应该能够在链接时调整堆栈大小(请参见ld命令的--stack-size参数)。

为了让您了解近似的堆栈大小:

  • 嵌入式设备为4K
  • Win10为1M
  • Linux为7.4M

许多C库函数(如mallocmemcpystrlen)将其参数和返回类型声明为size_t

size_t 使程序员能够使用添加/减去所需元素数而不是使用字节偏移量来处理不同类型。

通过检查C字符串和整数数组的指针算术运算中使用size_t的用法,让我们更深入地了解 size_t 对我们的好处:

这是一个使用C字符串的示例:

const char* reverse(char *orig)
{
  size_t len = strlen(orig);
  char *rev = orig + len - 1;
  while (rev >= orig)
  {
    printf("%c", *rev);
    rev = rev - 1;  // <= See below
  }
  return rev;
}

int main() {
  char *string = "123";
  printf("%c", reverse(string));
}
// Output: 321

0x7ff626939004 "123"  // <= orig
0x7ff626939006 "3"    // <= rev - 1 of 3
0x7ff626939005 "23"   // <= rev - 2 of 3
0x7ff626939004 "123"  // <= rev - 3 of 3
0x7ff6aade9003 ""     // <= rev is indeterminant. This can be exploited as an out of bounds bug to read memory contents that this program has no business reading.

对于理解使用size_t的好处并不是很有帮助,因为字符始终为一个字节,无论你的架构是什么。

当我们处理数值类型时,size_t变得非常有用。

size_t类型就像一个整数,具有可以保存物理内存地址的优点;这个地址的大小根据它所执行的平台类型而变化。

下面是如何利用sizeofsize_t在传递int数组时的方法:

void print_reverse(int *orig, size_t ary_size)
{
  int *rev = orig + ary_size - 1;
  while (rev >= orig)
  {
    printf("%i", *rev);
    rev = rev - 1;
  }
}

int main()
{
  int nums[] = {1, 2, 3};
  print_reverse(nums, sizeof(nums)/sizeof(*nums));

  return 0;
}

0x617d3ffb44 1  // <= orig
0x617d3ffb4c 3  // <= rev - 1 of 3
0x617d3ffb48 2  // <= rev - 2 of 3
0x617d3ffb44 1  // <= rev - 3 of 3

上面我们看到一个int类型占用4个字节(由于每个字节有8位,所以一个int占用32位)。

如果我们创建一个长整型数组,我们会发现在linux64操作系统上,long占用64位,但在Win64系统上只占用32位。因此,在不同架构的C代码上运行时,使用可以节省大量编码和潜在的错误,特别是执行地址算术运算时更是如此。

因此,这个故事的寓意是“使用size_t并让你的C编译器处理容易出错的指针算术工作。”


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接