2. Arrays
Compiling
우리가 C언어로 작성한 source code를 컴퓨터가 실행시키기 위해서는 2진법의 machine code로 compile 해야한다.
Source code를 machine code로 compile하는 과정은 몇 단계로 나누어져있다:
- preprocessing
- compiling
- assembling
- linking
Preprocessing
가장먼저 preprocessing은 #include
처럼 #
으로 시작하는 line에 관련되어있다. 예를 들어, #include <cs50.h>
는 clang
에게 현재 프로그램에 사용할 콘텐츠를 포함하고있는 header file을 찾으라고 하는 명령으로 clang
이 header file의 콘텐츠를 현재 프로그램으로 가져오게된다.
// Before preprocessing
#include <cs50.h>
#include <stdio.h>
int main(void)
{
string name = get_string("Name: ");
printf("hello, %s\n", name);
}
// After preprocessing
// cs50.h로부터 가져옴
string get_string(string prompt);
// stdio.h로부터 가져옴
int printf(const char *format, ...);
int main(void)
{
string name = get_string("Name: ");
printf("hello, %s\n", name)
}
Compiling
C로 작성한 source code를 assembly code로 변환한다.
...
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)
movq -8(%rbp), %rsi
movb $0, %al
callq printf
...
- 위의 instruction은 lower-level로 컴퓨터의 CPU가 바로 이해할 수 있는 binary instruction에 가깝다.
- variable 등을 이용해 추상화되지 않고, byte 자체에서 작동한다.
Assembling
Assembly code로 된 instruction을 binary로 변환한다.
Binary로 된 instruction을 machine code라고 하고 CPU가 직접적으로 실행할 수 있다.
Linking
먼저 compile 해놓은 library의 contents를 현재 프로그램의 machine code와 연결
현재 프로그램인 hello.c
의 machine code에 미리 compile 해놓은 cs50.c
와 printf.c
의 maching code를 연결하여 하나의 파일(hello
)로 만든다.
Debugging
Bug는 제작자가 의도하지 않는 프로그램의 error이다. 이를 찾고 수정하는 과정을 debugging이라고 한다.
help50 and printf
Debugging을 하기 위해서는 error message를 참고하거나 debugger를 이용하면 된다. CS50 sandbox나 CS50 IDE는 bug와 관련된 몇 가지 기능을 제공한다.
help50
help50
: compile 할 때 make
나 clang
등의 command-line 앞에 적으면 terminal에 뜬 error message에서 해석하는 것을 도와준다.
// buggy0.c
int main(void)
{
printf("hello, world\n")
}
- 위 프로그램을
make
하면implicitly declaring library function 'printf'
라는 error message가 나타난다. - Error message를 이해하기 어려울 때,
help50 make buggy0
를 실행하면printf
를 포함하는#include <stdio.h>
를 적지 않았다는 것을 함께 알려준다.
Printf
printf
등으로 중간 과정을 출력해 error를 찾을 수도 있다. 주로 logical한 error를 찾는데 이용한다.
// buggy2.c
#include <stdio.h>
int main(void)
{
for (int i = 0; i <= 10; i++)
{
printf("#\n");
}
}
#
을 10번 출력하려고 작성한 위의 프로그램은 실제로#
을 11번 출력한다.- 위처럼 logical error가 있을 때에는 compile 자체는 error없이 되기 때문에 문제의 원인을 파악하기 어렵다. Print line을 추가해서 문제를 파악해볼 수 있다.
#include <stdio.h>
int main(void)
{
for (int i = 0; i <= 10; i++)
{
// print i to check current i
print("i is now %i", i);
printf("#\n");
}
}
- 실행해보면
i
가 0에서 시작해서 10이 될 때 까지 출력을 반복하기 때문에 총 11번 출력하는 것을 알 수 있다. 이를 해결하기 위해서는i
가 10이 되기 전에 멈춰야하기 때문에i <= 10
대신i < 10
을 사용해야한다.
debug50
CS50 IDE는 debug50
라는 debugger를 지원한다. 원하는 부분부터 program을 step by step으로 실행하면서 bug를 찾아낼 수 있다.
위에서 printf
를 통해 파악했던 i
의 상태를 debug50
를 이용해서 확인할 수 있다.
- 우선 프로그램 실행을 멈추고 debugger를 시작 할 breakpoint를 설정한다. 원하는 line의 숫자 왼편을 클릭해서 설정할 수 있다. 설정하면 빨간 원이 나타난다.
- 이후에
debug50 ./buggy2
로 실행하면 debugger panel이 오른쪽에 표시된다. - Debugger panel에서
Local Variables
아래i
가 있고 현재0
으로 선정되어있다. - Panel 위쪽의 원형 화살표를 누르면 누를 때 마다 한 줄씩 넘어가면서 실행하는데 버튼을 누르면
printf
라인에 도달하고 한 번 더 누르면 terminal window에#
이 출력된다. 다시 버튼을 누르면 오른쪽의i
가1
로 증가한다. - 이런 식으로 프로그램을 step by step으로 순차적으로 실행하면서 오류를 찾을 수 있다.
- Breakpoint를 여러개 설정할 수도 있고 파란 삼각형 버튼을 누르면 다름 breakpoint까지 실행된다.
- Debugger를 종료하기 위해서는
control + C
해 프로그램을 종료한다.
check50 and style50
check50
를 이용하면 CS50에서 정해놓은 가이드라인에 따라 프로그램을 test해볼 수 있다. 실제로 개발자들도 자신들 만의 test code를 작성해서 code가 제대로 동작하는지 test 해본다.
style50
는 코드가 미적으로 (코드 가독성과 유지보수와 관련) 괜찮은지 판단해주는 프로그램이다. Style guide에 판단 기준이 소개되어있다.
Data Types
C에는 다양한 종류의 data를 저장하기 위한 variables이 있으며 data type에 따라 다른 크기를 갖는다.
type | size |
---|---|
bool |
1 byte |
char |
1 byte |
int |
4 byte |
float |
4 byte |
long |
8 byte |
double |
8 byte |
string |
? byte |
Memory
우리가 사용하는 컴퓨터 안에는 잠깐 사용할 데이터를 저장하는 RAM (Random-Access Memory)이라는 것이 존재한다. 일반적으로 프로그램은 Hard drive (혹은 SSD)에 긴 시간동안 저장하지만 그 프로그램을 열어서 사용할 때는 RAM에 옮긴 뒤 사용한다.
Data를 저장할 수 있는 RAM의 공간을 byte가 연속된 grid처럼 있는 것으로 생각할 수 있다.
- 실제로는 하나의 칩에 수백만 혹은 수십억 정도의 굉장히 많은 byte가 존재한다.
C언어에서 char
type인 variable을 하나 생성하면 위 그림의 박스중 한 개를 차지하고 저장되는 것으로 생각할 수 있다. integer
의 경우 4 bytes이므로 네 개를 차지한다.
각 박스는 특정한 숫자나 주소로 label 되어있다.
Arrays
예를 들어 다음과 같이 세 개의 variable을 저장해보자:
#include <stdio.h>
int main(void)
{
// single quote for a literal character
char c1 = 'H';
char c1 = 'I';
char c1 = '!';
// double quote for multiple characters together
printf("%i %c %c\n", c1, c2, c3)
}
- Character는 실제로 숫자이므로
printf("%i %i %i\n", c1, c2, c3)
로 출력하면72 73 33
이 출력된다. - 명백하게는 각 charahcter를
(int) c1
처럼 cast(변환)해서 출력해야하지만 이 경우 compiler가 자체적으로 처리해준다.
이는 메모리중 세 개의 박스를 각각 c1
, c2
, c3
로 lable 해서 차지하고 있는 것과 같다. 각 박스는 하나의 byte를 의미하며 그 안에는 variable의 값을 binary의 형태로 담고있다.
int
형 data type의 variable을 이용해서 평균을 구하는 프로그램에 대해 생각해보자:
#include <stdio.h>
int main(void)
{
int score1 = 72;
int score2 = 73;
int score3 = 33;
printf("Average: %i\n", (score1 + score2 + score3) / 3);
}
프로그램의 목적은 세 숫자의 평균을 구하는 것이지만 각 score마다 일일히 variable을 만들어줘서 사용해야하기 때문에 이후에 사용하기가 번거롭다.
이를 해결하기 위해 각 variable을 서로의 바로 다음에 (back-to-back) 저장할 수 있다. C에서는 이런 연속적으로 덩어리져있는 variable들의 목록을 array라고 한다.
#include <cs50.h>
#include <stdio.h>
int main(void)
{
// scores
// declare array of 3 integers
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);
}
- Array는 zero-indexed되어있다. 즉, 첫 번째 element가 index 0에서 시작.
위 프로그램에서 array이 길이인 3이 반복되므로 항상 동일한 값을 나타낼 수 있도록 constant (fixed value)를 사용해서 수정해준다.
#include <cs50.h>
#include <stdio.h>
// Set N as 3 (한 번 값을 지정하면 프로그램 내에서 바뀌지 않음)
const int N = 3;
int main(void)
{
// scores
int scores[N];
scores[0] = 72;
scores[1] = 73;
scores[2] = 33;
// Print average
printf("Average: %i\n", (scores[0] + scores[1] + scores[2]) / N);
}
Array를 이용하면 loop를 이용해서 score을 보다 쉽게 더하거나 이후에 다시 접근할 수 있다.
#include <cs50.h>
#include <stdio.h>
// average 함수 선언
float average(int length, int array[]);
int main(void)
{
// 입력받을 score의 개수 입력
int n = get_int("Scores: ");
// n개의 score를 받을 array 선언
int scores[n];
// score 입력
for (int i = 0; i < n; i++)
{
scores[i] = get_int("Score %i: ", i + 1);
}
// Average 출력 (%.1f: 소수점 한 자리 까지)
printf("Average: %.1f\n", average(n, scores));
}
// length: score 개수, array: score를 담고있는 array
float average(int length, int array[])
{
int sum = 0;
// sum에 length개의 array 항목을 모두 더함
for (int i = 0; i < length; i++)
{
sum += array[i];
}
// sum을 length로 나눔
// 나눈 결과가 정수가 아닐 수 있으므로 모두 float형으로 바꿔서 계산
return (float) sum / (float) length;
}
메모리에서 위의 scores array는 각 값이 int
형이므로 4칸을 차지한다.
Strings
실제로 string은 문자(character)로 이루어진 array이다. String s
가 있다면 각 문자는 s[0]
, s[1]
과 같은 방식으로 접근할 수 있다.
또한, String은 특별한 문자인 \0
으로 끝난다. 이는 모든 bits가 0인 문자로 'null character' 혹은 'null terminating character'라고 불린다.
그러므로 "Hi!"를 표현하기 위해서는 4 bytes가 필요하다.
Array에 네 개의 string이 들어간 경우는 어떤지 보자:
string names[4];
names[0] = "EMMA";
names[1] = "RODRIGO";
names[2] = "BRIAN";
names[3] = "DAVID";
// names의 첫 번째 값을 string(%s)으로 출력
printf("%s\n", names[0]);
// 첫 이름의 각 character를 다시 []를 사용해서 출력
//(names[0])[0]처럼 생각할 수 있다
printf("%c%c%c%c\n", names[0][0], names[0][1], names[0][2], names[0][3]);
String을 출력할 때 null character에 도달할 때 까지 string의 각 character를 printf
를 반복해서 출력한다.
첫 이름은 네 글자이므로 실제로 names[0][4]
를 int
형으로 출력하면 0
이 나타난다.
이 array의 각 character들이 memory에 저장되어있는 것을 나타내면 다음과 같다:
이를 다음의 코드로 실험해볼 수 있다:
#include <cs50.h>
#include <stdio.h>
// strlen을 사용하기 위한 library
#include <string.h>
int main(void)
{
string s = get_string("Input: ");
printf("Output: ");
// strlen(s): string s의 길이 (문자수)
// string의 각 문자를 순차적으로 하나씩 출력
for (int i = 0; i < strlen(s); i++)
{
printf("%c", s[i]);
}
printf("\n");
}
- 조건을
s[i] != '\0'
을 사용해도 된다. Null character가 나올 때 까지 출력. - 위의 코드는 조건을 확인할 때마다
s
의 길이를 계산해줘야 하므로int i = 0, n = strlen(s); i < n; i++
처럼 처음에s
의 길이를n
에 할당한 뒤n
을 사용하면 보다 효율적이다.
배운 것을 이용해서 단어를 capitalize시키는 프로그램을 작성할 수 있다.
#include <cs50.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
string s = get_string("Before: ");
printf("After: ");
// string s의 모든 문자를 순차적으로 대문자로 변경
for (int i = 0, n = strlen(s); i < n; i++)
{
// s[i]가 소문자인 경우 ('a'~'z' 사이에 있는 경우)
if (s[i] >= 'a' && s[i] <= 'z')
{
// s[i]에서 32를 뺀 값을 출력
// ASCII 에서 동일한 문자의 대문자, 소문자의 차이는 32이다
printf("%c", s[i] - 32);
}
else
{
printf("%c", s[i]);
}
}
printf("\n");
}
library를 이용하면 훨씬 간단하게 할 수 있다. man pages에서 사용할 수 있는 다양한 library function을 찾아볼 수 있다.
#include <cs50.h>
// toupper를 사용하기 위한 library
#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++)
{
// toupper(s[i]): s[i]를 대문자로 변경
printf("%c", toupper(s[i]));
}
printf("\n");
}
Command-line arguments
make
나 clang
같은 프로그램은 command line에서 각 명령어 뒤에 단어를 추가적으로 입력한다. (e.g file
을 compile하기 위해서 make 'file'
을 입력)
우리가 직접 만든 프로그램도 command line에 입력한 단어를 command-line argument로 사용할 수 있다.
// hello.c를 다음과 같이 작성한 뒤 terminal에 './hello name'을 입력하여 실행
// name에는 출력하고 싶은 아무 문자열이나 쓰면 된다
#include <cs50.h>
#include <stdio.h>
// `main` function이 `argc`, `argv` 두 변수를 받음
int main(int argc, string argv[])
{
// argument의 개수가 2개가 아닌 경우 (./hello와 name)
if (argc != 2)
{
// command-line argument를 입력하지 않았다는 안내를 출력
printf("missing command-line argument\n");
// 문제 발생을 알리기 위해 1을 return하고 종료.
return 1;
}
// argv[1]인 name을 이용하여 'hello, name' 출력
printf("hello, %s\n", argv[1]);
// 문제 없음을 알리기 위해 0을 return하고 종료.
return 0;
}
argc
: Argument count, 입력된 argument의 수argv
: 입력된 argument를 담고있는 string array- 첫 argument
argv[0]
은 실행시키는 프로그램의 이름이다. (e.g./hello
) - 프로그램을 종료하기 위해
return
과 함께 return value를 입력한다. 통상적으로 정상적으로 프로그램을 마쳤을 때0
을 return하며 에러가 발생했을 때는 그 외의 수를 return한다.
'공부를 합니다 > 컴퓨터 공학 (Computer Science)' 카테고리의 다른 글
CS50's Web Track-1 (0) | 2020.05.10 |
---|---|
CS50's Week 8_Information (0) | 2020.05.07 |
CS50's Week 7_SQL (0) | 2020.04.22 |
CS50's Week 6_ Python (0) | 2020.03.21 |
CS50's Week 5_ Data Structures (0) | 2020.03.07 |