컴파일링
#include <stdio.h>
int main(void)
{
printf("hello, world\n");
}
위와 같은 코드를 살펴보면 printf라는 함수는 괄호 안에 있는 문자열을 출력하는 함수이다.
그리고 이 함수를 출력하기 위해서는 stdio.h라는 라이브러리가 필요하다.
(stdio.h는 헤더파일로 C언어로 작성되어 있으며 파일명이 .h로 끝나는 파일이다.
그래서 이후 프로그램을 컴파일하게 되면, 파일 속 들어 있는 printf 함수의 초기 버전으로 하여금 컴퓨터가 알 수 있도록 한다.)
우리는 이전에
- clang hello.c 로 컴파일을 하고,
- ./a.out 명령으로 프로그램을 실행하였다.
만약 a.out 처럼 엉뚱한 이름이 아닌 다른 이름으로 컴파일을 하고 싶다면
- clang -o hello hello.c 로 명령행 인자를 추가하고
- clang -o hello hello.c -lcs50로 CS50라이브러리에 있는 모든 0과 1들을 여기에 연결시켜줬다.
하지만 실제로 우리가 사용할 때는 더 간단한 방법을 사용할 수 있다.
make를 통해서 위와 같은 과정을 자동으로 처리할 수 있다.
각 과정은 네단계를 거치게 되는데,
- 전처리
- 컴파일링
- 어셈블링
- 링킹
전처리(Precompile)
첫단계는 전처리이다. #으로 시작되는 C 소스 코드는 전처리기에게 실질적인 컴파일이 이루어지기 전에 무언가를 실행하라고 알려준다.
예를 들어, #include는 전처리기에게 다른 파일의 내용을 포함하라고 알려준다.
컴파일(Compile)
전처리한 소스코드를 받아 어셈블리어라는 저수준 프로그래밍 언어로 컴파일을 한다.
즉 변환된 코드는 컴퓨터가 이해할 수 있는 언어와 최대한 가까운 프로그램으로 만들어 준다.
어셈블(Assemble)
어셈블리 코드를 오브젝트 코드로 변환시키는 것이다.
CPU가 프로그램을 어떻게 수행해야 할지 알 수 있는 명령어 형태인 연속된 0과 1들로 바꿔 주는 작업이다.
이 작업은 어셈블러 라는 프로그램이 수행한다.
만약 소스코드에서 오브젝트 코드로 컴파일 되어야 할 파일이 하나라면 컴파일 작업은 끝이지만,
그렇지 않으면 링크 단계까지 이어진다.
링크(Link)
만약 프로그램이 여러 개의 파일로 이루어져 있어 하나의 오브젝트 파일로 합쳐야 한다면 링크라는 마지막 처리가 필요하다.
링커는 여러개의 다른 오브젝트 코드 파일을 실행 가능한 하나의 오브젝트 코드 파일로 합쳐준다.
디버깅
버그(Bug)란 코드에 들어 있는 오류를 말한다.
디버그(Debug)란 그 오류를 식별하여 고치는 과정을 말한다.
실제로 최초의 버그는 Mark 2 시스템이라는 것을 만들면서 커다란 기계속 나방이 들어가 오류를 발생시킨 것이 원인이 되어 컴퓨터 과학자 사이에서 계속해서 전해져 오는 말이다.
컴퓨터의 연산은 너무 빨라서 우리가 실행 도중 잘못된 점을 찾아내기 어렵다.
따라서 우리는 디버거로 프로그램을 특정 행에서 멈추도록 해야 한다.
그리고 그 멈추는 특정 지점을 중지점이라고 한다.
help50 make 파일이름
오류가 발생했을 경우
위와 같이 명령어를 입력하면 컴파일시 생기는 오류를 해석해준다.
하지만 help50프로그램이 만능인 것은 아니다.
왜냐하면, 문법적 오류는 없지만 논리적 오류로 인해 발생하는 문제는 알 수 없기 때문이다.
그래서 우리는 위에서 본 중지점을 활용해 어디서 오류가 발생하는지 찾아 볼 수 있다.
debug50 파일명
실행을 해보면 오른쪽 패널을 통해 변수의 값을 확인하거나 브레이크 포인트부터 한 줄씩 코드를 실행해 볼 수 있다.
디버깅 종료는 ctrl + c를 누르면 된다.
코드의 디자인
style50을 활용하면 코드가 심미적으로 잘 작성되어 있는지 검사할 수 있다.
공백의 수나 줄바꿈은 코드의 실행에 직접적으로 영향을 주지 않지만,
그 코드를 작성하고 읽는 사람에게 영향을 주기 때문에 보기 좋게 작성할 필요가 있다.
예를 들면
#1
for (int i = 0; i <= 10; i++)
{
printf("#\n");
}
#2
for (int i = 0; i <= 10; i++){
printf("#\n");
}
#3
for (int i = 0; i <= 10; i++){ printf("#\n"); }
for문을 3가지의 방법으로 사용하는 것처럼 여러 사람들이 코드를 적성할때, 서로 불필요한 오해를 없애고, 코드를 이해하는 데 드는 비용을 최소화 하기 위해서 각 집단 마다 특정 스타일 가이드를 준수한다.
고무 오리
때로는 이러한 디버깅 프로그램도 해결해주지 못하는 문제가 있는데,
우리는 이때 문제에서 눈을 떼고 산책을 하거나, 식사를 하거나, 낮잠을 자거나,
아니면 하나의 물체를 잡아 사람에게 설명하듯 문제의 정의부터 시작해보는 것이다.
설명을 하다보면 논리적으로 문제에 대해 접근하고, 내가 놓쳤던 부분을 다시 찾아낼 수도 있다.
배열
메모리
C에는 여러 자료형이 있고, 각 자료형은 서로 다른 크기의 메모리를 자지한다.
- bool: 1바이트
- char: 1바이트
- int: 4바이트
- float: 4바이트
- long: 8바이트
- double: 8바이트
- string: n바이트
위 사진은 RAM이다.
쉽게 생각하면, 여러개의 노란색 사각형이 메모리를 의미하고 각 사각형 하나가 1바이트를 의미한다고 볼 수 있다.
#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);
위 코드처럼 평균을 구하는 프로그램이 있다고 할 때,
우리는 배열의 개념을 활용할 수 있다.
#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);
}
int scores[3]; 이라는 코드는 int자료형을 가지는 크기 3의 배열을 scores 라는 이름으로 생성하겠다는 의미이다.
배열을 활용하면, 같은 자료형의 데이터를 메모리상에 연이어서 저장하고 이를 하나의 변수로 관리할 수 있다.
전역변수
const int N=3;
만약 N이 고정된 값이라면, 그 값을 선언할 때 const를 앞에 붙여서 전역 변수, 즉 코드 전반에 거쳐 바뀌지 않는 값임을 지정해줄 수 있다.
관례적으로 이런 전역 변수의 이름은 대문자로 표기한다.
#include <cs50.h>
#include <stdio.h>
float average(int length, int array[]);
int main(void)
{
// 사용자로부터 점수의 갯수 입력
int n = get_int("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;
}
위 코드 처럼 루프와 함수를 선언하여 좀 더 동적으로 프로그램을 설계할 수 있다.
배열의 크기는 사용자에게 입력 받고, 배열의 크기만큼 루프를 돌면서 각 인덱스에 해당하는 값을 역시 사용자에게 동적으로 입력 받아 저장한다.
average 함수는 length와 array[], 즉 배열의 길이와 배열을 입력으로 받는다.
함수 안에서는 배열의 길이만큼 루프를 돌며서 값의 합을 구하고 최종적으로 평균값을 반환한다.
문자열과 배열
우리가 사용하던 string이란 자료형의 데이터는 사실 문자(char) 자료형의 데이터 배열이다.
위 사진의 \0은 문자열의 끝을 나타내는 널 종단 문자이다.
단순히 모든 비트가 0인 1바이트를 의미한다.
여러 문자열이 나열되면 그 사이를 구분해줄 구분자가 필요하기 때문에 종단 문자를 사용한다.
문자열의 활용
문자열이 어떻게 저장되는지 알았다면 이를 탐색에 활용할 수도 있다.
간단하게 for 루프를 통해 문자열의 인덱스를 하나씩 증가시켜가면서 해당하는 문자를 출력하면 된다.
그리고 마지막 문자열의 끝은 널 종단문자, \0과 일치하는지 검사하면 된다.
#include <cs50.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
string s = get_string("Input: ");
printf("Output:\n");
for (int i = 0, n = strlen(s); i < n; i++)
{
printf("%c\n", s[i]);
}
}
하지만 위 코드처럼 strlen을 사용할 수도 있다.
위 코드에서는 n이라는 변수에 문자열 s의 길이를 저장하고, 해당 길이 만큼만 for 루프를 순환한다.
따라서 일일이 널 종단 문자를 검사하는 것 보다 훨씬 효율적이다.
명령행 인자
#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");
}
}
우리가 항상 쓰던 함수인 int main(void)가 아니라,
첫번째 변수 argc를 활용해볼 수 있다.
main 함수가 받게 될 입력의 개수를 의미한다.
두번째 변수 argv는 입력이 포함되어 있는 배열을 의미한다.
기본적으로 argv[0]에는 프로그램의 이름이 저장된다.
따라서 arg.c라는 이름으로 저장하고 컴파일 한 후 ./argc로 실행하면 hello, world 값이 출력된다.
'🖥️Computer Science > CS50' 카테고리의 다른 글
CS50_자료구조 (0) | 2023.06.13 |
---|---|
CS50_메모리 (0) | 2023.06.13 |
CS50_알고리즘 (0) | 2023.06.13 |
CS50_C언어 (0) | 2023.06.06 |
CS50_컴퓨팅 사고 (0) | 2023.06.06 |