본문 바로가기
STUDY 기록/CS50

[CS50] C언어 기초, 형식 지정자, 사용자 정의 함수, 하드웨어의 한계

by TREEKIM 2022. 6. 21.

C언어

하나하나  설명하자면 int main(void) 는 스크래치의 “초록색 깃발을 클릭했을 때” 블록과 같은 역할을 합니다.즉 '시작한다'의 의미를 가지고 있다고 보면 됩니다. 앞으로 우리가 작성할 코드 모두는 이 int main(void) { }의 중괄호 사이에 작성하게 될 것 입니다.

printf(“hello, world\n”) 은 스크래치의 “‘hello, world’라고 말하기” 블록과 같은 역할을 합니다. 글자나 단어, 문장을 적을 때는 언제나 텍스트에 " " 쌍따옴표로 감싸야 합니다. 그리고 우리가 일상에서 문장의 끝에 마침표(.)를 붙이는 것 처럼 C에서는 세미콜론(;)을 붙여야 합니다.

#include <stdio.h>는 “stdio.h”라는 이름의 파일을 찾아서 “printf” 함수에 접근할 수 있도록 해줍니다.

 

컴파일러


우리가 직접 작성한 코드는 “소스 코드” 라고 불립니다. 이를 2진수로 작성된 “머신 코드”로 변환해야 컴퓨터가 이해할 수 있습니다. 이런 작업을 컴파일러라는 프로그램이 수행해줍니다.

터미널창의 명령어 프롬프트에서 “$” 기호 옆에우리가 원하는 명령어를 입력하면 됩니다.

clang hello.c 라는 명령어는 “clang” 이라는 컴파일러로 “hello.c”라는 코드를 컴파일하라는 의미입니다. 

그 결과 a.out 이라는 파일이 생성됩니다. ./a. out 이라는 명령어를 실행하면 컴퓨터가 현재 디렉토리에 있는 a.out이라는 프로그램을 실행하게 해줍니다. (./a. out에서 제일 앞에 있는 .은 지금 있는 현재 폴더를 나타냅니다.)

참고) 왜 저는 줄바꿈 할때 \(백슬래시)가 ₩(원화)로 보이는 것이죠?
-> 해당 문제는 '한글 윈도우' 운영체제에서만 생기는 현상입니다.
한글 윈도우에서는 \를 ₩로 표시를 해주기 때문입니다. 따라서 ₩로 표시가 되어도 문제 없습니다.

내용 출처 : boostcourse 모두를 위한 컴퓨터 과학 (CS50 2019)

형식지정자

사용자의 이름을 받아서 저장할 변수를 스크래치와 같이 answer이라고 정해보겠습니다. 이때 변수는 xyz, name 등과 같이 여러분 마음대로 정하셔도 무관합니다. 하지만 여기서 유의해할 점은 C는 오래된 언어이기 때문에 변수가 저장하는 데이터의 종류를 아주 정확하게 명시해줘야 합니다그래서 우리는 저장하고자 하는 값의 종류가 문자열(string)이라는 것을 알려줘야 합니다.

이때 string을 형식지정자라고 합니다.

왜냐하면 너무나 당연하게 이름은 숫자가 아닌 문자이기 때문에 컴퓨터에게 "answer에 들어갈 것들은 문자야!"라고 말해주는 것이죠

우리는 answer이라는 변수에 들어있는 이름을 출력을 해야하기 때문에 %를 사용해 줍니다. 이 때도 어떤 종류의 인자를 받는지 말해줘야 합니다. 우리는 이름이라는 문자열을 받기때문에 string에서의 s %뒤에 붙여서 인자를 받아줍니다.

그래서 최종적으로는 printf("hello, %s\n", answer);이 되는 것입니다.

여러가지 데이터 타입 마다 사용되는 형식 지정자

  • %c : char
  • %f : float, double
  • %i : int
  • %li : long
  • %s : string

Make

$ clang -o string string.c -lcs50

터미널창에 아래 명령어를 입력하여 컴파일을 할 수 있습니다.

여기서 -o string 은 string.c 를 string.out 이라는 머신코드로 저장하도록 하는 명령어입니다.

-lcs50은 “link”라는 의미를 지닌 -l 이라는 인자에 우리가 추가로 포함한 “cs50” 파일을 합친 것입니다. 이를 통해 컴파일시 cs50 파일을 연결하도록 알려줄 수 있습니다.

다소 복잡한 이런 과정 대신에, 아래 make 명령어를 통해 간단하게 컴파일을 수행할 수도 있습니다.

$make string

이와 같이 작성한 코드를 컴파일 하고 실행하면, 사용자에게 입력값을 받고 문장 내에 포함하여 출력하는 프로그램이 됩니다.

기타 연산자 및 주석

그 외에도 아래 목록과 같이 다양한 수학 연산자, 논리 연산자, 주석 등이 기호로 정의되어 있습니다.

  • +:  더하기
  • -: 빼기
  • *: 곱하기
  • /: 나누기
  • %: 나머지
  • &&: 그리고
  • ||: 또는
  • //: 주석

참고) #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 등의 함수로 좀 더 쉽게 입력을 받아 보았는데요, 일반적으로 사용되는 입력을 받는 함수도 아래의 참고자료를 통해 학습하실 수 있게 준비해 두었으니 참고하시면 좋을 것 같습니다.

 

사용자 정의 함수

#include <stdio.h>

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

단순히 printf 를 세 번 반복하면 되지만, 동일한 작업을 반복하는 것이기 때문에 사용자 정의 함수를 이용하면 아래 코드와 같이 더 단순화 할 수 있습니다.

#include <stdio.h>

int main(void)
{
    for (int i = 0; i < 3; i++)
    {
        printf("cough\n")
    }
}

그럼 이번에는 우리만의 함수를 만들어 볼까요?

#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' 등의 함수는 우리가 직접 구현할 필요가 없었습니다. 과거의 어떤 사람들이 모두 구현해두었기 때문입니다.

하지만 여기에도 문제가 있습니다. 함수를 1개가 아닌 여러개를 만들수록 main 함수는 아래로 내려가기 때문입니다.

중요한 것이 아래에 있는 것보다 바로 나오는 것이 보기 좋습니다. 그럼 main 함수를 위로 올리고 cough 함수를 내려볼까요? 실행을 해보면 오류가 발생합니다. main 함수에서 cough() 함수를 사용했습니다.

그런데 cough함수는 아래에 있습니다. C는 오래되었고 똑똑하지 않기 때문에 아래에 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(void)를 세미콜론과 함께 위로 올리는 것입니다. 마치 이전에 cough를 봤던 것처럼 C를 속이는 방법입니다.
cough함수를 전부 본 적은 없어도 이름은 본적이 있으니 main 함수에 나올 때까지 코드를 계속 읽도록 하는 것입니다.

여기서 맨 아래의 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_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에 무엇이 들었는지는 모르지만 중요하지 않습니다. 나중에 제대로 넣으면 됩니다.

중첩 루프

#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개인 “#”이 출력되게 됩니다.

부동 소수점 부정확성

컴퓨터는 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으로 리셋했습니다. 

따라서 다루고자 하는 데이터 값의 범위를 유의하며 프로그램을 작성하는 것이 중요합니다.

댓글