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

C++ 프로그래밍에서 자주 발생하는 정의되지 않은 동작(UB) 8가지

C++에서 정의되지 않은 동작(Undefined Behavior, UB)이란 표준이 동작을 규정하지 않아 컴파일러가 임의로 처리할 수 있는 상황을 말합니다. UB가 발생하면 프로그램이 정상적으로 실행될 수도 있고, 충돌하거나 예측 불가능한 결과를 낳을 수도 있습니다. 더 위험한 점은 최적화 과정에서 컴파일러가 이러한 상황을 전제에 포함하지 않기 때문에, 겉보기에는 멀쩡해 보이는 코드도 의도치 않게 깨질 수 있다는 것입니다.

다음은 C++ 프로그래밍에서 가장 흔하게 나타나는 정의되지 않은 동작의 원인들입니다. 모두 C++ 표준에서 명시적으로 UB로 규정된 사항이므로, 코드 작성 시 반드시 피해야 합니다.

C++에서 피해야 할 대표적인 UB 사례

1. 부호 있는(signed) 정수 오버플로

부호 있는 정수형(int, long 등)의 연산 결과가 해당 타입이 표현할 수 있는 범위를 초과하면 정의되지 않은 동작이 됩니다. 예를 들어 INT_MAX + 1과 같은 연산은 랩어라운드(wrap-around)를 보장하지 않습니다.

2. 잘못된 포인터 역참조

다음과 같은 포인터를 역참조(dereference)하는 것은 모두 UB입니다.

  • NULL 포인터
  • 크기가 0인 new 할당이 반환한 포인터
  • 아직 확실하게 초기화되지 않은 포인터
  • 배열의 끝을 넘어선 위치를 가리키는 포인터

3. 수명이 끝난 객체에 대한 포인터 사용

범위(scope)를 벗어나 소멸했거나 delete로 이미 해제된 객체를 포인터를 통해 접근하는 것은 정의되지 않은 동작입니다. 이른바 '댕글링 포인터(dangling pointer)' 문제입니다.

4. 배열 경계를 벗어나는 포인터 연산

포인터 산술 연산의 결과가 유효한 배열의 경계 밖(배열 마지막 요소 다음 위치까지는 허용)을 가리키게 되면 UB입니다.

5. 호환되지 않는 타입으로의 포인터 변환

서로 호환되지 않는 타입의 객체 포인터로 변환하여 접근하면 타입 시스템을 위반하게 되며, 이는 정의되지 않은 동작으로 이어집니다.

6. 음수 오프셋으로 객체·배열 접근

객체나 배열에 대해 음수 오프셋 위치에서 읽기 또는 쓰기를 수행하는 것 역시 UB입니다.

7. 생성자·소멸자에서의 순수 가상 함수 호출

객체의 생성자나 소멸자 안에서 그 객체의 순수 가상 함수(pure virtual function)를 호출하는 것은 정의되지 않은 동작입니다. 생성·소멸 단계에서는 파생 클래스의 가상 함수 구현이 아직 준비되지 않았기 때문입니다.

8. 반환값이 있는 함수에서 값 미반환

반환 타입이 void가 아닌 함수가 값을 반환하지 않고 흐름이 종료되면 UB입니다. 단, main 함수는 예외적으로 return 문이 생략되면 0을 반환한 것으로 간주됩니다.

마무리

정의되지 않은 동작은 컴파일러 경고만으로는 놓치기 쉽습니다. -fsanitize=undefined(UBSan), Valgrind 같은 도구를 활용하고, 스마트 포인터와 범위 기반 컨테이너를 적극적으로 사용하면 대부분의 UB를 사전에 예방할 수 있습니다. 안전한 C++ 코드를 작성하기 위해 위 사항들을 항상 염두에 두시기 바랍니다.