libft 완성기(2) : mem 시리즈 부터 malloc 이 필요한 함수들

Ryu(Paul)·2021년 12월 18일
1

42_Seoul_생존기

목록 보기
6/7
post-thumbnail

시작하면서

libft 정리를 한다고 했지만 하면 할 수록 복잡해지는 기분이다;;
디펜스 준비한 것들, 이런걸 다 정리해서 넣는 다는게 보통일이 아닌듯 하다.
그나마 위안을 삼을 수 있는건, 내 스스로 이해된 순간 다른 사람들과 어떤식을 대화하면서 해당 내용을 이해해야 하는지는 기존의 이해한 것들을 통해 혹은 내 언변스킬? 을 통해 나름대로 잘 정리해 나갈 수 있다는 점 정도일 것이다.

앞서 언급 했듯이 해당 내용은 libft를 어느 정도 구성한 분들에게 도움이 되도록 하고 있다. 디펜스 준비 등, 개념적으로 디테일한 부분들 위주로만 정리하고 코드는 딱히 안 적을 것이다. 어차피 깃허브에 넘쳐나고 베끼는건 어떻게든 하니까.

Part 1 - Libc함수들 (2)

이번 파트는 필수 중에서도 메모리를 다루는 함수들, 그리고 할당(malloc)이 필요한 녀석들이다.
< mem 시리즈>
메모리 시리즈의 핵심은 메모리 상의 데이터에 '직접' 접근한다는 점에 있다. 바이트단위로 쪼개진 메모리 구조 속에서 들어오는 값은 void형 열로 들어오게 되는데, 이는 곧 해당 함수들이 int형 배열이나, char 형 배열, 그 어떤 것이라도 들어와 메모리 단위로 연산이 된다는 점이 해당 함수를 작성하면서 알아야 하는 가장 중요한 포인트 이다.

  • memset : int c 형으로 들어오는 매개변수를 void * 형의 포인트 배열에 삽입시키는데, 이때 size_t 형의 len 바이트 만큼 입력시킨다.
  • bzero : memset을 진행할 때 int c가 아닌 0을 입력한다.
  • memcpy : void 형 배열 dst에 src를 size_t 만큼 입력 시킨다. 이때 오버랩이 발생할 수 있다!
  • memmove : memcpy 의 오버랩 문제를 해결한 함수이다.
  • memchr : int c 를 받아서, void *s 배열에서 c가 발견되면 해당 위치의 포인터를 반환한다. 이때, n 바이트까지만 진행한다.
  • memcmp : n 바이트까지만 배열 두개를 비교한다. 이때 다른 값이 발견 되면, 해당 값 부분에서 차이를 int 값으로 반환한다.

메모리계열의 특징 size_t

해당 함수들은 기본적으로 '직접' 접근 한다. 이는 형과 관계 없이 아주 다이렉트하게 편리하게 값을 건드릴 수 있다는 메리트가 있다.
하지만, 문제는 문자열은 마지막을 표시하는 '\0' 구분이 필요하고, 다른 형태의 배열은 그러한게 존재하지 않는다. 이때 메모리로 다이렉트로 접근한다는 점에서 그러한 구분이 유의미 하지 않다고 보는 것 같다. 애초에 함수가 형과 상관없이 작동해야 하도록 만드니 당연한 것이리라. 따라서 해당 함수의 구현과 이해에선 size_t 라는 어디까지? 를 잘 생각해야 한다.
예를 들어 아무리 문자열이 들어왔다고 치자. 하지만 제공되는 바이트 수가 제공되는 문자열의 길이보다 크다면? memchr은 당당하게 문자열을 넘어서(!), 메모리 안의 엄한 값도 찾아서 해당하는 int c값을 찾아 출력해야한다.

'unsigned char' vs 'char'

어떤 형이든 건드려야 하고, 이때 최소 단위는 당연히 1 바이트이다. 이때 바이트 해당 매개변수를 어떤 형으로 다루어야 하는가? 는 기본적으로 unsigned char 즉 부호가 없는 양수 값으로 진행하게 되어 있다.
왜 굳이 1 바이트로 가능하 char를 쓰지 않을까? 결국 사이즈가 같은게 아닌가? 라고 생각해볼 수 있을 것이다. 더불어 음수 값의 표현이 가능한게 낫다고 볼 수도 있다. 그러나 문제는 1 바이트가 갖고 있는 의미를 잘 생각해보아야 한다.
char 형의 경우 부호를 담당하는 맨 좌측의 1비트를 갖고 있으며, 음과 양을 위한 계산이 자동으로 들어가 음양을 표현하게 된다. 이러한 점에서 메모리에 들어간 값이 음수와 양수로 되어 있는 자료형은 불가피하게 메모리 안에서 변환 과정을 거치게 되는데, 이때 1바이트씩 잘라서 편집이 들어가는 함수들의 경우 원하는 값과 다른 값을 도출하거나 작동할 수 가 있다.
따라서 해당 문제를 해결하기 위해선 애초에 음수와 양수가 아닌 순수한 값 상태에서의 바이트 단위 접근이 필요하고 이렇기에 붙여 넣는 memcpy 나 memmove 는 unsigned char 형으로 특히나 해놓고 진행하는 것이다.
관련 자료 : char와 unsigned char의 차이로 인한 구분의 필요성에 대해

오버랩

memcpy의 경우 임의로 메모리 안에 할당된 변수에 접근 과정에서, 제공되는 바이트 만큼 dest에 src를 붙여 넣는다. 이때 src의 주소 값이 dst의 주소값 보다 작은 경우, 바이트 수에 따라 src가 다시 src를 참조하는 경우가 발생할 수 있다.
즉 아래 그림과 같은 경우가 생길 수 있단 것이다.

이렇게 된 경우를 보자. src에서 dst까지 바이트에 맞춰 인덱스에 맞춰 바뀔 것이다. 그런데 dst를 바꾼다고 생각했는데, 정작 src의 값도 바뀌게 된다. 즉, 원하는 값으로 바꾸길 예상했지만, dst가 바뀌면서 src도 바뀌게 되고 그 바뀐 값이 다시 dst 배열의 값을 예상과 다른 값으로 바뀌게 만드는 것이다.
이를 개선한 것이 바로 memmove이다. memmove의 경우 이러한 경우를 인지하고 '거꾸로' 접근하여 입력을 하기 때문에 src가 수정되더라도, dst의 입력 값은 예상되는 값으로 지정되게 된다.

< 번외편 >

  • atoi : 입력 받은 문자열에서 isspace에 해당하는 케이스들을 스킵하고, 음수나 양수 기호를 받은 뒤 숫자를 인식하여 정수 값으로 환산한다. 중간에 다른 문자가 있는 경우 아무 값도 나와서는 안된다.

atoi 에 대한 이야기

atoi 구현 시 신기하게도 atoi는 int형으로 바꾼다는 이야기와는 다르게 long, long long까지도 대응 가능하도록 구현이 되어 있다. 이는 내부에서 동작할 때는 strtol를 이용하며, 해당 숫자가 각 형 이상의 값일 때 다음 자료형으로 이동하여 값을 반환한다.
다행이도 libft 과제에서는 이를 요구하지는 않으나, atoi가 LONG_MAX, LONG_min 값에서는 각각 -1, 0이 나오는 것은 처리해놓는 것이 좋다. (사실 정식 atoi는 LLONG_MAXLLONG_min 값에 대응한다. 해당 매크로 사용은 norminette에는 걸리지 않으니 <limits.h> 를 활용해 예외처리를 해주면 좋다.

< 할당 >
malloc 함수는 데이터를 heap 영역에 저장하는 것으로 기존의 stack 영역에 저장하는 것보다 더 많은 데이터를 저장할 수 있다. 단점은 해당 방식을 남발하는 경우, 계속 heap영역에서 데이터가 잔류하므로 메모리 누수를 야기하게 되고 따라서 해당 기능을 사용 후에는 반드시 free를 해줘야 하며, 이를 지목하는 포인터의 경우 free 후 Null로 저장 주소값을 없에 다른 곳에서 접근하지 않도록 만드는 것이 좋다.

  • calloc : malloc을 진행하며, 특징적으로 0값을 넣어주기 때문에 나중에 쓰는 경우 매우 편리하다. 활용 잘하면 코드를 줄이는 데 큰 역할을 한다(...)
  • strdup : 들어오는 문자열 s1을 새롭게 할당하여 사본을 만들어 낸다. 해당 함수의 경우 데이터를 조작 및 기존 할당을 해제하고 다시 전달해야 할 때 종종 사용하게 된다. 단, 이후 strndup을 더 많이 쓰게 되는 것 같다...

후기...

이제 필수 파트 1 을 마무리 지었다. 해당 파트의 경우 확실히 기본이 되는 함수들인 만큼, 얼마나 명확하게 이해 했냐에 따라 다음 파트에서 더 효과적이고 효율적인 함수를 작성할 수 있었다. 더불어 개정된 것이기에 생략된 부분들이 존재하나, 향후 사용을 해야 하는 경우 추가로 더 조작하는데 유용한 함수들을 만들어 두면 좋다. 이는 보다 앞 기수의 깃허브들을 탐방하다 보면 심심찬게(ㅋㅋ) 발견할 수 있고, 해당 함수 구현도 심심풀이로 해보면 좋을 것 같다.(는 해야 한다)

profile
무한 도전. 무한 실패. 무한 성공.

0개의 댓글