• TOC {:toc}

이 글은 CS50 x 2020의 weeks 1 강의내용을 복습하기 위해 강의 노트를 기반으로 작성한 글입니다.

내용을 이해하기 위한 개인적인 설명이나 해석이 있을 수 있기 때문에 되도록 원문을 참고해주시길 바랍니다. 잘못된 부분이 있다면 댓글이나 그 외 편하신 방법으로 알려주시면 감사하겠습니다.

C

새로운 프로그래밍 언어인 C 를 배워보자. C는 스크래치보다 더 많은 기능을 갖고 있지만 오롯이 문자로만 이루어져 있기 때문에 덜 익숙할 것이다.

#include <stdio.h>

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

사용한 단어들은 새롭지만, 아이디어(ideas)들 자체는 스크래치의 “when green flag clicked”, “say (hello, world)” 블록과 동일하다.

when green flag clicked

우리는 C의 많은 구조(constructs)를 스크래치에서 사용했던 블록과 비교하면서 배울 수 있다.

hello, world

스크래치의 “when green flag clicked” 블록을 누르면 메인 프로그램이 시작한다. 이는 초록 깃발(green flag)을 누르면 아래의 블록들이 실행된다는 것을 의미한다.

이와 동일하게 C에서 첫 번째 줄은 int main(void)이다. int main(void) 뒤에는 여는 중괄호{와 닫는 중괄호}가 이어지는데 이 안에 우리의 프로그램에 필요한 모든 내용이 들어있다.

int main(void)
{
  
}

다음 블록인 “say (hello, world)“는 함수(function)로 printf("hello, world");로 매핑된다.

  • C에서 어떤 내용을 화면에 출력하기 위한 함수는 printf이다.
    • 여기서 f는 형식(format)을 나타내는 것으로 출력할 문자열을 다른 방법으로 만들(format) 수 있음을 의미한다.
  • printf 뒤에 붙는 괄호는 스크래치 블록의 흰색 타원처럼 우리가 원하는 내용을 입력할 수 있는 부분이다.
  • 이곳에 우리가 출력하기 원하는 내용을 입력하여 함수에 넘겨주기 위해서는 따옴표를 사용한다. 우리가 넘기는 문자가 문자로 받아들여질 수 있도록 큰따옴표(double quotes)로 감싸주어야 한다.
  • 마지막으로 코드의 마지막에는 세미콜론(semicolon, ;)을 붙여야 한다.

우리의 프로그램이 작동하기 위해서는 맨 위에 #include <stdio.h> 코드를 추가로 작성해야 한다. 이는 헤더 라인(header line)으로 우리가 사용할 printf 함수를 정의한다.

  • 우리 컴퓨터 어딘가에 우리가 printf 함수에 접근할 수 있도록 하는 stdio.h라는 파일이 존재하고 있으며
  • #include로 컴퓨터에 우리 프로그램에 이 파일을 포함하라고 얘기하는 것이다.

스크래치로 프로그램을 만들기 위해 스크래치 웹사이트를 열었던 것처럼 C로 코드를 작성하고 실행하기 위해 CS50 Sandbox를 사용할 것이다. CS50 Sandbox는 다양한 언어로 프로그램을 작성할 수 있도록 여러 라이브러리(libraries)와 도구(tools)들이 설치된 클라우드 기반의 가상환경(virtual, cloud-based environment)이다.

  • 위쪽에는 우리가 텍스트를 적을 수 있는 간단한 코드 편집기가,
  • 아래에는 우리가 명령(commands)을 적을 수 있는 터미널 창(terminal window)이 있다.

CS50 Sandbox

우선 + 버튼을 눌러 hello.c라는 파일을 만들고 위의 코드를 입력하자.

  • 우리의 프로그램 파일은 이것이 C 프로그램으로 여겨진다(intended)는 것을 나타내기 위해 .c로 끝난다.

code in editor

Compilers

우리가 작성한 코드는 소스 코드(source code) 라고 한다. 소스 코드를 저장하고 나면 우리는 이것을 기계어(machine code) 로 바꿔주어야 한다. 기계어는 바이너리(binary)로 되어있어 컴퓨터가 바로(directly) 이해할 수 있는 명령(instruction)이다.

소스 코드를 기계어로 바꾸기(compile) 위해서는 컴파일러(compiler)라고 불리는 프로그램을 사용한다.

컴파일은 명령 프롬프트(command prompt) 를 갖는 터미널(terminal) 창에서 할 수 있다. 왼쪽의 $ 표시가 프롬프트로 이다음에 명령을 입력할 수 있다.

  • $ 다음에 clang hello.c를 적는다.
    • clang은 C언어를 나타내는 것으로 사람들이 작성한 컴파일러이다. C언어용 컴파일러를 실행하는 것이라고 이해하면 될 것 같다.
    • hello.c는 우리가 작성한 파일명으로 CS50 Sandbox의 왼쪽 위의 폴더 아이콘을 눌러 파일 이름을 확인할 수 있다.
  • 그 상태로 터미널 창에서 엔터를 누르면 a.out이라는 새로운 파일이 생긴다.
    • a.out은 assembly output의 줄임말이다.
    • 이 파일 안에는 우리 프로그램의 코드가 바이너리로 작성되어있다.
  • 다음으로 터미널 프롬프트에서 ./a.out를 입력하면 현재 폴더 안의 a.out 프로그램을 실행한다.

String

프로그램을 실행하면 hello, world$가 나타난다. 자세히 보면 새 프롬프트가 우리가 출력한 결과와 같은 줄에 나타나 있다. 프롬프트가 새로운 줄에서 깔끔하게 시작하도록 하려면 우리는 프로그램이 실행된 이후에 새로운 줄이 필요하다고 명시해야 한다. 이을 위해 우리 코드가 새 줄 문자(newline character, 줄바꿈문자라고도 한다) \n을 포함하도록 코드를 수정한다.

#include <stdio.h>

int main(void)
{
    printf("hello, world\n");
}
  • 코드의 두 번째 줄은 우리가 코드의 새로운 섹션(section)을 시작했다는 것을 알리기 위해 비어있다. 에세이에서 새로운 문단을 시작할 때 한 문장을 비우는 것과 비슷하다. 프로그램이 실행되는데 강제되는 것은 아니지만 사람들이 긴 프로그램을 더 쉽게 읽을 수 있도록 도와준다.

코드를 수정했기 때문에 새로운 버전의 프로그램을 실행하기 위해서는 clang hello.c를 입력해서 다시 컴파일(recompile)해주어야 한다.

실행하는 프로그램을 a.out에서 다른 것으로 바꿀 수도 있다. 우리는 터미널에서 프로그램에 명령 줄 인수(command-line arguments) 를 입력해줄 수 있다. 명령줄 인수는 추가적인 옵션 같은 역할을 한다.

  • 예를 들어 clang -o hello hello.c를 입력했을 때 -o hello는 프로그램 clang에게 컴파일된 결과를 hello라고 저장하라고 알려준다.
  • 그러면 우리는 ./a.out 대신 ./hello를 실행할 수 있다.

명령 프롬프트에서는 ls(list) 같은 다른 명령도 실행할 수 있다. ls는 현재 폴더의 파일들을 보여준다.

$ ls
a.out* hello* hello.c
  • 별표*는 실행 가능한 파일을 나타낸다.

rm(remove) 명령어로 파일을 지울 수 있다.

$ rm a.out
rm: remove regular file 'a.out'?
  • 이를 승인하기 위해 yyes를 입력한 뒤 다시 ls를 입력하면 파일이 사라진 것을 확인할 수 있다.

이제 스크래치에서 “hello, David”를 출력하기 위해 했던 것처럼 사용자에게 입력을 받아보자.

ask and join block

string answer = get_string("What's your name?\n");
printf("hello, %s\n", answer);
  • 가장 먼저, 문자열(string) 이 필요하다. 문자열은 큰따옴표 안에 0개 이상의 문자가 순서대로 존재하는 텍스트 조각이다. (e.g., "", "ba", "bananas")

    • 우리는 get_string 함수를 이용해 사용자에게 문자열을 입력하도록 요청한다.
    • get_string 함수의 괄호 안에는 사용자에게 물어보고 싶은 문구(prompts)인 "What is your name?\n"을 전달한다.
    • 그 왼쪽에는 사용자가 입력한 값을 받아 둘 answer라는 변수를 생성한다.
      • 등호 =는 오른쪽의 값을 왼쪽으로 설정한다.
    • 마지막으로, 우리가 받고 싶은 변수의 종류는 string이기 때문에 변수 answer의 왼쪽에 이를 명시해준다.
  • 다음으로 우리는 printf 함수 안에서 우리가 받은 answer의 값을 다시 출력하기 원한다.

    • 이를 위해서 hello, %s\n처럼 우리가 출력할 구(phrase) 안에 문자열 변수를 받을 자리 표시자(place holder)를 작성한다.
    • 다음으로 자리 표시자에 넣고(substitute) 싶은 값이 변수 answer라는 것을 알리기 위해 printf에게 인자(혹은 옵션)를 더 전달한다.

만약 우리가 printf("hello, world"\n)처럼 잘못된 코드를(\n가 큰따옴표 밖에 위치) 작성하면 컴파일러에 에러가 나타난다.

$ clang -o hello hello.c
hello.c:5:26: error: expected ')'
    printf("hello, world"\n);
                         ^
hello.c:5:11: note: to match this '('
    printf("hello, world"\n);
          ^
1 error generated.
  • 에러의 첫 번째 줄은 hello.c의 5번째 행의 26번째 열을 확인하라고 알려준다. 컴파일러는 이 부분에 백슬래쉬(backslash) 대신 닫는 괄호가 와야 할 것으로 예측한다.

위에서 작성한 코드로 string.c 파일을 만들어보자.

#include <stdio.h>

int main(void)
{
  string answer = get_string("What's your name?\n");
  printf("hello, %s\n", answer);
}

이 파일을 컴파일하면 많은 에러가 발생한다. 가끔 하나의 실수 때문에 컴파일러가 맞는 코드까지도 맞지 않는다고 해석하기 시작할 수 있다. 이는 실제 문제보다 더 많은 에러를 발생시킨다. 그러므로 우선 가장 첫 번째 에러부터 살펴보자.

$ clang -o string string.c
string.c:5:5: error: use of undeclared identifier 'string'; did you mean 'stdin'?
  string name = get_string("What's your name?\n");
  ^~~~~~
  stdin
/usr/include/stdio.h:135:25: note: 'stdin' declared here
extern struct _IO_FILE *stdin;          /* Standard input stream.  */
  • 에러의 처음에서는 string이라는 잘못된 식별자(identifier)가 사용되었다고 하고 있다.

  • stdin을 적으려고 했던 것이 아니냐고 묻고 있지만 우리는 string대신 stdin을 적으려고 했던 것이 아니기 때문에 에러 메시지가 크게 도움이 되지 않는다.

  • 사실 우리가 string을 사용하기 위해서는 string 타입을 정의하는 다른 파일을 불러와야 한다.

문제를 간단하게 해결하기 위해 CS50로부터 라이브러리(library)를 가져와 포함해보자. 라이브러리는 코드의 모음으로 볼 수 있다. 이 라이브러리는 string 변수 타입과 get_string 함수 등을 제공해준다. 라이브러리(cs50.h)를 포함(include)하기 위해서는 위쪽에 코드 한 줄을 작성해주면 된다.

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

int main(void)
{
  string answer = get_string("What's your name?\n");
  printf("hello, %s\n", answer);
}

그리고 프로그램을 컴파일하면 단 하나의 에러만 나타난다.

$ clang -o string string.c
/tmp/string-aca94d.o: In function `main':
string.c:(.text+0x19): undefined reference to `get_string'
clang-7: error: linker command failed with exit code 1 (use -v to see invocation)
  • 이는 우리가 컴파일러에 CS50 라이브러리를 추가했다고 알려주어야 한다는 것을 의미한다.

  • 컴파일 명령을 다음과 같이 입력한다. clang -o string string.c -lcs50

    • clang으로 cs50를 연결한 string.c 파일을 컴파일하여 string 실행 파일을 만든다.
    • -l은 링크를 의미한다.
  • 이것을 더 추상화해서 make string만 입력할 수도 있다.

    • CS50 Sandbox에서는 기본적으로 make 명령어가 string.cstring으로 컴파일하는데 clang을 사용한다.
    • 넘겨주어야 할 인자는 string뿐이다.

Scratch blocks in C

이전에 사용했던 스크래치 블록들을 C로 나타내보자.

Counter

“set [counter] to (0)” 블록은 변수를 생성한다. C에서는 이를 int counter = 0;으로 적는다. int는 우리의 변수가 정수라는 것을 나타낸다.

set counter to 0 block

“change [counter] by (1)” 블록은 C에서 counter = counter + 1; 이다.

  • 방정식에서는 =countercounter + 1이 ‘같다’는 의미로 사용된다. C에서 등호는 이와 다른 용도로 쓰인다. C에서 =는 “오른쪽의 값을 복사해서 왼쪽의 값으로 넣으라는” 대입 연산자(assignment operator)이다.
  • 또한 우리는 더는 counter 앞에 int를 쓰지 않는다는 것을 알 수 있다. 이미 이전에 counterint임을 명시했기 때문이다.
  • 위의 코드 대신 counter += 1;, counter++;라고 써도 된다. 이 둘은 “syntactic sugar”라고 불리는데, 보다 적은 코드로 동일한 동작(effect)을 만들 수 있다.

change counter by 1

Condition

조건문은 다음과 같이 매핑할 수 있다.

if x is less than y block

if (x < y)
{
    printf("x is less than y\n");
}
  • C에서는 코드가 중첩(nested)되는 것을 나타내기 위해 {} (그리고 들여쓰기(indentation))를 사용한다.

if-else 조건문은 다음과 같이 나타낸다.

if else condition

if (x < y)
{
    printf("x is less than y\n");
}
else
{
    printf("x is not less than y\n");
}
  • 스스로 어떤 동작을 취하지 않는 코드들은 (if...나 중괄호) 끝에 세미콜론이 붙지 않는다.

else if도 나타낼 수 있다.

if else if condition

if (x < y)
{
    printf("x is less than y\n");
}
else if (x > y)
{
    printf("x is greater than y\n");
}
else if (x == y)
{
    printf("x is equal to y\n");
}
  • C에서 두 값을 비교하는 데는 등호 두 개 ==를 사용한다.

  • 또한 마지막 if (x==y)조건이 마지막으로 남은 유일한 경우이기 때문에 적어줄 필요 없이 else만 적어도 된다.

Loops

반복문은 다음과 같이 적을 수 있다.

loops forever

while (true)
{
    printf("hello, world\n");
}
  • while 키워드도 조건을 필요로하기 때문에 부울표현식으로 true를 사용한다. 이러면 반복문이 영원히 실행된다.
    • 우리의 프로그램은 while 안의 표현 식이 true로 평가(evaluate)되는지 점검한 뒤 중괄호 안의 코드를 실행한다.
    • 그 이후로 안의 표현 식이 더는 참이 아닐 때까지 이를 반복한다.
    • 위의 경우에서는 상태가 거짓으로 변하지 않기 때문에 반복문이 영원히 실행된다.

while을 이용해서 정해진 횟수만큼 반복할 수도 있다.

repeat a certain number of times

int i = 0;
while (i < 50)
{
    printf("hello, world\n");
    i++;
}
  • 먼저 변수 i를 생성한 뒤 값을 0으로 설정한다.

  • 그리고 i < 50일 때까지 중괄호 안의 코드를 실행한다.

  • 매번 코드가 실행될 때마다 i에 1을 더한다.

  • 중괄호 안의 두 줄의 코드가 반복되는 코드이며 이후에 원한다면 코드를 추가해도 된다.

동일한 반복을 for 키워드를 사용해서 더 일반적으로 작성할 수 있다.

for (int i = 0; i < 50; i++)
{
    printf("hello, world\n");
}
  • 위에서와같이 우선 i라는 변수를 만들어 0으로 설정한다.

  • 다음으로 반복문을 실행할 때마다 중괄호 안의 코드를 실행하기 전에 i < 50인지 확인한다.

  • 조건인 표현 식(i.e., i < 50)이 참이면 중괄호 안의 코드를 실행한다.

  • 마지막으로 중괄호 안의 코드를 실행하고 난 뒤 i++i에 1을 더한다.

Types, formats, operators

위에서 사용한 int외에도 변수에 지정할 수 있는 타입은 다음과 같다.

타입 설명
bool truefalse가 될 수 있는 부울 표현식
char a2와 같은 하나의 문자
double float보다 많은 자릿수를 갖는 부동 소수점(floating-point)
float 부동 소수점(floating-point) 값 혹은 십진법으로 나타낸 실수
int 특정 크기 혹은 비트 수까지의 정수 (값의 제한이 있다고 생각하면 편하다)
long 더 많은 수의 비트로 만드는 정수로 int보다 더 큰 수를 셀 수 있다
string 문자열

CS50 라이브러리에는 각 타입에 대응되는 입력 함수가 존재한다.

함수
get_char
get_double
get_float
get_int
get_long
get_string

printf에서 사용하는 자리 표시자도 타입마다 다르다.

자리 표시자 타입
%c chars
%f floats, doubles
%i ints
%li longs
%s strings

우리가 사용할 수 있는 수학 연산자는 다음과 같다.

연산자 연산
+ addition
- subtraction
* multiplication
/ division
% remainder

More examples

sandbox links에서 아래 예제의 코드를 받을 수 있다.

int.c

int.c는 정수를 입력받은 뒤 출력하는 예제이다.

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

int main(void)
{
    int age = get_int("What's your age?\n");
    int days = age * 365;
    printf("You are at least %i days old.\n", days);
}
  • %i는 정수를 출력한다.

  • make int를 입력해서 컴파일 한 뒤 ./int로 프로그램을 실행할 수 있다.

  • days 변수를 제거하여 코드를 축약할 수도 있다.

    int age = get_int("What's your age?\n");
    printf("You are at least %i days old.\n", age * 365);
    
  • 다음과 같이 아예 한 줄로 코드를 축약할 수도 있지만 한 줄이 너무 길고 복잡해질 수 있어서 가독성을 위해 코드를 두세 줄로 유지하는 것이 나을 수도 있다.

    printf("You are at least %i days old.\n", get_int("What's your age?\n") * 365);
    

float.c

float.c에서는 십진수(decimal number)를 받는다.

  • 소수점(decimal point)이 숫자들 사이를 떠(float)다니기 때문에 컴퓨터에서는 부동소수점 값으로 불린다.
#include <cs50.h>
#include <stdio.h>

int main(void)
{
    float price = get_float("What's the price?\n");
    printf("Your total is %f.\n", price * 1.0625);
}
  • 프로그램을 실행하면 세금이 반영된 가격을 출력한다.

  • 자리 표시자를 %.2f처럼 작성하면 출력될 숫자의 소수점 다음에 오는 자릿수를 결정할 수 있다. %.2f는 소수점 다음 두 자리까지 숫자를 표시한다는 것을 의미한다.

parity.c

parity.c는 숫자가 짝수인지 홀수인지 체크한다.

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

int main(void)
{
    int n = get_int("n: ");

    if (n % 2 == 0)
    {
        printf("even\n");
    }
    else
    {
        printf("odd\n");
    }
}
  • 모듈로(modulo) 연산자 %를 사용하면 n을 2로 나눈 나머지를 얻을 수 있다.

    • 나머지가 0이면 n은 짝수이고
    • 아니면 n은 홀수이다.
  • CS50 라이브러리의 get_int와 같은 함수는 입력한 값이 우리가 받고 싶어 하는 타입과 일치하도록 에러 확인(error-checking)을 진행한다.

condition.c

condition.c은 이전에 작성했던 조건문 스니펫(snippet)을 프로그램으로 가져왔다.

// Conditions and relational operators

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

int main(void)
{
    // Prompt user for x
    int x = get_int("x: ");

    // Prompt user for y
    int y = get_int("y: ");

    // Compare x and y
    if (x < y)
    {
        printf("x is less than y\n");
    }
    else if (x > y)
    {
        printf("x is greater than y\n");
    }
    else
    {
        printf("x is equal to y\n");
    }
}
  • //로 시작하는 문장은 주석(comment)으로 사람의 이해를 돕기 위한 것이며 컴파일러는 이를 무시한다.

agree.c

agree.c에서는 사용자가 어떤 것을 승인하거나 거절하도록 요청한다.

// Logical operators

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

int main(void)
{
    // Prompt user to agree
    char c = get_char("Do you agree?\n");

    // Check whether agreed
    if (c == 'Y' || c == 'y')
    {
        printf("Agreed.\n");
    }
    else if (c == 'N' || c == 'n')
    {
        printf("Not agreed.\n");
    }
}
  • 두 개의 수직선 ||은 논리적으로 “or”을 나타내는 것으로 둘 중 하나의 표현 식이 참이면 조건을 만족한 것으로 여겨진다.

  • 두 개의 표현 식 모두 거짓이면 프로그램이 반복되지 않는 이상 아무 일도 일어나지 않는다.

cough.c

0주 차 수업에서 만들었던 기침하는(coughing) 프로그램을 가져와 보자.

#include <stdio.h>

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

for문을 이용할 수도 있다.

#include <stdio.h>

int main(void)
{
    for (int i = 0; i < 3; i++)
    {
        printf("cough\n");
    }
}
  • 일반적으로 프로그래머들은 카운팅을 0에서부터 시작한다. 즉 i는 멈추기 전까지 세 번의 반복(iteration)을 진행하는 동안 순서대로 0, 1, 2의 값을 갖는다.

printf 부분을 함수로 만들 수도 있다.

#include <stdio.h>

void cough(void);

int main(void)
{
    for (int i = 0; i < 3; i++)
    {
        cough(); // cough 함수 실행
    }
}

// cough 함수 정의
// cough 함수가 아직 어떤 입력도 받지 않기 때문에 cough(void)로 작성한다.
void cough(void)
{
    printf("cough\n");
}
  • main 함수에서 호출하기 전에 void cough(void);로 새로운 함수를 선언한다.

    • C 컴파일러는 코드를 위에서 아래로 읽기 때문에 cough라는 함수가 존재한다는 것을 사용하기 전에 알려주어야 하기 때문이다.
  • 그 이후에 main 함수 안에서 cough 함수를 실행한다.

  • 이 방식을 이용해 함수가 있다는 것을 알리면서 main 함수를 위쪽에 둘 수 있다.

cough 함수를 조금 더 추상화시킬 수 있다.

#include <stdio.h>

void cough(int n);

int main(void)
{
    cough(3);
}

void cough(int n)
{
    for (int i = 0; i < n; i++)
    {
        printf("cough\n");
    }
}
  • 이제 우리는 동일한 함수를 이용해서 “cough”를 원하는 만큼 반복해서 출력할 수 있다.

  • void cough(int n)cough 함수가 int를 입력으로 받는다는 것을 나타낸다. 입력받은 값은 n으로 나타낸다.

  • ncough 안의 for문 에서 “cough”의 출력 횟수를 결정한다.

positive.c

CS50 라이브러리에 양수만 입력받는 get_positive_int와 같은 함수는 존재하지 않지만, 우리 스스로 작성할 수 있다.

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

int get_positive_int(void);

int main(void)
{
    int i = get_positive_int();
    printf("%i\n", i);
}

// Prompt user for positive integer
int get_positive_int(void)
{
    int n;
    do
    {
        n = get_int("%s", "Positive Integer: ");
    }
    while (n < 1);
    return n;
}
  • int get_positive_int(string prompt) 함수는 사용자에게 보여줄 prompt라고 불리는 string을 입력받아서 int를 반환한다.

    • 반환된 값은 main 함수에서 i에 저장된다.
  • get_positive_int에서 우리는 변수 int n을 값을 할당하지 않은 채 초기화한다.

  • 다음으로 새로운 구조인 do ... while을 사용하는데 이는 어떤 것을 먼저 수행한 뒤 조건을 확인하는 것을 조건이 참이 아닐 때까지 반복한다.

  • 우리가 < 1이 아닌 n을 받게 되면 반복이 종료되고 그 값을 return 키워드로 반환한다.

  • 그러면 main 함수에서 int i 값을 받아온 값으로 설정할 수 있다.

Screens

슈퍼 마리오 브라더스와 같은 비디오 게임의 화면 일부분을 출력하는 프로그램을 만들어보자.

mario0.c 파일은 네 개의 물음표로 블록 한 줄을 출력한다.

// Prints a row of 4 question marks

#include <stdio.h>

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

다음과 같이 작성된 mario2.c를 실행하면 사용자에게 물음표를 몇 개 출력할 것인가를 물어본 뒤 그만큼 출력할 수도 있다.

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

int main(void)
{
    int n;
    do
    {
        n = get_int("Width: ");
    }
    while (n < 1);
    for (int i = 0; i < n; i++)
    {
        printf("?");
    }
    printf("\n");
}

mario8.c를 실행시키면 블록을 2차원으로 출력한다.

// Prints an n-by-n grid of bricks with a loop

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

int main(void)
{
    int n;
    do
    {
        n = get_int("Size: ");
    }
    while (n < 1);
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            printf("#");
        }
        printf("\n");
    }
}
  • 코드를 보면 반복문이 중첩되어있는 것을 확인할 수 있다.
    • 바깥쪽 반복문은 i를 사용해서 그 안의 코드를 n번 반복한다.
    • 안쪽 반복문은 다른 변수인 j를 사용해서 바깥쪽 코드가 한 번 실행될 때마다 안쪽 반복문 안의 코드를 n번 반복한다.
    • 즉 바깥쪽 반복문은 n개의 행을 출력하고 안쪽 반복문은 각 행에 n개의 열 혹은 #문자를 출력한다.

Memory, imprecision, and overflow

우리의 컴퓨터에는 메모리(memory)가 존재한다. 하드웨어 칩 안에 존재하며 RAM(random-access memory)이라고 불린다. 우리 프로그램은 RAM에 프로그램이 실행하는 자료를 저장한다. 문제는 메모리가 한정적이라는 것이다.

메모리는 한정적이기 때문에 우리는 모든 숫자를 무한히 표현할 수 없다. 그래서 컴퓨터는 float와 int에 특정 수의 비트(bits)를 부여하고 특정 지점에서 그 값을 가장 가까운 값으로 근사한다.

floats.c에서 floats를 사용하면 어떻게 되는지 살펴보자.

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

int main(void)
{
    // Prompt user for x
    float x = get_float("x: ");

    // Prompt user for y
    float y = get_float("y: ");

    // Perform division
    printf("x / y = %.50f\n", x / y);
}
  • %50f로 자릿수를 50개까지 표시하도록 정했다.

  • 출력 결과는 다음과 같다.

    x: 1
    y: 10
    x / y = 0.10000000149011611938476562500000000000000000000000
    
    • 어느 지점에서부터 정확한 값이 아닌 0이 나타나게 된다.
  • 이처럼 가능한 값 전부를 저장하기에는 우리가 가진 비트가 충분하지 않아 컴퓨터가 이에 가장 가까운 값을 저장하는 것을 floating-point imprecision 이라고 한다.

overflow.c에서 비슷한 문제를 확인해보자.

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    for (int i = 1; ; i *= 2)
    {
        printf("%i\n", i);
        sleep(1);
    }
}
  • i1로 설정한 뒤 *= 2를 이용해 그 값을 계속 두 배로 키우는 것을 무한히 반복한다. (체크할 조건이 없기 때문에)

  • 각 반복 사이에 unistd.hsleep 함수를 이용해 프로그램이 잠시 멈추도록 한다.

  • 프로그램을 실행하면 숫자가 점점 커지다가 어느 순간 다음과 같이 표시된다.

    1073741824
    overflow.c:6:25: runtime error: signed integer overflow: 1073741824 * 2 cannot be represented in type 'int'
    -2147483648
    0
    0
    ...
    
    • 프로그램은 더는 다음 값을 저장할 수 없다는 것을 알아차리고 에러를 출력한다. 그다음에도 수를 두 배 하려고 시도하면 i는 음수가 되었다가 0이 된다.
  • 이와 같은 문제를 integer overflow 라고 부른다. 정수는 비트가 다 소진되기 전까지만 커지다가 어느 순간 뒤집힌다(rolls over). 십진수에서 999에 1을 더하면 1,000이 되지만 숫자를 세 자리밖에 표시하지 못하면 마지막 1을 표시하지 못하고 결과적으로 000이 되는 것과 비슷하게 이해하면 된다.

실례로 2000년대로 넘어갈 때 Y2K 문제가 발생했었다. 많은 프로그램이 1998년은 98, 1999년은 99처럼 달력의 연도를 두 자릿수로 표현했었는데 2000년에 프로그램이 00을 값으로 저장하면서 많은 혼란이 있었다.