배열,포인터,링크드리스트,스택,큐 이해
[배열과포인터]
◐ C에서의 문자열
C에서 문자열은 포인터를 사용해서 구현된다는 것은 다 아실겁니다. 정말로
그런지 한번 살펴보도록 하지요. 다음 문장을 보세요.
char *sp = "Love";
┏━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃주소┃ 41 42 43 44 45 46 5A 5B 5C 5D 5E ┃
┃ ┃ ┳━┳━┳━┳━┳━┳━┳━━━━━━┳━━━━━━━┳━┳ ┃
┃ 값 ┃ ┃ ┃L ┃o ┃v ┃e ┃\0┃ … ┃42┃00┃00┃00┃ ┃ ┃
┃ ┃ ┻━┻━┻━┻━┻━┻━┻━━━━━━┻━━━━━━━┻━┻ ┃
┃이름┃ sp ┃
┗━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
제가 항상 포인터를 그 포인터가 가리키는 메모리 영역보다 뒤에 그리는데
반드시 그런건 아닙니다. 포인터 변수도 역시 변수이기 때문에 컴파일러가 알
아서 비어있는 곳에 메모리를 할당하는 것이지요.
위의 예제를 보면 그 동안의 예제와는 다르게 "Love"의 첫 부분에 따로 이
름이 붙어있지 않지요? 바로 그렇습니다. C에서는 문자열 자체에 이름을 붙일
수는 없는 거지요. 그 포인터를 통해서만 참조가 가능한 겁니다. 다시 말씀드
려서 C에서의 문자열은 포인터가 전부라는 얘깁니다. 그러니까 문자열의 길이
도 일반적인 방법으로는 알아낼 수가 없지요. 따로 길이를 저장하는 공간이
없으니까요.
그래서 C에서 사용하는 방법이 바로 문자열 끝에 0이라는 값을 넣어주는 것
입니다. 여기서 0이란 것은 코드값이지 문자로 표현되는 '0'이 아닙니다. 둘
이 어떻게 다른건지는 아시겠지요? (문자 '0'의 코드는 0x30 입니다, 10진수
로는 48이지요)
결국 문자열 중간에 0이라는 코드를 가지는 문자는 넣을 수 없게 되겠지요?
그러나 걱정할 것 없습니다. 코드 0은 사용되지 않는 문자이기 때문이지요.
그럼 이 문자열을 어떻게 다루지요?
puts(sp); ->(이해)puts(char *)) 형이삼.
이렇게하면 문자열을 출력하는게 되지요? 그런데 이상한 점이 있지요. 여태
까지는 포인터가 가리키는 내용을 출력할려면 *연산자를 붙여야 했었는데 여
기서는 그러질 않네요. 왜 그럴까요? 그냥 당연히 그런거다 생각하시나요? ^^
그렇지는 않지요. 사실상은 puts 함수 안에서 인자로 받은 포인터에 *를 붙
여서 내부적으로 한 문자씩 출력하기 때문이지요. 문자열을 다루는 함수들이
내부적으로 어떤 과정을 거치는지는 잠시 후에 알아보기로 합시다.
◐ 문자열과 배열
아시다시피 배열과 포인터는 매우 가까운 관계 입니다. 그러니 당연히 배열
과 문자열도 밀접한 관계를 가지고 있겠지요. 우리는 배열의 참조 연산자인
[]를 사용해서 문자열에도 접근 할 수 있었습니다.
sp[0]은 'L'이고 sp[1]은 'o'겠지요. 이 정도는 아시리라 믿고…
◐ 문자열 함수
puts 함수가 어떤 방법으로 구현되는지 볼까요? 일단 strlen 함수부터 보도
록 합시다.
int my_strlen(const char *ptr) {
int len;
for (len = 0; ptr[len] != '\0'; len++);
return len;
}
for문의 구문이 좀 특이해 보이기는 하지만 전혀 다를건 없습니다. const
는 모른다면 당장 신경쓰지 않으셔도 되고(있으나 마나라고 알고 계세요), 실
제로 구현된 부분인 for문을 봅시다. for문의 형식은 다음과 같지요.
for (초기값; 지속조건; 값변화)
지속조건이 ptr[len] != '\0'이지요. 흔히 for (i = 0; i < 3; i++)… 이런
형태로만 사용해 와서 초기값과 지속조건, 값변화에 사용되는 변수가 모두 동
일해야 한다고 생각하실지도 모르겠는데, 그렇지 않습니다. 모두 다른 변수가
사용되도 되지요.
지속조건을 보면, "ptr의 len번째 요소가 '\0'이 아닌 동안"이 됩니다. 다
시말해 ptr의 len번째 요소가 '\0'일때 끝나게 되는 것이지요. 그림을 보는게
좋겠네요.
┏━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃주소┃ 41 42 43 44 45 46 5A 5B 5C 5D 5E ┃
┃ ┃ ┳━┳━┳━┳━┳━┳━┳━━━━━━┳━━━━━━━┳━┳ ┃
┃ 값 ┃ ┃ ┃L ┃o ┃v ┃e ┃\0┃ … ┃42┃00┃00┃00┃ ┃ ┃
┃ ┃ ┻━┻━┻━┻━┻━┻━┻━━━━━━┻━━━━━━━┻━┻ ┃
┃len ┃ 0 1 2 3 4 ptr ┃
┗━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
len이 4일때의 ptr값이 '\0'이 되지요? 문자열의 길이에는 '\0'이 포함되지
않기 때문에 그때의 len값이 바로 문자열 길이가 되는 것이지요. 그런데 한번
보세요. my_strlen 함수도 분명 문자열을 다루는 함수 입니다. 문자열을 다루
는 함수는 내부에서 알아서 *를 붙여서 사용한다고 했는데, 눈을 씻고 찾아봐
도 *연산자는 보이질 않지요? 네. 바로 그렇습니다. 바로 위에서 말씀드린 []
연산자가 *연산자를 대신하고 있는 것이지요. []연산자와 *연산자의 관계는
조금 후에 다루기로 하고 puts 함수를 보도록 하지요. -->
void my_puts(const char *ptr) {
int i, len = my_strlen(ptr);
for (i = 0; i < len; i++) putch(ptr[i]);
putch('\n');
}
my_strlen의 결과 값이 4이니까 for 루프는 i가 0일때부터 3일때까지 회전
을 하겠지요. 그리고 역시 여기에서도 []연산자를 사용해 해당 위치의 문자를
읽어내 putch(한 문자를 출력하는 함수 입니다)로 출력을 합니다. 이제 우리
가 문자열을 다루는 함수에 문자열을 넘겨줄때 왜 *연산자를 사용하지 않아도
되는지 아시겠지요?
◐ 배열과 포인터
여태까지는 문자열만을 다뤘습니다. 문자열을 배열과 똑같이 사용을 했었지
요? 그런데 문자열이 일반적인 배열에 추가해서 가진 속성이 있습니다. 바로
각 문자의 크기가 1바이트라는 것이지요. 다시 말해서 문자열은 각 요소의 크
기가 1인 배열이라고 생각하셔도 된다는 것이지요. 그럼 문자열이 아닌 정수
형 배열을 보도록 합시다.
int ar[3] = { 1, 2, 3 };
int *ap = ar;
┏━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃주소┃ 41 42 43 44 45 46 5A 5B 5C 5D 5E ┃
┃ ┃ ┳━━━┳━━━┳━━━┳━━━━━━┳━━━━━━━┳━┳ ┃
┃ 값 ┃ ┃01┃00┃02┃00┃03┃00┃ … ┃41┃00┃00┃00┃ ┃ ┃
┃ ┃ ┻━━━┻━━━┻━━━┻━━━━━━┻━━━━━━━┻━┻ ┃
┃이름┃ ar ap ┃
┗━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
아시다시피 int 자료형의 크기는 2바이트이지요. 그림을 보시면 ar의 각 요
소가 각각 2바이트씩 차지하고 있는 것을 알 수 있습니다. 역시 값의 순서는
바이트 단위로 바껴서 들어가 있지요? 그럼 이 배열을 대표하는 ar은 도대체
무엇일까요? 포인터일까요? 아닙니다. 문자열 배열과 다르게 이 값은 포인터
가 아닙니다. 이것이 배열과 포인터의 차이인데요. 이 ar은 바로 이름을 가진 ********>
포인터 상수 입니다. 포인터 변수와 다 똑같지만 단지 변수가 아니므로 메모
리에 존재하지는 않는다는 것이지요. 다음 예제를 실행해 보세요.
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ void main(void) { ┃
┃ int ar[3] = { 1, 2, 3 }; ┃
┃ int *ap = ar; ┃
┃ ┃
┃ printf("%p %p\n", &ar, ar); ┃
┃ printf("%p %p\n", &ap, ap); ┃
┃ } ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃ <결과> ┃
┃ 0EF1:0FFA 0EF1:0FFA ┃
┃ 0EF1:0FF6 0EF1:0FFA ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
포인터 변수 ap는 그 값인 ap와 그 주소인 &ap가 다르지요. 당연히 메모리
에 존재하니까요. 그런데 위의 ar을 보세요. ar와 &ar이 같습니다. (물론 숫
자는 실행할 때 마다 다를 수 있습니다만 같고 다른 것은 확인이 가능합니다)
ar은 따로 메모리에 존재하는 변수가 아닌 컴파일러가 내부에서 다루는 상수
이기 때문에 주소가 없는 것이지요. 역시 신기합니다. ^^ (저도 말로만 했었
지 실제로 확인해 본건 처음이네요) 사실상 쓰임새에 대해서는 ap와 ar은 완
전히 동일하지만 내부적으로는 이런 차이점이 있었네요.
long al[2] = { 1L, 2L };
long은 4바이트이므로 이때의 메모리는 다음과 같겠지요.
┏━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃주소┃ 41 42 43 44 45 46 47 48 ┃
┃ ┃ ┳━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━ ┃
┃ 값 ┃ ┃01┃00┃00┃00┃02┃00┃00┃00┃ … ┃
┃ ┃ ┻━━━━━━━┻━━━━━━━┻━━━━━━━━━━━━━ ┃
┃이름┃ al ┃
┗━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
◐ 배열을 인자로 사용하기
배열 상수는 포인터 상수와 달리 크기를 갖고 있다고 했지요. 그런데 함수
로 배열을 전달하기 위해서는 포인터 변수를 사용할 수 밖에 없습니다. 다음
과 같이 말이지요. (값을 받는 함수가 그것이 배열인지 아닌지 구분할 방법이
없다는 얘기도 되지요. 배열을 넘겨줄 수는 없으니까…)
void sort(int *ia);
그렇다면 이 함수에서는 ia가 가리키는 배열의 크기가 얼마인지 알 수 있을
까요? 포인터만으로는 전혀 알 방도가 없습니다. 그래서 따로 배열의 크기가
얼마인지를 넘겨주어야 하지요. 또는 배열의 끝을 감지할 수 있도록 문자열을
사용하는 것처럼 끝에 특정한 값을 넣어주던지요. 그런데 이 경우 역시 그 특
정값은 배열 중간에는 사용할 수가 없기 때문에 정수형을 다루는 함수라면 문
제가 될 수 있겠지요. 만약 양수만을 다루는 함수라면 배열의 맨 끝에 -1을
넣어준다거나 해서 구분할 수도 있을 겁니다.
void sort(int *ia) {
int n = 0;
while (ia[n] != -1) { n++; }
}
void main(void) {
int a[6] = { 1, 2, 3, 4, 5, -1 };
sort(a);
} **********->
이렇게 말이지요. 하지만 갯수도 함께 넘겨주는 것이 일반적 입니다.
◐ 포인터 연산
위에서 []연산자는 *연산자의 기능을 대신할 수 있다고 했습니다. []연산자
와 *연산자의 관계를 알아보도록 하지요. 조금 어려운 내용이지만 천천히 잘
생각해 보시면 충분히 아실 수 있을 겁니다.
al[1]이라고 했을 때 실제로 접근한 메모리의 번지가 어떻게 되나요? 바로
45번이겠지요? 그러면 이걸 []연산자가 아닌 *연산자로 접근할려면 어떻게 해
야 할까요? 아시다시피 al의 값은 41이지요. 생각해 봅시다.
al이 41이니까 거기에 4를 더한 값에 *를 붙이면 되지 않을까요? 다음과 같
이 말입니다.
*(al + 4)
이때 *연산자는 + 연산자 보다 순위가 높기 때문에 반드시 괄호를 해 주어
야 합니다. 한번 다음 프로그램을 실행해 보세요.
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ void main(void) { ┃
┃ long al[2] = { 1L, 2L }; ┃
┃ printf("%ld %ld", *al, *(al + 4)); ┃
┃ } ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
어때요? 결과가 제대로 나오나요? 아니지요? 제대로 나왔다면 우연히 그런
겁니다. 그럼 왜 틀릴까요? 맞는 것 같은데…
우리가 *연산자를 사용할 때 그 포인터의 타입 만큼의 값을 읽어온다고 했
지요? 이 경우는 long *타입이라고 보면 되겠지요. 그래서 *al 했을때도 41번
주소부터 4바이트를 읽어서 1이 출력이 되었지요. 그런데 이 타입의 크기는
포인터에 덧셈이나 뺄셈을 할때도 적용이 됩니다. 그러니까 포인터가 long형
포인터인 경우는 그 포인터에 1을 더해도 실제로는 그 포인터 타입의 크기인
4가 더해진다는 것이지요. 물론 2를 더하면 8이 더해지고요.
****************=>
그래서 위의 예제의 경우 *(al + 4)가 아닌 *(al + 1)이라고 해야 2라는 결
과가 나온다는 겁니다. 이제 좀 아시겠지요? 그리고 사실 이게 훨씬 쉽지요.
al[0] = *(al + 0) = *al
al[1] = *(al + 1)
al[2] = *(al + 2)
이렇게 된다는 겁니다. 그 포인터 타입의 크기에 관계없이 동일한 결과를
얻을 수 있겠지요? 얼핏 보면 어려운것 같기도 하지만 그나마 C가 프로그래머
를 조금 배려했다고 생각되지 않으시나요? 이렇지 않다면 포인터를 통해 배열
을 참조할 때 그 배열 포인터 타입의 크기를 일일히 계산해야 할테니까요.
그리고 한 가지 더.
C는 []연산자를 내부적으로는 위처럼 *() 연산자로 바꿔서 사용한다는 것입
니다. 두 코드는 완전히 동일하게 되는 것이지요. ******->
◐ 이런것도 가능!
이번에는 아주 신기한 코드를 많이 볼 수 있을 겁니다. 다음을 보세요.
int pi[5] = { 1, 2, 3, 4, 5 };
┏━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃주소┃ 41 42 43 44 45 46 47 48 49 4A ┃
┃ ┃ ┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━━━━━━━ ┃
┃ 값 ┃ ┃01┃00┃02┃00┃03┃00┃04┃00┃05┃00┃ … ┃
┃ ┃ ┻━━━┻━━━┻━━━┻━━━┻━━━┻━━━━━━━━━ ┃
┃이름┃ pi ┃
┗━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
pi[3]은 *(pi + 3)과 같다고 했지요? 위 그림과 같이 저장되어 있다고 한다
면 pi + 3은 47이 될 겁니다. 그렇다면 3 + pi는 뭘까요? 역시 47이지요. 이
걸 []연산자로 바꾸면 어떻게 되지요?
네. 그렇습니다. 3[pi]지요. 결국 다음의 네 가지는 모두 동일합니다.
pi[3] = 3[pi] = *(pi + 3) = *(3 + pi) //3[pi]??
또 한가지 이번에는 문자열 상수를 한번 봅시다.
printf("%c", "ABC"[1]);
이 결과는 어떻게 될까요? 네. 문자열 "ABC"의 1번째 요소인 B가 출력되겠
지요. 다른 상수와 다르게 문자열 상수는 메모리에 임시나마 저장이 됩니다.
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ void main(void) { ┃
┃ printf("%p %p", "DEF", 5); ┃
┃ } ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
위의 결과는 어떻게 나오나요? 첫번째 %p에서는 문자열 "DEF"가 저장된 메
모리의 포인터가 나오지요. 그러나 5는 문자열 상수가 아니기 때문에 그 값을
직접 포인터로 보고 0000:0005를 출력하게 되는 것이지요. 그렇기 때문에 문
자열 상수에 한해서 상수로도 직접 포인터 연산을 행할 수 있는 겁니다. 한가
지 더 복잡한 것을 볼까요.
printf("%c", 2["ABCDEFGH" + 3]); //****
너무 복잡한가요? ^^ 하지만 별것 아닙니다. 차근히 살펴보도록 하지요. 위
의 연산은 *(2 + "ABCDEFGH" + 3)과 완전히 같은 코드겠지요. 이제 좀 알아보
시겠지요? *("ABCDEFGH" + 5) = "ABCDEFGH"[5]와 완전히 동일한 것입니다.
결국 결과는 F가 되겠지요. 어때요? 별로 안 어렵지요?
그리고 물론 포인터 연산은 덧셈만 가능한 것이 아닙니다. 뺄셈, 곱셈, 심
지어 나눗셈도 가능하지요. 왜냐구요? 포인터란 것도 그냥 숫자이기 때문이지
요. 그렇지만 곱셈과 나눗셈은 쓸 일이 없겠지요. 쓸수도 없고… 덧셈과 뺄셈
은 우리가 값을 알고 있는 일정한 연속 메모리에 대해 쓸 수 있긴 하지만, 곱
셈과 나눗셈을 통한 메모리 접근이라면 우리가 전혀 모르는 값이지요. 뺄셈도
한번 보도록 하지요.
printf("%c", 2["ABCDEFGH" - 1]);
결과는 B가 나오겠지요. 한 가지 예제를 더 봅시다.
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ #include <stdio.h> ┃
┃ ┃
┃ void main(void) { ┃
┃ int ia[5] = { 1, 2, 3, 4, 5 }; ┃
┃ int k = 2; ┃
┃ ┃
┃ printf("%d", (k + 1)[ia - 3]); ┃
┃ } ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
결과는 1이겠지요. 왜인지는 1분만 생각해 보면 알 수 있을껄요? ^^;
이번에는 문자열의 일부만 출력하는 방법을 생각해 보도록 합시다. 문자열
은 일반적으로 puts로 출력하지요. 이 함수를 그대로 이용합시다.
char *ps = "ABCDEFG";
puts 함수는 포인터를 인자로 받습니다. 그쵸? 그렇다면 "DEFG"만 출력할려
고 하면 D의 포인터를 넘겨주면 되겠지요. 다음과 같이 말입니다.
puts(&ps[3]); //*************
이해 되시죠? &보다는 []가 우선순위가 위이므로 괄호를 안 붙여도 됩니다.
◐ 증가 연산자와 감소 연산자, 그리고 연산자 최종 정리
우선 다음 예제를 봅시다.
int pi[5] = { 2, 5, 1, 3, 4 };
printf("%d", *pi + 1);
printf("%d", *(pi + 1));
결과가 어떻게 나오나요? 네. 3과 5가 나오지요. 두 결과는 전혀 다른 겁니
다. (전에 *가 + 보다 우선순위가 높다고 했었지요) 첫번째의 경우는 *pi, 즉
2에 1을 더해 3이 된 것이고요. 두번째는 아시다시피 pi + 1의 값을 읽어온
것이겠지요.
다음으로는 증가 연산자와 감소 연산자에 대해 알아 봅시다.
int ia[5] = { 1, 2, 3, 4, 5 };
int *pi = ia;
아시다시피 증가 연산자와 감소 연산자는 그 값 자체를 바꾸기 때문에 상수 //*****
에는 사용이 불가능 합니다. 배열명인 ia는 상수이기 때문에 ia++, ++ia 같은
방법으로는 사용이 불가능하지요. (컴파일시 에러가 발생합니다)
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ void main(void) { ┃
┃ int ia[5] = { 1, 3, 5, 7, 9 }; ┃
┃ int *pi = ia; ┃
┃ ┃
┃ int a, b, c; ┃
┃ a = *pi++; ┃
┃ b = *pi; ┃
┃ c = *++pi; ┃
┃ ┃
┃ printf("%d %d %d", a, b, c); ┃
┃ } ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
이처럼 a, b, c에 미리 값을 대입하는 이유는, 많은 컴파일러에서 printf문
에 직접 증가, 감소 연산자를 기입할 때 정확한 순서로 연산이 되지 않는 문
제점이 있기 때문 입니다. 제가 해 본 결과 BC++ 3.1에서의 결과는 상당히 특
이하게 모두 3이었습니다. 1학기 기말 고사때인가 이 문제가 나서 전부 다 맞
게 해 주었던 적이 있지요.
정확한 결과는 1 3 5 입니다. 증가, 감소 연산자는 *연산자보다 우선 순위
가 높습니다. 하나씩 살펴보도록 하지요.
*pi++은 우선순위에 의해 *(pi++)과 같습니다. 그러나 postfix 연산자는 연
산이 늦게 행해지므로 우선 *pi가 행해져 a에 1이 들어가고 pi++로 인해 pi포
인터가 하나 증가하게 되지요. //???***
만약 (*pi)++이라고 했다면 어떨까요? 우선 *pi로 인해 값을 읽어오긴 하겠
지요. 그러나 ++가 행해지는 대상은 pi가 아닌 *pi 입니다. 다시 말해서 읽어
온 값에 ++를 한다는 것이지요. a에 이 값을 대입했다면 결과는 2였겠지요.
*++pi는 *(++pi)와 같습니다. prefix 연산자이므로 우선적으로 pi를 하나
증가시킨 다음에 그 값을 읽어오게 되지요. 원래의 pi는 3을 가리키고 있었으
므로 하나 증가시킨 위치의 값인 5가 결과가 되겠지요.
++*pi의 경우는 어떨까요? 이것은 ++(*pi)와 같게 되겠지요. 우선순위에 의
해 *(++pi)가 되는 것 아니냐고 하실지도 모르겠는데. 그렇지 않지요. 순서로
볼때 ++가 연산을 행할 대상은 *pi이기 때문이지요. 다시 말씀드려 우선순위
는 두개의 연산자가 동등한 입장일 때 고려가 되는 것입니다. 위에서 *pi++처
럼 양쪽으로 나누어져 있을 때 말이지요. 바로 위에서 *++pi에서도 우선순위
얘기는 하지 않았지요. 당연히 연산할 대상에 더 가까운 것이 먼저 실행되는
법입니다.
◐ 메모리 동적 할당
이제 메모리를 동적 할당하는 방법에 대해서 알아보도록 하지요. 메모리 동
적 할당을 위해서는 alloc.h를 반드시 인클루드 해 주어야 합니다. 안해도 터
보 C에서는 에러 없이 컴파일되지만 실행 중에 문제가 생길 수도 있습니다.
일단 메모리 동적 할당은 malloc과 free를 사용한다는 건 아시겠지요.
char *pc, *pi;
pc = (char *)malloc(sizeof(char)*3);
pi = (int *)malloc(sizeof(int)*3);
위의 예에서 알 수 있듯이 malloc에 넘겨주는 값은 우리가 그 동안 사용했
던 값과는 차이가 있습니다. 우리가 배열에서 사용한 단위는 배열요소의 수를
기준으로 했지만, 여기서는 그 실제 바이트 수를 기준으로 하고 있지요. 그렇
기 때문에 sizeof로 실제 자료형의 크기를 계산하고 있습니다.
malloc은 주어진 크기 만큼의 메모리를 할당하고 그 포인터를 void *형으로
반환 합니다. 메모리 구조는 다음과 같지요.
┏━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃주소┃ 41 42 43 5A 5B 5C 5D 5E ┃
┃ ┃ ┳━┳━┳━┳━━━━━━━━━━━━┳━━━━━━━┳━┳ ┃
┃ 값 ┃ ┃▒┃▒┃▒┃ … ┃41┃00┃00┃00┃ ┃ ┃
┃ ┃ ┻━┻━┻━┻━━━━━━━━━━━━┻━━━━━━━┻━┻ ┃
┃이름┃ pc ┃
┗━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃주소┃ 61 62 63 64 65 66 7A 7B 7C 7D 7E ┃
┃ ┃ ┳━━━┳━━━┳━━━┳━━━━━━┳━━━━━━━┳━┳ ┃
┃ 값 ┃ ┃▒┃▒┃▒┃▒┃▒┃▒┃ … ┃61┃00┃00┃00┃ ┃ ┃
┃ ┃ ┻━━━┻━━━┻━━━┻━━━━━━┻━━━━━━━┻━┻ ┃
┃이름┃ pi ┃
┗━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
할당된 메모리는 초기화 된 것은 아닙니다. 즉, 어떤 값이 들어있을지 모른
다는 것이지요.
이제 일반 배열을 사용하는 것처럼 쓸 수가 있지요.
pc[0] = 1;
pi[2] = 3;
그리고 사용을 끝냈을 때에는 free로 해제를 해 주게 되지요. 반드시 해 주
어야 합니다. 물론 할당한 메모리가 작을 때는 안 해도 큰 문제가 없지만 언
제 문제가 발생할지는 모르는 거지요.
free(pc);
free(pi);
◐ 동적 할당과 인자
다음의 예제를 보세요.
void my_alloc(int *ap, int size) {
ap = (int *)malloc(size);
}
void main(void) {
int *pi;
my_alloc(pi, 10);
// ...
}
이 예제가 과연 제대로 작동을 할까요? 그럴듯 하지만, 전혀 제대로 동작하
지 않는 코드 입니다. 왜일까요? my_alloc 함수로 pi를 넘겼지만 실제로 넘어
간 것은 pi 변수가 아닌 그 변수의 값입니다. 역시 Call by value이기 때문이
지요. 물론 그 변수의 값은 초기화되지 않았으므로 알 수가 없습니다.
그렇게 넘어온 인자인 ap에 다시 malloc의 리턴 값을 대입해 넣지요? 그럼
어떻게 될까요? 네. 그렇지요. 원래 넘어간 pi의 값과는 전혀 관계없는 값이
ap에 들어가고 곧 이어 포인터 변수 ap는 없어져 버리지요. pi의 값은 아직도
초기화되지 않은 채로 남아있는 겁니다. ap에 할당된 메모리는 그 포인터를
가지고 있는 변수가 없으므로 해제할 수도 없게 되어 버리지요.
///??????
초보자들이 흔히 저지르는 실수는 이렇게 동적 할당을 하고는 포인터 관리
를 잘 하지 못하는 데서 일어납니다.
정리하자면, malloc으로 받은 값은 free가 되기 전까지 절대로 프로그래머
가 언제라도 사용할 수 있는 변수의 형태로 남아 있어야 한다는 것이지요. 위
에서 ap의 스코프는 my_alloc 함수이기 때문에 함수가 끝나면 사라지는 건 당
연하지요.
어때요? 너무 어려웠나요? 좀 어려워 보이긴 하지만 천천히 읽어보면 그렇
게 어려울 것도 없지요. 어렵게 생각되더라도, 이 부분만 이해를 했다면 C의
포인터는 이제 아무것도 아닙니다. 그냥 막 주무를 수 있어요. 이제부터는 다
차원 배열 포인터와 구조체 등에 대해 알아볼 겁니다. 역시 여태까지의 부분
을 이해하셨다면 아무것도 아닙니다.
------------------------------------------------------------------------
◐ 다차원 배열
이제 다차원 배열에 대해 알아보도록 합시다. C에서 배열의 차원은 제한이
없지요. 왜일까요? 바로 포인터를 사용해 구현이 되기 때문입니다.
int ia[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };
이런 2차원 배열이 있다고 합시다. a[0][0]이 1, a[0][1]이 2, 이렇게 된다
는건 아시겠지요? 그런데 이 이차원 배열 형태는 실제로 내부적으로는 1차원
배열의 1차원 배열 형태입니다. 사실 C에서는 ?차원 배열이란 건 원래 존재하
질 않는다는 거지요. 3차원 배열은 배열의 배열의 배열 형태… 이렇게 말이에
요. 어렵다고요?
나눠서 생각을 해 봅시다.
int ia1[3] = { 1, 2, 3 };
int ia2[3] = { 4, 5, 6 };
이 두개의 요소가 ia[2]의 각 요소라고 생각하면 되겠지요. 다시 말하자면
int[3]을 하나의 타입으로 보면 되는 겁니다. 타입을 새로 정의해서 생각하면
더 쉬울것 같네요.
typedef int arrayint[3];
크기가 3인 int형 배열을 따로 arrayint형으로 정의한 겁니다. 이제 다음과
같이 하면 크기가 2인 arrayint형 배열을 잡을 수 있지요. int의 [2][3] 배열
과 완전히 동일한 거겠고요.
arrayint ar[2];
여기서 배열과 포인터의 또 하나의 차이점을 말씀드리지요. 배열은 그 배열
이름을 사용해 크기를 알아낼 수 있습니다. 포인터는 그럴 수가 없지요. 다음
을 실행해 보세요. (#include는 제외하겠습니다)
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ void main(void) { ┃
┃ int ar[5], *p = ar; ┃
┃ printf("%d %d %d", sizeof(ar), sizeof(p), sizeof(*p)); ┃
┃ } ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
결과가 어떤가요? 10 4 2가 나오지요. 그렇습니다. 배열명은 그 타입 자체
int[5]를 대표하고 있기 때문에 크기를 알 수 있지만, p의 경우는 그냥 포인
터이므로 크기는 당연히 4가 되겠고, *p는 int형일테니까 2가 되지요.
그럼 다음은 어떨까요?
int ia[2][3];
printf("%d", sizeof(ia[0]));
결과는 6입니다. ia[0]은 사실상 int[3]형이기 때문이지요. ia[1]도 결과는
같을 겁니다. 그러면 sizeof(ia)의 결과를 예상해 보세요. 네. int[2][3]형이
기 때문에 sizeof(int)*2*3인 12가 되겠지요. 어때요? 아시겠나요? 다시 말씀
드리지만 C는 배열의 타입을 정확히 알고 있다는 겁니다. 3차원 배열 이상도
마찬가지 입니다. 물론 좀 복잡하니까 여기서는 다루지 않겠습니다.
◐ 다차원 배열과 포인터
이제부터는 다음의 값을 기준으로 예제를 작성하겠습니다.
int ia[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };
┏━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃주소┃ 41 42 43 44 45 46 47 48 49 4A 4B 4C ┃
┃ ┃ ┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━━━ ┃
┃ 값 ┃ ┃01┃00┃02┃00┃03┃00┃04┃00┃05┃00┃06┃00┃ … ┃
┃ ┃ ╋━━━┻━━━┻━━━╋━━━┻━━━┻━━━╋━━━━━ ┃
┃이름┃ ┗ia[0] ┻ia[1] ┛ ┃
┃ ┃ ┗ia ┛ ┃
┗━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
위의 그림을 잘 보세요. ia는 포인터 상수라고 말씀드렸지요. 그런데 이 경
우는 주소값만이 아닌 크기도 가지고 있다고 했습니다. 이런 포인터 상수를
배열 상수라고 합니다. 포인터 상수에 크기를 더한 개념이지요. 위 그림에서
ia[0]과 ia[1]도 역시 배열 상수 입니다. 이들이 가리키는 내용은 배열 ia의
구성요소이기 때문에 「부분 배열」이라고도 합니다. 다음 표를 보세요.
┏━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃상수┃ ia ┃ ia[0] ┃ ia[1] ┃
┣━━╋━━━━━━━━━╋━━━━━━━━━━╋━━━━━━━━━━┫
┃ 값 ┃ 41 ┃ 41 ┃ 47 ┃
┃크기┃ 12 ┃ 6 ┃ 6 ┃
┃타입┃ int[2][3] ┃ int[3] ┃ int[3] ┃
┗━━┻━━━━━━━━━┻━━━━━━━━━━┻━━━━━━━━━━┛
표를 보니 이해가 가시죠? 이제 ia[1][2]라는 연산이 내부적으로 어떻게 작
동하는지 알아 봅시다. ia[1][2]는 *(*(ia + 1) + 2)와 동일한 코드 입니다.
천천히 살펴보지요. ia + 1의 결과는 무엇이 될까요? 47 입니다. ia의 크기
는 12라고 했는데 왜 47이냐고요? 여기서 더해지는 크기는 ia의 크기가 아닌
ia의 배열요소의 크기이지요.
생각해 보세요. int a[4] 일때 a + 1은 a의 크기인 8이 더해지는 것이 아니
라 a의 배열요소의 크기인 int형 크기 2가 더해지잖아요.
다시 돌아가서 생각해 봅시다. ia의 배열요소는 ia[0]과 ia[1]이겠지요. 이
들의 크기는 표에 있다시피 int[3]이므로 6 입니다. 당연히 결과는 47이 되겠
지요. 그리고나서 *연산자를 사용해 값을 읽습니다.
바로 이때 읽혀오는 값은 무엇일까요? 47번지부터 6만큼의 내용입니다. 바
로 ia[1]이지요. 말씀드렸듯이 ia[1] 자체도 배열 상수 입니다. 배열을 가리
키고 있기 때문이지요. 이렇게 읽혀온 ia[1]은 역시 표에 있듯이 값이 47이고
크기가 6인 배열 상수이지요.
이제부터의 계산은 바로 이 ia[1]이 가리키는 { 4, 5, 6 }만을 가지고 행하
면 됩니다. ia[1]=*(ia + 1)={ 4, 5, 6 } 이라는 거지요. 배열 상수 ia[1]에
다시 2를 더합니다. 이때 실제로 더해지는 주소 값은 sizeof(int)*2인 4이겠
지요. 결과적으로 *(ia + 1) + 2는 4B가 되는 겁니다. 다시 *연산자를 사용해
값을 읽어오니까 최종 결과는 6이 되는 거지요.
ia[1][2]와 동일한 결과지요? 어때요? 꽤 복잡하네요. 2회까지의 내용을 다
이해하면 담부턴 쉽다고 했는데, 너무 어려웠나요? (그럴수도 있죠 머… ^^)
◐ 다차원 포인터 연산
다음 여러개의 코드가 동일하다는 것만 말씀드리고 넘어가지요.
ia[1][2] = *(*(ia + 1) + 2)
= *(ia[1] + 2) = (*(ia + 1))[2] = 1[ia][2] = 2[1[ia]]
어렵죠? 물론 프로그래밍 상에서 이렇게 쓰는 경우는 없습니다. 이렇게 쓰
다가는… ==; 포인터 연산의 이해를 위해서 알아본 것 뿐이지요. 한번 이해만
해 보세요.
◐ 다차원 배열의 포인터
int *ip = ia;
위와 같은 코드를 작성했다고 합시다. 컴파일이 될까요? 컴파일 옵션에 따
라서 될수도 있고 안 될수도 있지만 된다고 해도 문제가 생깁니다. ia는 배열
상수이므로 크기를 갖고 있는데 ip는 포인터 변수이므로 크기를 가지지 못한
다는 것이지요. 결국 ip[1][2]와 같은 방법으로는 접근을 할 수가 없지요. 각
ip[0], ip[1] 등의 크기를 알 방도가 없으니까요. 게다가 ia는 int형의 배열
이 아닌 int[3]의 배열이므로 원래는 대입도 불가능해야 하는 겁니다. 타입이
다르니까요. 다차원 배열의 포인터는 다음과 같이 해 주어야 합니다.
int (*ip)[3] = ia;
하나를 제외한 나머지는 모두 크기를 정해 주어야 한다는 겁니다. 이렇게
해 준다면 ip[1][2]와 같이 ia를 사용할 때와 동일한 방법으로 접근이 가능하
게 되는 것이지요.
┏━━━━━━━┓
ip┃▒┃▒┃▒┃▒┣┓
┗━━━━━━━┛┃
┏━━━━━━━━━━┛
[ ][0] [ ][1] [ ][2] ┃ [ ][0] [ ][1] [ ][2]
ia●━━━┳━━━┳━━━┓ ┗▶●━━━┳━━━┳━━━┓
┃┃01┃00┃02┃00┃03┃00┃[0][ ] ┃┃01┃00┃02┃00┃03┃00┃[0][ ]
┃┣━━━╋━━━╋━━━┫ ┃┣━━━╋━━━╋━━━┫
┃┃04┃00┃05┃00┃06┃00┃[1][ ] ┃┃04┃00┃05┃00┃06┃00┃[1][ ]
┗┗━━━┻━━━┻━━━┛ ┃┣━━━╋━━━╋━━━┫
· · · · .
· · · · .
<다차원 배열> <다차원 배열 포인터>
보시다시피 둘다 가로의 크기는 정해져 있고 똑같습니다. 그러나 ia로는 그
크기를 알 수 있는 반면 ip는 일반 포인터이기 때문에 알 수가 없지요. 그러
면 이런 다차원 배열 포인터를 왜 사용할까요?
물론 여러가지 용도가 있을 수 있겠으나 가장 흔히 사용되는 경우는 바로
함수의 인자로 다차원 배열을 쓰고자 하는 경우입니다. 여러번 말씀드렸지만
배열 상수를 인자로 받을 수는 없다고 했지요. 대신 포인터를 이용해야 합니
다. 다음을 보세요.
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ #include <stdio.h> ┃
┃ ┃
┃ void swap(int *ip1, int *ip2) { ┃
┃ int temp; ┃
┃ temp = *ip1; ┃
┃ *ip1 = *ip2; ┃
┃ *ip2 = temp; ┃
┃ } ┃
┃ ┃
┃ void swaparr(int *ip1, int *ip2) { ┃
┃ swap(&ip1[0], &ip2[0]); ┃
┃ swap(&ip1[1], &ip2[1]); ┃
┃ swap(&ip1[2], &ip2[2]); ┃
┃ } ┃
┃ ┃
┃ void sortarr(int (*ip)[3], int size) { ┃
┃ int i; ┃
┃ for (i = 0; i < size - 1; i++) ┃
┃ if (ip[i][0] > ip[i + 1][0]) swaparr(ip[i], ip[i + 1]); ┃
┃ } ┃
┃ ┃
┃ void printarr(int (*ip)[3], int size) { ┃
┃ int i; ┃
┃ for (i = 0; i < size; i++) ┃
┃ printf("[%d] %d %d %d\n", i, ip[i][0], ip[i][1], ip[i][2]);┃
┃ } ┃
┃ ┃
┃ void main(void) { ┃
┃ int ia[3][3] = { { 3, 5, 2 }, { 2, 6, 8 }, { 4, 2, 6 } }; ┃
┃ ┃
┃ sortarr(ia, 3); ┃
┃ printarr(ia, 3); ┃
┃ } ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
여태까지의 예제 중 가장 긴 예제 같네요. ^^; swap 함수는 다 아시리라 믿
고, swaparr 함수는 일차원 배열(sortarr 함수의 인자인 이차원 배열 포인터
ip의 부분 배열)을 받아 각 요소들을 바꾸어주는 함수이고, sortarr 함수는
[각 배열 요소가 3개의 int값을 가지는 이차원 배열]을 인자로 받아 각 요소
의 첫번째 값을 비교해 정렬을 하는 함수이지요. printarr 함수는 이름대로
출력을 해 주는 함수이고…
결과는 각 부분 배열 { 3, 5, 2 }, { 2, 6, 8 }, { 4, 2, 6 }의 첫번째 요
소의 순서대로 정렬되어 다음과 같이 나오겠지요.
[0] 2 6 8
[1] 3 5 2
[2] 4 2 6
이해가 되시나요? 안 되시면 몇번 더 읽어보시고 정 안 되시면 인자로 넘겨
주는 부분만 알아두세요. swaparr 함수와 swap 함수의 인자가 왜 똑같은지 등
에 대해서 의문이 생길지도 모르겠는데…
흠. 아무래도 분석을 좀 하는게 좋을 거 같네요. 일단 배열 ia의 시작 번지
가 50이라고 합시다. 그러면 마지막 요소인 6의 번지는 66이 되겠지요. ia[0]
도 역시 50일테고, ia[1]은 56, ia[2]는 62일 겁니다.
sortarr(ia, 3)에서 ia는 50이겠지요. sortarr 함수에서의 ip는 포인터 변
수이므로 따로 메모리 어딘가에 생성이 되겠고 그 값은 50이 됩니다. 그리고
는 루프로 0부터 size-1까지 돌면서 현재 부분 배열의 첫번째 값(ip[i][0])과
다음 부분 배열의 첫번째 값(ip[i + 1][0])을 비교해 앞의 값이 크면 순서가
잘못된 것이므로 swaparr 함수를 부르지요.
swaparr 함수로 넘어가는 값은 각 부분 배열의 배열 상수 입니다. 예를 들
어 i가 0인 경우에도 3이 2보다 크므로 swaparr이 불리는데 그 두 인자의 값
은 50과 56이 된다는 거지요. ip와 ia의 값이 같기 때문에 ip[0]과 ip[1]도
각각 ia[0]과 ia[1]과 같으니까요. ip와 ia의 차이는 단지 크기를 알 수 있는
지 없는지의 차이라고 했었지요. 가지고 있는 값은 차이가 없고 똑같이 어떤
주소를 포인트할 수 있는 것이지요.
이렇게 넘어온 50과 56이라는 값이 ip1, ip2라는 포인터 변수에 들어가게
되겠지요. 그리고 swaparr 함수에서는 []연산자를 사용해 각각의 int형 요소
에 접근을 하고 다시 그 주소를 구합니다. 그 부분만 보도록 하지요.
swap(&ip1[0], &ip2[0]);
ip1은 { 3, 5, 2 }라는 배열을 가리키고 있고, ip2는 { 2, 6, 8 }이라는 배
열을 가리키고 있겠지요. 결과적으로 ip1[0]은 3, ip2[0]은 2일 겁니다. 그리
고 다시 각각의 주소를 구하지요. 실제로 swap 함수에 넘어가는 값은 3과 2의
주소인 50과 56 입니다. 그리고 swap 함수에서는 *연산자를 사용해 두 주소의
값을 서로 바꾸게 되는 거지요.
어렵긴 하지만, 하나씩 차근차근 생각해 보세요.
◐ 포인터 배열
여태까지 배열의 포인터 때문에 머리 아파 죽겠는데, 이젠 또 포인터의 배
열이라니… ==; 하지만 역시 잘 생각해 보면 별게 아닙니다. ==;
포인터도 자료형의 하나이지요. 게다가 크기도 일정하고(4 바이트) 단순히
하나의 값을 가진 변수일 뿐입니다. 그쵸? 그렇기 때문에 역시 배열이 될수도
있는 거지요.
우선 이차원 배열을 보도록 합시다. 다른건 제쳐두고 문자열 배열만을 가지
고 생각해 보도록 하지요.
char seoulgu1[10][16];
strcpy(seoulgu1[0], "Kangnam-gu");
strcpy(seoulgu1[1], "Mapo-gu");
strcpy(seoulgu1[2], "Chung-gu");
strcpy(seoulgu1[3], "Kwanak-gu");
strcpy(seoulgu1[4], "Nowon-gu");
strcpy(seoulgu1[5], "Seoungbuk-gu");
strcpy(seoulgu1[6], "Seocho-gu");
strcpy(seoulgu1[7], "Dobong-gu");
strcpy(seoulgu1[8], "Youngdeungpo-gu");
strcpy(seoulgu1[9], "Songpa-gu");
위의 예에서 seoulgu1라는 배열이 차지하는 메모리는 과연 얼마일까요? 금
방 계산이 되지요? ^^ 바로 160 바이트 입니다. 가장 긴 "영등포구"가 널 문
자까지 포함해서 16자이기 때문에 배열로 잡을려면 이렇게 해야 하는 거지요.
그런데 과연 이것뿐일까요? 아닙니다. 전에도 말씀드렸다시피 문자열의 경우
는 따로 변수에서 사용되지 않아도 이미 메모리에 올라와 있습니다. 그러니까
위의 배열이 들어있는 소스를 컴파일하면 저 문자열들 자체가 실행 파일에 포
함되고 저 배열이 선언되기도 전부터 메모리에 존재한다는 것입니다. 그렇기
때문에 따로 105 바이트의 메모리가 필요하다는 것이지요.
이번에는 포인터의 배열로 생각해 봅시다. 우리가 처음에 문자열 배열에 대
해서 다룰 때 다음과 같이 썼었지요.
char *sp = "Love";
이미 아시겠지만 sp에는 단 5바이트 만이 할당되었습니다. 그쵸? 물론 sp라
는 포인터 변수가 차지하는 4바이트가 따로 있기 때문에 실제로는 9바이트가
사용되었겠지요. 이 경우도 역시 "Love"가 실행 파일에 포함되어 포인터 변수
sp가 선언되기 전부터 메모리에 올라와 있습니다. 그런데 sp는 포인터 변수이
기 때문에 "Love"가 존재하는 메모리의 주소가 직접 대입되기 때문에 위의 예
제처럼 따로 메모리를 할당하지 않는다는 것이지요.
char *seoulgu2[10];
seoulgu2[0] = "Kangnam-gu";
seoulgu2[1] = "Mapo-gu";
seoulgu2[2] = "Chung-gu";
seoulgu2[3] = "Kwanak-gu";
seoulgu2[4] = "Nowon-gu";
seoulgu2[5] = "Seoungbuk-gu";
seoulgu2[6] = "Seocho-gu";
seoulgu2[7] = "Dobong-gu";
seoulgu2[8] = "Youngdeungpo-gu";
seoulgu2[9] = "Songpa-gu";
이 경우는 위의 문자열들이 실제로 차지한 105바이트에 문자열 포인터 10개
인 40바이트만 더하면 위의 예제가 사용한 총 메모리 크기가 계산된다는 것이
지요. 훨씬 효율적인 것을 알 수 있습니다.
여기서 한가지 주의할 점이 있습니다. 다음과 같은 코드가 있다고 합시다.
char seoulgu1[10][16], *seoulgu2[10];
strcpy( ... );
...
seoulgu2 = seoulgu1;
초기화가 모두 된 이차원 배열인 seoulgu1를 일차원 배열 포인터 seoulgu2
에 대입을 하네요. 이게 가능할까요? 불가능한 것입니다. 다음을 보세요.
char (*sp)[3];
char *sp[3];
위의 내용은 전에 배웠듯이 부분 배열의 요소의 개수가 3개인 이차원 배열
의 포인터이지요. 아래의 내용은 방금 배운 것처럼 포인터를 3개 담을 수 있
는 일차원 포인터 배열 입니다. 구분이 가시나요? 괄호 하나 빼놓고 전부 똑
같지만 의미는 판이하게 다릅니다. 그림으로 보도록 하지요.
char (*sp)[3];
sp [0] [1] [2]
┏━━━━━━━┓ ┏━┳━┳━┓
┃ ┃ ┃ ┃ ┣━━━━━━━━━━▶┃ ┃ ┃ ┃[0]
┗━━━━━━━┛ ┣━╋━╋━┫
┃ ┃ ┃ ┃[1]
┣━╋━╋━┫
· · ·
· · ·
char *sp[3];
sp
┏━━━━━━━┓ sp[0] ┏━┳━┳━┳━┳━┳━┳
┃ ┃ ┃ ┃ ┣━━━━━━━━━━▶┃ ┃ ┃ ┃ ┃ ┃ ┃…
┣━━━━━━━┫ ┗━┻━┻━┻━┻━┻━┻
┃ ┃ ┃ ┃ ┣━━━━━┓ sp[1] ┏━┳━┳━┳━┳━┳━┳
┣━━━━━━━┫ ┗━━━━▶┃ ┃ ┃ ┃ ┃ ┃ ┃…
┃ ┃ ┃ ┃ ┣━━━┓ ┗━┻━┻━┻━┻━┻━┻
┗━━━━━━━┛ ┃ sp[2] ┏━┳━┳━┳━┳━┳━┳
┗━━━━━━▶┃ ┃ ┃ ┃ ┃ ┃ ┃…
┗━┻━┻━┻━┻━┻━┻
굳이 대입을 하고 싶다면 seoulgu2[0] = seoulgu1[0] 같이 일일히 해야하지
만, 그러면 의미가 많이 바뀌게 되겠지요.
제가 여기서 포인터 배열이 이차원 배열보다 무지무지 좋은 것처럼 말씀을
드리긴 했지만 항상 그런건 결코 아닙니다. seoulgu1에 문자열을 대입할 때
각각의 문자열을 strcpy로 대입하지 않고 선언시에 초기화 했다고 합시다.
char seoulgu1[10][16] = { "Kangnam-gu",
"Mapo-gu",
"Chung-gu",
"Kwanak-gu",
"Nowon-gu",
"Seoungbuk-gu",
"Seocho-gu",
"Dobong-gu",
"Youngdeungpo-gu",
"Songpa-gu" };
이 경우에 사용되는 메모리는 정확히 160 바이트 입니다. 왜일까요? 선언과
동시에 초기화 되면 seoulgu1은 그대로 배열 상수로 사용되어 포인터 배열처
럼 메모리에 존재하는 문자열들을 직접 포인트하기 때문이지요. 또한 상수이
기 때문에 포인터 배열처럼 따로 메모리를 차지하지도 않지요. 그러면 이차원
배열이 더 좋은 걸수도 있겠네요.
그렇습니다. 이들은 사용하는 방법 등에 따라서 각기 더 좋을 수도 있고 더
나쁠 수도 있는 것이지요. 이차원 배열이 더 좋은 경우는 문자열들의 길이가
크게 차이가 나지 않는 경우 입니다. 반면에 문자열들의 길이가 크게 차이나
는 경우는 그 크기가 일괄적으로 잡히는 이차원 배열보다는 문자열들의 길이
를 모두 다르게 잡을 수 있는 포인터 배열이 더 유리한 것이지요.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◐ 세번째 스터디가 끝났습니다. 다음 스터디를 기대해 주세요. 이해가 안 되
시는 내용이나 궁금한 사항이나 하여튼 아무 말이라도 하고 싶으시면 언제
라도 메일 주세요. (여태까지 볼랜드씨를 구한다는 메일 한통을 제외하고
는 단 한 통도 안 왔네요. 다들 잘 아시나봐요. 다 아시는데 괜히 하는 건
아닐지… ==;)
◐ 다음 문제들을 한번 풀어 보세요.
① 배열의 첨자가 0에서 시작하는 이유는?
<힌트> 배열의 []연산자가 실제로는 어떻게 사용되는지 생각해 보세요.
② int a[6];
a[0] = 1[a] = *(a + 2) = *(3 + a) = 0;
4[a] = "ABC"[0];
a[5] = 1["DEF" + 1];
위의 예에서 a의 각 요소 0부터 5까지의 값을 예상해 보세요.
③ char *sp = "School of Computing";에서 puts 함수로 "Computing"만 출
력해 보세요.
④ 증가 연산자를 사용해 문자열의 길이를 계산하는 함수를 만드세요.
⑤ int ia[2][3][4];에서 ia의 주소가 30이라고 할때 다음 각각의 주소와
타입, 크기를 예상해 보세요.
=> ia[1], ia[0][3], ia[1][2][0], ia[0], ia[0][1]
⑥ 다음과 같은 형태의 문자열 배열을 할당하는 방법을 생각해 보세요.
[0] [1] [2] [3] [4] [5] [6] [7] [8]
┏━┳━┳━┳━┳━┳━┳━┳━┳━┓
[0]┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃
┣━╋━╋━╋━╋━┻━┻━┻━┻━┛
[1]┃ ┃ ┃ ┃ ┃
┣━╋━╋━╋━╋━┳━┳━┓
[2]┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃
┣━╋━╋━╋━╋━╋━╋━┛
[3]┃ ┃ ┃ ┃ ┃ ┃ ┃
┗━┻━┻━┻━┻━┻━┛
◐ 정답(?)은 다음 시간에… ^^;
-------------------------------------------------------------------------
[정답풀이]
① 배열의 첨자가 0에서 시작하는 이유는?
▶ 힌트에서 배열의 []연산자가 실제로는 어떻게 전개되는지를 생각해 보라고
했었지요. "*(포인터변수 + 숫자)"와 같은 형태로 전개되기 때문에 만약에
첫 첨자가 1이라면 위 전개식에서의 숫자와 배열의 첨자가 직접적으로 매
치가 되지 않겠지요. 아무래도 매치가 되는 것이 개념적으로나 실제 컴파
일러 구현 상으로나 유리한건 당연할 겁니다.
② int a[6];
a[0] = 1[a] = *(a + 2) = *(3 + a) = 0;
4[a] = "ABC"[0];
a[5] = 1["DEF" + 1];
위의 예에서 a의 각 요소 0부터 5까지의 값을 예상해 보세요.
▶ 두번째 시간엔가 다루었던 내용과 유사하지요. a[?], ?[a], *(a + ?)등은
사실상은 모두 동일한 코드라고 했었지요. 즉 둘째줄의 코드는 a의 0번째
부터 3번째 까지의 요소에 모두 0을 넣으라는 코드지요. 그 다음 줄은 문
자열 "ABC"의 0번째인 'A'를 a[4]에 넣으라는 얘기고, 마지막 줄은 "DEF"
의 2번째인 "F"를 a[5]에 넣으라는 얘기겠지요. 이해가 안가시면 다시 전
의 강좌를 찾아보세요.
③ char *sp = "School of Computing";에서 puts 함수로 "Computing"만 출력
해 보세요.
▶ 이것도 다루었던 내용 그대로 입니다. 'C' 문자의 포인터를 puts 함수로
넘겨주면 되겠지요. puts(&sp[10]); 입니다.
④ 증가 연산자를 사용해 문자열의 길이를 계산하는 함수를 만드세요.
▶ 이걸 원래 문자열 포인터 부분에서 앞의 배열을 이용한 것과 비교해 설명
을 할까 하다가 그냥 문제로 냈는데요. 그렇게 어렵지는 않았을 겁니다.
int my_strlen_ptr(char *sp) {
int len;
for (len = 0; *sp != '\0'; sp++, len++);
return len;
}
⑤ int ia[2][3][4];에서 ia의 주소가 30이라고 할때 다음 각각의 주소와 타
입, 크기를 예상해 보세요.
=> ia[1], ia[0][3], ia[1][2][0], ia[0], ia[0][1]
▶ ┏━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┓
┃ ┃ 주소 ┃ 타입 ┃ 크기 ┃
┣━━━━━━╋━━━━━━━╋━━━━━━━╋━━━━━━━┫
┃ia[1] ┃ 54 ┃int[3][4] ┃ 24 ┃
┃ia[0][3] ┃ 54 ┃int[4] ┃ 8 ┃
┃ia[1][2][0] ┃ 70 ┃int ┃ 2 ┃
┃ia[0] ┃ 30 ┃int[3][4] ┃ 24 ┃
┃ia[0][1] ┃ 38 ┃int[4] ┃ 8 ┃
┗━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━┛
위의 표로 미루어 볼때 ia[0][1][0]과 ia[0][0][4]는 값이 동일하겠지요.
어짜피 *()식으로 바뀔때는 동일한 결과가 되기 때문에 위처럼 전혀 달라
보이는 식이라도 같아질 수가 있습니다. 다시 말씀드리자면 C에서는 배열
의 요소가 몇개이건 간에 []로 그 밖의 범위까지도 참조가 가능하다는 것
이지요. (물론 그 밖의 범위를 참조할 필요는 없지만…) 이렇게 각 요소
의 첨자와 실제 위치가 정확히 1:1로 매치가 되지는 않기 때문에 다차원
배열을 선언할 때 다음과 같은 사용법도 가능합니다.
int a[2][3] = { 1, 2, 3, 4, 5, 6 };
원래는 아래와 같이 해 주어야 하겠지요.
int a[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };
⑥ 다음과 같은 형태의 문자열 배열을 할당하는 방법을 생각해 보세요.
[0] [1] [2] [3] [4] [5] [6] [7] [8]
┏━┳━┳━┳━┳━┳━┳━┳━┳━┓
[0]┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃
┣━╋━╋━╋━╋━┻━┻━┻━┻━┛
[1]┃ ┃ ┃ ┃ ┃
┣━╋━╋━╋━╋━┳━┳━┓
[2]┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃
┣━╋━╋━╋━╋━╋━╋━┛
[3]┃ ┃ ┃ ┃ ┃ ┃ ┃
┗━┻━┻━┻━┻━┻━┛
▶ 포인터 배열을 사용해 동적으로 할당해야 하겠지요.
char *ca[4];
ca[0] = (char *)malloc(9);
ca[1] = (char *)malloc(4);
ca[2] = (char *)malloc(7);
ca[3] = (char *)malloc(6);
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
■ 4. 포인터의 포인터 ■
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◐ 다중 포인터
이번에는 포인터의 포인터 입니다. 좀 복잡할 거 같기도 하지만… 제가 누
누히 말씀드린게 있지요. 포인터는 단순히 그냥 숫자 하나를 저장하는 변수라
고요. 포인터 자체도 변수이기 때문에 메모리에 존재하고 그 주소가 존재한다
는 말씀도 이미 드렸었지요. 그리고 역시 그 주소를 가리키는 포인터도 존재
할 수 있는 거지요.
int i = 3;
int *pi = &i;
int **ppi = π
┏━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃주소┃ 41 42 50 51 52 53 74 75 76 77 78 ┃
┃ ┃ ┳━━━┳ ┳━━━━━━━┳ ┳━━━━━━━┳━┳ ┃
┃ 값 ┃ ┃03┃00┃… …┃41┃00┃00┃00┃… …┃50┃00┃00┃00┃ ┃ ┃
┃ ┃ ┻━━━┻ ┻━━━━━━━┻ ┻━━━━━━━┻━┻ ┃
┃이름┃ i pi ppi ┃
┗━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
포인터에 대해 안다면 전혀 어려운 개념이 아닙니다. 그쵸?
int **ppi는 int *(*ppi)와 같은 것이므로, 『int 포인터형의 포인터 변수
ppi』가 되는 것이지요. 물론 이 ppi 변수의 포인터 변수를 만드는 것도 가능
하지요.
int ***pppi = &ppi;
물론 이런 경우는 거의 쓰지 않습니다. 이중 포인터(포인터의 포인터) 정도
까지는 가끔씩 쓰는 일이 있을 겁니다. 위의 네개의 선언 과정을 거쳤다면 각
각의 변수의 값은 다음과 같습니다. (pppi는 80번지부터 83번지까지라고 가정
합시다)
┏━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┓
┃ ┃ i ┃ pi ┃ ppi ┃ pppi ┃
┣━━━╋━━━━━━╋━━━━━━╋━━━━━━╋━━━━━━┫
┃ &값 ┃ 41 ┃ 50 ┃ 74 ┃ 80 ┃
┃ 값 ┃ 3 ┃ 41 ┃ 50 ┃ 74 ┃
┃ *값 ┃ X ┃ 3 ┃ 41 ┃ 50 ┃
┃ **값 ┃ X ┃ X ┃ 3 ┃ 41 ┃
┃ ***값┃ X ┃ X ┃ X ┃ 3 ┃
┗━━━┻━━━━━━┻━━━━━━┻━━━━━━┻━━━━━━┛
위의 표를 잘 보면 왼쪽 위에서 오른쪽 아래 방향으로 같은 값을 갖는 것을
볼 수 있지요. &와 *의 관계도 한눈에 알 수 있을 겁니다. 그런데 왜 *는 세
개씩도 쓰는데 &는 한개밖에 쓰지 않을까요. &는 변수에만 붙일 수 있기 때문
이지요. 만약 &&i라고 쓰면 &(&i)가 되는데 i의 주소는 41이고 이 값은 상수
지요. 그렇기 때문에 &는 여러개씩 중복해서 사용할 수가 없는 겁니다.
그러면 이런 다중 포인터를 왜 사용하는 걸까요? 이전 스터디 시간에 설명
드렸듯이 포인터 배열은 int *[] 같은 형태를 가지지요. 이 포인터 배열의 포
인터를 받기 위해서는 이중 포인터를 사용해야만 합니다. 함수에서는 배열 자
체를 받지 못하고 그 포인터를 받는 방법밖에 없으니까요.
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ void printstrs(char **strs, int n) { ┃
┃ int i; ┃
┃ for (i = 0; i < n; i++) printf("%s\n", strs[i]); ┃
┃ } ┃
┃ ┃
┃ void main() { ┃
┃ char *strs[3] = { "Hello", "My name is", "pijean" }; ┃
┃ printstrs(strs, 3); ┃
┃ } ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃ <결과> ┃
┃ Hello ┃
┃ My name is ┃
┃ pijean ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
◐ main 함수의 인자
위의 예제와 같은 경우를 자주 써야 하는 경우가 있습니다. 바로 main 함수
의 인자를 받는 경우 입니다. main 함수는 C에서 가장 기본적인 함수지요. 그
런데 이 함수도 인자를 받을 수 있다는 것 아시나요? 우리가 어떤 프로그램을
실행시킬 때 다음과 같이 하지요. 예를 들어 압축을 풀기 위해 ARJ라는 프로
그램을 실행할 때,
ARJ x pijean.arj *.*
우리가 여태까지 썼던 void main()나 void main(void)로는 위처럼 주어지는
값들을 프로그램 내에서 사용할 수가 없었습니다. 그렇다면 어떻게 해야 할까
요? 바로 main 함수의 인자를 다음과 같이 해 주어야 합니다.
void main(int argc, char *argv[])
첫번째의 argc는 주어지는 값들의 개수 입니다. 위의 ARJ 예제의 경우에는
이 값이 4가 됩니다. 처음의 ARJ, 즉 프로그램의 이름 자체도 하나의 인자로
보는 것이지요. argv는 주어지는 문자열 배열의 포인터이지요. (물론 위의 경
우에 argc와 argv 같은 이름은 프로그래머가 마음대로 정할 수 있습니다. 순
서는 그대로 해야겠지요)
argv[0] = "ARJ"
argv[1] = "x"
argv[2] = "pijean.arj"
argv[3] = "*.*"
위와 같다는 것이지요. 그럼 한번 숫자를 여러개 입력 받아서 모두 더해서
출력하는 프로그램을 만들어 보세요. 어렵지 않을 겁니다.
------------------------------------------------------------------------
◐ 포인터란
변수는 메모리에 어떤 위치에 저장이 되고 그 위치를 『주소』라고 합니다.
주소를 다른 말로 『포인터 상수』나 그냥 『포인터』라고 하기도 하지요. 그
냥 우리가 사는 집의 주소를 생각하면 됩니다. 파이네 주소는 122-81번지. ^^
이 주소(포인터 상수)를 저장하는 변수가 『포인터 변수』 입니다. 이것 역
시 일반적으로 『포인터』라고도 부릅니다. (이후부터는 포인터 상수와 포인
터 변수를 똑같이 포인터라고 사용하는 경우가 많을 겁니다) 122-81 번지라는
주소가 적힌 종이로 파이네 집을 찾는다고 할때 그 종이가 포인터 변수가 되
는 것이지요.
아시다시피 포인터는 일반적으로 4바이트의 크기를 가지며 타입 또한 가집
니다. 이 타입은 포인터 자체와는 관계가 없지만, 그 포인터가 가리키는 메모
리나 변수의 값에 접근할 때 포인터의 타입의 크기 만큼을 읽어오거나 쓰게되
는 것이지요. 집들도 아주아주 큰 경우는 여러개의 주소를 가지기도 하죠. 그
거랑 똑같습니다. 파이네 집이 아주아주 커서 122-81, 122-82번지를 가진다고
할때 파이네 옆집의 주소는 122-83번지가 되는 거겠지요. 파이네 집의 122-82
번지는 쓸 필요가 없게 됩니다. 122-81번지만으로도 찾을 수 있으니까요.
포인터 변수는 타입에 *를 붙여서 선언 합니다. 그 타입의 변수를 가리키는
변수라는 것이지요. 이때의 타입은 C가 기본적으로 제공하는 것 이외에 직접
만든 타입도 모두 가능합니다.
*연산자는 포인터가 가리키는 메모리나 변수의 값에 접근할 때 사용합니다.
주소를 통해서 값에 접근하는 연산자라는 것이지요. 우편 집배원 아저씨라고
생각을 하면 쉽습니다. 아파트 같은 곳은 다르지만, 일반 가정집을 우리가 주
소만 가지고 찾아다니기는 매우 힘듭니다. 우리가 일반 가정집으로 편지를 썼
다고 할때 우편 집배원 아저씨는 그 주소의 집을 바로 찾아서 편지를 배달해
주는 것이지요. (핫핫. 스터디 내용이 동화틱해지는… ^^)
&연산자는 문패라고 생각하면 됩니다. 문패에는 주소가 적혀 있지요. 어느
집인지는 아는데 그 주소를 모를때 우리는 문패를 봅니다. (어느 집인지 안다
는 것은 변수의 이름을 안다는 것과 같겠지요. 즉 &연산자는 변수에만 붙일수
있습니다)
◐ 배열과 포인터
배열은 집들이 한줄로 쭈욱 이어져 있다고 생각하면 됩니다. 일반적인 변수
들은 저~ 농촌의 시골 마을처럼 띄엄띄엄 떨어져 있지요. 그런데 이 배열이란
것은 한줄로 쭈욱 있는 경우 입니다. 1층짜리 집들이 한줄로 있는 마을을 생
각해 봅시다. 마을의 이름은 신촌 입니다. 신촌의 첫번째 집의 주소가 100번
지라고 하지요. 마을에 10개의 집이 있다고 하면 신촌에는 109번지까지의 주
소가 존재할 겁니다. (char형 배열과 똑같지요)
집 신촌[10];
char sinchon[10];
집이 10개 존재하는 신촌이라는 마을은 위처럼 선언하면 되는 겁니다. ^^;
신촌[0]은 100번지의 집이고 신촌[1]은 101번지의 집이지요. 그리고 우리가
그냥 신촌이라고 할때는 100번지부터 시작하니까 100번지를 말하는 것이 됩니
다. *(신촌 + 2)는 신촌의 번지인 100에서 2만큼을 더한 102번지의 집을 말하
는 거겠지요.
다음과 같은 경우를 생각해 봅시다.
큰집 달동네[10];
int daldongne[10];
달동네의 집은 모두들 부자라서 2개씩의 주소를 가진다고 하지요. 달동네
첫번째 집의 주소는 200번지 입니다. 그렇다면 옆집의 주소는 당연히 202번지
가 되는 거겠지요. (int형 배열하고 똑같네요)
위의 두 동네에 우편 집배원 아저씨가 우편물을 배달한다고 합시다. 각각의
집마다 하나씩의 우편물이 왔을 때 각 집을 모두 다녀야겠지요. (배달되는 우
편물은 생각하지 맙시다. ==;)
집_마을에_배달(집_마을 주소) {
int 위치;
for (위치=0; 위치<10; 위치++) 배달(주소[위치]);
}
// void baedal_to_char_village(char *addr) {
// int pos;
// for (pos=0; pos<10; pos++) baedal(addr[pos]);
// }
큰집_마을에_배달(큰집_마을 주소) {
int 위치;
for (위치=0; 위치<10; 위치++) 배달(주소[위치]);
}
void main() {
집 신촌[10] = { 경수, 태겸, 일재형, 근필형, 동현,
병서, 상엽, 성수, 수헌, 영현 };
큰집 달동네[10] = { 용형, 은성, 태헌, 효중, 경준,
인석, 지웅, 명현, 재원, 민우 };
// 읔. 내 앞에서 짤렸네…
집_마을에_배달(신촌);
큰집_마을에_배달(달동네);
}
// char sinchon[10] = { ... };
// baedal_to_char_village(sinchon);
(이렇게 써놓고 보니 이야기의 매크로나 씨앗이라는 언어가 생각나네요. 한
글을 사용한 프로그래밍 도구인데 범용성이 좀 떨어져서 실패한 언어의 한가
지지요.)
위의 두개의 마을에_배달 함수는 모두 똑같지만 각 집이 차지하는 주소의
수가 다른 거지요. 여기서 위의 *() 연산자를 한번 더 볼까요.
(신촌 + 4)와 *(신촌 + 4)의 차이점을 말씀드리지요. 앞의 (신촌 + 4)는 동
현이네 "집"(=주소) 입니다. 아시겠지요. 그 집에 있는 사람이 바로 동현이인
것이고 이걸 *(신촌 + 4)로 표현할 수 있겠지요. ^^;
위의 마을에_배달 함수를 ++연산자를 사용해 바꿔볼까요.
집_마을에_배달(집_마을 주소, int 집수) {
for (; 주소<주소+집수; 주소++) 배달(*주소);
}
큰집_마을에_배달(큰집_마을 주소, int 집수) {
for (; 주소<주소+집수; 주소++) 배달(*주소);
}
큰집_마을의 경우는 각 집이 2개의 번지를 가지므로 주소++에서 주소의 값
이 실제로는 2씩 늘어나게 되는 것이지요.
이제 동적 할당에 대해 알아보도록 하지요. 우리가 이미 배웠다시피 메모리
의 동적 할당은 malloc을 사용 합니다. 가끔 calloc라는 함수가 사용되는 경
우도 있는데 이 두 함수의 차이점은, malloc은 그냥 메모리를 할당만 하는데
반해서 calloc은 할당한 메모리를 모두 0으로 초기화 한다는 것입니다. 다시
말해서 malloc으로 할당한 메모리에는 할당하기 전의 값들이 그대로 들어 있
다는 것이지요. calloc 처음의 c는 clear 정도로 생각해도 되겠지요.
동적 할당은 마을을 하나 새로 만든다고 생각하면 되겠지요.
집_마을 새동네 = (집_마을)malloc(sizeof(집)*10);
char *newvillage = (char *)malloc(sizeof(char)*10);
◐ 다차원 배열
아파트를 생각해 봅시다. 각 층별로 통로가 있는 대형 아파트를 보도록 하
지요. 아파트 이름은 누리마을 이라고 합시다. (요즘은 아파트 이름을 마을로
짓는 경우가 많이 있더군요. 특히 신도시에 가보면…) 5층의 누리마을 아파트
에 각 층별로 10개씩의 집이 있다면 아파트 전체가 차지하는 주소는 50개가
되겠지요. 그리고 누리마을 아파트 첫집 000호의 주소를 20이라고 합시다. 이
해하기 쉽도록 0층, 0호부터 시작하겠습니다.
집 누리마을[5][10];
char nuri_village[5][10];
쉽게 생각하기 위해 층별로 나누어 봅시다.
typedef 집 누리마을_각층[10];
typedef char nuri_village_chung[10];
이제 누리마을_각층은 10개의 집을 가질 수 있는 타입이 되었습니다.
누리마을_각층 누리마을0층;
누리마을_각층 누리마을1층;
누리마을_각층 누리마을2층;
누리마을_각층 누리마을3층;
누리마을_각층 누리마을4층;
누리마을_각층 누리마을[5] = { 누리마을0층, 누리마을1층, 누리마을2층,
누리마을3층, 누리마을4층 };
// 실제로 이렇게 나누어서 조합을 할수는 없습니다.
// 그 이유는 아래에 설명하도록 하지요.
우리가 그냥 누리마을이라고 할때 누리마을을 다른 마을들과 구별할 수 있
는 이름 이외의 척도가 무엇이 있을까요. 바로 첫번째 집의 주소가 있습니다.
그래서 그냥 누리마을이라고 쓰면 20과 같은 것이 되는 거지요.
누리마을0층의 경우는 어떤가요. 네. 역시 20 입니다. 누리마을1층은 30이
되겠고, 이렇게 계속해서 누리마을4층은 60이 되겠지요. 그렇기 때문에 위의
코드처럼 C에서 나누어서 조합을 했을 경우 누리마을0층은 사실은 20이라는
값일 뿐 누리마을0층 10개의 집 자체를 나타내지 않기 때문에 불가능하다는
것이지요. "*누리마을1층"은 어떨까요. 역시 불가능 합니다. 이론상은 가능
해야하지만, 그게 그렇지 않더군요. 일차원 배열의 내용을 이차원 배열의 부
분에 넣고 싶을 때는 하나씩 복사를 해 주어야만 합니다.
char s1[10] = { ... };
char s2[10] = { ... };
char s[2][10];
s[0] = s1; // 이렇게는 안된다는 겁니다.
s[0] = *s1; // 이렇게도 안되요.
C에서는 배열을 포인터로 구현하고 있기 때문에 배열을 한번에 통째로 복사
를 할수가 없기 때문 입니다. 문자열 복사도 strcpy라는 별도의 함수를 써서
내부적으로 일일히 복사해 주고 있지요.
다시 앞으로 돌아가서, 누리마을[0]은 20, 누리마을[1]은 30, … 이렇게 된
다고 했었지요. 이런 것들을 부분 배열이라고 하는 겁니다. 누리마을[0]은 다
시 10개의 집이라는 배열을 가지고 있으니까요.
누리마을[0][0]은 000호, 누리마을[0][1]은 001호, 누리마을[1][0]은 100호
처럼 생각하면 됩니다. *누리마을[0]은 0층 전체, *누리마을[1]은 1층 전체…
이렇게 생각할 수도 있겠지요. 그럼 *() 연산자를 볼까요.
*(*(누리마을 + 2) + 3)
위의 내용은 누리마을[2][3]과 같다는 건 아실 겁니다. 하나씩 분석해 보도
록 하죠.
누리마을 + 2는 &누리마을[2], 누리마을2층과 같습니다. 실제 값은 40이지
요. 여기에 *를 붙이게 되면 바로 누리마을2층 전체가 되는 겁니다. 그쵸? 위
의 연산식을 다시 해석해 보면 다음과 같습니다.
*(*누리마을2층 + 3) = *(누리마을2층_전체 + 3)
그냥 누리마을2층은 누리마을2층의 주소(실제로는 첫집의 주소겠죠)를 말하
고, 누리마을2층_전체는 *누리마을2층, 즉 누리마을2층의 10개의 집 전부를 말
합니다.
이제 누리마을 전체에 대해서는 생각할 필요 없이 2층만 생각합시다. 누리마
을2층_전체에 3을 더하면 바로 2층의 세번째 집이 되는 거겠지요. 어때요? 이
해가 가시나요?
(*누리마을2층)[3] = 누리마을2층_전체[3] = 누리마을[2][3]
이제부터는 다차원 배열의 포인터를 보도록 하지요. 바로 위에서 설명한 것
이 다차원 배열이지요? 누리마을의 포인터는 무엇인가요? 네. 20 입니다. 이
값을 저장할 포인터가 누리마을의 포인터 변수이지요.
누리마을_각층 *누리마을_포인터 = 누리마을
이렇게 하면 누리마을_포인터의 값은 20이 되고 누리마을의 첫번째 집을 가
리키게 되는 것이지요. 다차원 배열의 포인터는 다차원 배열을 함수에 넘겨줄
때 사용한다고 했습니다. 역시 우편 집배원 아저씨가 누리마을에 배달을 한다
고 하면,
누리마을에_배달(집 (*주소)[10], int 층수)
// = 누리마을에_배달(누리마을_각층 *주소, int 층수)
int 층, 위치;
for (층=0; 층<층수; 층++)
for (위치=0; 위치<10; 위치++) 배달(주소[층][위치]);
}
위와 같이 되는 것이지요. 이제는 포인터 배열을 알아 봅시다.
포인터 배열은 뭘 생각해 볼까요. 다이어리의 주소록을 생각해 봅시다. 주
소록에는 여러 집들의 주소가 적혀 있고 각 주소들은 그 주소의 집을 가리키
는 일종의 포인터 이지요. 이들 주소는 꼭 하나의 집들만 가리킬 필요도 없지
요. "누리마을 : 20번지" 이렇게 써 놓았다면 누리마을 아파트 전체를 포인트
하는 것이고… 더 얘기할 것도 없네요. 주소록… 쉽죠? ^^
char *ptr_ar[5];
// char (*ptr_ar)[5]와는 근본적으로 다릅니다!
ptr_ar[0] = (char *)malloc(10);
ptr_ar[1] = (char *)malloc(15);
ptr_ar[2] = (char *)malloc(11);
ptr_ar[3] = (char *)malloc(52);
ptr_ar[4] = (char *)malloc(16);
지난 시간에 설명드린 문자열 포인터 배열의 할당 방법 그대로지요.
포인터의 포인터. 헐헐. 이것 역시… 다이어리 어딘가에 "파이네집 주소는
파이 다이어리의 맨 마지막장에 써 있다"라는 문구가 있다고 하죠. 다이어리
맨 마지막장의 주소란에 있는 주소는 바로 파이네 집을 가리키는 포인터이고
위에서 말한 문구는 그 포인터를 가리키는 포인터인 거죠.
-----------------------------------------------------------------
◐ 구조체와 포인터
아시다시피 구조체는 여러개의 자료형을 하나로 묶어놓은 통합 자료형의 하
나 입니다. 구조체 역시 자료형이기 때문에 포인터 사용에 있어서도 일반 자
료형을 다루는 것과 다를 것이 없습니다. 단 구조체는 일반적으로 크기가 크
기 때문에 일반 자료형에서 흔히 사용되는 call by value 대신에 포인터를 사
용한 call by reference가 주로 사용 됩니다. 함수의 인자로 구조체를 넘긴다
고 할때 구조체 전체가 인자에 복사되기 때문에 시간과 공간 사용이 모두 비
효율적이 되기 때문이지요. 구조체의 포인터만을 가지고 접근을 하는 것이 훨
씬 효율적임을 알 수 있습니다.
우리가 배열의 요소에 접근할 때 [] 연산자를 사용하듯이 구조체의 요소에
접근을 할려면 Dot(.) 연산자를 사용하지요. 하지만 Dot 연산자는 구조체의
인스턴스화된 변수에서만 사용이 가능하고, 구조체의 포인터를 통해 접근하고
자 할때는 -> 연산자를 사용 합니다. 모두 아시지요? ^^
typedef struct grade_t {
int number;
int grade;
};
void swap_grade(grade_t *student1, grade_t *student2) {
grade_t temp;
temp.number = student1->number;
temp.grade = student1->grade;
student1->number = student2->number;
student1->grade = student2->grade;
student2->number = temp.number;
student2->grade = temp.grade;
}
void main(void) {
grade_t st1 = { 1, 100 };
grade_t st2 = { 2, 80 };
swap_grade(&st1, &st2);
// ...
}
◐ 함수 포인터
함수는 컴파일 할때 컴퓨터가 알아 들을 수 있는 기계어 코드가 되고 우리
는 흔히 이것을 목적 코드라고 부릅니다. 우리가 프로그램을 실행할 때 이런
목적 코드는 메모리에 올라가서 실행이 되게 되는 거지요. 즉 함수도 메모리
에 적재되기 때문에 그 메모리 번지가 있는 것이지요. 당연히 그 주소를 가리
키는 포인터도 만들 수가 있는 겁니다.
함수 포인터는 크기가 4바이트이고 대부분의 속성도 일반 포인터와 동일하
지만 선언할 때 많이 달라지는 것을 아실 겁니다. (시험에도 나왔었지요 ^^)
void *func_ptr(void);
void (*func_ptr)(void);
위 두 선언의 차이점을 아시죠? 첫번째 선언은 리턴값이 void *이고 인자가
필요없는 함수 func_ptr을 선언한 겁니다. 두번째 선언이 바로 함수 포인터의
선언이지요. 리턴값도 없고 인자도 필요없는 함수를 가리킬 수 있는 함수 포
인터 func_ptr의 선언 입니다.
이렇게 함수 포인터는 일반 함수를 선언하는 것과 비슷하게 하되 이름과 포
인터 선언(*) 앞뒤에 괄호를 붙여주어야 한다는 거지요. 그러면 함수 포인터
는 어디에 쓸까요?
다음과 같은 함수들이 있다고 합시다.
void putstring_moniter(char *str);
void putstring_printer(char *str);
위의 함수는 str을 모니터로 출력하는 함수, 아래의 함수는 str을 프린터로
출력하는 함수라고 가정하지요. 위의 함수들을 가리킬 수 있는 함수 포인터를
만들어 봅시다.
void (*putstring_ptr)(char *str) = NULL; // 초기값을 NULL로 합니다
void set_moniter(void) {
putstring_ptr = putstring_moniter;
}
void set_printer(void) {
putstring_ptr = putstring_printer;
}
set_moniter 함수는 putstring_ptr을 putstring_moniter로 세팅하고 아래의
set_printer 함수는 putstring_ptr을 putstring_printer로 세팅하지요. 초기
값을 NULL로 한 이유는 함수 포인터도 일종의 변수이기 때문에 초기화가 되지
않으면 쓰레기값이 들어가 있을 수도 있기 때문이지요. 쓰레기 값이 들어가는
게 무슨 상관이냐고요? 조금 후에 설명하지요. 다음 코드를 보세요.
void putstring(char *str) {
if (putstring_ptr != NULL) (*putstring_ptr)(str);
}
이렇습니다. 어때요? 별로 안 어렵지요? 이 함수를 사용해 보죠.
set_moniter();
putstring("이건 모니터에 출력 됩니다");
set_printer();
putstring("이건 프린터에 출력 됩니다");
위의 putstring 함수에서 (*putstring_ptr)(str)은 putstring_ptr(str)처럼
(*...)를 빼고 사용할 수도 있습니다. C에서는 모든 함수가 사실상은 그 포인
터를 통해 사용되기 때문이지요. (그래서 포인터에 함수를 대입할 때도 따로
& 연산자를 사용하지 않지요)
그런데 만약 set_... 함수를 부르지 않고 첨부터 그냥 putstring 함수를 불
렀다고 합시다. putstring 함수에서는 putstring_ptr이 NULL인지 비교를 하지
요. 네, 그렇습니다. putstring_ptr이 NULL 일때는 아무런 함수도 포인터에
연결되지 않았음을 뜻하는 거지요. 이것을 구분하기 위해서 함수 포인터를 선
언할 때는 대부분의 경우 초기값을 NULL로 해 주어야 하는 거지요.
이렇듯 함수 포인터는 흔히 여러개의 대상에 공통되는 기능이 적용될 때 효
율이 극대화 되곤 합니다. 만약 위의 예제를 int dest라는 변수를 두고 모니
터에 출력하고자 하면 dest에 1을, 프린터에 출력하고자 하면 dest에 2를 대
입하여 putstring 함수에서 dest 값을 검사해 putstring_moniter 함수를 부를
것인지 putstring_printer 함수를 부를 것인지를 판별하도록 한다면, 함수가
매우 많이 불릴때는 비교하는데 필요한 시간도 만만치 않게 될 것입니다.
이제 함수 포인터를 배열로 만들어 봅시다. 다음과 같은 세개의 메뉴가 있
다고 가정하지요.
void menu1(void) {
printf("1번 메뉴 입니다");
}
void menu2(void) {
printf("2번 메뉴 입니다");
}
void menu3(void) {
printf("3번 메뉴 입니다");
}
이들을 함수 포인터를 담을 수 있는 크기 3의 배열에 넣어 봅시다. (이름을
붙이자면 함수 포인터 배열 정도가 될까요?)
typedef void (*menu_ptr)(void);
menu_ptr menu[] = { menu1, menu2, menu3 };
void main() {
... // number에 메뉴 번호를 입력 받는다 (1-3)
menu[number-1]();
};
menu[첨자](인자)와 같은 방법으로 사용할 수 있게 되지요. 배열의 사용법
과 크게 다르지 않습니다.
◐ 파일 포인터
아시다시피 C에서 파일을 다룰 때는 구조체 변수의 포인터를 사용 합니다.
FILE *fp;
FILE은 컴파일러를 만든 사람들이 정의한 구조체이며 포인터로 선언해 파일
을 나타내는 포인터로 사용 됩니다. 이처럼 선언이 되었을 때까지는 당연하게
도 아무런 메모리는 할당되지 않은채 단지 fp에 4바이트의 포인터 영역이 할
당되지요. 이제 파일을 엽니다.
fp = fopen("파일명", "열기모드");
fopen 함수는 파일명에 주어진 파일을 열기모드로 열어 FILE 구조체를 새로
할당해 열린 파일에 관련된 정보들을 넣은 후 그 포인터를 리턴하게 되는 것
이지요. 다음과 같다고 보면 됩니다.
FILE *fopen(char *filename, char *openmode) {
FILE *temp;
temp = (FILE *)malloc(sizeof(FILE));
// ... filename을 openmode에 따라 연다
// ... temp 구조체 변수에 열린 파일의 정보를 넣는다
return temp;
}
이제부터는 이렇게 할당된 포인터 fp를 가지고 파일에 접근을 하게 되는 것
이지요. 자세한 내용은 C 입문서의 파일 다루기에 대한 내용을 참고하시고…
파일을 다 사용한 후에는 닫아야겠지요?
fclose(fp);
fclose 함수는 내부적으로 fp에 해당하는 파일을 닫은 후에 free 함수로 fp
에 할당된 FILE 구조체 만큼의 메모리를 해제해 주는 겁니다. 우리가 따로 할
당하거나 해제할 필요는 없는 것이지요.
이전에도 말씀 드렸는데 다음의 예제를 한번 보세요.
void open_files(FILE *fp1, FILE *fp2) {
fp1 = fopen("...", "...");
fp2 = fopen("...", "...");
}
void main(void) {
FILE *fp1, *fp2;
open_files(fp1, fp2);
// ...
}
위의 예제가 제대로 되었다고 생각하시는 분은 이제 안 계시겠지요? main 함
수의 fp1, fp2와 open_files의 fp1, fp2는 전혀 별개 입니다. open_files 에서
새로이 선언된 fp1, fp2는 함수 스코프를 가지므로 open_files 함수가 끝나게
되면 없어지지요. main 함수의 fp1, fp2는 여전히 쓰레기값을 가지고 있을 뿐
입니다.
------------------------------------------------
◐ 단일 링크드 리스트
링크드 리스트(linked-list)는 리스트의 하나 입니다. 리스트… 무언지는
아시겠지요? 배열 같은 걸 말하는 거지요. 내용이 쭈욱 나열되어 있는 형태라
고나 할까요… 링크드 리스트는 이름 그대로 무언가에 의해 연결됨으로써 리
스트와 같은 형태를 띠는 구조 입니다. 그 무언가는 아시다시피 포인터를 말
하는 것이겠지요.
주소록을 하나 만들어 봅시다. 간단한… 우선 정보를 담을 구조체를 선언해
야 겠지요? (주소록이 아니라 전화번호부라고 해야겠네요. ^^)
struct people_t {
char name[20];
char phone[15];
};
이 정도면 되겠지요. 그런데 우리가 만들 것은 링크드 리스트 입니다. 포인
터가 들어가야겠지요.
struct people_tag {
char name[20];
char phone[15];
struct people_tag *next;
};
typedef struct people_tag people_t;
아시다시피 링크드 리스트는 자기 자신을 가리키는 포인터에 의해 다음과
같이 연결된 구조를 말하지요. 각각을 노드(node)라고 합니다.
people_t *head = NULL, *tail = NULL;
int count = 0;
┏━━┓┏━━┓
┃head┃┃tail┃
┗━━┛┗━━┛
┃ ┗━━━━━━━━━━━━━━━━━━━━━┓
▼ ▼
┏━━━━━┓┏▶┏━━━━━┓┏▶┏━━━━━┓ ┏━━━━━┓
┃ name ┃┃ ┃ name ┃┃ ┃ name ┃ ┃ name ┃
┃ phone ┃┃ ┃ phone ┃┃ ┃ phone ┃…┃ phone ┃
┣━━━━━┫┃ ┣━━━━━┫┃ ┣━━━━━┫ ┣━━━━━┫
┃ *next ┃┛ ┃ *next ┃┛ ┃ *next ┃ ┃ *next ┃
┗━━━━━┛ ┗━━━━━┛ ┗━━━━━┛ ┗━━━━━┛
아직은 아무런 정보도 들어가지 않았으므로 head와 tail은 NULL 입니다. 이
제 이 리스트에 한 사람의 정보를 추가하는 함수를 만들어 봅시다.
people_t *list_add(char *name, char *phone) {
people_t *temp;
temp = (people_t *)malloc(sizeof(people_t));
if (temp == NULL) return NULL; // 메모리 할당 에러!
strcpy(temp->name, name);
strcpy(temp->phone, phone);
temp->next = NULL;
// 새로 할당된 temp에 주어진 정보를 모두 넣었습니다. [①]
// 이제 temp를 리스트에 추가시켜야 하겠지요.
if (tail != NULL) tail->next = temp;
// tail이 NULL이 아닌지 판단한 이유는 무엇일까요?
// tail이 NULL인 초기의 경우에 tail->next라는 구문을 사용한다면
// 당연히 심각한 문제가 발생하기 때문이지요.
if (tail == NULL) head = temp;
tail = temp;
// 초기 상태의 경우에 head도 역시 NULL 이므로 [②]
// 똑같이 temp를 대입해 주어야 합니다.
count++;
return temp; // 모두 성공적으로 끝났을 때 입니다.
}
이 리스트의 초기 상태를 볼까요.
┏━━┓┏━━┓
┃head┃┃tail┃
┗━━┛┗━━┛
┣━━━┛
▼
NULL
┏━━━━━┓
┃ ┃
┃ ┃
┣━━━━━┫
┃ ┃
┗━━━━━┛
이제 첫번째 값을 대입해 보도록 하지요.
list_add("shin", "111-2222"); // 처음 대입할 때
①┏━━┓┏━━┓
┃head┃┃tail┃
┗━━┛┗━━┛
┣━━━┛
▼
NULL
┏━━━━━┓◀┓┏━━━━━┓
┃ ┃ ┃┃ shin ┃
┃ ┃ ┃┃ 111-2222 ┃
┣━━━━━┫ ┃┣━━━━━┫
┃ ┃ ┗┃ *next ┃
┗━━━━━┛ ┗━━━━━┛
②┏━━┓┏━━┓
┃head┃┃tail┃
┗━━┛┗━━┛
┗━━━┫
┗━━━━┓
NULL ▼
┏━━━━━┓◀┓┏━━━━━┓
┃ ┃ ┃┃ shin ┃
┃ ┃ ┃┃ 111-2222 ┃
┣━━━━━┫ ┃┣━━━━━┫
┃ ┃ ┗┃ *next ┃
┗━━━━━┛ ┗━━━━━┛
결과적으로는 다음과 동일 합니다. (좌우를 뒤집었을 뿐이지요)
┏━━┓┏━━┓
┃head┃┃tail┃
┗━━┛┗━━┛
┣━━━┛
▼ NULL
┏━━━━━┓┏▶┏━━━━━┓
┃ shin ┃┃ ┃ ┃
┃ 111-2222 ┃┃ ┃ ┃
┣━━━━━┫┃ ┣━━━━━┫
┃ *next ┃┛ ┃ ┃
┗━━━━━┛ ┗━━━━━┛
더 대입해 봅시다.
list_add("jung", "333-4444");
①┏━━┓┏━━┓
┃head┃┃tail┃
┗━━┛┗━━┛
┣━━━┛
▼ NULL
┏━━━━━┓┏▶┏━━━━━┓◀┓┏━━━━━┓
┃ shin ┃┃ ┃ ┃ ┃┃ jung ┃
┃ 111-2222 ┃┃ ┃ ┃ ┃┃ 333-4444 ┃
┣━━━━━┫┃ ┣━━━━━┫ ┃┣━━━━━┫
┃ *next ┃┛ ┃ ┃ ┗┃ *next ┃
┗━━━━━┛ ┗━━━━━┛ ┗━━━━━┛
②┏━━┓┏━━┓
┃head┃┃tail┃
┗━━┛┗━━┛
┃ ┗┳━━━━━━━━━━━┓
▼ ┃ NULL ▼
┏━━━━━┓┃ ┏━━━━━┓◀┓┏━━━━━┓
┃ shin ┃┃ ┃ ┃ ┃┃ jung ┃
┃ 111-2222 ┃┃ ┃ ┃ ┃┃ 333-4444 ┃
┣━━━━━┫┃ ┣━━━━━┫ ┃┣━━━━━┫
┃ *next ┃┛ ┃ ┃ ┗┃ *next ┃
┗━━━━━┛ ┗━━━━━┛ ┗━━━━━┛
역시 그림을 조금 정리하면 다음과 같지요.
┏━━┓┏━━┓
┃head┃┃tail┃
┗━━┛┗━━┛
┃ ┗━━━━┓
▼ ▼ NULL
┏━━━━━┓┏▶┏━━━━━┓┏▶┏━━━━━┓
┃ shin ┃┃ ┃ jung ┃┃ ┃ ┃
┃ 111-2222 ┃┃ ┃ 333-4444 ┃┃ ┃ ┃
┣━━━━━┫┃ ┣━━━━━┫┃ ┣━━━━━┫
┃ *next ┃┛ ┃ *next ┃┛ ┃ ┃
┗━━━━━┛ ┗━━━━━┛ ┗━━━━━┛
하나를 더 추가하면,
list_add("cho", "555-6666");
┏━━┓┏━━┓
┃head┃┃tail┃
┗━━┛┗━━┛
┃ ┗━━━━━━━━━━━━━┓
▼ ▼ NULL
┏━━━━━┓┏▶┏━━━━━┓┏▶┏━━━━━┓┏▶┏━━━━━┓
┃ shin ┃┃ ┃ jung ┃┃ ┃ cho ┃┃ ┃ ┃
┃ 111-2222 ┃┃ ┃ 333-4444 ┃┃ ┃ 555-6666 ┃┃ ┃ ┃
┣━━━━━┫┃ ┣━━━━━┫┃ ┣━━━━━┫┃ ┣━━━━━┫
┃ *next ┃┛ ┃ *next ┃┛ ┃ *next ┃┛ ┃ ┃
┗━━━━━┛ ┗━━━━━┛ ┗━━━━━┛ ┗━━━━━┛
더 대입해 볼 필요는 없겠지요? ^^ 이제 특정 위치의 노드에 접근하는 함수
를 만들어 봅시다.
people_t *list_seek(int number) {
int n;
people_t *temp = head;
if (number >= count) return NULL;
for (n = 0; n < number; n++)
temp = temp->next;
return temp;
}
list_seek(1)->name과 같은 방법으로도 접근이 가능하겠지요? 물론 결과값
이 NULL인지 아닌지 비교는 해야 하겠지요. 이제 하나의 노드를 삭제하는 함
수를 만들어 볼까요?
void list_delete(people_t *node) {
people_t *prev;
for (prev = head; prev != tail; prev = prev->next)
if (prev->next == node) break;
// node로부터 직접 바로 앞의 노드를 알 방법이 없으므로
// 처음부터 검색을 합니다.
// prev가 바로 node 바로 전의 노드가 되겠지요.
if (node == head) head = node->next;
else prev->next = node->next;
count--;
// 이제 node는 리스트에서 삭제 되었습니다. [①]
free(node);
// 할당했던 메모리도 해제해야 하겠지요. [②]
}
list_delete(list_seek(1));
①┏━━┓┏━━┓
┃head┃┃tail┃
┗━━┛┗━━┛
┃ ┗━━━━━━━━━━━━━┓
▼ ┏━━━━━━━━┓ ▼ NULL
┏━━━━━┓┃ ┏━━━━━┓┣▶┏━━━━━┓┏▶┏━━━━━┓
┃ shin ┃┃ ┃ jung ┃┃ ┃ cho ┃┃ ┃ ┃
┃ 111-2222 ┃┃ ┃ 333-4444 ┃┃ ┃ 555-6666 ┃┃ ┃ ┃
┣━━━━━┫┃ ┣━━━━━┫┃ ┣━━━━━┫┃ ┣━━━━━┫
┃ *next ┃┛ ┃ *next ┃┛ ┃ *next ┃┛ ┃ ┃
┗━━━━━┛ ┗━━━━━┛ ┗━━━━━┛ ┗━━━━━┛
②┏━━┓┏━━┓
┃head┃┃tail┃
┗━━┛┗━━┛
┃ ┗━━━━━━━━━━━━━┓
▼ ┏━━━━━━━━┓ ▼ NULL
┏━━━━━┓┃ ┗▶┏━━━━━┓┏▶┏━━━━━┓
┃ shin ┃┃ ┃ cho ┃┃ ┃ ┃
┃ 111-2222 ┃┃ ┃ 555-6666 ┃┃ ┃ ┃
┣━━━━━┫┃ ┣━━━━━┫┃ ┣━━━━━┫
┃ *next ┃┛ ┃ *next ┃┛ ┃ ┃
┗━━━━━┛ ┗━━━━━┛ ┗━━━━━┛
list_delete(list_seek(0)); // 삭제되는 노드가 head 일 때
①┏━━┓┏━━┓
┃head┃┃tail┃
┗━━┛┗━━┛
┃ ┗━━━━┓
┗━━━━┓ ▼ NULL
┏━━━━━┓┣▶┏━━━━━┓┏▶┏━━━━━┓
┃ shin ┃┃ ┃ cho ┃┃ ┃ ┃
┃ 111-2222 ┃┃ ┃ 555-6666 ┃┃ ┃ ┃
┣━━━━━┫┃ ┣━━━━━┫┃ ┣━━━━━┫
┃ *next ┃┛ ┃ *next ┃┛ ┃ ┃
┗━━━━━┛ ┗━━━━━┛ ┗━━━━━┛
②┏━━┓┏━━┓
┃head┃┃tail┃
┗━━┛┗━━┛
┃ ┗━━━━┓
┗━━━━┓ ▼ NULL
┗▶┏━━━━━┓┏▶┏━━━━━┓
┃ cho ┃┃ ┃ ┃
┃ 555-6666 ┃┃ ┃ ┃
┣━━━━━┫┃ ┣━━━━━┫
┃ *next ┃┛ ┃ ┃
┗━━━━━┛ ┗━━━━━┛
여기까지 해서 링크드 리스트에 추가하고 접근하고 삭제하는 함수를 만들어
봤습니다. 원하는 위치에 추가하거나 검색을 하는 함수는 직접 만들어 보시기
바랍니다. 위의 함수들을 사용한 예제를 적습니다.
#include <stdio.h>
#include <alloc.h>
#include <string.h>
struct people_tag {
char name[20];
char phone[15];
struct people_tag *next;
};
typedef struct people_tag people_t;
people_t *head = NULL, *tail = NULL;
int count = 0;
people_t *list_add(char *name, char *phone) {
people_t *temp;
temp = (people_t *)malloc(sizeof(people_t));
if (temp == NULL) return NULL;
strcpy(temp->name, name);
strcpy(temp->phone, phone);
temp->next = NULL;
if (tail != NULL) tail->next = temp;
if (tail == NULL) head = temp;
tail = temp;
count++;
return temp;
}
people_t *list_seek(int number) {
int n;
people_t *temp = head;
if (number >= count) return NULL;
for (n = 0; n < number; n++)
temp = temp->next;
return temp;
}
void list_delete(people_t *node) {
people_t *prev;
for (prev = head; prev != tail; prev = prev->next)
if (prev->next == node) break;
if (node == head) head = node->next;
else prev->next = node->next;
count--;
free(node);
}
void print_list(void) {
int n;
for (n = 0; n < count; n++)
printf("%d %s %s\n", n + 1,
list_seek(n)->name, list_seek(n)->phone);
printf("\n");
}
void main(void) {
list_add("shin", "111-2222");
print_list();
list_add("jung", "333-4444");
print_list();
list_add("cho", "555-6666");
print_list();
list_delete(list_seek(1));
print_list();
list_delete(list_seek(0));
print_list();
list_delete(list_seek(0));
}
<결과>
1 shin 111-2222
1 shin 111-2222
2 jung 333-4444
1 shin 111-2222
2 jung 333-4444
3 cho 555-6666
1 shin 111-2222
2 cho 555-6666
1 cho 555-6666
---------------------------------------------------------------------------
◐ 이중 링크드 리스트
시간 시간에는 단일 링크드 리스트를 했었지요. 단일과 이중. 머가 하나고
두개란 말일까요? 네. 그렇지요. 바로 연결을 시켜주는 포인터 입니다. 단일
링크드 리스트에서는 포인터가 next 밖에 존재하지 않았었지요. 이중 링크드
리스트에는 여기에 prev 포인터를 더해서 포인터가 두개 존재 합니다. 포인터
가 두개라서 얻는 이득은 무엇이 있을까요? 지난 시간에 노드의 포인터를 받
아 그 노드를 삭제하는 함수를 만들 때 우리는 그 노드의 바로 앞 노드를 찾
기 위해 전체 리스트를 헤메고 다니도록 했었지요. 하지만 이제는 바로 앞 노
드의 포인터도 가지고 있기 때문에 그럴 필요가 없습니다. 어디에 끼워 넣기
를 하거나 할때도 바로 접근이 가능하기 때문에 훨씬 편리하지요. 그러나 메
모리를 조금 많이 사용하고(아무래도 포인터가 두개니까…) 소스 코드가 조금
복잡해진다는(경우에 따라서는 더 단순해 질수도 있고…) 단점이 있기는 하지
만 편리하기 때문에 단일 보다는 이중 링크드 리스트가 더 많이 사용 됩니다.
struct node_tag {
int number;
struct node_tag *prev, *next;
};
typedef struct node_tag node_t;
정수를 저장하는 이중 링크드 리스트의 노드를 선언 했습니다. 이제 앞에서
했던 것처럼 head와 tail 포인터를 만들고 count도 만듭시다.
node_t *head = NULL, *tail = NULL;
int count = 0;
이제 실제로 큐를 하나 만들어 보면서 이중 링크드 리스트의 사용 방법을
알아보도록 하지요.
◐ 큐
큐는 아시다시피 먼저 들어간 것이 먼저 나오는 FIFO(First In, First Out)
구조 입니다. 들어가는 곳을 head라고 하고 나오는 곳을 tail이라고 하지요.
int enqueue(int number) {
node_t *temp;
temp = (node_t *)malloc(sizeof(node_t));
if (temp == NULL) return 0; // 메모리 할당 에러
temp->number = number;
temp->prev = NULL; // head로 들어가므로 temp가 첫 노드가 됨
temp->next = head;
count++;
if (head != NULL) head->prev = temp;
if (head == NULL) tail = temp;
head = temp;
return 1;
}
우선 temp의 값들을 설정한 후에 다른 노드가 존재하면 head의 앞에 temp를
연결하지요. 이제 새로운 head는 temp가 되고, 초기 상태이면 tail도 temp가
되지요. (head = temp를 if (head ...)보다 앞에 쓰면 안됩니다)
int dequeue(void) {
int number;
if (tail == NULL) return NULL; // 초기 상태인 경우
number = tail->number; // tail은 잠시 후 free에 의해 삭제되므로
// 값을 따로 저장하고 있어야 합니다
tail = tail->prev; // 새로운 tail은 바로 앞 노드가 됩니다
if (tail != NULL) {
free(tail->next); // 원래의 tail이 삭제 됩니다
tail->next = NULL;
} else head = NULL; // 모든 node가 삭제된 경우 head도 NULL이 됨
count--;
return number;
}
void main(void) {
printf("%d\n", dequeue()); // 0
enqueue(1);
enqueue(2);
enqueue(3);
printf("%d\n", dequeue()); // 1
enqueue(4);
enqueue(5);
enqueue(6);
printf("%d\n", dequeue()); // 2
printf("%d\n", dequeue()); // 3
printf("%d\n", dequeue()); // 4
printf("%d\n", dequeue()); // 5
printf("%d\n", dequeue()); // 6
enqueue(7);
enqueue(8);
printf("%d\n", dequeue()); // 7
printf("%d\n", dequeue()); // 8
}
결과는 예상대로 나옵니다.
◐ 스택
스택은 들어간 곳과 나오는 곳이 동일하면 되겠지요. 둘다 tail이라고 가정
합시다.
int push(int number) {
node_t *temp;
temp = (node_t *)malloc(sizeof(node_t));
if (temp == NULL) return 0; // 메모리 할당 에러
temp->number = number;
temp->next = NULL; // tail로 들어가므로 temp가 끝 노드가 됨
temp->prev = tail;
count++;
if (tail != NULL) tail->next = temp;
if (tail == NULL) head = temp;
tail = temp;
return 1;
}
int pop(void) {
int number;
if (tail == NULL) return NULL; // 초기 상태인 경우
number = tail->number; // tail은 잠시 후 free에 의해 삭제되므로
// 값을 따로 저장하고 있어야 합니다
tail = tail->prev; // 새로운 tail은 바로 앞 노드가 됩니다
if (tail != NULL) {
free(tail->next); // 원래의 tail이 삭제 됩니다
tail->next = NULL;
} else head = NULL; // 모든 node가 삭제된 경우 head도 NULL이 됨
count--;
return number;
}
void main(void) {
printf("%d\n", pop()); // 0
push(1);
push(2);
push(3);
printf("%d\n", pop()); // 3
push(4);
push(5);
push(6);
printf("%d\n", pop()); // 6
printf("%d\n", pop()); // 5
printf("%d\n", pop()); // 4
printf("%d\n", pop()); // 3
printf("%d\n", pop()); // 2
push(7);
push(8);
printf("%d\n", pop()); // 8
printf("%d\n", pop()); // 7
}
pop 함수의 경우 큐의 dequeue 함수와 똑같습니다. 똑같이 tail에서 하나의
값을 빼오는 함수이니 다를 이유가 없지요. 그런데 위의 두 함수를 보면 head
값을 여기저기서 변경하기는 하는데 아무 곳에서도 참조를 안하지요? 즉 스택
에서는 head나 tail 중에 하나만 있어도 된다는 겁니다. 당연히 한쪽에서만
입출력을 하기 때문에 반대쪽의 포인터를 가질 필요가 없는 것이지요. head가
있는 부분을 빼고 실행해도 아무 문제가 없습니다.
위의 dequeue와 pop 함수의 경우 리스트에 아무런 값도 없을 때 0을 리턴하
는데요. 그렇다면 리스트에 0이라는 값을 넣었을 때 그 리스트가 비었다는 표
시인지 값 0인지 구별을 하지 못하지요. 어떤 책들을 보면 포인터를 리턴하므
로써 해결을 하기도 하는데 이런 방법은 메모리에서 문제가 생길 수도 있습니
다. 차라리 is_null 등의 함수를 새로 만들어서 비어 있는지 여부를 확인하고
값을 빼내는 것이 좋을 듯 하네요.
---------------------------------------------------------------------------
[연결리스트,스택,큐,트리 여러가지 방법으로.. ]
(출처 : http://internet512.chonbuk.ac.kr/datastructure/link/list5.htm)
-연결리스트의 정의
연결 리스트는 일정한 순서를 가지는 데이터 항목들을 표현하는 방법중의 하나이다.
배열과 같은 순차적 표현 방법과는 달리 데이터 항목들의 논리적인 순서만 유지되고 기억장소
내에서는 각 항목들의 임의의 위치를 가지도록 하는 자료구조이다.
-연결리스트의 구조
연결 리스트에서는 각 데이터 항목들이 기억장소내의 어떤 위치에 어떤 항목이 있는 지를 표시해 주어야 한다.
이를 위해 데이터 항목에는 값뿐만 아니라 다음 항목의 위치 정보도 함께 저장해둔다.
위치정보의 저장에는 포인터가 사용된다.
-연결리스트에서 포인터
연결 리스트에서 포인터는 각 항목의 다음 순서인 항목이나 앞 순서인
항목의 위치를 가르키는 지시자이다. 연결 리스트의 마지막 노드에는 다음 순서인
항목이 없고, 첫번째 노드에는 이전 순서인 항목이 없으므로 이들 포인터는 null로 해주어야 한다.
-단일연결리스트의 구조
선형리스트에서 발생하게 되는 원소들의 이동문제를 해결해주는 방법은 리스트를 구성하는
연속된 순서의 원소들이 기억장소내에서 어느곳에나 저장 가능하도록 원소들의 위치와
무관하게 원소들을 연결하여 표현하는 방법이다.
연결리스트에서는 원소들의 값이외에 포인터값이 포함되므로 리스트의 한원소의 노드(node)는
원소값을 저장하는 자료(data)부분과 다음 원소를 지적하는 포인터값을 저장 하는 링크(link)
부분으로 구성된다.
선형리스트에서 발생하게 되는 원소들의 이동문제를 해결해주는 방법은 리스트를 구성하는 연
속된 순서의 원소들이 기억장소내에서 어느곳에나 저장 가능하도록 원소들의 위치와 무관하게
원소들을 연결하여 표현하는 방법이다.
연결리스트에서는 원소들의 값이외에 포인터값이 포함되므로 리스트의 한원소의 노드(node)는
원소값을 저장하는 자료(data)부분과 다음 원소를 지적하는 포인터값을 저장 하는 링크(link)
부분으로 구성된다.
-단일 연결 리스트에서는 리스트의 마지막 노드의 링크 값이 널(null:^)이었지만,
환형 연결 리스트(circular linked list)는 마지막 노드의 링크의 링크 값이 널(null:^)이
아닌 리스트의 첫 번째 원소의 주소를 가리키도록 하는 구조로 되어 있다.
-환형 연결 리스트의 구조
환형 연결 리스트에서는 노드의 시작과 끝을 구별하지 않음으로써 마지막 노드와 처음 노드가
연결되어 있음으로 임의의 노드에서 다른 모든 노드로의 접근이 가능하여 임의 노드의 검색이
가능하게 된다. 단순 연결 리스트의 경우 정보가 끊어지면 그 다음은 찾을 수가 없는데 환형
연결 리스트는 그것을 극복할 수 있다.
-단일 연결 리스트(single linked list)와 환형 연결 리스트(circular linked list)는
각 노드에 다음 노드를 가리키는 한 개의 링크 필드만을 사용하기 때문에 특정한 노드를 검색하거나,
삽입 또는 삭제를 할 경우 한 쪽 방향으로만 해당 노드의 위치를 찾을 수 있는 구조이다.
그러나 임의의 노드를 찾을 때 양쪽 방향으로 해당 노드를 검색하면 쉽게 찾을 수 있다. 그러므로
이중 연결 리스트(double linked list)는 노드의 선행 노드를 가리키는 좌링크(LLINK),
자료(DATA)필드, 후행 노드를 가리키는 우링크(RLINK)의 세 개 영역으로 각 노드를 구분하여
양방향으로 특정 노드를 검색할 수 있도록 한 구조이다.
-이중 환형 연결 리스트(doubly circular linked list)는 이중 연결 리스트와 원형 연결 리스트를
혼합한 형태이다. 즉, 각 노드가 선행 노드와 후속 노드를 가리키기 위한 두 개의 포인터를 가지고 있으며,
마지막 노드의 RLINK는 처음 노드의 위치를 가리킨다.또한 처음 노드의 LLINK는 마지막 노드의 위치를
가리키는 형태를 취한다. 이중 환형 연결 리스트는 어떠한 노드도 널 링크를 갖지 않는다는 특징이 있다.
//****링크드 리스트로 큐 구현****
class Queue;//전방선언
class QueueNode{
friend class Queue;
private:
int data;
QueueNode *link;
QueueNode(int d = 0, QueueNode *l = 0) : data(d), link(l) {};
};
class Queue{
public:
Queue(){first = 0; last = 0;}
void Add(const int);
void Delete();
void PrintQueue();
void Release();
private:
QueueNode *first;//리스트의 처음
QueueNode *last;//리스트의 마지막
};
=>간단한 환형큐 <배열이용>
void print_queue()
{
int i;
printf("\n큐 컨텐츠 : Front---->Rear \n");
for(i=front; i != rear; i = ++i %MAX) //주의!!!!!!!!!!!
printf("%-3d", queue[i]);
}
//game큐 구현
큐는 스택과 달리 선입선출방식으로가장 먼저 저장된 데이터가 먼저 방출되는 방식놀이동산의 대기줄과 비슷
#include <stdio.h>
#include <stdlib.h>
/***********구조체 선언***********/
typedef struct Queue
{
char Data;
struct Queue * Next;
}QUEUE;
/***********구조체 선언***********/
// 형 재정의
typedef QUEUE * QueuePtr;
/***********함수 선언부***********/
// 메뉴
void Menu();
// 데이터 유/무 체크
int IsEmpty(QueuePtr EmptyQueue);
// 데이터 추가
void EnQueue(QueuePtr * HeadQueue, QueuePtr * TailQueue, char Value);
// 데이터 삭제
char DeQueue(QueuePtr * HeadQueue, QueuePtr * TailQueue);
// 데이터 출력
void PrintQueue(QueuePtr HeadQueue);
/***********함수 선언부***********/
int main()
{
// 최상위, 최하위 포인터 변수 선언
QueuePtr HeadQueue=NULL;
QueuePtr TailQueue=NULL;
// 메뉴 선택
int Choice=0;
// 값 저장
char Value;
// 메뉴 함수 호출
Menu();
printf("\n입력 대기 : ");
scanf("%d", &Choice);
while(Choice != 4)
{
switch(Choice)
{
// 데이터 추가
case 1:
system("cls");
printf("문자를 입력하세요 : ");
scanf("\n%c", &Value);
// 데이터 추가 함수 호출
EnQueue(&HeadQueue, &TailQueue, Value);
// 데이터 출력 함수 호출
PrintQueue(HeadQueue);
break;
// 데이터 삭제
case 2:
system("cls");
// 데이터가 존재한다면
if(!IsEmpty(HeadQueue))
{
// 반환값으로 삭제된 데이터의 값을 사용자에게 알린다
printf("%c 데이타가 삭제되었습니다\n", DeQueue(&HeadQueue, &TailQueue));
// 데이터 출력 함수 호출
PrintQueue(HeadQueue);
}
else
printf("데이타가 비었습니다\n\n");
break;
// 데이터 출력
case 3:
system("cls");
// 데이터 출력 함수 호출
PrintQueue(HeadQueue);
break;
// 기타
default:
system("cls");
// 메뉴 함수 호출
Menu();
break;
}
// 메뉴 함수 호출
Menu();
printf("\n입력 대기 : ");
scanf("%d", &Choice);
}
}
// 메뉴 함수
void Menu()
{
printf("메뉴를 선택해주세요\n\n");
puts("1 - 데이터 추가");
puts("2 - 데이터 삭제");
puts("3 - 데이터 출력");
puts("4 - 종료");
}
// 데이터 유/무 체크 함수
int IsEmpty(QueuePtr EmptyQueue)
{
// 데이터가 없다면 참 반환
return EmptyQueue == NULL;
}
// 데이터 추가 함수
void EnQueue(QueuePtr * HeadQueue, QueuePtr * TailQueue, char Value)
{
// 포인터 변수
QueuePtr NewQueue;
// 메모리 할당
NewQueue=malloc(sizeof(QUEUE));
// 정상적으로 메모리가 할당되었다면
if(NewQueue != NULL)
{
// 값 대입
NewQueue->Data=Value;
// 가장 최근에 입력한 데이터가 최하위 데이터가 되므로 다음 포인터는 NULL
NewQueue->Next=NULL;
// 데이터가 없다면
if(IsEmpty(*HeadQueue))
// 최상위 데이터로 가리킴
*HeadQueue=NewQueue;
// 데이터가 존재한다면
else
// 기존의 최하위 데이터의 다음 주소로 최근의 입력된 데이터가 최하위 데이터로 가리켜짐
(*TailQueue)->Next=NewQueue;
// 최근에 입력한 데이터를 최하위 데이터로 가리킴
*TailQueue=NewQueue;
}
else
printf("메모리를 할당할 수 없습니다\n\n");
}
// 데이터 삭제 함수
char DeQueue(QueuePtr * HeadQueue, QueuePtr * TailQueue)
{
// 최상위 데이터를 가리킴
QueuePtr Temp=(*HeadQueue);
// 최상위 데이터의 값을 대입
char Value=(*HeadQueue)->Data;
// 다음 데이터를 최상위 데이터로 가리킴
*HeadQueue=(*HeadQueue)->Next;
// 최상위 데이터가 없다면
if((*HeadQueue) == NULL)
// 최하위 데이터를 NULL을 가리킴
*TailQueue=NULL;
// 최상위 데이터의 메모리 해제
free(Temp);
// 삭제된 데이터의 값을 반환
return Value;
}
// 데이터 출력 함수
void PrintQueue(QueuePtr HeadQueue)
{
// 데이터가 존재한다면
if(HeadQueue !=NULL)
{
printf("최상위 --> ");
// 최하위 데이터까지
while(HeadQueue != NULL)
{
// 값을 출력
printf("%c --> ", HeadQueue->Data);
// 다음 데이터를 가리킴
HeadQueue=HeadQueue->Next;
}
printf("최하위\n\n");
}
else
printf("데이터가 없습니다\n\n");
}
//****game스택구현****
#include <stdio.h>
#include <stdlib.h>
/***********구조체 선언************/
typedef struct Stack
{
int Data;
struct Stack * Next;
}STACK;
// 포인터형 재정의
typedef STACK * StackPtr;
/***********구조체 선언************/
/***********함수 선언부***********/
// 메뉴
void Menu();
// 데이터 유/무 체크
int IsEmpty(StackPtr EmptyNode);
// 데이터 출력
void PrintStack(StackPtr PrintNode);
// 데이터 추가
void PushStack(StackPtr * PushNode, int Value);
// 데이터 삭제
int PopStack(StackPtr * PopNode);
/***********함수 선언부***********/
int main()
{
// 최상위 스택
StackPtr MainNode=NULL;
// 메뉴 선택
int Choice=0;
// 스택에 놓여질 데이터
int Value=0;
// 메뉴함수 호출
Menu();
printf("\n입력 대기 : ");
scanf("%d", &Choice);
while(Choice != 4)
{
switch(Choice)
{
// 데이터 추가
case 1:
system("cls");
printf("추가할 값을 입력하세요 : ");
scanf("%d", &Value);
// 데이터 추가 함수 호출
PushStack(&MainNode, Value);
// 데이터 출력 함수 호출
PrintStack(MainNode);
break;
// 데이터 삭제
case 2:
system("cls");
// 데이터가 존재한다면
if(!(IsEmpty(MainNode)))
{
puts("최상위 데이터를 삭제합니다");
// 삭제된 데이터를 반환값으로 사용하여 사용자에게 알린다
printf("%d 데이터가 삭제되었습니다\n\n", PopStack(&MainNode));
// 데이터 출력 함수 호출
PrintStack(MainNode);
}
else
printf("데이터가 비어있습니다\n\n");
break;
// 데이터 출력
case 3:
system("cls");
// 데이터 출력 함수 호출
PrintStack(MainNode);
break;
default:
system("cls");
// 메뉴 함수 호출
Menu();
break;
}
// 메뉴 함수 호출
Menu();
printf("\n입력 대기 : ");
scanf("%d", &Choice);
}
'자료구조 & 알고리즘' 카테고리의 다른 글
대학교에서 알고리듬 과목을 수강하게 되면 (0) | 2007.03.14 |
---|---|
그래프 알고리즘 연습. (0) | 2007.03.14 |
2차원배열의 포인터 (0) | 2007.03.14 |