끄적끄적

반응형

프로그래밍을 하기 위해서는 먼저 해당 프로그래밍에서 관리해야 할 데이터에 대한 분석과 구현해야 할 기능에 대한 조사가 이루어져야 합니다.  특히 관리해야 할 데이터에 대한 분석을 통해 사용할 타입을 정의를 하고 관리해야 할 자료구조를 선택하게 됩니다.  C언어에서는 사용자 정의타입을 정의할 수 있도록 구조체, 공용체, 열거형등의 문법을 제공하고 있으며 자료를 유효 적절하게 관리할 수 있도록 동적 메모리 할당이나 배열과 포인터에 관련된 문법 및 라이브러리를 제공하고 있습니다.  이번 장에서는 동적 메모리 할당을 제외한 나머지 사항에 대해 학습하기로 합시다.  동적 메모리 할당에 대한 내용은 기본적인 문법 사항을 다루고 나서 설명하기로 하겠습니다.

 

1. 배열

프로그래밍에서 배열이란 동일한 레코드를 연속적인 메모리에 관리하는 자료구조를 얘기합니다.  C언어에서는 동일 원소타입의 원소를 하나의 변수명(배열명)으로 관리할 수 있게 매커니즘을 제공하고 있습니다.

 

1.1       배열의 선언

배열을 선언을 할 때에는 배열명과 원소타입, 원소의 개수를 결정을 하여야 합니다.  C언어의 배열이 “같은 타입의 원소 여러개를 하나의 관리명(배열명)으로 관리하는 변수 타입”이라는 정의를 생각하며 쉽게 이해하실 수 있을 것입니다.

 

포맷:

[type] 배열명[배열의 크기];

 

배열의 경우 배열명(변수명)이 값을 지칭하지 않고 할당된 메모리 주소를 지칭하고 있는 reference 타입의 변수입니다.  배열명으로 여러개의 원소를 관리하기 때문에 특정 원소 값을 지칭하는 것은 불가능 하겠지요.  이에 배열명은 원소 타입의 포인터 상수로 취급을 하고 있으며 각 원소에 접근하기 위해서는 배열명이 갖고 있는 메모리 주소에서 거리에 해당하는 인덱스를 통해 접근하고 있다는 것이지요.

 

int arr[10]; /* 정수형 자료 10개를 사용하기 위해 배열 arr을 선언하였다. */

 

위와 같이 선언을 하면 정수형(4바이트) 10개를 사용할 수 있는 메모리가 연속적으로 할당되며 배열명은 할당된 첫번째 메모리 주소를 지칭하게 됩니다.

 

 다음의 그림은 배열을 선언하였을 때의 모습과 일반 변수를 선언하였을 때의 모습을 도식하여 보았습니다.  그림에서 알 수 있듯이 배열의 경우 배열명이 지칭하는 것은 할당된 메모리 주소이고 다른 타입의 변수명은 할당된 메모리에 저장된 값을 지칭합니다. (참고로 할당된 메모리 주소는 가정에 의해 명시한 것임에 유의하자.)

 

이로 인해 배열의 각 원소를 사용하기 위해서는 배열명이 갖고 있는 첫번째 원소의 주소에서 상대적인 거리를 나타내는 표현을 사용하게 됩니다. 

 

int arr[10];

arr[i] = 3;  /* arr에서 거리 i인 곳의 원소의 값을 3으로 대입 */

 

 int arr[10];에서 배열명 바로 뒤에 오는 []는 배열의 크기를 나타내는 지시연산자입니다.  하지만 arr[i]=3;에서의 []는 앞의 []연산자와는 다른 연산을 하는 연산자입니다.   arr[i]=3;는 *(arr + i)=3 과 완벽하게 동일한 표현으로써 []연산은 피연산자로 메모리 주소와 정수가 오게 됩니다.  해당 연산을 통해 메모리 주소에서 정수 거리에 있는 원소를 지칭하는 연산을 하게 됩니다.  즉, 두 개의 피 연산자 중 하나는 포인터형이 와야 하고 다른 하나는 정수형이 와야 합니다.  arr이 0x1000의 값을 갖고 있고 i가 2의 값을 갖고 있다고 가정을 할 경우에 arr[i] = 3; 은 0x1000 에서 거리 2인 곳에 있는 원소에 3을 대입하라는 표현인 것입니다. 앞서 연산자에서 포인터와 정수의 가감 연산에는 어떠한 원소에 대한 포인터냐에 따라 가감되는 기준이 다르다고 하였습니다.  즉 int형 포인터 변수 p가 0x1000주소를 갖고 있다고 할 때 p=p+2이라고 했을 때 연산 결과로 0x1002가 되지 않고 0x1008이 된다는 것이죠.  즉, p번지에서 int형 원소 2개 뒤의 메모리 주소가 얼마인지를 계산한다는 것이죠.  arr이 정수형 배열명이고 배열명이 첫번째 원소의 메모리 주소를 갖고 있다는 특징에서 arr은 선언문 이외에서는 포인터로 취급 받게됩니다.  특히 할당된 메모리 주소를 바꿀 수 없다는 것을 이해한다면 배열명은 선언문 이외에서는 포인터 상수로 인식해야 한다는 것이지요.  그렇다면 arr에서 거리 2인 곳에 있는 원소는 arr에서 정수형 크기의 두배가 더해진 곳에 있는 원소를 지칭하는 것이고 0x1000(정수형 포인터) + 2는 0x1008(정수형 포인터)이 되는 것입니다.  이러한 이유가 포인터형의 가감 연산에는 어떤 타입의 포인터 형인가에 따라 가감하는 기준을 다르게 준 이유입니다.  이로 인해 개발자는 배열을 선언하고 난 후에 사용할 때에는 배열의 몇번째 원소에 접근하는 것인지만 신경을 써도 큰 문제가 없게 된 것입니다. 

 

 추가로 배열의 원소를 초기화하면서 선언하는 방법에 대해서 살펴보기로 합시다.

 

int arr[10]={1,2,3,4,};

 

이와 같이 선언을 하면 초기화할 값이 명시된 영역은 해당 값으로 초기화 되고 나머지 부분은 0으로 초기화가 됩니다. 즉 arr[0]=1, arr[1]=2, arr[2]=3, arr[3]=4 가 되고 arr[4]~ arr[9]까지는 0으로 초기화가 된다는 것입니다.  하지만 배열의 선언시 어느 원소에도 초기화 값을 명시하지 않는다면 초기화 되지 않습니다.(물론 전역 변수의 경우에는 초기화가 되겠지요.)



1.2       배열의 사용

앞서 얘기를 하였듯이 배열명은 첫번째 원소의 메모리 주소를 지칭하고 각 원소를 사용하기 위해서는 인덱스를 이용하여야 합니다.  인덱스는 배열의 원소 중 몇 번째인가를 나타내는 것인데 C언어에서 인덱스는 1부터 시작하는 것이 아니라 0부터 시작하는 것에 유의하여야 합니다.  그 이유로 배열명이 첫번째 원소의 주소값을 지칭하고 인덱스는 상대적인 거리를 얘기하기 때문입니다.  즉, 첫번째 원소를 접근하고자 할 때 배열명에서 거리 0인 곳에 있는 원소라고 하는 게 당연하겠지요.

 

int arr[10]; /* 정수형 자료 10개를 사용하기 위해 배열 arr을 선언하였다. */

int index = 0;

while(index <10)

{

    arr[index]=index*2; /* 배열 원소 각각에 index*2의 값을 집어 넣는다. */

    index++;

}

index = 0;

while(index<10)

{

    printf("[%d]th number is [%d]\n",index, arr[index]);

    index++;

}

 

출력 결과

[0]th number is [0]

[1]th number is [2]

[2]th number is [4]

[3]th number is [6]

[4]th number is [8]

[5]th number is [10]

[6]th number is [12]

[7]th number is [14]

[8]th number is [16]

[9]th number is [18]

 

여기서 arr[index]는 *(arr+index)와 동치인 표현으로 수학적인 의미로는 arr + (sizeof(int) * index)주소에 있는 원소를 지칭하며 이는 arr배열의 index번째(0이 첫번째 원소) 원소를 사용하겠다는 것입니다.  이를 깊이있게 고민하면 왜 index를 1부터 시작하지 않고 0부터 시작하였는지를 알 수가 있을 것입니다. 

 

 이러한 특징은 배열과 포인터가 유효 적절하게 혼합하여 사용하게 해 줍니다.

 

 

 위의 그림처럼 arr+i를 수행하면 단순히 arr의 값에 i가 더해지는 것이 아니고 sizeof(int)의 의미가 숨어있다고 하였습니다.  또한 arr[i]는 arr+i와 같은 결과를 내는 것이 아니고 *(arr+i)와 같은 결과를 내게 된다고 하였습니다.  또한 &arr[i]는 arr+i와 동일한 표힌입니다.  다음의 두 개의 코드를 비교해 보시기 바랍니다.(두 개의 코드는 동일한 코드입니다.)


 

int arr[10];

int index=0;

while(index<10)

{

    scanf("%d",&arr[index]);

}

index = 0;

while(index<10)

{

    printf(" [%d]\n", arr[index]);

    index++;

}

int arr[10];

int index=0;

while(index<10)

{

    scanf("%d",arr+index);

}

index = 0;

while(index<10)

{

    printf(" [%d]\n", *(arr+index));

    index++;

}

 


int arr[10];

int index=0;

while(index<10)

{

    scanf("%d",&arr[index]);

}

index = 0;

while(index<10)

{

    printf(" [%d]\n", arr[index]);

    index++;

}

int arr[10];

int index=0;

while(index<10)

{

    scanf("%d",arr+index);

}

index = 0;

while(index<10)

{

    printf(" [%d]\n", *(arr+index));

    index++;

}

뒤에서 다시 하게 될 부분이지만 위의 코드를 포인터형 변수와 연동을 하여 작성한다면 다음과 같은 코드도 동일한 효과를 볼 수 있게 됩니다.

 

int arr[10];

int *parr;

int index=0;

 

parr = arr;

while(index<10)

{

    scanf("%d",parr);

    parr++;

}

index = 0;

parr = arr;

while(index<10)

{

    printf(" [%d]\n", *parr);

    index++;

    parr++;

}

 

  여러분은 간단하게 배열이 필요한 간단한 로직을 생각하여 구현해 보기를 바랍니다.  간단한 예로 m부터 n까지의 수를 더하는 로직이라든지 여러개의 수를 입력 받아 그 중 최대값을 찾는 로직이라든지 작은 것이라도 생각을 스스로 해 보고 구현하는 것이 남의 코드를 보고 이해하는 것보다 훨씬 값진 교육이라고 생각이 들어서 하는 얘기입니다.

 

 

1.2      문자열

  일상에서 우리는 이름이라든지 주소등의 여러 개의 문자의 집합을 하나의 값으로 취급하는 것들이 많습니다.  많은 프로그래밍 언어에서도 string이라는 용어로 혹은 타입으로 이를 제공하고 있는데 C언어에서는 타입으로는 제공하고 있지 않습니다.  다만, 문자형 배열을 통해 문자열을 관리할 수 있게 해주고 있는데 이를 위해 몇가지 약속된 것들이 있습니다.

 

  배열의 경우 선언을 하면서 배열의 크기를 정하게 되는데 (물론, 배열을 선언하면서 초기값을 명시하는 경우에는 표현상으로 배열의 크기를 명시하지 않아도 되지만 이는 초기값이 명시된 만큼을 배열의 크기로 하겠다는 표현이지 배열의 크기가 없는 것은 아닙니다.)  문자열로 취급하는 것들은 대부분 최대의 크기는 정할 수 있겠지만 언제나 그 크기가 같지는 않을 수 있습니다.  즉, char name[100];이라고 선언한다고 했을 때 name이라는 배열은 원소의 타입이 크기가 100인 배열이라기 보다 최대 100개의 문자를 가질 수 있는 문자열이라고 하는 것이 오히려 옳은 표현일 것입니다.  C에서 문자열이라는 별도의 타입이 지원되지 않기 때문에 문자형 배열을 빌려 사용하고는 있지만 문자열의 경우는 전체 원소를 하나의 값으로 취급한다는 점에서 문자 배열과는 다른 것입니다.  이러한 이유로 문자열의 경우에 유효한 문자와 유효하지 않은 문자의 경계를 구분하기 위해 ‘\0’(널 문자)를 두고 있습니다.  즉, 유효한 문자 뒤에 널 문자를 경계를 두어 그 이후의 문자는 유효하지 않음을 의미한다는 것입니다.  이는 char name[100]={‘j’,’a’,’n’,’g’,’\0’}; 와 같이 선언되어 있다고 할 때 name은 “jang”이라는 것이다.  C에서 문자 각각은 단일 따옴표(‘ ’)를 문자열은 이중 따옴표(“ ”)로써 표시하고 있습니다.  또한 “jang”과 같이 문자열을 표시하면 ‘j’,’a’,’n’,’g’,’\0’로 표현됩니다.  즉, 널문자가 언제나 유효 문자뒤에 들어가게 된다는 것입니다.  이는 개발자가 문자 배열을 문자열로 사용하고자 할 때 특정 문자열을 대입하고자 한다면 유효 문자 끝에 ‘\0’(널문자)를 대입하는 것을 잊지 말아야 하고 처리하고자 할 때에도 ‘\0’(널문자)가 있다는 것을 주의하여야 합니다.

 

char name[10];

while(i<8)

{

    name[i] = ‘a’+i;

    i++;

}

name[i]=’\0’;

 

  위는 최대 10개의 문자를 갖을 수 있는 문자열 name을 문자형 배열을 통해 선언하였고 name에 문자 ‘a’에서 순서대로 8개의 알파벳의 연속된 값을 대입하려고 할 때 ‘a’에서 ‘h’의 문자를 대입하는 것 이외에도 마지막에 ‘\0’문자를 대입해 주고 있습니다.  즉, 문자열을 위해 문자형 배열을 선언할 때 유효 문자의 최대 크기+1 만큼으로 배열 크기를 결정해 주어야 합니다.

 

☺ char name[100];에 문자열을 입력을 받고 입력한 문자의 개수를 카운팅하는 로직을 구현하시오.(strlen함수를 사용하지 말고 직접 구현해 보십시오


1.3      다차원 배열

많은 곳에서 다차원 배열을 설명을 할 때 행과 열등을 통해 구조화 시켜 설명하는 경우가 많습니다.  하지만 3차원 이상이 되며 우리는 이들을 쉽게 이해가 안 되지요.  실제 다차원 배열을 선언한다고 해서 메모리에 구조적으로 할당되는 것이 아니고 linear(선형)으로 할당이 됩니다.  실제 메모리에 할당되는 모습으로 여기서 설명을 할 것이고 이로써 3차원 이상의 배열에도 동일하게 적용할 수 있을 것입니다.

 

 "배열은 같은 타입(type)의 여러개의 원소를 하나의 변수명으로 사용하는 변수 타입이다."라고 앞에서 정의한 바가 있습니다.  결국 다차원 배열에 대한 정의는  "n차원 배열은 n-1차원 배열을 원소로 하고 이러한 원소 여러개를 하나의 변수명으로 사용하는 변수 타입이다."라고 할 수 있을 것입니다.  즉, 3차원 배열은 2차원 배열을 원소로 하는 배열이라는 것입니다.  그렇다면 int arr[3][3][5]; 라고 하였을 때 배열의 원소타입은 무엇이고 원소의 개수는 몇개가 되는 것일까요?  원소의 개수는 언제나 배열명 바로 뒤에 오는 []안에 수입니다.  즉, 위의 arr은 원소의 개수는 3이 되는 것이지요.  그리고 나머지는 원소 타입이 됩니다.  즉, int [3][5]가 원소 타입이라는 것입니다.  이를 메모리로 표현한다면 다음 그림과 같이 그릴 수가 있을 것입니다.

 


 위의 그림을 보면 알 수 있듯이 arr[0]은 삼차원 배열 arr의 첫번째 원소로써 이차원 배열이 됩니다.  실제 arr도 arr[0][0][0]의 메모리 주소를 가지고 있지만 arr[0]도 arr[0][0][0]의 주소를 갖게 됩니다.  물론, arr[0][0]도 같은 주소를 갖게 됩니다.  이러한 각 표현의 차이는 가감 연산에서 있게 됩니다.  arr+1의 경우는 원소 타입이 int [3][5]이기 때문에 현재 arr이 의미하는 메모리 주소에서 60번지를 더한 값이 되고 arr[0]+1의 경우는 원소 타입이 int [5]가 되기 때문에 20번지를 더한 값이 되고 arr[0][0]+1의 경우는 원소 타입이 int이기 때문에 4번지가 더한 값이 된다는 것입니다.  결국 우리는 해당 표현이 어떠한 원소에 대한 포인터로 취급하는 지를 이해한다면 다차원 배열은 쉽게 이해할 수 있게 됩니다.

 

이번장에서 배열에 대한 설명은 여기까지 하기로 하고 좀 더 자세한 내용은 함수 만들기 및 동적 메모리 할당등에 관한 주제를 다루면서 다시 설명하기로 하겠습니다.  여러분은 배열명은 할당된 메모리 주소를 의미하며 가감 연산에는 원소 타입이 기준이 된다는 것과 다차원 배열에서 원소의 개수는 배열명 바로 뒤에 있는 []안의 수라는 것에 대한 명확한 이해를 하시고 넘어가시길 바랍니다.


반응형
Please Enable JavaScript!
Mohon Aktifkan Javascript![ Enable JavaScript ]