unsigned int的坑

本文深入探讨了C语言中使用有符号数与无符号数进行比较和减法运算时可能遇到的非直观错误,通过实例解析了这些错误产生的原因,包括隐式类型转换的影响,并提供了代码优化建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

string.length() 的返回值就为unsigned int !!!

转载来源:http://blog.youkuaiyun.com/ljianhui/article/details/10367703

尽量避免有符号数与无符号数的比较运算和避免减法运算,在很多时候,在unsigned的世界里,x-y>0与x>y都是不等价的

正如我们所知道的,编程语句都有很多的基本数据类型,如char,inf,float等等,而在C和C++中还有一个特殊的类型就是无符号数,它由unsigned修饰,如unsigned int等。大家有没想过,就是因为这些不同的类型,而使大家编写的看似非常正确的程序出现了预想不到的错误呢?

 

一、迷惑人的有符号下无符号数的比较操作

废话不多说,马上来看一下例子,让你先来体验一下这个奇妙的旅程,源代码文件名为unsigned.c,源代码如下:

[cpp] view plain copy

 

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. int main()  
  5. {  
  6.     int a = -1;  
  7.     unsigned int b = 1;  
  8.   
  9.     if(a > b)  
  10.         printf("a > b, a = %d, b = %u\n", a, b);  
  11.     else  
  12.         printf("a <= b, a = %d, b = %u\n", a, b);  
  13.     exit(0);  
  14. }  

输出结果为:

 

 

看到输出结果之后,你可能会大吃一惊,-1竟然大于1,你没有看错,从输出结果上来看的确是这样。为什么会产生这样的结果呢?这还得从C语言对同时包含有符号数和无符号数表达式的处理方式讲起。

 

二、有符号数与无符号运算时数强制类型转换方式及底层表示

 

当执行一个运算时(如这里的a>b),如果它的一个运算数是有符号的而另一个数是无符号的,那么C语言会隐式地将有符号 参数强制类型为无符号数,并假设这两个数都是非负的,来执行这个运算。这种方法对于标准的算术运算来说并无多大差异,但是对于像<和>这样的运算就可能产生非直观的结果。

 

所以对应回上面的例子,就是它先把-1(变量a的值)这个有符号数强制转换成无符号数,然后再与1(变量b)的值,来进行比较,并假设这两个数原本都是非负的,然后进行比较。那么-1转换为无符号数后,其值为多少呢?你可以写一个小小的程序来验证一下,在32和64位的机子上,-1对应的无符号数应该是4 294 967 295,即32位的无符号数的最大值(UMax),所以if中的条件总是为真。

 

要想这段代码正常执行,我们需要怎么办呢?很简单,把if语句改为if(a > (int)b)即可。这样程序就会认为是两个有符号数在进行比较,-1就不会隐式地转换为无符号数而变成UMax。

 

可能你已经有一个问题,为什么使用强制类型,把变量b的类型变成int程序就能正常,而-1转换成无符号数为什么会是4 294 967 295呢?这就得从整型数据在计算机中的表示和C语言对待强制类型转换的方式说起。

 

我们知道,整数在计算机中通常是以补码的形式存在的,而-1的补码(用4个字节储存)为1111,1111,1111,1111。而C语言对于强制类型转换是怎么处理的呢?对大多数C语言的实现,处理同样字长的有符号数和无符号数之间的相互转换的一般规则是:数值可能会改变,但是位模式不变。也就是说,将unsigned int强制类型转换成int,或将int转换成unsigned int底层的位表示保持不变。

 

也就是说,即使是-1转换成unsigned int之后,它在内存中的表示还是没有改变,即1111,1111,1111,1111。我们知道在计算机的底层,数据是没有类型可言的,所有的数据非0即1。数据类型只有在高层的应用程序才有意义,也就是说,同样的储存表示对于应用程序而言可能对应着不同的数据,例如1111,1111,1111,1111对于有符号数而言它表示-1,但对于无符号数而言,它表示UMax,但是它们的底层存储都是一样的。现在你应该明白为什么-1转换成无符号数之后,就成了UMax了吧。

 

三、查看数据的底层表示

 

为了证明上面所说的内容,请再看下面的代码,里面有个函数show_byte,它可以把从指针start开始的len个字节的值以16进制数的形式打印出来。源文件为showbyte.c,代码如下:

[cpp] view plain copy

 

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. void show_bytes(unsigned char *start, int len)  
  5. {  
  6.     int i = 0;  
  7.     for(; i < len; ++i)  
  8.         printf(" %.2x", start[i]);  
  9.     printf("\n");  
  10. }  
  11.   
  12. int main()  
  13. {  
  14.     int a = -1;  
  15.     unsigned int b = 4294967295;  
  16.   
  17.     printf("a = %d, a = %u\n", a, a);  
  18.     printf("b = %d, b = %u\n", b, b);  
  19.   
  20.     show_bytes((unsigned char*)&a, sizeof(int));  
  21.     show_bytes((unsigned char*)&b, sizeof(unsigned int));  
  22.     exit(0);  
  23. }  

输出为:

 

 

分析:printf函数中,%u表示以无符号数十进制的形式输出,%d表示以有符号十进制的形式输出。通过show_bytes函数,我们可以看到,-1与4 294 967 295的底层表示是一样的,它们的位全部都是全1,即每个字节表示为ff。

 

四、由于无符号数减法引起的错误

 

你可能会说,你不会用一个无符号数与一个有符号数作比较,所以你觉得你可以放心了,但是来看看下面的两段代码。

代码1是一个求数组中前length个数据的和的函数,数组中元素的个数由参数length给出,代码如下:

[cpp] view plain copy

 

  1. float sum_elements(float a[], unsigned length)  
  2. {  
  3.     int i = 0;  
  4.     float sum = 0;  
  5.     for(i = 0; i <= length -1; ++i)  
  6.         sum += a[i];  
  7.     return sum;  
  8. }  

如果我告诉你这是一段有错的代码,可能你也不太相信,因为这个函数的一切看起来是这么的自然,因为数据的长度(或个数)肯定是一个非负数,所以把length声明为一个unsigned很合理,计算的数据个数和返回类型也正确。的确如此,但是这都是在length不为0的情况,试想,当调用函数时,把0作为参数传递给length会发生什么事情?回想一下前面我们所说的知识,因为length是unsigned类型,所以所有的运算都被隐式地被强制转换为unsigned类型,所以length-1(即0-1 = -1),-1对应的无符号类型的值为UMax,所以for循环将会循环UMax次,数组也会越界,发生错误。那么如何优化上面的代码呢?其实答案非常简单,你也可以自己想一想,这里就给出答案吧,就是把for循环改为:

[cpp] view plain copy

 

  1. for(i = 0; i < length; ++i)  

因为去除了length-1,所以当length为0时也能正常比较。

 

接下来是代码2,它是一个判断第一个字符串是否长于第二个字符串,若是,返回1,若否返回0,代码如下:

[cpp] view plain copy

 

  1. int strlonger(char *s1, char *s2)  
  2. {  
  3.     return strlen(s1) - strlen(s2) > 0;  
  4. }  

如果我又跟你说这段代码是有bug,你现在找不找得出来呢,还是认为这段代码是没有任何问题的呢?说真的就这么看这个函数好像的确是没有什么问题,但是如果你知道了strlen函数的原型,可能你就会有点明白了,在Linux下可用man 3 strlen命令查看,strlen函数的原型为:

[cpp] view plain copy

 

  1. size_t strlen(const char *s);  

注意这里有一个数据类型size_t,它被定义在stdio.h文件中,其实它就是unsigned int,一个字符串的长度当然不可能为负,这样的定义显然是合理的,但是有时却因为这样,而存在不少的问题,如函数strlonger的实现。当s1的长度大于等于s2时,这个函数并没有什么问题,但是你可以想像,当s1的长度小于s2的长度时,这个函数会返回什么吗?没错,因为此时strlen(s1) - strlen(s2)为负(从数学的角度来解释的话),而又由于程序把它作为unsigned为处理,则此时的值肯定是一个比0大的值。换句话来说,这个函数只有在strlen(s1) == strlen(s2)时返回假,其他情况都返回真。

 

下面是我的测试代码:

[cpp] view plain copy

 

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4.   
  5. int strlonger(char *s1, char *s2)  
  6. {  
  7.     return strlen(s1) - strlen(s2) > 0;  
  8. }  
  9.   
  10. int main()  
  11. {  
  12.     char s1[] = "abc";  
  13.     char s2[] = "cd";  
  14.   
  15.     if(strlonger(s1, s2))  
  16.         printf("s1 is longer than s2, s1 = %s, s2 = %s\n", s1, s2);  
  17.     else  
  18.         printf("s1 is shorter than s2, s1 = %s, s2 = %s\n", s1, s2);  
  19.   
  20.     if(strlonger(s2, s1))  
  21.         printf("s2 is longer than s1, s2 = %s, s1 = %s\n", s2, s1);  
  22.     else  
  23.         printf("s2 is shorter than s1, s2 = %s, s1 = %s\n", s2, s1);  
  24. }  

运行结果如下:

 

 

从运行结果来看,确实如此,只要s1与s2长度不等,就返回真。那么我们在怎么样改善这段代码呢?其实答案也是很简单的,所函数改为如下即可:

[html] view plain copy

 

  1. int strlonger(char *s1, char *s2)  
  2. {  
  3.     return strlen(s1) > strlen(s2);  
  4. }  

这样就可以利用两个无符号数进行直接的比较,而不会因为减法而出现负数(数学上来说)而影响比较结果。

 

五、建议

这么看来,unsigned还真是一个危险的东西,大家还是要谨慎使用啊。其实个人建议,没有什么必要的原因,就不要使用unsigned,即使有时它看起来是那么的合理,因为有它在的运算,很多时候会产生非直观的错误,而且这种错误还非常难发现。如果你要使用的话,则尽量避免有符号数与无符号数的比较运算和避免减法运算,在很多时候,在unsigned的世界里,x-y>0与x>y都是不等价的。

<think>好的,我现在要帮用户了解C++中memset的用法。首先,我得回忆一下自己对这个函数的理解。memset是C标准库中的一个函数,用来将某一块内存的内容设置为指定的值。通常在C++中也会用到,不过可能需要更注意类型安全的问题。 首先,用户可能想知道基本用法。我需要解释函数原型:void* memset(void* ptr, int value, size_t num)。这里三个参数分别是内存地址、要设置的值和字节数。需要说明ptr指向的内存区域,value虽然是int类型,但实际会被转换为unsigned char处理,所以通常用来填充字节。 然后,用户可能想知道常见的应用场景。比如初始化数组、结构体清零或者设置特定值。这时候要举例子,比如将int数组全部设为0,或者字符数组设为某个字符。不过要注意,对于非字符类型的数组,使用memset可能会有问题,比如用memset将int数组设为1,每个字节都会被设为0x01,导致每个int实际是0x01010101,这可能不是用户想要的。 接下来需要提到常见的错误用法。例如,错误计算第三个参数的大小,导致越界或者未完全初始化。比如,一个int数组有10个元素,应该用sizeof(int)*10,而如果只传10的话,可能只初始化了前10个字节,即前两个或两个半int(根据int的大小)。这时候需要强调num是以字节为单位的。 另外,C++中可能更推荐使用构造函数或者STL的填充方法,比如std::fill,因为类型更安全。比如std::fill(arr, arr+10, 0)可能比memset更合适,尤其是对于非POD类型。 还要注意memset不能用于初始化对象,特别是带有虚函数或动态内存的类,这样会破坏对象内部结构,导致未定义行为。 用户可能还会问为什么memset的value参数是int类型,但实际处理的是unsigned char。这时候需要解释历史原因,以及类型转换的细节。例如,传入255会被当作0xFF处理,而如果传入256,由于超出unsigned char范围,会被截断为0x00。 最后,总结使用memset的注意事项:正确计算字节数,仅用于POD类型,避免用于非字节级别的初始化,以及在C++中考虑更安全的替代方案。 可能还需要比较memset和其他初始化方法的性能差异,比如在大量数据时,memset通常更高效,因为可能使用底层优化。但现代编译器的std::fill也可能有类似的优化,所以不能一概而论。 需要确保给出的示例代码正确,比如使用char数组和int数组的例子,并指出潜在的问题。例如: char str[100]; memset(str, 'A', 100); // 正确,每个字符被设为'A' int arr[10]; memset(arr, 0, sizeof(arr)); // 正确,所有字节设为0 memset(arr, 1, sizeof(arr)); // 错误,每个int的每个字节是0x01,结果不是1 还要提醒用户,结构体填充字节的问题,即结构体可能有对齐的填充字节,使用memset会覆盖这些部分,但通常不会有问题,除非有特殊需求。 总之,需要结构清晰,逐步从函数原型、用法、示例、常见错误、替代方案等方面介绍,确保用户全面理解memset的正确使用方法和限制。</think>在C++中,`memset` 是一个用于内存初始化的基础函数,其正确使用需注意以下要点: --- ### 一、函数原型 ```cpp void* memset(void* ptr, int value, size_t num); ``` - **参数说明**: 1. `ptr`:指向待操作内存的指针 2. `value`:填充值(以`unsigned char`形式处理) 3. `num`:填充的字节数 --- ### 二、核心用途 1. **内存清零** ```cpp int arr[10]; memset(arr, 0, sizeof(arr)); // 所有字节设为0 ``` 2. **字符数组初始化** ```cpp char str[100]; memset(str, 'A', 100); // 所有字符设为'A' ``` 3. **结构体初始化** ```cpp struct Data { int a; float b; }; Data obj; memset(&obj, 0, sizeof(Data)); // 结构体清零 ``` --- ### 三、常见错误 1. **错误计算字节数** ```cpp int arr[10]; memset(arr, 0, 10); // 错误!仅初始化前10字节(假设int为4字节) ``` 2. **误用非字节初始化** ```cpp int arr[10]; memset(arr, 1, sizeof(arr)); // 错误!每个int变为0x01010101,而非数值1 ``` 3. **操作非POD类型** ```cpp class Bad { virtual void f() {} }; Bad obj; memset(&obj, 0, sizeof(Bad)); // 破坏虚函数表,导致未定义行为 ``` --- ### 四、C++替代方案 1. **`std::fill` 模板函数** ```cpp int arr[10]; std::fill(arr, arr+10, 0); // 类型安全,直接赋值0 ``` 2. **构造函数初始化** ```cpp struct Data { int a = 0; float b = 0.0f; }; ``` --- ### 五、性能比较 - **`memset`优势**:底层优化,适合大块内存操作(如MB级数据) - **`std::fill`优势**:类型安全,可优化为与`memset`等效的机器码 --- ### 六、总结 - **适用场景**:POD类型的内存块操作(如基本类型数组、结构体清零) - **避原则**: - 确保`num`参数为总字节数(可用`sizeof`计算) - 仅将`value`视为单字节值(0~255) - 避免用于非平凡对象(含虚函数、动态资源)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值