2.1 C언어
C언어
C는 아주 오래되고 전통적인 순수 텍스트 기반의 언어이다.
하나하나 설명하자면 int main(void) 는 스크래치의 “초록색 깃발을 클릭했을 때” 블록과 같은 역할을 한다.
**int main(void) { }**의 중괄호 사이에 코드를 작성.
C에서는 코드의 마지막에 **세미콜론(;)** 을 붙여야 한다.
**#include <stdio.h>**는 “stdio.h”라는 이름의 파일을 찾아서 “printf” 함수에 접근할 수 있도록 해준다.
C로 작성한 코드는 **“파일이름.c”**로 저장해야 한다.
2.2 문자열
CS50 Sandbox에서는 스크래치의 ask함수와 가장 비슷한 것은 get_string 함수이다.
우리는 저장하고자 하는 값의 종류가 **문자열(string)**이라는 것을 알려줘야 한다. 이때 string을 형식지정자라고 한다.
그럼 이 것을 printf 함수로 출력을 해보자.
2.3 조건문과 루프
if ( ) 의 괄호 안에는 검사하고자 하는
조건
이 들어가고, { } 안에는 조건을 만족할 때 수행하고자 하는 작업이 들어간다.
여기에서는 조건이 True면 {} 안의 명령을 수행한다.
else를 이용해 처음 조건이 아닌 경우에는 어떤 것을 하라라고 명령할 수 있다.
추가로 if, else, else if 뒤에는 세미콜론(;)이 붙지 않는다..
루프
C에서도 while 이나 for 을 통해서 루프를 구현할 수 있다.
먼저 while 의 경우 while ( )의 괄호 안에 조건을 넣고 { } 안에 수행할 작업을 포함시키면 된다.
2.4 자료형, 형식 지정자, 연산자
데이터 타입
아래 목록은 변수의 데이터 타입으로 사용할 수 있는 것들입니다.
- bool: 불리언 표현, (예) True, False, 1, 0, yes, no
- char: 문자 하나 (예) 'a', 'Z', '?'
- string: 문자열
- int: 특정 크기 또는 특정 비트까지의 정수 (예) 5, 28, -3, 0
- long: 더 큰 크기의 정수
- float: 부동소수점을 갖는 실수 (예) 3.14, 0.0, -28.56
- double: 부동소수점을 포함한 더 큰 실수
- int는 대략 40억까지 셀 수 있기 때문에 40억게 이상의 데이터를 가진 일부 거대 기업과 같은 상황이 아닌 일반 사용자들은 대부분 정수에 int를 사용합니다.
CS50 라이브러리 내의 get 함수
CS50 라이브러리는 위와 같은 데이터 타입을 입력값으로 받을 수 있는 아래와 같은 함수들을 포함합니다.
(CS50 라이브러리에서 사용되는 함수이기 때문에 가볍게 알고 가시면 됩니다.)
- get_char
- get_double
- get_float
- get_int
- get_long
- get_string
형식 지정자
printf 함수에서는 각 데이터 타입을 위한 형식 지정자를 사용할 수 있습니다.
지난 강의에서 문자열(string)인 answer 변수의 인자를 %s로 불러온 것을 기억하시나요?
이번에는 여러가지 데이터 타입 마다 사용되는 형식 지정자를 알아보도록 하겠습니다.
- %c : char
- %f : float, double
- %i : int
- %li : long
- %s : string
기타 연산자 및 주석
그 외에도 아래 목록과 같이 다양한 수학 연산자, 논리 연산자, 주석 등이 기호로 정의되어 있습니다.
- +: 더하기
- : 빼기
- : 곱하기
- /: 나누기
- %: 나머지
- &&: 그리고
- ||: 또는
- //: 주석
정수와 실수를 받아서 출력해보기
이번에는 문자열 대신 나이(정수)를 사용해보겠습니다.
# include <cs50.h># include <stdio.h>int main(void)
{
int age = get_int("what's your age?\\n");
int days = age * 365;
printf("Your are at least %i days old.\\n", days);
}
get_int라는 정수 값을 받아오는 CS50 라이브러리에 있는 함수를 사용합니다.
사용자의 나이는 오른쪽에서 왼쪽으로 복사되어 age라는 변수에 저장됩니다.
그 변수의 종류는 int 정수입니다.
그럼 이 사람의 나이를 일 수로 환산하면 며칠인지 계산해볼까요?
days라는 정수 변수에 age에 365를 곱한 수를 저장해줍니다.
그리고 printf 함수에 이번에는 문자가 아닌 정수이기 때문에 %i로 days의 인자를 받아주고 출력해줍니다.
이 코드를 좀 더 간단하게 작성해볼까요?
# include <cs50.h># include <stdio.h>int main(void)
{
int age = get_int("what's your age?\\n");
printf("Your are at least %i days old.\\n", age * 365);
}
이전에 days에 age에 365를 곱한 값을 저장했습니다.
하지만 엄밀히 말하면 이 행은 필요 없습니다.
days 대신 age*365를 넣으면 되기 때문입니다.
좀 더 극단적으로 줄여볼까요?
# include <cs50.h># include <stdio.h>int main(void)
{
printf("Your are at least %i days old.\\n", get_int("what's your age?\\n") * 365);
}
age라는 변수를 없애버리고 age*365 대신에 get_int 함수를 넣어 365를 곱할 수 있습니다.
그렇다면 극단적으로 줄여버린 코드가 옳은 것일까요?
마지막 코드는 좌우로 너무 길어서 가독성이 떨어집니다.
디자인 측면에서는 시선이 왼쪽에서 오른쪽으로 가는 것보다 위에서 아래로 가는 것이 좋습니다.
물론 이 것은 사람마다 생각이 다르기 때문에 대한 정답은 없습니다.
하지만 읽기 편하고 이해하기 쉬운 코드가 더 선호되어지는 것 또한 사실입니다.
이번에는 실수(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);
}
get_float 함수를 사용하여 물건의 가격을 물어보고 가격을 받아 price에 저장해줍니다.
그런 다음 세금을 포함한 값을 계산해서 출력해보겠습니다. (메사추세츠의 부가세는 6.25%입니다.)
총액은 **실수(float)**이므로 %f를 사용해줍니다.
이제 코드를 실행해서 가격을 100으로 넣어볼까요?
결과 값으로 105.250000이 나온 것을 볼 수 있습니다.
하지만 소수점이 6번째 자리까지 나와 보기에 안 좋습니다.
그럼 이 것을 일부분만 나오게 해볼까요? (소수점 2번째 자리까지 나오게 해보겠습니다.)
printf("Your total is %.2f \\n", price*1.0625);
이때는 %f에서 f앞에 **'.원하는 자리수'**를 넣어 %.2f로 소수점 2번째 자리까지 나오게 할 수 있습니다.
출력을 해보면 총 액을 좀 더 보기 좋게된 것을 볼 수 있습니다.
짝수인지 홀수인지 알려주는 코드짜기
#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")
}
}
우선 get_int로 사용자들에게 정수인 숫자를 받아서 n에 저장해보겠습니다.
받은 정수인 숫자가 짝수인지 홀수인지 알아보는 방법에 무엇이 있을까요?
하나하나 적을 수도 있습니다.
만약 숫자가 2이면 짝수다 그리고 숫자가 3이면 홀수다 숫자가 4면 짝수다 .......(무한반복)....... 128129312는 짝수다
이 방법은 숫자는 무한대이기 때문에 절대 완벽한 코드가 될 수 없습니다.
그럼 어떤 방법이 있을까요? 바로 2로 나누어 나머지가 0이냐 1이냐를 보는 것입니다.
짝수면 나머지가 0이 될 것이고 홀수면 1이 될 것입니다.
if ( n % 2 == 0 ) 을 풀어 쓰면 n을 2로 나누었을 때 나머지가 0이면
printf("even\n"); -> even(짝수)을 출력하라 입니다.
홀수를 출력하는 것은 왜 else if (n % 2 == 1)으로 하지 않았을 까요?
왜냐하면 짝수 아니면 홀수 이기 때문에 else로도 충분하기 때문이죠.
주석
C에서는 //로 주석을 달 수 있습니다.
// 주석입니다.
그렇다면 주석은 왜 다는 것일까요? 주석은 이 코드가 무슨 일을 하는지 설명하는 것 입니다.
여러분의 친구, 동료, 혹은 조교 등 여러분들이 짠 코드를 처음보는 사람들에게 설명이 필요하기 때문입니다.
우리는 지금까지 10줄에서 20줄 사이로 코드를 작성해보았는데요,
만약 코드가 수 백, 수 천줄이 되면 주석이 없다면 부분 부분마다 어떤 일을 하는지 찾기 힘들 것입니다.
이 것은 꼭 타인이 아닌 자기 자신에게도 해당합니다.
자신이 짠 코드라고 해도 한달 뒤, 일년 뒤에 보면 새롭기 때문입니다.
그렇기 때문에 주석으로 잘 설명하는 습관이 중요합니다.
참고) #include<cs50.h> 는 무엇인가요?
CS50 수업을 위해 만들어진 라이브러리 입니다. 라이브러리는 여러 함수들을 모아둔 것이라고 볼 수 있습니다. CS50 수업에서는 여러분들께서 좀 더 쉽게 코딩을 짤 수 있게 CS50 라이브러리 안에 여러 함수(get_int, get_double, get_float 등등)를 만들어 두었습니다.
반면에 C에는 표준 라이브러리도 있습니다. 우리가 처음부터 사용한 #inclue<stdio.h>가 표준 라이브러리 중 하나 입니다. 가장 많이 쓰고 가장 보편적으로 사용하는 라이브러리입니다. 그 밖에도 <math.h>, <time.h> 등 자신이 코딩하는데 필요한 함수들을 그때 그때 라이브러리를 불러와서 다른 사람들이 만들어둔 함수를 사용할 수 있습니다. 이 강좌 이후에 직접 코딩을 해보고 좀 더 공부를 하다 보면 배우실 수 있을 것입니다.
그렇다면 sandbox.cs50.io가 아닌 Visual Studio 같은 곳에서 CS50 라이브러리를 바로 사용이 가능할까요? 답은 사용할 수 없습니다. 표준 라이브러리는 기본적으로 설치가 되어 있기 때문에 사용이 가능하지만 앞서 말씀드렸듯이 CS50 라이브러리는 수업을 위해 만들어진 라이브러리 입니다. 그래서 sandbox.cs50.io 처럼 미리 설치가 된 곳이 아니면 따로 설치를 하여야만 사용이 가능합니다. 설치하는 방법은 현재 수준에서 필요한 것이 아니기 때문에 아래 첨부된 CS50 라이브러리 문서를 참고해 주세요.
추가로 CS50 라이브러리의 get_int, get_float 등의 함수로 좀 더 쉽게 입력을 받아 보았는데요, 일반적으로 사용되는 입력을 받는 함수도 아래의 참고자료를 통해 학습하실 수 있게 준비해 두었으니 참고하시면 좋을 것 같습니다.
2.5 사용자 정의 함수, 중첩 루프
사용자 정의 함수
우리가 스크래치에서 했던 것 처럼 “cough”라고 세 번 말하는 C 프로그램을 작성하고 싶으면 어떻게 해야 할까요?
가장 간단한 방법은 아래처럼 작성하는 것입니다.
#include <stdio.h>int main(void)
{
printf("cough\\n");
printf("cough\\n");
printf("cough\\n");
}
단순히 printf 를 세 번 반복하면 되지만, 동일한 작업을 반복하는 것이기 때문에 사용자 정의 함수를 이용하면 아래 코드와 같이 더 단순화 할 수 있습니다.
지난번에 배운 방법으로 더 간단하게 만들 수 있습니다. 어떤 것을 사용해야 할까요?
바로 루프입니다. 그 중 for를 사용해보겠습니다.
#include <stdio.h>int main(void)
{
for (int i = 0; i < 3; i++)
{
printf("cough\\n")
}
}
어렵지 않게 cough을 3번 출력해보았습니다.
그럼 이번에는 우리만의 함수를 만들어 볼까요?
#include <stdio.h>void cough(void)
{
printf("cough\\n")
}
int main(void)
{
for (int i = 0; i < 3; i++)
{
cough();
}
}
void를 입력하고 원하는 함수명(cough)을 적은 뒤 괄호 안에 void를 적어줍니다.
그리고 printf로 cough를 출력하는 코드를 작성합니다.
우리가 지금까지 사용하던 'get_int', 'get_string' 등의 함수는 우리가 직접 구현할 필요가 없었습니다.
과거의 어떤 사람들이 모두 구현해두었기 때문입니다.
우리는 함수의 이름을 cough라고 정했습니다.
이제 int main(void)의 안에 cough(함수명)를 사용해보겠습니다.
그럼 우리가 정의해둔 대로 cough가 출력됩니다.
하지만 여기에도 문제가 있습니다.
함수를 1개가 아닌 여러개를 만들수록 main 함수는 아래로 내려가기 때문입니다.
중요한 것이 아래에 있는 것보다 바로 나오는 것이 보기 좋습니다.
그럼 main 함수를 위로 올리고 cough 함수를 내려볼까요?
실행을 해보면 오류가 발생합니다.
main 함수에서 cough() 함수를 사용했습니다.
그런데 cough함수는 아래에 있습니다.
C는 오래되었고 똑똑하지 않기 때문에 아래에 cough라는 함수가 있을 것이라 생각하지 못하는 것이죠.
여러분이 시킨대로만 행동합니다.
이 것을 해결하려면 다시 cough함수를 위로 올려야합니다.
물론 이것은 악순환의 반복일 것입니다. 영원히 새로운 함수를 위에 올릴 수 없으니까요.
그래서 다른 방법이 있습니다.
#include <stdio.h>void cough(void);
int main(void)
{
for (int i = 0; i < 3; i++)
{
cough();
}
}
void cough(void)
{
printf("cough\\n");
}
void cough(void)를 세미콜론과 함께 위로 올리는 것입니다.
마치 이전에 cough를 봤던 것처럼 C를 속이는 방법입니다.
cough함수를 전부 본 적은 없어도 이름은 본적이 있으니 main 함수에 나올 때까지 코드를 계속 읽도록 하는 것입니다.
이번에는 cough 함수를 좀 더 다재다능하게 만들어 원하는 횟수만큼 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");
}
}
여기서 맨 아래의 void cough(int n){...} 은 cough 라는 이름의 함수를 우리가 직접 정의한 것입니다.
cough( ) 안의 int n 은 함수가 입력값을 받아서 int 형식을 갖는 n이라는 변수에 저장하겠다는 의미입니다.
그리고 { } 안의 내용을 보면 n번 동안 cough를 출력하는 for 루프가 있습니다.
다시 main 함수 안으로 돌아가보면 cough(3) 이라는 한 줄의 코드를 통해서 3이라는 값을 cough 함수에 전달하고, 궁극적으로는 cough를 세 번 출력할 수 있게 되는 것이죠.
다만 여기서 main 함수를 우리가 정의한 cough 함수보다 위에 위치시키고 싶다면, 예시에서와 같이 void cough(int n);를 먼저 입력해서 cough 라는 함수가 정의되어있음을 알려줘야 합니다.
누군가는 cough 함수를 어떻게 정의했는지 궁금해 할 수 있지만 적어도 여러분은 전혀 알 필요가 없습니다.
그냥 누군가가 구현해 준 기능을 그대로 활용해서 여러분에게 더 흥미로운 프로그램을 만들면 됩니다.
좀 더 이해하기 쉬운 예제를 확인해봅시다.
#include <cs50.h>#include <stdio.h>int get_positive_int(void);
int main(void)
{
int i = get_positive_int();
printf("%i\\n", i);
}
int get_positive_int(void)
{
int n;
do
{
n = get_int("Positive Integer: ");
}
while (n < 1);
return n;
}
여기서 get_positive_int 함수는 CS50라이브러리(cs50.h)에 없는 함수 입니다.
아래 **int get_positive_int(void)**를 보시면 우리가 처음 보는 기능이 있습니다.
이 부분의 논리를 차근차근 확인해 보겠습니다.
여기 get_postive_int라는 함수가 있는데 입력을 받지 않았습니다.
괄호 안에 아무것도 넣을 필요가 없습니다. 아무 양의 정수나 받으면 됩니다.
하지만 이 전에 사용했던 get_int나 get_string 함수처럼 어떤 값을 받아와서 변수에 저장하는 것처럼 이 함수가 뭔가를 반환하게 하고 싶습니다.
그래서 int get_positive_int(void) 파란색 글씨는 void가 아니고 int가 됩니다.
함수 왼쪽에 있는 **단어(파란색)**는 출력의 종류를 의미 합니다.
int get_positive_int(void) 괄호 안의 **빨간색 단어(void)**는 입력의 종류를 뜻합니다.
만약 입출력이 없다면 void를 적어주시면 됩니다.
그리고 int n; 이라고 하는 처음 보는 것이 있습니다.
컴퓨터에게 n이라고 하는 변수를 달라는 일종의 힌트입니다.
그 안에 어떤 값을 저장할지 아직 모르기 때문에 그냥 int n;만 적는 것입니다.
아직은 아무것도 할달할 필요가 없습니다.
그럼 n은 쓰레기 값(Garbage Value)이라고 부르는 값을 가지게 됩니다.
n에 무엇이 들었는지는 모르지만 중요하지 않습니다. 나중에 제대로 넣으면 됩니다.
그 다음 do-while의 루프를 알아보겠습니다.
이 불리언 표현 while(n<1); 이 참일때 다음을 수행하라는 뜻입니다.
만약 n이 1보다 작다면 계속해서 질문을 반복하는 것입니다.
while을 단독으로 사용하면 while의 조건이 참이어야만 수행을 합니다.
하지만 do-while은 do에서 무조건 한 번은 먼저 수행하게 해줍니다.
물론 이것은 여러가지 표현 방법 중 하나입니다.
중첩 루프
마리오 게임에서 흔히 보는 것 처럼 화면에 여러 개의 이미지를 가로나 세로로 여러 개 이어서 출력하고 싶으면 어떻게 해야 할까요? 아래처럼 for 루프를 사용할 수 있습니다.
#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");
}
}
먼저 int n; 으로 정수 값을 갖는 변수 n을 정의합니다.
그리고 do{ …}while()을 이용해서 while( )의 조건이 만족할때 까지 get_int 함수로 사용자가 입력값을 받아 n에 저장합니다. do{ }while()을 사용하면 조건과 상관없이 최소한 한 번은 { }안의 내용을 실행할 수 있습니다.
그리고 for 루프를 두 번 중첩해서 돌면서 “#”을 출력합니다. 첫 번째 루프에서는 변수 i를 기준으로 n번 반복하고, 그 안의 내부 루프에서는 변수 j를 기준으로 n번 반복합니다. 내부 루프에서는 “#”을 출력하고, 내부 루프가 끝날 때마다 줄바꿈을 수행합니다. 따라서 최종적으로는 가로가 n개, 세로가 n개인 “#”이 출력되게 됩니다.
2.6 하드웨어의 한계
컴퓨터는 RAM(랜덤 액세스 메모리)이라는 물리적 저장장치를 포함하고 있습니다. 우리가 작성한 프로그램은 구동 중에 RAM에 저장되는데요, RAM은 유한한 크기의 비트만 저장할 수 있기 때문에 때때로 부정확한 결과를 내기도 합니다.
부동 소수점 부정확성
아래와 같이 실수 x, y를 인자로 받아 x 나누기 y를 하는 프로그램이 있다고 해봅시다.
#include <cs50.h>#include <stdio.h>int main(void)
{
// 사용자에게 x 값 받기
float x = get_float("x: ");
// 사용자에게 y 값 받기
float y = get_float("y: ");
// 나눗셈 후 출력
printf("x / y = %.50f\\n", x / y);
}
나눈 결과를 소수점 50자리까지 출력하기로 하고, x에 1을, y에 10을 입력하면 아래와 같은 결과가 나옵니다.
x: 1
y: 10
x / y = 0.10000000149011611938476562500000000000000000000000
정확한 결과는 0.1이 되어야 하지만, float 에서 저장 가능한 비트 수가 유한하기 때문에 다소 부정확한 결과를 내게 되는 것입니다.
정수 오버플로우
비슷한 오류로, 1부터 시작하여 2를 계속해서 곱하여 출력하는 아래와 같은 프로그램이 있다고 해봅시다.
#include <stdio.h>#include <unistd.h>int main(void)
{
for (int i = 1; ; i *= 2)
{
printf("%i\\n", i);
sleep(1);
}
}
우리가 변수 i를 int로 저장하기 때문에, 2를 계속 곱하다가 int 타입이 저장할 수 있는 수를 넘은 이후에는 아래와 같은 에러와 함께 0이 출력될 것입니다.
...
1073741824
overflow.c:6:25: runtime error: signed integer overflow: 1073741824 * 2 cannot be represented in type 'int'
-2147483648
0
0
...
정수를 계속 키우는 프로그램에서 10억을 넘기자 앞으로 넘어갈 1의 자리가 없어진 것입니다.
int에서는 32개의 비트가 다였기 때문입니다. 그 이상의 숫자는 저장할 수 없는 것입니다.
이런 오버플로우 문제는 실생활에서도 종종 발견됩니다.
1999년에 큰 이슈가 되었던 Y2K 문제는 연도를 마지막 두 자리수로 저장했던 관습 때문에 새해가 오면 ‘99’에서 ‘00’으로 정수 오버플로우가 발생하고, 새해가 2000년이 아닌 1900년으로 인식된다는 문제였습니다.
그리고 세계는 수백만 달러를 투자해서 프로그래머들에게 더 많은 메모리를 활용해서 이를 해결하도록 하였습니다.
이는 통찰력 부족으로 발생한 아주 현실적이고 값비싼 문제였습니다.
또한 다른 사례로 비행기 보잉 787에서 구동 후 248일이 지나면 모든 전력을 잃는 문제가 있었습니다.
왜냐하면 강제로 안전 모드로 진입하였기 때문입니다.
이는 소프트웨어의 변수가 248일이 지난 뒤에 오버플로우가되어 발생하였기 때문이었습니다.
248일을 1/100초로 계산하면 대략 2의 32제곱이 나옵니다.
보잉을 설계할때 사용한 변수보다 너무 커졌던 것입니다.
이를 해결하기 위해 주기적으로 재가동을 하여 변수를 다시 0으로 리셋했습니다.
따라서 다루고자 하는 데이터 값의 범위를 유의하며 프로그램을 작성하는 것이 중요합니다.
자료 출처 : https://www.boostcourse.org/cs112/joinLectures/41307
'코딩딩 > CS' 카테고리의 다른 글
소프트웨어 개발 생명주기 모형의 개요 (1) | 2023.09.09 |
---|---|
시스템 공학 (0) | 2023.09.09 |
시스템 공학 (0) | 2023.08.30 |
[CS50] 모두를 위한 컴퓨터 과학 Ch.03 (0) | 2023.05.20 |
[CS50] 모두를 위한 컴퓨터 과학 Ch.01 (0) | 2023.05.14 |