[CS50] 배열
업데이트:
컴파일링
#include <stdio.h>
int main(void){
printf("hello, world\n");
}
- main 함수는 프로그램의 시작점으로써 실행 버튼을 클릭하는 것과 같다.
- printf는 출력을 담당하는 함수이며 사용하기 위해서는 stdio.h 라이브러리가 필요하다.
- stdio.h는 헤더 파일로 C 언어로 작성되어 있으며 파일명이 .h로 끝나는 파일이다.
- 이 파일에는 printf 함수의 프로토타입이 있어서 Clang 컴파일러가 프로그램을 컴파일할 때 printf가 무엇인지 알려주는 역할을 한다.
- 코드를 clang hello.c로 컴파일하고 ./a.out 명령으로 프로그램을 실행할 때 이 과정은 0과 1로 이루어진 파일 a.out을 생성하여 실행 가능하게 한다.
- a.out이 아닌 다른 파일명으로 컴파일하고 싶다면?
clang -o hello hello.c
- CS50 라이브러리를 사용한 프로그램을 컴파일 할 때는?
clang -o hello hello.c -lcs50
이것은 clang에게 CS50 라이브러리에 있는 모든 0과 1들을 여기에 연결하라는 의미이다.
더 간단히는 make
프로그램을 이용하면 이 모든 컴파일 과정을 자동으로 처리할 수 있다.
make나 clang을 사용해서 프로그램을 실행할 때 아래 네 개의 단계를 거친다.
- 전처리
- 컴파일링
- 어셈블링
- 링킹
전처리(Precompile)
- 컴파일의 전체 과정은 네 단계로 나누어볼 수 있다. 그 중 첫 번째 단계가 전처리이다.
- 전처리는 전처리기에 의해 수행된다.
- ’#’으로 시작되는 C 소스코드는 전처리기에 실질적인 컴파일이 이루어지기 전에 무언가를 실행하라 알려준다.
- ex)
#include
는 전처리기에게 다른 파일의 내용을 포함시키라고 알려준다.
컴파일(Compile)
- 전처리기가 전처리한 소스 코드를 생성하고 나면 그 다음 단계는 컴파일이다.
- 컴파일러라고 불리는 프로그램은 C 코드를 어셈블리어라는 저수준 프로그래밍 언어로 컴파일한다.
- 어셈블리는 C보다 연산의 종류가 훨씬 적지만, 여러 연산들이 함께 사용되면 C에서 할 수 있는 모든 것들을 수행할 수 있다.
- C 코드를 어셈블리 코드로 변환시켜줌으로써 컴파일러는 컴퓨터가 이해할 수 있는 언어와 최대한 가까운 프로그램으로 만들어준다.
- 컴파일이란 용어는 소스 코드에서 오브젝트 코드로 변환하는 전체 과정을 통틀어 일컫기도 하지만, 구체적으로 전처리한 소스 코드를 어셈블리 코드로 변환시키는 단계를 말하기도 한다.
어셈블(Assemble)
- 어셈블 단계는 어셈블리 코드를 오브젝트 코드로 변환시킨다.
- 컴퓨터의 중앙처리장치가 프로그램을 어떻게 수행해야 하는지 알 수 있는 명령어 형태인 연속된 0과 1들로 바꿔주는 작업
- 이 변환작업은 어셈블러라는 프로그램이 수행한다. 소스 코드에서 오브젝트 코드로 컴파일 되어야 할 파일이 딱 한 개라면, 컴파일 작업은 여기서 끝이 난다. 그렇지 않은 경우는 다음 단계인 링크라 불리는 단계가 추가된다.
링크(Link)
- 만약 프로그램이 여러 개의 파일로 이루어져 있어 하나의 오브젝트 파일로 합쳐져야 한다면
링크
라는 컴파일의 마지막 단계가 필요하다. - 링커는 여러 개의 다른 오브젝트 코드 파일을 실행 가능한 하나의 오브젝트 코드 파일로 합쳐준다.
이 네 단계를 거치면 최종적으로 실행 가능한 파일이 완성된다.
디버깅
버그와 디버깅
버그
(bug)는 코드에 들어있는 오류- 버그로 인해 프로그램의 실행에 실패하거나 프로그래머가 원하는 대로 동작하지 않게 된다.
디버깅
(debugging)은 코드에 있는 버그를 식별하고 고치는 과정이다.- 프로그래머는 디버거라고 불리는 프로그램을 사용하여 디버깅을 하게 된다.
디버깅의 기본
- 디버거는 프로그램을 특정 행에서 멈출 수 있게 해주기 때문에 버그를 찾는데 도움이 된다.
- 프로그래머는 멈춰진 그 지점에서 무슨 일이 일어나는지 볼 수 있다.
- 프로그램이 멈추는 특정 지점을
중지점
이라 한다. - 프로그러매가 프로그램을 한번에 한 행씩 실행할 수 있게 해준다.
help50
int main(void)
{
printf("hello, world\n");
}
- make 프로그램을 이용하여 컴파일해보면 “implicitly declaring library function ‘printf’“라는 에러 메세지가 나타난다.
- 이런 메세지를 이해하기 힘들 때 help50 프로그램을 사용하면 좋다.
- make 앞에 help50을 붙여 실행하면 다시 컴파일시 생기는 오류를 해석해준다.
help50 make 파일이름
printf
#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가 0에서 시작하기 때문에 for 루프의 i <= 10 이라는 조건은 실제로 11번 만족한다는 사실을 알 수 있다.
- 따라서 i < 10으로 수정해주면 ‘#’이 10번 출력된다.
debug50
- CS50 IDE를 사용하면 debug50이라는 프로그램을 사용할 수 있다.
-
소스 코드에 직접 브레이크포인트를 저장하고 소스파일을 컴파일한 후에 dubug50 파일명으로 실행한다.
- 디버깅 종료를 위해 Ctrl + c를 누르면 된다.
코드의 디자인
check50
- check50 프로그램을 이용하면 과제를 잘 수행했는지 자동으로 검사가 가능
- 여러 사람들이 각자 한 부분을 맡아 코드를 작성할 때 각자 수정한 코드가 전체 프로그램의 정확성을 해치지 않는지 쉽게 확인할 수 있다.
- 이 프로그램은 CS50 강의를 위해서만 작성되었다.
style50
- style50 프로그램을 이용하면 코드가 심미적으로 잘 작성되어 있는지 검사 가능
- 공백의 수나 줄바꿈과 같은 것들이 코드를 읽고 이해하는데 영향을 준다.
- 많은 회사들이 사내에서 코드를 작성할 때 특정한 스타일 가이드를 따르도록 한다.
- 여러 사람들이 코드를 작성하기 때문에 서로 불필요한 오해를 없애고, 코드를 이해하는 데 드는 비용을 최소화하기 때문이다.
고무 오리
- 때로는 코드에 포함된 오류를 해결할 때 위에 있는 프로그램들이 존재하지 않거나, 있다 하더라도 디버깅에 큰 도움이 안 될 수 있다.
- 이럴 때는 직접 곰곰히 생각해보는 수 밖에 없다.
- 한 가지 유명한 방법으로는
고무 오리
와 같이 무언가 대상이 되는 물체를 앞에 두고, 내가 작성한 코드를 한 줄 한 줄 말로 설명해주는 과정을 거쳐볼 수 있다.
배열(1)
메모리
- C에는 여러 자료형이 있고, 각각의 자료형은 서로 다른 크기의 메모리를 차지한다.
- bool: 불리언, 1바이트
- char: 문자, 1바이트
- int: 정수, 4바이트
- float: 실수, 4바이트
- long: (더 큰) 정수, 8바이트
- double: (더 큰) 실수, 8바이트
- string: 문자열, ?바이트
- 컴퓨터 안에는 아래 사진과 같은 RAM이라고 하는 물리적 칩이 메모리 역할을 한다.
- 여러 개의 노란색 사각형이 메모리를 의미하고, 작은 사각형 하나가 1바이트를 의미한다.
- 예를 들어 char 타입의 변수를 하나 생성하고, 그 값을 입력한다고 하면 위 사진에서 한 사각형 안에 그 변수의 값이 저장된다.
배열
#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라는 이름으로 생성하겠다는 의미이다.
- 배열의 인덱스는
0부터 시작
하기 때문에, scores의 인덱스는 0, 1, 2 세 개가 있다. - 이 인덱스를 변수명 뒤에
대괄호
[] 사이에 입력하여 배열의 원하는 위치에 원하는 값을 저장하고 불러올 수 있다. - 하지만 위와 같은 코드는 여전히 점수의 개수가 바뀌는 상황에서 제약이 많다.
배열(2)
전역 변수
#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);
}
- 위의 코드에서 scores 배열의 크기를 정해주는 N이라는 변수 새로 선언
- 만약 N이 고정된 값(상수)이라면 그 값을 선언할 때
const
를 붙여 전역 변수, 즉 코드 전반에 거쳐 바뀌지 않는 값임을 지정할 수 있다. - 관례적으로 이런 전역 변수 이름은 대문자로 표기한다.
- scores의 크기로 전역 변수를 선언하였기 때문에 점수 개수가 바뀌었을 때 수정해야 하는 코드가 좀 줄었다. 하지만 일일이 인덱스마다 점수를 지정해줘야 하는 불편함이 있다.
배열의 동적 선언 및 저장
#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 함수를 따로 만들어서 평균을 구한다.
- average 함수는 length와 array[], 즉 배열의 길이와 배열을 입력으로 받는다.
- 함수 안에서는 배열의 길이만큼 루프를 돌면서 합을 구하고 최종적으로 평균값을 반환한다.
문자열과 배열
- 우리가 여지껏 사용한 문자열(string) 자료형의 데이터는 사실 문자(char) 자료형의 데이터들의 배열이다.
- string s = “HI!”; 와 같은 문자열 s는 문자의 배열이기 때문에 메모리 상에 아래 그림과 같이 저장되고, 인덱스로 각 문자에 접근할 수 있다.
- 여기서 가장 끝의
\0
은 문자열의 끝을 나타내는 널 종단 문자이다. - 단순히 모든 비트가 0인 1바이트를 의미한다.
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]);
- names라는 문자열 형식의 배열에 네 개의 이름이 저장되었다.
- 첫 번째 printf에서는 names의 첫 번째 인덱스의 값, 즉 “EMMA”를 출력한다.
- 두 번째 printf에서는 형식 지정자가 %s가 아닌 %c로 설정되어 있음을 확인할 수 있다.
- 따라서 출력하는 것은 문자열이 아닌 문자다.
- 이는 names[0][1]과 같이 2차원 배열을 통해 접근할 수 있다.
- names[0][1]는 names의 첫 번째 값, 즉 “EMMA”라는 문자열에서, 그 두번째 값, 즉 ‘M’ 이라는 문자를 의미한다.
- 아래 그림에서 names가 실제 메모리상에 저장된 예시와 해당하는 인덱스를 확인할 수 있다.
문자열의 활용
문자열의 길이 및 탐색
#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
은 문자열의 길이를 알려주는 함수이며 string.h 라이브러리 안에 포함되어 있다.- 위 코드에서는 n이라는 변수에 문자열 s의 길이를 저장하고, 해당 길이 만큼만 for 루프를 순환한다.
문자열 탐색 및 수정
#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");
}
- 입력받은 문자열을 대문자로 바꿔주는 프로그램이다.
- 먼저 입력받은 문자열을 s라는 변수에 저장한 뒤 s의 길이만큼 for 루프를 돌면서, 각 인덱스에 해당하는 문자가 ‘a’보다 크고 ‘z’보다 작은지 검사한다.
- ASCII 값에서 각 알파벳의 소문자와 대문자는 32씩 차이가 난다.
- 따라서 각 문자가 소문자인 경우 그 값에서 -32를 하고 ‘문자’ 형태로 출력하면 대문자가 출력된다.
- 이와 동일한 작업을 수행하는 함수가 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");
}
명령행 인자
- main 함수 괄호 안에 기계적으로 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[]
는 그입력이 포함되어 있는 배열
이다.- 프로그램을 명령행에서 실행하므로, 입력은 문자열로 주어진다.
- 따라서 argv[]는 string 배열이 된다.
argv[0]
는 기본적으로프로그램의 이름
으로 저장된다.- 만약 하나의 입력이 더 주어진다면 argv[1]에 저장된다.
- 위 프로그램을 “arg.c”라는 이름으로 저장하고 컴파일 한 후
"./argc"
로 실행해보면 “hello, world”라는 값이 출력된다. "./argc David"
로 실행해보면 “hello, David”라는 값이 출력된다.
댓글남기기