C语言 <stddef.h> 常用定义

<stddef.h> 头文件是C语言标准库的头文件之一,其中定义了常用的宏和类型,但是没有声明任何函数。

类型定义

ptrdiff_t :该类型主要用于指针的相减运算的返回类型,下面是在头文件中找到的源代码定义

#ifndef __PTRDIFF_TYPE__
    #ifdef _WIN64
        #define __PTRDIFF_TYPE__ long long int
    #else
        #define __PTRDIFF_TYPE__ long int
    #endif
#endif

#ifndef _PTRDIFF_T_DEFINED
    #define _PTRDIFF_T_DEFINED
    __MINGW_EXTENSION typedef __PTRDIFF_TYPE__ ptrdiff_t;
#endif

通过上面的代码,我们可以看到,ptrdiff_t 是 long int 或者 long long int 的重定义,具体是哪一个则取决于机器类型是否为 64 位。但无论是哪一个,该类型都是有符号类型,因此取值可正可负

size_t : 该类型主要用于 sizeof 运算符的返回值,下面是笔者在标准库中找到的源代码部分

#ifndef __SIZE_TYPE__
    #ifdef _WIN64
        #define __SIZE_TYPE__ long long unsigned int
    #else
        #define __SIZE_TYPE__ long unsigned int
    #endif
#endif

#if !(defined (__GNUG__) && defined ())
    __MINGW_EXTENSION typedef __SIZE_TYPE__ size_t;
    
    #ifdef __BEOS__
    typedef long ssize_t;
    #endif /* __BEOS__ */

#endif /* !(defined (__GNUG__) && defined (size_t)) */

通过上面的源代码,我们可以很容易的看到,size_t 类型为 long unsigned int 或者 long long unsigned int 两种类型之一,具体为哪一种则取决于机器是否为 64 位。但是无论哪一种都是 unsigned 类型,即无符号类型。

wchar_t :该类型主要用于表示宽字符 ,宽字符可以支持所有地区的所有字符,因此范围上肯定要比 char 类型大,以下是在库中找到的源代码定义部分:

#ifndef __WCHAR_TYPE__
    /* wchar_t is unsigned short for compatibility with MS runtime */
    #define __WCHAR_TYPE__ unsigned short
#endif

#ifndef __cplusplus
    typedef __WCHAR_TYPE__ wchar_t;
#endif

可以看到,wchar_t 是 unsigned short 类型,且该类型是在 MSVCRT 即MS运行库适用的

宏定义

NULL :老熟人了,用于指定指针为空,下面是标准库源代码中的相关定义

#if defined (_STDDEF_H) || defined (__need_NULL)
    #undef NULL		/* in case <stdio.h> has defined it. */
    
    #if defined(__GNUG__) && __GNUG__ >= 3
        #define NULL __null
    #else   /* G++ */
        #ifndef __cplusplus
            #define NULL ((void *)0)
        #else   /* C++ */
            #ifndef _WIN64
                #define NULL 0
            #else
                #define NULL 0LL
            #endif  /* W64 */
        #endif  /* C++ */
    #endif  /* G++ */
#endif	/* NULL not defined and <stddef.h> or need NULL.  */

 从以上定义不难看出,在 C 语言中 NULL 与 ((void*) 0) 等价,而在 C++ 中,NULL 就是 整数 0。但是为什么C语言中的NULL不是定义成 0 呢?因为在使用到空指针的地方,如果使用 NULL 定义 0 的形式,编译器可能就无法正确转换 0 为空指针,因为编译器可能无法判断这个 0 是否为 整数,因此使用 (void*)0 的格式是最稳妥的选择。举个例子的话,就是在可变参数列表中传递 0,因为可变参数列表一大特点就是参数个数未知,类型未知,因此就有可能错误看待 NULL 为 0。

offsetof 宏:该宏的用法较为特殊,,主要用于结构体,测试结构体成员在结构体中的偏移

/* Offset of member MEMBER in a struct of type TYPE. */
#define offsetof(TYPE, MEMBER) __builtin_offsetof (TYPE, MEMBER)

以上代码只是说明了该宏的作用,但是并没有具体实现。笔者又在 VS 的编译器中找到了针对 MSVCRT 的定义

#if defined _MSC_VER && !defined _CRT_USE_BUILTIN_OFFSETOF
    #ifdef __cplusplus
        #define offsetof(s,m) ((::size_t)&reinterpret_cast<char const volatile&>((((s*)0)->m)))
    #else
        #define offsetof(s,m) ((size_t)&(((s*)0)->m))
    #endif
#else
    #define offsetof(s,m) __builtin_offsetof(s,m)
#endif

通过上面的代码可以看出,offsetof 的具体实现,过程解释如下:

首先是 ((s*)0) 这部分是值将 0 强制转换为 结构体 s 类型的指针,而C语言又强制规定了结构体指针必定指向结构体的第一个成员起始位置,因此这一步相当于将结构体放在起始地址为0的位置上去,接下来,(((s*)0)->m) 是指对转换后的指针执行取成员的操作,但是咋这里我们只关心成员的偏移而不关心成员本身,因此 &(((s*)0)->m) 会去取出成员 m 的地址,由于前面的强制类型转换,已经将结构体起始位置放在了 地址 0 处,因此取地址操作所得的地址大小,就是相对于起始地址的偏移量,因此,直接将该地址强制转换为 size_t 类型即可得出成员 m 相对于 结构体 起始位置偏移。

因为结构体具有字节对齐、字节填充的特性,这些都是为了更快的处理效率,因此结构体成员的 偏移可能是我们选举要注意的地方,而这个宏的使用也很简单

s#include <stdio.h>
#include <stddef.h> 

struct A
{
	char a;
	int b;
	short c;
	float d;
};

int main(int argc, char *argv[]) 
{
	struct A s;
	printf("sizeof(A) = %d\n", sizeof(struct A));
	printf("Address of s = %p\n", &s);
	printf("Address of s.a = %p\n", &s.a);
	printf("Address of s.b = %p\n", &s.b);
	printf("Address of s.c = %p\n", &s.c);
	printf("Address of s.d = %p\n", &s.d);
	
	printf("offsetof(struct A, a) = %d\n", offsetof(struct A, a));
	printf("offsetof(struct A, b) = %d\n", offsetof(struct A, b));
	printf("offsetof(struct A, c) = %d\n", offsetof(struct A, c));
	printf("offsetof(struct A, d) = %d\n", offsetof(struct A, d));
	
	return 0;
}

// 运行结果
sizeof(A) = 16
Address of s = 000000000062FE10
Address of s.a = 000000000062FE10
Address of s.b = 000000000062FE14
Address of s.c = 000000000062FE18
Address of s.d = 000000000062FE1C
offsetof(struct A, a) = 0
offsetof(struct A, b) = 4
offsetof(struct A, c) = 8
offsetof(struct A, d) = 12

从上面的程序结果可以看出,char 类型 的 a 原本只有 1 字节大小,但是却占用了4个字节空间,a 与 b 之间有3个字节的填充,同理, c 与 d 之间也有 2 个字节的填充。这样就浪费了 5 个字节的空间。但是当我们调整以下结构体成员的先后顺序,就会减少这种空间浪费

#include <stdio.h>
#include <stddef.h> 

struct A
{
	int a;
	float b;
	short c;
	char d;
};

int main(int argc, char *argv[]) 
{
	struct A s;
	printf("sizeof(A) = %d\n", sizeof(struct A));
	printf("Address of s = %p\n", &s);
	printf("Address of s.a = %p\n", &s.a);
	printf("Address of s.b = %p\n", &s.b);
	printf("Address of s.c = %p\n", &s.c);
	printf("Address of s.d = %p\n", &s.d);
	
	printf("offsetof(struct A, a) = %d\n", offsetof(struct A, a));
	printf("offsetof(struct A, b) = %d\n", offsetof(struct A, b));
	printf("offsetof(struct A, c) = %d\n", offsetof(struct A, c));
	printf("offsetof(struct A, d) = %d\n", offsetof(struct A, d));
	
	return 0;
}

// 运行结果
sizeof(A) = 12
Address of s = 000000000062FE10
Address of s.a = 000000000062FE10
Address of s.b = 000000000062FE14
Address of s.c = 000000000062FE18
Address of s.d = 000000000062FE1A
offsetof(struct A, a) = 0
offsetof(struct A, b) = 4
offsetof(struct A, c) = 8
offsetof(struct A, d) = 10

当我们调整了 成员结构的先后位置,使得字节填充的机会减少,这样就会减少浪费的空间


补充

标准库是由一个组织定义的,但是各个编译器厂商并不一定和标准库完全一致,上面我所贴出的源代码也就就只是某一种编译器的,但是,无论是何种编译器,最后都会遵守标准库的标准,例如 size_t 类型,无论被定义成何种类型,都一定是无符号类型的整数类型,ptrdiff_t 类型同理,也一定是一种有符号的整数类型, NULL 无论是否定义为 0 ,在特定的环境下都一定表示空指针的含义,即什么也不指向。

因此,大家更重要的事要理解其含义及学会使用即可

posted @ 2023-09-07 11:59  夏蝉冬雪|春华秋实  阅读(324)  评论(0)    收藏  举报