모두를 위한 컴퓨터 과학(CS50 2019) [3. 배열] 강의

David J. Malan (데이비드 J. 말란)의 <모두를 위한 컴퓨터 과학(CS50 2019)> 수강 내용
https://www.boostcourse.org/cs112/joinLectures/41307

3. 배열Array

1) 컴파일링

학습목표

컴파일링의 네 단계를 설명할 수 있습니다

핵심키워드

  • 컴파일링
  • 어셈블링
  • 링킹

배운 것들 가볍게 정리
첫 번째 프로그램

#include <stdio.h>

int main(void)
{
	printf("hello, world\n");
}
  • main이라는 함수 : 프로그램의 시작점으로 실행 버튼을 클릭하는 것과 동일
  • printf : 출력을 담당하는 함수
    • 사용하기 위해서는 stdio.h 라이브러리가 필요
      • stdio.h 라이브러리 : printf 함수의 프로토타입이 있어서 Clang 컴파일러가 프로그램을 컴파일할 때 printfrk 무엇인지 알려주는 역할
  • 코드 컴파일하고 실행
    • 기본적인 컴파일
      clang hello.c
      ./a.out
    • 다른이름으로 컴파일
      clang -o hello hello.c
      ./hello

두 번째 프로그램

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

int main(void)
{
	string name = get_string("What's your name?\n");
	printf("hello, %s\n", name);
}
  • CS50라이브러리를 사용했을 경우
    clang -o hello hello.c -lcs50
  • make 프로그램 이용
    • 모든 컴파일 과정으로 자동으로 처리(C 소스파일의 링크, 컴파일, 빌드 작업을 자동화)
    make hello
    ./hello

Compiling
make나 clang을 사용해서 프로그램을 실행할 때 다음 4단계를 거친다

  • 전처리preprocessing
  • 컴파일링compiling
  • 어셈블링assembling
  • 링킹linking

전처리precompile
#으로 시작되는 C 소스 코드는 전처리기기에게 실질적인 컴파일이 이루어지기 전에 무언가를 실행하라고 알림
ex) #include <stdio.h>

  • #include : 전처리기에게 다른 파일의 내용을 포함시키라고 알림
  • 프로그램의 소스 코드에 #include와 같은 줄을 포함하면 전처리기는 새로운 파일을 생성
    • 이 파일은 여전히 C 소스 코드 형태이며 stdio.h 파일 내용이 #include 부분에 포함

컴파일compile
컴파일러라고 불리는 프로그램은 C 코드를 어셈블리어라는 저수준 프로그래밍 언어로 컴파일
어셈블리 : C보다 연산의 종류가 훨씬 적지만, 여러 연산들이 함께 사용되면 C에서 할 수 있는 모든 것들을 수행
C코드를 어셈블리 코드로 변환시켜줌으로써 컴파일러는 컴퓨터가 이해할 수 있는 언어와 최대한 가까운 프로그램을 만듦

💡 컴파일 용어의 정의
<넓은 뜻> 소스 코드에서 오브젝트 코드로 변환하는 전체 과정
<좁은 뜻> 전처리한 소스 코드를 어셈블리 코드로 변환시키는 단계

어셈블assemble
어셈블리 코드를 오브젝트 코드로 변환
컴퓨터 중앙저리장치가 프로그램을 어떻게 수행해야 하는지 알 수 있는 명령어 형태인 연속된 0과 1들로 바꿔주는 작업
변환 작업은 어셈블러라는 프로그램이 수행

  • if 소스 코드에서 오브젝트 코드로 컴파일 되어야 할 파일이 딱 1개라면 컴파일 작업은 끝
  • else 링크라 불리는 단계가 추가

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

전처리컴파일어셈블링크 이 네 단계를 거치면 최종적으로 실행 가능한 파일이 완성


2) 디버깅

학습목표

디버깅 하는 여러 방법을 설명할 수 있습니다

핵심키워드

  • 디버깅
  • help50
  • debug50

버그와 디버깅

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

디버깅의 기본
디버거는 프로그램을 특정 행에서 멈출 수 있게 해주기 때문에 버그를 찾는데 도움이 된다
→ 프로그래머는 breakpoint에서 무슨 일이 일어나는 지 볼 수 있고 프로그램을 한 번에 한 행씩 실행할 수 있게 해준다

📚 중지점breakpoint : 프로그램이 멈추는 특정 지점

help50

int main(void)
{
	printf("hello, world\n");
}
  • 위 코드를 make 프로그램을 이용하여 컴파일하면 다음과 같은 에러메시지 발생
    error: implicitly declaring library function 'printf'

  • 에러 메시지를 이해하기 힘들다면 help50 프로그램을 사용
    help50 make 파일이름

  • help50이 컴파일시 생기는 오류를 해석
    Asking for help...
    파일이름.c : 3:5: error: implicitly declaring library function 'printf' with type
    Did you forget to #include <stdio.h> (in which printf is declared) atop your file?
    $ ..

  • printf 함수를 사용하기 위해 stdio.h 라이브러리를 포함하여 해결한다

printf

// #을 10개 출력하기 위한 코드만들기  
#include <stdio.h>

int main(void)
{
	for (int i = 0; i <= 10; i++)
    {
        printf("#\n");
    }
}
  • 이 코드를 컴파일하고 실행하면 에러는 발생하지 않는다
  • 하지만 우리 의도와는 다르게 #이 11개나 출력되는 상황
  • 디버깅의 다른 방법으로 직접 의심이 가는 변수를 출력해서 확인가능
  • 변수 i를 출력
#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이라는 프로그램도 사용가능
  • breakpoint를 지정하고 소스파일을 컴파일한 후에 debug50 파일명 으로 실행
    • 오른쪽 패널을 통해 변수의 값을 확인가능
    • breakpount부터 한 줄씩 코드를 실행가능
  • 디버깅 종료 : Ctrl + c

앞으로 코드를 실행할 때 버그가 생기고 에러 메시지를 이해할 수 없다면 help50를 이용
하지만 debug50은 직감이 되어야 한다 만약 어딘가에 버그가 있거나 프로그램이 제대로 작동하지 않거나 분석하는 신용카드 번호가 잘못되었다면 당장 debug50을 사용

💡 debug50은 C뿐만 아니라 다른 언어에서도 평생 사용할 수 있는 기술이므로 빨리 익숙해지는 것이 중요


3) 코드의 디자인

학습목표

코드의 정확성과 디자인을 관리하는 방법을 설명할 수 있습니다

핵심키워드

  • check50
  • sytle50
  • 고무 오리

check50
과제를 잘 수행했는지 자동으로 검사할 수 있는 프로그램
cs50 강의를 위해 만들어진 프로그램

  • 실제로 많은 사람들이 협업하는 환경에서 이와 같은 자동 검사 프로그램은 많은 도움이 된다
    • 여러사람들이 각자 한 부분을 맡아 코드를 작성할 때 각자가 수정한 코드가 전체 프로그램의 정확성을 해치지 않는지 쉽게 확인가능하기 때문

style50
style50 프로그램을 이용하면 코드가 심미적으로 잘 작성되어 있는지 검사 가능

  • 공백의 수나 줄바꿈과 같은 것들은 코드의 실행에 직접적으로 영향을 주지는 않지만 코드를 작성하는 사람들이 코드를 읽고 이해하는데 영향을 끼치기 때문

ex1)

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

ex2)

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

ex3)

int main(void){printf("hello\n");}
  • 많은 회사들은 사내에서 코드를 작성할 때 특정한 스타일 가이드를 따르도록 한다
    : 여러 사람들이 코드를 작성하기 때문에 서로 불필요한 오해를 없애고, 코드를 이해하는데 드는 비용을 최소화하기 때문

러버덕(고무오리)
고무오리와 같이 무언가 대상이 되는 물체를 앞에 두고, 내가 작성한 코드를 말로 설명해주는 과정
→ 미처 놓치고 있었던 논리적 오류를 찾아낼 수도 있다.

✅ 러버덕 디버깅의 목표는 결국 문제를 해결하는 데 있어 도구 의존도를 줄이고 다시 사람에게 집중하는 추가적인 메커니즘일 뿐이다.


4) 배열(1)

학습목표

배열을 정의하고 사용하는 방법을 설명할 수 있습니다

핵심키워드

  • 배열

메모리
C에는 아래와 같은 여러 자료형이 있고, 각각의 자료형은 서로 다른 크기의 메모리를 차지

  • bool : 불리언, 1byte
  • char : 문자, 1byte
  • int : 정수, 4byte
  • float : 실수, 4byte
  • long : (더 큰) 정수, 8byte
  • double : (더 큰) 실수, 8byte
  • string : 문자열, ?byte

컴퓨터 안에 RAM이 메모리 역할을 한다
→ 여러 개의 노란색 사각형이 메모리를 의미하고, 작은 사각형 하나가 1byte를 의미

ex) char 타입의 변수를 하나 생성하고, 그 값을 입력한다고 하면 한 사각형 안에 그 변수의 값이 저장

배열
같은 자료형의 데이터를 메모리상에 연이어서 저장하고 이를 하나의 변수로 관리하기 위해 사용

ex) 3개의 점수를 저장하고 그 평균을 출력하는 프로그램

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

int main(void)
{
  // Score
  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가 존재
    • 이 인덱스를 변수명 뒤 대괄호 [] 사이에 입력하여 배열의 원하는 위치에 원하는 값을 저장하고 불러올 수 있다
    • 하지만 여전히 점수의 개수가 바뀌는 상황에서 제약이 많다

5) 배열(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);
}
  • 만약 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라는 함수를 따로 선언하여 평균을 구한다
    • lengthd와 array[], 즉 배열의 길이와 배열을 입력 받는다
    • 함수의 안에서는 배열의 길이만큼 루프를 돌면서 값의 합을 구하고 최종적으로 평균값을 반환

6) 문자열과 배열

학습목표

문자열이 C에서 정의되는 방식과 메모리에 저장되는 방식을 설명할 수 있습니다

핵심키워드

  • 문자
  • 문자열

문자열(string) 자료형의 데이터는 사실 문자(char) 자료형의 데이터들의 배열
string s = "HI!";
s는 문자의 배열이기 때문에 메모리상에 아래 그림과 같이 저장되고 인덱스로 각 문자에 접근가능

  • '\0'은 문자열의 끝을 나타내는 널 종단 문자
    • 단순히 모든 비트가 0인 1byte를 의미
#include <stdio.h>
#include <cs50.h>

int main(void)
{
    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라는 문자열 형식의 배열에 4개의 이름이 저장
  • 1번째 printf에서는 names의 1번째 인덱스의 값 "EMMA"를 출력
  • 2번째 printf에서는 형식지정자가 %s가 아닌 %c로 설정 확인
    → 출력하는 것은 문자열이 아닌 문자
  • 각 이름의 두번째 문자를 출력한다면?
    → 2차원 배열을 통해 접근가능
    • 1번째 대괄호는 이름들의 배열의 인덱스
      2번째 대괄호는 이름을 문자들의 배열
      ex) names[0][1]
      : names의 첫 번째 값, 즉 “EMMA”라는 문자열에서
      names의 두번째 값, 즉 ‘M’ 이라는 문자를 의미

아래 그림에서 names가 실제 메모리상에 저장된 예시와 해당하는 인덱스를 확인가능


7) 문자열의 활용

학습목표

문자열을 탐색하고 일부 문자를 수정하는 코드를 구현할 수 있습니다.

핵심키워드

  • strlen
  • toupper

문자열의 길이 및 탐색
사용자로부터 문자열을 입력받아 한 글자씩 출력하는 프로그램 만들기
간단하게 for 루프를 통해 문자열의 인덱스를 하나씩 증가시키면서 해당하는 문자를 출력
→ 문자열의 끝은 해당하는 인덱스의 문자가 널 종단 문자('\0')와 일치하는지 검사
s라는 문자열이 있을 때 for (int i = 0; s[i] !='\0'; i++){...}과 같은 루프를 사용
but 아래 코드와 같이 strlen()이라는 함수를 사용

#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값, 즉 그 문자가 정의되는 ASCII 코드 상에서의 숫자값으로 비교할 수 있기 때문
    • 알파벳의 ASCII 값을 잘 살펴보면 각 알파벳의 소문자와 대문자는 32씩 차이나기 때문
  • 각 문자가 소문자인 경우 그 값에서 32를 뺀 후에 '문자' 형태로 출력하면 대문자가 출력

  • 각 문자가 이미 대문자인 경우는 그대로 출력

📚 ctype 라이브러리의 toupper()함수
이미 동일한 작업을 수행하는 함수가 정의되어 있기 때문에 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");
}

8) 명령행 인자

학습목표

명령행 인자를 받는 프로그램을 C로 작성할 수 있습니다.

핵심키워드

  • 명령행 인자
  • argv
  • argc

명령행인자
C언어 프로그램을 실행할 때 'clang' 혹은 'make'라는 명령을 입력합으로써 프로그램을 컴파일 한다
그런데 명령을 할 때, 추가 인자(argument)를 넣을 수 있는 이를 명령행 인자라고 부른다.

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

int main(void)
{
}  
  • main 함수에 함수 인자를 넣지 않을 수 있지만 아래와 같이 명령행 인자를 넣어줄 수도 있다
#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");
    }
}
  • 첫 번째 변수 argcmain 함수가 받게 될 입력의 개수
  • argv[]는 그 입력이 포함되어 있는 배열
    • 프로그램을 명령행에서 실행하므로 입력은 문자열로 주어진다
      argv[]string배열이 된다
  • argv[0]는 기본적으로 프로그램의 이름으로 저장
    • 만약 하나의 입력이 주어진다면 argv[1]에 저장될 것
      ex) 위 프로그램을 "arg.c"라는 이름으로 저장하고 컴파일 한 후 "./argc"로 실행하면 "hello, world"라는 값이 출력
      → 명령행 인자에 주어진 값이 프로그램 이름 하나밖에 없기 때문
      but "./argc David"로 실행하면 "hello, David"라는 값이 출력
      → 명령행 인자에 David라는 값이 추가 입력되었고 따라서 argc는 2, argv[1]은 "David"가 되기 때문

Quiz 3

좋은 웹페이지 즐겨찾기