malloc

malloc으로 다차원 배열 할당

malloc

가끔 동적할당에 대한 질문을 받으면 별 이상한 코드를 다 보게 된다.
아래 코드를 보자

#include<stdio.h>
#include<malloc.h>
void make_answer();
int main(void)
{
	make_answer();
	return 0;
}
void make_answer()
{
	int n, *list;
	scanf("%d", &n);
	list = (int *)malloc(sizeof(int) * n);
	free(list);
}

ㅇㅇ 1차원 배열 동적할당 받을때 이렇게 짜면 된다.

근데 2차원은요? 3차원은요?? 이런 질문이 오면서 별 이상한 코드가 나온다 예를 들면

#include<stdio.h>
#include<malloc.h>
void make_answer();
int main(void)
{
	make_answer();
	return 0;
}
void make_answer()
{
	int h, w, *list, i;
	scanf("%d %d", &h, &w);
	list = (int *)malloc(sizeof(int) * h);
	for (i = 0; i < h; i++)
	{
		list[i] = (int *)malloc(sizeof(int) * w);
	}
	free(list);
}

근데 난 이정도만 되도 참 기특하다. ㄹㅇ for문이라도 쓸 생각을 하게 진짜 다행이다.

근데 이건 틀렸으니 아래 코드를 보자

#include<stdio.h>
#include<malloc.h>
void make_answer();
int main(void)
{
	make_answer();
	return 0;
}
void make_answer()
{
	int h, w, **list, i;
	scanf("%d %d", &h, &w);
	list = (int **)malloc(sizeof(int *) * h);
	for (i = 0; i < h; i++)
	{
		list[i] = (int *)malloc(sizeof(int) * w);
	}
	for (i = 0; i < h; i++)
	{
		free(list[i]);
	}
	free(list);
}

솔직히 포인터 변수가 뭔지 알면 이런 질문도 안했을꺼다.
그럼 우린 쉽게 생각하자 int할당을 위해서는 int* 가 필요하고 int* 할당을 위해서는 int**가 필요하다는 걸로
그리고 1차원 이 필요하면 * 2차원이 필요하면 ** 이런식으로 외우자

그리고 할당, 해제에도 포문 돌리는거 까먹지 말고

Q. 그럼 넌 문제 풀때 맨달 동적할당함?

A. 한 700문제 풀때까지는 동적할당함 ㅋ 근데 이젠 안함


내용 추가 @Optatum, 2019-10-30, 16:44

2차원 배열을 동적할당 받고 싶을 때 이렇게 하는 방법도 있다.

int **p; // h행 w열로 선언할거임

p = malloc(sizeof(int*) * h);
p[0] = malloc(sizeof(int) * h * w);
for (int i = 1; i < h; ++i) p[i] = p[i - 1] + w;

이 방법이 생소한 사람도 있고, 밥먹듯이 이렇게 짜와서 너무 당연한 사람도 있을텐데 뭐 후자는 이미 알고 있으니까 냅두고 전자의 케이스를 위해 설명해보자.

p에 int*형으로 h만큼 할당해준 것까진 위에서 본 내용과 동일하다. 근데 그 밑줄엔 p[0]에 h*w만큼 int를 할당하고 있다. 이게 뭐하는 짓인데?

침착해라. 어차피 2차원 배열이든 5차원 배열이든 메모리 상에 올라가면 그냥 1차원 연속된 메모리 덩어리일 뿐이다. 그래서 2차원 배열임에도 일단 연속된 h*w 크기의 int를 할당해 주었다. 뭐 여기까진 납득은 된다. 근데 이렇게 쓰면 p[3][7] 이렇게 못 쓸텐데?

그래서 이 점을 해결해주기 위해 for문을 돌린다. 본래의 2차원 배열이라면 각 행에 w개의 int가 있다고 볼 수 있을테니 p[1]에 p[0]에 w를 더한 만큼의 주소를 넣어준다. p[2]에는 역시 p[1]에 w를 더한 만큼의 주소를 넣어준다. 이렇게 p[h-1]까지 돌리면 연결 작업은 끝난다.

굳이 이렇게까지 짜는 이유가 뭐야?

크게 2가지의 이점이 생긴다.

1. 말했듯이, for문으로 malloc하면 모두 메모리상에 뿔뿔이 흩어진 상태지만 이렇게 하면 메모리가 연속됨
2. 할당을 해제할 때 for문을 돌며 free를 안 해줘도 됨

1번은 설명 안 해도 이해했을거라 믿는다. 2번의 경우는 꽤 tricky한데, 그럼 위 코드를 기준으로 free하려면 어떻게 해야할까? 다음의 코드를 보자.

free(p[0]);  // h*w 크기의 int 할당 해제
free(p);     // h 크기의 int* 할당 해제

여기서 두 코드의 순서가 뒤바뀌면 큰일난다. p를 먼저 해제해버리면 p[0]는... 더 이상 유효한 포인터 주소가 아니게 되어버렷...! (이를 유식하게 dangling pointer가 되었다고 한다)

와, for문을 돌지 않고도 free가 가능하다니 정말 놀랍지 않은가?

참고로 이렇게 하기 너무 복잡해서 전 잘 모르겠어요! 하는 사람은 다음과 같은 방법으로 코딩해도 무방하다.

int *p = malloc(sizeof(int) * h * w);
p[a * w + b]; // p[a][b]와 동일

인덱스 부분이 좀 가독성이 떨어지므로 실제 계산 부분은 함수로 빼줄 수도 있다. 3차원, 4차원 이상이 되면(쓸 일이 꽤 드물겠지만) 굉장히 지저분해지므로 차라리 함수화를 추천한다.

그리고 위의 malloc 코드에서 malloc의 반환값을 int**나 int* 등으로 형변환하지 않았음을 볼 수 있다. 이 글 작성자는 malloc 쓸줄도 모르냐? 하고 생각할 수도 있겠다.

하지만 C에서 void*형은 대입되는 포인터의 자료형에 맞게 적절히 casting 된다는게 C언어 표준문서에서 보장해주고 있다. 굳이 형변환을 써 줄 필요가 없다는 말. 누군가는 'C++에서 저렇게 짜면 컴파일 에러인데? 머리에 총 맞기 싫으면 형변환해라' 라고 태클을 걸지 모르겠다. 근데 C++에서 malloc을 쓰려고 하는 니가 머리에 총맞은게 아닐까 의심이 된다.

한 가지 더 써볼까. 만약 위의 int 2차원 배열 동적할당 코드에서 갑자기 double 2차원 배열로 바꿀 필요가 생겼다. 에구머니나! 그럼 어떻게 해야하는가?

포인터 변수 선언부의 int**자료형을 double**로 바꾸고, sizeof(int*)를 sizeof(double*)로 바꾸고, sizeof(int)를 sizeof(double)로 바꾸고... 아주 번거롭다(만약 malloc의 반환값을 형변환까지 했다면 거기도 바꿔야한다. 그러니까 쓰지 말라면 좀 쓰지 마라). 유지보수 용이하게 코딩하려면 다음과 하는게 좋다.

int **p;

p = malloc(sizeof(*p) * h);
p[0] = malloc(sizeof(**p) * h * w);
for (int i = 1; i < h; ++i) p[i] = p[i - 1] + w;

이렇게 코딩했다면 선언부의 int**를 double**로 바꾸면 그만이다.

int **p = malloc(sizeof(*p) * h);

참고로 선언하면서 바로 p로 접근해도 오류가 없다. 어차피 sizeof의 피연산자는 컴파일 타임에 타입만 추론할 수 있으면 되기 때문이다. p를 앞에서 int**로 선언했기 때문에 p는 당연히 int* 타입이 된다. 그러니까 이 방법을 애용하자.

Reference