一个警告 - 有符号和无符号整数表达式的比较

94
我目前正在学习《Accelerated C++》,在第二章的练习2-3中遇到了一个问题。
程序主要是接受一个名字,然后在星号框架内显示问候信息 - 即以 * 包围的 Hello!。
在示例程序中,作者使用 const int 来确定问候信息和星号之间的填充(空格)。作为练习的一部分,他们要求读者询问用户输入他们想要的填充大小。
看起来很简单,我已经要求用户输入了两个整数(int)并将它们存储起来,并更改了程序以使用这些整数。但是,当我编译时,却收到以下警告:
Exercise2-3.cpp:46:警告:有符号和无符号整数表达式之间的比较
经过一些研究,似乎是因为代码尝试将其中一个整数(int)与 string::size_type 进行比较,这是可以的。但是我想知道 - 这是否意味着我应该将其中一个整数更改为 unsigned int?明确指定我的整数是有符号还是无符号重要吗?
 cout << "Please enter the size of the frame between top and bottom you would like ";
 int padtopbottom;
 cin >> padtopbottom;

 cout << "Please enter size of the frame from each side you would like: ";
 unsigned int padsides; 
 cin >> padsides;

 string::size_type c = 0; // definition of c in the program
 if (r == padtopbottom + 1 && c == padsides + 1) { // where the error occurs

这是相关的代码,变量 c 是类型为 string::size_type 的字符串类型,因为我们不知道问候语会有多长 - 但是,作者在使用 const int 时没有遇到问题,为什么现在我遇到了这个问题?此外,如果有人完成了《Accelerated C++》 - 这个问题是否会在后面的章节中解释?

我正在使用 Geany 上的 g++ 在 Linux Mint 上进行编译,如果这有帮助或区别(因为我读到确定 string::size_type 是什么时可能会有影响)。


2
难道你不认为你想要无符号整数吗?我想不出为什么顶部和底部应该是负数的逻辑原因。 - Woot4Moo
这是正确的,我在上面的帖子中提到了这一点,但我仍然不明白为什么作者在使用const int时,他们的示例程序没有出现这个问题?我相信我会在书中找到答案,但还是很好奇。 - Tim Harrington
算了吧 - 显然在那种情况下它不会发出警告,因为整数始终将是1...哎呀。 - Tim Harrington
1
一般来说,增加范围并不值得使用无符号整数类型来计数的麻烦。无符号数字还具有保证的回绕行为,使它们略微不够高效。 - Jon Purdy
5
作者可能看到了同样的警告,但选择忽略它。不要认为书籍的作者比普通程序员更有知识和谨慎。 - Kristopher Johnson
6个回答

126

如果将变量与大小进行比较,通常声明为unsignedsize_t是一个好主意,以避免这个问题。尽可能使用精确的类型进行比较(例如,当与std::string的长度进行比较时,请使用std::string::size_type)。

编译器会对比较有符号和无符号类型发出警告,因为有符号和无符号整数的范围不同,当它们互相比较时,结果可能会令人惊讶。如果必须进行这样的比较,应该明确地将其中一个值转换为与另一个值兼容的类型,可能需要在确保转换有效后进行。例如:

unsigned u = GetSomeUnsignedValue();
int i = GetSomeSignedValue();

if (i >= 0)
{
    // i is nonnegative, so it is safe to cast to unsigned value
    if ((unsigned)i >= u)
        iIsGreaterThanOrEqualToU();
    else
        iIsLessThanU();
}
else
{
    iIsNegative();
}

11
我知道现行的 C 标准有时要求负的有符号值比无符号值更大,但是是否应该将这种情况视为过时?我希望标准能够发展,至少允许编译器产生算术上正确的行为(即如果有符号值为负,则比较较小,如果无符号值超过有符号类型的最大值,则比较较大)。在没有显式类型转换的情况下,编译器被要求产生荒谬的行为,这似乎很奇怪。 - supercat
7
@supercat: 由于整数比较会编译成一条机器指令,而任何测试或边缘情况处理都需要多条机器指令,所以你建议的内容不太可能作为C语言的特性而被添加进去...即使程序员知道它不是必要的,它也肯定不能成为默认行为,因为这将不必要地降低性能。 - Blake Miller
@BlakeMiller:想要将有符号值和无符号值作为无符号值进行比较的代码可以将其中一个强制转换并运行“全速”。否则,在许多情况下,比较和跳转需要两个指令和三个指令之间的差异,这比手动处理各种情况的代码更便宜。 - supercat
1
@BlakeMiller:(我之所以说两个而不是三个,是因为大多数比较两个数字的代码将使用一个指令进行比较并基于它们设置标志。在许多情况下,编译器可以安排事情,使得在比较之前,“符号”标志将保留其中一个操作数的上位位,因此在比较之前单个条件跳转就足以确保正确的语义)。请注意,由于有多种实现正确语义的方式,编译器可以选择最便宜的方式。编写C代码以实现正确的语义将更困难。 - supercat
7
为了展示“结果可能出人意料”,下面这个程序(在顶部插入 #include <cstdio>,我使用的是 g++ 4.4.7)将打印出“true”,表明(有符号的)-1 大于(无符号的)12:int main(int, char**) { int x = -1; unsigned int y = 12; printf("x > y: %s\n", x > y ? "true":"false"); return 0; } - villapx

9
昨天我在解决《C++ Primer》中的问题2-3时遇到了完全相同的问题。关键是将所有要进行比较(使用布尔运算符)的变量更改为兼容的类型。在这种情况下,这意味着使用string::size_type(或unsigned int,但由于此示例使用前者,因此我将坚持使用前者,尽管两者在技术上是兼容的)。请注意,在他们的原始代码中,他们确实对c计数器执行了此操作(书中第2.5节第30页),正如你所指出的那样。
使这个例子更复杂的是,不同的填充变量(padsides和padtopbottom)以及所有计数器都必须被更改为string::size_type
针对你的例子,你发布的代码最终会看起来像这样:
cout << "Please enter the size of the frame between top and bottom";
string::size_type padtopbottom;
cin >> padtopbottom;

cout << "Please enter size of the frame from each side you would like: ";
string::size_type padsides; 
cin >> padsides;

string::size_type c = 0; // definition of c in the program

if (r == padtopbottom + 1 && c == padsides + 1) { // where the error no longer occurs

注意,在之前的条件语句中,如果您没有将变量r初始化为string :: size_type,则会出现错误。因此,您需要使用类似以下内容来初始化for循环:

    for (string::size_type r=0; r!=rows; ++r)   //If r and rows are string::size_type, no error!

因此,基本上一旦您将string :: size_type变量引入混合物中,任何时候当您想对该项执行布尔运算时,所有操作数必须具有兼容类型才能在没有警告的情况下编译。


6
重要的区别在于有符号整数和无符号整数对最后一位的解释不同。在有符号类型中,最后一位表示数字的符号,例如:

0001代表1,有符号和无符号都是如此 1001代表-1,有符号,代表9,无符号

(为了清楚起见,我避免了补码问题!这不是整数在内存中的精确表示方式!)

你可以想象知道你是与-1还是+9进行比较是有区别的。在许多情况下,程序员只是太懒了,不愿将计数整数声明为无符号(这会使for循环头膨胀)。通常这不是问题,因为使用整数时,你必须计数到2^31才能被符号位所限制。这就是为什么它只是一个警告。因为我们懒得写'unsigned'而不是'int'。


1
@Tim: "unsigned" 是 "unsigned int" 的同义词。你应该使用 unsigned int 或者 stl 标准计数/迭代变量类型 std::size_t(也是一个同义词)。在所有“迭代元素 0 到 n”的情况下使用 unsigned 是最佳实践。它可以提高清晰度并消除警告,所以它是一个赢家;-) - AndreasT
9
有符号整数的内部表示取决于编译器(即机器)。由于存在一些问题(+/-零是其中之一),所以带符号位的表示法并不常用。大多数机器使用二进制补码表示负数。其优点是可以使用普通(无符号)算术而无需任何更改。在二进制补码表示法中,-1将被表示为1111。 - sstn
1
@AndreasT:虽然为了清晰起见“避免整个补码问题”是可以理解的,但您可以使用与几乎所有平台使用的2的补码兼容的示例。1001表示-1是一个糟糕的选择,更好的选择是1111等于-1有符号和15无符号” - MestreLion
此外,说最后一位的唯一区别是解释错误了:当使用2的符号表示法时,所有位都会以不同的方式“解释”!最后一位只告诉您一个有符号数字是正数还是负数。 - MestreLion
“last bit”这个术语并不像其他选择那样清晰,因为“last”可以指最不重要或最重要的位。建议在个位数时使用“least significant”,在无符号的最高有效位时使用“most significant”,在有符号类型中所称的“last bit”处使用“sign bit”。C规范使用“sign bit”。 - chux - Reinstate Monica
显示剩余2条评论

4

在极端情况下,无符号整数可以比有符号整数更大。
因此,编译器会生成一个警告。如果您确定这不是问题,请随意将类型强制转换为相同类型,以使警告消失(使用C++强制转换以便易于识别)。

或者,将变量设置为相同的类型,以防止编译器抱怨。
我的意思是,是否可能有负填充?如果是,则保持为int。否则,您应该使用无符号整数,并让流捕获用户输入负数的情况。


2
最初的回答是主要问题在于底层硬件——CPU只有比较两个带符号值或比较两个无符号值的指令。如果将带符号、负值传递给无符号比较指令,它会将其视为一个很大的正数。因此,-1(补码表示法下所有位都为1)变成了相同位数的最大无符号值。
8位:-1的带符号值和255的无符号值相同 16位:-1的带符号值和65535的无符号值相同 等等
因此,如果您有以下代码:
int fd;
fd = open( .... );

int cnt;
SomeType buf;

cnt = read( fd, &buf, sizeof(buf) );

if( cnt < sizeof(buf) ) {
    perror("read error");
}

你会发现,如果read(2)调用由于文件描述符变为无效(或其他错误)而失败,cnt将被设置为-1。当与sizeof(buf)进行比较时,这是一个无符号值,if()语句将为false,因为0xffffffff不小于一些(合理的,而不是虚构的最大大小)数据结构的sizeof()。
因此,你必须编写上述if语句,以消除有符号/无符号警告:
if( cnt < 0 || (size_t)cnt < sizeof(buf) ) {
    perror("read error");
}

这只是大声地指出了问题。最初的回答。
1.  Introduction of size_t and other datatypes was crafted to mostly work, 
    not engineered, with language changes, to be explicitly robust and 
    fool proof.
2.  Overall, C/C++ data types should just be signed, as Java correctly
    implemented.

如果您的值太大以至于找不到适用的有符号值类型,那么您使用的处理器太小或选择的语言中值的幅度太大了。如果像金钱一样每个数字都很重要,大多数语言都有提供无限精度数字的系统。C/C++不能很好地处理这个问题,您必须在类型周围非常明确地设定所有内容,就像其他答案中提到的那样。"最初的回答"

0

或者使用这个头文件库并编写:

// |notEqaul|less|lessEqual|greater|greaterEqual
if(sweet::equal(valueA,valueB))

不必关心有符号/无符号或不同的大小


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接