Computer >> 컴퓨터 >  >> 프로그래밍 >> C 프로그래밍

C와 C++에서 발생하는 정의되지 않은 동작(UB)의 이해

C와 C++에서는 컴파일러가 프로그램의 동작을 보장하지 않는 상황이 존재합니다. 이를 정의되지 않은 동작(Undefined Behavior, UB)이라고 부르며, 같은 코드라도 컴파일러나 실행 환경에 따라 전혀 다른 결과를 낳을 수 있습니다.

이 글에서는 대표적인 정의되지 않은 동작 사례들을 코드 예제와 함께 살펴보고, 각각 어떤 런타임 오류나 예기치 않은 결과를 유발하는지 알아보겠습니다.

1. 0으로 나누기

정수를 0으로 나누는 연산은 표준에서 정의되지 않은 동작으로 규정되어 있습니다. 런타임 오류가 발생하거나 프로그램이 비정상 종료될 수 있습니다.

예제 코드

#include <iostream>
using namespace std;
int main() {
    int x = 10, y = 0;
    int z = x / y;   // 0으로 나누기 → 정의되지 않은 동작
    cout << "Done" << endl;
}

실행 결과

0으로 나누기 연산으로 인한 런타임 오류

2. 초기화되지 않은 변수 사용

초기화하지 않은 지역 변수의 값은 결정되어 있지 않습니다. 이 값을 읽는 것 역시 정의되지 않은 동작이며, 컴파일러마다 다른 결과가 나올 수 있습니다.

예제 코드

#include <iostream>
using namespace std;
int main() {
    bool x;          // 초기화되지 않음
    if(x == true)
        cout << "true value";
    else
        cout << "false value";
}

실행 결과

false value (컴파일러에 따라 결과가 달라질 수 있음)

3. 널 포인터 역참조

NULL로 설정된 포인터가 가리키는 메모리에 접근하면 프로그램이 즉시 비정상 종료됩니다. 접근하려는 메모리 영역 자체가 유효하지 않기 때문입니다.

예제 코드

#include <iostream>
using namespace std;
int main() {
    int *ptr = NULL;
    cout << "The pointer value is: " << *ptr;   // 널 포인터 역참조
}

실행 결과

널 포인터 접근으로 인한 런타임 오류

4. 배열 범위를 벗어난 접근

배열의 크기를 초과하는 인덱스로 요소에 접근하는 것도 정의되지 않은 동작입니다. 크기가 10인 배열을 인덱스 10까지 순회하면 범위를 벗어난 접근이 발생합니다.

예제 코드

#include <iostream>
using namespace std;
int main() {
    int array[10];
    for(int i = 0; i<=10; i++) {   // i < 10이어야 안전함
        cout << array[i] << endl;
    }
}

실행 결과

범위를 벗어난 접근으로 인한 런타임 오류.
일부 컴파일러에서는 오류 없이 임의의 쓰레기 값을 반환할 수 있음

5. 부호 있는 정수(signed int)의 오버플로우

부호 있는 정수형의 최댓값을 초과하는 연산 역시 정의되지 않은 동작입니다. 많은 환경에서 최솟값으로 되돌아가는(순환하는) 것처럼 보이지만, 이는 표준이 보장하는 동작이 아닙니다.

예제 코드

#include <iostream>
using namespace std;
int main() {
    int x = INT_MAX;               // 2147483647
    cout << "x + 1: " << x + 1;    // 오버플로우 발생
}

실행 결과

x + 1: -2147483648
부호 있는 int의 최솟값으로 순환됨

6. 문자열 리터럴 수정 시도

문자열 리터럴은 읽기 전용(read-only) 메모리 영역에 저장됩니다. 문자 포인터로 리터럴을 가리킨 뒤 내용을 변경하려고 하면 런타임 오류가 발생합니다.

예제 코드

#include <iostream>
using namespace std;
int main() {
    char *str = "Hello World";   // 문자열 리터럴은 수정 불가
    str[2] = 'x';                // 상수 영역 변경 시도
    cout << str;
}

실행 결과

상수 변수의 값을 변경하려고 시도했기 때문에 런타임 오류 발생

마무리

정의되지 않은 동작은 컴파일 타임에는 잡히지 않는 경우가 많아 더욱 위험합니다. 위 예제들에서 볼 수 있듯이, 같은 코드라도 컴파일러 최적화 수준이나 플랫폼에 따라 전혀 다른 방식으로 동작할 수 있습니다.

안전한 C/C++ 코드를 작성하기 위해서는 나누기 전에 제수가 0인지 확인하고, 모든 변수를 선언과 동시에 초기화하며, 포인터 사용 전 NULL 여부를 검사하고, 배열 인덱스가 항상 유효 범위 내에 있는지 점검해야 합니다. 또한 문자열 조작이 필요하다면 std::string과 같은 안전한 자료구조를 활용하는 것이 좋습니다.