1) 흔히 발생하는 메모리 문제
- 메모리 관련 버그가 발생하는 상황을 명확히 정리하기란 쉽지 않다.
메모리 누수나 잘못된 포인터가 발생하는 원인은 코드마다 다양하다.
메모리 문제를 한 번에 해결할 수 있는 방법은 없지만, 흔히 발생하는 문제의 유형이 알려져 있고,
이러한 문제를 찾아서 해결하는데 도움이 되는 도구도 몇 가지 나와 있다.
(1) 스트링 과소 할당 문제
- C 스타일 스트링에서 가장 흔히 발생하는 문제는 과소 할당(underallocation)이다.
이 문제는 주로 프로그래머가 스트링의 끝을 나타내는 널문자('\0')가 들어갈 공간을 빼먹고
공간을 할당할 때 발생한다.
- 또한, 프로그래머가 스트링의 최대 크기를 특정한 값으로 미리 정해둘 때도 발생한다.
C 스타일 스트링 함수는 크기에 제한을 두지 않기 때문에 스트링에 할당된 메모리 공간을 얼마든지 넘어갈 수 있다.
- 과소 할당 문제에 대한 예를 들면 다음 코드와 같다.
먼저 네트워크에서 읽은 데이터를 C 스타일 스트링에 저장한다.
이 작업은 반복문으로 처리한다.
네트워크를 토해 한 번에 받을 수 있는 데이터 양이 제한돼 있기 때문이다.
루프를 한 번 돌 때마다 getMoreData()를 호출한다.
- 이 함수는 동적으로 할당한 메모리에 대한 포인터를 리턴한다.
데이터를 다 받았다며 getMoreData()는 nullptr를 리턴한다.
C 함수는 strcat()은 C 스타일 스트링 인수 두 개를 받아서
첫 번째 스트링 뒤에 두 번째 스트링을 이어붙인다.
이 과정에서 버퍼의 크기가 결과로 나올 스트링의 크기보다 커야 한다.
- 과소 할당 문제를 해결하는 방법은 다음 세 가지다.
가장 바람직한 방법 순으로 나열했다.
1. C++ 스타일 스트링을 사용한다. 그러면 스트링을 연결하는 작업에 필요한 메모리를 알아서 관리해준다.
2. 버퍼를 전역 변수나 스택(로컬) 변수로 만들지 말고 힙 공간에 할당한다.
공간이 부족하면 현재 스트링을 저장하는데 부족한 만큼만 추가로 할당하고,
원본 버퍼를 새 버퍼로 복사한 뒤, 스트링을 연결하고 나서 원본 버퍼를 삭제한다.
3. 최대 문자 수('\0 포함')를 입력받아서 그 길이를 넘어선 부분은 리턴하지 않고,
현재 버퍼에 남은 공간과 현재 위치를 항상 추적한다.
(2) 메모리 경계 침범
- 포인터는 단지 메모리 주소일뿐이며 메모리에서 아무 곳이나 가리킬 수 있다고 설명했다.
실제로 이렇게 아무 곳이나 가리키는 상황이 종종 발생한다.
예를 들어, 어떤 이유로 스트링의 끝을 표현하는 '\0' 문자가 사라졌다고 하자.
이 때 다음과 같이 스트링의 모든 문자를 'm'으로 바꾸는 함수를 호출하면
루프의 종료 조건을 만족하지 못하기 때문에 스트링에 할당된 공간을 지나서도
계속해서 'm'으로 채운다.
- 이 함수에 종료 문자가 잘못된 스트링을 입력하면 결국 메모리에서 중요한 영역까지
덮어써서 프로그램이 뻗어버린다.
프로그램에서 객체에 관련된 메모리 영역이 갑자기 'm'으로 채워지면
분명 좋지 않은 상황이 펼쳐질 것이다.
- 이러한 문제가 스트링이 아닌 배열에 발생하는 것은
버퍼 오버플로 에러(buffer overflow error)라고 부른다.
지금까지 알려진 악명 높은 바이러스나 웜 중 상당수는 이 버그를 악용해서
경계를 벗어난 메모리 영역을 덮어쓰는 방식으로
현재 구동중인 프로그램에 악의적인 코드를 주입한 것이다.
- 현재 나와 있는 메모리 검사 도구는 버퍼 오버플로 문제를 찾아준다.
또한 string이나 vector와 같은 C++의 고급 기능을 활용하면
C 스타일의 스트링이나 배열을 사용할 때 흔히 발생하던 여러 버그를 방지할 수 있다.
(3) 메모리 누수
- 메모리 누수 문제는 C/C++ 프로그래밍 과정에서 발견하거나 해결하기 가장 힘든 작업으로 손꼽힌다.
원하는 결과를 내도록 힘들여 만든 프로그램이 실행될수록 메모리 공간을 잡아먹는다면
메모리 누수 현상이 발생한 것이다.
이럴 때는 가장 먼저 스마트 포인터를 도입하는 것이 좋다.
- 메모리 누수 현상은 할당했던 메모리를 제대로 해제하지 않을 때 발생한다.
얼핏 보면 조금만 주의를 기울이면 쉽게 해결될 거라고 여기기 쉽다.
하지만 new에 대응되는 delete를 빠짐없이 작성하더라도
누수 현상이 발생하는 경우가 있다.
다음에 나온 Simple 클래스 코드를 보면 할당한 메모리를 적절히 해제하도록 작성했다.
- 그런데 doSomething()을 보면 outSimplePtr 포인터가 다른 Simple 객체를 가리키도록 변경했는데,
이 때 기존에 가리키던 객체를 삭제하지 않아서 메모리 누수가 발생했다.
객체를 가리키고 있던 포인터를 놓치면 그 객체를 삭제할 방법이 없다.
class Simple
{
public:
Simple() { mIntPtr = new Int(); }
~Simple() { delete mIntPtr; }
void setValue(int value) { *mIntPtr = value; }
private:
int* mIntPtr;
};
void doSomething(Simple*& outSimplePtr)
{
outSimplePtr = new Simple(); // 버그! 원본 객체를 삭제하지 않았다.
}
int main(){
Simple* simplePtr = new Simple(); // Simple 객체 하나를 할당한다.
doSomething(simplePtr);
delete simplePtr; // 두 번째 객체만 해제한다.
return 0;
}
(4) 중복 삭제와 잘못된 포인터
- delete로 포인터에 할당된 메모리를 해제하면 그 메모리를 프로그램의 다른 부분에서 사용할 수 있다.
하지만 그 포인터를 계속 쓰는 것을 막을 수는 없다.
이를 댕글링 포인터(dangling pointer)라 부른다.
이 때 중복 삭제하면 문제가 발생한다.
한 포인터에 delete를 두 번 적용하면 이미 다른 객체를 할당한 메모리를 해제해버리기 때문이다.
- 중복 삭제 문제와 해제한 메모리를 다시 사용하는 문제를 사전에 찾아내기란 굉장히 힘들다.
짧은 시간 동안 메모리를 삭제하는 연산이 두 번 실행되면
그 사이에 같은 메모리를 재사용할 가능성이 적기 때문에 프로그램이 계속해서 정상적으로 실행될 수 있다.
- 마찬가지로 객체를 삭제한 직후에 곧바로 다시 사용하더라도 그 영역이 삭제 전 상태로 계속 남아 있을 가능성이
많기 때문에 문제가 생기지 않을 수 있다.
- 그렇다 하더라도 문제가 발생하지 않는다고 장담할 수는 없다.
메모리를 할당할 때 삭제된 객체를 보존하지 않기 때문이다.
설령 제대로 작동하더라도 삭제된 객체를 이용하는 것은 바람직한 코드 작성 방식이 아니다.
- 마이크로소프트 비주얼 C++이나 밸그라인드처럼 메모리 누수 감지 기능을 제공하는 도구는
중복 삭제 문제와 해제된 객체를 계속 사용하는 문제를 감지하는 기능도 함께 제공한다.
- 스마트포인터를 사용하라는 충고를 무시하고 계속해서 일반 포인터를 사용하려면
메모리를 해제한 후 포인터값을 nullptr로 초기화하는 작업만이라도 반드시 하기 바란다.
그러면 실수로 같은 포인터를 두 번 삭제하거나 해제한 포인터를 계속 사용하는 문제를 막을 수 있다.
참고로 nullptr로 설정된 포인터에 대해 delete를 호출해도 문제가 발생하지 않는다.
그저 아무 일도 하지 않을 뿐이다.
'CS Fundamental' 카테고리의 다른 글
[CS Fundamental] 공유 라이브러리 기초 (0) | 2025.01.14 |
---|---|
[CS Fundamental] 단순 전자우편 전송 프로토콜(SMTP)이란? (0) | 2025.01.14 |
[CS Fundamental] PNG 포맷에서 투명을 어떻게 표현하나요? (0) | 2025.01.10 |
[CS Fundamental] 로그인용 암호를 관리자도 모르게 보관하려면 어떻게 하나요? (0) | 2025.01.10 |
[CS Fundamental] 네이버 홈페이지가 표시되기위해 몇개의 파일이 필요할까요? (0) | 2025.01.10 |