관리 메뉴

Kim's Programming

C언어 - 포인터(Pointer)(3/3) 본문

Programming/C

C언어 - 포인터(Pointer)(3/3)

Programmer. 2015. 8. 19. 23:08

::env


운영체제의 환경 변수를 알려 줍니다. 환경 변수는 운영체제마다 다르게 정의하는데 DOS의 경우 Path, Prompt등이 있고 윈도우즈의 경우는 컴퓨터 이름, 시스템 디렉토리 등의 정보들이 있습니다. 응용 프로그램에게 자신이 실행되는 환경을 알 수 있도록 해 준다는 의도로 전달되는 인수이지만 이 인수가 아니더라도 환경 변수를 조사할 다른 방법이 있기 때문에 실질적으로 사용되지는 않습니다. 다음 소스는 main함수로 전달되는 인수를 화면으로 출력하기만 합니다. 명령행에서 입력한 인수가 어떻게 프로그램으로 전달되는지를 볼 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
 
void main(int argc, char *argv[],char *env[])
{
    int i = 0;
 
    printf("전달된 인수의 개수 = %d\n", argc);
    for (i = 0; i < argc; i++)
    {
        printf("%d번째 인수 = %s\n", i, argv[i]);
    }
}
cs
여기서도 인수를 지정할 수는 있지만 직접 프로프트를 통해서 인수는 지정해 보겠습니다. 인수를 넣게되면 결과는 다음과 같이 나오게됩니다.

전달된 인수의 개수를 보여주고 입력 된 인수들을 순서대로 출력하였습니다. 또 다른 것으로 인수를 받아 작동하는 계산기를 만들어보겠습니다.

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
35
36
37
38
39
40
41
42
#include<stdio.h>
#include<Windows.h>
void main(int argc, char *argv[])
{
    int num_1, num_2, result;
    char op;
 
    if (argc != 4)
    {
        printf("인수가 부족합니다. 다시 입력해주세요\n");
        exit(0);
    }
 
    num_1 = atoi(argv[1]);
    num_2 = atoi(argv[3]);
    op = atoi(argv[2]);
 
    switch (op)
    {
    case '+':
        result = num_1 + num_2;
        break;
 
    case '-':    
        result = num_1 + num_2;
        break;
 
    case'*':
        result = num_1 *num_2;
        break;
        
    case'/':
        result = num_1 / num_2;
        break;
    
    default:
        printf("사칙 연산으로 입력해 주세요\n");
        exit(0);
 
    }
    printf("결과 값 = %d", result);
}
cs

다음 소스를 실행 시키면 다음과 같습니다. 인수를 넣어줄때는 인수마다 띄어서 입력하여야합니다. 띄어주지 않으면 전체를 하나의 인수로 인식되어 정상적으로 작동 하지 않습니다.


인수가 4개가 아니거나 사칙연산이 아닌경우는 종료하도록 하였습니다. 일수가 4개일 경우엔 argv[1]과 argv[3]에 전달된 피연산자들을 구해서 num_1변수와 num_2변수에 대입시킵니다. 명령행에 전달된 인수는 모두 문자열로 되있기 때문에 atoi함수를 통해서 정수로 변경시켜 줍니다. 연산자는 중간의 argv[2]로 전달이 되게 되는데 모두 한 문자로 구성되있기 때문에 argv[2]의 첫 문자만 살펴보면 됩니다. 만약 연산자가 사칙연산이외의 연산자를 이용하면 오류를 출력합니다.


동적 문자열 배열


이미 알듯 배열을 선언할 때는 그 크기를 반드시 상수로 지정하여야 합니다. 그래서 다음과 같이 선언을 불가합니다.
1
2
int len = 지정값;
char name[len];
cs
여기서 len은 함수의 인수로 전달되었거나 또는 사용자로부터 입력된 값, 즉 실행 중에 결정되는 값이라고 생각하겠습니다. 컴파일러는 배열을 선언할 시점에 크기를 알아야 하므로 변수로 배열 크기를 지정할 수는 없으며 실행 중에 가변적인 크기의 배열을 생성하려면 동적 메모리 할당 함수인 malloc함수를 사용해야 합니다. 임의의 타입 T형의 요소 n개를 가지는 배열은 실행 중에 생성하는 코드는 다음과 같이 정리가 됩니다.
1
2
*p;
p=(T *)malloc(n*sizeof(T));
cs
총 할당크기는 개수 n에 T형 타입의 크기를 곱한 바이트 수이며 malloc 함수로 할당되는 메모리는 이름이 없기 때문에 시작 번지를 반드시 포인터 변수로 대입받아야 사용할 수 있습니다. 그래서 T형 포인터p를 선언하고 malloc이 리턴하는 번지를 대입하되 (T*)타입으로 캐스팅을 했습니다. 동적 할당의 결과 p는 T형 요소 n개를 가지는 배열처럼 사용할 수 있습니다. 이 공식(?)에 따라 크기 len의 문자형 배열을 생성하는 코드는 다음과 같이 작성할 수 있습니다.
1
2
3
4
5
int len;
scanf("%d",&len);
char *name;
name=(char *)malloc(len*sizeof(char));
free(name)
cs
len의 값은 실행 중에 주어지게 되며 malloc은 이 크기만큼 문자형 배열을 할당합니다. lne이 백만이든 1억이든 상관없이 할당해줍니다. 이 할당에 의해 name은 len의 크기를 가지는 문자형 배열이 되며 이는 곧 len-1개의 문자를 저장할 수 있는 문자열이 된다는 의미입니다. 이렇게도 되면 더 고차원의 코드도 작성이 가능합니다. 다음으론 임의의 크기의 문자열을 임의의 갯수로 만드는 소스입니다.
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
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<malloc.h>
void main()
{
    int len = 10, num = 5;
    char **name;
 
    name = (char**)malloc(num*sizeof(char*));
    for (int i = 0; i < num; i++)
    {
        name[i] = (char *)malloc(len*sizeof(char));
    }
 
    for (int i = 0; i < num; i++)
    {
        sprintf(name[i], "string %d", i);
        puts(name[i]);
    }
 
    for (int i = 0; i < num; i++)
    {
        free(name[i]);
    }
    free(name);
}
cs
len은 문자열의 길이이며 num은 이런 문자열의 개수인데 둘 다 변수이므로 실행 중에 사용자나 외부에서 주어지는 값입니다. 이 값은 경우에 따라 달라질 수 있는 값이기 떄문에 5나 10 이 아닌 다른 값을 가질 수 있습니다. 이 소스는 len 길이의 char *형을 요소로 가지는 num 크기의 배열을 동적으로 할당하는 것입니다. 요소의 타입은 char *이고 크기는 num이므로 공식대로 작성하게 되면 다음과 같은 코드로 요약되게 됩니다.
1
2
char **p;
p=(char *)malloc(num*sizeof(char *))
cs
이 할당에 의해 name은 num개의 char *형으로 구성된 배열이 되는데 malloc은 항당만 하지초기화는 하지 않으므로 name 배열은 쓰레기 값을 가지고 있을 것입니다. 이제 0~num까지 루프를 돌면서 name의 각 요소에 대해 다시 메모리를 할당하여 len 길이의 문자형 배열을 만들면 name은 2차원 문자형 배열이 되며 개념적으로 1차원 문자열 배열이라고 할 수 있습니다. 정적으로 선언한 변수는 name뿐이며 나머지는 모두 동적으로 할당된 것들입니다. name이 가리키는 곳에 크기 num의 char * 배열이 있고 이 배열의 요소들이 가리키는 곳에는 또 len길이의 char 배열이 있습니다. 역으로 말하면 char 배열은 *char 배열에 의해 참조되며 이런 char * 들의 집합을 name이 가리키고 있으니 name의 타입은 char **이 되어야 합니다. 각 문자들이 메모리상의 어디에 할당된지는 알기 어렵지만 name을 통해서 이 문자열의 시작번지를 가지는 배열을 찾을 수 있으며 이 배열로 부터 각 문자열에 접근할 수 있습니다. 이 경우 name은 동적으로 할당된 문자열에 접근할 수 있는 유일한 경로이며 그 자체는 개념적으로 문자열 배열의 자격을 가집니다. 동적으로 할당한 배열을 해제할 때는 name 배열의 각 요소가 가리키는 메모리 블록을 순서대로 해제 하고 마지막으로 name이 가리키는 배열을 해제합니다. 순서를 바꾸게 되면 하위 블록 번지를 잃어버리게 되어 일부 해제가 안될 것입니다. name 그 자체는 지역적으로 선언된 포인터 변수이므로 따로 해제할 필요가 없습니다.

void 이중 포인터

void **라는 타입이 void *타입을 가르키는 유도 타입이므로 void ** vpp;변수를 선언할 떄 cpp의 대상체는 임의의 대상체를 가리키는 void * 타입입니다. 비슷해 보이는 타입이지만 void *와는 다르게 가리키는 대상체의 타입이 void*로 정해져있고 대상체의 크기도 명확하게 알고 있습니다. 따라서 vpp는 void형 포인터에 적용되는 규칙 대신 일반 포인터의 규칙이 적용됩니다. 임의 타입의 포인터를 대입받을 수 없으며 반드시 void *형 변수의 번지만 대입 받을 수 있습니다. 또한 대상체가 정해져 있으므로 *연산자로 대상체를 읽거나 변경할 수 있고 ++, --, +n 등의 연산으로 앞 뒤 요소로 이동도 가능하고 같은 void ++타입끼리 대입, 비교, 뺄셈도 가능합니다. 다음 소스를 통해서 알아보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
 
void main()
{
    void *vp;
    void *av[5];
    void **vpp;
    int i, *pi=&i;
 
    vpp = &vp;    //가능
    vpp = av;    //가능
    vpp++;        //가능
    *vpp;        //가능
    vpp = &pi;    //불가능
    **vpp;        //불가능
}
cs

void **로 선언된 vpp가 대입받을 수 있는 값은 void *형 변수 vp의 번지, void *배열 av의 선두 번지 등입니다. int **ppi가 int *pi의 번지나 int *ar[5]의 선두 번지를 대입받을 수 있는 것과 같은 이치입니다. av배열의 각 요소는 void * 타입이므로 임의 타입 변수에 대한 변수를 가질 수 있습니다. 따라서 av 배열 요소가 가리키는 값을 읽으려면 반드시 캐스트 연산자가 있어야 하며 ++,--연산자도 직접적으로는 적용할 수 없습니다. 하지만 vpp가 가리키는 대상체는 void *로 타입이 정해져 있고 포인터는 부호 없는 4바이트 길이를 가지므로 vpp 자체를 증가하여 av의 다음 요소로 이동할 수 있으며 *vpp로 가리키는 요소의 값(이 경우 번지값)을 읽을 수도 있습니다.

'Programming > C' 카테고리의 다른 글

C언어 - 구조체(Structure)(2/2)  (23) 2015.08.21
C언어 - 구조체(Structure)(1/2)  (1) 2015.08.20
C언어 - 포인터(Pointer)(2/3)  (1) 2015.08.18
C언어 - 포인터(Pointer)(1/3)  (2) 2015.08.16
C언어 - 배열(Array)(2/2)  (0) 2015.08.15