Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |
Tags
- 백준
- 언리얼
- 탐색기법
- 개발
- UE5
- 언리얼로그
- 코딩테스트
- 게임개발
- 얌얌코딩
- 링크드리스트
- 내가해냄
- 오늘의에러
- 게임프로그래밍
- TPS
- 연산자오버로딩
- 구조체포인터
- unreal
- 커스텀로그
- dfs
- 코딩
- C++
- c++자료구조
- fstring
- 프로그래밍
- 미라클모닝
- 자료구조
- 재귀함수
- permutation
- 구조체
- 개발자
Archives
- Today
- Total
All is well
[C++/ETC] 미정의 동작(Undefined Behavior, UB) 본문
미정의 동작(Undefined Behavior, UB)은 C 및 C++에서 언어 표준이 특정한 동작을 정의하지 않은 경우를 의미합니다. 즉, 같은 코드라도 실행 환경이나 컴파일러에 따라 예측할 수 없는 결과를 초래할 수 있음을 이야기합니다.
미정의 동작이 발생하면 프로그램이 비정상 종료되거나, 잘못된 결과를 출력하거나, 심지어는 정상적으로 실행되는 것처럼 보이지만 내부적으로는 오류를 포함할 수도 있습니다.
미정의 동작이 왜 위험한가?
- 재현 불가능: 같은 코드를 실행해도 실행할 때마다 다른 결과가 나올 수 있습니다.
- 컴파일러 최적화 영향: 컴파일러가 UB를 가정하지 않고 최적화하므로, 코드가 비정상적으로 동작할 수 있습니다.
- 디버깅 어려움: UB가 발생해도 프로그램이 즉시 크래시하지 않을 수도 있어 버그를 찾기 어렵습니다.
따라서 UB를 피하는 것이 안정적인 C++ 프로그래밍의 핵심입니다.
미정의 동작이 발생하는 주요 원인
1. 댕글링 포인터 사용
int* ptr = new int(42);
delete ptr;
std::cout << *ptr; // UB 발생
// 발생 이유
- `new`를 통해 할당된 메모리는 `delete`를 호출하면 해제됩니다.
- 하지만 `delete` 후에도 `ptr`은 여전히 해제된 메모리 주소를 저장하고 있으며, 운영체제가 이를 즉시 재사용할 수도 있습니다.
- 이 상태에서 `*ptr`을 참조하면 미정의 동작이 발생할 가능성이 높습니다.
// 방지 방법
- `delete` 후 `ptr = nullptr;`로 설정합니다.
2. 배열 범위 초과 접근
int arr[3] = {1, 2, 3};
std::cout << arr[5]; // UB 발생
// 발생 이유
- C++은 배열 접근 시 범위 검사를 수행하지 않습니다.
- 배열 크기를 초과한 인덱스에 접근하면 할당되지 않은 메모리의 값을 읽거나, 심한 경우 세그멘테이션 폴트(Segmentation Fault)가 발생할 수 있습니다.
// 방지 방법
- `std::vector::at()`을 사용하여 범위 초과를 방지합니다.
std::vector<int> vec = {1, 2, 3};
std::cout << vec.at(5); // 예외 발생, 안전함!
3. 0으로 나누기
int x = 10;
int y = 0;
std::cout << (x / y); // UB 발생
// 발생 이유
- 정수를 0으로 나누는 것은 C++에서 미정의 동작으로 간주됩니다.
- 대부분의 CPU에서 정수 나눗셈 연산에서 0으로 나누면 Divide-by-Zero 예외가 발생하며, 프로그램이 강제 종료될 가능성이 있습니다.
- 반면, 부동소수점 연산(`float`, `double`)에서는 `0.0 / 0.0`은 `NaN`, `x / 0.0`은 `Infinity`로 처리됩니다.
// 방지 방법
- 나누기 전에 분모가 0인지 체크합니다.
if (y != 0) {
std::cout << (x / y);
} else {
std::cout << "0으로 나눌 수 없습니다.";
}
4. 초기화되지 않은 변수 사용
int x;
std::cout << x; // UB 발생
// 발생 이유
- 지역 변수(스택 변수)는 자동으로 초기화되지 않으며, 해당 메모리의 기존 값(쓰레기 값)을 그대로 가지게 됩니다.
- 디버그 빌드에서는 초기화되지 않은 변수를 사용할 경우 경고가 발생할 수 있지만, 릴리즈 빌드에서는 확인이 어렵습니다.
// 방지 방법
- 변수를 항상 초기화 후 사용합니다.
int x = 0;
std::cout << x;
5. 잘못된 타입 변환 (Type Punning)
int x = 42;
double* p = (double*)&x; // 잘못된 형변환
std::cout << *p; // UB 발생
// 발생 이유
- `int` 타입 변수를 `double*`로 변환하여 접근하면, `int`와 `double`의 메모리 레이아웃이 다르기 때문에 예상하지 못한 값이 출력될 수 있습니다.
- CPU가 메모리 정렬(Alignment)을 강제하는 플랫폼에서는 버스 오류(Bus Error) 또는 세그멘테이션 폴트가 발생할 수도 있습니다.
- C++에서는 strict aliasing rule을 따르므로, 다른 타입의 포인터를 통해 값을 읽는 것은 UB를 유발할 가능성이 있습니다.
// 방지 방법
- reinterpret_cast 대신 정확한 타입 변환을 사용합니다.
int x = 42;
double y = static_cast<double>(x);
std::cout << y; // 올바른 변환
미정의 동작을 방지하는 방법
- 삭제된 포인터는 `nullptr`로 설정합니다.
- 배열 접근 시 `std::vector::at()`을 사용합니다.
- 나누기 연산 전에 0을 체크합니다.
- 변수는 항상 초기화 후 사용합니다.
- 올바른 타입 변환을 사용합니다 (`static_cast<>`).
'C++ > ETC' 카테고리의 다른 글
| [C++/ETC] `!!temp`와 `temp != nullptr` 비교 (1) | 2025.02.09 |
|---|