命令行参数的解析

info
LICENSE
本文使用 GNU通用公共许可证 v3(GNU General Public License, version 3) 发布.

命令行参数的解析

真巧,在笔者近日的程序设计实践中又涉及到了命令行参数,笔者便再谈谈他.因单篇博客不宜过长,该内容将拆分在一系列博文中,该系列博文中,笔者将只讨论 getopt()getopt_long()getopt_long_only() 的使用,不涉及其他方案.

getopt() 的基本信息

1
2
3
4
#include <unistd.h>
int getopt(int argc, char *const argv[], const char *optstring);
extern char *optarg;
extern int optind, opterr, optopt;

上面有 getopt() 函数的函数原型和相关全局变量,注意使用 getopt() 函数需要包含 unistd.h

getopt() 被用来处理遵循 Single UNIX Specification 的命令行参数

Single UNIX Specification 要求[1]:

  • 限制每个命令行选项为一个单一的阿拉伯字符

  • 所有选项必须以 - 作为开头字符

举例来说就是getopt()可用于处理 command -t 123 -p 456.txt -uroot 这类命令,而不能用于 command --times 123 --path 456.txt --userroot

getopt()的参数

argvargc

这两个参数即为待解析的命令行参数的计数和指向字符串存储位置的指针数组.这两个参数的实参通常作为int main(int argc,char *argv[])的参数传入程序.对该处有疑问的读者可参考笔者的博文命令行参数的误解

optstring

optstring是一个字符串,用来说明预期的命令行参数的格式.它的作用可能有点类似 scanf() 中转换说明的作用.

optstring is a string containing the legitimate option characters. If such a character is followed by a colon, the option requires an argument, so getopt() places a pointer to the following text in the same argv-element, or the text of the following argv-element, in optarg. Two colons mean an option takes an optional arg

  • if there is text in the current argv-element (i.e., in the same word as the option name itself, for example, −oarg), then it is returned in optarg, otherwise optarg is set to zero. This is a GNU extension.
  • If optstring contains W followed by a semicolon, then −W foo is treated as the long option −−foo. (The −W option is reserved by POSIX.2 for implementation extensions.) This behavior is a GNU extension, not available with libraries before glibc 2.

optstring 是包含合法选项字符的字符串.如果此类字符后接 : ,则该选项需要一个参数,因此 getopt() 将指针指向位于同一 argv 元素中的后续文本,或位于 argarg 中的后续 argv 元素的文本. :: 表示一个选项带有一个可选的参数

  • 如果当前argv元素中有文本( 例如与选项名称本身相同的词,例如 -oarg ),则将其以 optarg 返回,否则 optarg 设置为 0.这是一个 GNU 扩展
  • 如果 optstring 包含 W 后跟一个分号,则将 -W foo 视为长选项 --foo .( -W 选项由 POSIX.2 保留用于实现扩展.)此行为是 GNU 扩展 ,不适用于 glibc 2 之前的库.

getopt()的返回值

  • If an option was successfully found, then getopt() returns the option character.

  • If all command-line options have been parsed, then getopt() returns −1.

  • If getopt() encounters an option character that was not in optstring, then ? is returned.

  • If getopt() encounters an option with a missing argument, then the return value depends on the first character in optstring:

    • if it is :, then : is returned
    • otherwise ? is returned.[3]

用笔者糟糕的英语勉强翻译一下:

warning

受限于笔者低劣的英文水平,笔者的翻译可能存在众多谬误,更无法做到 信、达、雅 的要求.笔者提供的翻译内容仅供参考.建议读者自行翻译或直接阅读英文原文.笔者不为本文中翻译内容的准确性负责.

  • 如果一个选项被成功的找到,getopt() 返回这个选项的字母

  • 如果完成了所有的选项的解析,getopt() 将返回 -1

  • 如果发现不在 optstring 中的选项,则返回 ?
  • 如果发现丢失参数的选项,返回值取决于 optstring[0]
    • 如果 optstring[0]:,则返回 :
    • 否则返回 ?,即 return optstring[0] == ':' ? ':' : '?';

getopt() 的扫描模式

  • If the first character of optstring is + or the environment variable POSIXLY_CORRECT is set, then option processing stops as soon as a nonoption argument is encountered.
  • If the first character of optstring is , then each nonoption argv-element is handled as if it were the argument of an option with character code 1. (This is used by programs that were written to expect options and other argv-elements in any order and that care about the ordering of the two.)
  • The special argument −− forces an end of option-scanning regardless of the scanning mode.[3]

还是由笔者来翻译一下

  • 如果 optstring[0]+ 或者 环境变量 POSIXLY_CORRECT 被设置,则 getopt() 将会在遇到非 optsting 中的选项时停止.

  • 如果 optstring[0]- ,则任何一个非选项的 argv 中的元素将被按照 ASCII 编码1 的字符处理.(这常被用于期待 选项argv 的元素 按某种顺序排列并关注两者的顺序的程序 )

  • 特殊的参数 -- 将强制结束选项扫描,无论扫描模式是什么.

请看示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* getopt1.c */
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int ch;
// opterr = 0;
while ((ch = getopt(argc, argv, "a:b:c::d::e::fxg:")) != -1)
{
printf(" ch\t函数的返回值\t%c\n", ch);
printf("optarg\t当前选项的参数\t%s\n", optarg);
printf("optind\t指向下个字符串\t%d\n", optind);
printf("argv[optind]\t\t%s\n", argv[optind]);
printf("optopt\t指向出错字符串\t%d\n", optopt);
printf("opterr\t若出错输出消息\t%d\n", opterr);
printf("\n");
}
}

请读者们编译后以参数 -a -- -c-- -- -g 运行程序.

笔者得到的输出内容
1
2
3
4
5
6
7
8
9
10
11
12
13
    ch  函数的返回值    a
optarg 当前选项的参数 --
optind 指向下个字符串 3
argv[optind] -c--
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 c
optarg 当前选项的参数 --
optind 指向下个字符串 4
argv[optind] --
optopt 指向出错字符串 0
opterr 若出错输出消息 1

请读者注意:-- 作为 选项的参数 被读取时 getopt() 正常的的返回 选项字符 ,但当 -- 不作为 选项的参数 被读取时,getopt() 返回值为 -1 ,循环中止.

danger

The use of + and - in optstring is a GNU extension.[3]

optstring中使用+- 是一个 GNU 扩展

这意味着使用+-的程序在不兼容 GNU 扩展编译器可能 无法正常的编译或运行

如果在编译中使用了的-std=c99-std=c11 等指定 C语言标准 的编译选项需对应替换成 -std=gnu99-std=gnu11

getopt() 的错误处理

While processing the option list, getopt() can detect two kinds of errors:

  1. an option character that was not specified in optstring

  2. a missing option argument (i.e., an option at the end of the command line without an expected argument).

Such errors are handled and reported as follows:

  • By default, getopt() prints an error message on standard error, places the erroneous option character in optopt, and returns ? as the function result.

  • If the caller has set the global variable opterr to zero, then getopt() does not print an error message. The caller can determine that there was an error by testing whether the function return value is ?. (By default, opterr has a nonzero value.)

  • If the first character (following any optional + or described above) of optstring is a colon (:), then getopt() likewise does not print an error message. In addition, it returns : instead of ? to indicate a missing option argument. This allows the caller to distinguish the two different types of errors.[3]

笔者翻译成中文便是

在处理选项列表时, getopt() 可以检测两种错误:

  1. 未在 optstring 中指定的选项字符

  2. 选项缺少参数(例如,命令行末尾没有预期参数的选项)

这些错误的处理和报告如下:

  • 默认情况下,getopt() 会在标准错误上显示一条错误消息,将错误的选项字符放入 optopt ,然后返回 ? 作为函数结果.
  • 如果调用者已将全局变量 opterr 设置为零,则 getopt() 不会输出错误消息. 调用方可以通过测试函数返回值是否为 ? 来确定是否存在错误.(默认情况下, opterr 具有非零值.)
  • 如果optstring的第一个字符(上述任意可选的 +- 之后)是冒号(:),则getopt()同样不会输出错误消息. 另外,它返回:而不是?表示缺少选项参数. 这使调用者可以区分两种不同类型的错误.

getopt() 相关的全局变量

其后,来讨论与 getopt() 相关的 4 个 全局变量

optarg 如果一个选项需要参数,在处理该选项时,getopt会设置optarg指向该选项的参数字符串.

opterr 如果一个选项发生了错误,getopt会默认打印一条出错消息.应用程序可以通过设置opterr参数为0来禁止这个行为.

optind 用来存放下一个要处理的字符串在argv数组里的下标.它从1开始,每处理一个参数,getopt都会对其递增1.

optopt 如果处理选项时发生了错误,getopt会设置optopt指向导致出错的选项字符串.[1]

请看示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* getopt1.c */
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int ch;
// opterr = 0;
while ((ch = getopt(argc, argv, "a:b:c::d::e::fxg:")) != -1)
{
printf(" ch\t函数的返回值\t%c\n", ch);
printf("optarg\t当前选项的参数\t%s\n", optarg);
printf("optind\t指向下个字符串\t%d\n", optind);
printf("argv[optind]\t\t%s\n", argv[optind]);
printf("optopt\t指向出错字符串\t%d\n", optopt);
printf("opterr\t若出错输出消息\t%d\n", opterr);
printf("\n");
}
}

这段代码将演示getopt()的使用方法与 getopt()调用中相关的变量的变化.
请读者一定要使用-a 234 -b -c456 -d 789 -e -f -h -g作为参数运行该程序,查看输出逐个分析原因.

笔者得到的输出内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    ch  函数的返回值    a
optarg 当前选项的参数 234
optind 指向下个字符串 3
argv[optind] -b
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 b
optarg 当前选项的参数 -c456
optind 指向下个字符串 5
argv[optind] -d
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 d
optarg 当前选项的参数 (null)
optind 指向下个字符串 6
argv[optind] 789
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 e
optarg 当前选项的参数 (null)
optind 指向下个字符串 8
argv[optind] -f
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 f
optarg 当前选项的参数 (null)
optind 指向下个字符串 9
argv[optind] -h
optopt 指向出错字符串 0
opterr 若出错输出消息 1

./getopt1.out: invalid option -- 'h'
ch 函数的返回值 ?
optarg 当前选项的参数 (null)
optind 指向下个字符串 10
argv[optind] -g
optopt 指向出错字符串 104
opterr 若出错输出消息 1

./getopt1.out: option requires an argument -- 'g'
ch 函数的返回值 ?
optarg 当前选项的参数 (null)
optind 指向下个字符串 11
argv[optind] (null)
optopt 指向出错字符串 103
opterr 若出错输出消息 1

值得特别关注的是:

  • 2 段,-c456 被解释为 选项b 的参数,而不是 选项c 和其参数 456
  • 4 段,定义为含有可选参数选项d 的参数为 null ,而不是 789,因为可选参数的选项的参数和选项间不能加空格,要使 789选项d 的参数,则该写为 -d789

请读者再次以参数 -ab -c123 -de -fx 执行该程序.

笔者得到的输出内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    ch  函数的返回值    a
optarg 当前选项的参数 b
optind 指向下个字符串 2
argv[optind] -c123
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 c
optarg 当前选项的参数 123
optind 指向下个字符串 3
argv[optind] -de
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 d
optarg 当前选项的参数 e
optind 指向下个字符串 4
argv[optind] -fx
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 f
optarg 当前选项的参数 (null)
optind 指向下个字符串 4
argv[optind] -fx
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 x
optarg 当前选项的参数 (null)
optind 指向下个字符串 5
argv[optind] (null)
optopt 指向出错字符串 0
opterr 若出错输出消息 1

值得特别关注的是:

  • 1 段,b 被解释为 选项a 的参数,而不是 选项a选项b .请将第 1 段 和 第 4 与 第 5 段比较, -fx 被解释为了 选项f选项x
  • 4 段,定义为可选参数选项d的参数为 null ,而不是 789,因为含「可选参数的选项的参数」和选项间不能加空格,要使 789选项d 的参数,则该写为 -d789

    长选项

    长选项以两个「-」开头,长选项的参数写法可以为以下两种格式:「--arg=param」或「--arg param」.

    基本信息

1
2
3
4
5
6
7
8
9
10
#include <getopt.h>
struct option
{
const char *name;
int has_arg;
int *flag;
int val;
};
int getopt_long(int argc, char *const argv[], const char *optstring, const struct option *longopts, int *longindex);
int getopt_long_only(int argc, char *const argv[], const char *optstring, const struct option *longopts, int *longindex);

getopt_long()

  • argcargv 不必解释含义.
  • optstring:当程序只接受长选项时,optstring 应该为 ""(空字符串),而不是 NULL
  • longopts:是一个 struct option 的数组.
  • name
    is the name of the long option.
  • has_arg
    is: no_argument (or 0) if the option does not take an argument; required_argument (or 1) if the option requires an argument; or optional_argument (or 2) if the option takes an optional argument.
  • flag
    specifies how results are returned for a long option.
    • If flag is NULL , then getopt_long() returns val . (For example, the calling program may set val to the equivalent short option character.)
    • Otherwise, getopt_long() returns 0, and flag points to a variable which is set to val if the option is found, but left unchanged if the option is not found.
  • val
    is the value to return, or to load into the variable pointed to by flag .>The last element of the array has to be filled with zeros.[1]

也就是说:

  • name
    选项名.
  • has_arg
    需要为 no_argument(无参数)、required_argument(需要参数)、optional_argument(可选参数)这三个宏中的一个.
  • flagval
    当解析到该长选项时:
    • 如果 flagNULLgetopt_only() 返回 val.(例如,调用者设置 val 为对应的短选项字符)
    • 如果 flag 不为 NULLgetopt_only() 返回 0,并且 flag 指向的变量将被设置为 val.当未解析的该选项时,flag 指向的值不变.
      longopts 数组的最后一个元素需要全部字段为 0

If longindex is not NULL, it points to a variable which is set to the index of the long option relative to longopts.[1]
如果 longinedx 不是 NULL,它指向的值将被设置为解析到的长选项在 longopt 中的索引.

getopt_long_only()

getopt_long_only()getopt_long() 是相似的.但 - 开头的选项也被认为是选项,当选项以 - 开头时,getopt_long_only() 将认为他是一个长选项.当选项以 - 开头且不匹配长选项但却能匹配短选项时,getopt_long_only() 将这个选项解析为短选项.

整理整理思路.

对于一个以 - 开头的选项:

  • getopt_long() 认为他是一个短选项
  • getopt_long_only() 认为他是一个长选项
    换而言之,-abgetopt_long() 眼中解释为:「选项a和选项b」或者「选项a和选项a的参数b」;但是 getopt_long_only() 将他首先解释为「长选项ab」,除非 longopts 的数组中不包含一个 nameab 的长选项.

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>     /* for printf */
#include <stdlib.h> /* for exit */
#include <getopt.h>
int
main(int argc, char **argv)
{
int c;
int digit_optind = 0;
while (1) {
int this_option_optind = optind ? optind : 1;
int option_index = 0;
static struct option long_options[] = {
{"add", required_argument, 0, 0 },
{"append", no_argument, 0, 0 },
{"delete", required_argument, 0, 0 },
{"verbose", no_argument, 0, 0 },
{"create", required_argument, 0, 'c'},
{"file", required_argument, 0, 0 },
{0, 0, 0, 0 }
};
c = getopt_long(argc, argv, "abc:d:012",
long_options, &option_index);
if (c == -1)
break;
switch (c) {
case 0:
printf("option %s", long_options[option_index].name);
if (optarg)
printf(" with arg %s", optarg);
printf("\n");
break;
case '0':
case '1':
case '2':
if (digit_optind != 0 && digit_optind != this_option_optind)
printf("digits occur in two different argv-elements.\n");
digit_optind = this_option_optind;
printf("option %c\n", c);
break;
case 'a':
printf("option a\n");
break;
case 'b':
printf("option b\n");
break;
case 'c':
printf("option c with value '%s'\n", optarg);
break;
case 'd':
printf("option d with value '%s'\n", optarg);
break;
case '?':
break;
default:
printf("?? getopt returned character code 0%o ??\n", c);
}
}
if (optind < argc) {
printf("non-option ARGV-elements: ");
while (optind < argc)
printf("%s ", argv[optind++]);
printf("\n");
}
exit(EXIT_SUCCESS);
}

[3]

编译并以 ./getopt_long -add --append -d34 -1 --verbose 运行,程序的输出为:

1
2
3
4
5
6
option a
option d with value 'd'
option append
option d with value '34'
option 1
option verbose

上面的代码中,如果把第 21 的代码中的 getopt_long 改成 get_long_only 再次编译并以相同的参数运行就会得到如下的输出:

1
2
3
4
option add with arg --append
option d with value '34'
option 1
option verbose

区别很明显.

getopt_long()-add 解释为了 选项a、选项 d、选项 d 的参数 d

getopt_long_only()-add 解释为了 选项 add

参考书籍

1. W.RichardStevens.Stephen.UNIX环境高级编程[M].第3版.戚正伟,译.北京:人民邮电出版社
2. C++ 命令行参数解析.[G/OL].CSDN.https://blog.csdn.net/qq_34719188/article/details/83788199
3. Linux Programmer’s Manual.[G/OL].https://man7.org/linux/man-pages/man3/getopt.3.html

数据结构对齐

数据结构对齐

对于大多数 x86-64 指令来说,保持数据对齐能够提高效率,但是它不会影响程序的行为.另一方面,如果数据没有对齐,某些型号的 Intel 和 AMD 处理器对于有些实现多媒体操作的 SSE 指令,就无法正确执行.这些指令对 16 字节数据块进行操作,在 SSE 单元和内存之间传递数据的指令要求内存地址必须是 16 的倍数.任何试图以不满足对齐要求的地址来访问内存都会导致异常,默认的行为是程序终止.[1]

结构体的大小不总是等于各数据成员的大小之和

1
2
3
4
5
6
7
struct test
{
char a;
long b;
int c;
char d;
};

结构体的大小不总是等于各数据成员的大小之和,但事实上结构体的成员间 常常 存在「空隙」.
例如上面的结构体,在笔者的设备上的大小为: 24 byte,但「各成员的大小的和」仅为 14 byte.
经过简单的计算就知道该结构体中有 41.6% 的没有被利用,这不一定是一件坏事,但是在可用内存较小的设备上创建过多的该类结构体可能不是一个好的做法.

对齐的方式

基本数据类型的「对齐要求」

上文说到结构体的数据成员间存在「间隙」,但这个间隙到底是如何分布的?

为此,需要了解先每个基本的数据类型的「对齐要求」.

info
INFO

C11 中为定义了 _Alignof 运算符来输出数据的「对齐要求」, _Alignof 的使用方式与 sizeof 类似.

_Alignof 运算符给出一个类型的对齐要求,在关键字 _Alignof 后面的圆括号中写上类型名即可:

1
size_t d_align = _Alignof(float);

假设 d_align 的值是 4,意思是 float 类型 对象的对齐要求是 4.也就是说,4 是储存该类型值相邻地址的字节数.一般而言,对齐值都应该是 2 的非负整数次幂.[2]

_Alignof(type) 的意义为:「若定义 TYPE a;,则 (unsigned long)&a%_Alignof(type)==0」.

较大的对齐值被称为 stricterstronger,较小的对齐值被称为 weaker.[2]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
int main(void)
{
printf("char %zu\n", _Alignof(char));
printf("short %zu\n", _Alignof(short));
printf("int %zu\n", _Alignof(int));
printf("void* %zu\n", _Alignof(void *));
printf("long %zu\n", _Alignof(long));
printf("long long %zu\n", _Alignof(long));
printf("float %zu\n", _Alignof(float));
printf("double %zu\n", _Alignof(double));
printf("long double %zu\n", _Alignof(long double));

printf("char %zu\n", _Alignof(const char));
printf("short %zu\n", _Alignof(const short));
printf("int %zu\n", _Alignof(const int));
printf("void* %zu\n", _Alignof(const void *));
printf("long %zu\n", _Alignof(const long));
printf("long long %zu\n", _Alignof(const long));
printf("float %zu\n", _Alignof(const float));
printf("double %zu\n", _Alignof(const double));
printf("long double %zu\n", _Alignof(const long double));

printf("char %zu\n", _Alignof(unsigned char));
printf("short %zu\n", _Alignof(unsigned short));
printf("int %zu\n", _Alignof(unsigned int));
printf("long %zu\n", _Alignof(unsigned long));
printf("long long %zu\n", _Alignof(unsigned long));
}

可以看到的是:「对齐要求」仅与数据类型本身有关,与 constsignedunsigned 无关.

结构体的「对齐要求」

一个定义完成的结构体是一个 复合数据类型 ,那么结构体也该有它自己的「对齐要求」.
结构体的对齐要求为其 成员 的「对齐要求」中的最大值.
因此,下面的结构体的对齐要求为:「1、8、4、1」中的最大值,也就是 8,当然也可以用 _Alignof(struct test) 验证刚才的结论.
由此,得到数据结构对齐的要求之1:结构体地址 满足 结构体的『对齐要求』

1
2
3
4
5
6
7
struct test
{
char a;
long b;
int c;
char d;
};

特别需要注意的是:「对于任意数据类型,数据类型的大小应当为其『对齐要求』的整数倍.」
该要求在基本数据类型中没有意义,因为单个元素总是其「对齐要求」的整数倍.在结构体中,结构体的最后一个成员后 可能 需要添加「空隙」使结构体的大小为其「对齐要求」的整数倍.
由此,得到数据结构对齐的要求之2:结构体大小 为结构体的『对齐要求』的倍数」

结构体的「空隙」

讨论完了数据类型的「对齐要求」,现在来看看结构体中的「空隙」究竟是如何分布的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stddef.h>//提供 offsetof
#include <stdio.h>
struct test
{
char a;
long b;
int c;
char d;
};
int main(void)
{
printf("offsetof(struct test, a)\t%lu\n", offsetof(struct test, a));
printf("offsetof(struct test, b)\t%lu\n", offsetof(struct test, b));
printf("offsetof(struct test, c)\t%lu\n", offsetof(struct test, c));
printf("offsetof(struct test, d)\t%lu\n", offsetof(struct test, d));
}

info
INFO

如果你必须确定结构某个成员的实际位置,应该考虑边界对齐因素,可以使用 offsetof 宏(定义于 stddef.h).

1
offsetof( type, member )

type 就是结构的类型,member 就是你需要的那个成员名.表达式的结果是一个 size_t 值,表示这个指定成员开始存储的位置距离结构开始存储的位置偏移几个字节.[3]

根据每个成员的大小和相对结构体开始处的偏移量,能得到下面的表格.

offset内容offset内容
0char a12long b
113long b
214long b
315long b
416int c
517int c
618int c
719int c
8long b20char d
9long b21
10long b22
11long b23

根据上文,一个结构体的「对齐要求」为其成员的「对齐要求」的最大值,又因为「对齐要求」通常为 2的幂,那么结构体的「对齐要求」必然是其成员对齐要求的 最小公倍数.即「结构体的首地址」满足「结构体的任一成员」的「对齐要求」.那么,只需要让结构体中的成员的偏移量为成员的「对齐要求」的倍数,那么成员的地址必将满足其「对齐要求」.
由此,得到数据结构对齐的要求之3:「成员的偏移量为成员『对齐要求』的倍数」

联合的「对齐要求」

联合与结构体相比在「对齐要求」只有些许不同:「联合的『对齐要求』为其最大的成员的『对齐要求』」
对下面的联合而言,其「对齐要求」为long y;或者说double z;的「对齐要求」,即 8

1
2
3
4
5
6
union test
{
char x;
long y;
double z;
};

复合数据结构的嵌套

在考虑数据结构对齐的问题时,如果遇到了「复合数据结构」嵌套的情况,只需要把内层的「复合数据结构」当作一个新的数据类型进行思考即可,在思考时不必关注其成员是 基本数据类型 还是 结构体 亦或是 联合体,只需逐层分析,逐层分析其「对齐要求」.

举个例子吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct TEST
{
union U1
{
char a;
int b;
short c;
} d;
long e;
struct S1
{
int f;
union U1 g;
unsigned long h;
} i;
union U2
{
struct S1 j;
union U1 k;
} l;
char m;
};

danger
WARNING

上面这段代码仅为了说明「复合数据结构的嵌套」,代码本身无应用价值且难以理解和维护.实际开发中,除非有十分充足的理由,否则不应当写出类似的代码.

现在笔者尝试分析 struct TEST

  1. 首先得出 U1 中最大的成员为 int b;,则 U1 的「对齐要求」为 int b; 的「对齐要求」,即 U1 的「对齐要求」为 4.
  2. 又因为 long e; 的「对齐要求」为 8,则 de 间有 4 bytes 的「间隙」.
  3. 现在分析 S1
    1. 1 中知 U1 的「对齐要求」为 4.又因为 int f; 的大小为 4,所以 fg 间无「间隙」.
    2. unsigned long h; 的「对齐要求」为 8,又因为在 S1 中, h 前面的成员 fg 正好占用了 S1 的前 8 bytes.可知,hg 间无间隙.
    3. 此时共占用 S1 的前 16 bytes ,而 S1 的「对齐要求」为 unsigned long h; 的「对齐要求」,即为 8.可知 h 后无「空隙」.
    4. 又因为 S1 的「对齐要求」为 8,而 de 共占用 struct TEST 的前 16 bytes,则 ie 间无 「间隙」.
  4. 现在分析 union U2;
    1. 由上:「 struct S1 j; 的『对齐要求』为 8;union U1 k; 的『对齐要求』为 4 」,则 union U2; 的「对齐要求」为:「4、8」中的最大值,即为 8.
    2. k 的「对齐要求」为 4,j 占据了 U2 的前 16 bytes ,则 kj 间无「间隙」.且 k 后无「间隙」.
    3. il 的「对齐要求」均为 8 ,则il 间无「间隙」.
  5. char m; 的「对齐要求」为 1,而 l 的「对齐要求」为8,故此 lm 间无「间隙」.
  6. 由上,struct TEST 的「对齐要求」为:「4、8、8、8、1」中的最大值,即为 8.
  7. 最终得到,m 后有 7 bytes 的「空隙」.

调整结构体的成员的顺序

有了上面一大堆的铺垫,笔者相信读者们 数据结构对齐 有了自己的理解.但是还有一个遗留的问题值得在此共同探讨:怎么排列成员才能提高结构体的空间利用率.
答案很简单:「将成员按照其『对齐要求』降序排列」.
重新回到最开始的示例:

1
2
3
4
5
6
7
struct test
{
char a;
long b;
int c;
char d;
};

将其成员按照「对齐要求」降序排列便得到了:
1
2
3
4
5
6
7
struct test
{
long b;
int c;
char a;
char d;
};

经过简单的重新排序,struct test 现在只需要占用 16 bytes,节省了 8 bytes.

但是这种做法并不一定是最好的,有时结构体的成员的排列具有逻辑顺序,具有便于开发者理解的作用,重排可能会打破原有的逻辑顺序.

tip
TIP

有时,我们有充分的理由,决定不对结构的成员进行重排以减少因对齐带来的空间损失.例如,我们可能想把相关的结构成员存储在一起,提高程序的可维护性和可读性.但是,如果不存在这样的理由,结构的成员应该根据它们的边界需要进行重排,减少因边界对齐而造成的内存损失.
当程序将创建几百个甚至几千个结构时,减少内存浪费的要求就比程序的可读性更为急迫.在这种情况下,在声明中增加注释可能避免可读性方面的损失.[3]

参考资料

1. Randal E.Bryant.深入理解计算机系统[M].第三版.龚奕利,译.北京:机械工业出版社
2. Stephen Prata.C Primer Plus[M].第六版.姜佑,译.北京:人民邮电出版社
3. Kenneth.A.Reek.C和指针[M].徐波,译.北京:人民邮电出版社

命令行参数的误解

命令行参数的误解

前言

我们都知道C语言中允许main函数拥有0个或2个参数,但也存在部分操作系统向程序传入更多的参数,还有部分实现中对标准进行扩展,允许main函数拥有更多的参数
命令行参数作为main函数的两个参数被传递给程序,这两个参数通常被命名为int argc,char **argv,其中argc为参数的数量,argv为一个指向内含 argc + 1char 类型指针指针数组

但仅用这段话进行描述可能难以对命令行参数有一个正确的认识,这种描述可能对命令行参数的理解不利.
我们先来分析一个程序.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* ShowCommandLineArgument.c   */
#include <stdio.h>
int main(int argc, char **argv)
{
printf("argc:%d\n", argc);
for (int i = 0; i < argc; i++)
printf("argv[%d]:%s\n", i, argv[i]);
//argv[i]就是*(argv+i),很明显是一个指向char的指针
//程序并不以%s打印argv[argc],而是退出循环.

//请不要忘记:表达式(argv[argc]==NULL)为真
printf("argv[%d]:%p\n", argc, argv[argc]);
return 0;
}

在笔者的电脑中,该文件被存储在/home/admin/blog/ShowCommandLineArgument.c,输入命令 gcc ShowCommandLineArgument.c 进行编译,得到a.out,并以cd && ./blog/a.out -f ~/bolg/test1.md >./blog/test2.md /home/admin/blog/test3.md ./blog/test4.md执行该程序.

请思考,该程序会输出什么内容?你是否认为程序的输出为

1
2
3
4
5
6
7
8
9
argc:6
argv[0]:a.out
argv[1]:-f
argv[2]:~/bolg/test1.md
argv[3]:>./blog/test2.md
argv[4]:/home/admin/blog/test3.md
argv[5]:./blog/test4.md
argv[6]:(nil)


什么?你说没看到输出?请认真查看笔者输入的指令,其中包括了 >./blog/test2.md 意味把 a.out标准输出 重定向至文件./blog/test2.md .所以笔者使用 cat >./blog/test2.md 查看输出的内容,该程序在笔者的设备上的输出为:
1
2
3
4
5
6
7
argc:5
argv[0]:./blog/a.out
argv[1]:-f
argv[2]:/home/admin/bolg/test1.md
argv[3]:/home/admin/blog/test3.md
argv[4]:./blog/test4.md
argv[5]:(nil)

是不是和你的预期不尽相同,请听笔者逐一解释.

常见误区

误区1—-「认为 argv[0] 存储文件名」

实际上,argv[0] 会存储调用的指令中的第一个字符串,而不是文件名,strcmp(argv[0],__FILE__)并不总为0

误区2—-「认为命令行参数总是被原样传递」

在上面的例子中可以发现,相对路径 ~/blog/test1.md 作为命令行参数传给程序,程序收到的实际上是文件的绝对路径 /home/admin/blog/test3.md
但同为相对路径./blog/a.out./blog/test4.md却可以正常传递给程序,而不被转换为绝对路径
其他的相对路径写法是否能被正常传递?笔者在此使用由 ShowCommandLineArgument.c 编译得到的 a.out 文件继续测试.使用的指令为 ~/blog/a.out ./test/../blog/test1.md ../test2.md ~admin/blog/test3.md 由这两次测试,笔者大胆猜测只有以 ~ 开头的相对路径会被转换为绝对路径 然后才传递给程序.

1
2
3
4
5
6
argc:4
argv[0]:/home/admin/blog/a.out
argv[1]:./test/../blog/test1.md
argv[2]:../test2.md
argv[3]:/home/admin/blog/test3.md
argv[4]:(nil)

为什么要这么做呢?

请分析笔者的这个程序.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
int main(void)
{
char path1[] = "./blog/test2.md";
char path2[] = "~/blog/test2.md";
char path3[] = "~admin/blog/test2.md";
char path4[] = "~/blog/test2.md";
char path5[] = "../blog/test2.md";

if (fopen(path1, "r") == NULL)
perror("path1");
else
printf("1Success\n");
if (fopen(path2, "r") == NULL)
perror("path2");
else
printf("2Success\n");
if (fopen(path3, "r") == NULL)
perror("path3");
else
printf("3Success\n");
if (fopen(path4, "r") == NULL)
perror("path4");
else
printf("4Success\n");
if (fopen(path5, "r") == NULL)
perror("path5");
else
printf("5Success\n");
return 0;
}

笔者用cd && ./blog/a.out调用该程序编译得到的可执行文件,得到的输出为:

1
2
3
4
5
1Success
path2: No such file or directory
path3: No such file or directory
path4: No such file or directory
path5: No such file or directory

我们可以惊讶的发现只有第一次成功的打开了文件,其他4次操作全部报错.当然,其中第五次打开文件的操作失败是理所当然的,因为确实没有这个文件存在.笔者复制该可执行文件至~/test/a.out后重新执行该程序即发现,第5次文件打开操作成功了.

1
2
3
4
5
path1: No such file or directory
path2: No such file or directory
path3: No such file or directory
path4: No such file or directory
5Success

这说明:fopen()无法识别以~开头的相对路径,也体现了命令行参数在传递过程中,转换以~开头的相对路径绝对路径的必要性.

误区3—「认为重定向是命令行参数」

重定向虽然也在命令行参数的位置,但和命令行参数具有本质的区别.

实践说明重定向指令不会被当中命令行参数传递给程序.

在开发中应该小心,防止误认,也需防止命令行参数中出现相关符号被系统当做重定向指令,导致命令行参数传递错误.

测试环境

OS: Arch Linux
Kernel: x86_64 Linux 5.8.14-arch1-1

参考书籍

1. Stephen Prata.C Primer Plus[M].第六版.姜佑,译.北京:人民邮电出版社
2. Kenneth.A.Reek.C和指针[M].徐波,译.北京:人民邮电出版社.2008