[C] C언어 공부하기 9. 배열

2020. 1. 2. 17:30C,C++

출처 : http://www.soen.kr/

 

SoEn:소프트웨어 공학 연구소

 

www.soen.kr

 

9-1 배열.

9-1-가. 배열의 정의

3장에서 정의했듯이 배열은 동일한 타입을 가지는 변수들의 유한 집합이다

 

int ar[5];                // 크기가 5인 정수형 배열 ar

double avg[10];      // 크기가 10인 실수형 배열 avg

char st[128];          // 크기가 128인 문자형 배열 st

 

int ar[5]; 선언에 의해 컴파일러는 정수형 변수 5개를 저장할 수 있는 연속적인 메모리 공간을 확보한다. 이 배열은 메모리상에 다음과 같이 생성될 것이다.

자료 구조에는 배열 외에도 연결 리스트, 스택, 큐, 트리 같은 것들이 있다. 다음에 자료 구조를 따로 공부해 보면 알겠지만 이 중 배열이 가장 단순하면서도 사용 빈도가 높다.

 다른 자료 구조에 비해 배열은 낭비되는 메모리가 없으며 배열 요소들이 연속적인 공간에 배치되어 있기 때문에 요소를 참조하는 속도가 대단히 빠르다. 하지만 반드시 연속적이어야 한다는 제약이 있어서 새로운 요소를 삽입하거나 기존 요소를 삭제하는 속도는 무척 느리다는 것이 단점이다.

그래서 삽입 삭제가 빈번한 자료를 다룰 때는 배열 대신 연결 리스트를 많이 사용했었다. 하지만 요즘은 CPU의 속도가 워낙 빨라져서 크기가 지나치게 크지 않으면 복잡한 연결 리스트 대신 배열을 사용해도 성능상의 큰 차이가 없다. 

 

9-1-나. 배열의 특징

배열은 기본형과는 달리 여러 개의 변수를 하나의 이름으로 모아 놓은 것이다. 그래서 기본형 변수들과는 다른 면이 많다.

1) 배열 요소의 번호인 첨자는 항상 0부터 시작(Zero Base)한다.

2) 배열이 차지하는 총 메모리양은 배열의 크기에 배열 요소의 크기를 곱해서 구할 수 있다.

배열 크기=sizeof(배열)/sizeof(배열[0]);

3) 배열을 선언할 때 크기값은 반드시 상수로 주어야 한다. 

4)  C언어는 배열의 범위를 전혀 점검하지 않는다. 

     int ar[5];

     ar[8]=1234;

 

ar의 크기가 5밖에 안되는데 ar[8]이라는 존재하지 않는 배열 요소에 어떤 값을 쓰려고 했다. 이렇게 코드를 작성하면 당연히 에러로 처리될 것 같지만 이 코드는 에러는 고사하고 경고 하나 없이 아주 잘 컴파일된다.

물론 컴파일만 잘될 뿐이지 정상적으로 실행되지는 않을 것이다.

이렇게 만들어 놓은데는 다 이유가 있다. 배열 요소를 참조할 때마다 첨자 번호가 배열 크기보다 큰지, 작은지 점검해 보고 만약 범위를 벗어나면 에러나 경고로 처리할 수도 있다. 그러나 컴파일러가 이런 점검을 할 수 있는 경우는 첨자가 상수일 때뿐이다. 변수인 경우는 실행 시간에 범위를 점검하는 코드를 추가해야 하는데 매번 실시간으로 첨자의 유효성을 일일이 점검하려면 어쩔 수 없이 그만큼 실행 시간이 느려질 것이다. 배열의 강점은 무엇보다 신속한 요소 참조인데 이런 장점이 감소되는 것이다.

그래서 컴파일러는 배열 참조문에 대해 아무런 처리도 하지 않으며 배열의 범위를 점검하는 것은 고스란히 개발자의 몫으로 남겨져 있다. 

 

예제)

#include <Turboc.h>

 

void main()

{

     int i;

     int ar[5];

 

     i=1234;

     ar[5]=5678;

     printf("i=%d\n",i);

}

이 상태에서 i값을 출력해 보면 원래 대입했던 1234가 아닌 ar[5]에 대입된 5678이 출력될 것이다.

지역변수들은 선언된 순서대로 스택에 생성되는데 i가 ar 배열 다음에 인접하게 배치될 것이다. 그래서 ar[5]의 자리가 우연히 i가 기억된 스택 위치와 일치하게 되고 그래서 ar[5]에 대입된 값이 i의 값을 바꾸게 되는 것이다. 일부러 이런 효과를 노리는 경우는 없기 때문에 이는 단순한 실수라고 봐야 한다.

(추가 : * 하지만 실제로 코드 돌려보니 i=1234로 정상 출력됨. 항상 다음과 같이 저장되지는 않는듯 하다. *)

 

9-1-다. 다차원 배열

2차원 배열은 첨자 두 개를 사용하는 배열이다.

 

int ar[3][6];

double rate[2][20];

 

메모리는 선형적 1차원 공간이기 때문에 실제로는 2차원 배열을 선언하더라도 일직선으로 죽 연결되어 있다. 이렇게 1차원 메모리 공간에 저장될 수 있는 이유는 x,y 어느 방향으로도 길이가 무한하지는 않기 때문이다. 이 배열의 제일 마지막 요소는 ar[x-1][y-1]이 될 것이다.

 

9-1-라. 배열명

배열명이 단독으로 사용되면 배열의 시작번지값을 가지는 포인터 상수이다.

배열의 본질을 이해하는 아주 핵심적인 문장이므로 음절 하나 틀리지 않고 그대로 외워야 할 정도로 중요하다. 배열명이 단독으로 사용된다는 말은 첨자없이 배열의 이름만 적는다는 뜻이다. ar[0], ar[1]과 같이 첨자와 함께 쓰면 배열 요소 변수지만 ar과 같이 배열명만 쓰면 이 값은 배열의 시작번지를 가리키는 포인터값이 된다. 

 

char str[6]={'K','o','r','e','a'};

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

 

크기 6의 문자형 배열 str을 선언하되 "Korea"라는 문자열을 가지도록 초기화했다. 그리고 이 배열에 저장된 문자들을 printf의 %s 서식으로 출력해 보았다. 화면에 "Korea"가 출력될 것이다. 이 예제에서 보다시피 str이라는 배열명 자체는 "Korea"라는 문자열이 들어 있는 배열의 시작 번지를 가리키며 그래서 str 배열명을 printf의 %s서식에 대응시키면 이 번지에 들어 있는 문자열이 출력된다.

 str의 다른 표현은 &str[0]라고 할 수 있다. 

배열명이 포인터 상수이기 때문에 배열끼리는 대입할 수 없다. 설사 좌, 우변의 배열이 타입과 크기가 완전히 일치하더라도 대입은 허용되지 않는다. 그래서 다음과 같이 배열의 사본을 만들 수 없다.

 

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

int ar2[5];

 

ar2=ar;

 

만약 ar2와 ar을 완전히 같게 만들고 싶다면 루프를 돌면서 배열 요소를 개별적으로 대입해야 한다.

 

for (i=0;i<sizeof(ar)/sizeof(ar[0]);i++) {

     ar2[i]=ar[i];

}

 

9-2-가. 쓰레기값

배열을 함수 안에서 선언하면 지역변수가 되고 함수 밖에서 선언하면 전역변수가 되는데 기억 장소나 통용 범위가 일반 변수와 같다. 

C는 성능에 최우선의 가치를 두는 언어이기 때문에 조금이라도 불필요한 동작은 하지 않는다. 물론 전역으로 선언하면 컴파일러가 이 배열을 초기화한다. 하지만 지역변수일 경우 초기화하지 않는다. 만약 배열 요소를 원하는 값으로 초기화하려면 루프를 돌면서 배열 요소에 일일이 값을 대입해야 한다.

 

(** memcpy 함수를 쓰면 다 0으로 넣어줄 수 있지 않나? **)

 

일정 규칙을 가지지 않는 초기값으로, 예를 들어 4,8,3,69,-7 등의 임의값으로 초기화할 때는 루프를 돌아서는 안되며 별도의 배열 초기화 방법을 사용해야 한다. 

 

9-2-나. 1차 배열 초기화

int ar[5]={4,8,3,69,-7};

 

이렇게 선언과 동시에 초기화를 하면 메모리를 할당받음과 동시에 초기화 값들로 메모리를 채운다(=초기화한다). 선언 직후의 메모리 모양은 다음과 같을 것이다.

9-2-다. 초기식

배열을 선언과 동시에 초기화할 때는 초기값의 개수가 배열 크기와 일치하는 것이 가장 이상적이다. 다음과 같이 말이다.

 

int ar[5]={4,8,3,69,-7};

 

C는 초기값이 배열 크기보다 적을 경우 나머지 배열 요소들을 전부 0으로 초기화하며 뒤쪽의 0으로 초기화 할 요소에 대해서는 초기값을 따로 적지 않아도 된다.

 

int arBig[1000]={1,2,3};

 

이렇게 쓰면 앞쪽 세 개 요소만 1,2,3으로 초기화하고 arBig[3]~arBig[999] 까지를 모두 0으로 초기화한다.

(**그럼 반복문이 아니라 0으로 초기화할 때는 int arBig[1000] = {0,}으로 선언하면 그만이다.**)

 

 다음 세 선언문은 비슷해 보이지만 결과는 다르다.

 

int arBig[1000];

int arBig[1000]={0};

int arBig[1000]={0,};

 

선언만 할 경우 배열 요소들은 모두 쓰레기값을 가지지만 ={0}나 ={0,} 초기식을 뒤에 붙이면 모든 배열 요소가 0으로 초기화된다. 지역 배열을 전역 배열처럼 초기화하고 싶다면 선언문 다음에 ={0}만 붙이면 된다. 

다음은 초기값이 배열 크기보다 더 많은 경우를 보자.

 

int ar[5]={4,8,3,69,-7,1};

 

배열 크기는 5인데 초기값은 6개가 주어져 있다. 이 코드를 컴파일하면 too many initializer라는 에러로 처리되는데 즉, 초기값이 너무 많다는 얘기다. 초기값이 남는 경우는 십중 팔구 입력시의 오타가 원인인데 중간에 같은 값을 반복해서 입력했다거나 불필요한 콤마가 하나 더 삽입된 것이다

배열 선언문이 초기식을 가질 때는 배열의 첨자 크기를 생략할 수 있다. 다섯개의 초기값을 가지는 ar배열은 다음과 같이 선언하면 된다.

 

int ar[]={4,8,3,69,-7};

 

9-2-라. 2차 배열 초기화

1) 초기값 개수가 꼭 맞는 경우

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

 

이 선언문에 의해 ar배열은 다음과 같이 초기화될 것이다.

이렇게 초기값을 죽 나열해 버리면 어디서부터 어디까지가 한 행인지 구별이 잘 안되므로 행별로 초기값을 따로 묶어주는 것이 더 좋다.

 

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

 

2차 배열을 초기화하는 가장 좋은 방법은 다음과 같은 형식을 따르는 것이다.

 

int ar[2][3]={

     {1,2,3},

     {4,5,6},

};

소스가 조금 길어지기는 하겠지만 얼마나 보기 좋고 깔끔한가? 보기에 좋으면 유지, 보수할 때도 시간을 많이 절약할 수 있다. 

 

2) 초기값이 모자랄 때

특정행의 나머지 요소를 모두 0으로 초기화할 때는 0을 일일이 밝히지 않아도 된다. 만약 첫 행의 두 번째 이후 요소가 전부 0이라면 다음과 같이 초기화한다.

 

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

 

다음 두 선언문도 동일하다.

 

int ar[2][3]={{1,0,0},{4,5,6}};

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

 

3) 초기값 개수가 남는 경우

1차원 배열에서와 마찬가지로 초기값이 남으면 too many initializer에러가 된다. 다음 두 선언문 모두 에러이다.

 

int ar[2][3]={{1,2,3},{4,5,6,7}};

int ar[2][3]={{1,2},{4,5,6,7}};

 

4) 배열의 크기를 생략하는 방법

2차원 배열도 1차원 배열과 마찬가지로 배열의 크기를 생략할 수 있되 1차 첨자의 크기만 생략 가능하며 나머지 첨자는 반드시 밝혀야 한다.

 

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

 

9-3. 배열의 활용

9-3-가,나 생략

9-3-다. 작업 결과 저장

 

ex) 영문 소문자로 구성된 긴 문장을 입력받아 이 문자열 내의 각 알파벳 문자 개수를 구해 출력하라.

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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define BUFFER_SIZE 1024
int *CountAlpha(char *s,int s_len){
        int i;
        static int a[26= {0,};
        for(i=0;i<s_len;i++){
                if((s[i]-'a')>26 || (s[i]-'a' <0)) continue;
                a[s[i]-'a']++;
        }
        return a;
}
void PrintArr(int *a){
        int i;
        for(i=0;i<26;i++){
                if(a[i] == 0continue;
                printf("%c : %d\n",'a'+i,a[i]);
        }
}
void main(){
        char s[BUFFER_SIZE];
        scanf("%s",s);
        int i;
        int* a;
        a = CountAlpha(s,strlen(s));
        PrintArr(a);
}
 
cs

실행결과

 

알파벳 소문자가 아닌 문자들은 무시하도록 함.