C, 포인터, 메모리


그림이 잘 안보일 경우 확대해서 보세요




이번 챕터의 목표


두 수를 교환하는 swap 함수 만들기


c_pointer_goal_1

변수 x와 y는 코드에서 미리 설정해놓고,

main()함수 밖에 swap()함수를 만들어 사용합니다.




컴퓨터와 메모리


프로그래밍을 하다보면 변수를 만들어 사용합니다.

예를들어 int a = 7 이렇게 변수를 선언 및 정의하면

우리는 a라는 변수를 계속해서 사용할 수 있습니다.

그 말인 즉슨, 어딘가에 a라는 변수를 저장해두고 사용하고 있다는 말이 됩니다.

특별히 지정을 하지 않으면 보통 변수는 컴퓨터의 주요 부품 중 RAM에 저장됩니다.

ram

그런데 a는 int므로 4byte 밖에 안됩니다.

현재 사용하는 컴퓨터의 RAM이 4GB라고 하면,

방대한 4GB(4,294,967,296 byte)의 공간 중에서

사막의 모래 한 알만큼(4 byte)이 a를 위해 할당될 것입니다.

4gb_to_byte


만약 RAM에 별다른게 없다면, a를 다시 찾아서 쓰려면

사막(4GB)에서 바늘(a 변수)찾기를 해야합니다.


하지만 실제로는 메모리는 주소 체계를 사용합니다.

memory_picture

메모리에서 데이터 저장 기본 단위는 1byte 입니다.

위 그림에서 각각의 방이 1byte 라는 의미입니다.

그럼 4GB의 램에는 총 4,294,967,296개의 방이 있겠죠.

그리고 32비트 운영체제 기준으로 각각의 방은

0x00000000 ~ 0xFFFFFFFF 중 하나의 주소를 갖게 됩니다.

앞의 0x의 의미는 16진수 수라는 이야기 입니다.

그리고 하나하나의 숫자는 4개의 비트로 이루어져있습니다.

예를 들어 F는 1111 입니다. 즉, 한 숫자 당 4비트인데 8개 숫자이니 32비트입니다.

하나의 비트는 2가지 정보(0 과 1)를 나타낼 수 있으므로,

32개의 비트로 나타낼 수 있는 정보의 가짓수는 다음과 같습니다.

\[2^{32} = 4,294,967,296\]

즉, 32비트 운영체제에서는 4,294,967,296개의 주소를 식별할 수 있고,

4,294,967,296 byte 즉, 4GB까지 주소로 관리할 수 있습니다.

그러니 32비트 운영체제를 사용하는데 RAM은 8GB를 쓴다면

남은 4GB는 아예 못쓰게 됩니다.

windows_bit_max_memory

고성능 작업이나 게임을 하는 사람들에게 4GB의 용량은 많이 아쉬운 성능이기 때문에

그 이상의 용량을 사용하기 위해 64비트 운영체제의 점유율이 높아지고 있습니다.


그럼 int형 변수 a가 메모리에 저장이 됐을 때 램의 모습을 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
	int a = 7;
	printf("&a = %p\n", &a);

	return 0;
}

참고로 &a는 변수 a의 주소를 반환하는 연산자입니다.

%p는 주소를 16진수로 나타내라는 의미입니다.

이 코드의 결과는 다음과 같습니다.

c_address_1

a라는 변수는 램에서 006FF76C 에 저장되어 있습니다.

정확히 말하면 시작 주소가 006FF76C라는 것입니다.

주소는 아마 99%의 확률로 저와는 다르게 나타날 것입니다.

컴퓨터의 그때그때 환경에 따라서 운영체제가 알아서 값을 저장하기 때문입니다.

그림으로 살펴보겠습니다.

c_memory_pic_1

아까 메모리의 기본 저장 단위는 1byte라고 했었죠.

1byte는 8bit므로 각각의 방엔 8개의 비트가 저장된다고 볼 수 있습니다.

0x00000000부터 0xFFFFFFFF까지의 주소 중에서

0x006FF76C부터 시작해 4개의 방(4바이트)의 데이터가 나타내는 값은 7입니다.

자료형을 int로 지정했기 때문에 시작 주소부터 4바이트의 데이터를 한꺼번에 보게 됩니다.


사실 사람이 읽는 방식이라면 7의 값을 나타내기 위해서는 다음과 같아야 합니다.

00000000 00000000 00000000 00000111

그런데 c에서 지역변수를 메모리에 할당할 때는 메모리의 끝에서부터 할당합니다.

이는 지역변수가 메모리의 여러 부분 중 STACK 영역에 할당되는데

이 STACK 영역을 얼마나 사용할지 모르기 때문에 끝에서부터 할당하는 것입니다.

메모리의 구조를 간단히 살펴보면 다음과 같습니다.

c_memory_structure

즉, 여기서는 0x006FF76F부터 4개의 방을 거꾸로 읽으면 되겠습니다.

포인터를 학습하는데 중요한 내용은 아니니 일단은 넘어가겠습니다.




포인터 변수


보통 우리가 변수를 선언해서 사용할 때는

변수이름과 해당 정보가 저장된 메모리의 주소가 매칭이 되어있기 때문에

특정 변수가 램의 어느 주소에 저장된지 몰라도

그냥 변수 이름을 통해서(한다리 건너서, 간접적으로) 원하는 데이터에 접근할 수 있었습니다.

그런데 포인터를 사용한다는 것은 직접 변수가 저장된 주소를 통해 접근한다는 의미입니다.

포인터는 어떤 변수의 데이터가 저장된 메모리의 주소를 의미하고,

포인터 변수는 변수가 저장된 곳의 시작 주소, 즉 포인터를 저장하는 변수입니다.

간단한 예제 코드를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
	// int 형식 변수 선언 및 정의(직접지정)
	int x = 10;

	// 포인터 변수 정의
	int *pnData;
	// int 변수 x가 저장된 메모리의 주소를 pnData에 대입
	pnData = &x;

	printf("x: %d\n", x);
	printf("pnData: %p\n", pnData);
	printf("*pnData: %d\n", *pnData);
	printf("&x: %p\n\n", &x);
	

	// pnData 포인터 변수가 가리키는 메모리 주소에 저장된 값을 20으로 변경
	*pnData = 20;

	printf("x: %d\n", x);
	printf("pnData: %p\n", pnData);
	printf("*pnData: %d\n", *pnData);
	printf("&x: %p\n\n", &x);
	return 0;
}

출력결과는 다음과 같습니다.

c_pointer_ex_1

주석으로 포인터 변수를 정의하고 선언하는 방법을 설명해놨습니다.

pnData를 %p로 출력한다는 것은 x의 주소값을 16진수로 출력하겠다는 의미입니다.

*pnData를 하면 pnData에 저장된 주소의 데이터에 접근할 수 있습니다.

여기서는 그 데이터값은 x의 값이죠.

그리고 여기서 눈여겨 봐야할 점은 pnData의 출력값과 &x의 출력값이 같다는 것입니다.

즉, pnData는 x의 주소값을 저장하는 변수다 라는 걸 알 수 있습니다.




포인터와 배열


배열의 이름은 해당 배열이 저장된 메모리의 시작 주소입니다.

아까 & 연산자도 해당 변수의 시작 주소라고 했었죠.

즉, 이 시작 주소를 저장하는 변수인 포인터 변수는 배열과 똑같이 생각할 수 있습니다.

다음 예제를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
	int aList[5] = { 0 };
	// 배열의 이름은 0번째 요소에 대한 '주소 상수'
	int *pData = aList;

	printf("aList: %p\n", aList);
	printf("pData: %p\n\n", pData);

	printf("aList[0]: %d\n", aList[0]);
	printf("pData[0]: %d\n\n", pData[0]);

	*pData = 20;
	printf("aList[0]: %d\n", aList[0]);
	printf("pData[0]: %d\n\n", pData[0]);

	*(pData + 1) = 30;
	printf("aList[1]: %d\n", aList[1]);
	printf("pData[1]: %d\n\n", pData[1]);

	int n = 7;
	int *pN = &n;
	printf("pN[0]: %d\n", pN[0]);
	printf("*pN: %d\n", *pN);
	

	return 0;
}

c_pointer_ex_2

aList는 배열의 이름으로 배열의 시작 주소이고,

이를 포인터 변수 pData에 대입했습니다.

즉, aList와 pData는 완전히 똑같은 것입니다.

그래서 pData[0]과 같이 사용할 수 있습니다.


그리고 pData는 시작 주소이므로,

배열의 시작인 0번째 요소의 주소인 &pData[0]과 같습니다.

그래서 *pData = *(&pData[0]) 이고,

&는 해당 변수의 주소에 접근, *는 해당 주소의 변수에 접근이므로 상쇄되서 없어지면

결국 *pData = *(&pData[0]) = pData[0] 입니다.

즉 *pData = 20 이 의미하는 바는

20이라는 값을 pData[0]에 대입하는 것과 같습니다.


일단 *(pData+1)는 건너뛰고, n 부분을 살펴보면

pN[0]과 같이 int의 주소를 담은 포인터 변수를 배열같이 사용한 걸 볼 수 있습니다.

이는 아까 언급한대로 포인터 변수와 배열은 똑같기 때문입니다.

int n = 7int n[1] = 7 로 생각하면 됩니다.

물론 그냥 int로 변수를 선언하는 것과 int형 배열로 변수를 선언하는 것은 다르지만

메모리에 저장되는 형식은 똑같기 때문입니다.


이번엔 *(pData + 1) = 30 부분을 보겠습니다.

이 구문을 자세히 살펴보면 배열의 시작주소(pData)에 1을 더하고 있습니다.

그렇다면 시작주소에 1을 더하면 무슨 값이 되는지 살펴봐야겠죠.

다음 예제를 살펴보겠습니다.

// int 배열과 char배열의 포인터 증가 테스트

#include <stdio.h>

int main(void)
{
	int arr1[5] = { 1, 2, 3, 4, 5 };
	int *parr1 = arr1;

	printf("parr1 = %p\n", parr1);
	printf("*parr1 = %d\n\n", *parr1);
	parr1++;
	printf("parr1 = %p\n", parr1);
	printf("*parr1 = %d\n\n", *parr1);

	char arr2[6] = { 't', 'e', 'e', 'm', 'o' };
	char *parr2 = arr2;

	printf("parr2 = %p\n", parr2);
	printf("*parr2 = %c\n\n", *parr2);
	parr2++;
	printf("parr2 = %p\n", parr2);
	printf("*parr2 = %c\n\n", *parr2);

	return 0;
}

c_pointer_ex_3

먼저 int배열인 arr1을 보겠습니다.

시작 주소를 parr1이란 포인터 변수에 저장 후,

parr1++ 을 통해 1 증가시켜 보았습니다.

그랬더니 나오는 주소는 이전 값보다 4가 증가된 것을 볼 수 있습니다.

int는 4바이트이고, 4증가된 주소로 접근하니 *parr1은 그 다음값인 arr1[1]이 나옵니다.


char 배열인 arr2도 같은 방식으로 1 증가 시키고

출력을 해보니 이번엔 주소값이 1만큼 증가된 것을 볼 수 있습니다.

그리고 *parr2 를 출력해보니 그 다음 문자인 e가 나옵니다.

즉, 배열의 주소에 1을 증가시키면 해당 주소의 자료형만큼 크기가 증가합니다.

이는 다음과 같이 이해할 수 있습니다.

&arr1[0] + 1 == &arr1[1];


그렇다면 이전 코드의 *(pData + 1) = 30 를 살펴보겠습니다.

pData는 int형 배열의 주소값이고 &pData[0]과 같습니다.

이에 1을 더하니, &pData[0] + 1 은 &pData[1]이 됩니다.

즉, *(&pData[1])은 그냥 pData[1]이 되어 저 식은 결국

pData[1] = 30이 됩니다.

다시한번 축약하면 *(pData + 1) == pData[1] 이 됩니다.

같은 방식으로 *pData = 20 도 이렇게 생각할 수 있습니다.

*pData == *(pData + 0) == pData[0]

그렇다면 이 코드를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
    int arr[4] = {4, 3, 2, 1};
    
    printf("arr[0] = %d\n", arr[0]);
    printf("arr[1] = %d\n", arr[1]);
    printf("2[arr] = %d\n", 2[arr]);
    printf("3[arr] = %d\n", 3[arr]);
    
    return 0;
}

/*
 arr[0] = 4
 arr[1] = 3
 2[arr] = 2
 3[arr] = 1
*/

arr[0]과 arr[1]은 우리가 지금까지 써오던 방법입니다.

그런데 2[arr]을 해도 2번째 요소(arr[2])가 나오고,

3[arr]을 해도 3번째 요소(arr[3])가 잘 나오는 것을 볼 수 있습니다.

이는 []연산자의 행동때문입니다.

예를 들어 arr[0] 은 *(arr + 0) 으로 연산됩니다.

마찬가지로 0[arr] 도 *(0 + arr) 로 연산됩니다.

arr + 0 이나 0 + arr 이나 값은 똑같이 때문에 같은 결과가 나옵니다.

하지만 가독성 때문에 보통 2[arr] 같은 방식은 사용하지 않습니다.


이 방식을 이해했다면, 문자열의 길이를 계산하는 프로그램을 만들 수 있습니다.

// 문자열 길이 계산하는 프로그램

#include <stdio.h>

int main(void)
{
    char string1[30] = { "Teemo" };
    char *pstring1 = string1;
    int length = 0;
    
    while (*pstring1 != '\0')
    {
        pstring1++;
        length++;
    }
    
    printf("length: %d\n", length);
    printf("strlen(string1): %d\n\n", strlen(string1));
    
    return 0;
}

/*
 length: 5
 strlen(string1): 5
*/

문자열의 끝은 ‘\0’임을 이용해서

while문을 통해 한글자 한글자씩 이동해가며 length를 증가시키고 있습니다.

마지막 printf()문에서 쓰인 strlen() 함수는

c에 기본적으로 내장되어 있는 문자열의 길이를 계산하는 함수입니다.

알맞게 계산했는지 확인용입니다.


같은 문자열 길이 계산 프로그램을 이렇게도 작성할 수 있습니다.

// 문자열 길이 구하기 2

#include <stdio.h>

int main(void)
{
    char string[30] = { "Jinx" };
    char *pstring = string;
    
    while (*pstring != 0) pstring++;
    
    printf("string 문자열의 길이는 %d입니다.\n\n", pstring - string);
    
    return 0;
}

/*
string 문자열의 길이는 4입니다.
*/

여기서는 pstring값을 문자열의 끝일 때까지 증가시킵니다.

그럼 while문이 끝나고 pstring은 문자열의 끝부분의 주소를 가지게됩니다.

그럼 문자열의 끝주소(pstring) - 문자열의 시작주소(string)를 통해

문자열의 길이를 계산할 수 있습니다.




다중 포인터(포인터의 포인터)


포인터 변수는 포인터(변수의 주소)를 저장하는 변수입니다.

즉, 포인터 변수도 결국 변수이고, 메모리의 어떤 주소에 저장됩니다.

이 포인터 변수의 포인터(주소)를 저장하는 변수를 2중 포인터 변수라 부릅니다.

포인터 변수는 다중(2중, 3중, 4중, … n중)으로 사용할 수 있습니다.

다중 포인터가 가능은 하지만 3중 포인터도 잘 쓸일은 없습니다.

간단한 에제를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
	int a = 7;
	// 포인터 변수 pa
	int *pa = &a;
	// 2중 포인터 변수 ppa
	int **ppa = &pa;
	// 3중 포인터 변수 pppa
	int ***pppa = &ppa;

	printf("a = %d\n", a);
	printf("pa = %p\n", pa);
	printf("ppa = %p\n", ppa);
	printf("pppa = %p\n\n", pppa);

	printf("a = %d\n", a);
	printf("*pa = %d\n", *pa);
	printf("**ppa = %d\n", **ppa);
	printf("***ppa = %d\n\n", ***pppa);

	printf("&a = %p\n", &a);
	printf("pa = %p\n", pa);
	printf("*ppa = %p\n", *ppa);
	printf("**pppa = %p\n\n", **pppa);

	printf("&pa = %p\n", &pa);
	printf("ppa = %p\n", ppa);
	printf("*pppa = %p\n\n", *pppa);

	printf("&ppa = %p\n", &ppa);
	printf("pppa = %p\n\n", pppa);

	return 0;
}

c_pointer_ex_6

예제코드와 실행결과만으로 이해는 될 것입니다.

자료형에 대해서만 짚어보고 넘어가자면,

a는 int 자료형

pa는 int* 자료형

ppa는 int** 자료형

pppa는 int*** 자료형 입니다.


참고로 포인터를 선언할때 1중 포인터는 다음과 같이도 선언할 수 있습니다.

int* a
int * a
int *a

2중 포인터도 다음과 같이 다양하게 선언할 수 있습니다.

int** pa
int* *pa
int **pa




2차원 배열의 포인터


아까 1차원 배열의 주소를 포인터 변수에 넣고 사용한 예제를 다시 살펴보겠습니다.

// int 배열과 char배열의 포인터 증가 테스트

#include <stdio.h>

int main(void)
{
	int arr1[5] = { 1, 2, 3, 4, 5 };
	int *parr1 = arr1;

	printf("parr1 = %p\n", parr1);
	printf("*parr1 = %d\n\n", *parr1);
	parr1++;
	printf("parr1 = %p\n", parr1);
	printf("*parr1 = %d\n\n", *parr1);

	char arr2[6] = { 't', 'e', 'e', 'm', 'o' };
	char *parr2 = arr2;

	printf("parr2 = %p\n", parr2);
	printf("*parr2 = %c\n\n", *parr2);
	parr2++;
	printf("parr2 = %p\n", parr2);
	printf("*parr2 = %c\n\n", *parr2);

	return 0;
}

이 예제의 포인트는 배열의 포인터 변수를 1 증가시키면

해당 배열의 자료형의 크기만큼 증가하는 것이었습니다. 예를 들어

parr1은 int형 배열의 포인터 변수이므로 int의 크기(4바이트)만큼 증가하였습니다.

parr2는 char형 배열의 포인터 변수이므로 char의 크기(1바이트)만큼 증가하였습니다.

그렇다면 2차원 배열의 포인트 변수가 1만큼 증가하면 어떻게 될까요?

예를 들어 int arr[2][3]의 포인터 변수가 parr이라고 하겠습니다.

이때 parr++을 한다면? 다음 예제 코드를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
	int arr[2][3] = { 0 };

	printf("arr = %p\n", arr);
	printf("arr[0] = %p\n", arr[0]);
	printf("arr[1] = %p\n", arr[1]);
	printf("arr[1] - arr[0] = %d\n\n", (int)arr[1] - (int)arr[0]);

	printf("&arr[0][0] = %p\n", &arr[0][0]);
	printf("&arr[0][1] = %p\n", &arr[0][1]);
	printf("&arr[0][2] = %p\n", &arr[0][2]);
	printf("&arr[1][0] = %p\n", &arr[1][0]);
	printf("&arr[1][1] = %p\n", &arr[1][1]);
	printf("&arr[1][2] = %p\n", &arr[1][2]);
	printf("&arr[0][1] - &arr[0][0] = %d\n\n", (int)&arr[0][1] - (int)&arr[0][0]);
	
	return 0;
}

c_pointer_ex_7

살펴볼 사항이 여럿있는데요, 먼저

arr과 arr[0]과 &arr[0][0]의 값이 똑같다는 것입니다.

배열의 이름은 배열의 시작 주소와 같으므로,

arr[0]은 arr[0][0], arr[0][1], arr[0][2] 중 시작요소인

arr[0][0]의 주소값과 동일합니다.

또한 arr도 arr[0], arr[1] 중 시작요소인

arr[0]의 주소값과 동일합니다.


같은 원리로 arr[1]과 &arr[1][0]의 값도 같은 걸 볼 수 있습니다.

이 메모리 상황을 그림으로 나타내본다면 다음과 같습니다.

c_pointer_ex_8


그리고 또 하나 살펴볼 사항은

arr[0]과 arr[1]의 차이가 12 차이난다는 것입니다.

이는 arr[2][3] = {{0, 0, 0}, {0, 0, 0}}이니까

{0, 0, 0} 차이만큼, int 3개 차이만큼, 4byte 3개 차이만큼 해서 12가 나옵니다.

그럼 이 문제를 다시 생각해보겠습니다.

arr의 포인터 변수가 parr이라고 한다면, parr++을 하면 parr은 12가 증가해야합니다.

그렇다면 parr은 어떻게 선언해야 할까요?

이를 명확히 알기 위해선 포인트 변수명 앞에 써주는 자료형의 의미를 알아야합니다.

앞서 일차원 배열에서는 그냥 이렇게 선언해주었습니다.

int *parr;

포인트 변수명 앞에 int를 써줌으로써,

parr을 1 증가시키면 주소값은 int의 크기(4바이트)만큼 증가시키라고 명시한 것입니다.

즉, 시작 주소를 써주고 어떻게 해석해야 하는지를 자료형을 써줘서 명시했습니다.


그런데 이번엔 12만큼 증가시켜야 합니다.

이 12는 int [3]의 크기입니다.

즉, int [3]이라는 자료형을 써줘서 이렇게 해석하라고 명시해야합니다.

그렇다면 이렇게 2차원 배열의 parr선언은 이렇게 되겠죠

int [3] *parr;

그런데 이건 문법에 맞지 않고, 올바르게 쓰려면 순서를 조금 바꿔서 다음과 같이 선언해야합니다.

int (*parr)[3];

이렇게 우리가 지금까지 살펴봤던 char, int, long 같은 것만 자료형인게 아니라

int[2], long[100], char[30] 등등도 모두 각기 다른 자료형입니다.

여하튼 중요한 것은 다음 문장입니다.

배열의 포인터는 배열의 요소의 자료형에 대한 포인터에 담는다.

일차원 int 배열의 포인터 변수를 int *parr; 로 선언한 까닭은 요소가 int 자료형이기 때문이고,

2차원 int 배열의 포인터 변수를 int (*parr)[3]; 로 선언한 까닭은 요소가 int[3] 자료형이기 때문입니다.

다음은 2차원 배열의 포인터를 선언해서 사용한 예제 코드입니다.

#include <stdio.h>

int main(void)
{
    int arr[2][3] = {
        {1, 2, 3},
        {4, 5, 6}
    };
    
    // arr의 배열의 포인트 변수 선언
    int (*parr)[3];
    parr = arr;
    
    for(int i=0; i<2; i++){
        for(int j=0; j<3; j++){
            printf("parr[%d][%d]: %d ", i, j, parr[i][j]);
            if(j<2) printf("/ ");
        }
        putchar('\n');
    }
    
    putchar('\n');
    return 0;
}

/*
 parr[0][0]: 1 / parr[0][1]: 2 / parr[0][2]: 3
 parr[1][0]: 4 / parr[1][1]: 5 / parr[1][2]: 6
*/

이번엔 2차원 char배열의 예제 코드입니다.

#include <stdio.h>

int main(void)
{
    char champion[3][30] = {"Teemo", "Jinx", "Gnar"};
    char (*pchampion)[30];
    pchampion = champion;
    
    for(int i=0; i<3; i++) printf("%s \n", pchampion[i]);
    
    putchar('\n');
    return 0;
}

/*
 Teemo
 Jinx
 Gnar

 */




포인터 배열


아까는 배열의 포인터 변수에 관해 살펴봤었고,

이번엔 요소가 포인터인 배열에 대해 살펴보겠습니다.

간단한 예제 코드를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
	// char* 배열 선언
	char *arr[3];
	char a = 'a', b = 'b', c = 'c';

	arr[0] = &a;
	arr[1] = &b;
	arr[2] = &c;

	printf("sizeof(a): %d\n", sizeof(a));
	printf("sizeof(&a): %d\n\n", sizeof(&a));

	printf("arr[0]: %p\n", arr[0]);
	printf("*arr[0]: %c\n\n", *arr[0]);

	printf("arr[1]: %p\n", arr[1]);
	printf("*arr[1]: %c\n\n", *arr[1]);

	printf("arr[2]: %p\n", arr[2]);
	printf("*arr[2]: %c\n\n", *arr[2]);

	printf("arr: %p\n", arr);
	printf("&arr[0]: %p\n\n", &arr[0]);

	return 0;
}

c_pointer_ex_9

이번 예제에서는 char* 배열을 사용해보았습니다.

일단 변수 a는 char형 이므로 sizeof(a)는 1(바이트)가 나옵니다.

그 아래 printf()를 통해 a의 포인터(&a)는 크기가 4 바이트라는 걸 알 수 있습니다.

64비트 운영체제를 사용하시는 분은 8이 나올 것입니다.

64비트 운영체제에서 포인트의 크기는 8바이트 입니다.

64비트여도 개발툴에 따라 포인트의 크기가 4바이트일 수 있습니다.

저는 윈도우10(64비트)의 Visual Studio에서는 4바이트로 나오고,

맥OS(64비트)의 Xcode에서는 8바이트로 나왔습니다.


그리고 arr[0], arr[1], arr[2]를 출력해본 결과

포인터 배열의 크기(4바이트)만큼 차이나는 걸 볼 수 있습니다.


이번엔 문자열 배열의 코드를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
    char *champion[3];
    char arr[6] = "teemo";
    champion[0] = arr;
    champion[1] = "Jinx";
    champion[2] = "Rammus";
    
    for(int i=0; i<3; i++){
        printf("champion[%d]: %s\n", i, champion[i]);
    }
    
    putchar('\n');
    
    printf("champion[0]+1: %s\n", champion[0]+1);
    printf("champion[1]+2: %s\n", champion[1]+2);
    printf("champion[2]+3: %s\n", champion[2]+3);
    
    putchar('\n');
    
    printf("champion[0][0]: %c\n", champion[0][2]);
    printf("champion[1][1]: %c\n", champion[1][2]);
    printf("champion[2][2]: %c\n", champion[2][2]);
    
    putchar('\n');
    
    return 0;
}

/*
 champion[0]: teemo
 champion[1]: Jinx
 champion[2]: Rammus
 
 champion[0]+1: eemo
 champion[1]+2: nx
 champion[2]+3: mus
 
 champion[0][0]: e
 champion[1][1]: n
 champion[2][2]: m
*/

char*형 배열인 champion을 살펴보겠습니다.

0번째 요소는 arr이라는 문자 배열의 이름이 들어갔습니다.

문자배열의 이름은 해당 문자 배열의 첫 문자 주소를 가리키죠.

즉, 여기서는 “teemo”라는 문자열에서 ‘t’가 저장된 주소를 가리킵니다.

즉, char*라고 자료형을 명시한 것에 적합합니다.


다음 1번째, 2번째 요소는 문자열 자체를 입력했습니다.

“Jinx”와 “Rammus”라는 문자열은 비록 특정 변수로는 지정이 안되어 있지만

메모리의 어딘가에 저장이 되고, 저장된 곳의 시작 주소를 값으로 가집니다.

간단한 코드를 보면 이해하기 쉽습니다.

#include <stdio.h>

int main(void)
{
    printf("%p\n", "Jinx");
    printf("%c\n", *"Jinx");
    printf("%c\n", *("Jinx"+1));
    printf("%c\n", *("Jinx"+2));
    printf("%c\n", *("Jinx"+3));
    printf("%s\n", "Jinx");
    
    putchar('\n');
    
    char arr[] = "Jinx";
    printf("%p\n", arr);
    printf("%c\n", *arr);
    printf("%c\n", *(arr+1));
    printf("%c\n", *(arr+2));
    printf("%c\n", *(arr+3));
    printf("%s\n", arr);
    
    putchar('\n');
    
    return 0;
}

/*
 0x100000fb0
 J
 i
 n
 x
 Jinx
 
 0x7ffeefbff5b7
 J
 i
 n
 x
 Jinx
*/

결론은, 문자열을 변수에 저장해서 쓰는 것과 그냥 쓰는 것은

저장되는 위치(주소)는 다르지만, 변수에 쓰는 것과 동일하게 사용할 수 있다는 것입니다.

그래서 저 champion 예제에서 *champion[3]을 더 짧게 선언할 수 있습니다.

#include <stdio.h>

int main(void)
{
    char *champion[3] = {"teemo", "Jinx", "Rammus"};
    
    for(int i=0; i<3; i++){
        printf("champion[%d]: %s\n", i, champion[i]);
    }
    
    putchar('\n');
    
    printf("champion[0]+1: %s\n", champion[0]+1);
    printf("champion[1]+2: %s\n", champion[1]+2);
    printf("champion[2]+3: %s\n", champion[2]+3);
    
    putchar('\n');
    
    printf("champion[0][0]: %c\n", champion[0][2]);
    printf("champion[1][1]: %c\n", champion[1][2]);
    printf("champion[2][2]: %c\n", champion[2][2]);
    
    putchar('\n');
    
    return 0;
}

이전 코드와 동일한 코드인데 *champion[3]의 선언 방식만 바꿨습니다.

다시 한번 상기하자면 문자열의 자료형은 char* 입니다.

코드를 조금 자세히 살펴보겠습니다.


champion[i]는 풀어서 쓰면,

*(champion + i)

champion이라는 주소(기준)에서 i만큼 떨어져있는 주소에 접근하는데,

그 주소를 해석하는 방법은 char* 입니다.


그리고, 예를 들어 int arr[3] 이 있다면,

배열의 이름인 arr자체의 자료형은 int*입니다.

그래서 arr의 포인터 변수를 선언할 때 다음과 같이 했습니다.

int *parr = arr;

즉, champion은 char* 자료형의 배열이면서,

champion 자체의 자료형은 char** 라고 할 수 있습니다.


이렇게 생각할 수도 있습니다.

예를 들어 int arr[3]의 자료형은 int[3]입니다.

이걸 풀어서 쓰면 *(배열요소에 대한 포인터 + 3)이므로 *(int* + 3)이 됩니다.

그리고 이 배열의 포인터 변수의 자료형은 int*입니다.

같은 방식으로, champion의 자료형은 char*[3] 이고 이걸 풀어서 써보면

*(char** + 3)이 됩니다.

char** + 3의 자료형은 char**이고, 결국

char* 배열의 주소를 담을 수 있는 포인터 변수는 char** 입니다.


champion[1]+2를 풀어서 써보면 *(champion + 1) + 2 입니다.

이를 자료형으로 나타내면 *(char**)이므로 결국 char* 이고

이는 문자열이므로 %s로 출력을 하고 있습니다.


champion[1][2]를 풀어서 써보면 먼저 champion[1]에 접근 후, [2]에 접근한 것이므로

*(*(champion + 1) + 2) 라고 볼 수 있습니다.

이를 자료형으로 나타내면 *(*(char**)) 즉, **(char**)이므로,

결국 char이 되고, 그러므로 %c로 출력하고 있습니다.


참고로 저 champion을 다음과 같이 2차원 배열로 바꿔도

코드에 오류는 안나고 잘 실행됩니다.

char champion[3][10] = {“teemo”, “jinx”, “rammus”};

다만 메모리를 사용한 방식은 조금 다릅니다.

char* 배열로 선언했을 경우에는 문자열을 저장하기 위한 메모리가

6(“teemo”) + 5(“jinx”) + 7(“rammus”) 로 총 18바이트 입니다.

c_pointer_ex_10

그런데 2차원 배열로 선언했을 경우에는 문자열을 저장하기 위한 메모리가

10(“teemo”) + 10(“jinx”) + 10(“rammus”) 으로 총 30바이트 입니다.

c_pointer_ex_11


마지막으로 포인트의 배열과 다중 포인터를 섞어서 쓴 코드를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
    char *city[3] = {"seoul", "new york", "london"};
    // city의 요소의 자료형이 char* 이므로, city의 포인터 변수 자료형은 char**
    char **pcity = city;
    // pcity의 자료형이 char**이므로, pcity의 포인터 변수 자료형은 char***
    char ***ppcity = &pcity;
    
    // pcity[0] == *(pcity + 0) == *(city + 0) == city[0]
    printf("pcity[0]: %s\n", pcity[0]);
    // pcity[1] == *(pcity + 1) == *(city + 1) == city[1]
    printf("pcity[1]: %s\n", pcity[1]);
    printf("city[2]: %s\n", city[2]);
    
    putchar('\n');
    
    // *ppcity[0] == **(ppcity + 0) == **(ppcity) == *(*ppcity) == *pcity == city == city[0]
    printf("*ppcity[0]: %s\n", *ppcity[0]);
    // *(*(ppcity+0)+1) == *(*(ppcity)+1) == *(pcity+1) == *(city+1) == city[1]
    printf("*(*(ppcity+0)+1): %s\n\n", *(*(ppcity+0)+1));
    
    return 0;
}

/*
 pcity[0]: seoul
 pcity[1]: new york
 city[2]: london
 
 *ppcity[0]: seoul
 *(*(ppcity+0)+1): new york
*/


진짜 마지막으로 살펴볼 내용은 main()함수의 원형입니다.

지금까지 우리는 int main(void)라고 써왔었습니다.

그런데 Xcode에서 프로젝트를 새로 생성하고 main.c 코드를 살펴보면…

c_main_original

main()함수에 매개변수가 있습니다.

첫번째 매개변수인 argc는 두번째 매개변수로 전달되는 배열 요소의 개수이고,

두번째 매개변수인 argv[]는 char* 자료형의 배열입니다.

그동안은 포인터에 대해서 몰랐기 때문에 그냥 void로 사용했었습니다.

일단 별도로 입력하지 않고 argc와 argv를 출력해보면

#include <stdio.h>

int main(int argc, const char * argv[]) {
    
    printf("argc: %d\n", argc);
    
    for(int i=0; i<argc; i++){
        printf("%s\n", argv[i]);
    }
    return 0;
}

/*
 argc: 1
 /Users/onsil/Library/Developer/Xcode/DerivedData/t10-14-dlpykxaqjnqrducaqsbjtvqynuum/Build/Products/Debug/t10-14
*/

문자열로 해당 파일의 경로가 argv에 전해지고,

문자열 하나만 전해지므로 argc는 1이 나옵니다.

이번엔 Visual Studio 기준으로 인자를 입력해보겠습니다.

먼저 프로젝트 > 속성 을 들어갑니다.

c_input_parameter

그곳에서 디버깅 > 명령인수 부분에 입력하고 싶은 문자열을 입력합니다.

참고로 여러 문자열은 띄어쓰기로 구분합니다.

c_input_parameter2

이제 실행을 해보면

c_main_original_input_result

참고로 0번째 요소는 해당 파일의 경로입니다.


맥의 Xcode에서 매개변수를 입력하는 방법은 다음과 같습니다.

먼저 Project > Scheme > Edit Scheme 로 들어갑니다.

c_input_argument_xcode1

그러면 아래와 같은 창이 뜨는데

Run > Arguments 에 가서 원하는 문자열을 입력하면 됩니다.

c_input_argument_xcode2

그러면 결과창이 다음과 같이 뜹니다.

c_input_argument_xcode3




Back to the Goal


c_pointer_goal_1

#include <stdio.h>

void swap(int *a, int *b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

int main(void)
{
	printf("두 변수의 값을 변경하는 swap 함수 만들기\n\n");

	int x = 7, y = 77;
	printf("x: %d, y: %d\n\n", x, y);

	printf("두 변수 스왑!\n\n");
	swap(&x, &y);

	printf("x: %d, y: %d\n\n", x, y);

	return 0;
}

swap()함수의 인자를 저렇게 포인터로 받지 않으면

swap()함수 내에서 main()함수에서 선언된

x와 y의 주소값에 접근할 방법이 없습니다.

결국 주소값의 데이터를 바꿔야 진짜로 해당 변수의 값이 바뀌는 것이기 때문에

꼭 포인터를 활용해야합니다.