a, 'a', "a" 의 차이



a 는 변수입니다.

a 라는 변수는 형에 따라서 다양한 값을 입력시킬 수 있습니다.

int a;
a = 5;
a = 'a';
a = '7';


'a' 는 문자 상수입니다. 상수이기 때문에 'a' 는 변하지 않습니다.

시스템 내부적으로 'a' 는 ASCII 수치로 변환되어 처리됩니다. ( 'a' 의 ASCII 의 값은 97 입니다. )


"a" 는 문자열 입니다.

문자열은 반드시 널 문자로 끝나도록 정해져 있으므로 "a" 도 역시 '\0' 를 포함하고 있습니다.




문자열 포인터 변수



문자열 포인터 변수는 포인터 변수이기 때문에 변수에 저장되는 값은 주소가 됩니다.

이 주소가 가리키는 대상체가 문자열일 때 이것을 문자열 포인터 변수라고 지칭합니다.

정확히는 문자열이 저장된 곳의 가장 첫번째 문자의 위치를 가리킵니다.


char *str;
str = "mozi";

str 에 'm' 가 저장되는 과정을 그림으로 나타냈습니다.

str 이라는 변수가 할당(4 byte) 되고 "mozi" 를 위한 공간이 할당되며 'm' 이 위치한 주소를 str 이 가리키고 있습니다.

즉, str 에는 'm' 이 저장된 곳의 주소가 할당됩니다.



포인터를 이용한 문자열 조작



이전의 포인터와 관련된 포스팅을 보고나면 아래의 소스가 이해가 되실거라 믿습니다.

main()
{
    char *str;
    str = "mozi";

    pirintf("%c\n", *(str));        // m
    pirintf("%c\n", *(str + 1));    // o
    pirintf("%c\n", *(str + 2));    // z
    pirintf("%c\n", *(str + 3));    // i

    pirintf("%s\n", str);           // mozi
    pirintf("%s\n", str + 1);       // ozi

    puts(str)                       // mozi
}

포인터에서 잠깐 일탈하면 printf 보다는 puts 가 메모리를 적게 잡아먹는 함수이기 때문에 가능하면 puts 를 애용합시다. :-)



착각하기 쉬운 첨자



포인터 변수를 쓰고 배열처럼 사용이 가능할까? 결론부터 말하면 가능합니다.

사실 배열은 아니지만 배열처럼 사용할 뿐입니다.


str[2] 는 배열이 아닙니다. 배열처럼 보일 뿐입니다.

str[2] 의 의미는 str 이 가리키는 번지에서 3번째 (0 부터 시작하므로) 문자를 지칭하고 기리키고 있는 곳의 값까지를 의미합니다.

main()
{
    char *str;
    str = "mozi";

    printf("%s\n", &str[2]);        // zi
}

결국 위의 의미는 아래와 다 동일합니다.

*(str + 2) == str[w] == *(2 + str) == 2[str]



*string 과 string[] 의 차이



아래 2개는 어떤 차이가 있을까?

차이를 설명할 수 있을까?

main()
{
    char *str = "mozi";
    char str2[] = "mozi";
}

차이를 그림으로 그려보면 다음과 같습니다.

주소값을 동일하게 쓰긴 했지만, 이 부분은 살짝 무시해주고 위의 메모리 구조와 아래의 메모리 구조를 보면 차이점이 느껴지실 겁니다.

위는, mozi 가 어딘가에 저장되고 'm' 의 주소가 str 에 할당됩니다. str 의 변수와 mozi 의 문자열은 별개로 존재합니다.

아래는, 배열입니다. mozi 라는 문자열이 str2 배열에 할당됩니다.



포인터 배열에 문자열 할당



포인터도 마찬가지로 배열이 될 수 있고, 이 배열에 문자열들을 넣을 수 있습니다.

무슨말이냐? 소스로 보겠습니다.

main()
{
    char *str[2];

    str[0] = "mozi";
    str[1] = "tistory";
}

위의 소스를 메모리 구조로 그려보면 다음과 같습니다.



이렇게 써도 str[0] 과 str[1] 에는 의도하는 문자열이 들어가게 됩니다.

그러나 위와 차이점이 있습니다. 아래의 소스는 위의 소스보다 공간상에서 단점이 심한데요.

한 번 생각해 보도록 합시다.

main()
{
    char str[2][10];

    strcpy(str[0], "Mozi");
    strcpy(str[1], "Tistory");
}


블로그 이미지

사용자 꽁담

들어가기 앞서



아래의 소스를 읽기만 하고 넘어가겠습니다.

main() { int i, j; int mozi[2][2] = { {0, 0}, {1, 1} }; int (*tistory)[2]; tistory = mozi; for(i=0; i<2; i++){ for(j=0; j<2; j++){ printf("%d\n", *(*(tistory + i) + j)); // 포인터 연산만 이용한 계산

printf("%d\n", *(tistory[i] + j)); // 부분 첨자를 이용한 계산

printf("%d\n", tistory[i][j]); // 첨자를 이용한 계산

printf("%d\n", mozi[i][j]); // 비열을 이용한 계산

}

} }



2차원 배열과 포인터



int *tistory; 라면 mozi 는 1차원 배열이어야 하므로 맞지 않습니다.

int **tistory; 라면 1차원 포인터 배열에 대한 변수이므로 이 역시 맞지 않습니다.

** 는 나중에 다루기로 하고 2차원 포인터는 어떻게 연결시켜야 할까요?

int *tistory; // 1차원 배열 포인터 int *(tistory)[2] // 2차원 배열 포인터

int *(tistory)[2][2] // 3차원 배열 포인터

*(tistory)[2] 와 *tistory[2] 는 어떻게 다를까? 는 포스팅 하단에서 다루겠습니다.


할당된 메모리는 3개의 변수 모두 4 바이트만 할당되며 주소 값을 지정할 수 있습니다.

1차원 배열 포인트는 4바이트가 할당, 2차원 배열 포인터는 8바이트가 할당이 아닙니다.

포인터변수 이므로 무조건 4바이트가 할당이며, 단지 가리키는 대상체의 차원에 따라 정의하는 형식만 달라집니다.


2차원 배열과 2차원 배열 포인터를 연결하는 방법입니다.

int mozi[2][2]; int *(tistory)[2]; tistory = mozi;

만약 ( ) 를 안붙이고 아래처럼 하면 어떻게 될까요?

컴파일 자체가 되지 않습니다. 두 개의 차이점을 그림으로 비교해보겠습니다.

int mozi[2][2];

int *tistory[2]; tistory = mozi;


위의 *(tistory)[2] 는 2차원배열 포인터 변수를 정의한 포인터 주소를 담을 수 있는 4바이트만 할당됩니다.

반면 *tistory[2] 는 4바이트 주소를 담을수 있는 포인터주소 공간이 2개 즉 8바이트가 할당됩니다.


각 변수들이 메모리에 할당된 크기를 알아봅니다.

int mozi[2][2];

int (*tistory)[2]; sizeof(mozi); sizeof(tistory); sizeof(*tistory);

mozi : mozi[2][2] 를 대표하는 모 배열입니다. 따라서 [0][0] ~ [2][2] 까지 할당된 모든 메모리 영역에 대한 크기가 출력됩니다.

tistory : 포인터변수이므로 무조건 4 라는 수치가 출력됩니다.

*tistory : tistory 가 가리키는 대상체를 뜻합니다. 첨자에 사용된 2라는 수치가 대상체의 개수([0][0], [0][1])를 나타내므로 4 * 2 = 8 이 출력됩니다.



2차원 포인터 다루기


배열과 포인트의 매칭으로 설명을 대체하겠습니다.

int mozi[2][2] = {{3, 5}, {12, 54}};

int (*tistory)[2]; tistory // mozi tistory + 1 // mozi + 1 tistory + 2 // mozi + 2 *tistory // mozi[0] *(tistory + 1) // mozi[1] *(tistory + 2) // mozi[2] **tistory // mozi[0][0] 선언의 ** 와 사용의 ** 을 착각하시면 안됩니다. *(*tistory + 1) // mozi[0][1] *(*(tistory + 1)) // mozi[1][0] *(*(tistory + 1) + 1) // mozi [1][1]


배열 포인터에서 별표는 배열에서 대괄호를 이용한 첨자들을 사용한 것과 같습니다.

블로그 이미지

사용자 꽁담

1차원 배열과 초기화



아래의 프로그램을 보면

배열명을 사용하기 전에 이 배열이 어떠한 값을 저장할 수 있는지를 지정하는 타입이 있고,

배열의 첨자를 이용하여 배열의 개수를 지정한다는 것을 알 수 있습니다.

C 에서 배열의 첨자는 반드시 0 부터 시작합니다.

그러므로 정의할 때의 첨자 수와 사용할때의 첨자는 -1의 오차를 보이게 됩니다.

즉 아래 프로그램에서  mozi[2] 는 사용해서는 안됩니다.

main() { int mozi[2]; printf("%d %d\n", mozi[0], mozi[1]); }


배열의 초기화

위의 프로그램에서 배열은 초기화를 하지 않았습니다.

초기화를 하지 않았기 때문에 어떠한 값이 들어가 있는지 아무도 모르며, 아무값이나 출력됩니다.

아무값이나 출력되지 않게 하기 위해서는 초기화를 해줘야 합니다.

main()

{

int mozi[2];

mozi[0] = 0;

printf("%d %d\n", mozi[0], mozi[1]);

}

이 프로그램은 mozi[0] 에 대해서만 초기화를 하였기 때문에, mozi[1] 에 대해서는 여전히 쓰레기 값이 들어가있습니다.

배열 전체를 초기화 하기위해서는 아래처럼 해주어야 합니다.

초기화 방법 1은 너무 무식한 방법이므로, 초기화 방법 2나 3을 사용하도록 합니다.

초기화 방법 1 mozi[0] = 0; mozi[1] = 0; 초기화 방법 2 for(i = 0; i < 2; i++) mozi[i] = 0; 초기화 방법 3 int mozi[3] = {0};



첨자 생략



배열의 초기값을 다음과 같이 설정했다고 가정합니다.

int mozi[2] = {4, 3, 7};

배열의 첨자는 2 로 지정하고, 초기치의 개수는 3 개 입니다.

이 정의는 경고를 발생시키지만 에러로 처리시키지는 않습니다.

컴파일 하고 실행하면 mozi[0] 은 4 mozi[1] 은 3 으로 잘 출력됩니다.

그러나 mozi[2] 에 대해서는 컴파일러가 메모리 할당을 하지 않기 때문에 7 이라는 값이 출력되지 않습니다.


이러한 실수를 방지하는 방법은 배열선언시 첨자의 길이를 생략하는 것입니다.

int mozi[] = {4, 3, 7};

위 처럼 작성하는 경우 컴파일러는 초기치의 개수를 세고나서 이를 첨자에 반영합니다.

따라서 mozi 의 첨자는 자동으로 3 이 되고 메모리는 12 바이트가 할당될 것 입니다.



다차원 배열



다차원 배열은 배열의 배열로 생각하면 쉽습니다.

int mozi[2][3]; printf("%d %d %d %d %d %d\n", mozi[0][0], mozi[0][1], mozi[0][2], mozi[1][0], mozi[1][1], mozi[1][2]);

마찬가지로 모두 쓰레기 값이 출력됩니다.


1차원 배열을 초기화 했듯이, 2차원 배열도 초기화를 해줍니다.

int mozi[2][3] = {0};

만약 0으로 초기화 하지 않고 다른 값으로 초기화 하고 싶은 경우 아래처럼 진행합니다.

int mozi[2][3] = { {1, 2, 3}, {4, 5, 6} };



1차원 배열과 포인터



그렇다면 배열을 포인터와 어떻게 결합할 건지 보기로 해봅니다.

main() { int mozi[2] = {1, 2}; int *tistory; tistory = mozi; printf("%d\n", *tistory); }

소스를 보면 tistory는 mozi 변수에 대해 & 기호를 사용하지 않고 바로 사용했습니다. 왜그럴까요?

배열명은 배열의 첫번째 배열 요소의 주소를 뜻합니다.

mozi[0], mozi[1] 배열 요소들은 메모리의 어딘가에 할당되어 있을 것이고, &mozi[0], &mozi[1] 로 알 수 있습니다.

그런데 배열명은 배열의 첫번째 배열 주소를 가지고 있다고 했으므로, 다음의 식이 성립합니다.

mozi == &mozi[0]

결국 mozi 라는 배열명 자체가 주소 값을 뜻하기 때문에 이 주소 값을 tistory 라는 포인터 변수에 넣을 수 있는 겁니다.

같은의미이지만 달리말하면 tistory 는 mozi[0] 을 가리키고 있다고 할 수 있습니다.

그렇다면 mozi[1] 을 얻기위해선 어떻게 해야할까?



포인터 + 정수의 의미



아래의 프로그램의 tistory + 1 을 봅니다. 이 수식은 어떤걸 의미할까요?

tistory 가 정수형 변수라면 "정수 + 1" 이 되겠지만, 포인터 변수이므로 주소가 들어가 있으며 이 주소에 1을 더한다는 의미입니다.

int mozi[2] = {0, 1}; int *tistory; tistory = mozi; tistory + 1;

여기에서 1을 더한다는 의미는

tistory 의 주소가 0x22334455 인 경우 +1 증가한 0x22334456 가 아닙니다.

tistory 의 주소에 포인터 변수의 타입 크기만큼 증가한다는 것 입니다. 정수형 변수의 크기는 4 byte 이므로 0x22334459 가 됩니다.

mozi[0] = 0; // 0x8047dlc mozi[1] = 1; // 0x8047d20 tistory + 0; // 0x8047dlc tistory + 1; // 0x8047d20


그렇다면 정수를 더할 때 괄호의 중요성를 알아보도록 합니다.

int mozi[2] = {0, 3}; int *tistory; tistory = mozi; printf("%d %d\n", *(tistory + 1), *tistory + 1);

결과는 어떻게 출력될까요? 괄호 없이 계산시 * 연산자는 + 보다 순위가 높습니다.

따라서, *(tistory + 1) 은 mozi 시작주소에서 4 byte 를 더한 주소에 있는 값을 출력하므로 3 이라는 값이 출력됩니다.

*tistory + 1 은 mozi 의 시작주소에 있는 값을 가지고 온 후에 1 을 더하므로 1 이라는 값이 출력됩니다.

블로그 이미지

사용자 꽁담

포인터란 무엇인가?



포인터는 번지에 대한 기호화된 표현을 의미합니다.

즉, 포인터는 번지입니다.


이해가 잘 안될 수 있으므로,

int mozi; 라는 변수를 정의합니다. 시스템은 정의를 만나는 순간 mozi 변수에 대해 4 byte 의 메모리를 할당해줍니다.

시스템이 할당해준 4 byte 는 다른 프로세스들이 할당받지 못하며, 각 byte 는 모두 이름을 가지는데 이를 번지라고 합니다.


그림으로 표현하면 다음과 같습니다.


결국 포인터라는 것은 메모리의 위치를 표현한 기호를 의미합니다.



포인터 변수



포인터는 메모리의 특정 위치를 가리킨다고 했습니다.

그렇다면 포인터 변수는 무엇을 의미할까요?

포인터 변수는 포인터를 저장할 수 있는 변수를 말합니다.

이 말은, 메모리의 특정 위치를 저장한다는 말과도 동일합니다.


즉, 포인터 변수에는 상수 값이나 문자, 문자열이 들어가는 것이 아니라 번지가 들어가는 것 입니다.

포인터 변수는 번지 이외에는 어떤 것도 들어갈 수 없다는 것을 꼭 기억해주세요.



포인터 변수 정의



포인터 변수를 정의하는 방법입니다.

int *mozi;

mozi 라는 변수에 * 를 붙였습니다.


포인터 변수를 정의하는 방법은 위처럼 매우 간단합니다.

이것을 해석하면 "mozi 는 정수가 저장된 곳의 위치를 카리킬 수 있다" 라는 말입니다.


그러므로 다음과 같은 식은 성립할 수 없게됩니다.

mozi = 5;
mozi = 'a';


포인터 변수를 정의했으므로 시스템은 mozi 에 대해서 아래와 같이 할당을 해줍니다.



그렇다면 아래의 소스에서 각각의 값들은 어떻게 출력이 될까?

main() { int *mozi; printf("%p\n", mozi); printf("%p\n", &mozi); }

첫 번째 printf 의 경우, mozi 에 저장된 값을 출력하라는 의미입니다.

mozi 에 저장된 값은 현재 초기화가 되어있지 않은 상태이므로 쓰레기 값이 들어가 있으며, 매번 실행할 때마다 값이 바뀝니다.

값이 바뀐다고 해서 그 값이 의미가 있는게 아닙니다.


두 번째 printf 의 경우, mozi 에 할당된 첫 번째 메모리의 주소를 출력하라는 의미입니다.



& * 연산자



포인터 변수를 정의했으므로 포인터 변수에 값을 할당해봅니다.

위에서 말한것처럼 포인터 변수에는 번지 이외에는 어떤 값도 들어갈 수 없습니다.


그렇다면 포인터 변수에 번지를 저장하기 위해서는 어떻게 해야할까?

포인터 변수에 번지를 저장하기 위해서 마련된 것이 & 연산자 입니다.

int mozi;
&mozi;

mozi 는 4 바이트의 정수를 저장할 수 있는 변수입니다.

& 연산자에 의해서 &mozi 는 mozi 가 저장된 곳의 위치를 뜻하게 됩니다.

main()
{
    int mozi = 5;
    printf("mozi %d\n", mozi);
    printf("&mozi %010x\n", &mozi);
}

mozi 5
&mozi 0x8047d24

위의 소스는 mozi 정수형 변수에는 5라는 값과 5 가 저장된 곳의 메모리 번지는 0x8047d24 라는 것을 알 수 있습니다.


* 연산자는 & 의 반대의 뜻이 됩니다.

즉, 포인터 변수에 저장된 선두 번지를 참조하여 해당하는 장소에 저장된 값을 읽어옵니다.


포인터 변수에 번지 할당



main() { int mozi = 5; int *tistory; // 좌측 그림 tistory = &mozi; // 우측 그림 구조로 변경되는 순간 printf("*tistory %d\n", *tistory); }

mozi 4 byte 정수형 변수 메모리 어딘가에 할당하고 그곳에 5 라는 값을 저장합니다.

5 라는 값이 저장된 곳의 위치를 tistory 에 저장합니다.

마지막으로 * 연산자를 이용하여 tistory 가 가리키고 있는 곳의 값을 출력합니다.




Segmentation Fault



포인트를 사용하다보면 Segmentation Fault 라는 오류를 발생하는 경우를 많이 경험해볼듯 합니다.

이 에러는 해당 번지를 시스템에서 이미 사용하고 있는 경우, 수행이 되려고 하는 프로그램을 중단하며 중단할 때 발생되는 에러입니다.


main()
{
    int *mozi;
    *mozi = 5;
}

위 소스의 의미는 mozi 가 가르키는 곳에 5라는 값을 할당하겠다 라는 의미입니다.

중요한 점이 있습니다. mozi 가 가르키는 곳이 명확하게 정의되어 있어야 한다는 겁니다.


이 소스는 운이 좋다면 Segmentation Fault, 운이 나쁘다면 해당 에러가 발생하지 않을 수 있습니다.

왜 에러가 발생하는데 운이 좋다고 하는걸까?


mozi 는 번지를 저장할 수 있는 변수입니다.

현재 이 번지는 사용자가 초기화 해 준 것이 아니라 변수를 생성할 때 자동으로 들어간 알 수 없는 즉, 쓰레기 값이 들어있는 것 입니다.

만약 쓰레기 값의 번지가 다른 프로그램에 의해 이미 사용되고 있는 상태면 어떻게 될까요?

같은 장소의 메모리를 사용하려고 하다보니 충돌나는 프로그램은 엉망이 되고, 시스템 장애로 발생되게 됩니다.

당연히 시스템은 이 행위를 막으려고 할 것이고 이 때 발생되는 에러메세지가 Segmentation Fault 입니다.

이 에러가 발생하는 경우, "이 프로그램이 다른 프로그램의 메모리 영역을 침범하는구나" 라고 생각하시면 됩니다.


그러므로 이 치명적인 에러를 조기에 발견했다는 점에서 운이 좋다고 하는거였습니다.


위의 소스를 고치려면 어떻게 할까?

mozi 의 변수를 초기화 해준 뒤, 사용합니다.

main()
{
    int tistory;
    int *mozi;

    mozi = &tistory;
    *mozi = 5;
}



항상 기억합시다.

포인터 변수는 번지를 초기화(할당) 해주어야 합니다.

블로그 이미지

사용자 꽁담

Pointer

주소값을 가지는 변수입니다.




주소값이란

모든 변수는 주소값을 가지며, & 를 사용하여 조회할 수 있습니다.

다음 소스는 p 의 변수 값과, p 가 할당된 메모리 주소를 조회합니다.

#include <stdio.h>

main()
{
        int p = 0;
        printf("p value = %d, p memory address %p\n", p, &p);

        return 0;
}
p value = 0, p memory address 0x7ffc317fdcdc

위의 소스를 수행할 때 memory 상의 0x7ffc317fdcdc 주소에 0 이 저장되어 있습니다.




포인터를 선언하는 방법

변수명 앞에 * 를 사용하여 포인터 변수를 선언합니다.

Valuetype *valuename;


int 형 포인트 변수 p 를 선언하였습니다. 이후에 포인터 변수 p 에 3을 대입하였습니다.

실행이될까요? 실행되지 않습니다.

#include <stdio.h>

main()
{
        int *p;
        *p = 3;

        printf("p memory address %p\n", p);
        printf("p value = %d\n", *p);

        return 0;
}

이유는 포인터 변수 p 를 선언만 하고 초기화해주지 않아 주소가 쓰레기 값으로 되며, 포인터 변수가 어디를 가르킬지 알 수 없기 때문입니다.

따라서 위의 코드는 매우 치명적이며, 절대로 이렇게 사용해서는 안됩니다.


그렇다면 포인터 변수 p 는 어떻게 초기화 해야할까요? = 0 혹은 = NULL 로 초기화를 할 수 있습니다.

아래 소스는 p, q 포인터 변수는 아무것도 가르키지 않는다는 의미입니다.

#include <stdio.h>

main()
{
        int *p = 0;
        int *q = NULL;

        return 0;
}




포인터 사용 예제

1. int 형 x 를 선언한 뒤 0 의 값을 대입해 줍니다.

2. point 형 변수 p 에 x 의 주소값을 대입합니다.

   x 와 p 는 동일한 주소를 가르키며, 주소가 동일하기 때문에 하나의 변수 값만 바꿔도 둘 다 변경됩니다.

#include <stdio.h>

main()
{
        int x = 0;
        int *p = &x;

        printf("x memory address %p\n", &x);
        printf("p memory address %p\n", p);
        printf("x value = %d, p value = %d\n", x, *p);

        x = 1;
        printf("x memory address %p\n", &x);
        printf("p memory address %p\n", p);
        printf("x value = %d, p value = %d\n", x, *p);

        return 0;
}
x memory address 0x7ffdce9a6494
p memory address 0x7ffdce9a6494
x value = 0, p value = 0
x memory address 0x7ffdce9a6494
p memory address 0x7ffdce9a6494
x value = 1, p value = 1




정리를 해봅니다.


일반변수와 포인터 변수의, 메모리 주소를 가져오고 싶은 경우 아래와 같은 방법으로 사용합니다.

포인터 변수는 포인터 변수명으로

일반 변수는 &일반 변수명으로


일반변수와 포인터 변수의, 값을 가져오고 싶은 경우 아래와 같은 방법으로 사용합니다.

포인터 변수는 *포인터 변수명으로

일반 변수는 일반 변수명으로



포인터 변수에 일반 변수를 대입하고 싶은 경우 아래와 같은 방법으로 사용합니다.

포인터 변수 = &일반변수


블로그 이미지

사용자 꽁담