C++에는 정의되지 않은 동작(Undefined Behavior, UB)이라 불리는 위험한 상황들이 존재합니다. 이는 표준에서 명확한 동작을 규정하지 않기 때문에, 컴파일러나 실행 환경에 따라 결과가 달라지거나 프로그램이 비정상 종료될 수 있습니다. C++를 다양한 목적으로 활용하려는 개발자라면 이러한 UB 사례들을 반드시 숙지하고 코드에서 피해야 합니다.
아래에서는 실제로 정의되지 않은 동작을 유발하는 대표적인 코드 예제와 그 결과를 살펴보겠습니다.
1. 0으로 나누기 (Divide by Zero)
정수를 0으로 나누는 연산은 C++ 표준에서 정의되지 않은 동작입니다.
#include <iostream>
using namespace std;
int main() {
int x = 10, y = 0;
int z = x / y; // 0으로 나누기 → UB
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; // 널 포인터 접근 → UB
}실행 결과: 널 포인터에 접근하여 런타임 오류가 발생합니다.
4. 배열 범위를 벗어난 접근 (Out of Bounds)
크기가 10인 배열에 인덱스 0부터 10까지 접근하면 마지막 인덱스 10은 유효 범위를 초과합니다. 배열 인덱스는 0부터 시작하므로 유효한 인덱스는 0~9까지입니다.
#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;
cout << "x + 1: " << x + 1; // INT_MAX + 1 → UB
}실행 결과: x + 1: -2147483648 — 부호 있는 int의 최솟값으로 순환하는 것처럼 보입니다.
6. 문자열 리터럴 수정
문자열 리터럴은 읽기 전용(read-only) 메모리 영역에 저장되므로, 이를 수정하려는 시도는 정의되지 않은 동작입니다.
#include <iostream>
using namespace std;
int main() {
char *str = "Hello World";
str[2] = 'x'; // 문자열 리터럴 수정 → UB
cout << str;
}실행 결과: 상수 성격의 데이터를 변경하려고 시도하기 때문에 런타임 오류가 발생합니다.
마무리
위 예제들은 모두 컴파일 타임에는 문제없이 통과될 수 있지만, 실행 시점에 심각한 오류를 일으킬 수 있는 대표적인 정의되지 않은 동작들입니다. 안전한 C++ 코드를 작성하려면 변수 초기화 습관화, 포인터 유효성 검사, 배열 경계 확인, 오버플로우 주의, std::string 사용 등의 방어적 프로그래밍 기법을 적용하는 것이 좋습니다.