7

C语言程序开发中,两个初学者经常犯的错误

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91%E4%B8%AD-%E4%B8%A4%E4%B8%AA%E5%88%9D%E5%AD%A6%E8%80%85%E7%BB%8F%E5%B8%B8%E7%8A%AF%E7%9A%84%E9%94%99%E8%AF%AF/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在《》一文中,我们讨论了C语言程序开发中基础的赋值操作,涉及到了一些数据类型转换的过程,虽然简单,但还是有不少初学者弄错,因此文章较为详细的分析了易错点以及原因。

文章发出后,我收到了一些读者回复,但是有些回复其实是不准确的,甚至是错误的。为此,本文挑出两个典型,做进一步的解析。

“对常数使用强制类型转换毫无意义”

这是网友 @Neverdies 的回复。那这句话对不对呢?严格来说,这句话是不准确的,强制类型转换可以限定常数的宽度。例如下面这行C语言赋值语句:

int res = (unsigned char)0xffff;

该语句执行完成后,res 中的数值将为 0xff,而不是 0xffff。当然了,在实际的C语言程序开发中,也可以将 ret 声明为 unsigned char 型,这样就可以省去对常数 0xffff 的强制类型转换了。

如果读者觉得上面这个例子没有实际应用价值,再来看下面这段C语言代码:

#include <stdio.h>

int main()
{
    unsigned long long i = 0;

    i = 146*100000000;
    printf("i: %lld\n", i);

    return 0;
}

编译并执行这段C语言代码,会输出什么呢?请看:

# gcc t.c
# ./a.out 
i: 1715098112

居然不是 14600000000!事实上,这段简短的C语言代码常常会让初学者百思不得其解,甚至会怀疑C语言或者计算机有问题,出故障了。

其实原因很简单,就是数据溢出了而已。以我的机器为例,在C语言程序的计算过程中,所有乘数默认都是 int 型的,上述C语言代码中的两个常数的乘法运算结果显然超出了 int 型能够表示的最大数据,当然会出错了。

这其实是一个隐藏的较深的错误,稍微有些经验的C语言程序员能够避开变量的数据溢出,因为C语言中的变量总是需要使用数据类型定义的,而某种数据类型能够表达的数据范围,程序员心里一般都有一把标尺。但是常数导致的数据溢出,很多程序员就容易忽略了。

要改正上述C语言代码的错误其实很简单,只需将任意一个乘数转换为宽度更宽的数据类型就可以了,相关C语言代码如下,请看:

#include <stdio.h>

int main()
{
    unsigned long long i = 0;

    i = 146*(unsigned long long)100000000;
    printf("i: %lld\n", i);

    return 0;
}
f16a12f3b5a6cf30283fdf0cdb36ebcf.png

编译并执行这段C语言代码,发现输出与预期一致了。
# gcc t.c
# ./a.out 
i: 14600000000

“char 类型加上 signed 是多余,因为char默认就是有符号的”

计算机存储的最小单位是一个字节(Byte,有人习惯称为“大B”),一个字节通常是 8 个位(bit,有人习惯称为“小b”)。C语言规定 char 型占一个字节的存储空间,还规定了 signed 和 unsigned 两个关键字,分别用于表示有符号数和无符号数。

那么不带 signed 和 unsigned 关键字的 char 型变量到底是有符号还是无符号呢?C语言标准规定这是 Implementation Defined,也即未定义,究竟有无符号取决于编译器,有些编译器默认 char 型是有符号的(但不是 所有编译器都如此)。

C语言标准的原则之一是:优先考虑效率,可移植性其次。这就要求C语言程序员非常清楚这些规则,如果项目要求了可移植性,那么在编写代码时,就必须了解哪些写法是不可移植的,应该避免使用。

当然了,也没有必要视“不可移植”为洪水猛兽,例如 Linux 的内核C语言代码使用了很多 gcc 的特性,以得到最佳的执行效率,在编写代码时就没打算使用别的编译器编译,“可移植性”也就无从说起。

不过,这也不是说C语言程序员就可以随意写代吗,如果要写不可移植的代码,就必须明白代码中的哪些部分不可移植,以及这么设计的原因,一般来说,如果不是为了执行效率,就完全没有理由故意写不可移植的C语言代码。

本节主要讨论的两个问题是C语言初学者容易忽略的。但是也有些程序员认为没有必要太抠细节。

对于其他编程语言也许如此,但是对于C语言程序员来说,如果不够“抠门”,不想着如何尽量多节约一个字节出来,如何写出可移植性强以及效率高的代码出来,对自身的能力发展是没有好处的。这是由嵌入式C语言程序开发本身的特性决定的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK