CS50 3주차 - 컴파일링과 디버깅 & 배열
이 포스팅은 부스트코스의 CS50 2019 강의의 내용을 공부한 글 입니다.
3주차 배열
C언어 돌아보기
#include <stdio.h>
#include <cs50.h>
int main(viod)
{
string name = get_string("What's your name?\n");
printf("Hello, %s\n", name);
}
#include <stdio.h>
#include <cs50.h>
라이브러리를 불러오는 것이다. 위 두 라이브러리에는 string, printf, get_string 등의
함수 프로토타입을 사전에 정의해줘서 우리가 코드를 작성할 때 편하게 작성할 수 있다.
int main(viod)
{
string name = get_string("What's your name?\n");
printf("Hello, %s\n", name);
}
int main(void)는 실행 버튼을 클릭한 것과 같은 역할을 하며 main이라는 함수를 뜻한다.
string은 stdio.h라이브러리에 있는 함수이고 get_string은 cs50.h라이브러리에 있는 함수다.
printf라는 함수로 문자를 출력할 수 있다.
%s 는 형식 지정자를 뜻하고, \n을 통해 줄바꿈을 할 수 있다.
터미널 콘솔에서 clang 또는 make를 통해 작성한 c언어를 컴파일해서
컴퓨터가 알아들을 수 있는 0과1로 바꿔준다.
프로그램을 실행할 때에는 4단계를 거친다.
- 전처리 (Precompile)
- 컴파일링 (Compile)
- 어셈블링 (Assemble)
- 링킹 (Link)
전처리
clang이나 make를 통해서 프로그램을 실행하게 되면 # 기호로 시작하는
라이브러리는 해당 파일의 실제 코드로 대체된다. 예를 들어
#include와 같은 줄을 포함하여 라이브러리를 불러오면, 전처리기는 새로운 파일을 생성하는데
이 파일은 여전히 C 소스 코드 형태이며 stdio.h 파일의 내용이 #include 부분에 포함된다.
컴파일
컴파일러라고 불리는 프로그램은 C 코드를 어셈블리어라는 저수준 프로그래밍 언어로 컴파일한다.
C 코드를 어셈블리 코드로 변환시켜줌으로써 컴파일러는 컴퓨터가 이해할 수 있는
언어와 최대한 가까운 프로그램으로 만들어 준다.
구체적으로 전처리한 소스 코드를 어셈블리 코드로 변환시키는 단계를 말하기도 한다.
어셈블
컴파일을 통해 소스코드가 어셈블리 코드로 변환되면, 어셈블 단계로 어셈블리 코드를
오브젝트 코드로 변환 시킨다. CPU가 알아들을 수 있는 언어인 0과1로 바꿔주는 작업이다.
이 변환 작업은 어셈블러라는 프로그램이 수행한다.
링크
프로그램이 여러개의 라이브러리를 포함해 여러 개의 파일로 이루어져 있어 하나의 오브젝트 파일로
합쳐야 할 때 링크라는 컴파일의 단계에 오게된다.
링커는 여러 개의 다른 오브젝트 코드 파일을 실행 가능한 하나의 오브젝트 코드 파일로 합쳐준다.
만약 컴파일링 과정을 거치지 않기 위해 바로 머신코드로 우리가 원하는 프로그램을 작성하려고 한다면 어떤 문제가 있을까요?
코드작성이 오래걸리고, 코드가길어지니 가독성도 떨어진다.
디버깅
버그는 코드안에 들어있는 오류이다. 다른 말로는 의도하지 않은 프로그램안의 실수이기도 하다.
오류가 있으면 프로그램이 동작하지 않는다. 디버깅은 이러한 버그를 식별하고 고치는 과정이다.
디버거라는 프로그램을 사용하여 디버그를 하게되고 여러 IDE(통합개발환경에이터)들이 있다.
cs50에서의 디버깅 방법
cs50 IDE 에서는 디버깅을 할때 사용하는 명령어가 있다.
help50 - help50 make 파일명
debug50 - debug50 파일명 ( 디버깅 종료 ctrl + c )
check50 - 코드를 자동으로 검사
style50 - style50 파일명.c
개발하는 입장에서 버그를 파악하고 디버깅 능력을 키우는것이 매우 중요하다.
개발하면서 자주 빌드를 해주고 버그가 발견되면 빠르게 캐치해서 코드를 수정하여야한다.
디버깅을 도와주는 프로그램은 어떤 경우에 더 큰 도움이 될까요? 만약 이런 프로그램의 도움 없이 직접 디버깅을 해야 한다면 어떻게 코드를 작성하는 것이 좋을까요?
문법적 오류와 논리적 오류를 찾아 문제를 해결할 수 있다. 프로그램의 도움이 없다면
빌드를 자주 해보고 주석처리를 하여 가독성을 높인다.
코드의 디자인
코드 작성시에는 읽기 쉽도록 작성하는 것이 중요하다.
유지보수를 위한 가독성을 높이기 위해 코드의 디자인은 필수이다.
cs50에서는 style50을 사용해 코드의 디자인을 도와주고
지금 공부중인 스위프트는 스위프트 린이라는 프로그램으로 코드를 이쁘게 작성할 수 있다.
좋지 않은 코드 디자인
#include <stdio.h>
int main(void){printf("hello, world"\n);}
좋은 코드 디자인
#include <stdio.h>
int main(void)
{
printf("hello, world"\n);
}
만약 여러 사람들이 함께 참여하는 프로젝트에서, 각자가 작성하는 코드 스타일 서로 다르다면 어떤 비효율적인 일이 발생할까요?
가독성이 떨어지므로 유지보수하는것이 어렵고 서로 코드를 소통하는데 있어서 힘들다.
배열
C의 자료형에는 여러가지가 있다. 이 자료형들은 각각 다른 크기의 메모리를 차지한다.
- bool: 불리언, 1바이트
- char: 문자, 1바이트
- int: 정수, 4바이트
- float: 실수, 4바이트
- long: (더 큰) 정수, 8바이트
- double: (더 큰) 실수, 8바이트
- string: 문자열, ?바이트( 크기가 정해져 있지 않음 )
string에 들어오는 문자열 끝에는 \0(null= 1바이트)가 붙어서
글자수보다 1자리 더 많은 바이트를 차지한다.
이 하나하나의 바이트는 RAM이라는 컴퓨터나 스마트폰 등의 기기안에 있는 부품의 일부에 저장된다.
C언어에서 char를 저장할 때는 작은따옴표를 사용한다. (string은 큰따옴표)
#include <stdio.h>
int main(void)
{
int score1 = 72;
int score2 = 73;
int score3 = 33;
printf("Average: %i\n", (score1 + score2 + score3) / 3)
}
#include <stdio.h>
int main(void)
{
int scores[3]; //int 변수를 세개 저장하겠다.
// 배열의 인덱스는 0부터
scores[0] = 72;
scores[1] = 73;
scores[2] = 33;
printf("Average: &i\n", (scores[0] + scores[1] + scores[2]) / 3);
}
위에 두 코드는 같은 값을 출력하지만 저장해야할 값이 많아질수록 첫번째 코드는
관리하기가 힘들어 진다. 그러므로 배열을 이용하여 값을 저장하고 인덱스를 불러오는것이 좋다.
하지만 두번째 코드도 여러가지 제한이 있는 문제가 있다.
#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];
for (int i = 0; i < n; i++)
{
scores[i] = get_int("Score %i: ", i + 1);
}
printf("Average: %.2f\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;
}
이 코드는 몇개의 점수의 평균을 낼건지 물어보고
n만큼의 점수를 합산해 평균을 구해주는 코드이다.
배열, 형변환, 함수 생성 등을 잘 활용해서 이렇게 유용한 코드도 짤 수 있다.
C언어는 스위프트와 다르게 스스로 배열의 길이를 기억하지 않는다.
그래서 배열의 길이를 지정하주는 과정이 필요하다.
점수의 평균을 구하는 예제에서, 동적으로 작성한 코드는 그렇지 않은 코드에 비해 어떤 장단점이 있을까요?
제한적이였던 문제가 해결되고, 내용일 많아질수록 코드가 길어지는 것을 방지하며 다양한 상황해 적용이 가능하다.
문자열 활용
문자열(string)은 문자(char)의 배열로 정의한 것이다.
char a = 'H';
char b = 'I';
char c = '!';
string s = "HI!"; // ['H','I','!']
여러개의 문자들을 저장하는 것이 각 변수마다 하나의 문자를 저장하는 것보다 훨씬 유용하다.
위에서 자료형들의 바이트를 알아보았지만 문자열의 자료형은 정해진 크기를 가질 수 없다.
하지만 문자열일 언제 끝나는지를 알려주는 정보도 필요하다.
문자열의 끝을 알기위해 Null표현으로 \0 를 사용한다.
그림을 통해 보면 ‘H’ ‘I’ ‘!” 세개의 문자를 세개의 바이트가 차지할것 같지만
Null을 통해서 4개의 바이트가 필요한 것을 볼 수 있다.
널 종단 문자는 단순히 모든 비트가 0인 1바이트를 의미한다.
#include <stdio.h>
#include <cs50.h>
int main(void)
{
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]);
}
이 코드 처럼 두가지를 활용해서 name[0]에 접근할 수 있다.
두번 째 프린트를 보면 [0][0] 이 있는데 첫번째는 변수의 인덱스에 접근하고
두번째는 저장된 값의 인덱스에 접근한다. ‘E’ = 0, ‘M’ = 1, ‘M’ = 2, ‘A’ = 3
string.h 라이브러리에 있는 strlen 함수는 문자열의 길이를 알려준다.
ctype.h 라이브러리에 있는 toupper() 함수는 소문자를 대문자로 변환해준다.
toupper()함수를 사용하게되면 if-else를 사용하지 않고도 간단히 문자변환이 가능하다.
널 종단 문자는 왜 필요할까요?
문자열의 길이는 정해져 있지 않아서 문자열을 구분하고 문자의열 끝을 표시하기 위함.
명령행 인자
앞에 배웠던 mint(void)로 함수를 정의했다면
이번에는 argc와 argv로 함수를 정의하였다.
#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 - 입력이 포함되어 있는 배열
명령행 인자는 프로그램의 확장성에 어떤 도움이 될까요? 구체적인 예시를 떠올려보세요.
- 많은 데이터를 저장할 수 있다.
- 출력값의 길이를 줄이고 동적으로 프로그램을 짤 수 있다.
- 다른 함수를 사용하지 않아도 여러 값을 결과로 낼 수 있다.
- 반복적인 작업을 할 경우 실용적이다.
Comments