0. 배열을 쓰는 이유
- 앞에서 배웠듯이 변수를 선언하면 그 자료형의 크기만큼 메모리에 할당된다.
- 이 때 메모리의 할당은 연속적으로 할당되지 않고 제각각 할당된다.
- 만약 비슷한 자료에 점화식을 적용하거나 접근하고 싶을 경우 각각 변수를 선언했다면, 선언한 만큼 일일이 적용해줘야한다.
- 이런 경우 비슷한 자료를 연속적으로 모아서 관리하고 사용할 수 있다.
- 이렇게 연속적으로 비슷한 자료를 모은 것을 배열(array)라고 한다.
1. 배열 사용법
1-1. 배열 선언 및 초기화
- 추후 동적할당 챕터가 따로 있어서 malloc을 이용한 동적할당은 나중에 설명합니다.
- 배열의 선언에는 자료형, 배열의 이름, 배열의 요소 갯수가 필요하다.
- "자료형" "배열의 이름""[요소의 갯수]" 로 주로 표현된다.
- 요소의 개수를 정적으로 지정할 때는 숫자 리터럴이 들어간다.
- 이 책에서는 3이나 4.5 등을 상수라 표현하고 const를 상수처럼 쓸 수 있는 변수라고 표현하고 있는데,
- 주로 const를 사용하면 상수라고 하며 위에서 예약어나 3, 4.5, "abcd" 이렇게 변할 수 없는 데이터는 리터럴이라고 하는 것이 더 보편적인 것 같다. (자료들을 찾아 봤을 때)
int arr[5] //여기서 int가 자료형, arr이 우리가 사용할 배열의 이름
//[5]가 arr의 총 사이즈를 의미한다.
- 배열도 변수와 마찬가지로 선언만 하고 값을 할당하지 않으면 쓰레기값들이 들어있다.
- 그래서 선언과 동시에 초기화하거나 각 요소들을 순회하면서 값을 넣어줘야 한다.
- 이 챕터에서는 선언과 동시에 초기화하는 방법을 소개한다.
- 초기화는 {}를 이용해서 한다.
(과거 필자는 자바스크립트의 []와 헷갈려서 부끄러움을 당한적이 있다.) - 가장 대표적인 방법으로는 중괄호 안에 ,로 구분하여 각각의 요소에 값을 넣어주는 것이다.
- 아래의 경우 arr의 첫번째 인덱스 (0에 해당합니다. 1-2에서 설명)부터 마지막 인덱스(4에 해당합니다.)에 순서대로 1~5가 들어간다.
int arr[5] = {1, 2, 3, 4, 5};
- 만약 배열의 사이즈보다 적게 할당한다면 나머지는 0이 들어간다.
int arr[5] = {1, 2, 3}; // 이 경우 arr[3]과 arr[4]에는 0이 들어간다.
- 이를 이용한 스킬로 {0}을 사용하면 모든 요소를 0으로 초기화할 수 있다. (사이즈 보다 작게 할당하면 전부 0으로 할당하므로)
int arr[5] = {0} // 이 경우에는 arr의 모든 요소가 0으로 초기화된다.
- 또한 []안이 생략된 초기화도 있다.
- 이 부분은 조금 신기했는데, 동적할당이 아닌 정적할당이라고 한다.
- 아마 컴파일러가 [] 내부가 생략되어 있다면 {}의 요소 갯수를 세서 컴파일 당시에 사이즈를 정의하는 것 같다. (필자 추측)
int arr[] = {1, 2, 3}; // arr의 사이즈가 3이 되고 각각의 요소가 1, 2, 3으로 초기화된다.
1-2. 배열 요소 접근 및 반복
- 배열의 선언과 사용은 조금 다르다.
- int arr[5] 라고 한다면 5만큼의 정수 자료형 크기를 가진 배열을 의미하고,
- arr[5]라고 하면 arr의 인덱스 5의 요소를 의미한다.
- 배열의 인덱스는 0부터 시작한다. 즉 크기를 5만큼 선언했다면 배열의 인덱스는 0, 1, 2, 3, 4가 된다.
- for문이나 while 문을 쓸 때 이를 유의해야 한다.
- 그냥 하는 소리가 아니라 간단한 예시를 보면 쉬워 보이지만 프로그램의 규모가 커지고 함수로 배열을 이리저리 주고 받다보면 seg fault (접근하면 안되는 메모리에 접근하면 생기는 오류)가 생기는데, 규모가 클수록 찾아내기도 어렵고 진짜 사람 생명력 빨아 먹는 부분이고, 참 아쉬운게 컴파일은 너무 잘돼서 코드 계속 들여다 보면서 찾아야 하므로 신경쓰는 연습이 많이 필요하다! 진짜로! (필자의 눈물로 쓰인 아주 개인적인 의견입니다.)
- 설명은 예시 코드와 함께 보자면
#include <stdio.h>
int main(void)
{
int arr[5]; // 길이 5짜리 정수 배열을 선언했다.
int total = 0; // 총 점수를 저장할 total 변수를 선언했다.
double avg; // 평균을 출력하기 위한 실수 변수인 avg를 선언했다.
for (int i = 0; i < 5; i++) // 0 ~ 4 까지 (5 미만 까지 동작하므로) arr의 i 번째 인덱스에 입력을 받는다.
scanf("%d", &arr[i]);
for (int i = 0; i < 5; i++)
{
total += arr[i]; // 배열의 요소를 돌면서 total에 저장한다.
printf("%5d", arr[i]);
}
printf("\n");
avg = total / 5.; 평균을 5.로 나눠서 평균을 저장한다
printf("평균 : %.1lf\n", avg);
return (0);
}
- 배열의 요소에 접근해서 값을 가져오거나 값을 넣을 때는 arr[i] 처럼 "배열의 이름""[접근할 인덱스]" 로 접근해서 사용한다.
1-3. VLA (가변길이 배열)
- 이 책에는 나와있지 않은데 C99에서 지원하는 가변길이 배열이라는 것이 있다.
- 컴파일시에 배열의 크기가 정해지지 않고 런타임에 배열의 크기를 정하는 방식이다.
#include <stdio.h>
int main(void)
{
int n;
scanf("%d", &n);
int arr[n]; // arr의 길이가 사용자가 입력한 값에 따라서 달라진다.
// 즉 소스코드를 컴파일 할 때 컴파일러는 이 배열의 크기를 알 수 없고
// 런타임에서 사용자의 입력을 받아야지만 알 수 있다.
}
- 그래서 당연히 힙에 생기는 동적할당 일 줄 알았는데, 스택 영역에 생긴다.
- 그래서 얻는 장점으로는 메모리 관리를 따로 해주지 않아도 된다.
- 그렇지만 그 말을 반대로 하면 우리가 런타임때 제한없이 수를 넣어버릴 수 있기 때문에 스택이 터질 위험이 있다. (스택 오버플로우)
- 그래서 C11 부터는 C99와 달리 VLA지원이 필수가 아니다.
- 마찬가지로 C++에서도 특정 환경에서는 VLA를 사용할 수도 있지만 이는 환경에 따라 다를 수 있고, C++에서는 new를 사용한 동적할당을 권장하기 때문에 이런게 있다 정도로만 알고 넘어가도 될 듯하다.
- 알고리즘 풀 때 사용하면 은근 쏠쏠하다.
2. C에서의 문자열
- C에서는 파이썬 같은 언어와 달리 "문자열"이라는 자료형이 따로 존재하지 않는다.
- C에서는 문자들의 배열로써 문자열을 표현한다. (혹은 char 형 포인터)
- 물론 char 형 포인터와 중요한 몇가지가 다르지만 추후 챕터에서 설명할 기회가 있을 것이다.
2-1. char형 배열 선언 및 초기화
- 위에서 사용한 배열의 선언 및 초기화와 동일하다.
- 다른점은 배열의 자료형이 char라는 점이다.
- char str[80] = "applejam"; 라는 코드가 있는데,
- 이는 char str[80] = {a, p, p, l, e, j, a, m}; 과 동일하게 동작한다.
- scanf로 다시 입력을 받을 수 있다.
#include <stdio.h>
int main(void)
{
char str[80] = "applejam"; //총 사이즈가 80인 char 배열 str을 선언하고 "applejam"을 초기화했다.
printf("init line : %s\n", str);
printf("input new line : ");
scanf("%s", str);
printf("new line : %s\n", str);
return (0);
}
2-2. 주의 사항
- 여기서 부터는 책의 내용과 반하는 내용이 다른 챕터에 비해 좀 더 들어있을 예정이다.
- 이 책에서는 char 형 배열을 선언할 때 넉넉하게 선언하라는게 1번 주의사항이지만, 사실 임의로 큰 사이즈를 만들어서 쓰는 건 비효율적이기도 하고, 제한이 너무 많다고 생각한다.
- 실제로는 임의의 버퍼사이즈를 정하고 널문자를 만날때까지 동적할당을 통해서 읽어오면서 strjoin을 사용하거나 비슷하게 구현해서 사용하는 경우가 더 유용하다고 생각한다.
- 배열을 정적으로 선언할 때나 동적으로 할당할 때나 최소 문자열의 길이보다 1만큼은 더 크게 선언해야한다.
- C에서 문자열을 핸들링할 때 문자열의 끝을 판단하는 요소로 널문자 (아스키 코드 0, 표기상으로는 \0)을 사용하는데,
- 만약 apple이라는 문자열을 만들때는 {a, p, p, l, e, 0} 와 같이 할당이 되어 있어야 이 문자열을 정상적으로 처리할 수 있다.
- 왠만하면 쓰지 말라는 함수들은 쓰지 말자 (심지어 백악관에서는 c나 c++ 자체를 사용하지 말라는 권고까지 했는데)
- 이 부분은 온전히 개인적인 생각이 담겨 있는데, 이 책에서 gets를 사용해서 공백이 포함된 문자열을 입력받거나
- strcpy를 통해서 문자열을 복사하는 예제들이 있다.
- 심지어 secure no warnings를 이용해서 컴파일 하면 된다고 나와있는데, 내 vscode에서도 gets를 쓰면 터미널에서 경고메세지가 주르륵 나오는 걸 볼 수 있다.
- 책에서도 써있듯이 gets 함수도 그렇고 string.h에 있는 string 관련 함수들 중에는 배열의 크기를 검사하지 않는 함수들이 많아서 정의되지 않은 행동이나 보안에 위험할 수 있다.
- 그래서 strncpy나 strcat도 strlcat 같이 배열의 크기를 검사하는 보완된 함수들이 있으니 왠만하면 이 부분을 공부해서 사용하는 게 맞다고 생각한다.
- 이 책에서도 초심자를 위한 C이긴 하지만 C의 보안위험도 같이 알려주셨으니 동시에 좀 더 개선된 string 관련 함수들로 예제를 만드셨으면 어땠을까 한다.
- 그래서 이 챕터에서는 strcpy나 gets 예제를 제외한다. (마지막 도전문제를 제외하고)
3. 도전 실전 예제
- 대소문자 변환 예제 (바뀐 알파벳 갯수 출력)
#include <stdio.h>
int main(void)
{
char str[100];
char str2[100];
int cnt = 0;
printf("문장 입력 : ");
gets(str); // 이 부분에서 99보다 큰 길이의 문자열을 입력하면 정의되지 않은 결과가 발생한다.
int i = 0;
while (str[i])
{
if (str[i] >= 'A' && str[i] <= 'Z')
{
str2[i] = str[i] - 'A' + 'a';
cnt++;
}
else
str2[i] = str[i];
i++;
}
str2[i] = 0;
printf("바뀐 문장 : %s\n", str2);
printf("바뀐 문자 수 : %d\n", cnt);
}
'Language > C' 카테고리의 다른 글
혼공C (10장) - 배열과 포인터 : 배열을 인자로 (0) | 2024.04.02 |
---|---|
혼공C (9장) - 포인터, 상수 포인터와 포인터 상수 (0) | 2024.03.27 |
혼공C (7장) - C 함수 기초, 혼공C 7장 실전예제 (0) | 2024.03.22 |
혼공C (6장) - 반복문 (while, do while, for) (0) | 2024.03.20 |
혼공C (5장) - 선택문, 조건문 (if, else if, else, switch, case) (0) | 2024.03.20 |