June 14, 2023
#include <stdio.h>
int main(void)
{
printf("hello, world\n");
}
printf는 출력을 담당하는 함수입니다.
printf 함수를 사용하기 위해서는 stdio.h 라이브러리가 필요합니다.
이 파일에는 printf 함수의 프로토타입이 있어서 Clang 컴파일러가 프로그램을 컴파일할때 printf가 무엇인지 알려주는 역할을 합니다.
코드를 clang hello.c로 컴파일하고 ./a.out 명령으로 프로그램을 실행할 때 이 과정은 컴퓨터가 이해하는 0과 1로 가득찬 파일 a.out을 생성하여 실행 가능하게 합니다.
clang -o hello hello.c
CS50 라이브러리를 사용한 코드
#include <cs50.h>
#include <stdio.h>
int main(void)
{
string name = get_string("what's your name?\n");
printf("hello, $s\n", name);
}
CS50 라이브러리를 사용한 프로그램을 컴파일 할때는 clang에 또 하나의 프로그램(-lcs50)이 필요했습니다.
clang -o hello hello.c -lcs50
make나 clang을 사용해서 프로그램을 실행할 때 아래 네 개의 단계를 거칩니다.
그 중 첫 번째 단계는 전처리인데, 전처리기에 의해 수행됩니다. # 으로 시작되는 C 소스 코드는 전처리기에게 실질적인 컴파일이 이루어지기 전에 무언가를 실행하라고 알려줍니다.
전처리기가 전처리한 소스 코드를 생성하고 나면 그 다음 단계는 컴파일입니다. 컴파일러라고 불리는 프로그램은 C 코드를 어셈블리어라는 저수준 프로그래밍 언어로 컴파일합니다.
어셈블리는 C보다 연산의 종류가 훨씬 적지만, 여러 연산들이 함께 사용되면 C에서 할 수 있는 모든 것들을 수행할 수 있습니다. C 코드를 어셈블리 코드로 변환시켜줌으로써 컴파일러는 컴퓨터가 이해할 수 있는 언어와 최대한 가까운 프로그램으로 만들어 줍니다.
컴파일이라는 용어는 소스 코드에서 오브젝트 코드로 변환하는 전체 과정을 통틀어 일컫기도 하지만, 구체적으로 전처리한 소스 코드를 어셈블리 코드로 변환시키는 단계를 말하기도 합니다.
소스 코드가 어셈블리 코드로 변환되면, 다음 단계인 어셈블 단계로 어셈블리 코드를 오브젝트 코드로 변환시키는 것입니다.
컴퓨터의 중앙처리장치가 프로그램을 어떻게 수행해야 하는지 알 수 있는 명령어 형태인 연속된 0과 1들로 바꿔주는 작업이죠. 이 변환작업은 어셈블러라는 프로그램이 수행합니다. 소스 코드에서 오브젝트 코드로 컴파일 되어야 할 파일이 딱 한 개라면, 컴파일 작업은 여기서 끝이 납니다.
그러나 그렇지 않은 경우에는 링크라 불리는 단계가 추가됩니다.
만약 프로그램이 (math.h나 cs50.h와 같은 라이브러리를 포함해) 여러 개의 파일로 이루어져 있어 하나의 오브젝트 파일로 합쳐져야 한다면 링크라는 컴파일의 마지막 단계가 필요합니다. 링커는 여러 개의 다른 오브젝트 코드 파일을 실행 가능한 하나의 오브젝트 코드 파일로 합쳐줍니다.
예를 들어, 컴파일을 하는 동안에 CS50 라이브러리를 링크하면 오브젝트 코드는 GetInt()나 GetString() 같은 함수를 어떻게 실행할 지 알 수 있게 됩니다.
→ 이 네 단계를 거치면 최종적으로 실행 가능한 파일이 완성됩니다.
버그(bug)는 코드에 들어있는 오류입니다. 버그로 인해 프로그램의 실행에 실패하거나 프로그래머가 원하는 대로 동작하지 않게 됩니다. 버그를 만들고 싶지 않겠지만 모든 프로그래머들은 버그와 마주하게 되어있습니다.
디버깅(debugging)은 코드에 있는 버그를 식별하고 고치는 과정입니다. 프로그래머는 디버거라고 불리는 프로그램을 사용하여 디버깅을 하게 됩니다.
int main(void)
{
printf("hello, world\n");
}
// make 프로그램을 이용하여 컴파일해보면
// “implicitly declaring library function 'printf'”
// 이라는 에러 메시지가 발생.
강의에선 help50 프로그램을 사용합니다.
help50 make 파일이름
→ printf 함수를 사용하기 위해 stdio.h
라이브러리를 추가해야합니다.
다음과 같은 문제가 발생할 땐 어떻게 해결할까요?
#include <stdio.h>
int main(void)
{
for (int i = 0; i <= 10; i++)
{
printf("#\n");
}
}
// -> #이 11개가 나옵니다.
디버깅을 다음과 같이 출력해서 확인할 수 있습니다.
#include <stdio.h>
int main(void)
{
for (int i = 0; i <= 10; i++)
{
printf("i is now %i: ", i);
printf("#\n");
}
}
// 이를 확인하니 i < 10 으로 바꾼다면 원하는 값이 나옵니다.
C에는 아래와 같은 여러 자료형이 있고, 각각의 자료형은 서로 다른 크기의 메모리를 차지합니다.
문자
#include <stdio.h>
int main(void)
{
char c1 = 'H';
char c2 = 'I';
char c3 = '!';
printf("%c %c %c\n", c1, c2, c3);
printf("%i %i %i\n", c1, c2, c3);
}
// H I !
// 72 73 33
정수
#include <cs50.h>
#include <stdio.h>
int main(void)
{
// Scores
int score1 = 72;
int score2 = 73;
int score3 = 33;
// Print average
printf("Average: %i\n", (score1 + score2 + score3) / 3);
}
// 59
배열은 같은 자료형의 데이터를 메모리상에 연이어서 저장하고 이를 하나의 변수로 관리하기 위해 사용됩니다.
다음 코드는 배열을 사용해서 바꾼 코드입니다.
#include <cs50.h>
#include <stdio.h>
int main(void)
{
// Scores
int scores[3];
scores[0] = 72;
scores[1] = 73;
scores[2] = 33;
// Print average
printf("Average: %i\n", (scores[0] + scores[1] + scores[2]) / 3);
}
// 59
#include <cs50.h>
#include <stdio.h>
const int N = 3;
int main(void)
{
// 점수 배열 선언 및 값 저장
int scores[N];
scores[0] = 72;
scores[1] = 73;
scores[2] = 33;
// 평균 점수 출력
printf("Average: %i\n", (scores[0] + scores[1] + scores[2]) / N);
}
만약 N이 고정된 값(상수)이라면 그 값을 선언할 때 const를 앞에 붙여서 전역 변수, 즉 코드 전반에 거쳐 바뀌지 않는 값임을 지정해줄 수 있습니다.
관례적으로 이런 전역 변수의 이름은 대문자로 표기 합니다.
→ 수정해야 하는 코드가 줄어들었지만 여전히 배열의 인덱스마다 점수를 지정해야합니다.
#include <cs50.h>
#include <stdio.h>
float average(int length, int array[]);
int main(void)
{
// 사용자로부터 점수의 갯수 입력
int n = get_int("Number of scores: ");
// 점수 배열 선언 및 사용자로부터 값 입력
int scores[n];
for (int i = 0; i < n; i++)
{
scores[i] = get_int("Score %i: ", i + 1);
}
// 평균 출력
printf("Average: %.1f\n", average(n, scores));
}
//평균을 계산하는 함수
float average(int length, int array[])
{
int sum = 0;
for (int i = 0; i < length; i++)
{
sum += array[i];
}
return (float) sum / (float) length;
}
C의 배열은 배열의 크기를 저장하지 않습니다.
→ 배열의 크기를 매개변수로 받아야 합니다.
배열의 크기를 사용자에게 직접 입력 받고, 배열의 크기만큼 루프를 돌면서 각 인덱스에 해당하는 값을 역시 사용자에게 동적으로 입력 받아 저장합니다.
그리고 average 라는 함수를 따로 선언하여 평균을 구합니다.
average 함수는 length 와 array[], 즉 배열의 길이와 배열을 입력으로 받습니다. 함수 안에서는 배열의 길이만큼 루프를 돌면서 값의 합을 구하고 최종적으로 평균값을 반환합니다.
이와 같은 방법을 통해서 임의의 점수 개수와 점수 배열에 대해서 동적으로 평균값을 구하는 프로그램을 작성할 수 있습니다.
우리가 여지껏 사용한 문자열(string) 자료형의 데이터는 사실 문자(char) 자료형의 데이터들의 배열이었습니다.
string s = “HI!”; 과 같이 문자열 s가 정의되어 있다고 생각해봅시다.
s는 문자의 배열이기 때문에 메모리상에 아래 그림과 같이 저장되고, 인덱스로 각 문자에 접근할 수 있습니다.
string(문자열)은 문자 갯수(char)만큼 메모리를 저장합니다.
여기서 가장 끝의 \0
은 문자열의 끝을 나타내는 널 종단 문자입니다.
단순히 모든 비트가 0인 1바이트를 의미합니다. (0000 0000
)
string names[4];
names[0] = "EMMA";
names[1] = "RODRIGO";
names[2] = "BRIAN";
names[3] = "DAVID";
printf("%s\n", names[0]);
printf("%c%c%c%c\n", names[0][0], names[0][1], names[0][2], names[0][3]);
// EMMA
// EMMA
printf("%c%c%c%c%i\n", names[0][0], names[0][1], names[0][2], names[0][3], names[0][4]);
// EMMA0
위 코드는 실제 메모리상에 저장된 예시처럼 표현됩니다.
간단하게 for 루프를 통해 문자열의 인덱스를 하나씩 증가시켜가면서 해당하는 문자를 출력하면 될텐데요, 문자열의 끝은 어떻게 알 수 있을까요?
한가지 방법은 해당하는 인덱스의 문자가 널 종단 문자, 즉 \0
와 일치하는지 검사하는 것입니다.
즉, s라는 문자열이 있다고 할 때
for (int i = 0; s[i] != ‘\0’; i++) { ..}
과 같은 루프를 사용하면 됩니다. 하지만 아래 코드와 같이 strlen() 이라는 함수를 사용할 수도 있습니다.
#include <cs50.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
string s = get_string("Input: ");
printf("Output:\n");
// for (int i = 0, s[i] != '\0'; i++) -> 널 종단 문자로 검사
for (int i = 0, n = strlen(s); i < n; i++)
{
printf("%c\n", s[i]);
}
}
i 와 n은 같은 타입이므로 n도 타입을 선언할 필요가 없습니다.
strlen은 문자열의 길이를 알려주는 함수로, string.h 라이브러리 안에 포함되어 있습니다.
→ 일일이 널 종단 문자를 검사하지 않아도 됩니다.
다음은 문자열을 입력받아 대문자로 바꿔주는 프로그램입니다.
#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); // A = 65 a = 97
}
else
{
printf("%c", s[i]);
}
}
printf("\n");
}
이와 동일한 작업을 수행하는 함수가 ctype 라이브러리에 있는 toupper
함수가 있습니다. 다음과 같이 코드를 작성할 수 있습니다.
#include <cs50.h>
#include <ctype.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++)
{
printf("%c", toupper(s[i]));
}
printf("\n");
}
make나 clang과 같은 프로그램을 실행할 때 컴파일하고자 하는 코드 외에도 컴파일 후 저장하고자 하는 파일명과 같이 추가적인 정보를 함께 줄 수도 있습니다. 이런 정보들을 명령행 인자 라고 부릅니다.
clang -o hello # -o 가 명령형 인자
C에서의 main을 들여다보기
#include <cs50.h>
#include <stdio.h>
int main(int argc, string argv[])
{
if (argc == 2)
{
printf("hello, %s\n", argv[1]);
}
else
{
printf("hello, world\n");
}
}
여기서 첫번째 변수 argc는 main 함수가 받게 될 입력의 개수입니다.
그리고 argv[]는 그 입력이 포함되어 있는 배열입니다. 프로그램을 명령행에서 실행하므로, 입력은 문자열로 주어집니다. 따라서 argv[]는 string 배열이 됩니다.
argv[0]는 기본적으로 프로그램의 이름으로 저장됩니다. 만약 하나의 입력이 더 주어진다면 argv[1]에 저장될 것입니다.
예를 들어 위 프로그램을 arg.c
라는 이름으로 저장하고 컴파일 한 후 .**/argc**
로 실행해보면 hello, world
라는 값이 출력됩니다.
명령행 인자에 주어진 값이 프로그램 이름 하나밖에 없기 때문입니다.
하지만 ./argc David
로 실행해보면 hello, David
라는 값이 출력됩니다.
명령행 인자에 David라는 값이 추가로 입력되었고, 따라서 argc 는 2, argv[1] 은 “David”가 되기 때문입니다.