네트워크나 파일 같은 외부 입출력을 열고 닫는 작업은 실패할 가능성이 워낙 높기 때문에 프로그램 작성에서 에러 처리가 거의 무조건적인 필수이다.

하지만 메모리 할당은 그렇지 않다. 수~수십 MB 정도 공간을 요청하거나 재할당하는 것쯤은 요즘 컴에서는 실패할 가능성이 0에 수렴한다. malloc의 결과값이 NULL인지 일일이 체크하는 코드는 요즘 거의 찾을 수 없다. C++의 new 연산자도 예전에는 실패 시 NULL 리턴이었지만 지금은 예외를 던지는 형태로 디자인이 바뀐 지 오래다. 그거 일일이 NULL 체크를 하는 건 너무 남사스럽고 성가시고 민망하기 때문이다.

메모리 할당을 위해 어지간해서는 C++의 깔끔한 new와 delete를 쓴다지만, C++ 연산자에는 메모리의 재할당 기능이 없기 때문에 이를 위해서는 여전히 malloc/realloc/free 쌍이 쓰인다. 그리고 좀 원시적인 테크닉이긴 하지만 가변 길이 구조체의 메모리 할당을 위해서도 크기 지정이 자유로운 C 스타일의 메모리 함수가 필요하다. 아니면 operator new 함수를 직접 호출하든가 말이다.

그런데 realloc은 실행이 실패했을 때의 상태가 꽤 복잡하다. 보통 ptr=realloc(ptr, newsize) 같은 형태로 활용을 하는데, 재할당이 실패했다고 생각해 보자. 이때는 realloc은 재할당을 할 수 없어서 NULL을 되돌린다. 이는 분명 비정상적인 오류 상황이고 프로그램이 그에 대한 별도의 대비를 하긴 해야 하지만, 그렇더라도 ptr이 원래 가리키던 메모리는 아무 이상이 없다. 그런데 ptr에다가 무턱대고 마치 malloc의 리턴값처럼 NULL을 대입해 버리면 ptr은 소실되고 메모리 leak이 발생하게 된다.

그러니 실행이 실패하더라도 메모리 leak은 발생하지 않게 하려면

ptr_tmp=realloc(ptr, newsize);
if(ptr_tmp) ptr=ptr_tmp; //성공
else { } //실패

번거롭지만 이렇게 임시 포인터 변수를 하나 추가로 둬서 실행이 성공했을 때에만 포인터의 실제값을 반영하게 해야 안전하다. 본인은 이 점을 한 번도 생각을 안 하고 있었는데 비주얼 C++ 2012에서부터 추가된 코드 정적 분석기가 지적을 해 주는 걸 보고서야 “아하!”하고 무릎을 쳤다.

이런 것을 생각하면 realloc의 실패야말로 리턴값보다는 예외 처리로 알려 주는 게 더 편리하겠다는 생각이 든다.
절차형으로 실행되는 컴퓨터 프로그램에서는, 당연한 말이지만 n+1단계 명령은 그 앞의 1~n단계의 모든 명령들이 성공적으로 차곡차곡 잘 실행됐다는 전제하에서만 실행 가능하다. 중간에 뭔가 탈이 났다면 더 진행을 할 수 없으며 어디까지 앞뒤로 되돌아가면 되는지를 컴퓨터가 스스로 판단할 수 없다. 컴퓨터에게는 인간 같은 유도리가 존재하지 않는다. 그렇기 때문에 그런 정보가 없다면 그 프로그램은 전체가 강제 종료되는 것밖에 답이 없다.

자동차 운전을 하는 사람이라면 단순히 핸들과 페달과 변속기를 조작하는 것 말고도 사고가 났을 때의 대처 요령과 보험사 연락처 같은 것도 숙지하고 있어야 하듯, 컴퓨터 프로그램도 마찬가지이다. 중간에 탈이 나도 최대한 부드럽게 수습하고, 피치 못할 상황에서  프로그램이 죽더라도 최소한 지금 작성 중인 문서를 저장이라도 한 뒤에 죽는 그 로직 자체도 프로그래밍이 돼 있어야 한다. 그것이 바로 예외 처리라는 분기 제어에 해당한다.
아울러, 숙달된 프로그래머라면 예외 처리를 구현하는 데 드는 추가 오버헤드와 비용을 숙지해 둘 필요도 있다. 수많은 객체들의 생명 주기를 관리하면서 여러 함수들을 한꺼번에 이탈하는 것도 그냥 될 리는 없으니 말이다.

C/C++은 애초에 운영체제/하드웨어 차원에서의 crash는 있어도 언어 차원에서의 예외 처리라는 게 아예 존재하지 않던 언어이다 보니 이쪽의 지원이 다른 언어들보다 상대적으로 미비하다. C++에 try/catch 키워드는 한참 뒤에 등장했으며 언어 자체는 이 예외 구문을 전혀 사용하지 않는다. 이걸 사용하는 건 라이브러리 계층에서이다. 그리고 예외 처리용 객체를 날려 줄 때조차도 new로 메모리를 할당했다면 해제를 수동으로 해 줘야 하니 불편한 점이 아닐 수 없다.

다시 본론인 realloc 얘기로 돌아온다.
저런 예외 처리도 오버헤드가 크니 싫고 리턴값만으로 모든 책임을 회피하고 싶다면, realloc 함수의 프로토타입을 차라리 이렇게 설계했으면 더 편했을지도 모른다.

bool realloc(void **pptr, size_t newsize);

void **라니 참 COM스러워 보이지만(CoCreateInstance, IUnknown::QueryInterface ㅋㅋ), C++이라면 템플릿 함수로 이걸 감싸서 지저분함을 한결 예방할 수 있을 것이다.

if(realloc((void **)&ptr, newsize)) { /* 성공 */ }
else { /* 실패 */ }
free(ptr);

내가 무엇을 의도하는지는 딱 보면 알 수 있을 것이다. 기존 메모리를 가리키고 있는 포인터의 주소를 받아서, 재할당이 성공하면 그 포인터가 가리키는 값을 그대로 바꿔 버리고 true를 되돌리는 것이다. 어차피 지금 realloc 함수는 ptr=realloc(ptr, newsize)라고 ptr이 함수 인자(input) 겸 리턴값(output) 형태로 동시에 쓰이고 있으며, 재할당이 성공했다면 예전 주소는 보관하고 있을 필요가 전혀 없으니 말이다.

실패했다면 ptr은 *ptr이든 **ptr이든 아무 변화가 없고 리턴값만 false가 된다. free(ptr)을 해 주는 한 어떤 경우든 메모리 leak 걱정은 안 해도 된다. realloc 함수가 이렇게 만들어지는 게 더 낫지 않았나 싶은 생각이 든다.
뭐, realloc이 결코 실패하지 않는다고 가정하고 프로그램이 막무가내로 동작한다면, 차라리 NULL 포인터 일대를 액세스하다가 확실하게 죽는 게 기존 메모리를 범위를 초과하여 건드리다가 죽는 것보다는 더 안전할지도 모르겠지만 말이다.

끝으로 하나 더. fopen에서 접근 모드와(read/write 등) 데이터 처리 모드(바이너리/텍스트) 인자는 들어올 수 있는 조합이 뻔하고 상수 명칭 조합으로 처리해도 하등 이상할 게 없을 텐데, 왜 하필 더 파싱도 어렵게 문자열을 쓰고 있는지도 이유를 모르겠다. 딱히 확장의 여지가 있어 보이지도 않는데 굳이 _open 같은 저수준 함수와 형태를 달리할 이유가 없다. 이런 것들이 C 라이브러리에 대해서 궁금한 점이다.

Posted by 사무엘

2015/04/14 08:25 2015/04/14 08:25
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1083

Trackback URL : http://moogi.new21.org/tc/trackback/1083

Leave a comment
« Previous : 1 : ... 1253 : 1254 : 1255 : 1256 : 1257 : 1258 : 1259 : 1260 : 1261 : ... 2204 : Next »

블로그 이미지

그런즉 이제 애호박, 단호박, 늙은호박 이 셋은 항상 있으나, 그 중에 제일은 늙은호박이니라.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/12   »
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        

Site Stats

Total hits:
3042730
Today:
2357
Yesterday:
1700