1. 배열
배열(array)은 여러 개의 변수들을 모아놓은 것이다.
단순히 여러 개의 변수를 선언한 것과는 차이가 있다.
예를 들어 다음과 같이 변수를 선언했다고 하자.
1
2
3
4
5
|
int num1;
int num2;
int num3;
int num4;
int num5;
|
cs |
이 때 메모리의 상태는 다음과 같다.
하지만 배열로 선언할 경우 다음과 같다.
배열은 말 그대로 변수들을 나열한 것, 즉 변수들의 배열이다.
그냥 변수를 여러 개 선언한 것과는 메모리에 할당되는 형태가 다르다.
따라서 선언하는 방법도, 접근하는 방법도 다르다.
배열은 다음과 같이 선언한다.
int arr[5];
이 선언문은 다음과 같이 구성된다.
int: 배열을 이루는 요소(변수)들의 자료형
arr: 배열의 이름
[5]: 배열의 길이
그러니까 이 선언문은 다음을 의미한다.
int형 변수 5개로 이루어진 arr이라는 배열을 선언한다
배열의 핵심은 변수들이 나열되어있다는 것, 즉 나란히 붙어있다는 것이다(앞의 그림 참조).
배열은 다양한 자료형으로 선언할 수 있다.
char arr1[15]; //char형 변수 15개의 나열
double arr2[6]; //double형 변수 6개의 나열
배열의 길이에는 무조건 상수만 가능하다.
...고 생각하는 게 좋다.
사실 C 표준이 바뀌면서 변수도 허용됐지만, 이를 허용하지 않는 컴파일러가 다수 있기 때문에
가급적 상수로 쓰는 게 좋다.
배열은 다음의 장점을 갖는다.
① 여러 개의 변수 선언이 쉽다.
만약에 int형 변수 50개가 필요하다고 하자.
어떻게 할 것인가?
변수 하나씩 선언하려면 벌써부터 손가락이 저려온다.
하지만 배열은 한 줄로 끝난다.
int arr[50];
② 인덱스로 접근할 수 있다.
인덱스(index, 색인)는 배열의 위치 정보를 말한다.
다음과 같이 배열이 선언되었다고 하자.
int arr[3];
이 배열은 메모리에 다음과 같은 형태로 할당된다.
배열의 각 요소에는 인덱스가 매겨진다.
차례대로 번호가 매겨진다는 이야기다.
단, 0부터 시작한다. (0, 1, 2, …)
따라서 다음과 같이 나타낼 수 있다.
배열의 요소에 값을 저장하는 방법은 다음과 같다.
arr[0] = 1;
arr[1] = 3;
arr[2] = 4;
위 코드를 실행한 후의 메모리 상태는 다음과 같다.
이처럼 []연산자의 중간에 위치 정보를 명시하는데,
앞에서 언급했듯이 이 위치 정보를 인덱스라 하며, 1이 아닌 0부터 시작한다.
따라서 arr[idx] = 3; 은 arr[idx + 1]에 3 을 대입하라는 의미다.
③ 인덱스로 접근하기 때문에 반복문을 통해 순차적 접근이 가능하다.
다음 코드를 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include <stdio.h>
int main()
{
int arr[4];
int sum = 0;
int i;
arr[0] = 1; arr[1] = 2; arr[2] = 3; arr[3] = 4;
for (i = 0; i < 4; i++)
sum += arr[i];
printf("sum: %d\n", sum);
return 0;
}
|
cs |
위 예제에서는 for 문을 통해 간단하게 배열 arr 의 요소에 차례대로 접근하고 있다.
다음과 같이 4개의 변수가 선언된다면 어떨까?
int num1, num2, num3, num4;
4개밖에 안 되니 일일이 작성하면 가능하긴 하다. 하지만 요소가 50개라면? 요소가 100개라면?
가능하긴 하지만 굉장히 번거롭다.
게다가 변수 하나씩 선언할 경우 반복문 기반의 접근은 불가능하다.
2. 배열의 초기화
배열도 초기화할 수 있다. 다만 변수와 달리 그 방법이 여러 가지다.
① int arr[5] = { 1, 2, 3, 4, 5 };
② int arr[] = { 1, 2, 3, 4, 5 };
③ int arr[5] = { 1, 2 };
중괄호로 묶인 부분을 초기화 리스트라고 한다. 설마 지역으로 오해하진 않으리라 믿는다.
초기화 리스트에는 하나만 있을 수도 있고, 끝에 콤마가 없어도 된다.
그러니까 ③을 다음과 같이 써도 결과는 같다.
int arr[5] = { 1, 2 };
우선 ①과 ②은 사실상 같은 선언이라고 볼 수 있다.
컴파일러가 초기화 리스트를 토대로 배열의 길이 정보를 알아서 채워주기 때문이다.
유심히 볼 것은 ③이다.
배열도 변수와 마찬가지로 초기화하지 않으면 각 요소에는 쓰레기값이 들어있다.
그런데 여기서는 2개의 요소( arr[0], arr[1])만 초기화하고 있다.
그럼 나머지 3개의 요소에는 쓰레기값이 들어있을 거라 추측할 수 있다.
하지만 실제로는 0이 들어있다.
정리하면 다음과 같다.
배열의 일부 요소만 초기화하면 나머지 요소는 자동으로 0으로 초기화된다.
여기서 주의할 점은 대입과 초기화의 차이다.
Q. 다음 코드에서 arr[3]의 값은?
int arr[4] = ;
A. 0. arr[0]만 초기화되었으므로 나머지 요소는 0으로 초기화된다.
Q. 다음 코드에서 arr[3]의 값은?
int arr[4] = ;
arr[0] = 3;
이번에도 0이 들어있을까?
결론부터 말하면, 아니다.
앞의 정리를 다시 보자.
배열의 일부 요소만 초기화하면 나머지 요소는 자동으로 0으로 초기화된다.
조건은 일부 요소만 초기화하면이다.
그렇다. 일부 요소에 대입해봤자 나머지 요소는 0이 아닌 쓰레기값이 들어있는 것이다.
이미 언급한 내용이지만, 다시 한 번 짚고 넘어가겠다.
대입: 변수에 값을 넣는 것
초기화: 변수에 선언과 동시에 값을 대입하는 것.
그러니까 다음은 대입이고,
arr[2] = 3;
num = 4;
다음은 초기화다.
int arr[3] = ;
int num = 3;
사실 변수에서는 초기화와 대입의 차이가 크게 중요하지 않을 수도 있다.
하지만 배열에서는 아니다. 배열에서는 초기화냐 대입이냐에 따라 에러가 발생할 수도 있다.
또 한가지 주의할 점은 할당되지 않은 영역에 침범하지 않는 것이다.
예를 들어 다음과 같이 배열을 선언했다고 하자.
int arr[4] = { 1, 2, 3, 4 };
다음 코드의 문제점은 무엇일까?
arr[4] = 5;
배열 길이가 4이므로 인덱스는 0~3인데, 위 코드에서는 인덱스를 4로 해서 접근하고 있다.
그런데 컴파일러는 배열 접근의 유효성을 검사하지 않는다.
때문에 위 코드는 에러가 발생하지 않는다. 하지만 오히려 그래서 더 위험한 것이고,
더 주의해야 하는 것이다.
이런 경우 전혀 예상치 못 한 결과가 나올 수 있기 때문이다.
3. 문자열
C언어에서 문자열은 문자의 배열, 그러니까 char형 배열의 형태로 저장 및 변경할 수 있다.
참고로 문자 하나는 문자(character), 둘 이상은 문자열(string)이라 한다.
C언어에서 문자열은 다음과 같이 큰 따옴표로 감싸서 나타낸다.
char str[16] = "Happy Birthday!";
물론 배열의 길이를 생략하고 선언할 수도 있다.
이 경우 컴파일러가 문자열의 길이를 계산해 길이 정보(16)을 삽입해준다.
char str[] = "Happy Birthday!";
그리고 메모리에는 다음과 같이 저장된다.
여기서 주목할 점은 문자열 끝의 \0 과 길이가 16 이라는 점.
\0 은 널 문자(null character)를 나타내는 특수 문자(escape sequence)다.
C언어에서는 무조건 문자열 끝에 자동으로 널 문자가 삽입된다.
따라서 문자열을 선언할 때도 널 문자까지 고려해 길이를 1 크게 선언해야 한다.
널 문자는 아스키 코드로 0이고, 이는 다음 코드를 통해 알 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
|
#include <stdio.h>
int main()
{
char str[] = "Happy Birthday!";
printf("str의 길이: %d\n", sizeof(str));
printf("str[15]: %c\n", str[15]);
printf("str[15]: %d\n", str[15]);
return 0;
}
|
cs |
실행 결과에서 다음의 세 가지를 알 수 있다.
① 널 문자를 문자로 출력하면 아무것도 출력되지 않는다.
② 널 문자를 정수로 출력하면 0이 출력된다. 따라서 널 문자의 아스키 코드 값은 0이다.
참고로 char형은 1바이트이므로 sizeof(str)은 문자열의 길이를 나타낸다.
여기서 널 문자와 공백을 헷갈리지 않도록 주의해야 한다.
둘 다 %c로 출력할 경우 아무것도 출력되지 않으므로 헷갈릴 수 있지만,
공백 문자의 아스키 코드 값은 32다.
사실 엄밀히 따지면 널 문자는 공백 문자가 출력되지만, 눈에 보이지 않으니 헷갈리기 쉽다.
printf 함수로 출력할 때 %s를 사용한 것처럼, scanf 함수로 입력받을 때도 %s를 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#include <stdio.h>
int main()
{
char str[50];
int idx = 0;
printf("문자열 입력: ");
scanf("%s", str);
printf("문자열로 출력: %s\n", str);
printf("문자 하나씩 출력: ");
while (str[idx] != '\0')
{
printf("%c", str[idx]);
++idx;
}
printf("\n");
return 0;
}
|
cs |
14행부터 18행은 널 문자가 나올 때까지 문자 하나씩 출력하는 반복문이다.
그리고 실제로 문자열로 출력할 때와 동일한 결과가 나타났다.
여기서 다음을 알 수 있다.
scanf 함수로 입력받은 문자열의 끝에도 널 문자가 삽입된다.
이처럼 C언어에서 표현하는 모든 문자열의 끝에는 널 문자가 삽입된다.
그런데 널 문자가 문자열의 끝이 아니라 중간에 있으면 어떻게 될까?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#include <stdio.h>
int main()
{
char str[50] = "Apple is delicious";
printf("str: %s\n", str);
str[8] = 0; //널 문자의 아스키 코드 값은 0
printf("str: %s\n", str);
str[6] = '\0';
printf("str: %s\n", str);
return 0;
}
|
cs |
우리 눈에는 문자로 보이지만 메모리 상에는 이진 데이터로 저장된다.
따라서 문자열의 끝을 표시할 수단이 필요한데, 그 수단이 널 문자다.
위 예제에서는 널 문자를 삽입함으로써 새로운 문자열의 끝을 명시했고,
그에 맞춰 문자열이 출력된 것이다.
4. 문자열과 문자 배열
문자열의 끝에는 무조건 널 문자가 삽입된다.
반대로 말하면, 널 문자가 없으면 문자열이 아니라는 것이다.
예를 들어 다음과 같이 선언된 배열은 문자열이다.
char str[] = "Happy";
그렇다면 다음은 문자열일까?
char str[] = { 'H', 'a', 'p', 'p', 'y', '\0' };
큰 따옴표로 감싸 문자열로 초기화하는 것이 아니라,
작은 따옴표로 감싼 문자 요소들로 작성한 초기화 리스트를 통해 초기화하고 있다.
하지만 문자열이다.
다음은 문자열일까?
char str[] = { 'H', 'a', 'p', 'p', 'y' };
마찬가지로 작은 따옴표로 감싼 문자 요소들로 작성한 초기화 리스트를 통해 초기화하고 있다.
하지만 문자열이 아니다.
차이가 보이는가?
그렇다. 끝에 널 문자의 유무에 따라 문자열인지, 아닌지가 나뉜다.
따라서 후자는 문자열이 아니라 char형 배열, 그러니까 문자 배열이다.
5. scanf 함수로 문자열을 입력받을 때 주의할 점
① & 연산자를 붙이지 않는다.
그러니까 다음과 같이 작성하면 안 된다는 것이다.
char str[50];
scanf("%s", &str);
② 공백을 포함하지 않는다.
scanf 함수는 입력의 구분을 공백과 개행(Enter)으로 한다.
그러니까 I am Groot를 입력하면 "I", "am", "Groot"로 인식한다는 것이다.
따라서 I am Groot를 입력하려면 scanf 함수를 세 번 호출해야 한다.
'Programming > C' 카테고리의 다른 글
포인터와 배열의 관계 (0) | 2019.03.17 |
---|---|
포인터 (0) | 2019.03.16 |
변수 (0) | 2019.03.07 |
함수 (0) | 2019.03.06 |
조건 분기 (0) | 2019.03.04 |