当前位置: 首页 > article >正文

C缺陷与陷阱 — 7 可移植性缺陷

目录

1 应对C语言标准变更

2 标识符的名称限制

3 整数的大小

4 字符是有符号整数还是无符号整数

5 移位运算符

6 内存位置0

7 除法运算时发生的截断


1 应对C语言标准变更

使用新特性可以使代码更容易编写且减少错误,但可能会导致代码在旧编译器上无法编译。square函数是一个简单的数学函数,用于计算一个数的平方。在新风格中,函数原型明确指定了参数类型,如下所示:

double square(double x) {
    return x * x;
}

如果这样写,这个函数在很多编译器上都不能通过编译。如果我们,ANSI标准为了保持和以前的用法兼容,按照旧风格来重写这个函数,这就增强了它的可移植性。但在旧风格中,函数原型不包含参数类型(这里只举例说明,旧标准现在基本不再使用),如下所示:

double square(x) 
    double x;  // 函数参数声明
{
    return x * x;  // 计算x的平方并返回结果
}

这种可移植性为了与旧用法保持一致,我们必须在调用了square函数的程序中作如下声明:

double square() 

但是函数square的声明中并没有对参数类型做出说明,因此在编译main函数时,编译器无法得知函数square的参数类型应该是double还是其他类型。如下面的示例,函数调用将会报错。

double square();
main()
{
  printf("g\n",square(3));
}

为避免这类问题,可在声明中带入参数类型,3会被自动转换为double类型:

double square(double):
main()
{
  printf("g\n",square(3));
}

另一种改写的方式是,在这个程序中显式地给函数square传入一个double类型的参数:

double square(double x); // 显式指定参数类型
main() {
    printf("%g\n", square(3.0)); // 显式传入double类型的参数
}

2 标识符的名称限制

在C语言的不同实现中,对标识符的处理方式存在差异。一些实现会接受标识符中的所有字符,而另一些实现可能会截断过长的标识符。连接器对外部名称也有特定的限制,例如可能只允许使用大写字母。ANSI C标准规定,C语言实现至少能够区分外部名称的前6个字符,且不区分大小写。

因此,为了确保程序的可移植性,选择外部标识符的名称时需要谨慎。例如,不应使用容易混淆的名称,如print_fields和print_float,或者State和STATE。例如下面的示例代码:

// 定义一个函数 Malloc
char Malloc(unsigned n) {
    char *p;
    char *malloc(unsigned); // 声明 malloc 函数的原型

    p = malloc(n); // 调用 malloc 函数尝试分配 n 字节的内存
    if (p == NULL) // 如果 malloc 分配失败,返回 NULL
        panic("out of memory"); 
    return p; // 如果分配成功,返回分配的内存的指针
}

这个程序的意图是在需要分配内存的地方调用Malloc函数,而不是直接调用malloc。如果malloc失败,panic函数会被调用,终止程序并打印错误消息。这样,客户程序就不需要在每次调用malloc时都进行检查。

然而,如果编译环境是不区分大小写的C语言实现,那么函数malloc和Malloc可能会被视为相同,导致调用Malloc时实际上是在递归调用自己。在这种情况下,程序在第一次尝试分配内存时对Malloc函数的调用将引起一系列递归调用,而这些递归调用没有返回点,最终导致程序崩溃。

3 整数的大小

C语言提供了三种不同长度的整数类型:short、int 和 long,以及字符类型。这些类型的长度有以下特点:

  • 长度是非递减的,即 short ≤ int ≤ long。
  • 普通整数(int 类型)足够大,可以容纳任何数组下标。
  • 字符长度由硬件决定,现代大多数机器的字符长度是8位,但有些实现中字符长度可能是16位。

ANSI标准规定 long 至少32位,short 和 int 至少16位。这些规定意味着我们不能依赖于具体的位数,但可以保证一定的最小长度。

在编程实践中,这意味着我们不能依赖于具体的精度,而应该根据需要选择合适的类型。例如,如果需要存储可能达到千万数量级的数值,最好声明为 long 类型。

4 字符是有符号整数还是无符号整数

在C语言中,char 类型可以是无符号的或有符号的,这取决于编译器的实现。大多数现代编译器将字符实现为8位整数。

  • 字符的符号性:当需要将字符值转换为较大的整数时,字符是有符号还是无符号的变得重要。如果字符是有符号的,转换为 int 时符号位会扩展;如果是无符号的,则高位会填充0。
  • 字符的取值范围:如果字符被视为有符号,其取值范围是 -128 到 127;如果被视为无符号,则取值范围是 0 到 255。

为了确保字符被视为无符号整数,程序员可以声明 unsigned char。这样,无论在什么编译器上,字符在转换为整数时多余的位都会被填充为0。

与此相关的一个常见错误认识是:如果c是一个字符变量,使用(unsigned) c就可得到与c等价的无符号整数。这是会失败的,因为在将字符c转换为无符号整数时,c将首先被转换为int型整数,而此时可能得到非预期的结果。正确的方式是使用语句(unsigned char) c,因为一个unsigned char类型的字符在转换为无符号整数时无需首先转换为int型整数,而是直接进行转换。

#include <stdio.h>

int main() {
    char signedChar = -1; // 有符号字符
    unsigned char unsignedChar = 255; // 无符号字符

    // 打印字符的整数值
    printf("Signed char as int: %d\n", signedChar);
    printf("Unsigned char as int: %u\n", unsignedChar);

    // 正确转换有符号字符为无符号整数
    printf("Correctly converted signed char as unsigned int: %u\n", (unsigned char)signedChar);

    return 0;
}

5 移位运算符

使用移位运算符的程序员经常对这样两个问题感到困惑:

(1)向右移位时的填充问题

  • 对于无符号数,空出的位由0填充。
  • 对于有符号数,C语言实现可能用0或符号位的副本填充。

如果程序员关心向右移位时空出的位,可以将变量声明为无符号类型,这样空出的位都会被设置为0。

(2)移位计数的取值范围:

  • 移位计数必须大于等于0,且小于被移位对象的位数。
  • 这个限制确保了移位操作可以在硬件上高效实现。

例如,对于32位的整数,n << 31 和 n << 0 是合法的,而 n << 32 和 n << -1 是不合法的。

(3)移位和除法不完全等同

即使在某些C语言实现中,有符号整数的右移会用符号位填充新位,这也不等同于除以2的幂。例如,(-1) >> 1 的结果通常不为0,但 1 / 2 在大多数C实现中结果为0。

在C语言中,对于有符号整数,-1 >> 1 的操作结果取决于整数的位数和计算机的架构。对于一个32位的整数:

-1 在二进制中通常表示为一个32位的全1的模式,即 11111111 11111111 11111111 11111111。当你将 -1 向右移动1位时,根据补码规则,最左边会填充1(符号位扩展),结果仍然是 -1。因此,在大多数现代计算机上,-1 >> 1 的结果是 -1。

如果已知 low + high 为非负,那么:mid = (low + high) >> 1; 与 mid = (low + high) / 2; 完全等效,但前者的执行速度要快得多。

6 内存位置0

空指针不指向任何对象,使用它除了赋值或比较外都是非法的。例如,使用空指针进行strcmp操作会导致未定义行为,不同编译器结果可能不同。

  • 某些编译器对内存地址0有硬件级的读保护,使用空指针会导致程序立即终止。
  • 有些编译器允许读但不允许写内存地址0,空指针看似指向字符串,但内容可能是无意义的“垃圾信息”。
  • 还有些编译器允许读写内存地址0,错误使用空指针可能导致覆盖操作系统内容,造成严重问题。

在所有C程序中,错误使用空指针都是未定义的,但可能在某些编译器上“看似”能工作,直到换到另一台机器上才会出现问题。

检查这类问题的一个方法是将程序移到不允许读取内存地址0的机器上运行。以下是一个示例程序,用于检测C语言实现如何处理内存地址0:

#include <stdio.h>

int main() {
    char *p = NULL;
    printf("Location 0 contains %d\n", *p);
    return 0;
}

在禁止读取内存地址0的机器上,这个程序会失败。在其他机器上,它将打印出内存位置0中存储的字符内容。

7 除法运算时发生的截断

假定我们让q = a /b,r = a % b,商为q,余数为r,在整数除法和余数运算中,我们希望满足以下三条性质:

  1. 定义余数的关系a == qb+r
  2. 符号变化:改变a的正负号应改变q的符号,但不改变q的绝对值。
  3. 余数范围:当b>0时,希望保证0≤ r <b。这对于使用余数作为哈希表索引等场景很重要。

然而,这三条性质不可能同时满足。考虑一个简单的例子:3/2,商为1,余数也为1。此时,第1条性质满足。(-3)/2的值应该是多少呢?如果要满足第2条性质,答案应该是-1,但如果是这样,余数就必定是-1,这样第3条性质就无法满足。如果我们首先满足第3条性质,即余数是1,这种情况下根据第1条性质则商是-2,那么第2条性质又无法满足了。

因此,C语言或者其他语言在实现整数除法截断运算时,必须放弃上述三条原则中的至少一条。大多数程序设计语言选择了放弃第3条,而改为要求余数与被除数的正负号相同。这样,性质1和性质2就可以得到满足。

然而,C语言的定义只保证了性质1,以及当a>=0且b>0时,保证 |r|<b以及r>=0.后面部分的保证与性质2或者性质3比较起来,限制性要弱得多。

C语言的定义虽然有时候会带来不需要的灵活性,但大多数时候,这个定义对让整数除法运算满足其需要来说还是够用了的。


http://www.kler.cn/a/399650.html

相关文章:

  • 开源项目低代码表单设计器FcDesigner获取表单的层级结构与组件数据
  • 【unity小技巧】一些unity3D灯光的使用与渲染及性能优化方案
  • 【爬虫实战】抓取某站评论
  • sql中的聚合函数
  • 校园二手交易网站毕业设计基于SpringBootSSM框架
  • 大模型(LLMs)进阶篇
  • 【计算机网络】协议定制
  • uni-app快速入门(五)--判断运行环境及针对不同平台的条件编译
  • ZYNQ程序固化——ZYNQ学习笔记7
  • WebRTC视频 02 - 视频采集类 VideoCaptureModule
  • SQL注入注入方式(大纲)
  • 运算放大器的学习(一)输入阻抗
  • 阅读2020-2023年《国外军用无人机装备技术发展综述》笔记_技术趋势
  • Spring Boot框架:电商系统的技术优势
  • RN开发遇到的坑
  • 力扣 最小路径和
  • Hyper-v中ubuntu与windows文件共享
  • ML 系列: 第 23 节 — 离散概率分布 (多项式分布)
  • SpringBoot开发——整合 apache fileupload 轻松实现文件上传与下载
  • Freemarker模板 jar!/BOOT-INF/classes!/**.html
  • 编译安卓SDK时出现:600:26 test android/soong/ui/build/paths的解决方案
  • Swift 宏(Macro)入门趣谈(二)
  • 【网络安全】记一次APP登录爆破
  • 抖音热门素材去哪找?优质抖音视频素材网站推荐!
  • Flutter网络通信-封装Dio
  • 网络安全:数字时代的护城河