[这是我写给一个询问如何编写将二进制数转换为十进制数的函数的人的答复。我已经意译了原信件的询问。]

发件人:scs@eskimo.com (Steve Summit)
主题:回复:请帮忙
日期:2000 年 4 月 29 日星期六 12:36:07 -0700 (PDT)
消息 ID:<200004291936.MAA18560@mail.eskimo.com>

你写道
>我正在尝试编写一个函数,该函数接受一个二进制数并返回
>其十进制值。有现成的库函数可以做到这一点吗?

算是吧;事实上,根据你对这个问题的理解方式,有几个函数可以做到。但这些函数的工作方式可能和你想象的略有不同,因为你思考问题的角度不同,而这最终却显得不太有意义。

>我所说的函数是指
>

>	#include <math.h>
>
>	int binary_to_decimal(int number)
>	{
>		int decimal = 0;
>		int i, digit;
>
>		for(i = 0; i < length(number); i++)
>		{
>			digit = substring(number, i);
>			/* where, say, substring(11011, 2) returns 0 */
>			/* but I'm not sure about this part */
>
>			if(digit == 1)
>				decimal += pow(2,i);
>		}
>
>		return decimal;
>	}

你的基本算法是正确的,如果你的“number”变量(即你函数的输入)是一个字符串,那么你将得到一个非常实用的函数。另外请注意,如果你修改了这几行

	if(digit == 1)
		decimal += pow(2,i);
	decimal += digit * pow(2, i);
你就可以通过将值 2 更改为其他值来使程序适用于任何基数。

>但我不知道如何让substring(number, i)工作。

那是因为你将“数字”看作是一个数字字符串。但整数在内部表示为数字字符串。

现在,如果你的函数接受一个数字字符串(也就是说,如果它的输入参数是字符串类型,而不是 int 类型),那么要获取每个数字就会很容易,因为获取构成字符串的单个字符总是很容易的。

(C 语言中没有标准的“substring”函数,尽管写一个并不难,尽管正如其名称所示,它几乎总是用于解决从字符串中提取子字符串的通用问题,而不一定是单个数字。而且,再次强调,它提取的对象是字符串变量,而不是整数变量。)

你打算如何将“二进制”数传递给你的函数?你是想这样调用吗?

	binary_to_decimal(11011);
在此调用中,你传递的不是一零一零一的二进制数;你传递的是十进制数字一万一千零一十一。我想你可以编写一些代码,接受数字为零或一的十进制数,并假装它是二进制数,然后“转换”为十进制,但这将是一个令人困惑且不太有用的函数。(要获取数字,你最终会除以 10,这对于提取你认为是基数为二的位来说是一种奇怪的做法。)

你试图编写一个接受 int 并返回 int 的函数。你可能打算其中一个 int 是“基数为 2”而另一个是“基数为 10”。但谈论整数是“什么基数”真的没有意义;整数只是一个数字。“基数”只在我们把数字写在纸上时才重要。

这里有一个例子。这一行有多少个 x?

	xxxxxxxxxxxxxxxxx
你的答案是基数为二还是基数为十?这个问题没有太大意义。如果你回答“十七”,你的答案不是基数为二或基数为十;你的答案是用英语。十七是行中 x 的数量

我们可以用不同的基数表示数字。如果我们用基数为十表示数字十七,我们得到数字 17。如果我们用基数为二表示,我们得到数字 10001。但是当我们将 17 基数十转换为 10001 基数二时,行中 x 的数量并没有改变。

我相信将存储在 int 变量中的数字视为仅仅是数字是正确的思考方式。询问它们是什么基数真的无关紧要,也没有意义。(事实上,在现代计算机上,整数几乎总是以二进制存储,但大多数时候,这个事实确实与我们无关。)

这里有一种方法可以让你确信 C 中的 int 只是数字。假设你有一个 int 变量

	int i;
并且你使用以下方式打印它printf:

	printf("%d\n", i);

现在,正如你可能知道的,printf也可以以基数 8 和基数 16 打印内容。所以我们也可以这样写

	printf("%o\n", i);
	printf("%x\n", i);

我们必须匹配printf格式(%d, `%o``%x`)来匹配我们要打印的数字的基数吗?不,因为正如我所说,int 变量i只是一个数字;它没有固有的基数。(或者,如果它有固有的基数,那就是基数为二,这意味着所有这三个printf转换都涉及从基数为二转换为其他基数。)

事实上,“我如何将二进制数转换为十进制数?”这个问题的答案之一是,“使用printf %d打印。”这个答案只在你将 int 视为内部基数为二时才有效,而且如果你试图显式地进行二进制到十进制的转换,这可能不是你期望的答案,但它确实是一个非常实际的答案。(如果你有一个由 0 和 1 组成的二进制数字字符串,那么你真正想做的是将该字符串转换为 int,也许是为了用 %d 重新打印它。)

如果我们相信(像我相信的那样)int 变量“只是数字”,那么只有当我们把它们打印出来供人们在现实世界中在纸上或屏幕上查看时,或者当我们从正在敲击键盘(键盘上有 0 到 9 的数字键)的人那里读取它们时,谈论它们是什么基数才有意义。在这些情况下,几乎所有的转换都是自动完成的,而我们几乎没有意识到。

  1. 当我们编写一个包含类似以下行的 C 程序时
    	int i = 1234;
    
    C 编译器会自动将这些十进制数字转换为整数。如果我们愿意,我们可以输入八进制或十六进制的整型常量1 2 3 4编译器会相应地进行八进制或十六进制转换。
    	int j = 0123;
    	int k = 0x123;
    
    当我们打印整数时,使用

  2. 或者类似的,如果我们使用printf它将以十进制打印,或者如果我们使用%d它将以八进制或十六进制打印。`%o``%x`.

  3. 当我们请求用户进行数字输入时,并且我们选择使用scanf或者类似的,我们再次使用格式字符选择基数,%d, `%o``%x`.

  4. 当我们有一个我们想转换为整数的字符串时,我们可以调用atoi函数
    	char *str = "123";
    	int i = atoi(str);
    
    atoi总是进行十进制转换。

  5. 最后,如果我们有一个任意基数的字符串,我们可以使用strtol函数
    	char *str2 = "1234";
    	i = strtol(str2, NULL, 7);
    
    此函数将字符串"1234"解释为基数为 7 的数字,转换为整数,并存储在i。 (如果我们之后用i重新打印%d,我们将看到十进制数字 466,即 1234 基数为 7。)

所以strtol是另一个标准 C 库函数,在实际的基数转换问题中经常有用,它是为数不多的允许你指定任意基数的标准 C 库函数之一。(你也可以将基数指定为 0,在这种情况下strtol将使用与 C 编译器相同的规则,即前导 0 表示基数为 8,前导0x表示基数为 16。事实证明scanf也可以这样做,使用%i格式。)

你会注意到,所有似乎与基数转换有关的标准函数(或其他语言特性)要么是从整数转换为字符串或打印表示,要么是从字符串或打印表示转换为整数。你找不到一个“基数转换”函数可以从整数“转换”到整数,因为正如我解释过的,这种“转换”没有意义。整数在内部不存储为数字字符串。但是,如果你想考虑数字的“基数”,这是一个概念,只有在(事实上,它是被定义的)数字字符串的意义上才有意义。

如果你想真正理解基数转换,在编写现代计算机程序时真正遇到它的上下文,这里有几个函数供你编写。

  1. 编写一个函数,该函数接受一个数字字符串并将其转换为整数,将数字字符串解释为基数为 10 的数字。这个函数实际上会很像你试图编写的“转换”函数,除了(a)它接受一个字符串,而不是一个 int;(b)它很容易访问数字(也就是说,执行“substring”操作),因为 C 中的字符串只是一个字符数组,而数组下标很容易;(c)你将从基数为 10 转换,而不是基数为 2。(此外,如果你安排得当,可以完全避免调用pow()。如果你从左到右转换数字,你只需要保持一个运行的总和,并且在每个数字时,将之前的总和乘以 10,然后再加上新数字。)

    如果你编写了这个函数,你基本上就重新实现了标准库的atoi()函数。

    你可能遇到的另一个问题是处理字符形式的数字。你的输入是一个字符串,如"1234",所以数字是字符,如'1'。但是字符'1'在 ASCII 中的值不是 1。但是,事实证明将数字字符转换为它们相应的数字值很容易:只需减去字符'0'的字符集值,无论它是什么。'0' - '0'显然是 0,无论'0'有什么值。同样,'1' - '0'是 1。所以如果你有一个字符变量c,包含数字字符'0''9'之一,那么c - '0'就是该数字的十进制值。[关键在于你不需要知道字符'0'的字符集值;你只需要减去字符常量'0'.]

  2. 编写一个函数,该函数接受一个数字字符串和一个整数值b,并将字符串转换为整数,将数字字符串解释为基数为b的数字。基本上,你只需要采用你在第 1 项中的函数,并将“10”更改为“b”。完成后,你将重新实现标准库的**部分**strtol()函数。[如果你喜欢,你也可以使用它来执行“二进制定点转换”,方法是将其基数设置为 2。正如我们所讨论的,这实际上是“二进制到数字转换”,而不是“二进制到十进制”。]

  3. 编写一个函数,该函数接受一个整数并创建一个数字字符串,该字符串构成该整数的基数 10 表示法。(你的编译器可能已经有一个这样的函数,可能称为“itoa”,尽管它不是标准函数。)当你编写这个函数时,你基本上是在重新实现任务,即printf打印某物时所执行的任务。%d.

    计算数字的基数 10 表示法的数字实际上很容易:你可以采取i % 10来提取最右边的数字,然后说i = i / 10来丢弃最右边的数字(因为你已经提取了它),然后说i % 10再次提取之前是倒数第二个数字的数字,依此类推。

    编写此函数的一个棘手之处在于,它提供的数字顺序颠倒了:它以从右到左的顺序提取数字,而你可能希望它们以从左到右的顺序打印或存储在字符串中。有各种方法可以解决这个问题。(另一个棘手之处是将数字值转换为字符,但在这里你只需要使用上面第 1 项中提到的技巧的反向操作:如果d是你计算出的一个数字值,那么d + '0'就是你想要的字符值。)

  4. 编写一个函数,该函数接受一个整数并创建一个数字字符串,该字符串构成基数b的表示法,其中b是函数的另一个 int 参数。(一些编译器附带一个itoa函数,它也接受一个基数。)基本上,你只需要采用你在第 3 项中的函数,并将“10”更改为“b''.

最后,如果你编写了这些函数,并且让它们正常工作,请看一下:你在第 1 项和第 2 项中的函数将数字基数 10 或基数b转换,而你在第 3 项和第 4 项中的函数将数字转换为基数 10 或基数b。但是你的第 1 项和第 2 项函数转换什么基数?你的第 3 项和第 4 项函数转换什么基数?你无法确定;它们真正转换到和转换自的只有“int”。基数(如果询问它有什么意义的话)是隐含的;整数内部表示的细节都由你的 C 编译器、它生成的机器代码以及你机器的 CPU 为你处理了。你执行了一定数量的乘以或除以 10 或b的操作,但“另一个”基数本质上是内置到乘法和除法运算符中的。如果你必须将你编写的代码从内部使用二进制整数的机器移植到假设的内部使用十进制整数的机器,你就不必做任何改动。所有这些事实都支持我的说法,即询问这些 int 是什么基数没有多大意义;它们只是数字。

另请参阅 comp.lang.c FAQ 列表,问题 20.10(以及 13.1)。

Steve Summit
scs@eskimo.com