#include <cs50.h>
#include <stdio.h>
int main(void)
{
string name = get_string("What's your name?\n")
printf("hello, %s\n", name)
}
이 코드를 컴파일해보자. 컴파일링은 다음 명령어를 입력하면 실행된다.
clang -o hello hello.c -lcs50
make hello
컴파일은 소스코드를 오브젝트코드(기계어)로 변환시키는 과정인데, 총 4단계로 이뤄진다.
코드 맨 위의
#include <cs50.h>
#include <stdio.h>
는 cs50, stdio 라이브러리의 헤더파일들이다. 컴파일러는 이 라이브러리에 들어가서 hello.c 코드에서 사용하는 함수 프로토타입을 복사해서 붙여넣는다. 그럼 위의 #include ~ 코드는 이렇게 바뀐다. (여전히 소스코드 형태임)
string get_string(string prompt);
int printf(string format, ...);
전처리(1단계)가 완료된 코드
string get_string(string prompt);
int printf(string format, ...);
int main(void)
{
string name = get_string("What's your name?\n")
printf("hello, %s\n", name)
}
컴파일링 단계를 거치면 소스코드가 어셈블리어로 바뀐다.
main: # @main
.cfi_startproc
# BB#0:
pushq %rbp
.Ltmp0:
.cfi_def_cfa_offset 16
.Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
.Ltmp2:
.cfi_def_cfa_register %rbp
subq $16, %rsp
xorl %eax, %eax
movl %eax, %edi
movabsq $.L.str, %rsi
movb $0, %al
callq get_string
movabsq $.L.str.1, %rdi
movq %rax, -8(%rbp)
...
이 어셈블리 코드에서 pushq, movq, subq, xorl 등의 언어들은 '명령어'인데, CPU가 알아들을 수 있는 언어에 가깝다.
위의 어셈블리 코드를 머신코드로 바꾸는 과정이다.
0111111001010100111010101100001100000
1001001110101101110101101111001011110
1010100001111110011011010101001010000
...
위와 같은 머신코드로 바뀐다. 만약 컴파일할 파일이 하나뿐이라면 컴파일링은 이 단계에서 완료.
hello.c에는 cs50.c, stdio.c, hello.c(내가 작성한 코드)의 3가지 파일이 사용된다. (cf. stdio.c 안의 printf.c파일과 cs50.c 안의 get_string.c파일 사용)
이런 경우에는 컴파일링 과정에서 세 파일을 연결해야 하는데 이를 링킹이라고 한다. 즉, 기계어로 번역된 세 파일을 하나의 오브젝트 파일로 뭉치는 과정이다.
주의: 이 기능은 cs50라이브러리의 기능이라 일반적으로 사용하기엔 제한적일 수 있다. 문법 오류(syntax error)에 사용한다.
int main(void)
{
printf("Hello, world.\n");
}
이걸 컴파일하면 에러 발생. 터미널창에
help50 make buggy1
라고 치면
Asking for help...
buggy1.c:3:5: error: implicitly declaring library function 'printf' with type 'int (const char *, ...)' [-Werror,-Wimplicit-function-
declaration]
printf("Hello, world.\n");
Did you forget to #include <stdio.h> (in which printf is declared) atop your file?
이라고 도움말이 노란색으로 뜬다.
논리오류 발생시 사용한다. ex. #을 10번 출력하고 싶은데 11번 출력되는 경우
#include <stdio.h>
int main(void)
{
for(int i=0;i<=10;i++)
{
printf("i is now %i\n", i); //디버깅 장치
printf("#\n");
}
}
커맨드창에는 이런 출력물이 뜬다.
$ ./buggy2
i is now 0
#
i is now 1
#
i is now 2
#
i is now 3
#
i is now 4
#
i is now 5
#
i is now 6
#
i is now 7
#
i is now 8
#
i is now 9
#
i is now 10
#
이렇게 for 구문에서 i<=10
을 i<10
으로 바꿔야 함을 알 수 있다.
debug50 ./duggy2
입력디버그창 나옴. 여기에서 디버그창 위쪽의 두번째 버튼 클릭하면 for문을 한단계 한단계씩 실행할 수 있고, 어디에서 잘못되었는지 알 수 있음.
디버깅 종료: Ctrl+C
디버거는 프로그램을 특정 행에서 멈추고 한번에 한 행씩 프로그램을 실행할 수 있게 해주기 때문에 버그를 찾는데 도움이 된다. 중지점은 디버깅할 때 프로그램이 멈추는 특정 지점이다.
GDB는 자주 쓰는 디버거 중 하나임. C프로그램에서 GDB사용하려면 우선 make 파일명
으로 컴파일한 후 gdb 파일명
을 쳐서 디버깅을 시작함.
1. 시작
make 파일명
gdb 파일명
b #
(# 멈추고 싶은 행 번호) info b
clear [#]
r
(run)r
로 프로그램 실행시 프로그램이 중지점에서 자동으로 멈추고 프롬프트가 나타나는데 이 때 몇가지 옵션이 있음 p [var]
(print) info locals
n
(next) s
(step) c
(continue)cs50 라이브러리에 있는 도구 설명하는 섹션
check50 hello.c
style50 hello.c
코드의 디자인이 중요한 이유: 나중에 유지보수할때 코드를 이해할 수 있어야 함.
cf. 고무오리: 계속 버그가 발생하고 해결할 수 없을때. 잠시 휴식을 취하고 다른거 하다가 고무오리에게 코드를 한줄씩 설명해보자.
1GB의 RAM은 10억 byte의 정보를 저장할 수 있다. 간단하게 램 하나에 10억개의 칸이 있다고 생각해보자
다음은 각 자료형의 크기이다.
|자료형|크기|
|---|---|
|bool|1 byte|
|char|1 byte|
|int|4 bytes|
|float|4 bytes|
|long|8 bytes|
|double|8 bytes|
|string|? bytes|
각 자료형은 램에서 그 크기만큼의 공간을 차치한다. ex. bool=1칸, double=8칸
다음 코드를 보자
char c1='H'; //규칙: char 입력할때는 작은따옴표('')를 사용한다.
char c2='I';
char c3='!';
printf("%c %c %c\n", c1, c2, c3);
-> 결과: H I ! //중간에 띄어쓰기 됨
H, I, !는 램에 숫자의 형태로 저장된다(ASCII의 특징이다. 각 문자에 숫자를 할당하는 것). H, I, !에 어떤 숫자가 할당되었는지는 다음과 같이 알 수 있다.
char c1='H';
char c2='I';
char c3='!';
printf("%i %i %i\n", (int) c1, (int) c2, (int) c3); //앞에 (int) 붙이는 것을 '형변형(caste)'라고 한다.
-> 결과: 72 73 33
cf. 형변환: 하나의 자료형을 다른 종류로 바꾸는 행위. clang은 굳이 변수이름 앞에 (int) 등을 붙이지 않아도 알아서 바꿔줌
72, 73, 33은 실제로 RAM에는 이진수의 형태로 저장된다 (01001000, 01001001, 00100001)
과제 점수의 평균을 구하는 프로그램을 작성해 보자
...
int scores[3]; //scores는 하나의 변수 안에 3개 값을 저장하는 배열변수임.
scores[0]=90; //배열의 첫번째 값은 0번째 자리에 저장된다.
scores[1]=70;
scores[2]=55;
printf("avg=%i\n", (scores[0]+scores[1]+scores[2])/3);
#include <stdio.h>
const int N = 3 //전역변수
int main(void)
{
int scores[N];
scores[0]=90;
...
}
루프와 함수를 선언해 좀 더 동적인 프로그램을 작성할 수 있다.
#include <stdio.h>
#include <cs50.h>
// 함수 스테레오타입
float average(int length, int array[]);
// 본문: 배열에 점수 입력하고 평균내기.
int main(void)
{
int n = get_int("Number of scores?"); //시험점수 갯수 입력
int scores[n]; //주석1
for (int i=0; i < n; i++) //배열 안에 점수 입력
{
scores[i]=get_int("Score=");
}
printf("Average = %.1f\n",average(n,scores)); //주석2
}
// 평균 계산 함수 정의
float average(int length, int array[]) //주석3
{
int sum = 0;
for (int i=0; i<length; i++)
{
sum = sum + array[i];
}
return (float)sum/(float)length; //주석4
}
int scores[n]
: 배열에 정수형 데이터 입력 예정이므로 꼭 int 붙여서 선언하기!점수의 평균을 구하는 예제에서, 동적으로 작성한 코드는 그렇지 않은 코드에 비해 어떤 장단점이 있을까요?
scores[0]=90
, scores[1]=95
,... 이런식으로 데이터를 입력하는 것보다 사용자에게 데이터를 입력받아 배열에 값을 저장하는 것이 코드가 간결하고 효율적이다. int ages[]
와 같이 배열에 저장되는 자료의 유형과 배열의 이름을 지정한다. ages[]
의 대괄호 안에는 배열의 크기가 들어간다. 예를 들어 ages[5]
안에는 5개의 데이터가 들어갈 수 있다. for(int i=0; i<5; i++)
{
ages[i] =+ 2
}
이러면 ages의 각 나이를 2살씩 손쉽게 올릴 수 있다.
문자열(String)은 char로 구성된 배열이다. 즉,
|name|E|l|e|n|a|\0|
|---|---|---|---|---|---|---|
||name[0]|name[1]|name[2]|name[3]|name[4]|name[5]|
코드로 증명해보자
#include <stdio.h>
#include <cs50.h>
int main(void)
{
string name = "Elena";
for (int i=0; i<5; i++)
{
printf("name[%i]= %c\n", i, name[i]);
}
printf("name[5]= %i", name[5]);
}
코드 실행 결과는 다음과 같다
$ ./string
name[0]= E
name[1]= l
name[2]= e
name[3]= n
name[4]= a
name[5]= 0
사용자로부터 문자열을 입력받아 한 글자씩 출력하는 프로그램을 만들어보자
s[i] != '\0'
여부 검사해 출력하기#include <stdio.h>
#include <cs50.h>
//문자열을 출력해보자 (ver1. 각 문자가 \0인지 아닌지 검사한 후 출력)
int main(void)
{
string s = get_string("Input: ");
printf("Output: ");
for (int i = 0; s[i] != '\0'; i++) //\0을 한 글자로 인식하기 때문에 작은 따옴표로 감싼다.
{
printf("%c", s[i]);
}
printf("\n");
}
#include <stdio.h>
#include <cs50.h>
#include <string.h> //주석1
int main(void)
{
string s = get_string("Input: ");
printf("Output: ");
for (int i = 0; i < strlen(s); i++) //주석2
{
printf("%c", s[i]);
}
printf("\n");
}
#include <stdio.h>
#include <cs50.h>
#include <string.h>
int main(void)
{
string s = get_string("Input: ");
printf("Output: ");
for (int i = 0, n = strlen(s); i < n; i++) //주석1
{
printf("%c", s[i]);
}
printf("\n");
}
입력받은 문자열을 대문자로 바꿔주는 프로그램을 작성해보자.
#include <cs50.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
string s = get_string("Before: ");
printf("After: ");
for (int i = 0, n=strlen(s); i<n; i++)
{
if(s[i]>='a' && s[i]<='z')
{
printf("%c", s[i]-32);
}
else
{
printf("%c", s[i]);
}
}
printf("\n");
}
#include <stdio.h>
#include <cs50.h>
#include <ctype.h>
#include <string.h>
int main(void)
{
string s = get_string("Input: ");
printf("Output: ");
for (int i=0, n=strlen(s); i<n; i++)
{
printf("%c", toupper(s[i]));
}
printf("\n");
}
터미널창에 입력했던 것들을 기억할 것이다.
1. clang -o hello hello.c
2. make hello
3. help50 hello
여기에서 명령어는 clang, make, help이고, 명령행 인자는 -o hello, hello.c, hello가 된다. 즉, 명령어 뒤에 딸려서 입력되는 것들이 명령행인자다.
여태까지 코드 본문은 int main(void)함수 안에 입력했었다. 이 때 void는 명령행인자가 없음을 뜻한다. 즉, 괄호 안에 인자를 넣으면 그것이 명령행 인자가 된다.
1. string argv[]: 문자열로 된 인자의 배열을 뜻함
2. int argc: argv 배열 안 인자의 갯수
이제 새로 배운 내용을 적용해보자
#include <stdio.h>
#include <cs50.h>
int main(int argc, string argv[]) //명령행인자 넣음
{
if (argc == 2) //명령행 인자를 입력한 경우
{
printf("hello, %s\n", argv[1]); //"hello, 명령행 인자" 출력
}
else //명령행 인자를 입력하지 않은 경우
{
printf("hello, world\n");
}
}
터미널창의 결과는 다음과 같다.
$ ./argv 지혜 -> 명령행 인자로 '지혜'를 입력했음
hello, 지혜
$ ./argv -> 명령행 인자를 입력하지 않았음.
hello, world
#include <stdio.h>
#include <cs50.h>
int main(int argc, string argv[])
{
if (argc != 2) //주석 1
{
printf("missing command-line argument\n");
return 1; //main함수가 1 반환: 뭔가 문제가 있다는 뜻
}
printf("hello, %s\n", argv[1]); //
return 0; //main함수가 0 반환: 문제가 없다는 뜻
}