[CS50] 2. C언어

찐새·2022년 2월 14일
0

CS50

목록 보기
2/6
post-thumbnail

2. C언어


(1) C언어 기초

내 평생 살면서 C언어는 손에 댈 일이 없을 줄 알았는데, 《부스트코스》의 'CS50' 강의 2일 차 주제는 C언어였다. 마냥 어려울 거라고 생각했는데, 막상 기초는 다른 프로그래밍 언어 배울 때와 별 차이가 없었다.

hello.c

#include <stdio.h> 
int main(void) { printf("hello world\n"); }
가장 먼저 세상에 인사하는 코드다. 위에서부터 살펴 보면, #include <stdio.h>는 기본적으로 사용할 함수가 들어 있는 C의 패키지를 호출한다. int main(void) { }로 코드 입력 장소를 초기화하고, 내부에 출력하고자 하는 문구를 printf( ) 함수 안에 넣는다. 마지막으로 ;(세미콜론)으로 닫아주면 "hello world"가 출력되는 c언어 파일이 생성된다. \n은 Escape 문자로, 개행을 의미한다. 개행은 해도 되고 안 해도 되는데, 출력 후 가독성을 위해 적는 것이 낫다.

이렇게 생성된 파일은 '소스코드'로 우리(인간)이 이해는 언어로 구성되어 있다. 실제로 컴퓨터가 실행하게 만드려면 Compile(컴파일)이라는 단계가 필요하다.

컴파일러의 위치

컴파일은 명령어 '소스코드'를 기계어인 '머신코드'로 번역하는 작업이다. 1강에서 나왔듯이 기계는 0과 1의 조합으로 이해하기 때문에 컴파일 작업이 수반되어야 실행할 수 있다.

Terminal

$ clang hello.c
$ ./a.out
hello world
clang으로 컴파일하면 기본적으로 a.out이라는 실행파일이 생성되고, 현재 디렉토리를 나타내는 ./과 함께 실행파일을 호출하면 소스코드에 구현된 기능이 실행된다. $ clang -o hello hello.c와 같이 -o 옵션을 활용해 임의로 파일명을 지정할 수도 있다. 그러나 이러한 옵션들을 일일이 붙이다 보면 명령어가 점점 길어진다. 그래서 간단하게 컴파일해주는 명령어도 존재한다.

Terminal

$ make hello
clang -fsanitize=signed-integer-overflow -fsanitize=undefined -ggdb3 -O0 -std=c11 -Wall -Werror -Wextra -Wno-sign-compare -Wno-unused-parameter -Wno-unused-variable -Wshadow hello.c -lcrypt -lcs50 -lm -o hello
$ ./hello
hello world
대신 컴파일하려는 소스코드 파일명과 make의 파일명은 동일해야 한다. 그러면 컴퓨터가 알아서 같은 이름을 가진 .c를 찾아 컴파일하여 output을 만들어줄 것이다.

그 외 자잘한 명령어도 있으니 참고하면 좋다.

Terminal

$ ls // 현재 디렉토리 리스트
a.out* hello* hello.c // '*'은 실행 가능, 머신코드, 소스코드 구분 가능

$ rm a.out // 파일 삭제
rm: remove regular file 'a.out'? y // y or n
$ ls
hello* hello.c

$ cd 디렉토리 // 해당 디렉토리로 이동
$ cd ../ // 상위 디렉토리로 이동
$ clear // 터미널 내용 지움

(2) 문자열

문자의 배열인 문자열도 출력할 수 있다.

string.c

#include <cs50.h>
#include <stdio.h> 
int main(void) { string answer = get_string("What's your name?\n"); printf("hello, %s\n", answer); }
다만, hello.c와 약간 코드가 다른데, <cs50.h> 패키지를 불러온다. main 안에 있는 get_string( )은 기본 패키지에 포함되어 있지 않기 때문에 해당 함수를 가진 패키지를 호출해야 동작한다. 출력에 있는 %s형식지정자로, 출력하고자 하는 변수가 어떤 자료형인지 알려주는 역할을 한다. 자료형에 따른 형식지정자는 (4) 자료형, 형식 지정자, 연산자에서 정리할 예정이다.

기본 패키지 외에 하나가 더 늘었기 때문에 컴파일 명령어에도 약간의 변화가 생긴다. 이때, 앞서 언급한 make가 제 역할을 한다.

Terminal

$ clang -o string string.c -lcs50
$ make string
clang -fsanitize=signed-integer-overflow -fsanitize=undefined -ggdb3 -O0 -std=c11 -Wall -Werror -Wextra -Wno-sign-compare -Wno-unused-parameter -Wno-unused-variable -Wshadow string.c -lcrypt -lcs50 -lm -o string
패키지가 늘어날수록 컴파일 명렁어에 붙일 꼬리도 늘어나는 셈인데, make 하나면 깔끔하게 해결된다. 컴파일한 'string'을 실행한 결과는 아래와 같다.

Terminal

$ ./string
What's your name?
Real-Bird
hello, Real-Bird

(3) 조건문과 루프

특정한 조건일 때만 앞선 코드를 실행할 수도 있다. 이때 사용하는 명령어를 조건문이라고 한다. '만약에 ~라면 A를 한다'의 표현이다.

ifelse.c

#include <cs50.h>
#include <stdio.h> 
int main(void) { int x = get_int("x: "); int y = get_int("y: "); if (x < y) //1번 조건 { printf("x is less than y\n"); //1번 결과 } else if (x > y) //2번 조건 { printf("x is not less than y\n"); //2번 결과 } else //나머지 조건 { printf("x is equal to y\n"); //나머지 결과 } }

Terminal

$ ./ifelse
x: 3
y: 5
x is less than y
if는 1번 조건을 의미하고, else if 2번 조건을, else는 나머지 조건을 의미한다. 각각의 조건은 참일 때 중괄호 속 결과를 출력한다.

루프는 반복문을 의미하며 기능을 여러 번 되풀이하게 해준다.

ifelse.c

#include <cs50.h>
#include <stdio.h> 
int main(void) { int i = 0; while (i < 50) { printf("hello world\n"); i++; } for (int i = 0; i < 50; i++;) { printf("hello world\n"); } }
while( )문은 괄호 속 조건이 참일 경우 무한히 반복되는 명령어다. 위 코드에서는 i가 50보다 작을 때까지, 즉 0~49까지 i의 크기를 확인하면서 "hello world"를 출력한다. i가 50이 되는 순간, 조건은 거짓이 되면서 반복을 종료한다.

for( ; ; )문은 괄호에 3개의 인자가 들어간다. '반복의 시작점, 반복 횟수, 시작점 증감'을 넣어 원하는 만큼 반복한다. 여기서도 while문과 마찬가지로 50번 반복 후 종료한다.

(4) 자료형, 형식 지정자, 연산자

C언어에서는 변수 선언 시 자료형을 함께 선언해야 한다. 앞선 코드에서도 숫자라면 'int', 문자라면 'string' 등 변수 이름 앞에 함께 적었다. 그 이유는 변수의 메모리 크기가 유한하기 때문에, 한정된 기억 공간을 효율적으로 활용하기 위함이다. 자료형의 종류는 다음과 같다.
자료형 의미 예시
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부동소수점을 포함한 더 큰 실수3.14, 0.0, -28.56
출처: 《부스트코스》 CS50 2-4
변수를 출력하려면 각각의 데이터 타입에 맞는 형식 지정자를 사용해야 한다.
형식 지정자 자료형
%cchar
%ffloat, double
%iint
%lilong
%sstring
출처: 《부스트코스》 CS50 2-4
자료형과 형식 지정자를 알았으니 연산자를 이용해 간단한 계산 문제를 출력해보자. 연산자는 말 그대로 변수와 변수를 연산해주는 역할을 한다. 더하기, 빼기, 곱하기, 나누기 등등.

calculate.c

#include <cs50.h>
#include <stdio.h> 
int main(void) { int x = get_int("x: "); int y = get_int("y: ");
printf("x + y = %i\n", x + y); // 더하기 printf("x - y = %i\n", x - y); // 빼기 printf("x * y = %i\n", x * y); // 곱하기 printf("x / y = %i\n", x / y); // 나누기 printf("x %% y = %i\n", x % y) // 나머지 }

Terminal

$ ./calculate
x: 5
y: 10
x + y = 15
x - y = -5
x * y = 50
x / y = 0
x % y = 5
논리 연산자 &&(그리고, and)||(또는, or)도 있고, 조건문에서 사용한 비교 연산자도 있다.

(5) 사용자 정의 함수, 중첩 루프

때로는 패키지가 제공하는 함수로는 내가 원하는 기능을 구현하지 못할 수 있다. 나에게 3번 인사하는 함수를 어디서 만들어놨겠는가. 사용자 정의 함수는 이럴 때 사용한다. 문자 그대로, 사용자인 내가 임의로 정의해 사용하는 함수인 것이다.

hellome.c

#include <stdio.h> 
void hellome(int n);
int main(void) { hellome(3); }
void hellome(int n) { for (int i = 0; i < n; i++) { printf("hello, Real-Bird!\n"); } }

Terminal

$ ./hellome
hello, Real-Bird!
hello, Real-Bird!
hello, Real-Bird!
여기에는 몇 가지 규칙이 있다. C언어는 정직한 로직이라 위에서 아래로만 읽는다. 함수를 메인 위에 선언해두었다면 실행에 지장이 없겠지만, 만약 가독성을 목적으로 메인 아래로 빼놓는다면 에러가 발생한다. 때문에 위의 코드에서도 아무런 기능이 없는 함수를 일단 선언해 두었다.

함수의 void 부분에는 리턴값의 자료형을 부여한다. 괄호에는 함수가 받을 매개변수로, 선언된 자료형만 함수 내부로 받겠다는 의미다. 에러가 싫다면 염두에 두는 것이 좋겠다.

중첩 루프는 두 개 이상의 반복문을 겹쳐서 사용하는 방식이다. for문 속 for문이라고 보면 된다. 대표적으로 구구단을 만들 때 사용할 수 있다.

multiplication.c

#include <stdio.h> 
int main(void) { for (int i = 2; i <= 9; i++) { for (int j = 1; j <= 9; j++) { printf("%i * %i = %i\n", i, j, i * j); } printf("\n"); } }

Terminal

$ ./multiplication
2 * 1 = 2
2 * 2 = 4
2 * 3 = 6
. . .
9 * 7 = 63
9 * 8 = 72
9 * 9 = 81

(6) 하드웨어의 한계

지금까지 구현한 코드는 실행 시 모두 메모리에 적재된다. 메모리는 하드웨어의 기억 공간으로, 크기가 유한하다. 그래서 만능인 듯한 컴퓨터라도 유한한 크기의 비트를 넘어서는 데이터는 부정확한 결과가 나오기도 한다.

calcu.c

#include <stdio.h> 
int main(void) { float x = get_float("x: "); float y = get_float("y: ");

printf("%.15f\n", x / y);
}

Terminal

$ ./calcu
x: 1
y: 10
0.100000001490116
소수점을 15자리까지 늘려 확인해보니 0.1이 요상한 꼬리를 달고 있다. 이러한 현상은 float에 저장되는 부동소수점 비트가 유한하기 때문에 발생한다.

메모리가 유한해 발생하는 또 다른 상황은 오버플로우(overflow)현상이다. 이것은 자료형의 크기를 넘어서는 출력이 지속되면 더는 표현하지 못한다.

출처: 《부스트코스》 CS50 2-4

이러한 까닭으로 프로그램 설계 시 가용 메모리와 데이터 타입에 대한 깊은 고민과 통찰이 필요하다.
profile
프론트엔드 개발자가 되고 싶다

0개의 댓글