C语言中size_t类型详细说明【转载】

本文深入探讨了C语言中size_t类型的使用及其对代码移植性、效率和可读性的影响。通过对比int和unsigned long等类型,阐述了size_t在处理对象大小时的优势,尤其是在不同平台和数据模型下的表现。

在c语言的很多库函数中,函数原型中,参数类型都是size_t。但是在我们编写程序时size_t类型却很少有所使用。那么这个类型到底有什么作用呢

使用size_t可能会提高代码的可移植性、有效性或者可读性,或许同时提高这三者。

 

  在标准C库中的许多函数使用的参数或者返回值都是表示的用字节表示的对象大小,比如说malloc(n) 函数的参数n指明了需要申请的空间大小,还有memcpy(s1, s2, n)的最后一个参数,表明需要复制的内存大小,strlen(s)函数的返回值表明了以’\0’结尾的字符串的长度(不包括’\0’),其返回值并不是该字符串的实际长度,因为要去掉’\0’。
  或许你会认为这些参数或者返回值应该被申明为int类型(或者long或者unsigned),但是事实上并不是。C标准中将他们定义为size_t。标准中记载malloc的申明应该出现在,定义为:

void *malloc(size_t n);

memcpy和strlen的申明应该出现在中:

void *memcpy(void *s1, void const *s2, size_t n);
size_t strlen(char const *s);

size_t还经常出现在C++标准库中,此外,C++库中经常会使用一个相似的类型size_type,用的可能比size_t还要多。
  据我所知,大部分的C和C++程序员害怕这些库使用size_t,因为他们不知道size_t代表什么或者为什么这些库需要使用它,归根结底,原因在于他们什么时候什么地方需要用到它。

可移植性问题

  早期的C语言(由Brian Kernighan 和 Dennis Ritchie 在The C Programming Language书中所写,Prentice-Hall, 1978)并没有提供size_t类型,C标准委员会为了解决移植性问题将size_t引入,举例如下:
  让我们来写一个可移植的标准memcpy函数,我们将会看到一些不同的申明和它们在不同平台不同大小的地址空间上编译下的情况。
  回忆memcpy(s1, s2, n)函数,它将s2指向地址开始的n个字节拷贝到s2指向的地址,返回s1,这个函数可以拷贝任何数据类型,所以参数和返回值的类型应该为可以指向任何类型的void*,同时,源地址不应该被改变,所以第二个参数s2类型应该为const void*,这些都不是问题。
  真正的问题在于我们如何申明第三个参数,它代表了源对象的大小,我相信大部分程序员都会选择int:

void *memcpy(void *s1, void const *s2, int n);

  使用int类型在大部分情况下都是可以的,但是并不是所有情况下都可以。int是有符号的,它可以表示负数,但是,大小不可能是复数。所以我们可以使用unsigned int代替它让第三个参数表示的范围更大。
  在大部分机器上,unsigned int的最大值要比int的最大值大两倍,比如说再也给16位的机器上,unsigned int的最大值为65535,int的最大值为32767。
  尽管int类型的大小依赖于C编译器的实现,但是在给定的平台上int对象的大小和unsigned int对象的大小是一样的。因此,使用unsigned int修饰第三个参数的代价与int是相同的:

void *memcpy(void *s1, void const *s2, unsigned int n);  

这样似乎没有问题了,unsigned int可以表示最大类型的对象大小了,这种情况只有在整形和指针类型具有相同大小的情况下,比如说在IP16中,整形和指针都占2个字节(16位),而在IP32上面,整形和指针都占4个字节(32位)。(参见下面C数据模型表示法)

C数据模型表示法   最近,我偶然发现几篇文章,他们使用简明的标记来表述不同目标平台下c语言数据的实现。我还没有找到这个标记的来源,正式的语法,甚至连名字都没有,但他似乎很简单,即使没有正规的定义也可以很容易使用起来。这些标记的一边形式形如:   I nI L nL LL nLL P nP。   其中每个大写字母(或成对出现)代表一个C的数据类型,每一个对应的n是这个类型包含的位数。I代表int,L代表long,LL代表long long,以及P代表指针(指向数据,而不是函数)。每个字母和数字都是可选的。   例如,I16P32架构支持16位int和32位指针类型,没有指明是否支持long或者long long。如果两个连续的类型具有相同的大小,通常省略第一个数字。例如,你可以将I16L32P32写为I16LP32,这是一个支持16位int,32位long,和32位指针的架构。   标记通常把字母分类在一起,所以可以按照其对应的数字升序排列。例如,IL32LL64P32表示支持32位int,32位long,64位long long和32位指针的架构;然而,通常写作ILP32LL64。

  不幸的是,这种memcpy的申明在I16LP32架构上(整形是16-bit 长整形和指针类型时32-bits)显得不够用了,比如说摩托罗拉第一代处理器68000,在这种情况下,处理器可能拷贝的数据大于65535个字节,但是这个函数第三个参数n不能处理这么大的数据。
  什么?你说很容易就可以改正?只需要把memcpy的第三个参数的类型修改一下:

void *memcpy(void *s1, void const *s2, unsigned long  n); 

  你可以在I16LP32目标架构上使用这个函数了,它可以处理更大的数据。而且在IP16和IP32平台上效果也还行,说明它确实给出了memcpy的一种移植性较好的申明。但是,在IP16平台上相比于使用unsigned int,你使用unsigned long可能会使你的代码运行效率大打折扣(代码量变大而且运行变慢)。
  在标准C中规定,长整形(无论无符号或者有符号)至少占用32位,因此在IP16平台上支持标准C的话,那么它一定是IP16L32 平台。这些平台通常使用一对16位的字来实现32位的长整形。在这种情况下,移动一个长整形需要两条机器指令,每条移动一个16位的块。事实上,这个平台上的大部分的32位操作都需要至上两条指令。
  因此,以可移植性为名将memcpy的第三个参数申明为unsigned long而降低某些平台的性能是我们所不希望看到的。使用size_t可以有效避免这种情况。
  size_t类型是一个类型定义,通常将一些无符号的整形定义为size_t,比如说unsigned int或者unsigned long,甚至unsigned long long。每一个标准C实现应该选择足够大的无符号整形来代表该平台上最大可能出现的对象大小。

使用size_t

  size_t的定义在<stddef.h>, <stdio.h>, <stdlib.h>, <string.h>, <time.h>和<wchar.h>这些标准C头文件中,也出现在相应的C++头文件, 等等中,你应该在你的头文件中至少包含一个这样的头文件在使用size_t之前。   包含以上任何C头文件(由C或C++编译的程序)表明将size_t作为全局关键字。包含以上任何C++头文件(当你只能在C++中做某种操作时)表明将size_t作为std命名空间的成员。   根据定义,size_t是sizeof关键字(注:sizeof是关键字,并非运算符)运算结果的类型。所以,应当通过适当的方式声明n来完成赋值:

n = sizeof(thing);

考虑到可移植性和程序效率,n应该被申明为size_t类型。类似的,下面的foo函数的参数也应当被申明为sizeof:

foo(sizeof(thing));

  参数中带有size_t的函数通常会含有局部变量用来对数组的大小或者索引进行计算,在这种情况下,size_t是个不错的选择。 
  适当地使用size_t还会使你的代码变得如同自带文档。当你看到一个对象声明为size_t类型,你马上就知道它代表字节大小或数组索引,而不是错误代码或者是一个普通的算术值。  

<think> 首先,用户的问题是关于函数指针的用法和含义,特别要求结合给出的包含函数指针的结构体代码进行分析。结构体是 struct proto,它包含 close、disconnect 等函数指针。 从系统级指令: - 所有行内数学表达式必须使用 $...$ 格式。 - 独立公式使用 $$...$$ 格式并单独成段。 - LaTeX 语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如 [^1]。 - 用户可见层:回答结构清晰,帮助用户逐步解决问题,保证真实可靠。 用户提供了参考站内引用 [^1] 到 [^4],我应该参考这些内容来回答问题。 关键引用: - [^1]: 解释了 struct proto 和 struct proto_ops 的函数指针差异:struct proto 更关注协议层实现细节(如TCP拥塞控制),struct proto_ops 更关注套接字层通用操作(如数据交换)。 - [^2]: 讨论了 struct proto 与其他结构体的关联,如 inet_protosw。 - [^3]: 展示了 inet_protosw 结构体,其中包含 struct proto *prot。 - [^4]: 讨论了 tcp_child_process 函数,可能涉及函数指针的使用。 用户特别提到 struct proto 包含 close、disconnect 等函数指针。我需要详细解释函数指针的概念,并基于这些结构体进行分析。 定义函数指针:函数指针是一个指向函数的指针变量,它允许动态调用不同的函数实现。在C语言中,常用于实现回调、接口或多态。 现在,结合 struct proto: - struct proto 通常在Linux内核网络协议栈中使用,定义在 include/net/sock.h 或其他地方。 - 典型定义可能类似: struct proto { void (*close)(struct sock *sk); int (*disconnect)(struct sock *sk, int flags); // 其他函数指针,如 connect, bind, etc. }; 解释用法: - 函数指针允许协议(如TCP、UDP)定义自己的实现,而高层代码可以通用地调用这些函数。 - 例如,在创建套接字时,struct proto 的函数指针被初始化为特定协议的函数(如 tcp_close、tcp_disconnect)。 - 这使得网络栈模块化:协议层和套接字层解耦。 分析用户提供的引用: - [^1] 强调了 proto 关注协议层细节,proto_ops 关注套接字层操作。 - [^3] 中的 inet_protosw 包含 proto *prot,用于注册协议处理函数。 在回答中,我需要: 1. 定义函数指针。 2. 解释其在C语言中的用法。 3. 结合 struct proto 分析:struct proto 的函数指针如何允许协议特定实现。 4. 使用示例:例如,在调用 close 函数时,通过函数指针调用实际协议的函数。 确保真实可靠:基于Linux内核网络栈知识。 结构回答: - 逐步解答: 1. 介绍函数指针的概念。 2. 解释 struct proto 的结构和作用。 3. 详细分析函数指针在 struct proto 中的用法。 4. 结合引用讨论差异和关联。 5. 可能提供一个简化的代码示例。 - 使用中文。 生成相关问题:在回答结束时添加相关问题。 引用标识:在引用段落末尾添加 [^1] 等,但用户提供了引用,所以我需要整合它们。用户说“参考站内引用”,我应该基于这些引用回答。 最终,生成相关问题,如关于函数指针在其他结构体中的应用或性能影响。 </think> ### 函数指针的用法和含义详解(结合 `struct proto` 结构体分析) #### 1. **函数指针的基本概念** 函数指针是一种特殊的指针变量,它指向函数的入口地址,而不是数据。通过函数指针,程序可以动态调用不同的函数实现,实现多态性、回调机制等高级编程模式。在C语言中,函数指针的声明格式为: ```c 返回类型 (*指针名)(参数列表); ``` 例如: ```c void (*func_ptr)(int); // 声明一个指向函数的指针,该函数接受int参数,返回void ``` - **含义**:函数指针允许在运行时决定调用哪个函数,提高代码的灵活性和模块化。 - **用法**:常用于实现策略模式、事件处理或接口定义(如Linux内核中的协议栈)。 #### 2. **`struct proto` 结构体分析** `struct proto` 是Linux内核网络协议栈中的关键结构体,用于定义协议层(如TCP、UDP)的实现细节。它包含多个函数指针(如`close`、`disconnect`),允许不同协议提供自定义行为。以下是基于引用内容的简化定义和解释[^1][^2]: ```c struct proto { void (*close)(struct sock *sk); // 关闭套接字的函数指针 int (*disconnect)(struct sock *sk, int flags); // 断开连接的处理函数指针 // 其他函数指针(如connect、bind等) }; ``` - **函数指针的作用**: - `close` 指针:指向协议特定的关闭函数(如TCP的`tcp_close`),释放资源并终止连接。 - `disconnect` 指针:指向断开连接的处理函数(如TCP的`tcp_disconnect`),处理状态迁移和错误恢复。 - 这些指针在协议初始化时被赋值为具体实现函数(例如TCP协议初始化时指向`tcp_close`)。 - **结合引用分析**: - `struct proto` 关注协议层实现(如拥塞控制、重传逻辑),而 `struct proto_ops` 关注套接字层通用操作(如数据交换)[^1]。 - 在 `inet_protosw` 结构体中,`struct proto *prot` 成员用于注册协议处理函数,将套接字层(`proto_ops`)与协议层(`proto`)解耦[^3]。 - 实际调用时,内核通过函数指针动态执行协议逻辑(例如`tcp_child_process`中通过`sock_owned_by_user`检查后调用状态处理函数)[^4]。 #### 3. **函数指针在 `struct proto` 中的具体用法示例 以下是一个简化的内核代码场景,展示函数指针如何被初始化和调用: ```c // TCP协议实现(协议层) void tcp_close(struct sock *sk) { // TCP特定的关闭逻辑(如发送FIN包) } int tcp_disconnect(struct sock *sk, int flags) { // TCP断开连接处理(如状态机迁移) return 0; } // 初始化struct proto(TCP协议注册) struct proto tcp_prot = { .close = tcp_close, .disconnect = tcp_disconnect, }; // 套接字层调用 void sock_close(struct sock *sk) { if (sk->sk_prot->close) { sk->sk_prot->close(sk); // 通过函数指针调用具体协议实现 } } ``` - **关键点**: - **动态绑定**:在创建套接字时,`sk->sk_prot` 被赋值为 `tcp_prot`,函数指针指向TCP的实现。 - **多态性**:不同协议(如UDP)可定义自己的`close`函数,而套接字层无需修改代码。 - **性能优势**:避免大量`if-else`分支,提升执行效率(直接跳转至函数地址)[^1]。 #### 4. **函数指针的意义与优势** - **模块化设计**:协议实现与调用方解耦(例如TCP和UDP共享同一套接字接口)[^2]。 - **可扩展性**:新增协议只需实现`struct proto`的函数指针,无需修改内核核心代码。 - **错误处理**:如引用[^4]所示,在`tcp_child_process`中,通过函数指针处理子套接字状态,确保协议逻辑隔离。 #### 5. **注意事项** - **空指针风险**:调用前需检查函数指针是否有效(如`if (sk->sk_prot->close)`)。 - **性能开销**:间接调用比直接调用略慢,但Linux内核优化后影响微乎其微。 - **调试复杂性**:函数指针跳转可能增加调试难度(需使用`ftrace`等工具跟踪调用链)[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值