1. C
- 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)” 블록과 동일하다.
우리는 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)이 있다.
우선 +
버튼을 눌러 hello.c
라는 파일을 만들고 위의 코드를 입력하자.
- 우리의 프로그램 파일은 이것이 C 프로그램으로 여겨진다(intended)는 것을 나타내기 위해
.c
로 끝난다.
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'?
- 이를 승인하기 위해
y
나yes
를 입력한 뒤 다시ls
를 입력하면 파일이 사라진 것을 확인할 수 있다.
이제 스크래치에서 “hello, David”를 출력하기 위해 했던 것처럼 사용자에게 입력을 받아보자.
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.c
를string
으로 컴파일하는데clang
을 사용한다. - 넘겨주어야 할 인자는
string
뿐이다.
- CS50 Sandbox에서는 기본적으로
Scratch blocks in C
이전에 사용했던 스크래치 블록들을 C로 나타내보자.
Counter
“set [counter] to (0)” 블록은 변수를 생성한다. C에서는 이를 int counter = 0;
으로 적는다. int
는 우리의 변수가 정수라는 것을 나타낸다.
“change [counter] by (1)” 블록은 C에서 counter = counter + 1;
이다.
- 방정식에서는
=
가counter
와counter + 1
이 ‘같다’는 의미로 사용된다. C에서 등호는 이와 다른 용도로 쓰인다. C에서=
는 “오른쪽의 값을 복사해서 왼쪽의 값으로 넣으라는” 대입 연산자(assignment operator)이다. - 또한 우리는 더는
counter
앞에int
를 쓰지 않는다는 것을 알 수 있다. 이미 이전에counter
가int
임을 명시했기 때문이다. - 위의 코드 대신
counter += 1;
,counter++;
라고 써도 된다. 이 둘은 “syntactic sugar”라고 불리는데, 보다 적은 코드로 동일한 동작(effect)을 만들 수 있다.
Condition
조건문은 다음과 같이 매핑할 수 있다.
if (x < y)
{
printf("x is less than y\n");
}
- C에서는 코드가 중첩(nested)되는 것을 나타내기 위해
{
와}
(그리고 들여쓰기(indentation))를 사용한다.
if-else 조건문은 다음과 같이 나타낸다.
if (x < y)
{
printf("x is less than y\n");
}
else
{
printf("x is not less than y\n");
}
- 스스로 어떤 동작을 취하지 않는 코드들은 (
if...
나 중괄호) 끝에 세미콜론이 붙지 않는다.
else if
도 나타낼 수 있다.
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
반복문은 다음과 같이 적을 수 있다.
while (true)
{
printf("hello, world\n");
}
while
키워드도 조건을 필요로하기 때문에 부울표현식으로true
를 사용한다. 이러면 반복문이 영원히 실행된다.- 우리의 프로그램은
while
안의 표현 식이true
로 평가(evaluate)되는지 점검한 뒤 중괄호 안의 코드를 실행한다. - 그 이후로 안의 표현 식이 더는 참이 아닐 때까지 이를 반복한다.
- 위의 경우에서는 상태가 거짓으로 변하지 않기 때문에 반복문이 영원히 실행된다.
- 우리의 프로그램은
while
을 이용해서 정해진 횟수만큼 반복할 수도 있다.
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 |
true 나 false 가 될 수 있는 부울 표현식 |
char |
a 나 2 와 같은 하나의 문자 |
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
은 홀수이다.
- 나머지가 0이면
-
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
라는 함수가 존재한다는 것을 사용하기 전에 알려주어야 하기 때문이다.
- C 컴파일러는 코드를 위에서 아래로 읽기 때문에
-
그 이후에
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
으로 나타낸다. -
이
n
은cough
안의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
에 저장된다.
- 반환된 값은 main 함수에서
-
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);
}
}
-
i
를1
로 설정한 뒤*= 2
를 이용해 그 값을 계속 두 배로 키우는 것을 무한히 반복한다. (체크할 조건이 없기 때문에) -
각 반복 사이에
unistd.h
의sleep
함수를 이용해 프로그램이 잠시 멈추도록 한다. -
프로그램을 실행하면 숫자가 점점 커지다가 어느 순간 다음과 같이 표시된다.
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을 값으로 저장하면서 많은 혼란이 있었다.
-
draft