[CS] TIL 8. 배열(2), 문자열과 배열


  1. C로 “hello, world”를 출력하는 프로그램을 만들 수 있다.
  2. C로 문자열 형식을 가진 변수를 선언하고 출력하는 프로그램을 만들 수 있다.


boostcourse모두를 위한 컴퓨터 과학 (CS50 2019) 강의를 듣고 정리한 필기입니다. 😀


1. 배열(2)

1.1 전역 변수

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    // Scores
    int scores[3];
    scores[0] = 72;
    scores[1] = 73;
    scores[2] = 33;

    // Print average
    printf("Average: %i\n", (scores[0] + scores[1] + scores[2]) / 3);
}

큰 문제는 아니지만, 여전히 복사 붙여넣기를 하고 있다. 적어도 두 군데서 같은 값을 반복하고 있다. int scores[3]printf("Average: %i\n", (scores[0] + scores[1] + scores[2]) / 3)의 3이다. 아주 사소해 보이지만 이것들은 수많은 버그의 원인이 된다. 지금 당장이야 두 곳의 3이 같아야 한다는 것을 알지만, 내일 아침이나 다음 주, 다음 달, 혹은 내년에 이 코드를 다시 본다면, 두 곳이 같아야 한다는 사실을 알 수 있을까? 아닐 것이다. 우리가 코드를 작성할 당시에 3으로 한 이유는 그 당시의 상황에서 결정한 것이지, 코드상의 조건이 아니기 때문이다.

#include <cs50.h>
#include <stdio.h>

const int N = 3;

int main(void)
{
    // 점수 배열 선언 및 값 저장
    int scores[N];
    scores[0] = 72;
    scores[1] = 73;
    scores[2] = 33;

    // 평균 점수 출력
    printf("Average: %i\n", (scores[0] + scores[1] + scores[2]) / N);
}

그렇다면 이것을 위해서 ¹N이라는 변수를 새로 선언해 ²scores라는 배열 크기와 printf()에 적힐 숫자를 대체할 수 있다.

const int N = 3;은 변하지 않는 값인 상수라고 한다. (변수를 대문자로 적는 것과 프로그램 맨 위에 그냥 관습이다.)실제로 C언어를 포함한 프로그래밍 언어에서는 어떤 변수의 특정값을 바꾸고 싶지 않거나 실수로 바뀌는 일을 방지하고 싶을 때 사용하는 방법이 있다. const로 해당 변수를 상수로 지정하면 clang 컴파일러는 우리나 타인이 실수로 그 값을 바꾸지 않도록 한다. 이를 전역변수라고 하는데, 함수 바깥에서 선언한 말이다. 이렇게 하면 N을 어디에 사용하든 언제나 같은 값을 가지고, 나중에 프로그램을 열어도 `scores`라는 배열 크기와 `printf()`에 적힐 숫자 같아야 하는 것을 알게 된다. 그리고 우리의 과제 수가 4개나 5개로 바뀌었다면 코드를 컴파일링하기 전에 값을 수정하기도 편리하다.

우리는 이 프로그램이 동적으로 돌아가게끔 개선할 수 있다.

1.2 배열의 동적 선언 및 저장

이제 우리가 어제에 해결하지 못한 문제를 차근차근 해결해보고자 한다.

  1. 평균을 구해야 하는 점수의 개수가 더 많아질 때마다 이 프로그램을 수정해야 할 것이다.
  2. 평균이 소수점으로 떨어질 경우 부정확함의 문제도 있다.
  3. 사실상 위 코드는 복사 붙여넣기 한 것에 가깝다는 것이다. 같은 일을 계속 반복하는 것은 디자인 개선의 여지가 있다는 뜻이기도 하다.
  4. 그리고 printf()에 해당하는 줄이 너무 길다.
#include <stdio.h>

int main(void)
{
    int n = get_int("Number of scores: ");
    
    int scores[n];
    
    for (int i = 0; i < n; i++)
    {
        scores[i] = get_int("Score[%i]: ", i+1)
    }    
    
    printf("Average: %i\n", (scores[0] + scores[1] + scores[2]) / n);
}

일차적으로는 get_int()를 이용해 사용자에게 점수의 개수를 받아옵니다. 그리고 이 개수를 scores라는 배열의 크기로 설정하고 for 문을 이용해서 사용자에게 i번째 점수를 묻고 저장하게 될 것이다. for 문의 i는 0부터 시작해 n 직전까지 세기 때문에 사용자가 요청한 개수만큼 점수를 입력할 수 있게 된다. 이제 평균을 구해야 하는 점수의 개수가 더 많아질 때마다 이 프로그램을 수정해야 한다는 것을 해결한 것이다.

for문 안에 있는 Score[%i]에 대응되는 숫자가 i+1인 이유는 i는 0부터 세는데, 사람들 중 0부터 숫자를 세는 사람은 드물기 때문이다.

이제 딱 하나가 남았다. 어떻게 하면 평균을 동적은 방식으로 계산해서 각각의 점수를 일일이 입력하지 않도록 할 수 있을까?

#include <cs50.h>
#include <stdio.h>

float average(int length, int array[]);

int main(void)
{
    // 사용자로부터 점수의 갯수 입력
    int n = get_int("Scores:  ");

    // 점수 배열 선언 및 사용자로부터 값 입력
    int scores[n];
    for (int i = 0; i < n; i++)
    {
        scores[i] = get_int("Score %i: ", i + 1);
    }

    // 평균 출력
    printf("Average: %.1f\n", average(n, scores));
}

//평균을 계산하는 함수
float average(int length, int array[])
{
    int sum = 0;
    for (int i = 0; i < length; i++)
    {
        sum += array[i];
    }
    return (float) sum / (float) length;
}

어떻게 하면 평균을 동적은 방식으로 계산해서 각각의 점수를 일일이 입력하지 않도록 할 수 있을까?

이것을 해결하기 위해서는 도우미 함수를 사용해야 한다. 진행하기에 앞서 평균은 소수점이 될 수도 있다. 그 때문에 자료형을 double이나 float 둘 다 가능하다.

그다음은 average라는 함수를 만들 것이다. 사용자가 입력한 값들을 평균을 내기 위해서는 두 가지를 알아야 한다. 먼저 점수가 들어가는 배열의 길이를 알아야 하고, 그 배열 자체도 받아야 할 것이다. 때문에 두 번째 매개변수를 array[]로 나타냈다. 배열의 크기는 아직 모르지만, 컴파일러가 알아서 계산하는 함수를 선언할 수는 있다. 너무 간단한 문제지만 어떤 값들을 받으면 평균은 어떻게 계산하나요? 값들은 리스트, 여기서는 배열이라고 한다. 그리고 그 배열의 길이를 안다면 평균을 계산할 때 사용할 수 있을 것이다.

우리는 배열의 길이와 점수들의 값을 사용자에게 받기 때문에 어려움은 없을 것이다. average()가 매개변수로 받는 lengtharray[]는 각각 배열의 길이와 배열을 입력 받는다. 함수 안에서는 배열의 길이만큼 루프를 돌면서 값의 합을 구하고 최종적으로 평균값을 반환한다.



2. 문자열과 배열

어제와 오늘, 배열을 활용해서 코드 디자인을 개선했고, 여러 값을 하나의 변수에 저장해서 복사 붙여넣기 문제를 방지할 수 있었다. 이는 메모리 안에서 일어나는 일과도 크게 다르지 않다.

#include <stdio.h>

int main(void)
{
    int score1 = 72;
    int score2 = 73;
    int score3 = 33;
}

세 개의 변수가 있으면 각각 4바이트를 차지한다. [CS] TIL7의 2. 배열에 나온 자료형 각각의 바이트 크기를 떠올려보면 char은 1바이트이고, int는 4바이트였다.

title

즉, scoore1이라는 변수에 저장된 72는 RAM에서 4칸을 차지할 것이다. 각 칸은 1바이트를 나타내기 때문이다.

title

score2와 score3 역시 비슷한 식으로 저장될 것이다. 물론 실제로는 비트로 저장되겠지만 그 단계까지는 고려하지 않아도 된다. 그냥 실제는 비트로 저장된다라는 것만 기억하면 된다.

#include <stdio.h>

int main(void)
{
    int scores[3];
    scores[0] = 72;
    scores[1] = 73;
    scores[2] = 33;
}

이제 scores라는 배열을 생성해서 위처럼 세 개의 값을 저장할 수 있다.

title

이제 RAM에 저장된 각 칸의 이름이 달라졌다. score1scores[0]과 같이 말이다. 여기서 배열 내 원소의 순서는 바이트 수와 무관하다. int가 4바이트를 차지한다고 해서 코드를 작성하는 사람이 0번째, 4번째, 8번째와 같이 셀 필요는 없다. 왜냐하면 컴퓨터가 각 값의 자료형에 따라 알아서 필요한 공간을 계산하기 때문이다.

title

이는 문자열과도 연관이 매우 깊다. H, I, !는 각각 c1, c2, c3에 저장되어있다. 심지어 우리가 여태껏 사용한 문자열(string) 자료형의 데이터는 사실 문자(char) 자료형 데이터들의 배열이었다. string s = “HI!”; 과같이 문자열 s가 정의되어 있다고 생각해보자. s는 문자의 배열이기 때문에 메모리상에 위 그림과 같이 저장되고, 인덱스로 각 문자에 접근할 수 있다. 여기서 가장 끝의 ‘\0’은 문자열의 끝을 나타내는 널 종단 문자이다. 단순히 모든 비트가 0인 1바이트를 의미한다.

string names[4];

names[0] = "EMMA";
names[1] = "RODRIGO";
names[2] = "BRIAN";
names[3] = "DAVID";

printf("%s\n", names[0]);
printf("%c%c%c%c\n", names[0][0], names[0][1], names[0][2], names[0][3]);

그럼 위 코드와 같이 여러 문자열이 동시에 선언된 경우를 살펴보겠다. names라는 문자열 형식의 배열에 네 개의 이름이 저장되어있다. 첫 번째 printf()에서는 names의 첫 번째 인덱스의 값, 즉 “EMMA”를 출력한다. 두 번째 printf()에서는 형식 지정자가 %s가 아닌 %c로 설정되어 있음을 확인할 수 있다.

title

따라서 출력하는 것은 문자열이 아닌 문자이다. stringchar의 배열이다. 그런데 위 코드에서는 string 자료형을 배열로 선언했다. 배열(2)까지 보던 1차원 배열이 아니라 2차원 배열이라는 것이다. 여기서는 각 이름의 두 번째 문자를 출력하고자 한다면, names[0][1]과 같은 모양으로 출력할 수 있다. 다시 말해 names[0][1]names의 첫 번째 값, 즉 “EMMA”라는 문자열에서, 그 두 번째 값, 즉 ‘M’이라는 문자를 의미한다.




© 2020. by RIVER

Powered by RIVER