메모리 동적할당우리가 프로그램을 만들어보면서 이러한 문제에 봉착한 적이 있을 것이다. Show
맞다. 우리는 지금까지 배열을 정의할 때 배열의 크기를 컴파일 시간에 확정해야 했다. 즉, 컴파일러가 배열의 크기를 추측할 필요 없이 명확하게 나타나 있어야 했다. 이는 참으로 고역스러운 일이 아닐 수 없었다. 예를 들어 우리가 컴퓨터로 학생들의 수학 점수를 입력 받아 평균을 내는 프로그램을 만든다고 생각해보자. 반마다 학생의 수가 다르기 때문에 우리는 배열의 크기를 명확하게 정할 수 없다. 따라서 배열의 크기를 '충분히 크게' 잡게 되는게 그러면 메모리가 낭비가 된다. 컴퓨터에서 낭비가 된다는 말은 비효율적인 프로그램이라고 말할 수 밖에 없다. 이렇게 쓸데 없는 낭비되는 자원을 막기 위해서 '학생의 수'를 입력 받고 학생의 수 만큼 배열의 크기를 지정하면 얼마나 좋을까? 하지만 놀랍게 그렇게 할 수 있는 방법이 있다. 바로 동적 메모리 할당 이라는 방법이다. 말 그대로 메모리를 '동적'으로 할당한다는 말이다. 동적이라는 말은 딱 정해진 것이 아니라 가변적으로 변할 수 있다는 말이다. 또한 메모리를 '할당'한다는 말은 우리가 배열을 정의하면 배열에 맞는 메모리 상의 특정한 공간에서 배열이 나타내는 것 처럼 메모리의 특정한 부분을 사용할 수 있게 된다. 참고로 할당 되지 않는 메모리는 절대로 사용할 수 없다.
실행 결과
일단 위 예제부터 살펴보자.
먼저 위 과정을 통해서 우리가 원하는
바로 이녀석이 우리가 원하는 일을 해주는 녀석이다. 이 함수의 이름은 이 함수는 이 함수는 인자로 전달될 크기의 바이트 수 만큼 메모리 공간을 만든다. 즉, 메모리 공간을 할당하게 되는 것이다. 우리가 원소의 개수가 이 함수가 리턴하는 것은 자신이 할당한 메모리의 시작 주소를 리턴한다. 이 때, 리턴형이 free , 메모리 누수따라서
그리고 마지막에 이렇게 malloc 은 어디에 할당할까?우리는 이전에 메모리 구조에 대해서 배울 때 메모리에는 다음과 같은 구조들이 있다고 하였다. 이 때 힙(heap) 부분은 나중에 다룬다고 하였는데 지금 알아보자. 스택이나, 데이터 영역, Read Only Data 부분은 당연하게도 하지만 힙은 경우는 다르다. 메모리의 힙 부분은 사용자가 자유롭게 할당하거나 해제할 수 있다. 따라서 힙의 할당과 해제가 자유로운 만큼 제대로 사용해야 한다. 만일 힙에 할당하였는데 해제를 하지 않았다면 공간이 낭비 될 것이다. 다른 메모리 부분의 경우는 컴퓨터가 알아서 처리하기 때문에 문제가 발생할 여지가 적지만 힙은 인간이 다루는 만큼 철저히 해야 한다. 예제 1다음은 동적 할당을 활용한 예제이다.
실행 결과
위 과정을 통해서 우리는 배열의 개수가 2 차원 배열의 동적 할당그렇다면 좀 더 높은 난이도의 문제를 도전해보자. 2 차원 배열을 동적으로 할당할 수 있을까? 물론 가능하다. 2 차원 배열을 동적 할당 하는 방법으로는 크게 두 가지 방법을 생각할 수 있다.
먼저 전자의 방법을 이용해보자. 포인터 배열을 이용하여 2 차원 배열 할당하기포인터 배열은 이전에 말했듯이 각각의 원소들이 모두 포인터인 배열이다. 따라서, 각각의 원소들이 다른 일차원 배열들을 가리킬 수 있다. 따라서 우리가 해야할 일은 먼저 포인터 배열을 동적으로 할당한 뒤에 다시 포인터 배열의 각각의 원소들이 가리키는 일차원 배열을 다시 동적으로 할당해주면 마치 2 차원 배열을 만든 것 같은 효과를 낼 수 있다.
실행 결과
잘 실행된 것 같은데 한 번 하나씩 살펴보자.
일단 이 부분을 보자. 우리가 만약에
따라서 위와 같이 그러면 배열의 각각의 원소는 int * 형 이므로 다른
int 형 배열을 가리키기를 갈망하고 있을 것이다. 그러면 우리가 그 욕구를 해결주자. 각각의 원소들에 대해 원하는 메모리 공간을 짝 지어 주자.
각각의 원소들에 대해 메모리 공간을 할당해주었다. 따라서 arr 의 하나의 원소가 크기가 y 인 배열을 가리키고 있다. 그런데 arr 의 원소가 x 개 이므로 전체적으로 x * y 배열을 가진 셈이다. 하지만 이렇게 만들어진 배열은 정확하게 말하자면 2 차원 배열이라고 말하기 어렵다. 왜냐하면 2 차원 배열은 메모리에서 연속적으로 있어야 하기 때문이다.
예를 들자면와 같이 말이다. 하지만 우리가 만든 배열은 메모리에서 연속으로 존재한다고 보장할 수 없다. 또한 한가지 재미있는 점은 우리가 만든 '2 차원 배열 처럼 생긴' 포인터 배열은 2 차원 배열과 달리 함수의 인자로 쉽게 넘겨줄 수 있다. 예를 들면
이렇게 말이다. 이러면 그렇다고 해서 2차원 배열의 성질을 잃어버리는 것은 아니다. 이 배열도 2차원 배열처럼 아무튼 이렇게 2 차원 배열을 생성하였다(물론 정확히는 2차원 배열이 아니지만) 우리가 이 배열을 힙에 할당했으면 사용이 끝나면 역시 돌려줘야 한다. 해제하는 순서는 할당하는 순서와 반대로 하면 된다. 예제
실행 결과
이렇게 잘 나온다. 위 예제에서 우리는 2 차원 배열을 사용하였다. 즉,
를 사용하였다. 이를 이용하여 각각 값을 대입하였고 평균을 내는 함수를 만들어서 평균도 출력하였다. 그리고 마지막으로 할당 받은 메모리의 사용이 끝났기 때문에 진짜 2차원 배열주의 사항 애석하게도 이 방법은 비주얼 스튜디오에서는 작동하지 않습니다. 비주얼 스튜디오의 C 컴파일러가 VLA 를 지원하지 않기 때문입니다. 하지만 GCC 나 Clang 같은 컴파일러에서는 사용 가능한 방법입니다. 참고로 VLA 는 C99 에 표준으로 들어갔습니다만, 비주얼 스튜디오의 C 컴파일러는 C90 을 사용하고 있습니다. 앞서 사용했었던 방법으로는 진짜 2 차원 배열처럼 메모리상에 쭈르륵 존재하는 배열을 만들 수 없었다. 왜냐하면 1 차원 배열이 쭈르륵 할당 되면서 메모리상에 여기저기 퍼져서 만들어질 것이기 때문이다. 따라서 메모리상에서 연속적으로 존재하는 진짜 2 차원 배열을 만들기 위해서는 반드시
와 같은 배열을 할당하고자 하면 메모리의 크기는
가 될 것이다. 이렇게 메모리가 할당 되었다면 이제 해당 메모리를 2 차원 배열 처럼 생각해라! 라고 컴파일러에게 알려줘야 한다. 이 경우 메모리 주소값을 2 차원 배열을 가리키는 포인터로 전달해주면 된다. 이전에 2 차원 배열을 만들 때 배웠듯이 2 차원 배열 포인터의 경우 포인터 연산을 처리하기 위해선 반드시 포인터 타입 안에 행 길이 가 들어가야 한다고 했다. 따라서 아래와 같이 2 차원 배열 포인터
이제 컴파일러는"
예를 들어서 위와 같이
주의 사항
예제
실행 결과
와 같이 잘 나온다.
한 가지 주의해야할 점은 위 배열 포인터를 함수의 인자로 전달할 때 이다. 예를 들어서 2 차원 배열의 모든 원소를 출력하는 함수를 만든다고 생각해보자. 그렇다면 아래와 같이 함수를 만들 수 있을 것이다.
그런데 문제가 있다. 컴파일러가 컴파일 오류
해결책은 간단하다. 컴파일러가
그러면 컴파일시 잘 작동하는 것을 볼 수 있다. 그래서 어떤 방법을 사용해야할까?되도록 메모리상에서 연속된 공간에 2 차원 배열을 할당하는 후자의 방법을 사용하는 것이 좋다. 이유는 다음과 같다.
구조체 동적 할당
실행 결과
이번에는 구조체를 사용해보았다. 구조체에 대해서는 앞서 얘기 했듯이 전혀 특별하게 생각할 필요가 없다. 그냥 '하나의 타입' 으로 생각하면 된다.
일단 1 차원 구조체 배열을 가리키기 위해서
그리고 물론 위 경우는 조금 특별하지만 예를 들어 구조체의 크기가 10 바이트일 경우 컴퓨터가 더블워드 경계(double word boundary) 에 놓음으로 속도를 향상시키는 경우가 있는데 이 경우 구조체의 크기는 12 바이트로 간주될 수 있다. 사실 자세한 내용은 여기서 생략하기로 하고 아무튼 기억해야 할 점은 언제나
위와 같이
그리고 마지막으로 위와 같이 노드우리는 지금까지 여러가지 자료형에 대해서 배웠다. 변수를 무식하게 나열하는 것을 막기 위해서 배열을 배웠고 배열의 한계점을 느껴 구조체를 배웠다. 또한 구조체 한 개, 한 개를 다루는데 한계를 느껴 구조체 배열을 이용하였고 결국 배열로 다시 돌아왔다. 동적할당을 함으로써 사용자가 원하는 만큼의 크기의 입력을 다룰 수 있게 되었다. 하지만 아직 문제점을 느끼고 있다. 만일 사용자가 마음이 변해서 한 개를 더 입력받고 싶다면 어떨까? 새롭게 동적할당을 하면 되지만 예컨대 1000000000개의 데이터가 있는데 1 개의 추가적인 데이터를 위해서 1000000001개를 위한 공간을 새롭게 잡으면 너무 아깝다. 이를
해결하기 위한 것이 '노드' 이다. 아무튼 이렇게 생겼다. 이를 코드로 나타내면 다음과 같다.
이렇게 생긴 노드는 어떻게 사용할까? 위와 같이 사용한다, 첫 번째 노드는 다음 노드를 가리키고 다음 노드는 그 다음 노드를 가리키고 마지막 노드는 아무것도 가리키지 않는다. 또한 각각의 노드는 데이터를 하나 씩 가지고 있다. 다시말해서 나중에 데이터 한 개를 더 추가하려면 마지막 노드에서 새 노드를 만들어 이어주기만 하면 된다. 뿐만 아니라 기존의 배열에서는 거의 불가능 했던 작업인 '배열 중간에 새 원소 집어 넣기' 가 가능하다. 다시 말해 기존에 노드 사이에 새로운 노드를 끼워 넣을 수 있다. 위와 같이 기존에 있었던 연결을 끊어주고 그 사이에 새롭게 연결해주면 된다. 이러한 사실을 바탕으로 노드를 만들어보자. 가장 먼저 새로운 노드를 만들기 위해 데이터와 이 노드가 가리키는 다음 노드가 필요한데 이 함수는 단순히 첫번째 노드를 만드는 역할을 한다고 하고
따라서 사실 이 함수는 노드를 생성만 할 뿐 노드를 어떻게 관계짓지는 못한다. 따라서 어떠한 노드 뒤에 새로운 노드를 생성하는 함수를 만들어야 한다. 이 함수를
위 함수는 이 그림을 보면서 생각해보자.
일단 새로운 노드
그리고
그리고 이렇게 노드를 잘 만들어주었다면 이제는 노드를 파괴하는 함수를 만들어보자. 이러한 함수를 만들기 위해선 우리가 삭제할 노드를 가리키고 있는 이전 노드를 찾아야 한다. 이러한 노드를 찾기 위해선 맨 처음 노드부터 뒤져나가야 하는데 맨 처음 노드를 헤드라고 하며 우리의
이와 같이 만들면 된다.
여기서 잘 나타나 있다. 만일 이 과정을 하나의 소스 코드로 나타내면 다음과 같다.
실행 결과
일단 추가적으로
그리고 메인함수에 위와 같이 노드를 정의했다. 첫 헤드는 이렇게 노드를 만들어보았다. 노드는 배열과 달리 삽입/삭제/추가 등이 월등히 편리하다. 그렇다고 노드가 배열보다 월등히 좋다고 할 수 있을까? 꼭 그렇다고 말할 수는 없다. 왜냐하면 배열의 경우 3 번째
원소를 찾기 위해서 단순히 메모리 관련 함수이 단원이 메모리에 관한 것인 만큼 메모리에 대해서 빠삭하게 알아보자. C 표준 라이브러리에서 지원되는 메모리 관련 함수들에 대해서 알아보자. 대표적으로 memcpy
실행 결과
이
"
마찬가지로 memmove
실행 결과
memcmp이 함수는 예상할 수 있듯이 두 개의 메모리 공간을 비교하는 함수이다.
실행 결과
|