ProgramingTip

C ++는 음수 저장이있는 'for'루프에서 충돌합니다.

bestdevel 2021. 1. 5. 21:16
반응형

C ++는 음수 저장이있는 'for'루프에서 충돌합니다.


다음 코드는 실행 오류와 함께 C ++와 충돌합니다.

#include <string>

using namespace std;

int main() {
    string s = "aa";
    for (int i = 0; i < s.length() - 3; i++) {

    }
}

이 코드는 충돌하지 않지만 :

#include <string>

using namespace std;

int main() {
    string s = "aa";
    int len = s.length() - 3;
    for (int i = 0; i < len; i++) {

    }
}

나는 그것을 설명하는 방법을 전혀 모른다. 이 행동의 이유는 무엇일까요?


s.length()부호없는 정수 유형입니다. 3을 빼면 음수가됩니다. 의 경우 매우 크다는unsigned 의미 입니다.

해결 방법 (문자열이 INT_MAX까지 길면 유효 함)은 다음과 같이하는 것입니다.

#include <string>

using namespace std;

int main() {

    string s = "aa";

    for (int i = 0; i < static_cast<int> (s.length() ) - 3; i++) {

    }
}

루프에 들어 가지 않을 것입니다.

"서명 된 값과 서명되지 않은 값 비교"라는 경고를 가능성이있을 것입니다. 문제는 다음과 같은 경고를 무시 하면 정의 된 동작이있는 매우 위험한 암시 적 "정수 변환" (*) 필드에 포함 되지만 따라 가기가 어렵습니다. 가장 좋은 방법은 컴파일러 경고를 무시하지 않는 것입니다.


(*) "정수 프로모션" 에 대해 알고 싶을 수도 있습니다 .


우선 : 충돌합니까? 디버거처럼 프로그램을 단계별로 보겠습니다.

참고 : 루프 본문이 비어 있지 않습니다. 않은 경우 충돌 오는가의 원인은 정수 오버플로를 통한 정의되지 않은 동작 입니다. Richard Hansens의 답변을 참조하십시오.

std::string s = "aa";//assign the two-character string "aa" to variable s of type std::string
for ( int i = 0; // create a variable i of type int with initial value 0 
i < s.length() - 3 // call s.length(), subtract 3, compare the result with i. OK!
{...} // execute loop body
i++ // do the incrementing part of the loop, i now holds value 1!
i < s.length() - 3 // call s.length(), subtract 3, compare the result with i. OK!
{...} // execute loop body
i++ // do the incrementing part of the loop, i now holds value 2!
i < s.length() - 3 // call s.length(), subtract 3, compare the result with i. OK!
{...} // execute loop body
i++ // do the incrementing part of the loop, i now holds value 3!
.
.

우리는 검사가 기대 i < s.length() - 3의 길이가 있기 때문에, 바로 실패하는 s두 가지 (우리는 모든 것이 시작 부분에 주어 잘 못) 및 2 - 3-1, 0 < -1거짓입니다. 그러나 우리는 여기서 "OK"를 얻습니다.

이것은 아니기 때문 s.length()입니다 2. 그것은이다 2u. 부호없는 정수인 std::string::length()리턴 유형 size_t이 있습니다. 루프 상태로 필요한 것이 그래서, 우리는 먼저 값이 있기 s.length()때문에, 2u지금 빼기를 3. 3정수 리터럴이며 컴파일러에서 유형으로 해석합니다 int. 따라서 컴파일러는 2u - 3서로 다른 유형의 두 값 을 계산해야 합니다. 기본 유형에 대한 작업은 동일한 유형으로 작동하는 작업은 다른 유형으로 변환해야합니다. 이 경우에는 unsigned"승리"라는 엄격한 규칙이 있으므로 3get은 3u. 없는 정수에서 부호 2u - 3u수 없습니다 -1u같은 번호가되지 않는 존재 (이 과정의 기호를 가지고 있기 때문에, 잘!). 대신 모든 작업을 계산합니다 modulo 2^(n_bits). n_bits이 유형의 비트 수입니다 (일반적으로 8, 16, 32 또는 64). 그래서 -1우리는 대신에 4294967295u(32 비트로 가정) 얻습니다.

이제 컴파일러는 s.length() - 3(나보다 훨씬 빠릅니다 ;-)), 이제 비교를 시작하겠습니다 : i < s.length() - 3. 값 입력 : 0 < 4294967295u. 다시 말하지만, 다른 유형 00u, 비교 0u < 4294967295u가 분명히 참이고 루프 조건이 긍정적으로 확인되고 이제 루프 본문을 수 있습니다.

증분 후 위에서 변경되는 유일한 것은 값입니다 i. 의 값은 i비교에 필요하면 다시 unsigned int로 변환됩니다.

그래서 우리는

(0u < 4294967295u) == true, let's do the loop body!
(1u < 4294967295u) == true, let's do the loop body!
(2u < 4294967295u) == true, let's do the loop body!

여기에 문제가 있습니다. 루프 본문에서 무엇을 수신합니까? 당신 은 당신의 아마도 당신의 아마도 문자에 접근i^th할 것입니다. 당신의 의도는 아니었지만, 당신은 0과 첫 번째 아니라 두 번째에도 접근하지 않습니다! 두 번째는 존재하지 않습니다 (문자열에 0과 첫 번째 두 문자 만 있기 때문에), 메모리에 액세스 할 수 있습니다 (정의되지 않은 동작). 프로그램이 즉시 충돌 할 필요는 없습니다. 30 분 더 잘 작동하는 것처럼 보일 수 있으므로 실수를 실행하기 어렵습니다. 그러나 경계를 넘어서 메모리에 액세스하는 것은 항상 위험합니다. 여기서 대부분의 충돌이 발생합니다.

요약하면, s.length() - 3예상했던 것과는 다른 값을 얻습니다 . 이로 인해 긍정적 인 루프 조건 검사가 수행되어 루프 본문이 반복적으로 실행되고 그 자체로 메모리에 액세스합니다.

이제이를 피하는 방법, 즉 루프 조건에서 실제로 의미하는 바를 컴파일러에 알리는 방법을 보겠습니다.


-length와 컨테이너 문자열 크기는 본질적으로 부호가 없으므로에 대한 루프에서 부호없는 정수를 사용해야합니다.

이후 unsigned int상당히 상당히 많은 것이므로 이상 작성하는 방법과 다시 루프에서, 그냥 사용합니다 size_t. STL의 모든 컨테이너가 길이 또는 크기를 저장하는 데 사용하는 유형입니다. cstddef플랫폼 독립성을 주장 할 필요가 있습니다 .

#include <cstddef>
#include <string>

using namespace std;

int main() {

    string s = "aa";

    for ( size_t i = 0; i + 3 < s.length(); i++) {
    //    ^^^^^^         ^^^^
    }
}

a < b - 3는 수학적으로와 동일 하게 a + 3 < b서로 바꿀 수 있습니다. 그러나 큰 가치가되는 a + 3 < b것을 방지 b - 3합니다. s.length()부호없는 정수 반환하고 부호없는 정수 2^(bits)는 비트가 유형의 비트 수 (일반적으로 8, 16, 32 또는 64) 인 연산 모듈을 수행합니다 . 따라서 s.length() == 2, s.length() - 3 == -1 == 2^(bits) - 1.


또는 i < s.length() - 3개인 취향 에 사용하는 비용 조건을 추가해야합니다.

for ( size_t i = 0; (s.length() > 3) && (i < s.length() - 3); ++i )
//    ^             ^                    ^- your actual condition
//    ^             ^- check if the string is long enough
//    ^- still prefer unsigned types!

첫 번째 버전 에서는에서는 매우 큰 숫자 를 포함 i하는 부호없는 정수와 정수와 매우 긴 시간 동안 반복 됩니다. 동일한 크기는 (사실상) size_t부호없는 정수 와 동일 합니다. 3그 값에서 빼면 그 값은 언더 플로우되고 계속해서 큰 값이됩니다.

코드의 두 번째 버전에서는 부호없는 값을 부호있는 변수에 할당하여 올바른 값을 얻습니다.

그리고 실제로 충돌을 오류 조건이나 값이 아닙니다. 정의되지 않은 동작의 경우에는 경계를 벗어난 인덱싱 할 가능성이 먹을 것입니다.


for루프 에서 중요한 코드를 사용하고 있습니다.

여기에있는 대부분의 사람들은 내 자신을 포함하여 충돌을 재현 할 수 있습니다. 여기에있는 다른 답변은 for루프 본문에 중요한 코드를 빠뜨 렸고 누락 된 코드가 원인 이라는 가정을 기반으로 한 것입니다. 크래시.

당신이 사용하는 경우 i의 본문에 액세스 메모리 (문자열 아마도 문자)에 for루프, 당신은 최소한의 예를 제공하기 위해 귀하의 질문 중 그 코드를 왼쪽, 다음 충돌이 쉽게 사실에 의해 설명되어 s.length() - 3있습니다 SIZE_MAX부호없는 정수 유형에 대한 모듈 식 산술로 인한 . SIZE_MAX는 매우 큰 숫자이므로 isegfault를 트리거하는 주소에 액세스하는 데 사용될 때까지 계속 커집니다.

그러나 for루프 본문 이 비어 있더라도 이론적으로 코드가있는 그대로 충돌 할 수 있습니다. 충돌하는 구현을 알지 못하지만 컴파일러와 CPU가 이국적 일 수 있습니다.

다음 설명은 질문에서 코드를 생략했다고 가정하지 않습니다. 질문에 게시 한 코드가있는 그대로 충돌한다는 믿음이 필요합니다. 충돌하는 다른 코드에 대한 축약 된 스탠드 인이 아닙니다.

첫 번째 프로그램이 충돌하는 이유

첫 번째 프로그램은 코드에서 정의되지 않은 동작에 대한 반응이기 때문에 충돌합니다. (코드를 실행하려고하면 정의되지 않은 동작에 대한 내 구현의 반응이기 때문에 충돌없이 종료됩니다.)

정의되지 않은 동작은 int. C ++ 11 표준은 다음과 같이 말합니다 ([expr] 절 5 단락 4).

식을 평가하는 동안 결과가 수학적으로 정의되지 않았거나 해당 유형에 대한 표현 가능한 값 범위에 있지 않으면 동작이 정의되지 않습니다.

예제 프로그램에서 값이 2 인 s.length()a size_t반환 합니다. 여기서 3을 빼면 size_t부호없는 정수 유형을 제외하고 음수 1 이됩니다. C ++ 11 표준은 다음과 같이 말합니다 ([basic.fundamental] 절 3.9.1 단락 4) :

선언 된 부호없는 정수 unsigned는 산술 모듈로 2 n 의 법칙을 준수해야합니다. 여기서 n 은 특정 정수 크기의 값 표현에서 비트 수입니다. 46

46) 이것은 결과적으로 부호없는 정수 유형으로 표현할 수없는 결과가 결과 부호없는 정수 유형으로 나타낼 수있는 가장 큰 값보다 하나 더 큰 숫자로 축소되기 때문에 부호없는 산술이 오버플로되지 않음을 의미합니다.

이 방법의 결과는 s.length() - 3A는 size_tSIZE_MAX. 이것은 INT_MAX(로 나타낼 수있는 가장 큰 값) 보다 큽니다 int.

s.length() - 3크기가 너무 크기 때문에 i도달 할 때까지 실행이 루프에서 회전 합니다 INT_MAX. 바로 다음 반복에서 증분을 시도 i하면 결과는 INT_MAX+ 1이되지만의 표현 가능한 값 범위에 포함되지 않습니다 int. 따라서 동작이 정의되지 않았습니다. 귀하의 경우 행동은 충돌하는 것입니다.

내 시스템에서 내 구현의 행동 i과거를 증가는 INT_MAX(설정 래핑하는 것입니다 i로를 INT_MIN)하고 계속. 일단 i도달 -1, 일반적인 산술 변환 (C ++ [EXPR] 제 5 항 9) 원인 i과 동일하기 SIZE_MAX루프 종료되도록.

두 반응 모두 적절합니다. 이것이 정의되지 않은 동작의 문제입니다. 의도 한대로 작동하거나, 충돌하거나, 하드 드라이브를 포맷하거나, Firefly를 취소 할 수 있습니다. 당신은 몰라요.

두 번째 프로그램이 충돌을 피하는 방법

첫 번째 프로그램과 마찬가지로 s.length() - 3이있는 size_t유형입니다 SIZE_MAX. 그러나 이번에는 값이 int. C ++ 11 표준은 다음과 같이 말합니다 ([conv.integral] 절 4.7 단락 3) :

대상 유형이 서명 된 경우 대상 유형 (및 비트 필드 너비)으로 표현할 수 있으면 값이 변경되지 않습니다. 그렇지 않으면 값이 구현에서 정의됩니다.

SIZE_MAX이 너무 커서으로 표현할 수 int없으므로 len구현 정의 값을 얻습니다 (아마도 -1이지만 아닐 수도 있음). 조건 i < len은에 할당 된 값에 관계없이 결국 참이 len되므로 정의되지 않은 동작이 발생하지 않고 프로그램이 종료됩니다.


s.length ()의 유형은 size_t값이 2이므로 s.length ()-3도 unsigned 유형 size_t이며 SIZE_MAX구현이 정의 된 을가집니다 (크기가 64 비트 인 경우 18446744073709551615). 최소 32 비트 유형 (64 비트 플랫폼에서 64 비트 일 수 있음)이며이 높은 숫자는 무한 루프를 의미합니다. 이 문제를 방지하기 위해 당신은 단순히 캐스트 할 수 있습니다 s.length()합니다 int:

for (int i = 0; i < (int)s.length() - 3; i++)
{
          //..some code causing crash
}

두 번째 경우 len는 a signed integer이고 루프에 들어 가지 않기 때문에 -1 입니다.

충돌과 관련하여이 "무한"루프는 충돌의 직접적인 원인이 아닙니다. 루프 내에서 코드를 공유하면 추가 설명을 얻을 수 있습니다.


s.length ()는 unsigned 유형 수량이므로 s.length ()-3을 수행하면 음수가되고 음의 값은 부호없는 변환 사양으로 인해 큰 양의 값으로 저장되고 루프가 무한대로 진행되어 충돌이 발생합니다. .

작동하게하려면 s.length ()를 다음과 같이 타입 캐스트해야합니다.

static_cast <int> (s.length ())


귀하가 겪고있는 문제는 다음 진술에서 발생합니다.

i < s.length() - 3

s.length ()의 결과는 unsigned size_t 유형입니다. 두 가지의 이진 표현을 상상한다면 :

0 ... 010

그런 다음 여기에서 3 개를 대체하면 효과적으로 1을 3 번 이륙합니다. 즉,

0 ... 001

0 ... 000

그러나 왼쪽에서 다른 숫자를 얻으려고 할 때 아래로 흐르는 세 번째 숫자를 제거하는 문제가 있습니다.

1 ... 111

이것은 부호없는 유형 이든 부호있는 유형 이든 상관없이 발생 하지만, 차이점은 부호있는 유형이 숫자가 음수인지 여부를 나타 내기 위해 최상위 비트 (또는 MSB)를 사용한다는 것입니다. 언 플로우가 발생하면 단순히 부호있는 유형에 대해 음수를 나타냅니다 .

반면에 size_t는 unsigned 입니다. 언더 플로되면 이제 size_t가 나타낼 수있는 가장 높은 숫자를 나타냅니다. 따라서 루프는 사실상 무한합니다 (컴퓨터에 따라 최대 size_t에 영향을 미침).

이 문제를 해결하기 위해 몇 가지 다른 방법으로 코드를 조작 할 수 있습니다.

int main() {
    string s = "aa";
    for (size_t i = 3; i < s.length(); i++) {

    }
}

또는

int main() {
    string s = "aa";
    for (size_t i = 0; i + 3 < s.length(); i++) {

    }
}

또는:

int main() {
    string s = "aa";
    for(size_t i = s.length(); i > 3; --i) {

    }
}

주목해야 할 중요한 점은 대체가 생략되었으며 대신 동일한 논리적 평가를 사용하여 다른 곳에서 추가가 사용되었다는 것입니다. 첫 번째와 마지막 모두 루프 i내에서 사용 가능한 값을 변경하는 for반면 두 번째는 동일하게 유지합니다.

나는 이것을 코드의 예로 제공하고 싶었다.

int main() {
    string s = "aa";
    for(size_t i = s.length(); --i > 2;) {

    }
}

약간의 생각 끝에 나는 이것이 나쁜 생각이라는 것을 깨달았습니다. 독자들의 연습은 그 이유를 알아내는 것입니다!


이유는 int a = 1000000000과 같습니다. long long b = a * 100000000; 오류가 발생합니다. 컴파일러가이 숫자를 곱하면 a와 리터럴 1000000000이 int이고 10 ^ 18이 int의 상한보다 훨씬 크기 때문에 int로 평가하므로 오류가 발생합니다. 귀하의 경우에는 s.length ()-3이 있습니다. s.length ()는 unsigned int이므로 음수가 될 수 없으며 s.length ()-3은 unsigned int로 평가되고 값은 -1입니다. 여기에서도 오류가 발생합니다.

참조 URL : https://stackoverflow.com/questions/17398959/c-crashes-in-a-for-loop-with-a-negative-expression

반응형