일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- html
- 자료구조
- Algorithm
- queue
- LineTracer
- C언어
- 시스템프로그래밍
- set
- 운영체제
- vector
- Visual Micro
- 수광 소자
- list
- 컴퓨터 그래픽스
- 아두이노
- 통계학
- 아두이노 컴파일러
- map
- priority_queue
- 라인트레이서
- c++
- Deque
- Arduino
- Stack
- 아두이노 소스
- WinAPI
- arduino compiler
- stl
- directx
- Array
- Today
- Total
Kim's Programming
C언어 - 포인터(Pointer)(2/3) 본문
1 | int arrayScore[30]; | cs |
1 2 3 4 | int student; printf("학생수를 입력해주세요."); scanf("%d",&student); int arrayScore[student]; | cs |
음.. 좋아보이는데요? 미리 입력해서 할당을 하는 방식. 하지만! 배열첨자에는 변수가 들어 갈 수 없으며 상수로만 이루어 져야 합니다. 이는 곧 컴파일러 에러 발생이 될 것입니다. 또한 student라는 변수 값은 실행중에 사용자가 입력하는 값이기 떄문에 컴파일할 때는 이 값이 얼마인지 알 수 없습니다. 컴파일러가 아무리 성능이 좋아도 미래의 일을 예측 할 수는 없습니다. 그러므로 정적 할당시는 필요한 배열 크기를 변수로 지정할 수 없습니다. 메모리 필요량을 프로그램 작성 중에 결정할 수 없을 때는 정적 할당할 수 없으며 동적 할당을 사용해야 합니다. 동적 할당이 필요한 또 다른 경우는 임시적인 메모리가 필요한 때입니다. 예를 들어서 텍스트 파일에서 특정 문자열이 있는지만 알고 싶다고 했을때 텍스트 파일을 검색하려면 파일을 읽며 그러기 위해서는 텍스트 파일을 읽기 위한 버퍼가 필요합니다. 이런 버퍼를 미리 정적 할당해 놓을 필요 없이 파일 크기만큼만 동적 할당한 후 작업만 하고 해제하면 됩니다.
1 2 3 4 | char *buf = 동적할당(파일크기) buf에 파일 읽음 원하는 작업 - buf에 문자열 있는지 조사 buf해제 | cs |
파일 크기가 최대 1M라고 할 때 char buf[1048576]과 같이 전역 버퍼를 정적 할당해 놓고 작업 하는 것도 가능은 합니다. 하지만 임시 기억 공간인 스택은 용량이 그다지 크지 않기 때문에 buf는 지역변수로 선언할 수 없으며 전역으로만 선언이 가능합니다. 하지만 이렇게 하는 경우 이 프로그램은 항상 1M라는 메모리는 항상 사용해야 하기 떄문에 효율이 떨어지게 됩니다. 또한 텍스트 파일의 크기도 1M이하로 항상 유지해야 정상적으로 버퍼로 읽혀지지만 1M이하로 유지될거란건 알 수 가 없습니다. 동적 할당된 메모리는 이름이 없는 변수라고 할 수 있습니다. 독점적인 메모리 영역을 차지하고 있기 떄문에 값은 기억을 할 수 있지만 이름이 없기 때문에 포인터로만 접근할 수 있습니다. 그래서 malloc함수가 리턴하는 포인터는 반드시 적절한 타입의 포인터 변수로 대입 받아야 합니다. 시작 번지를 잃어버리면 할당된 메모리를 쓸 수 없음은 물론이고 다 사용한 후에도 해제 하지도 못합니다.
메모리라는 것은 시스템에 부착되어 있는 RAM을 의미합니다. RAM은 장착랑이 다르긴 한데 요즘은 램이 많이 싸져서 보통 많은 RAM을 사용하긴 하지만 RAM은 다다익선입니다. 하지만 복수의 프로그램이 꼭 필요한 만큼의 메모리를 충돌없이 나눠서 사용하려면 정교한 메모리 관리 원칙이 필요합니다. 메모리 관리는 응용프로그램은 할 수 없으며 운영체제가 직접 하는데 하드웨어 관리, 스케줄링 등과 함께 운영체제의 주요 임무중 하나입니다. 다음은 운영체제에서 메모리를 관리하는 일반적인 원칙입니다.
- 메모리 관리의 주체는 운영체제입니다. 응용 프로그램은 직접 메로리를 관리할 수 없는데 메모리가 필요할 경우 운영체제에 할당 요청을 해야합니다. 16비트 운영체제에서는 응용 프로그램이 임의의 주소 공간을 액세스 할 수 있었지만 32비트의 보호된 운영체제들은 안정성을 위해 응용 프로그램이 임의의 메모리에 엑세스 하는 것을 금지하고 있습니다. 반드시 운영체제를 통해서만 메모리를 항당 받을 수 있습니다.
- 운영체제는 메모리가 있는 한은 할당 요청을 거절하지 않습니다. 메모리는 결국 작업을 위해 존재하는 것이므로 응용프로그램 (프로그램을 쓰는 사용자)이 달라고 하는만큼 내 주도록 되어 있습니다. 하지만 먄약 요청한 양만큼 메모리가 남아 있지 않을 경우에는 에러를 리턴하여 프로그램에게 메모리가 없음을 알려줍니다. 하지만 최근의 운영체제는 메모리가 없을 경우 가상메모리 공간으로 메모리를 만들어 주기도 합니다.
- 한 번 할당된 메모리 공간은 절대로 다른 목적을 위해 재할당 되지 않습니다. 운영체제는 메모리 공간을 누가 얼마나 사용하는지 기억을 하며 반납하기 전에는 응용프로그램이 이 공간을 독점적으로 사용할 수 있도록 보장합니다. 그래서 한 번 할당한 메모리는 일부러 해제하지 않는 한은 언제까지든 안심하고 사용할 수 있습니다.
- 응용 프로그램이 할당된 메모리를 해제하면 운영체제는 이 공간을 빈 영역으로 인식하고 다른 목적을 위해 사용할 수 있도록 합니다. 즉, 특정 메모리 공간을 동시에 두 프로그램이 사용 할 수는 없지만 번갈아 가면서 사용하는 것은 가능합니다. 메모리 공간이 무한하지 않기 때문에 응용 프로그램들은 자신이 꼭 필요한 만큼 할당해서 사용하고 다 쓴 후에는 반드시 반납해서 다른 목적에 사용될 수 있도록 해야합니다.
32비트 운영체제에서는 메모리 관리 원칙은 한마디로 "중앙 집중적인 신고제"라고 할 수 있습니다. 개별 응용 프로그램은 메모리를 관리하지 않고 오로지 운영체제만이 메모리를 관리합니다. 그래서 일관된 관리가 가능하고 응용프로그램 끼리 서로의 영역을 침범할 위험이 없습니다. 또한 허가제가 아닌 신고제기 떄문에 응용프로그램은 필요한 만큼의 메모리를 요청만 하면 얼마든지 할당 받을 수 있습니다. 다만 다른 프로그램과 조화로운 실행을 위해서 다 쓴 메모리를 즉시 반납하여야 합니다.
메모리의 할당 및 해제
메모리를 동적 할당 및 할당 해제할 때는 다음 두 함수를 사용합니다.
1 2 | void *malloc(size_t size); void free(void *memblock); | cs |
먼저 malloc함수부터 알아보겠습니다. 인수로 필요한 메모리양을 바이트 단위로 전달하면 요청한만큼 할당합니다. size_t는 메모리의 양을 나타내는 단위인데 _t로 끝나는 사용자 정의 타입은 표준에 의해 반드시 정의하도록 되어 있으므로 기본 타입과 거의 대등한 자격을 가집니다. 플랫폼에 따라서 다르게 정의되어있지만 일반적으로 32비트 컴파일러들은 size_t를 unsigned의 부호없는 정수형으로 정의 하며 이 함수로 할당할 수 있는 최대 용량은 4GB입니다.
10바이트가 필요하면 malloc(10)이라고 호출하고 100바이트가 필요하면 malloc(100)이라고 호출 하면 됩니다. 또한 실행중에 할당하는 것이므로 malloc(Size)등의 변수를 이용하여도 할당이 가능합니다. malloc은 응용 프로그램이 필요로 하는 양만큼 운영체제에게 할당을 요청하며 운영체제는 사용되지 않는 빈영역(힙)을 찾아 요청한 메모리를 할당하여 그 시작 번지를 리턴하게 됩니다. 응용 프로그램이 할당한 메모리를 어떤 목적에 사용할지는 알 수 없으므로 malloc은 void *형을 리턴하며 받는 쪽에서는 원하는 타입으로 캐스팅 해야합니다. free함수는 동적으로 할당된 메모리를 해제해야합니다. 응용프로그램은 메모리를 다 사용한 후에 free함수를 호출하여 해제하여야 합니다. 그래야 이 영역이 다른 프로그램을 위해 재활용 될 수 있습니다. 다음 소스는 변수 10개를 담을 수 있는 메모리 할당 예입니다.
1 2 3 4 | int *ar; ar = (int *)malloc(10 * sizeof(int)); // ar 사용 free(ar); | cs |
동적으로 할당된 메모리를 사용하려면 그 시작 번지를 기억해야 하므로 포인터 변수가 필요합니다. 이 경우 정수형 변수를 위한 메모리를 할당하는 것이므로 정수형 포인터 ar을 선언하였습니다. malloc을 호출할 때는 필요한 메모리양을 할당하고 그 시작번지를 리턴하되 리턴 타입이 void*형이므로 이 포인터를 변수에 대입할 떄는 반드시 원하는 타입으로 캐스팅할 필요가 있습니다. 물론 void*형 변수로 받을 수도 있는데 이렇게 하면 받을 때는 편하지만 쓸때마다 캐스팅해야 하므로 더 불편합니다. 정수형 포인터에 대입해야 하므로 (int *)로 캐스팅했습니다. 이렇게 할당하면 ar번지 이후 40바이트를 응용프로그램이 배타적으로 소유합니다. 즉 포인터 ar이 가리키는 번지는 마치 ar[10] 정수형 배열과 같아지며 메모리 내에서의 실제 모양과 내용도, 적용되는 문법도 배열과 동일합니다. ar 번지 이후의 40바이트는 다른 목적에 사용되지 않으므로 마치 정적 할당된 배열처럼 이 메모리를 활용할 수 있습니다. 물론 다 사용하고 난 후에는 반드시 free 함수로 메모리를 해제해야 합니다. 예시 소스를 보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #define _CRT_SECURE_NO_WARNINGS #include<cstdio> #include<windows.h> void main() { int *arrayScore; int i,Student_num; int sum=0; printf("학생수를 입력 하세요."); scanf("%d", &Student_num); arrayScore = (int *)malloc(Student_num*sizeof(int)); if (arrayScore == NULL) { printf("메모리가 부족합니다.\n"); exit(0); } for (i = 0; i < Student_num; i++) { printf("%d번 학생의 성적을 입력하세요 : ", i + 1); scanf("%d", &arrayScore[i]); } for (i = 0; i < Student_num; i++) { sum += arrayScore[i]; } printf("\n총점은 %d점이고 평균은 %d점입니다. \n", sum, sum / Student_num); free(arrayScore); } | cs |
실행 결과는 다음과 같습니다.
실행 직후 Student_num를 입력받아서 arrayScore 배열을 할당 했습니다. 만약 malloc이 NULL을 리턴하면 성적처리를 계속할 수 없으므로 메모리가 부족하다는 에러메세지를 출력하고 프로그램을 종료해야합니다. (만약 arrayScore=NULL이라고 써버린다면 0번지를 건들이게 되어 프로그램이 다운되어 버립니다.) Student_num 크기의 정수형 배열과 똑같아지며 이후 코드에서는 arrayScore를 정적 할당된 배열과 똑같은 방법으로 사용할 수 있습니다. arrayScore[i]연산식을 이용하여 i번째 요소를 자유롭게 읽고 쓸 수 있으며 해제하기 전까지 이 메모리는 다른 응용프로그램이 건드릴 수 없는 독점적인 공간이 됩니다. 모든 작업이 끝나면 free함수로 할당된 메모리를 해제해야 합니다. 동적 할당은 필요한 만큼 malloc으로 할당해서 다 쓰고 나면 free로 해제하기만 되는 간단한 것입니다.
다음은 malloc과 free함수에 대한 참고사항입니다. malloc 함수는 할당에 실패하게 되면 에러의 포시로 NULL을 리턴하며 그래서 이 함수를 호출할 때는 항상 malloc이 리턴한 번지를 점검하는 것이 원칙입니다. 메모리가 부족한 경우는 언제든지 다가올 수 있고 점검을 하지않으면 0번지를 엑세스 할 위험이 있습니다. 제대로 만든 프로그램은 극한의 상황에서도 다운되는 상황은 없는게 좋습니다. 하지만 32비트 환경은 메모리가 충분하며(64비트에서 더욱더..)운영체제 메모리 관리 기법이 정교해 져서 작은 메모리를 할당할 떄는 에러 점검을 생략해도 무리가 없습니다. 작은 메모리라는 것의 기준은 없지만 메가 단위 이상 할당할 떄는 꼭 점검해야하며 수십~수백 바이트 정도는 굳이 점검 하지 않아도 상관 없습니다. 왜냐? 메모리 부족이라는 것은 응용 프로그램만의 문제는 아닙니다. 만약 malloc(100)호출이 실패하는 상황이 발생한다면 응용 프로그램이 다운되기 전에 운영체제가 먼저 이 상황을 처리 하도록 되어 있습니다. 에러 점검 코드를 일일이 작성한다면 안전성, 이식성면에서는 더 좋겠지만 실행 속도는 느려지고 크기도 커지기 떄문에 무리한 메모리 확장으로 인한 다운보다 더 치명적인 상황이 발생할 수도 있습니다.
다음 함수는 malloc 함수와 마찬가지로 메모리를 할당하되 필요한 메모리양을 지정하는 방법만 다릅니다.
1 | void *calloc(size_t num,size_t size); | cs |
첫 번쨰 인수 num은 할당할 요소의 개수이고 size는 요소의 크기입니다. malloc은 필요한 메모리를 바이트 단위 하나로만 전달을 받게되지만 calloc은 두 개의 값으로 나누어 전달받는다는 점이 다릅니다. malloc이 "몇 바이트 할당해 주세요"라고 요청하는 것이지만 calloc은 " 몇 바이트 짜리 몇개 할당해 주세요"라고 요청하는 것입니다. 그래서 다음의 두줄은 같은 의미가 됩니다.
1 2 | ar=(int *)malloc(10*sizeof(int)); ar=(int *)calloc(10,sizeof(int)); | cs |
구조체같은 큰 데이터의 배열을 할당할 때는 calloc으로 할당하는 것이 더 보기에도 읽기도 좋습니다. calloc도 실제 할당하는 양은 size*num으로 계산하므로 calloc(1,10*sizeof(int))나 calloc(10*sizeof(int))은 결과는 동일하다만 필요한 메모리양을 단위와 개수로 나누어 좀 더 논리적으로 표현 한다는 점에서 조금 다릅니다. 또 calloc이 malloc과의 차이점은 메모리 할당 후 전부 0으로 초기화한다는 것입니다. malloc은 메모리 할당만 하기때문에 할당된 메모리에 쓰레기 값이 들어있게 되지만 calloc으로 할당하면 할당 후 모든 메모리를 0으로 채웁니다. 할당후에 배열을 따로 초기화 해야한다면 malloc 호출후 memset을 쓰는거 보단 calloc함수로 바로 할당해주는 것이 편합니다.
다음 함수는 이미 할당된 메모리의 크기를 바꾸어 재할당합니다. 최초 할당한 크기보다 더 큰메모리가 필요할 때는 이 함수를 이용하여 크기 조정이 가능합니다. 원리 크기보다 더 작게 축소 재할당도 물론 가능하지만 보통은 확대 재할당을 많이 하게됩니다.
1 | void *realloc(void *memblock,size_t size); | cs |
첫 번째 인수로 malloc이나 calloc으로 할당한 메모리의 시작 번지를 주고 두 번째 인수로 재할당할 크기를 전달합니다. 만약 첫 인수가 NULL일 경우,즉 다시말해 할당되어 있지 않을 경우는 새로 메모리를 할당하므로 realloc의 동작은 malloc과 같아지게 됩니다. 두 번째 인수 size가 0일 경우에는 할당을 취소하라는 이야기가 되므로 free함수와 같은 동작을 하게 됩니다. 일반적으로 축소 재할당했을 때는 같은 번지이며 확대 재할당 했을 때는 다른 번지로 이동될 확률이 높습니다. 현재의 메모리위치에서 할당 크기 만큼 늘릴 수 없을 경우에는 뒤로 쪽의 가장 빠른 주소의 메모리로 이동하게 되며 뒤쪽이 충분한 경우는 그 자리에서 늘어나서 할당되게 됩니다.
realloc 함수의 간단한 사용 예를 알아 보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include<cstdio> #include<windows.h> void main() { int *array; array = (int *)malloc(5 * sizeof(int)); array[4] = 1234; array = (int*)realloc(array, 10 * sizeof(int)); array[9] = 5678; printf("ar[4]=%d, ar[9]=%d\n", array[4], array[9]); free(array); } | cs |
위 소스의 결과는 다음과 같이 나오게 됩니다.
최초 array배열을 20바이트 크기로 할당을 했으므로 array는 array[0]~array[4]까지의 요소를 가지게 될 것입니다. 이 상태에서 어떤 이유로 ar이 40바이트로 확장되어야 할 이유가 생기게 되면 realloc함수로 array의 크기를 늘려 재할당 하면 됩니다. 크기를 확장하면 array의 번지가 바뀔 수는 있지만 원래 배열에 있던 값들은 그대로 유지되게 됩니다. 동적할당이 필요한 이유는 컴파일할 시점에서는 필요한 메모리양이 감이 안잡힐때가 있기 떄문입니다. 재할당이 필요한 이유는 실행 중에라도 필요한 메모리양을 가늠할 수 없을 때가 있기 때문입니다.
이중 포인터
이중 포인터란 포인터 변수를 가리키는 포인터라는 의미이며 다른 말로 하면 포인터의 포인터라고 할 수 있습니다. 포인터 변수도 메모리를 차지하기 때문에 이 변수도 번지가 존재하게 됩니다. 이 번지를 가리키는 또 다른 포인터 변수를 선언할 때는 * 구두점을 두 번 연속하게 다음과 같이 선언하게 됩니다.
1 | int **ppi | cs |
이 선언에서는 ppi 정수형 대상체를 가리키는 포인터 변수의 번지를 가리키는 포인터 변수로 선언되었습니다. 앞쪽에서 말했던 문장을 다시 보겠습니다.
A *형은 하나의 타입으로 인정된다.
A형 병수를 선언할 수 있으면 A *형도 항상 선언할 수 있다.
정수형 포인터 변수는 다음과 같이 선언합니다.
1 | int *pi | cs |
이 선언문에서 int *이라는 표현이 "정수형 포인터"라는 뜻으로 그 자체가 하나의 타입입니다. 따라서 다음과 같이 괄호로 묶으면 좀 더 읽기 쉬워지고 뜻이 분명해 집니다.
1 | (int *) pi | cs |
두 번째 명제에 의해 A형에 대해 항상 A *형이 가능하므로 int *에 대한 포인터 형을 만들 수 있습니다. int *형 변수를 가리키는 변수 ppi를 선언하면 다음과 같이 됩니다.
1 | (int *) *ppi | cs |
이 선언문에서 괄호를 제거하면 이중 포인터 선언문인 int **ppi;가 됩니다. 물론 괄호를 이용한 것은 설명을 위한 괄호일뿐 실제로 괄호를 이용하면 컴파일러는 에러처리를 하게 됩니다. 하지만 typedef을 이용하여 int *형을 별도의 타입으로 정의 한 후 이 타입의 변수를 선언 하는 것은 가능합니다.
1 2 | typedef int *PINT PINT pi; | cs |
보다시피 int *형을 PINT라는 사용자 정의 타입으로 정의하였고 이 타입의 변수 pi를 선언할 수 있습니다. 포인터 변수 pi를 가리킬 수 있는 포인터 변수를 선언하면 다음과 같습니다.
1 | PINT *ppi | cs |
이 선언문에서 PINT를 사용자 정의형으로 풀어 쓰게되면 **ppi가 됩니다. 같은 원리로 3중 포인터는 int ***pppi와 같이 선언되고 8중 10중 포인터도 *을 그만큼 붙이면 가능하게 됩니다. 3중 이상의 포인터는 사용 되진 않지만 원리는 동일하므로 이중 포인터만 알아도 활용가능은 합니다. 다음 소스를 보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include<cstdio> void main() { int i = 0; int *pi; int **ppi; i = 123; pi = &i; ppi = π printf("%d\n", **ppi); } | cs |
정수형 번지 i에 123을 대입해 놓고 이중 포인터로 이 값을 읽는 것입니다. 정수형 포인터 pi가 i의 번지를 가지고 정수형 이중 포인터 변수 pi의 번지를 대입해 놓고 **ppi의 값을 읽으면 결국 i의 값을 읽는 결과가 나타나게 됩니다. i는 일반 변수이기 떄문에 4바이트를 차지하고 123이 대입되었습니다. 또 정수형 포인터 pi도 일종의 변수이므로 메모리상에 할당되며 그 초기값으로 &i, 즉 i번지를 대입 받았습니다. 이 상태에서 pi가 i를 가리키고 있으므로 *pi연산문으로도 i를 읽는 것이 가능합니다. 더 나아가 포인터 변수 pi도 일종의 변수이므로 분명히 메모리에 할당될 것이고 따라서 번지를 가지게 됩니다. pi가 할당되어 있는 번지값인 &pi를 이중 포인터 변수 ppi에 대입했습니다. 그래서 ppi는 pi를 가리키고 pi는 다시 i를 가리키고 있는 구조가 됩니다. 이상태에서 **ppi연산문으로 값을 읽으면 *(*ppi) = *(pi) = i라는 결과가 되기 때문에 결국 출력 값은 i 가 되게 됩니다. 다음은 모두 같은 대상을 나타냅니다.
1 2 3 | i=*pi=*ppi &i=pi=ppi *&i=*&*pi=*&**ppi | cs |
*연산자와 &연산자는 서로 반대되는 동장을 하는데 이 두 연산자에 의해 가리키고 끄집어 내오다 오면 동등한 수식이 여러 개 생길 수 있습니다. 그렇다고 &&i=&pi=ppi라는 등식은 성립하지 않는데 &연산자를 두 번 쓰는 것은 적법하지 않습니다. 왜냐하면 &연산자의 피 연산자는 메모리상의 실제 번지를 점유하고 있는 좌변값(Ivalue)이어야 하는데 &i는 i가 저장된 번지를 나타내는 포인터 상수일 뿐 좌변값이 아니기 때문입니다. 이중 포인터의 전형적인 활용에 대한 소스를 알아 보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #define _CRT_SECURE_NO_WARNINGS #include<cstdio> #include<Windows.h> #include<string.h> void InputName(char **pName) { *pName = (char *)malloc(12); strcpy(*pName, "Cabin"); } void main() { char *Name; InputName(&Name); printf("이름은 %s입니다.\n", Name); free(Name); } | cs |
main에서 char *형 변수 Name을 선언하고 이 포인터 변수의 번지, 즉 char **형의 이중 포인터를 InputName 함수로 전달했으며 이 함수는 이중 포인터를 형식 인수 pName으로 대입받습니다. Name은 함수 내부에서 값이 출력용 인수이기 때문에 호출원에서 초기화하지 않아도 상관없습니다. InputName함수는 필요한 만큼 (위에서는 12로 가정) 동적으로 메모리를 할당하여 할당된 번지를 pName이 가리키는 번지인 *pName에 대입했습니다. 여기서 *pName이라는 표현식은 곧 main에서 InputName으로 전달한 실인수 Name을 의미 합니다. 그리고 할당된 번지에 어떤 문자열을 복사했습니다. 결국 InputName 함수는 main의 Name 포이터 변수를 참조 호출로 전달받아 Name에 직접 메모리를 할당하고 이 번지에 scanf로 입력받은 이름까지 복사한 것입니다. InputName이 리턴되었을 때 Name은 12바이트 길이로 할당된 번지를 가르키며 이 안에는 입력된 이름까지 들어 있으므로 printf로 출력할 수 있고 다 사용한 후 free로 해제해주면 됩니다. 이 문제를 잘못생각하면 다음과 같이 char * 를 받도록 작성할 수도 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #define _CRT_SECURE_NO_WARNINGS #include<cstdio> #include<Windows.h> #include<string.h> void InputName(char **pName) { *pName = (char *)malloc(12); strcpy(*pName, "Cabin"); } void main() { char *Name; InputName(Name); printf("이름은 %s입니다.\n", Name); free(Name); } | cs |
언 뜻 보기에는 맞아보이지만 아예 컴파일도 되지 않습니다. 왜 그런지 알아보겠습니다. main에서 Name을 선언하고 초기화되지도 않은 Name의 값(비록 그 값이 번지라 하더라도 어쨋든 값)을 InputName의 pName으로 전달했습니다. 이 함수는 pName에 메모리를 할당하고 이름 문자열을 복사해 넣었지만 pName은 함수의 지역변수일 뿐이지 호출원의 실 인수 Name과는 상관이 없습니다. 함수가 char *의 값을 전달받으면 이 번지가 가리키는 내용을 변경할 수는 있지만 포인터 자체를 변경해서 호출원으로 돌려줄 수는 없습니다. 앞에서 보았듯 인수 X의 값을 함수 내부에서 변경하려면 X의 포인터를 넘기는 참조호출을 해야합니다. 그러므로 InputName에서 char * 형의 인수 Name을 변경할 수 있도록 하려면 char *의 포인터인 char **형을 넘겨야하고 InputName에서 함수에서는 *pName으로 실인수를 참조해야 하는 것입니다.
main 함수의 인수
main함수도 일종의 함수이기 떄문에 인수들을 가질 수 있고 리턴값도 물론 가질 수 있습니다. main함수는 프로그램 실행 직후 자동으로 호출된다는 점에 있어서 일반 함수와는 다른 특별한 면이 있으며 프로그램의 시작점이기 떄문에 이름은 고정되어 있지만 함수의 원형은 고정적이지 않습니다. main함수의 원형은 다음과 같은 조합이 가능합니다.
1 | void(또는 int) void(int argc, char *argv[],char env[]); | cs |
리턴값은 int형이거나 void형 중 하나를 선택 할 수 있으면 세 개의 인수를 가지게 되는데 인수는 뒤쪽부터 차례대로 생략 가능합니다. 그래서 main함수의 가능한 원형은 여덟가지나 되는데 8가지 모두 살펴보겠습니다.
1 2 3 4 5 6 7 8 | void main(void); void main(int argc); void main(int argc, char *argv[]); void main(int argc, char *argv[],char env[]); int main(void); int main(int argc); int main(int argc, char *argv[]); int main(int argc, char *argv[],char env[]); | cs |
이중에서 자주 사용되는 형식은 5번쨰 줄과 7번쨰 줄이며 가장 완벽한 원형은 8번입니다. 이원형에서 리턴값과 인수들의 의미를 알아보겠습니다.
::리턴값
main 함수가 리턴하는 값을 탈출 코드(Exit Code)라고 하는데 프로그램이 실행을 마치고 운영체제로 복귀할때 리턴되는 값입니다. 함수가 작업 결과를 호출원으로 돌려주듯이 응용 프로그램도 작업결과를 리턴할 수 있습니다. main함수가 프로그램 그 자체이므로 main 함수의 리턴값이 곧 프로그램의 리턴값이 됩니다. 탈출 코드는 보통 사용되지 않고 무시되는데 이 프로그램을 호출한 프로그램(보통 쉘)이 꼭 필요할 경우 탈출 코드를 사용하기도 합니다. 예를 들어 도스의 배치파일(*.bat)내에서 응용 프로그램을 실행했을 때 이프로그램의 실행결과를 ERRORLEVEL이라는 환경 변수로 참조할 수 있습니다. 32비트 환경에서는 탈출 코드 외에도 응용프로그램간의 통신을 위한 장치가 많이 준비되어 있어서 요즘은 main의 리턴 값을 잘 사용하지 않습니다. 만약 main함수가 int형을 리턴할 때 , 즉 원형을 int main() 으로 했을 때는 main의 끝에 반드시 return 문이 있어야 하며 그렇지 않으면 리턴값이 없다는 경고가 발생합니다. 다른 함수들은 값을 리턴하지 않을 경우 에러로 처리되지만 main함수만큼은 경고로 처리한다는 면에서 컴파일러가 main을 조금 다르게 취급함을 알 수 있습니다.
1 2 3 4 5 6 7 8 | int main() { }//경고로 처리 int function() { }//에러로 처리 | cs |
C++ 표준에서는 main이 값을 리턴하지 않을 경우를 인정하고 있으므로 main은 설사 리턴 타입이 int이라더라도 return 문을 생략할 수 있습니다.
::argc
운영체제가 이 프로그램을 실행했을 때 전달되는 인수의 개수입니다. 함수를 호출할 때 인수를 전달하듯이 이 운영체제가 프로그램을 호출할 때도 인수를 전달할 수 있습니다. 만약 파일끼리 copy.exe라는 프로그램을 작성했다고 하면 복사 원본의 파일 이름과 복사 대상 파일 이름을 다음과 같이 인수로 받아들일 것입니다.
1 | copy file1.txt file2.txt | cs |
이때 실행 파일의 이름인 copy뒤에 file1.txt와 file2.txt가 프로그램의 인수입니다. 이 인수들은 운영체제가 프로그램으로 전달하는 작업거리, 즉 어떤 일을 하라는 정보입니다. main함수가 프로그램의 시작점이기 떄문에 프로그램의 인수가 main함수로 전달됩니다. argc인수는 main 함수로 전달된 인수의 개수입니다. 첫 번쨰 인수는 실행 파일명으로 고정되어 있으므로 argc는 항상 1보다 큽니다. copy a b식으로 호출될 경우는 argc는 3이 됩니다. 이 값은 인수를 필요하는 프로그램에서 인수가 제대로 입력되었는지 검사할 때 사용합니다. 인수가 없으면 실행할 수 없는 경우 이 값을 조사해서 인수가 제대로 전달되었는지 호가인합니다. 만약 필요한 인수가 없다거나 또는 남는다면 에러 메시지를 출력하고 프로그램을 끝내거나 아니면 디폴트를 취해야 할 것입니다. 예를 들어 copy.exe는 복사 원본과 복사 대상 파일이 반드시 전달되어야 하므로 argc가 3 미만 일 경우 "복사할 파일을 입력하세요"라는 에러 메세지를 출력하면 될 것 입니다.
::argv
프로그램으로 전달된 실제 인수값이며 이 값을 읽으며 운영체제로부터 언떤 인수가 전달되었는지 알 수 있습니다. 운영체제가 프로그램을 실행할 때는 항상 문자열 형태의 쉘 명령을 입력하기 때문에 인수의 타입은 항상 문자열일 수박에 없습니다. file1.txt라고 하든 123이라고 하든 명령행에서는 정수 또는 실수라는 것은 없으므로 입력된 되면 그대로 문자열 인수가 전달되게 됩니다. 만약 전달된 인수가 정수라면 일단 문자열로 받아서 atoi 등의 변환 함수로 정수로 바꿔 사용해야 합니다. 이런 문자열 인수가 한꺼번에 여러 개 전달될 수 있습니다. copy file1.txt file2.txt의 경우 copy , file1.txt , file2.txt 세 개의 문자열이 인수로 전달됩니다. 그래서 argv는 문자형 포인터를 가리키는 문자열 배열 입니다. 원형에는 char * agrv[]로 되어 있는데 이 표현은 char **agrv와 같습니다. argv[0]는 항상 프로그램의 이름이 전달되는데 통상 완전 경로라고 보면 됩니다. 정확히 말해 쉘이 이 프로그램을 실행할 때 입력한 실행 파일명인데 명령행에서 실행 파일명만 입력했으면 경로는 포함 되지 않을 수도 있습니다. C:\program이라는 경로에 copy.exe라는 프로그램을 실행했다면 argv[0]는 "C:\Program\copy.exe"입니다. 이 값은 우리가 원하는 인수, 즉 프로그램의 작업거리라고는 할 수 없어 잘 사용되지 않습니다. 프로그램으로 전달된 첫 번째 인수는 argv[1]이며 두번째 인수부터 argv[2], argv[3]식으로 전달됩니다.
'Programming > C' 카테고리의 다른 글
C언어 - 구조체(Structure)(1/2) (1) | 2015.08.20 |
---|---|
C언어 - 포인터(Pointer)(3/3) (0) | 2015.08.19 |
C언어 - 포인터(Pointer)(1/3) (2) | 2015.08.16 |
C언어 - 배열(Array)(2/2) (0) | 2015.08.15 |
C언어 - 배열(Array)(1/2) (0) | 2015.08.13 |