동적 할당과 포인터
동적인 할당을 하는 함수들에는 아래와 같이 있습니다.
#include <stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
void free(void *ptr);
#include <alloca.h>
void *alloca(size_t size);
ISO C는 메모리를 할당을 하는 데 있어서 3개의 함수를 사용합니다.
malloc
은 특정한 바이트 수만큼의 메모리를 할당을 합니다. 메모리의 초기 값으로 아무 값이나 들어가 있을 수 있습니다.calloc
은 특정 크기의 오브젝트들의 수만큼 공간을 할당을 합니다. 이를 사용하여 공간을 할당하면 메모리의 초기 값은 0이 되게 됩니다.realloc
은 이전에 할당한 영역의 크기를 줄이거나 늘릴 수 있습니다. 새롭게 할당되는 영역의 초기 값은 아무 값이나 들어가 있을 수 있습니다.
이 이외 free
는 메모리의 해제를 지칭하고 alloca
는 일반적인 동적 할당이 힙에 할당하는 것과 달리 스택에 동적으로 할당하는 함수에 해당합니다. 후자는 스택 오버플로를 야기할 수 있기 때문에 권장되지는 않습니다.
아래와 같은 소스 코드를 작성하는 경우를 생각해봅시다.
#include "apue.h"
#include <sys/shm.h>
#define ARRAY_SIZE 40000
#define MALLOC_SIZE 100000
#define SHM_SIZE 100000
#define SHM_MODE 0600
char array[ARRAY_SIZE];
int main(void){
int shmid;
char *ptr, *shmptr;
printf("array[] from %lx to %lx\n", (unsigned long)&array[0],
(unsigned long)&array[ARRAY_SIZE]);
printf("stack around %lx\n", (unsigned long)&shmid);
if ((ptr = malloc(MALLOC_SIZE)) == NULL)
err_sys("malloc error");
printf("malloced from %lx to %lx\n", (unsigned long) ptr,
(unsigned long)ptr+MALLOC_SIZE);
// 나중에 배울 내용에 해당합니다.
if((shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0)
err_sys("shmget error");
if((shmptr = shmat(shmid, 0, 0)) == (void *)-1)
err_sys("shmat error");
printf("shared memory attached from %lx to %lx\n",
(unsigned long)shmptr, (unsigned long)shmptr+SHM_SIZE);
if (shmctl (shmid, IPC_RMID, 0) < 0)
err_sys("shmctl error");
exit(0);
}
여기서 char array[ARRAY_SIZE];
는 전역 변수로 메모리의 데이터 세그먼트Data Segment에 들어가게 됩니다. 그러한 데이터 세그먼트 중에서도 초기화되지 않은 데이터uninitialized data 영역인 bss 영역에 들어갑니다.
다음으로 ptr
은 동적 할당되는 내용으로 malloc
을 통한 동적 할당은 heap 영역에 들어가게 됩니다.
그 이외에 shmid를 통해 shmat으로 하는 부분이 있습니다. 이 부분은 뒤에서 배우시겠지만 이 부분은 공유 메모리shared memory에 할당되는 부분으로 IPCInter-Process Communication와 관련되어 있습니다.
메모리 계층을 보자면 다음과 같이 될 수 있습니다.
그리고 위의 코드에서도 알 수 있듯이 malloc
은 void *
를 반환을 합니다. 이 말은 malloc
입장에서는 이것이 어떤 포인터인지 알지 못하기에 일단 매개 변수로 주어진 크기만큼 힙 영역에서 할당을 해주고, 시작 주소를 반환해주는 것을 의미합니다. 따라서 이를 변환해 줄 필요가 있고, 일반적으로 malloc
으로 할당을 할 때에 아래와 같이 진행을 하도록 합니다.
int *ptr = (int *)malloc(sizeof(int)*20); //=> int ptr[20];과 사실상 동일한 의미
물론 (int *)
이 없어도 문제없이 할당이 되기는 합니다. 왜냐하면 컴파일러에 의해서 묵시적으로 변환이 되기 때문입니다. (int *)
을 붙여주는 이유는 순전히 명시적으로 변환됨을 표현하기 위해서 입니다.
void
포인터를 사용하면 다음과 같이 사용하는 것이 가능해지게 됩니다.
#include <stdio.h>
int main(void) {
int x = 4;
void *ptrA = &x;
int *ptrB = ptrA; // 묵시적인 형 변환이 일어납니다.
int *ptrC = (char *)ptrB; // Warning을 발생 시킵니다.
printf("%d %d\n", *ptrB, *ptrC); // 4 4가 나오게 됩니다.
return 0;
}
정확히 void
라는 것은 nothing. 즉, 형type이 없음을 나타냅니다.
이런 포인터를 사용할 때 유의사항으로는 만약에 누군가가 Read-only 위치를 쓰게 된다면 segfault가 발생하게 됩니다. 예를 들어, char *s= "Hello World"
의 경우에는 메모리상의 텍스트 세그먼트에 해당하게 됩니다. 이런 상황에서 *s = 'h'
로 하게 되면 segfault가 발생하게 됩니다.
또 다른 유의사항으로는 SIGBUS가 발생하는 경우가 있습니다. 이는 지정된 int
형의 포인터에 대해서 char
형 5 바이트를 할당을 하는 경우에 4 바이트씩 읽는 int
형 포인터에는 읽는 데 문제를 발생을 시키게 됩니다. 예를 들어 아래와 같이 작성을 하도록 하겠습니다.
/************
* 만약에 integer pointer를 1 byte 단위로 이동을 하고 싶다면
* char를 사용해야 합니다.
************/
#include <stdio.h>
#include <stdlib.h>
int main(void){
#if defined(__GNUC__)
# if defined(__i386__)
/* Enable Alignment Checking on x86 */
__asm__("pushf\norl $0x40000,(%esp)\npopf");
# elif defined(__x86_64__)
/* Enable Alignment Checking on x86_64 */
__asm__("pushf\norl $0x40000,(%rsp)\npopf");
# endif
#endif
int *iptr; // integer pointer
char *cptr; // character pointer
// malloc() always provides aligned memory << This is very important!!
// Increment the pointer by one, making it misaligned
cptr = malloc(sizeof(int) + 1);
iptr = (int*)++cptr;
// dereference it as an int pointer, causing an unaligned access
*iptr = 42;
printf("%d\n", *iptr);
return 0;
}
Alignment를 체크를 하는 이 소스를 실행을 하게 되면 SIGBUS가 발생을 하는 것을 알 수 있습니다. 이런 문제가 발생하는 이유는 메모리에서 레지스터로 값을 가져올 때에 32 비트의 컴퓨터에서는 4 바이트 단위로 가져옵니다. 위 코드에서 cptr은 malloc을 통해 0 ~ 4번지를 할당을 받습니다. 이 경우에 malloc은 0 ~ 3번지와 4 ~ 7번지를 읽어서 0 ~ 4 번지를 할당을 해주도록 합니다.
이렇게 할당된 cptr은 0번지를 먼저 가지고 있습니다. 이 경우 ++cptr을 해주변 1번지를 가지고 iptr에 의해서 4 바이트가 읽어지면서 1번지 ~ 4번지를 가져오도록 합니다. 이런 경우에 alignment가 맞지 않게 되고, SIGBUS가 발생하게 됩니다. 이런 문제도 조심을 해야 합니다.
malloc의 사용
그렇다면 본격적으로 malloc
을 사용해보도록 하겠습니다. 아래와 같은 소스 코드를 작성해주시길 바랍니다.
#include <stdio.h>
#include <stdlib.h>
int main(void){
int *student_id; // saved in garbage data
student_id = (int *)malloc(sizeof(int) * 3); // 12 bytes가 할당됩니다.
// *(student_id + 1) == student_id[1]
if (student_id == NULL) { // 메모리 할당을 실패한 경우
fprintf(stderr, "Out of memory\n");
}
student_id[0] = 1;
student_id[1] = 2;
student_id[2] = 3;
printf("%d\n", student_id[0]);
printf("%d\n", student_id[1]);
printf("%d\n", student_id[2]);
free(student_id);
return 0;
}
위의 코드는 정수형 배열을 동적으로 할당하는 코드에 해당합니다. student_id
는 정수형 포인터로 정수형 데이터에 대한 주소를 받을 수 있습니다.
이런 포인터에 대해서 정수형 3개짜리 배열을 만든다고 하면 malloc
의 매개 변수의 크기로 먼저 정수 크기인 sizeof(int)
를 써서 4 바이트임을 알리고, 3을 곱해주어서 총 12 바이트를 할당을 받게 만들어줍니다.
다음에 할당된 메모리는 void *
로 반환이 되므로 int *
형식으로 명시적으로 변환을 표기해주도록 합니다.
그리고 이렇게 만들어진 동적 배열에 대해서 접근은 student_id[1]
또는 *(student_id + 1)
과 같이 해주도록 합니다. 둘의 의미는 동치equivalent이기에 어느 것을 사용해도 괜찮습니다.
다음으로 이런 배열 할당뿐만 아니라 구조체에서도 이를 사용할 수 있습니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct student {
int student_id;
char name[20];
};
int main(void){
struct student *a_student;
a_student = (struct student*)malloc(sizeof(struct student));
a_student->student_id = 123456789;
strcpy(a_student->name, "BlaCkinkGJ");
free(a_student);
return 0;
}
거의 배열과 동일한 방식으로 할당을 할 수 있습니다. 특이사항으로는 일반적인 정적 구조체와 달리 동적 구조체의 경우에는 ->
를 사용을 해서 멤버에 접근을 해야 한다는 것입니다. 그리고 이러한 구조체를 복사를 하는 경우에는 매우 주의해야 할 것이 몇 가지가 있습니다.
- 구조체 내 포인터의 내용은 복사할 수가 없다.
- 정적인 내용을 가지는 구조체의 복사는 되도록이면
memcpy();
를 사용해서 해주시길 바랍니다.
1의 경우
먼저 1의 경우를 보도록 하겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct student {
int id;
char *name;
};
int main(void) {
struct student *src, *dst;
src = (struct student *)malloc(sizeof(struct student));
dst = (struct student *)malloc(sizeof(struct student));
src->name = (char *)malloc(sizeof(char)*20);
src->id = 123456789;
strcpy(src->name, "BlaCkinkGJ");
printf("struct location: %p\n"
"i d: %d\n"
"name: %s (location:%p)\n\n",
src, src->id, src->name, src->name);
memcpy(dst, src, sizeof(struct student));
printf("struct location: %p\n"
"i d: %d\n"
"name: %s (location:%p)\n\n",
dst, dst->id, dst->name, dst->name);
free(src->name);
free(src);
free(dst->name);
free(dst);
return 0;
}
이 코드를 실행한 결과는 아래와 같이 나오게 됩니다.
struct location: 0x55c1d22ac260
i d: 123456789
name: BlaCkinkGJ (location:0x55c1d22ac2a0)
struct location: 0x55c1d22ac280
i d: 123456789
name: BlaCkinkGJ (location:0x55c1d22ac2a0)
여기서 구조체의 위치는 다르지만 name 포인터가 가리키는 위치는 동일한 것을 알 수 있습니다. 이것은 꽤나 치명적인 문제가 되고, 이렇게 내용 전체가 복사되지 않는 것을 얕은 복사shallow copy라고 합니다. 그렇다면 만약에 src
에 대한 free
를 dst
의 결과를 출력하기 전에 수행을 하면 어떤 일이 벌어지는 지 확인해보겠습니다.
memcpy(dst, src, sizeof(struct student));
free(src->name);
free(src);
printf("struct location: %p\n"
"i d: %d\n"
"name: %s (location:%p)\n\n",
dst, dst->id, dst->name, dst->name);
위 코드와 같이 먼저 변경을 해줍니다. 그러면 결과는 아래와 같습니다.
struct location: 0x55b23e55d260
i d: 123456789
name: BlaCkinkGJ (location:0x55b23e55d2a0)
struct location: 0x55b23e55d280
i d: 123456789
name: (location:0x55b23e55d2a0)
해당 내용이 free
가 되어 아무 내용도 출력하지 않는 것을 알 수 있습니다. 이것이 지금은 간단한 문자열 출력이니 큰 문제가 없지만 좀만 복잡한 프로그램이면 충분히 큰 문제를 야기할 것입니다. 단적인 예입니다. 위의 코드를 부분적으로 수정했습니다.
printf("struct location: %p\n"
"i d: %d\n"
"name: %s (location:%p)\n\n",
dst, dst->id, dst->name, dst->name);
if(!strcmp(dst->name, "BlaCkinkGJ"))
fputs("I found!\n", stdout);
else
fputs("I can't find...\n", stdout);
free(src->name);
free(src);
이 경우 결과는 “I found!”가 나옵니다. 하지만 만약에 free
의 위치를 아래와 같이 변경을 한 경우를 생각해보겠습니다.
printf("struct location: %p\n"
"i d: %d\n"
"name: %s (location:%p)\n\n",
dst, dst->id, dst->name, dst->name);
free(src->name);
free(src);
if(!strcmp(dst->name, "BlaCkinkGJ"))
fputs("I found!\n", stdout);
else
fputs("I can't find...\n", stdout);
이 경우 결과로 “I can’t find…“가 나옵니다. 결과적으로, 우리는 “I found!”를 기대했지만 실질적으로 다른 것이 나오는 논리적 오류를 나타나게 됩니다. 따라서 이런 copy를 수행을 할 때에는 깊은 복사deep copy를 수행해서 위와 같은 문제를 해결하도록 해야 합니다. 즉, 다음과 같이 코드를 작성하도록 해야 합니다.
printf("struct location: %p\n"
"i d: %d\n"
"name: %s (location:%p)\n\n",
src, src->id, src->name, src->name);
dst->id = src->id;
dst->name = (char *)malloc(sizeof(char)*20);
strcpy(dst->name, src->name);
printf("struct location: %p\n"
"i d: %d\n"
"name: %s (location:%p)\n\n",
dst, dst->id, dst->name, dst->name);
조금 기존보다는 더러워졌지만 이렇게 한 후에 결과를 위와 같이 free(src); free(src->name)
을 수행하고 결과를 돌리면 아래와 같이 나옵니다.
struct location: 0x557f16f3c260
i d: 123456789
name: BlaCkinkGJ (location:0x557f16f3c2a0)
struct location: 0x557f16f3c280
i d: 123456789
name: BlaCkinkGJ (location:0x557f16f3c6d0)
I found!
결과적으로, 논리적 문제를 해결할 수 있습니다.
2의 경우
이는 구조체 내부에 정적인 내용만 있는 경우에 해당합니다. 즉, 아래와 같은 구조체인 경우에 해당합니다.
struct student {
int id;
char name[20];
};
이런 경우에는 memcpy
를 쓰는 게 =
보다 안전합니다. 예를 들어, 아래와 같은 경우를 보도록 하겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct student {
int id;
char name[20];
};
int main(void) {
struct student src = {
.id = 123456789,
.name = "Hello World!"
};
struct student dst;
dst = src;
printf("struct location: %p\n"
"i d: %d\n"
"name: %s (location:%p)\n\n",
&src, src.id, src.name, &src.name[0]);
printf("struct location: %p\n"
"i d: %d\n"
"name: %s (location:%p)\n\n",
&dst, dst.id, dst.name, &dst.name[0]);
return 0;
}
이 경우에는 문제없습니다. 결과는 아래와 같이 나옵니다.
struct location: 0x7ffc44095e50
i d: 123456789
name: Hello World! (location:0x7ffc44095e54)
struct location: 0x7ffc44095e70
i d: 123456789
name: Hello World! (location:0x7ffc44095e74)
여기까지는 좋습니다. 하지만 만약에 이런 경우에는 어떻게 될까요?
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct student {
int id;
char name[20];
};
int main(void) {
struct student *src = (struct student*)malloc(sizeof(struct student));
struct student *dst = (struct student*)malloc(sizeof(struct student));
src->id = 123456789;
strcpy(src->name, "Hello World!");
dst = src;
printf("struct location: %p\n"
"i d: %d\n"
"name: %s (location:%p)\n\n",
src, src->id, src->name, src->name);
printf("struct location: %p\n"
"i d: %d\n"
"name: %s (location:%p)\n\n",
dst, dst->id, dst->name, dst->name);
free(src);
free(dst);
return 0;
}
우리는 아무 생각 없이 src
를 dst
에 대입했지만 결과는 처참하게 나옵니다.
struct location: 0x55cc7fa04260
i d: 123456789
name: Hello World! (location:0x55cc7fa04264)
struct location: 0x55cc7fa04260
i d: 123456789
name: Hello World! (location:0x55cc7fa04264)
동일한 구조체를 가리키게 됩니다. 이런 경우라면 우리는 dst
를 고침에도 불구하고 src
의 내용까지 변경되는 문제가 발생할 수 있습니다. 따라서 이를 해결하려면 *dst = *src
라고 하면 됩니다. 하지만 이들에서 알 수 있지만 이런 방식은 너무 많은 것을 감추게 되기에 사용자로 하여금 혼란을 야기합니다.
따라서 정적인 구조체에 대해서는 memcpy(&dst, &src, sizeof(struct student));
라고 하여 할당하고, 동적인 구조체에 대해서는 memcpy(dst, src, sizeof(struct student));
로 할당해주도록 합니다. 이러면 좀 더 명시적으로 어떤 복사가 일어나고 그 크기는 어느 정도인지를 알 수 있게 됩니다.