티스토리 뷰


본 강좌는 아래 동영상 강좌와 같이 진행됩니다. 되도록이면 동영상과 같이 보시는 것을 추천합니다.

 

유튜브 채널 가기

 

강좌 14편 동영상 보기

 


 

 이번시간에는 지난시간에 이어, 여러가지 포인터의 사용법에 대해 알아보도록 하겠습니다. 내용이 조금 어려울 수 있으므로 한 단락씩 나눠서 보시는 것도 좋을 것 같습니다.

 

 

 

 1. 다중 포인터

 

 지금까지는 포인터를 선언할때 '*'를 한개만 썼는데 이것을 여러개 써줄 수 있습니다. 두개를 쓰는 것을 '이중 포인터' 혹은 '더블 포인터' 라고 하며, 이는 포인터의 주소를 가리킵니다.

 

int a = 1;

int *p1 = &a;

int **p2 = &p1;

 

 위와 같은 식입니다. 아래 그림을 봅시다.

 


 int형 변수 a는 0x01번지를 시작주소로 가지고 있으며, 그 주소에는 1이라는 값이 들어 있습니다. p1은 포인터 변수이고, a의 주소를 가지고 있죠. p2는 포인터 변수 p1 자체의 주소를 가지고 있습니다.

 

 이렇게 값을 얻어내는 단계에 대해 생각해보죠.  

 

a : 자기 자신의 값을 직접 읽는다. (직접)

*p1 : 저장된 a의 주소의 값을 읽는다. (한단계)

**p2 : 저장된 p1의 주소의 값인 a의 주소의 값을 읽는다. (두단계)

 

 이런 식으로 최종 값을 얻어내는 단계의 수 만큼 '*'을 붙여 선언하고 사용한다고 생각하시면 되겠습니다.

 

 이해를 돕기 위해 다른 방식으로 설명해보겠습니다. 포인터는 *을 붙여서 선언하고, 주소를 값으로 가진다고 했엇습니다. 아래처럼 선언하고, 같은 형으로 받는다고 생각하는게 이해가 빠를 수도 있습니다.

 

일반 변수 : a

a의 주소를 담을때 : *p (별이 한개 붙음)

*p의 주소를 담을때 : **pp (별이 한개 더 붙음)

 

a의 값을 받을때 : b

*p의 값을 받을때 : *p2

**pp의 값을 받을때 : **pp2

 

 주소를 담을때는 원래 변수보다 *이 한개씩 많아집니다. 값을 담을때는 *의 갯수를 맞춰줍니다.

 

 이런것도 위의 단계로 따져보면

 

int ****p;

int *****p2 = &p;

 

 크게 어렵지가 않습니다.

 

 이런식으로 '*'이 한개만 붙으면 '싱글 포인터', 두개 붙으면 '이중 포인터' 혹은 '더블 포인터', 3개 붙으면 '삼중 포인터' 혹은 '트리플 포인터' 라고도 부르니 참고하시기 바랍니다.

 

 이제 이중 포인터에서 값을 꺼내봅시다.

 

int a = 1;

int *p1 = &a;

int **p2 = &p1;

 

*(*p2) = 123;

 

printf("%d", *(*p2));

 

실행 결과

 

123

 

 위의 예는 p2를 이용해서 a가 가지고 있는 값을 대입연산을 통해 바꾼 후 출력하였습니다. 아래 부분만 놓고 생각해봅시다.

 

*(*p2) = 123;

 

 괄호안이 먼저 연산이 되기 때문에 '*p2'가 먼저 처리됩니다. p2에는 p1의 주소값이 들어 있으며, '*'을 붙인다는 것은 p2에 들어있는 p1의 '값'을 의미하기 때문에, a의 주소가 반환되게 됩니다. 그러므로 ()안은 a의 주소값 (= p1)이 들어가게 됩니다. '*p1'은 a의 주소에 들어있는 값이므로 최종적으로 '*(*p2)'는 a의 값이 되게 됩니다.

 

 별도의 포인터 연산 (뒤에 설명함) 이 들어가지 않을 경우는 괄호를 생략할 수도 있습니다.

 

**p2 = 123;

 

 삼중, 사중 등등 '*'이 많아도 이러한 규칙은 동일하니 참고하시기 바랍니다.

 

 

 

 2. 1차원 배열과 포인터

 

 배열을 하나 선언해봅시다.

 

char str[5] = {'a', 'b', 'c'};

 

char형 배열 str을 5개만큼의 크기로 선언했고 초기화를 해 주었습니다. 이것을 그림으로 그려보면

 


 이런식으로 그려볼 수 있습니다. 여기서 배열의 각 원소들은 일렬로 나란히 붙어있고, 배열의 이름 'str'은 이 배열의 시작 주소를 가지고 있는 포인터가 됩니다. 그래서 다음과 같이

 

char str[5] = {'a', 'b', 'c'};

char *p = str;

 

int i = 0;

for (i = 0; i < 5; i++)

printf("%c", p[i]);

 

실행 결과

 

abc

 

 배열의 이름만 쓰는 것 자체가 포인터이기 때문에, 포인터 변수 p에 바로 대입할 수 있고, 포인터 변수 p를 마치 배열처럼 사용할 수 있습니다.

 

 

 

 3. 포인터 연산

 

 포인터도 일반 변수와 마찬가지로 연산자를 이용해 연산을 할 수 있습니다.

 

char str[5] = "abc";

char *p = str;            // 대입연산

p = p + 1;                 // 산술연산

p++;                        // 증감연산

p += 1;                    // 할당연산

 

if (p == NULL)          // 관계연산

 

 아래 그림을 다시 보고 하나씩 알아보도록 합시다.

 


 ◆ 대입 연산 (=)

 위에서 배열의 이름은 포인터라고 했기 때문에 같은 형식끼리는 대입이 가능하게 됩니다. 일반 변수와 같기 때문에 따로 설명은 하지 않겠습니다.

 

 ◆ 산술 연산 (+, -, *, /)

 포인터는 주소를 담고 있는데, 이런 주소값도 산술 연산자를 통해 연산이 가능합니다.

 


 포인터 변수 p에 str을 대입했으므로 p는 str과 같은 '0x01번지'를 가리키게 됩니다. 여기에 '+1' 을 해주게 되면 다음칸으로 이동하게 되어 문자 'b'가 저장되어 있는 '0x02번지'를 가리키게 되고, 이것은 str[1]과 같게 됩니다. 그래서 아래 예를 보면

 

char str[10] = "abc123";

char *p = str;

 

int i = 0;

for (i = 0; i < 10; i++)

printf("%c", *(p + i));    // p + i는 p[i]의 주소이다.

 

실행 결과

 

abc123

 

 배열처럼 p[0], p[1]... 같이 써주게 되면, 배열의 원소이므로 값이 나오게 되고, 산술연산으로 p + 0, p + 1 ... 과 같이 해주게 되면, 각각 p[0], p[1]의 주소를 가리키게 됩니다.

 

 여기서 주의해야 할 것은 'p + 1' 이라고 해서 주소값을 1 더한다는 의미가 아닙니다.

 

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

 

int *p = &a;

 

int i = 0;

for (i = 0; i < 5; i++

printf("%d", *(p + i));

 

실행 결과

 

12345

 

 위의 코드는 char형 예제를 int형으로만 바꾼 것뿐입니다. 동작도 거의 같은 것을 알 수 있는데, 정확하게는

 


 'p + 1'이 아까 char형에서처럼 주소값 + 1과 같은 결과가 나오지 않습니다. 그것은 처음에 선언한 자료형 때문으로, 포인터 변수를 산술연산할때는 단순히 주소값에 더하고 빼는 것이 아니라 주소값에서 자료형 몇개만큼을 더하고 빼고 하라는 겁니다. char형은 1바이트이기 때문에 +1을 해줄 경우 1바이트 만큼 이동했지만, int형의 경우 4바이트이기 때문에 +1을 하면 4바이트만큼 이동을 하게 됩니다. 배열의 원소 1 증가하는거랑 같습니다.

 

'*p + 1'과 '*(p + 1)'도 혼동할 수 있는데 이 두가지는 엄연히 다릅니다. 첫번째 것은 p가 가리키는 주소 안의 값에 1을 더하는 것이고, 두번째 것은 p의 주소를 자료형 1개만큼 증가시킨 후 그 안의 값을 나타냅니다.

 

 ◆ 증감 연산 (++, --)

 산술연산을 이해했다면 증감 연산 역시 별다를게 없습니다.

 

p++; ++p;

 

 일반 변수의 증감연산자와의 차이점은 단하나, 포인터를 증감 연산하면 그 자료형 만큼 주소값이 증가, 감소 한다는 것입니다.

 

 역시 이 두가지는 구분되어야 합니다.

 

*p++;

*(p++);

 

 '*p++'은 p가 가리키는 주소의 값을 1 증가시키는 것이고, '*(p++)'은 p의 주소를 자료형 만큼 증가시킨 후, 그 값을 나타내는 것입니다.

 

 ◆ 할당 연산 (+=, -=)

 'p += 1'은 'p = p + 1'을 줄여서 사용하는 식이기 때문에 산술 연산자를 이해했다면 크게 어렵지는 않을 것이므로, 생략하도록 하겠습니다.

 

 ◆ 관계 연산 (<, >, <=, >=, ==, !=)

 포인터도 대소 관계가 존재합니다. 단순히 주소값의 크기를 비교한다고 보시면 됩니다.

 

int a[5] = {0};

int *p1 = &a[1];

int *p2 = &a[3];

 

if (p1 < p2)

printf("p1이 p2보다 작다.");

else if (p1 > p2)

printf("p1이 p2보다 크다.");

 

실행 결과

 

p1이 p2보다 작다.

 

 주소는 뒤로 갈수록 커지기 때문에 배열의 앞쪽 원소의 주소를 대입한 포인터가 뒷쪽 원소의 주소를 대입한 포인터보다 작은걸 알 수 있습니다.

 

 

 

 4. 포인터 상수, 상수 포인터

 

 포인터도 변수만 있는 것이 아니라 상수도 존재합니다.

 

자료형 *const 이름 = 초기값;

 

 이런식으로 선언하는 것을 '포인터 상수' 라고 합니다. 포인터 상수는 선언 이후에 주소값을 바꿀 수 없게 됩니다. 예를들어

 

int a = 3;

int *const p = &a;

 

*p = 123;    // 가능

p = NULL;  // 불가능

 

 위의 예에서 포인터 상수의 초기값은 a의 주소를 주었습니다. 주소값만 바꿀 수 없을 뿐, 그 주소가 가리키는 값은 변경이 가능하여 123을 대입해도 문제가 없습니다. 하지만 p의 주소를 직접적으로 NULL로 바꾸려 하면 애러메시지를 만나게 됩니다.

 

 배열의 이름 같은 경우도 일종의 상수 포인터라 할 수 있습니다.

 

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

*a = 123;    // a[0] = 123; 과 같다.

 

a = NULL;    // 주소를 바꿀 수 없다.

 

 배열 a도 상수 포인터와 같이 값을 얼마든지 바꿀 수 있지만, 주소를 바꾸려 하면 에러를 만나게 됩니다.

 

 '상수 포인터' 외에 '포인터 상수' 라는 것도 존재합니다. 선언은

 

const 자료형 *이름;

 

 이렇게 선언합니다. 상수 포인터의 선언과는 const의 위치만 다를 뿐이지만, 이것은 주소는 변경이 가능하고 주소가 가리키는 값을 변경할 수 없습니다. 다음 예를 봅시다.

 

int a = 3;

const int *p = &a;

 

*p = 123;    // 불가능

p = NULL;  // 가능

 

 상수 포인터와는 정반대로 주소값은 변경이 가능하나, 주소가 가리키는 값은 변경이 불가능하다는걸 알 수 있습니다. 정리하자면

 

상수 포인터

자료형  *const 이름 = 초기값;

주소를 바꿀수 없다.

주소가 가리키는 값은 바꿀 수 있다.

 

포인터 상수

const 자료형 *이름;

주소를 바꿀 수 있다.

주소가 가리키는 값은 바꿀 수 없다.

 

 '상수 포인터'와 '포인터 상수'는 실수로 주소를 바꾸거나 값을 바꾸지 못하게 하여, 프로그래밍을 하는 과정에서의 실수를 미연에 방지하기 위해 사용됩니다. 의도적으로 사용하지 않더라도 여러곳에서 이것들을 사용하고 있는걸 발견하게 됩니다. 앞으로의 강좌에서 그런것들이 보이면 또 설명하도록 하겠습니다.

 

 

 

 5. 포인터 배열

 

 포인터도 배열로 선언이 가능합니다.

 

int *p[3] = { NULL };

 

 일반 변수의 배열과 크게 다르지 않습니다. 초기화할때도 역시 일반 배열처럼 중괄호 {}로 묶어주어 사용합니다. 일반 변수와 다른 점은 각각의 원소마다 값이 아닌 주소를 담을 수 있다는 점입니다. 아래는 포인터 배열의 사용 예입니다.

 

int a = 1, b = 2, c = 3;

int *p[3] = {&a, &b, &c};

 

*p[0] = 123;

*p[1] = 456;

*p[2] = 789;

 

printf("%d %d %d\n", a, b, c);

printf("%d %d %d\n", *p[0], *p[1], *p[2]);

 

실행 결과

 

123 456 789

123 456 789

 

 int형 포인터 배열 p[3]을 선언하였고, 각각 int형 변수 a, b, c의 주소를 넣어 초기화 하였습니다. 지난시간에 사용한 포인터 연산자 '*'를 사용해 값을 바꿔주었고, 출력을 해봤습니다.

 

 그러면 위의 예에서 포인터 배열 변수명 'p'를 포인터로 받고 싶을 때가 있을 것입니다. 그럴때에는

 

int a = 1, b = 2, c = 3;

int *p[3] = {&a, &b, &c};

int **p2 = p;        // &p를 받는것과 혼동하지 맙시다.

 

 이렇게 받을 수 있습니다. 이유는 아래 그림을 봅시다.

 


 각각의 원소 p[0], p[1], p[2]는 각각 a, b, c의 주소를 가지고 있는 포인터가 되고, 이런 포인터를 가리키는 포인터는 이중 포인터라고 맨 위에 알아보았었는데 같은 이치입니다. a의 값을 얻기 위해 *을 몇개 붙이는지 표시해놨으니 참고하시기 바랍니다.

 

 많은 변수를 하나로 관리하기 위해 배열을 사용하듯, 많은 포인터를 하나로 관리하기 위해 포인터 배열을 사용합니다.

 

 

 

 6. 다차원 배열과 포인터

 

 2차원 배열의 예를 봅시다.

 

char str[3][5] = {{'a', 'b', 'c'},

{'d', 'e', 'f'},

{'g', 'h', 'i'}}; 

 

 이런 배열을 그림으로 그려봅시다.

 


 각각의 원소들인 str[0][0] ~ str[2][4] 는 쭉 일렬로 붙어있고, str[0], str[1], str[2]는 각각 str[0][0], str[1][0], str[2][0]을 가리키는 포인터가 됩니다. 또한 str은 str[0]을 가리키는 '이중 포인터'가 됩니다.

 

 위의 그림을 이용해 코드로 옮겨 보았습니다.

 

char str[3][5] = {{'a', 'b', 'c'},

{'d', 'e', 'f'},

{'g', 'h', 'i'}}; 

char *p1 = str[0];

char *p2 = str[1];

char *p3 = str[2];

 

printf("%s\n%s\n%s\n", p1, p2, p3);

 

실행 결과

 

abc

def

ghi

 

 배열의 이름 str은 이중 포인터라 했으니 그것을 사용해서 포인터 연산을 해보겠습니다.

 

char str[3][5] = {{'a', 'b', 'c'},

{'d', 'e', 'f'},

{'g', 'h', 'i'}}; 

 

int i = 0, j = 0;

for (i = 0; i < 3; i++)

for (j = 0; j < 5; j++)

printf("%c", *(*(str + i) + j));

// *(*(str + i) + j)는 str[i][j]와 같다.

 

실행 결과

 

abc  def  ghi

 

 이중 포인터의 연산은 이런 식으로 2차원 배열과 연관지어 생각해보면 의외로 간단합니다. 괄호 안에서부터 계산하므로 부분부분을 떼어 생각해봅시다.

 

*(str + i) : i만큼 이동한 곳의 값 (= str[i])

*(위의 결과 + j) : 위의 결과에서 j만큼 이동한 곳의 값 (= str[i][j])

 

 가령 i가 1, j가 2일때 그림을 그려보면

 


 위의 그림과 같이 나타낼 수 있습니다.

 

 그럼 이중 포인터로 배열을 받아 보겠습니다.

 

 char str[3][5] = {{'a', 'b', 'c'},

{'d', 'e', 'f'},

{'g', 'h', 'i'}}; 

char **p = str;

 

int i = 0, j = 0;

for (i = 0; i < 3; i++)

for (j = 0; j < 5; j++)

printf("%c", *(*(p+ i) + j));

 

 컴파일을 하면 경고는 있지만 에러는 없습니다. 실행을 해봅시다.

 


 컴파일할때 에러는 없었는데, 실행해보니 오류가 발생하여 프로그램이 작동하지 않습니다. 이유는 2차원 배열 같은 경우는 건너뛸 크기를 'str[3][5]' 같이 선언에서 지정을 해줬지만, '**p' 와 같이 선언한 이중 포인터는 이렇게 정해주지 않았기 때문입니다. 그렇기 때문에 2차원 배열을 대입하는 포인터의 경우는 다음과 같이 선언해줍니다.

 

 char str[3][5] = {{'a', 'b', 'c'},

{'d', 'e', 'f'},

{'g', 'h', 'i'}}; 

char (*p)[5] = str;

 

 배열은 포인터고, 그것을 포인터로 선언했으므로 결과적으로는 이중 포인터입니다. 하지만 일반적인 이중 포인터와는 다릅니다. 이와 같이 선언 하는 것을 '배열 포인터' 라고 하며, 뒤에 얼마만큼 건너뛸지를 []안에 넣어주게 됩니다. '*p[5]'로 선언하면 포인터 배열이므로, 반드시 '(*p)[5]'와 같이 괄호를 묶어 선언해줘야 합니다.

 

 아래 예는 배열을 받는 여러가지 배열 포인터의 예입니다.

 

int a[3];

 

int *pa = a;

 

int b[4][3];

int (*pb)[3] = b;

 

int c[5][4][3];

int (*pc)[4][3];

 

 

 이런 식으로 사용하면 됩니다

 

 이번시간에는 지난시간에 이어 포인터에 대해 알아보았습니다. 한번에 모두를 이해하지 못하였더라도 충분히 그럴 수 있으니, 차근차근 직접 해보시고 어떻게 동작하는지 직접 확인해 보시기 바랍니다. 다음 시간에는 메모리 할당에 대해 알아보도록 하겠습니다.

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
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