IT Japan

제 6장 - 배열과 포인터 본문

IT/Programming

제 6장 - 배열과 포인터

swhwang 2016. 3. 22. 00:13
반응형

* 제 6장 - 배열과 포인터 


지난 장까지의 내용은 여러분들도 무리없이 잘 따라왔을 것이다. 하지만 

6장부터는 C의 가장 중요하고도 이해하기 어려운 포인터, 구조체. 공용체, 

등등의 난관이 여러분 앞에 버티고 있다. 포인터에 관한 내용으로만 책 한 

권을 써재끼는 사람이 있는 것으로 봐서는 포인터라는 것은 난해한 것임에 

틀림이 없다. 하지만 여기에 수록되는 예제들을 직접 수행시켜 보고, 설 

명을 잘 읽어 본다면 이해가 그렇게 어려운 것만은 아닐 것이다. 



1. 배열(Array) 


배열과 포인터는 서로 밀접하게 관련되어 있다. 그래서 보통 이 두가지는 

동시에 논하여 진다. 먼저 배열에 관해서 알아보자. 


배열은 일련된 공간에 저장된 자료의 집합이다. 여기서 자료란 배열 요소 

라고 하는데 이 요소들은 어떤 규칙에 의해 저장된다. 

그리고 전장에서 배운 변수의 기억 부류의 통용 범위의 규칙도 배열에서 

도 똑같이 적용된다. (배열도 변수의 일종이다) 


int a[10], b[3][6], c, d; /* 배열선언의 보기 */ 


위의 배열 선언에 의해 생성되는 배열의 실체는 아래와 같다. 

0 1 2 3 4 5 

0 1 2 3 4 5 6 7 8 9 +-+-+-+-+-+-+ 

+-+-+-+-+-+-+-+-+-+-+ 0| | | | | | | 

배열a | | | | | | | | | | | +-+-+-+-+-+-+ 

+-+-+-+-+-+-+-+-+-+-+ 1| | | | | | | 

+-+-+-+-+-+-+ 

2| | | | | | | 

+-+-+-+-+-+-+ 

위에서 보듯이 a[10]은 a라는 이름으로 int형의 자료를 넣을 수 있는 방 

을 10개 확보하라는 명령이다. (1차원 배열이다) 

2차원 배열의 경우에는 b[3][6]과 같이 하는데, b라는 이름의 int 배열을 

3행 6열로 선언하는 예이다. 


지난 장에서 정적변수와 동적 변수를 배웠는데 배열에서도 정적 배열과 

동적 배열이 있다. 정적 배열은 앞에 static를 붙이며, 1회에 한하여 모든 

배열요소에 초기치를 부여할 수 있다. 초기치는 {}로 묶어서 표현한다. 동 

적 배열은 전체를 초기화할 수 없고 모든 요소를 하나씩 지정해서 초기화 

해야 한다. 


1)static char a[6]={'a','b','c','d','e'}; /* 이 네가지 초기화 */ 

2)static char a[6]="abcde"; /* 는 모두 쓸 수있다*/ 

3)static char a[]={'a','b','c','d','e'}; /* (모두 같다) */ 

4)static char a[]="abcde"; /* 대중적 */ 


정적 배열의 경우 배열의 크기를 선언하지 않으면 컴파일러가 초기치의 

갯수대로 방의 갯수를 결정해 준다. 그리고 문자 배열은 배열의 끝에 문자 

열의 끝임을 알리는 자동적으로 '\0'이 추가되므로 1), 2)의 경우처럼 


** 문자열을 배열내에 저장할 때는 문자열의 길이보다 적어도 1개 

이상 크게 배열 크기을 확보하여야 한다. ** 


(위에서 주로 많이 쓰이는 양식은 4번이다) 

이차원 정적 배열에 초기화를 행하는 예를 보자. 


static int b[2][5]={0,1,2,3,4},{5,6,7,8,9}; 

static int b[2][5]={{0,1,2,3,4},{5,6,7,8,9}}; 


위의 두가지 표현은 서로 동일한 것이다. 


배열명은 배열의 선두 번지를 가리키는 포인터 상수이다. 그러므로 변수 

처럼 값을 대입할 수는 없지만 그 값을 참조하여 배열의 1행, 혹은 배열의 

요소 전부를 읽어낼 수 있다. 다음과 같은 배열을 생각해 보자. 


1) static char c[3][7]={{"ABCDEF\0"},{"GHIJKL\0"},{"MNOPQR\0"}}; 

2) static char c[3][7]={"ABCDEF\0",GHIJKL\0","MNOPQR\0"}; 

3) static char c[3][7]={"ABCDEF",GHIJKL","MNOPQR"}; 

4) static char c[][7]={"ABCDEF","GHIJKL",MNOPQR"}; /* 대중적 */ 



위의 세가지 표현은 모두 동일한 표현이다. ( '\0'은 자동적으로 붙으므 

로 생략이 가능하다 ) 4번의 경우처럼 배열 첨자를 생략할 수 있으나 마지 

막 첨자까지는 생략할 수 없다. 그러면 저장 형태를 그림으로 보자. 


0 1 2 3 4 5 6 

+-+-+-+-+-+-+--+ 

0 |A|B|C|D|E|F|\0| . 배열 c[3][7] 요소의 지정과 출력 형태 

+-+-+-+-+-+-+--+ 

배열c 1 |G|H|I|J|K|L|\0| printf("%c",c[1][2]); => I 출력 

+-+-+-+-+-+-+--+ printf("%s",c[1]; => GHIJKL 출력 

2 |M|N|O|P|Q|R|\0| * 문자열 출력이므로 %s 사용 

+-+-+-+-+-+-+--+ 



배열에서의 중요한 점을 간추려 보면 다음과 같다. 


1) 정적 배열이 아닌 이상 배열 크기의 기억 장소만을 확보할 뿐 거기에 

대한 초기화는 하지 않는다. 

2) 배열명, 혹은 세로측(행)의 첨자만 기술하여 배열 전체, 혹은 그 행의 

전체를 나타낼 수 있다. 

3) 배열 크기를 선언할 때 변수를 사용하지 못한다. 즉 int arr[n];과 같 

은 문장은 에러이다. 

4) 배열 첨자가 허용 범위를 벗어났는지의 여부를 조사하지 않는다. 



<예제1> 초기화 되지 않은 배열의 값(정적 배열, 자동 배열) 


<리스트1> #include 

main() 

int fuzzy[2]; /* 자동 배열이므로, main()함수를 벗*/ 

/* 어 나면 자동적으로 소멸된다. */ 

static int wuzzy[2]; /* 정적 배열 */ 


printf("%d %d\n",fuzzy[1],wuzzy[1]); 


위 프로그램을 실행시키면 자동 배열은 기억 장소만을 확보할 뿐 초기화 

하지 않으므로 그 기억장소의 쓰레기 값이 출력되며, 정적 배열은 초기화 

하지 않았을 경우 자동으로 0으로 채워지므로 0이 출력될 것이다. 


<예제2> 배열의 초기화 - 크기보다 초기값의 갯수가 적은 경우 


<리스트2> 1: int days[12]={31,28,31,30,31,30,31,31,30,31}; 

2: 

3: main() 

4: { 

5: int i; 

6: extern int days[]; /* 외부배열 선언 */ 

7: 

8: for (i=0;i < 12;i++) 

9: printf("%d월은 %d일이다.\n",i+1,days[i]); 

10: } 


위 프로그램에서 days는 외부 배열로 선언되고 초기화되었다. 외부 배열 

도 정적 배열과 같이 초기화하지 않으면 0으로 자동적으로 채워진다. 

위에서는 초기화를 하긴 하였으나, 배열요소 12개 전체를 한 것이 아니라 

10개밖엔 초기화하지 않았다. 하지만 컴파일러는 자동적으로 남은 요소를 

0으로 채워 버린다. 출력 결과를 보면 알 수 있을 것이다. 

배열 선언시 첨자를 생략하는 경우는 어떻게 될까? 1행을 


int days[]={31,28,31,30,31,30,31,31,30,31}; 


로 바꾸면 출력 결과는 10월까지만 나올 것이다. 왜냐하면 자동적으로 컴 

파일러가 배열 크기를 초기화 갯수와 같은 10으로 간주하기 때문이다. 배 

열 첨자를 생략할 경우에는 초기화시 주의가 필요하다. 


그리고 int i, arr[10]; 

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

arr[i]=i+1; 

printf("arr[%d]=%d\n",i,i+1); 


와 같이 배열 선언과 동시에 초기화하지 않을 시에는 절대로 배열 첨자를 

생략하면 안된다. 위의 예는 10개의 방에 1에서 10까지 대입하는 보기이다 


( ** 변수나 배열은 선언과 동시에 초기화 (0으로라도) 하는 습관을 

가지는게 쓰레기값 해소에 좋다 ** ) 



<예제3> 1 - 12월의 이름을 영어로 배열에 기억시켜두고 1-12 의 수치를 

입력하여 그에 대응하는 월의 이름을 출력하는 프로그램 작성. 


<리스트3> 1: #include 

2: 

3: main() 

4: { 

5: int n; 

6: static char arr[][10]={ 

7: "January", "February","March","April", 

8: "May","Jun","July","August","September", 

9: "October","November","December" }; 

10: do { 

11: printf("원하시는 달은? "); 

12: scanf("%d",&n); 

13: if (n == -999) break; 

14: printf("%d는 영어로 %s입니다.\n",n,arr[n-1]); 

15: } while (1); 

16: } 


6행: 문자열 자체는 1차원 배열로 간주되므로 당연히 2차원 배열로 선언. 

=0;i 12 printf("\n자동생성된 배열:"); 

13 for (i=0;i 14 bubble(x,MAX); 

15 printf("\n거품정렬된 배열: "); 

16 for (i=0;i 17 } 

18 

19 void bubble(int x[], unsigned n) 

20 { 

21 register i,j; 

22 int temp; 

23 

24 for (i=0;i 25 for (j=n-1;j>i;j--) { 

26 if (x[j-1] <= x[j]) continue; 

27 temp=x[j-1]; 

28 x[j-1]=x[j]; 

29 x[j]=temp; 

30 } 

31 } 


1행: 내장 함수인 ramdomize(),random()함수는 stdlib.h에 선언되어 있다 

2행: #define은 매크로로써 프로그램 안의 MAX를 20으로 치환하라는 의미 

이다. 예를 들어 이런 식으로 매크로를 지정해 두면 x[]배열의 크기 

를 변화시킬 필요가 있을 때 x[]배열이 사용된 모든 부분을 찾아가 

수를 변화시켜야 한다. 하지만, 이 경우에는 #define MAX 20에서 20 

을 다른 수로만 바꿔주면 된다.(매크로에 관해서는 마지막 장에서 공 

부할 것이다) 

4행: 사용자 함수의 선언 

10행: 난수 발생자를 초기화 하는 함수 

11행: random(n)은 0에서 n-1까지의 정수 난수를 발생시켜 돌려주는 함수 

14행: x는 배열 x[20]의 첫번째 원소 x[0]의 번지를 가리키는 포인터이다 

19행: void bubble(x,n); 

int x[]; ===> void bubble(int x[],unsigned n) 

unsigned n; 

가인수 형선언을 함수명의 가인수 리스트에 위와 같이 포함시킬 수 

있다. 

19-30행: bubble()함수는 버블 정렬을 하는 함수이다(각종 정렬의 원리는 

자료구조 관련 서적을 참고하길 바란다). 



배열도 변수처럼 함수끼리 주고받을 수 있다. 14행과 같이 배열명과 배열 

의 크기를 넘겨주면, 피호출측 함수는 배열을 처리할 수 있다. 배열명은 

배열을 가리키는 주소값을 가지고 있으므로 참조에 의한 전달에 속한다고 

할 수 있다. 그러므로 bubble()함수에서는 배열의 내용을 직접 바꾸는 것 

이 가능한 것이다. ( 5장 #1 참고 ) 



<예제5> 매개 변수가 배열인 함수 - II 


<리스트5> main() 

char a[]="Konglish"; 


prnt(a); 

printf("a[]=%s",a); 


prnt(char x[]) /* void함수 => prnt(x) */ 

{ /* char x[]; 로 써도 된다 */ 

int k; 

for (k=0;k<9;k++) 

printf("a[%d]=%c\n",k,x[k]); 

x[0]='E'; 


<예제6> 2차원 배열을 함수에 넘겨주는 예제 


<리스트6> main() 

static char a[][4]={ 

"Son", "Kim", "Han" }; 


prnt(a); 


prnt(char x[][4]) 

int i,j; 

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

printf("%s\n",x[i]); 



2. 포 인 터 (Pointer) - 포인터는 주소값(번지)이다. 


포인터는 다른 변수의 주소를 기억하는 변수이다. 포인터는 포인터 연산 

자 '*'를 이용해서 선언한다. 포인터는 번지를 기억한다고 했으므로 연산 

자 '&'와 동일하다고 생각할지 모르겠으나 실제로는 전혀 다르다.'&'는 번 

지 자체를 말하는 것이지만 포인터 변수는 그 포인터가 가리키는 '번지의 

내용'을 말한다. 이와 같이 포인터를 이용하여 자료를 지정하는 것을 ' 자 

료의 간접 지정'이라고 말한다. 

포인터 변수는 그 포인터가 가리키는 실체의 형에 맞추어 선언해야 한다. 

정수형의 자료를 가리키는 포인터라면 정수형으로 선언되어야 하며, 장차 

실수형의 자료를 가리킬 포인터라면 실수형으로 선언해야 한다. 또한 문자 

열이나 단일 문자를 가리킬 포인터는 문자형으로 선언해야 한다. 예를 들 

어, 


int a=10, b=20, c=30, *p; 

p=&a; 


는 정수형 변수와 정수형 포인터 변수를 선언하고 초기화한 것이다. 여기 

서 변수 a,b,c가 연속된 공간에 저장되며 그 선두 번지가 1000H라고 가정 

할 경우 저장형태는 다음과 같다. 


이 때 &a는 1000H, &b는 1002H, &c는 1004H가 

a b c 된다. 여기서 'p=&a'라고 명령하면 포인터 p의 

+-----+-----+-----+ 실체는 1000H가 되지만 우리에게 되돌려 주는 

| 10 | 20 | 30 | 값은 1000H에 저장된 값 즉 10이 된다. 

+-----+-----+-----+ 그리고 p += 1;에 의해 p이 값을 하나 증가시 

1000H 1002H 1004H 키면 포인터 p의 값은 그 자신이 가리키는 자 

료의 형(int)에 맞추어 자동으로 2가 증가되어 

1002H가 되고 우리에게 20이라는 값을 되돌려 준다. 


이와 같이 포인터를 이용하면 임의의 메모리 번지에 접근해서 그 값을 꺼 

낼 수 있게 된다. 또한 프로그래머는 포인터의 형만 적절히 지정하면 굳이 

메모리 번지를 의식하지 않아도 메모리내의 자료를 정확히 엑세스할 수 있 

는 것이다. 즉, 포인터의 형이 정수형이면 연속된 2Byte이 값을 꺼내와 정 

수로 치환하여 돌려주고, 포인터가 실수형으로 선언되었을 때는 일련의 자 

료들을 실수형에 맞추어 변환한 다음 돌려준다. 


(***하지만 볼랜드 C의 경우에는 변수가 메모리 공간에 역순으로 저 

장이 된다. 여기서는 설명의 편의상 TC를 기준으로 하였다***) 


포인터가 문자형일 경우에는 그 위치의 문자를 되돌려 주거나 '\0'을 만 

날 때까지의 문자열을 돌려준다. 




<예제7> 정수형 변수에 각각 10,20,30,40,50,60을 기억시킨 후 포인터를 

참조하여 그 값을 출력하는 프로그램 작성. 


<리스트7> main() 

int a=10,b=20,c=30,d=40,e=50,f=60, *p; 

p=&a; 

printf("변수(x) 주 소(&x)값(*p)포인터(p)\n"); 

printf(" %c %04X %d %4X\n",'a',&a,*p,p); 

p ++; /* 볼랜드 C의 경우는 p --;로 기술한다 */ 

printf(" %c %04X %d %4X\n",'b',&b,*p,p); 

p ++; 

printf(" %c %04X %d %4X\n",'c',&c,*p,p); 

p ++; 

printf(" %c %04X %d %4X\n",'d',&d,*p,p); 

p ++; 

printf(" %c %04X %d %4X\n",'e',&e,*p,p); 

p ++; 

printf(" %c %04X %d %4X\n",'f',&f,*p,p); 


<실행결과> # TC의 경우이다. 옆의 표에서 보듯이 포인터 자체 

+-------+---------+------+---------+ 의 값은 변수의 번지와 동일하나, 

|변수(x)|주 소(&x)|값(*p)|포인터(p)| 포인터를 참조한 값은 변수의 내용 

+-------+---------+------+---------+ 과 일치한다. 여기서 주목할 것은 

| a | FFD2 | 10 | FFD2 | p++로 포인터를 1씩 증가시키고 있 

| b | FFD4 | 20 | FFD4 | 지만 포인터는 자신의 형인 int형 

| c | FFD6 | 30 | FFD6 | 에 맞추어 2씩 증가하고 있다는 점 

| d | FFD8 | 40 | FFD8 | 이다. 

| e | FFDA | 50 | FFDA | 그렇다면, 여러분들이 직접 변수 

| f | FFDC | 60 | FFDC | 들을 실수형으로 선언하여, 실수형 

+-------+---------+------+---------+ 포인터에서는 어떤 현상이 일어나 

# 주소는 시스템에 따라 다르다. 는지 실행해 보라. 



문자열 포인터(문자열을 가리키는 포인터) - '터보 C 정복' 



우선 예제를 보면서 시작하자. 


1: main() 

2: { 

3: char array[20 + 1]; /* 문자열의 끝에 '\0'을 추가하기 위해 */ 

4: char *ptr; /* 확보할려는 공간에 1을 더하여야한다 */ 

5: 

6: array="abcdefghijklmnopqrst"; /* 문자열을 array에 할당 */ 

7: ptr=array; /* array의 주소값을 ptr에 대입 */ 

8: 

9: printf("%s\n", array); 

10: printf("%s\n", ptr); 

11: printf("%p\n", array); 

12: printf("%p\n", ptr); 

13: printf("Str length(array) : %d\n", strlen(array)); 

14: printf("Str length(ptr) : %d\n", strlen(ptr)); 

15: printf("array[0] - %c\n", array[0]); 

16: printf("array[9] - %c ptr[9] - %c\n", array[9], ptr[9]); 

17: printf("End of ptr: %d\n", ptr[strlen(ptr)]); 

18: printf("*(ptr + 9)=%c\n", *(ptr + 9)); 

19: printf("================================\n"); 

20: getch(); 

21: } 


(실행결과) 

abcdefghijklmnopqrst 

abcdefghijklmnopqrst 

2000 <--- 시스템에 따라 다르다. 

2000 (두 개의 값이 같다는 점에 주목하라) 

Str length(array) : 20 

Str length(ptr) : 20 

array[0] - a 

array[9] - j ptr[9] - j 

End of ptr: 0 

*(ptr + 9)=j 

================================ 



문자 배열 array(상수) 

6행 수행 : +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+ 

|a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|\0| 

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+ 

7행 수행 : | 

포인터 ptr -+ (변수) 


6행을 수행하면 배열 array는 내부적으로는 문자열"abcd.."가 저장된 공 

간의 첫째 영역의 주소값(2000)을 가지게 된다. 


배열명은 그 배열의 시작점의 주소값을 가지는 <포인터 상수>이다. 


7행에서 문자배열의 시작 주소값(array)을 포인터 <변수>ptr에 대입하였음 

으로 ptr의 값도 array와 같은 주소값(2000)을 가지게 된다. 하지만 개념 

적으로는 




라고 한다. 


9행, 10행 수행: 

printf함수의 %s서식은 인수를 문자열 포인터로 인식하고 그 포인터가 가 

리키는 번지부터 널 종료문자(\0)까지의 문자들을 출력하는 기능을 가진다 

그러므로, array와 ptr가 가리키는 주소를 찾아가 그 곳에 있는 문자(a)로 

부터 \0문자까지 출력을 하는 것이다. 


11행, 12행 수행: 

%p서식은 인수가 가지는 주소값을 출력하라는 서식이다. 위에서 array와 

ptr을 같은 주소값을 가지는 것을 알 수 있다. 


13행, 14행 수행: 

strlen함수는 포인터를 전달받아 그 포인터가 가리키는 번지부터 널 종료 

문자 직전까지의 문자의 갯수를 세어서 정수값으로 리턴한다. 위에서 arra 

y와 ptr이 같은 문자열 영역을 가리킴을 알 수 있다. 포인터의 입장에서 

보면 문자열과 문자 배열을 전혀 구분할 수 없고, 완전히 동등하게 취급된 

다. 문자 배열을 문자열로 간주하면 문자 배열이 곧 문자 배열이 되는 것 

이다. 


15, 16, 17행 수행: 

char array[21]="...."배열에서 일반적으로 array[n]은 n번째(처음이 0 

번째) 문자를 나타내는 것을 잘 알고 있을 것이다. 그러면, ptr[n]이란 것 

은 무엇을 의미할까? 곧, <문자열> 포인터ptr이 가리키는 번지부터 n번째 

문자열 요소를 가진다. 

위에서 보듯이 배열과 포인터는 매우 밀접한 관계를 가짐을 알 수가 있다 

17행에서 strlen(ps)의 결과는 20이다. 그러므로 ptr[strlen[ptr])은 ptr 

[20]과 동일하다. 위의 그림에서 그 위치에는 널 종료문자 \0이 있으므로 

화면에는 0이 출력된다. 


18행 수행: 


px + 1; 의 의미는 과연 무엇일까? 

이것은 px가 가리키는 배열 요소의 바로 다음 배열요소를 가리킨다 

(배열요소가 있는 곳의 번지값을 가진다) 

여기서 주의 할 점은 px 바로 다음 바이트의 번지를 가리키는 것이 아님 

에 유의해야 한다. px+1이 바로 다음 바이트의 번지를 가리키게 되는 경우 

는 px가 문자열 포인터일 때뿐이다.(각 요소가 1byte크기의 문자이므로) 

만일 px가 int형 포인터라면 px + 1은 2바이트 다음의 번지를 가진다. 


이렇게 C에서는 포인터가 무슨 형임에 상관없이 자동적으로 번지를 증가 

시켜줌에 유의해야 한다. 


그럼, ptr+9의 의미는 무엇일까? ptr이 가리키는 번지(a가 저장되어 있는 

곳의 번지)로부터 9번째 요소가 저장되어 있는 곳(j)의 번지를 가리킨다. 


그런데 위에서 *(ptr + 9)라고 했다. 이것의 의미는 (ptr + 9)가 가리키 

는 곳에 있는 내용을 나타내라는 의미이다. 


출력결과를 두고 보았을 때 


+-------------------------------------------------+ 

| array[i]와 *(ptr + i)는 완전히 동등한 수식이다. | 

+-------------------------------------------------+ 


라는 사실을 유추할 수 있다. 아예, 위 문장을 암기해 버리자. 


+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+ 

|a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|\0| 

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+ 

| | | 

ptr+0 ptr+2 .... ptr+9 ==> 이 각각의 내용은 주소값 

*(ptr+0) , ..... *(ptr+9) ==> 이 각각의 내용은 문자 


&array[9] == &(*(ptr+9)) == ptr+9 

array[9] == *(ptr+9) == 'j' 


 

3. 포인터와 배열 


포인터는 배열과 아주 밀접한 관계를 가진다. 실제로 배열명은 배열의 선 

두 번지를 가리키는 '포인터 상수'이다. 배열은 그 내용이 배열 영역에 저 

장되나 문자열을 포인터 변수로 지정할 경우에는 그 내용이 단순 변수 영 

역에 저장된다. 또 배열은 첨자의 값을 계산해야 하지만 포인터는 연속된 

메모리 영역의 '내용'을 꺼내오므로 처리속도도 빠르다. 


만일 arr[]가 배열이라면, 

arr == &arr[0] (배열명은 배열의 첫번째 요소의 번지를 나타내는) 

( 포인터 상수이다 ) 

<예제8> 포인터와 배열 


<리스트8> main() 

int date[4], *pti, i; 

float bill[4], *ptf; 


pti=date; 

ptf=bill; 


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

printf("Pointer+%d: %10x %10x\n",i,pti+i,ptf+i); 


위 프로그램의 실행 결과에서 번지의 증가량을 주목하라! 

위의 예제는 배열과 포인터의 밀접한 관계를 보여준다. 이것은 배열의 각 

원소를 구별하고 그 원소의 값을 획득하는데 포인터를 사용할 수 있음을 

의미한다. 실제로 컴파일러는 배열 표기를 포인터로 변환시켜 처리한다. 


*(date+2)와 *date + 2는 엄연히 다른 것이다. 포인터 연산자'*'는 +보다 

높은 우선순위를 가지고 있으므로 후자는 (*date)+2를 의미한다. 


*(date + 2) => date의 세번째 원소의 값 

*date + 2 => 첫번째 원소의 값에 2를 더한 값 


배열과 포인터의 이러한 관련성 때문에 프로그램 작성시 아무것이나 선택 

하여 사용할 수 있다. 함수가 배열을 인자로 넘겨받는 경우가 그 일례이다 

. ( <예제5> , <예제6> , <예제7> 참조 ) 



그럼, 아래의 두 가지 비교를 보자. 


*** 배열을 사용한 평균값을 구하는 함수 


int mean(array,n) 

int array[], n; 

int i; 

long sum; /*모든 요소의 합이 int범위를 넘을지도 모르므로 */ 

/* long형으로 선언 */ 

if (n>0) { 

for (i=0,sum=0;i sum += array[i]; /*배열요소 모두를 더한다*/ 

return((int)(sum/n)); /*평균값을 int형으로 변환하여 넘겨줌*/ 

else { 

printf("No array.\n"); 

return(0); 


*** 포인터를 사용한 배열의 평균값을 구하는 함수 


int mean(pa,n) /* array와 pa는 완전히 동일하다. */ 

int *pa,n; /* 함수의 매개변수로 선언되었을 때는 배열은 */ 

/* 자동적으로 포인터로 변환된다. */ 

int i; 

long sum; 


if (n>0) { 

for (i=0,sum=0;i sum += *(pa + i); 

return((int)(sum/n)); 

else { 

printf("No array.\n"); 

return(0); 


그렇다면, 위의 두 함수를 호출하는 호출문은 어떻게 될까? 


mean(numb,size); 


위의 numb는 배열명이다. 앞에서 이야기 한 것 처럼, 


int pa[]; 와 int *pa;는 같은 것이다. 


단지 차이점은 pa[]에서의 pa는 포인터 상수(변화시킬수 없다) 

이고, *pa에서의 pa는 포인터 변수(변화시킬 수 있다)라는 것이다. 


다시 말해서 포인터 상수 pa는 pa++라는 표현이 불가능하나 포인터 변수 

pa는 pa++는 표현이 가능하다. 



비록, 배열과 포인터가 위와 같이 밀접하게 관련되어 있다고는 하지만 차 

이점도 가지고 있다. 주로 포인터가 훨씬 광범위하게 이용된다. 하지만 초 

보자들은 배열이 더 분명하고 혼동이 덜 간다고 느낄 것이다. 그러나, 여 

러가지 이점으로 볼 때 포인터가 훨씬 효율성이 높다고 할 것이다. 



문자열을 포인터 변수로 초기화했을 경우에는 배열처럼 처리할 수 있다. 

<배열과 포인터의 비교> 


+- char *p ="ABCDEFG"; 초기화한 문자열 포인터는 배열로 생각할수 있다. 

| 즉, char p[]="ABCDEFG";와 같다. 

+- p[0]='K'; 문자열의 첫 문자가 K로 바뀐다. 


+- char *p; 이것은 에러이다. 초기화하지 않은 포인터 변수는 

| 배열로 생각할 수 없다. 

+- p[0]='A'; 


+- int x, a[10], *p; 

| p=a; p=&a[0]과 같다 

| x=*p; x=a[0]과 같다 

| x=a + 3; x=&a[3]과 같다 

| x=*(a + 3); x=a[3]과 같다 

| *a=123; a[0]=123과 같다 

| *(a + 1)=456; a[1]=456과 같다 

+- *(a + 2)=789; a[2]=789와 같다 


&a[i] + n=10; &a[i+n]=10; a + i + n=10; 

이 세식은 모두 같다 


<예제9> 다음과 같이 출력하는 프로그램을 작성하라. 


z | 

yz |<리스트9> main() 

xyz | { 

: | char *arr="abcdefghijklmnopqrstuvwxyz", 

: | *buf=" "; 

abcde .... vwxyz | int i,j; 

-----------------+ for (i=26;i>=0;i--) { 

for (j=i;j<=26;j++) 

*(buf+j)=*(arr+j); 

printf("%s\n",buf); 



4. 포인터 연산 



<예제10> 포인터가 할 수 있는 연산 


<리스트10> 

1: main() 

2: { 

3: static int urn[]={100,200,300}; 

4: int *ptr1, *ptr2; 

5: 

6: ptr1=urn; /* ptr1=&urn[0]과 같다 */ 

7: ptr2=&urn[2]; 

8: 

9: printf("ptr1=%x, *ptr1=%d, &ptr1=%x\n",ptr1,*ptr1,&ptr1); 

10: ptr1++; /* 증가: 포인터가 그 다음 2byte를 가리키게 함 */ 

11: printf("ptr1=%x, *ptr1=%d, &ptr1=%x\n",ptr1,*ptr1,&ptr1); 

12: printf("ptr2=%x, *ptr2=%d, &ptr2=%x\n",ptr2,*ptr2,&ptr2); 


13: ++ptr2; 

14: printf("ptr2=%x, *ptr2=%d, &ptr2=%x\n",ptr2,*ptr2,&ptr2); 


15: printf("ptr2 - ptr1=%x\n", ptr2 - ptr1); 

16: } /* 차: 두 포인터의 차를 구할 수 있다 */ 


위에서 말했듯이 배열명은 포인터 '상수'라고 하였다. 상수에 상수값을 

변화시키는 증감 연산자를 사용할 수 없듯이, 포인터 상수에도 사용할 수 

가 없다. 


< 적 법 > | < 위 법 > 

ptr1 ++; |urn ++; 

x ++; |3 ++; 

ptr2=ptr + 2; |ptr2=urn++; 

ptr2=urn + 1;/* urn의 값을 변화시*/ |x=y + 3++; /* 상수값을 */ 

/* 진 않으므로 적법 */ | /* 변화시키므로 위법*/ 


위의 예로 보더라도 배열명보다는 배열명을 포인터 변수에 대입시켜서 사 

용하는 것이 훨씬 효율적이다. 


<리스트10>에서 ptr, *ptr, &ptr의 차이점을 명확히 확인할 수가 있다. 



포인터는 주소값을 가지고 있는데, &ptr은 포인터 자체의 번지를 가리킨다 

. 9행의 상태는 아래와 같다. 

ptr urn[0] urn[1] urn[2] 

+--------+ +---------+---------+---------+ 

| | | | | | 

| dd32 | =======> | 100 | 200 | 300 | 

| | | *ptr | | | 

+--------+ +---------+---------+---------+ 

각영역의 주소 ff22 dd32 dd34 dd36 

==== => &ptr 



5. 다차원 배열과 포인터 



이번 장의 #1편에서 다차원 배열에 관한 예제가 부족했는데 예제를 작성 

해 보면서 덤으로 다차원 배열과 포인터의 관계에 대하여 알아보자. 



1) 'C Primer plus'에 나오는 강우량을 분석하는 프로그램 


5년 동안의 월별 강우량을 분석하려고 한다. 5년이 총 60개월이므로 60개 

의 원소(강우량)를 갖는 1차원 배열을 사용하여도 되나, 각 연도별로 12개 

의 원소를 갖는 2차원 배열을 쓰는 것이 알아보기도 쉽고 효율적이다. 


static float rain[5][12]; /* 5는 연도이고 12는 달을 나타낸다 */ 


이 프로그램은 연도별 총강우량과 5년 평균 강우량 및 월평균 강우량을 

구하는 것을 목적으로 한다. 


1 #include 

2 #define TWLV 12 

3 #define YRS 5 

5 main() 

6 { 

7 static float rain[YRS][TWLV]={ 

8 {10.2, 8.1, 6.8, 4.2, 2.1, 2.8, 0.2, 0.3, 1.1, 2.3, 6.1, 7.4}, 

9 {9.2, 9.8, 4.4, 3.3, 2.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 5.2}, 

10 {6.6, 5.5, 3.8, 2.8, 1.6, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 4.2}, 

11 {4.3, 4.3, 4.3, 3.0, 2.0, 1.0, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6}, 

12 {8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.2} 

13 }; 

14 int year, month; 

15 float subtot, total; 

16 

17 printf(" 년도별 강우량 (인치)\n\n"); 

18 for (year=0,total=0;year < YRS; year++) { 

19 for (month=0,subtot=0;month < TWLV;month++) 

20 subtot += rain[year][month]; 

21 printf(" %5d %15.1f\n", 1989+year, subtot); 

22 total += subtot; 

23 } 

24 printf("\n 해마다 평균 %.1f인치의 비가 내렸습니다.\n\n" 

,total/YRS); 

25 printf("월별 평균 : \n"); 

26 printf("1월 2월 3월 4월 5월 6월 7월 8월 9월10월11월12월\n"); 

27 for (month=0;month 28 for (year=0,subtot=0;year 29 subtot += rain[year][month]; 

30 printf("3.1f ",subtot/YRS); 

31 } 

32 printf("\n"); 

33 } 


<설명> 

18 - 23행: 중첩된 for문으로 year가 하나씩 변할 동안 month는 12번 변 

한다. 즉, subtot는 연도별 강우량을 구하기 위한 변수이고, total은 5 

년간 총강우량을 구하는 변수이다. 

24행: 5년간 총강우량을 YRS(연도수: 5)로 나눈 연평균 강우량을 출력. 

27 - 31행: month가 하나씩 변할 동안 year는 5번 변한다. 즉, subtot 변 

수로 해마다 같은 달의 강우량을 모두 더해 저장한 후, 30행에서 그 달 

의 5년 평균 강우량을 출력한 후, 다시 다음 달로 루프를 돈다. 




2) 포인터와 다차원 배열 

0 1 

0 +--+--+ 

int zippo[3][2]; 배열 zippo 1 +--+--+ 

int *pri; 2 +--+--+ 

pri=zippo; +--+--+ 


로 배열이 선언되고 포인터가 초기화 되어 있을 때, pri는 어디를 가리킬 

까? 이것은 첫째 행의 첫째 열의 번지를 가리킨다. 


zippo == &zippo[0][0] 


그러면, pri+1으로 pri를 하나씩 증가시킬 때 pri는 얼만큼 증가될까? 


pri == &zippo[0][0] 

pri + 1 == &zippo[0][1] 

pri + 2 == &zippo[1][0] 

pri + 3 == &zippo[1][1] 

pri + 4 == &zippo[2][0] 식으로 행우선으로 열이 하나씩 변화해 간다. 


위에서 zippo는 이차원 배열명이다. 그러면, 각각 두개의 원소로 구성된 

배열인 3개행에 대한 이름은 무엇일까? 첫 행의 이름은 zippo[0]이고 3 행 

의 이름은 zippo[2]이 된다. 이것은 아래 사실을 의미한다. 

0 1 

zippo[0] == &zippo[0][0] | zippo는 zippo =============> +--+--+ 

zippo[1] == &zippo[1][0] | 3*2배열 zippo[0] ==> 0 | | | 

zippo[2] == &zippo[2][0] | 전체의 +--+--+ 

| 2 차원 zippo[1] ==> 1 | | | 

각 부분배열명은 각 부분배열 | 배열명임 +--+--+ 

의 첫번째 요소의 주소를 가리 | zippo[2] ==> 2 | | | 

키는 포인터이다. | +--+--+ 


<예제11> 부분 배열명과 포인터의 관계 ( #1의 <예제 6>과 비교 요함 ) 


<리스트11> 

1 : main() 

2 : { 

3 : static int junk[3][4]={ 

4 : { 2, 4, 6, 8 }, 

5 : { 100, 200, 300, 400 }, 

6 : { 10, 40, 60, 90 } 

7 : }; 

8 : int row; 

9 : 

10: for (row=0;row < 3;row++) 

11: printf("%d행의 평균은 %d이다.\n",row,avrg(junk[row],4)); 

12: } 

13: 

14: int avrg(int array[],int n) 

15: { 

16: int i; 

17: long sum; 

18: 

19: if (n>0) { 

20: for (i=0,sum=0;i < n; i++) 

21: sum += (long) array[i]; 

22: return((int)(sum/n)); /*괄호는 cast연산자의 우선순위 때문*/ 

23: } 

24: else { 

25: printf("배열 어디갔어?\n"); 

26: return(0); 

27: } 

28: } 



배열을 함수로 넘기는 예제들에서 반드시 배열의 크기도 같이 넘기는 것 

을 눈치빠른 분들은 느꼈을 것이다. C는 배열의 크기가 어찌됐건 상관하지 

않으므로 이 점은 항상 신경을 쓰는 것이 좋은 습관이다. 

11행에서는 2차원 배열의 부분배열(1차원 배열)명을 함수로 넘겨서 함수 

에서는 2차원 배열의 부분배열인 1차원 배열로 처리를 하고 있다. 




6. 포인터 배열 




포인터 배열이란 포인터를 요소로 하는 배열을 말한다. 포인터란 다른 변 

수의 주소를 기억하는 변수라 했으므로 각각의 변수의 주소를 배열에 저장 

해 둔 것이라고 볼 수 있다. 이 포인터 배열을 이용하면 단순 변수를 배열 

처럼 취급할 수 있으며, 2차원 배열을 1차원 배열로 표현하는 것이 가능하 

여 진다. 이러한 기능은 다른 언어에는 없는 C 고유의 기능이다. 예를들면 


main() 

static int a=10,b=20,c=30,d=40,e=50; 

static int *arr[5]={&a,&b,&c,&d,&e}; 

int i; 

*arr[3]=400; 

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

printf("arr[%d] ==> Address: %04X,Value: %d\n",i,arr[i],*arr[i]); 


위 프로그램에서는 배열 *arr[]의 요소로 변수의 주소를 지정하고 있다.그 

런 다음에는 배열의 요소를 참조하여 수의 값을 출력하고 있다. 즉, 단순 

변수를 배열 요소로 취급하게 되는 것이다. 포인터 배열은 위 프로그램과 

같이 그 원소로 다른 변수들의 주소를 갖는다. 

만약, 이 프로그램에서 변수 a - e 의 값을 키보드로 입력받으려면 다음 

과 같은 부분을 삽입하면 된다. 


for (i=0;i<=4;i++) { 

printf("*arr[i] =? "); 

scanf("%d",arr[i]); 

위에서 'arr[i]'는 그 요소의 번지를 기억하므로 scanf()함수에서 배열명 

앞에 번지 연산자 &를 기술하지 않아야 한다. 포인터 배열이 메모리에 수 

록될 때 배열 자체는 배열 영역에 저장되고, 그 내용들은 단순변수 영역에 

저장된다. 


하지만 이러한 포인터 배열이 등장하게 된 것은 길이가 각각 다른 문자열 

을 배열로 처리할 경우에 편리하게 이용되기 때문이다. 


<예제12> 포인터 배열을 사용하는 대표적인 예 - 이외에는 거의쓰지 않음 

( 길이가 일정치 않은 문자열을 배열로 처리하는 경우! ) 


<리스트12>1 main() 

2 { 

3 static char *week[]={ 

4 "Sunday","Monday","Tuesday","Wednesday", 

5 "Thursday","Friday","Saturday"}; 

6 int k; 

8 for (k=0;k<7;k++) 

9 printf("week[%d]=%c, %s\n",k,*week[k],week[k]); 

10 } 


<실행결과> week[0]=S, Sunday 

week[1]=M, Monday 

<도 해> 

+---+ +----------+ 

week[0] | . | ----------> | Sunday\0 | 

+---+ +----------+ 

week[1] | . | ----------> | Monday\0 | 

+---+ +----------+ 

| | | | 


week[0]에 s가 들어있는 것이 아니고 week[0]가 가리키고 있는 

번지의 내용이 S 이다. 


그리고, 프로그램의 9행에서 printf()함수에서 문자열을 나타내는 경우에 

%s와 메치되는 인자는 문자열의 첫번째 주소를 나타내는 포인터이다. 

(%s는 포인터가 가리키는 위치로부터 '\0\을 만날 때까지 그 내용을 출력 

하는 형변환 지정자이다 - 제 1장 참고 ) 


그러면, 포인터 배열을 함수로 인자로 넘겨줄 경우에는 어떻게 할까? 


<예제13> <리스트12>에서 9행의 출력을 함수에서 행하는 프로그램 작성. 


<리스트13> <리스트12>의 9행을 prnt(week[i]);로 고치자. 

그리고 prnt()함수를 작성하자. 

prnt(char *w[]) 

{ /*포인터 배열을 넘겨 받았으니 가인 */ 

printf("Week=%s\n", w); /*수 형선언도 포인터 배 */ 

} /* 열로 하자 */ 





7. 포인터와 인수 



우리는 앞에서 함수를 배울 때 Call by reference(참조전달)을 배웠다.포 

인터는 참조 전달을 행할 때도 이용이 된다. 이러한 경우 호출측에서는 인 

수의 주소를 전달하며, 피호출측에서는 인수를 포인터로 받아온다. 


sub(&a,&b,&c); 함수호출 | char *s="Welcome to Korea!"; 

| int a=10, b=20; 

void sub(k,b,s) | sub(s,b,c); 함수 호출 

int *k, *b, *s; 인수를 포인터로| 

{ ... 선언한다. | void sub(char *k,int a,int b); 

| { ... 인수를 포인터로 선언(*k) 


참조에 의한 전달은 앞에서 예제로 많이 들었다. 우리가 알고 있어야 하 

는 것은 포인터(주소값)을 함수로 넘김으로써 참조의 전달이 가능하게 된 

다는 것이다. 



<참고> 문자열 배열에 관한 터보 C 2.0의 버그 


필자가 프로그램을 작성하던 도중 우연히 터보 C의 버그를 발견하게 되었 

다. 이 글을 읽는 분들은 이러한 버그때문에 몇날 몇칠 고생하는 경우가 

없도록 그 사례를 밝히고자 한다. 

아래 프로그램을 보자. (알기 쉽게 최대한 간략화 시킨 것이다) 


main() 

char *str1=" "; 

char *str2=" "; 


printf("암호를 입력하십시요:"); 

gets(str1); 

printf("확인을 위해 한번더 입력하십시요:"); 

gets(str2); 


if (!strcmp(str1, str2)) printf("암호가 같습니다!"); 


여러분들도 감을 잡으셨겠지만, 위 프로그램은 두 개의 문자열을 받아 

비교하여 같으면 (strcmp함수는 두 개의 문자열의 주소를 받아 비교하여 

같으면 0을 되돌린다) 같다는 메시지를 출력하는 프로그램이다. 

하지만 위 프로그램을 실행시키면 메시지는 무조건 같다고 출력이 된다. 

그러나 str2의 공백을 증가시켜 str1과 다르게 하면 실행결과가 바로 나 

온다. 다른 컴파일러에서는 이러한 현상이 발생하지 않는다... 

무슨 말인지 확인하기 위해서 5행에 printf("%p %p", str1, str2);를 

삽입해 보라.. 이상한 게 보일 것이다.. 



이번장에서는 배열과 포인터에 대한 기본적인 개념을 많은 예제들과 함께 

설명을 하였다. 여러분들이 한 번 읽고는 이해가 잘 가지 않을 것이다. 그 

러나 예제들을 수행해 보고 그 예제들을 변경시켜 가면서 포인터가 어떠한 

작용을 하는지 알려고 한다는 의지만 있다면 이 들의 이해도 그리 어려운 

것만은 아닐 것이다. 

이 글에서는 포인터의 기본적인 내용만을 다루었다. 이 글에서 언급하지 

않은 포인터의 깊은 곳에 관한 내용은 여러분들이 초보딱지를 떼고나서 공 

부를 해야 이해하는데 무리가 없을 것이다. 


 



반응형

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

제 8장 - 화일 조작 / 기타의 것들  (0) 2016.03.22
제 7장 - 구조체와 공용체  (0) 2016.03.22
제 5장 - 함수 / 기억부류  (0) 2016.03.22
제 4장 - 연 산 자  (0) 2016.03.22
프로그래밍 기초 3  (0) 2016.03.22
Comments