[CS50] 배열

업데이트:




컴파일링

#include <stdio.h>

int main(void){
    printf("hello, world\n");
}


  • main 함수는 프로그램의 시작점으로써 실행 버튼을 클릭하는 것과 같다.
  • printf는 출력을 담당하는 함수이며 사용하기 위해서는 stdio.h 라이브러리가 필요하다.
  • stdio.h는 헤더 파일로 C 언어로 작성되어 있으며 파일명이 .h로 끝나는 파일이다.
    • 이 파일에는 printf 함수의 프로토타입이 있어서 Clang 컴파일러가 프로그램을 컴파일할 때 printf가 무엇인지 알려주는 역할을 한다.
  • 코드를 clang hello.c로 컴파일하고 ./a.out 명령으로 프로그램을 실행할 때 이 과정은 0과 1로 이루어진 파일 a.out을 생성하여 실행 가능하게 한다.


  • a.out이 아닌 다른 파일명으로 컴파일하고 싶다면?
    clang -o hello hello.c
    
  • CS50 라이브러리를 사용한 프로그램을 컴파일 할 때는?
    clang -o hello hello.c -lcs50
    

이것은 clang에게 CS50 라이브러리에 있는 모든 0과 1들을 여기에 연결하라는 의미이다.
더 간단히는 make 프로그램을 이용하면 이 모든 컴파일 과정을 자동으로 처리할 수 있다.

make나 clang을 사용해서 프로그램을 실행할 때 아래 네 개의 단계를 거친다.

  • 전처리
  • 컴파일링
  • 어셈블링
  • 링킹



전처리(Precompile)

  • 컴파일의 전체 과정은 네 단계로 나누어볼 수 있다. 그 중 첫 번째 단계가 전처리이다.
  • 전처리는 전처리기에 의해 수행된다.
  • ’#’으로 시작되는 C 소스코드는 전처리기에 실질적인 컴파일이 이루어지기 전에 무언가를 실행하라 알려준다.
  • ex) #include는 전처리기에게 다른 파일의 내용을 포함시키라고 알려준다.


컴파일(Compile)

  • 전처리기가 전처리한 소스 코드를 생성하고 나면 그 다음 단계는 컴파일이다.
  • 컴파일러라고 불리는 프로그램은 C 코드를 어셈블리어라는 저수준 프로그래밍 언어로 컴파일한다.
  • 어셈블리는 C보다 연산의 종류가 훨씬 적지만, 여러 연산들이 함께 사용되면 C에서 할 수 있는 모든 것들을 수행할 수 있다.
  • C 코드를 어셈블리 코드로 변환시켜줌으로써 컴파일러는 컴퓨터가 이해할 수 있는 언어와 최대한 가까운 프로그램으로 만들어준다.
  • 컴파일이란 용어는 소스 코드에서 오브젝트 코드로 변환하는 전체 과정을 통틀어 일컫기도 하지만, 구체적으로 전처리한 소스 코드를 어셈블리 코드로 변환시키는 단계를 말하기도 한다.


어셈블(Assemble)

  • 어셈블 단계는 어셈블리 코드를 오브젝트 코드로 변환시킨다.
  • 컴퓨터의 중앙처리장치가 프로그램을 어떻게 수행해야 하는지 알 수 있는 명령어 형태인 연속된 0과 1들로 바꿔주는 작업
  • 이 변환작업은 어셈블러라는 프로그램이 수행한다. 소스 코드에서 오브젝트 코드로 컴파일 되어야 할 파일이 딱 한 개라면, 컴파일 작업은 여기서 끝이 난다. 그렇지 않은 경우는 다음 단계인 링크라 불리는 단계가 추가된다.


  • 만약 프로그램이 여러 개의 파일로 이루어져 있어 하나의 오브젝트 파일로 합쳐져야 한다면 링크라는 컴파일의 마지막 단계가 필요하다.
  • 링커는 여러 개의 다른 오브젝트 코드 파일을 실행 가능한 하나의 오브젝트 코드 파일로 합쳐준다.


이 네 단계를 거치면 최종적으로 실행 가능한 파일이 완성된다.





디버깅

버그와 디버깅

  • 버그(bug)는 코드에 들어있는 오류
  • 버그로 인해 프로그램의 실행에 실패하거나 프로그래머가 원하는 대로 동작하지 않게 된다.
  • 디버깅(debugging)은 코드에 있는 버그를 식별하고 고치는 과정이다.
  • 프로그래머는 디버거라고 불리는 프로그램을 사용하여 디버깅을 하게 된다.



디버깅의 기본

  • 디버거는 프로그램을 특정 행에서 멈출 수 있게 해주기 때문에 버그를 찾는데 도움이 된다.
  • 프로그래머는 멈춰진 그 지점에서 무슨 일이 일어나는지 볼 수 있다.
  • 프로그램이 멈추는 특정 지점을 중지점이라 한다.
  • 프로그러매가 프로그램을 한번에 한 행씩 실행할 수 있게 해준다.



help50

int main(void)
{
    printf("hello, world\n");
}
  • make 프로그램을 이용하여 컴파일해보면 “implicitly declaring library function ‘printf’“라는 에러 메세지가 나타난다.
  • 이런 메세지를 이해하기 힘들 때 help50 프로그램을 사용하면 좋다.
  • make 앞에 help50을 붙여 실행하면 다시 컴파일시 생기는 오류를 해석해준다.
    help50 make 파일이름
    



printf

#include <stdio.h>

int main(void)
{
    for (int i = 0; i <= 10; i++)
    {
        printf("#\n");
    }
}
  • 이 코드를 실행하면 에러는 발생하지 않지만 의도와는 다르게 ‘#’이 11개나 출력된다.
  • 디버깅의 다른 방법으로는 직접 의심이 가는 변수를 출력해서 확인해 볼 수 있다.


#include <stdio.h>

int main(void)
{
    for (int i = 0; i <= 10; i++)
    {
        printf("i is now %i: ", i);
        printf("#\n");
    }
}
  • i가 0에서 시작하기 때문에 for 루프의 i <= 10 이라는 조건은 실제로 11번 만족한다는 사실을 알 수 있다.
  • 따라서 i < 10으로 수정해주면 ‘#’이 10번 출력된다.



debug50

  • CS50 IDE를 사용하면 debug50이라는 프로그램을 사용할 수 있다.
  • 소스 코드에 직접 브레이크포인트를 저장하고 소스파일을 컴파일한 후에 dubug50 파일명으로 실행한다.
    debug50

  • 디버깅 종료를 위해 Ctrl + c를 누르면 된다.





코드의 디자인

check50

  • check50 프로그램을 이용하면 과제를 잘 수행했는지 자동으로 검사가 가능
  • 여러 사람들이 각자 한 부분을 맡아 코드를 작성할 때 각자 수정한 코드가 전체 프로그램의 정확성을 해치지 않는지 쉽게 확인할 수 있다.
  • 이 프로그램은 CS50 강의를 위해서만 작성되었다.



style50

  • style50 프로그램을 이용하면 코드가 심미적으로 잘 작성되어 있는지 검사 가능
  • 공백의 수나 줄바꿈과 같은 것들이 코드를 읽고 이해하는데 영향을 준다.
  • 많은 회사들이 사내에서 코드를 작성할 때 특정한 스타일 가이드를 따르도록 한다.
  • 여러 사람들이 코드를 작성하기 때문에 서로 불필요한 오해를 없애고, 코드를 이해하는 데 드는 비용을 최소화하기 때문이다.



고무 오리

  • 때로는 코드에 포함된 오류를 해결할 때 위에 있는 프로그램들이 존재하지 않거나, 있다 하더라도 디버깅에 큰 도움이 안 될 수 있다.
  • 이럴 때는 직접 곰곰히 생각해보는 수 밖에 없다.
  • 한 가지 유명한 방법으로는 고무 오리와 같이 무언가 대상이 되는 물체를 앞에 두고, 내가 작성한 코드를 한 줄 한 줄 말로 설명해주는 과정을 거쳐볼 수 있다.





배열(1)

메모리

  • C에는 여러 자료형이 있고, 각각의 자료형은 서로 다른 크기의 메모리를 차지한다.
    • bool: 불리언, 1바이트
    • char: 문자, 1바이트
    • int: 정수, 4바이트
    • float: 실수, 4바이트
    • long: (더 큰) 정수, 8바이트
    • double: (더 큰) 실수, 8바이트
    • string: 문자열, ?바이트


  • 컴퓨터 안에는 아래 사진과 같은 RAM이라고 하는 물리적 칩이 메모리 역할을 한다.
    RAM
  • 여러 개의 노란색 사각형이 메모리를 의미하고, 작은 사각형 하나가 1바이트를 의미한다.
  • 예를 들어 char 타입의 변수를 하나 생성하고, 그 값을 입력한다고 하면 위 사진에서 한 사각형 안에 그 변수의 값이 저장된다.



배열

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

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

    // Print average
    printf("Average: %i\n", (score1 + score2 + score3) / 3);
}
  • 점수의 개수가 더 많아진다면 이 프로그램은 많은 부분을 수정해야 한다.
  • 이 때 활용할 수 있는 것이 배열의 개념이다.
  • 배열은 같은 자료형의 데이터를 메모리상에서 연이어서 저장하고 이를 하나의 변수로 관리하기 위해 사용된다.
  • 위 코드는 배열을 이용하면 아래와 같이 바꿀 수 있다.
#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]; 이라는 코드는 int 자료형을 가지는 크기 3의 배열을 scores라는 이름으로 생성하겠다는 의미이다.
  • 배열의 인덱스는 0부터 시작하기 때문에, scores의 인덱스는 0, 1, 2 세 개가 있다.
  • 이 인덱스를 변수명 뒤에 대괄호 [] 사이에 입력하여 배열의 원하는 위치에 원하는 값을 저장하고 불러올 수 있다.
  • 하지만 위와 같은 코드는 여전히 점수의 개수가 바뀌는 상황에서 제약이 많다.





배열(2)

전역 변수

#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);
}
  • 위의 코드에서 scores 배열의 크기를 정해주는 N이라는 변수 새로 선언
  • 만약 N이 고정된 값(상수)이라면 그 값을 선언할 때 const를 붙여 전역 변수, 즉 코드 전반에 거쳐 바뀌지 않는 값임을 지정할 수 있다.
  • 관례적으로 이런 전역 변수 이름은 대문자로 표기한다.
  • scores의 크기로 전역 변수를 선언하였기 때문에 점수 개수가 바뀌었을 때 수정해야 하는 코드가 좀 줄었다. 하지만 일일이 인덱스마다 점수를 지정해줘야 하는 불편함이 있다.



배열의 동적 선언 및 저장

#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;
}
  • 위의 코드는 배열의 크기를 사용자에게 직접 입력 받고, 배열의 크기만큼 루프를 돌면서 각 인덱스에 해당하는 값을 역시 사용자에게 입력 받아 저장한다.
  • average 함수를 따로 만들어서 평균을 구한다.
  • average 함수는 length와 array[], 즉 배열의 길이와 배열을 입력으로 받는다.
  • 함수 안에서는 배열의 길이만큼 루프를 돌면서 합을 구하고 최종적으로 평균값을 반환한다.





문자열과 배열

  • 우리가 여지껏 사용한 문자열(string) 자료형의 데이터는 사실 문자(char) 자료형의 데이터들의 배열이다.
  • string s = “HI!”; 와 같은 문자열 s는 문자의 배열이기 때문에 메모리 상에 아래 그림과 같이 저장되고, 인덱스로 각 문자에 접근할 수 있다.
    string메모리
  • 여기서 가장 끝의 \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로 설정되어 있음을 확인할 수 있다.
  • 따라서 출력하는 것은 문자열이 아닌 문자다.
  • 이는 names[0][1]과 같이 2차원 배열을 통해 접근할 수 있다.
  • names[0][1]는 names의 첫 번째 값, 즉 “EMMA”라는 문자열에서, 그 두번째 값, 즉 ‘M’ 이라는 문자를 의미한다.
  • 아래 그림에서 names가 실제 메모리상에 저장된 예시와 해당하는 인덱스를 확인할 수 있다.
    names메모리





문자열의 활용

문자열의 길이 및 탐색

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

int main(void)
{
    string s = get_string("Input: ");
    printf("Output:\n");
    for (int i = 0, n = strlen(s); i < n; i++)
    {
        printf("%c\n", s[i]);
    }
}
  • strlen은 문자열의 길이를 알려주는 함수이며 string.h 라이브러리 안에 포함되어 있다.
  • 위 코드에서는 n이라는 변수에 문자열 s의 길이를 저장하고, 해당 길이 만큼만 for 루프를 순환한다.



문자열 탐색 및 수정

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

int main(void)
{
    string s = get_string("Before: ");
    printf("After:  ");
    for (int i = 0, n = strlen(s); i < n; i++)
    {
        if (s[i] >= 'a' && s[i] <= 'z')
        {
            printf("%c", s[i] - 32);
        }
        else
        {
            printf("%c", s[i]);
        }
    }
    printf("\n");
}
  • 입력받은 문자열을 대문자로 바꿔주는 프로그램이다.
  • 먼저 입력받은 문자열을 s라는 변수에 저장한 뒤 s의 길이만큼 for 루프를 돌면서, 각 인덱스에 해당하는 문자가 ‘a’보다 크고 ‘z’보다 작은지 검사한다.
  • ASCII 값에서 각 알파벳의 소문자와 대문자는 32씩 차이가 난다.
  • 따라서 각 문자가 소문자인 경우 그 값에서 -32를 하고 ‘문자’ 형태로 출력하면 대문자가 출력된다.
  • 이와 동일한 작업을 수행하는 함수가 ctype 라이브러리에 toupper()라는 함수로 정의되어 있다.
#include <cs50.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    string s = get_string("Before: ");
    printf("After:  ");
    for (int i = 0, n = strlen(s); i < n; i++)
    {
        printf("%c", toupper(s[i]));
    }
    printf("\n");
}





명령행 인자

  • main 함수 괄호 안에 기계적으로 void라고 입력하는 대신 아래 코드와 같이 argc, argv를 정의해보자.
#include <cs50.h>
#include <stdio.h>

int main(int argc, string argv[])
{
    if (argc == 2)
    {
        printf("hello, %s\n", argv[1]);
    }
    else
    {
        printf("hello, world\n");
    }
}
  • 여기서 첫 번째 변수 argc는 main 함수가 받게 될 입력의 개수이다.
  • argv[]는 그 입력이 포함되어 있는 배열이다.
  • 프로그램을 명령행에서 실행하므로, 입력은 문자열로 주어진다.
  • 따라서 argv[]는 string 배열이 된다.


  • argv[0]는 기본적으로 프로그램의 이름으로 저장된다.
  • 만약 하나의 입력이 더 주어진다면 argv[1]에 저장된다.
  • 위 프로그램을 “arg.c”라는 이름으로 저장하고 컴파일 한 후 "./argc"로 실행해보면 “hello, world”라는 값이 출력된다.
  • "./argc David"로 실행해보면 “hello, David”라는 값이 출력된다.



퀴즈

태그:

카테고리:

업데이트:

댓글남기기