16. 메모리 동적 할당
1 동적 할당 함수(malloc, free)
지금까지 프로그램에서 필요한 기억공간을 확보하는 방법은 변수나 배열을 선언하는 것이었다. 이 방법은 프로그램을 작성하는 단계에서 이미 필요한 기억공간을 예상할 수 있어야 한다. 예를 들어, 최대 100명의 나이를 입력 받아서 평균 나이를 계산한다면 배열요소가 100개인 int형 배열을 선언할 것이다. 이와 같이 프로그램을 작성하는 단계에서 필요한 기억공간의 크기를 결정하는 것을 메모리의 정적 할당이라고 한다.
반면에 프로그램을 실행하는 도중에 입력되는 데이터에 따라서 필요한 기억공간을 확보해야 하는 경우도 있다. 예를 들어, 세 명의 인사말을 저장할 2차원의 문자배열을 선언한다고 하자. 이때 한 행의 길이는 각 인사말 중에서 가장 긴 인사말을 저장할 수 있을 만큼 충분해야 할 것이다.
|
char greetings[3][20]; // 가장 긴 인사말이 20바이트라고 할 때 배열 선언 |
그러나 실제로 실행 중에 입력된 인사말이 짧다면 사용된 기억공간보다 낭비되는 기억공간이 더 많게 될 것이다. 이 문제는 처리할 데이터의 수가 많을수록 심각해질 것이다.
|
greetings 배열 H i \0 ← 낭비되는 기억공간 L e t m e i n t r o d u c e . . . \0 H e l l o \0 ← 낭비되는 기억공간 |
따라서 메모리의 낭비를 최소화하기 위해서는 프로그램의 실행 중에 입력되는 데이터에 맞게 기억공간을 할당할 필요가 있다. 이와 같이 기억공간을 확보하는 것을 메모리의 동적 할당이라고 한다.
메모리를 동적으로 할당하기 위해서는 함수를 호출해야 한다. 메모리를 동적으로 할당해 주는 함수는 용도에 따라 여러 가지가 있지만 가장 기본적으로 사용되는 것은 malloc 함수이다. 이 함수를 사용하기 위해서는 "stdlib.h" 헤더파일을 포함해야 한다.
malloc 함수의 원형은 다음과 같다.
|
void *malloc(unsigned int); // memory allocation |
이함수는 전달인자로 원하는 크기의 바이트 수를 주고 호출한다. 이 값은 항상 양수이므로 매개변수의 형태가 unsigned int형이 되었을 것이다. 호출된 함수는 메모리에 전달인자로 받은 바이트 크기만큼 연속된 기억공간을 확보한 후에 그 시작 주소값을 리턴해 준다. 이때 malloc 함수는 확보된 기억공간이 어떤 용도로 사용될지 알 수 없으므로 일단 형태가 없는 (void *)형으로 리턴하게 된다. 우리는 이 포인터를 원하는 형태로 형변환하여 용도에 맞게 사용하는 것이다. 물론 변환된 포인터는 적절한 포인터변수에 저장해야 할 것이다.
예를 들어 int형 변수로 사용할 기억공간이 필요하다면, 먼저 4바이트의 기억공간을 동적으로 할당 받는다. 그리고 리턴되는 포인터를 명시적으로 형변환하여 할당 받은 기억공간을 int형 변수로 만들어 준다. 마지막으로 변환된 포인터를 int형 포인터변수에 저장하면 포인터변수를 사용하여 할당 받은 기억공간을 int형 변수로 참조할 수 있는 것이다.
|
int *ip; // 할당 받은 기억공간을 가리킬 포인터변수 원하는 바이트 수를 주고 호출 ↓ ip = (int *) malloc(4); ③ ② ① ① : 기억공간을 할당하고 void 포인터를 리턴한다. ② : int형 변수로 활용하기 위해서 형변환 한다. ③ : int형을 가리키는 포인터변수에 저장한다. 이제 포인터변수로 가리키는 기억공간을 참조하여 값을 저장하거나 출력할 수 있을 것이다. *ip=10; // ip가 가리키는 기억공간에 10을 저장한다. printf("%d\n", *ip); // ip가 가리키는 기억공간의 값을 출력한다. |
정수값과 실수값을 저장할 두 개의 변수를 동적으로 할당 받아서 사용하는 코드를 작성해 보자.
|
1. #include <stdio.h> 2. #include <stdlib.h> // malloc 함수를 사용하기 위한 헤더파일 포함 3. 4. int main() 5. { 6. int *ip; // int형을 가리킬 포인터변수 7. double *dp; // double형을 가리킬 포인터변수 8. 9. ip=(int *)malloc(sizeof(int)); // 기억공간을 동적으로 할당받아서 각 포인터변수에 연결한다. 10. dp=(double *)malloc(sizeof(double)); 11. 12. *ip=10; // 포인터변수로 각각 할당받은 기억공간을 참조하여 값을 저장한다. 13. *dp=3.4; 14. 15. printf("정수형으로 사용 : %d\n", *ip); // 포인터변수로 저장된 값을 출력 16. printf("실수형으로 사용 : %lf\n", *dp); 17. return 0; 18.} |
|
출력 결과 정수형으로 사용 : 10 실수형으로 사용 : 3.400000 |
2번줄은 malloc 함수를 사용하기 위해 stdlib.h 헤더파일을 포함하는 문장이다. 헤더파일은 컴파일러에 따라서 약간 차이가 있을 수 있다. 예를 들어, 도스용 TC컴파일러는 alloc.h 헤더파일을 사용할 수 있고, 유닉스 컴파일러에서는 별도의 헤더파일 없이 컴파일이 가능한 경우도 있다. 그러나 공통적으로 stdlib.h 헤더파일을 포함하면 모두 사용 가능하므로 stdlib.h 헤더파일을 포함하도록 하자.
3번줄과 10번 줄이 메모리를 동적으로 할당받는 문장이다. int형 변수로 사용하기 위해서는 4바이트를, double형 변수로 사용하기 위해서는 8바이트를 할당받아야 한다. 여기서 직접 바이트 수를 전달인자로 주기 보다 각 자료형에 대한 크기를 계산하여 넣어주면 호환성 있는 프로그램이 될 것이다. 나중에 int형 변수의 크기가 8바이트로 바뀐다 해도 프로그램은 수정할 필요가 없을 것이다.
하나의 프로그램은 실행될 때 메모리의 일정한 영역을 사용하게 된다. 이 영역은 다시 몇 개의 영역으로 나뉘어서 관리되는데 이를 기억부류(storage class)라 고 하는 것이다. 구체적인 구분은 시스템에 따라 다르겠지만 주로 프로그램이 올라가는 코드 영역과 데이터가 저장되는 데이터 영역으로 나누어 진다. 데이터 영역은 자동변수들이 할당되는 스택(stack) 영역이 있고, 동적 할당되는 기억공간은 힙(heap) 영역을 사용하게 된다. 그 외에 외부 변수나 정적 변수를 위한 데이터 영역이 있을 것이다.
|
코드 영역 → 실행파일을 위한 영역 스택(stack) → 자동변수 영역 기타 데이터 영역 → 외부 변수, 정적 변수 영역 힙(heap) → 동적 할당 메모리 영역 |
힙에서 할당받은 기억공간은 자동변수와 마찬가지로 쓰레기값이 존재하게 된다. 그러나 생존기간은 자동변수와 달리 한번 할당 받은 기억공간은 프로그램이 종료될 때까지 사용할 수 있다. 따라서 특정 함수에 구속받지 않고 그 위치값만 알면 어디서나 참조하여 사용할 수 있는 것이다.
동적 할당이 갖는 이러한 특징들 때문에 할당된 기억공간을 효율적으로 사용하기 위해서는 세심한 주의가 필요하다. 자동변수의 경우는 함수가 리턴되면서 자동으로 기억공간이 회수되어 재활용되지만 동적 할당 된 기억공간은 사용이 끝나면 직접 반환해야 하기 때문이다.
동적 할당된 기억공간의 반환은 역시 함수를 호출하여 수행한다. 함수의 원형은 다음과 같다.
|
void free(void *); // 동적할당된 기억공간을 반환한다. |
이함수는 반환될 기억공간의 포인터를 전달인자로 받는다. 반환될 기억공간이 어떤 형태로 사용되었더라도 모두 반환이 가능해야 하므로 매개변수는 void 포인터변수가 되어야 할 것이다.
free 함수를 사용하여 함수 내에서 동적으로 할당받은 기억공간을 반환하는 코드를 만들어 보자.
|
void func() { int a=10; // 함수 내에서 선언된 자동변수 int *ip; // 할당받은 기억공간을 가리킬 포인터변수
ip=(int *)malloc(sizeof(int)); // 기억공간을 할당 받고 포인터변수에 연결 *ip=20; // 할당받은 기억공간에 20 저장 printf("%d\n", a + *ip); // 두 기억공간의 값을 더하여 출력 free(ip); // 할당받은 기억공간을 반환한다. } |
func 함수에서 사용된 기억공간은 세 개이다. 두 개는 자동변수인 a와 ip이고 하나는 동적으로 할당받은 기억공간이다. 이들 기억공간은 func 함수가 리턴되면 더 이상 사용되지 않을 것이므로 리턴될 때 회수하여 재활용할 필요가 있다. 다행이 자동변수인 a와 ip는 함수가 리턴될 때 자동으로 회수되지만 동적으로 할당 받은 기억공간은 함수가 리턴된 후에도 기억공간이 존재하므로 리턴되기 전에 free 함수로 직접 반환해야 할 것이다.
하나의 프로그램에서 동적으로 할당받은 기억공간은 프로그램이 종료될 때 운영체제에 의해서 자동으로 회수되어 다른 프로그램이 실행될 때 재활용 된다. 따라서 메인함수가 끝날 때는 굳이 반환할 필요가 없지만 그 외 다른 함수에서 사용하던 기억공간은 불필요한 경우 반납하여 새로운 동적 할당에 재활용 되도록 해야 할 것이다.
동적으로 메모리를 사용할 때는 반환문제 외에도 한 가지 더 주의할 것이 있다. 메모리 할당 함수는 힙 영역에 원하는 크기의 메모리가 존재하지 않으면 0번지의 포인터를 리턴하게 되는데, 이 포인터를 널 포인터(null pointer)라고 한다. 널 포인터는 포인터의 특별한 상태를 나타내기 위해 사용되며 참조할 수 없는 포인터이다. 따라서 malloc 함수가 리턴할 널 포인터를 받아서 참조하게 되면 프로그램은 실행중에 오류메시지를 표시하며 중단될 것이다.
이것은 프로그램이 실행될 때 메모리의 상태에 따라 달라지므로 더욱 심각한 문제를 일으킬 수 있다. 즉, 평소에는 잘 실행되던 프로그램이 어느날 갑자기 메모리가 부족하여 문제를 일으킬 수 있다는 것이다. 따라서 malloc 함수를 호출한 후에는 그 리턴값을 검사하여 널 포인터인지를 확인하는 부분이 반드시 필요한 것이다.
|
ip=(int *)malloc(sizeof(int)); if(ip==0) { printf("메모리가 부족합니다.\n"); } else { *ip=10; printf("%d\n", *ip); } |
malloc 함수가 널 포인터를 리턴하는 것이 단순히 메모리의 부족 때문만은 아니다. 메모리에는 기억공간이 넉넉히 남아 있더라도 원하는 크기의 기억공간이 없으면 널 포인터를 리턴할 수 있다.
힙영역의 메모리의 사용과 반환이 불규칙적이기 때문에 사용 가능한 영역이 조각나서 흩어져 있을 수 있다. 이때 많은 용량의 연속된 기억공간을 요구한다면 malloc 함수는 원하는 기억공간을 찾지 못하고 널 포인터를 리턴할 수 있는 것이다.
널포인터는 보통 "NULL"이라는 이름으로 기호화하여 사용하는데 전처리 단계에서 0으로 바뀌므로 상수값 0과 같다고 생각해도 문제가 없다.
메모리를 동적으로 할당받을 때는 제대로 할당되었는지를 검사하고 사용이 끝난 기억공간을 반환하는 것은 중요한 일이다. 그러나 코드를 생략해도 당장 프로그램의 실행에 영향을 미치지 않는 경우가 많으므로 무의식 중에 코드를 생략할 수 있다. 따라서 항상 주의를 기울여서 코드를 작성하는 습관이 필요할 것이다. 단, 이후에 나오는 예제에서는 간결한 코드를 위해서 생략하도록 하겠다.
2 동적 할당 기억공간의 활용
동적으로 기억공간을 할당받아서 사용하는 것은 생각과는 달리 현실적으로 불합리한 면이 있다. 할당받은 기억공간을 사용하기 위해서는 포인터변수가 필요한데 입력되는 데이터의 수를 알 수 없다면 적절한 포인터변수를 선언하는 것도 힘들기 때문이다. 뿐만 아니라 포인터변수도 4바이트의 기억공간을 차지하므로 작은 기억공간을 일일이 포인터변수로 연결한다면 메모리를 효율적으로 사용한다고 볼 수 없다.
메모리의 동적 할당은 주로 많은 기억공간을 한꺼번에 할당받아서 배열로 사용하는 것이 효율적이다. 이때 할당받은 기억공간의 시작 위치만 포인터변수로 가리키게 하면 포인터변수를 배열명으로 사용하여 배열과 같이 활용할 수 있을 것이다.
예를 들어, int형 변수 5개를 동적으로 할당받는다고 하자. 5개 변수의 전체 크기는 20바이트이므로 일단 20바이트를 동적으로 할당받아서 int형을 가리키는 포인터변수에 연결한다. 그러면 처음 4바이트는 포인터변수로 참조하여 int형 변수로 사용할 수 있을 것이다. 그 이후의 기억공간은 포인터변수에 정수값을 더하여 차례로 참조하면서 배열과 같이 사용할 수 있는 것이다.
다섯 명의 나이를 입력 받아서 평균 나이를 계산하는 프로그램을 동적 할당을 사용하여 작성해 보자.
|
1. #include <stdio.h> 2. #include <strdlib.h> 3. 4. int main() 5. { 6. int *ip; 7. int i, sum=0; 8. 9. ip=(int *)malloc(5*sizeof(int)); 10. if(ip==0){ 11. printf("메모리가 부족합니다!\n"); 12. return 1; 13. } 14. printf("다섯 명의 나이를 입력하세요 : "); 15. for(i=0; i<5; i++){ 16. scanf("%d:, ip+1); 17. sum+=ip[i]; 18. } 19. printf("다섯 명의 평균 나이 : %.1lf\n", sum/5.0); 20. free(ip); 21. return 0; 22. } |
|
출력 결과 다섯 명의 나이를 입력하세요 : 21 27 24 22 35 (Enter) 다섯 명의 평균나이 : 25.8 |
9번 줄이 메모리를 동적으로 할당받는 문장이다. int형 변수 5개로 사용할 기억공간을 할당받기 위해서 int형 변수의 크기와 5를 곱한 값을 전달인자로 주고 있다. 결국 20바이트의 연속된 기억공간이 할당된 것이다.
10번 줄은 포인터변수의 값이 널 포인터인지를 검사하여 처리하는 부분이다. 널 포인터이면 기억공간이 할당되지 않은 것이므로 오류메시지를 출력하고 프로그램을 끝낸다.
15번에서 18번 줄까지는 할당받은 메모리를 배열처럼 사용하여 값을 입력받는 부분이다. scanf 함수는 입력받을 기억공간의 포인터가 필요하므로 포인터변수에 정수값을 더하여 계산된 포인터를 직접 전달인자로 주었다. 물론 &ip[i]와 같이 사용해도 되지만 포인터 표현으로 바꾸면 &*(ip+i)가 되므로 불필요한 연산을 수행할 필요는 없을 것이다. 17번 줄은 입력받은 값은 sum에 누적할 때 포인터변수로 ip로 배열표현을 사용해 보았다.
이제 입력되는 문자열의 길이에 맞게 기억공간을 할당하는 문제를 생각해 보자. 서두에서도 언급했지만 길이가 다른 여러개의 문자열을 효율적으로 저장하기 위해서는 메모리를 동적으로 할당해야 할 필요가 있다.
그런데 문자열을 입력받기 전에는 그 길이를 알 수 없으므로 우선 충분한 크기의 문자배열에 문자열을 입력받아야 한다. 그리고 그 길이를 알 수 없으므로 우선 충분한 크기의 문자배열에 문자열을 입력받아야 한다. 그리고 그 길이를 맞게 다시 동적으로 기억공간을 할당받아서 입력받은 문자열을 복사하는 것이다. 이러한 작업을 반복적으로 수행하면 여러 개의 문자열을 그 길이에 맞게 저장할 수 있을 것이다. 물론 문자배열은 다음 문자열을 입력받을 때 까지 계속 재활용해야 할 것이다.
세개의 문자열을 입력받아서 저장하는 프로그램을 작성해 보자.
|
1. #include <stdio.h> 2. #include <string.h> // 문자열 처리함수를 위한 헤더파일 포함 3. #include <stdlib.h> // 동적 할당 함수를 위한 헤더파일 포함 4. 5. int main() 6. { 7. char temp[80]; // 임시 문자배열, 충분히 크게 확보한다. 8. char *str[3]; // 동적 할당된 기억공간을 연결할 포인터배열 9. int i; // 반복 제어변수 10. 11. for(i=0; i<3; i++){ // 세 번 반복한다. 12. printf("문자열을 입력하세요 : "); // 입력 안내메시지 13. gets(temp); // 문자열 입력 14. str[i]=(char *)malloc(strlen(temp)+1); // 기억공간 할당과 연결 15. strcpy(str[i], temp); // 문자열 복사 16. { 17. 18. for(i=0; i<3; i++){ 19. printf("%s\n", stri[i]); // 입력된 문자열의 출력 20. } 21. 22. for(i=0; i<3; i++){ 23. free(str[i]); // 할당받은 메모리 반환 24. } 25. return 0; 26. } |
|
출력 결과 문자열을 입력하세요 : Hi (Enter) 문자열을 입력하세요 : Let me introduce (Enter) 문자열을 입력하세요 : Hello (Enter) Hi Let me introduce Hello |
이예제는 앞서 설명했던 문자열 처리 순서를 그대로 따르고 있다. 단지 문자열을 연결할 포인터변수가 세 개 필요하므로 이것을 포인터배열로 선언했을 뿐이다. 8번 줄이 포인터배열을 선언하는 문장이다.
11번 줄에서 16번 줄까지가 세 개의 문자열을 입력받는 반복문이다. 13번 줄에서 일단 임시 문자배열에 문자열을 입력받는다. 그리고 14번 줄에서 입력받은 문자열의 길이를 strlen 함수로 계산하여 malloc 함수의 전달인자로 주고 있다. 이때 strlen 함수는 널문자를 제외하고 문자열의 길이를 계산하므로 malloc 함수에 전달인자로 줄 때는 1을 더해서 널문자도 저장할 수 있는 기억공간을 할당해야 할 것이다. 기억공간을 할당하고 malloc 함수가 리턴하는 포인터는 포인터배열의 배열요소에 저장하여 할당받은 기억공간을 연결하도록 만든다. 마지막으로 15번 줄에서 할당받은 기억공간에 입력받은 문자열을 복사하면 문자열의 길이에 따라 행의 길이가 다른 2차원의 문자배열이 완성될 것이다.
|
포인터배열 str str[0] → H i \0 str[1] → L e t m e i n t r o d u c e . . . \0 str[2] → H e l l o \0 |
여기서 포인터의 사용과 관련하여 한 가지 주의할 점이 있다. 포인터변수나 포인터배열을 자동변수로 선언하면 쓰레기값이 존재하게 된다. 만약 쓰레기값이 참조가 불가능한 코드 영역의 주소값이고 부주의로 이 값을 참조하게 된다면 프로그램은 중간에 실행을 멈추게 될 것이다.
따라서 포인터배열은 선언과 동시에 널 포인터로 초기화해주는 것이 좋으며, 참조할 때 널 포인터인지를 검사하는 방법을 사용하면 보다 안정적인 프로그래밍이 가능할 것이다. 물론 최소한 포인터배열의 마지막 배열요소는 널 포인터가 되도록 할 것이다. 포인터배열의 배열요소의 개수가 100개라면 문자열은 최대 99개까지만 입력하고 마지막 배열요소는 널 문자로 남겨두자는 것이다.\
|
포인터 배열을 선언할 때 char *str[100] = {0}; // 포인터배열을 널 포인터로 초기화한다. 저장된 문자열을 출력할 때 for(i=0; str[i]!=0; i++){ printf("%s\n", str[i]); // 배열요소가 널 포인터가 아닌 동안 출력한다. } |
이예제는 행의 길이가 가변적인 2차원 배열을 만드는 방법을 보여준다. 이러한 가변배열을 사용하면 효과적인 메모리 사용이 가능할 것이다. 물론 입력될 문자열의 수를 미리 예상하여 포인터배열의 크기를 충분히 선언해야 하겠지만, 포인터배열이 차지하는 기억공간은 동적 할당으로 얻는 이익에 비해 충분히 희생할 가치가 있을 것이다.
이제 문자열을 출력하는 부분을 함수로 만들어 보자. 즉, 가변배열에 저장된 문자열을 함수를 사용하여 출력해보자는 것이다.
문자배열에 저장된 문자열을 출력하기 위해서는 배열명을 함수의 전달인자로 주고 호출한다. 마찬가지로 가변배열도 포인터배열의 배열명을 전달인자로 주고 출력함수를 호출해야 할 것이다. 출력함수의 이름은 str_prn으로 하자.
|
str_prn(str) ; 포인터배열의 배열명을 주고 함수 호출 |
배열명 str은 포인터이므로 매개변수는 포인터변수를 선언해야 할 것이다. 그렇다면 어떤 포인터변수를 선언해야 하는 것일까? str은 포인터배열의 첫번째 배열요소를 가리키므로 가리키는 것의 형태는 (char *)형이 될 것이다.
|
배열명 str → char * char * char * |
따라서 str을 저장할 포인터변수도 (char *)형을 가리키는 포인터변수로 선언해야 한다. 결국 이중포인터변수를 매개변수로 사용해야 하는 것이다.
|
void str_prn(char **); // 함수의 원형 선언 |
매개변수 sp가 배열명 str을 저장하면 sp 역시 배열명과 같이 사용할 수 있으므로 함수의 정의에서 반복문을 사용하여 문자열을 출력하면 될 것이다. 그런데 여기서 반복문의 형식을 살짝 바꿔서 for문 대신 while문을 사용하여 코드를 작성해 보도록 하자.
메인함수에서 배열명을 사용하여 문자열을 출력할 때는 str이 배열명이므로 그 값을 변경시킬 수 없다. 따라서 str[i]와 같이 배열표현을 사용하거나 *(str+i)와 같이 정수값을 더하면서 각 문자열을 출력할 수 밖에 없는 것이다. 그러나 배열명을 포인터변수에 저장하면 포인터변수 자체의 값을 바꿀 수 있으므로 매개변수를 하나씩 증가시키면서 문자열을 출력할 수 있을 것이다.
|
void str_prn(char **sp) // 매개변수는 이중포인터변수로 배열명을 받는다. { while(*sp!=0) // 포인터배열의 값이 널 포인터가 아닌 동안 반복 { printf("%s\n", *sp); // sp가 가리키는 값은 문자열의 시작주소값이다. sp++; // sp는 다음 배열요소를 가리키도록 한다. } } |
지금까지 여러 개의 문자열을 포인터배열로 연결하고 함수로 출력하는 방법을 살펴보았다. 이제 이와 같은 문자열 처리가 사용되는 대표적인 예를 살펴보도록 하자.
3 메인함수의 전달인자
우리가 작성하는 프로그램은 운영체제가 실행시키게 되는데, 실행방법은 운영체제마다 다르다. 윈도즈는 바탕화면에서 프로그램 아이콘을 더블클릭하여 실행시키며 도스나 유닉스에서는 명령행에서 실행파일의 이름을 직접 입력하여 실행시킨다. 명령행에서 프로그램을 실행시킬 때는 프로그램의 이름 외에도 프로그램에 필요한 정보를 함께 입력할 수 있는데 이들을 모두 명령행 전달인자(command line argument)라고 한다.
예를 들어, 도스에서 사용하는 복사 프로그램은 복사될 대상과 새롭게 복사될 파일의 이름을 함께 입력하는데 이들이 모두 명령행 전달인자가 되는 것이다. 이때 명령행 전달인자의 개수는 프로그램의 이름까지 포함하여 3개가 될 것이다.
|
C:\> copy src.txt des.txt (copy : 실행파일 이름, src.txt : 복사할 파일 이름, des.txt : 복사될 파일 이름) |
명령행에서 프로그램을 실행시키면 운영체제는 명령행 전달인자를 가공하여 문자열의 형태로 메모리에 저장하고 포인터배열로 연결한 후에 포인터배열의 시작위치를 실행프로그램의 메인 함수에 넘겨주게 된다. 이때 명령행 전달인자의 개수도 함께 넘겨준다.
만약 이 값들이 프로그램에서 필요하다면 메인함수에 매개변수를 선언하여 사용할 수 있다. 운영체제는 프로그램이 실행될 때마다 항상 명령행 전달인자를 가공하여 프로그램에서 사용할 수 있도록 준비하지만 프로그램에서 그 값들을 사용하는 것은 선택이다.
프로그램에서 명령행 전달인자를 사용하는 방법은 다음과 같다. 첫번째는 전달인자는 문자열의 개수이며 두번째 전달인자는 포인터배열의 배열명이다. 따라서 매개변수의 첫번째는 int형 변수를 사용하고 두번째는 이중포인터를 사용하여 전달인자를 받으면 될 것이다. 매개변수의 이름은 임의로 작성할 수 있으나 관례적으로 argc와 argv를 사용한다(여기서 argc는 arguement count, argv는 argument vector를 뜻한다).
명령행 전달인자를 받아서 출력하는 프로그램을 작성해보자. 프로그램의 이름은 "mycommand"로 한다.
|
1. #include <stdio.h> 2. 3. int main(int argc, char **argv) // 명령행 전달인자를 받을 매개변수 4. { 5. int i; // 반복 제어변수 6. 7. for(i=0; i<argc; i++){ // 전달인자(문자열)의 개수만큼 반복 8. printf("%s\n", argv[i]); // 전달인자(문자열)를 하나씩 출력한다. 9. } 10. return 0; 11. } |
|
출력 결과 C:\>mycommand first_arg second_arg (Enter) MYCOMMAND first_arg second_arg C:\> |
3 번 줄에서 메인함수는 명령행 전달인자를 받기 위해 매개변수를 선언하고 있다. 만약 출력 결과와 같이 명령행을 입력했다면 명령행 전달인자의 개수는 3개가 될 것이고 이 값은 argc 매개변수에 저장될 것이다. 그리고 문자열을 연결하고 있는 포인터배열의 시작주소값은 argv 매개변수에 저장될 것이다.
7번 줄에서는 제어변수의 값을 0부터 시작하여 argc보다 작을 때까지 반복하므로 결국 명령행에서 입력한 문자열의 개수만큼 반복될 것이다. 8번 줄에서는 argv를 사용하여 연결하고 있는 문자열을 하나씩 출력하고 있다(출력 결과를 보면 실행파일의 이름은 대문자로 출력되었는데 이것은 운영체제 나름대로의 구현방식으로 볼 수 있을 것이다).
운영체제는 명령행 전달인자를 가공할 때 각 문자열을 포인터배열에 연결하고 다음 배열요소를 널 포인터로 채운다. 따라서 반복문에서 널 포인터가 아닌 동안 출력하도록 while문으로 작성할 수도 있을 것이다.
|
while(*argv != 0){ // argv가 가리키는 포인터배열의 값이 널 포인터가 아닌 동안 printf("%s\n", *argv); // 문자열을 출력한다. argv++; // 포인터배열의 다음 배열요소로 이동 } |
단, 이 방법은 argv의 값이 바뀌므로 명령행 전달인자를 다시 사용해야 할 필요가 있을 때는 적합하지 않을 것이다.
4 기타 동적 할당 함수
동적으로 메모리를 할당받기 위해서 기본적으로 사용하는 함수는 malloc 함수이다. 그러나 응용에 따라 좀더 편리하게 사용할 수 있는 함수들이 있다. 메모리를 동적으로 할당받아서 배열의 용도로 사용하고자 한다면 calloc 함수를 사용하면 편리하다. calloc 함수의 원형은 다음과 같다.
|
void *calloc(unsigned int, unsigned int) |
이 함수의 전달인자는 두 개이다. 먼저 두 번째 전달인자는 malloc 함수와 마찬가지로 할당받고자 하는 기억공간의 크기를 바이트 단위로 입력한다. 그리고 첫번째 전달인자로 그 개수를 입력한다. 만약 double형 변수 5개로 사용할 배열의 기억공간이 필요하다면 다음과 같이 사용할 수 있을 것이다.
|
double *dp; dp = (double *) calloc ( 5, sizeof(double) ); (5 : 배열요소의 개수, sizeof(double) : double형 변수 하나의 크기) 그렇다면 배열 전체의 기억공간을 malloc 함수로 할당받는 것과 어떤 차이가 있는 것일까? malloc 함수를 사용하는 경우 → dp = (double *) malloc (5 * sizeof(double)); |
특별한 차이점은 없다. 다만, calloc 함수를 사용하면 할당받은 기억공간을 모두 0으로 초기화해주게 된다. 이것은 동적으로 할당받은 기억공간에 0으로 초기화가 필요한 경우 반복문으로 일일이 초기화해야 하는 수고를 덜어줄 것이다.
|
double *ap; int i; ap=(double *)calloc(5, sizeof(double)); for(i=0; i<5; i++) { printf("%lf\n", ap[i]); } |
|
출력 결과 0.000000 // 모두 0으로 초기화된 것을 확인할 수 있다. 0.000000 0.000000 0.000000 0.000000 |
메모리를 동적으로 할당받을 때는 대부분 입력되는 데이터에 맞게 기억공간을 확보하게 된다. 그러므로 한 번 할당받은 기억공간에 저장된 데이터에 변화가 생기면 기억공간이 부족하거나 남는 경우가 발생할 것이다. 이때는 realloc 함수를 사용하여 기억공간의 크기를 조절할 수 있다.
|
void *realloc(void *, unsigned int); // 기억공간의 재할당 |
이 함수는 첫번째 전달인자로 이미 할당받은 기억공간의 포인터를 넘겨준다. 그리고 두 번째 전달인자로 새로 할당받고자 하는 크기를 바이트단위로 입력한다.
기억공간을 새롭게 할당받는 위치는 이미 할당받은 기억공간의 위치와 같으므로 재할당하는 기억공간의 크기가 크면 기존의 데이터는 그대로 보존되게 된다. 반대의 경우라면 이미 입력되어 있는 데이터는 잘려나갈 것이다. 새로 추가되는 기억공간에는 쓰레기값이 채워진다.
예를 들어, int형 변수 5개로 사용하고 있는 기억공간을 10개로 늘이고 싶다면 다음과 같이 사용한다.
|
int *ip; ip = (int *) calloc (5, sizeof(int)); ... // 할당받은 기억공간에 10, 20, 30, 40, 50을 저장한 경우 ip = (int *) realloc (ip, 10*sizeof(int)); (ip : 이미 할당받은 기억공간의 위치, 10*sizeof(int) : 재할당받을 전체 기억공간의 크기) ip → 10 20 30 40 50 ? ? ? ? ? 이미 입력된 데이터 새로 추가된 기억공간 |
|
1. #include <stdio.h> 2. #include <stdlib.h> 3. 4. int main() 5. { 6. int *ip; // 할당받은 기억공간을 연결할 포인터변수 7. int size=5; // 한번에 할당받을 기억공간의 크기, int형 변수 5개씩 8. int cnt=0; // 현재 입력된 양수값의 개수 9. int num; // 양수값을 입력받을 변수 10. int i; // 반복 제어변수 11. 12. ip=(int *)calloc*(size, sizeof(int)); // 일단 기억공간을 할당받는다. 13. while(1){ // 무한 반복 14. printf("양수를 입력하세요 => "); 15. scanf("%d", &num); // 데이터 입력 16. if(num<=0) break; // 0또는 음수이면 종료한다. 17. if(cnt<size){ // 기억공간이 남아있으면 18. ip[cnt++]=num; // 입력받은 데이터를 저장한다. 19. } 20. else{ // 기억공간이 부족하면 21. size+=5; // 크기를 늘려서 22. ip=(int *)realloc(ip, size*sizeof(int)); // 재할당받는다. 23. ip[cnt++]=num; // 재할당받은 공간에 데이터를 저장 24. } 25. } 26. 27. for(i=0; i<cnt; i++){ // 입력 받은 데이터 출력 28. printf("%5d", ip[i]); 29. } 30. free(ip); // 할당받은 기억공간 반환 31. return 0; 32. } |
12번 줄은 우선 int형 변수 5개 크기의 기억공간을 할당한다. 그리고 반복문으로 양수값을 입력받는다. 이때 16번 줄에서 음수값을 입력하면 반복문을 빠져나와 입력을 종료하도록 한다.
17 번 줄에서 24번 줄까지는 입력받은 데이터의 수와 현재 기억공간의 크기를 비교하여 재할당 여부를 결정하는 부분이다. 만약 기억공간이 부족하다면 22번 줄에서 기억공간의 크기를 늘려서 기억공간을 재할당한 후에 데이터를 저장하게 될 것이다.
|
Reference : 뇌를 자극하는 C 프로그래밍, 서현우, 한빛미디어 |
Trackback ADDRESS :: http://blog.sinovino.org/trackback/52
