📚 시리즈 목차
3. 배열
3. 배열
1) 컴파일링
저번시간에 컴파일에 대해 궁금해서 찾아보다가 소스코드가 머신코드가 되기까지 4단계가 있다는 걸 알고 정리했는데 오늘 수업내용에서 자세히 나왔다. 복습하면서 더 자세히 알아보자
이전시간에도 배웠듯이 우리는 clang이나 make를 통해서 컴파일링을 해왔다.
그렇다면 이 clang과 make가 도대체 뭘까?
clang
clang은 C, C++, Objective-C, Objective-C++ 등의 언어를 위한 컴파일러 도구이다.
높은 성능과 최신 언어 기능 지원, 진단 메시지의 품질 등으로 인해 많은 개발자들에게 선호되는 컴파일러이다.
컴파일 단계에서 소스 코드를 기계어로 변환하여 실행 가능한 파일을 생성한다.
clang을 사용하여 개별 소스 파일을 컴파일 할 수도 있고, 여러 소스 파일을 함께 컴파일하여 프로그램을 빌드할 수도 있다.
make
make는 빌드 자동화 도구로 소스 코드의 컴파일 및 빌드 프로세스를 관리하는 데 사용된다.
'make'는 Makefile이라는 특정 형식의 파일을 사용하여 빌드 단계와 의존성을 정의하고, 변경된 파일만 다시 컴파일하거나 빌드할 수 있도록 한다. Makefile은 소스 코드 파일, 컴파일 옵션, 의존성 등을 지정하여 프로젝트의 빌드 과정을 자동화하는데 사용된다.
조금 더 쉽게 설명하자면 clang은 개별 파일을 컴파일 할 때 사용한다. clang은 딱 선택한 파일만 컴파일을 한다. 하지만 make는 해당 파일과 의존성이 있는 파일까지 파악하고 변경된 파일만 재컴파일한다. 이렇게 함으로써 빌드 시간을 단축할 수 있다. 특히 대규모 프로젝트나 의존성이 많을 때 유용할 수 있다. 물론 그안에서 make와 clang 둘 다 사용할 수도 있다.
이렇게해서 clang이나 make를 사용해서 프로그램을 실행할 때 아래 네 개의 단계를 거친다.
- 전처리
- 컴파일링
- 어셈블링
- 링킹
전처리(Precompile)
#으로 시작되는 C소스 코드는 전처리기에게 실질적인 컴파일이 이루어지기 전에 무언가를 실행하라고 알려준다.
예를 들어, #include는 전처리기에게 다른 파일의 내용을 포함시키라고 알려준다.
프로그램의 소스 코드에 #include와 같은 줄을 포함하면, 전처리기는 새로운 파일을 생성하는데 이 파일은 여전히 C소스 코드 형태이며 stdio.h 파일의 내용이 #include 부분에 포함된다.
컴파일(Compile)
C 코드를 어셈블리어라는 저수준 프로그래밍 언어로 컴파일 한다.
어셈블리는 C보다 연산의 종류가 훨씬 적지만, 여러 연산들이 함께 사용되면 C에서 할 수 있는 모든 것들을 수행할 수 있다.
C코드를 어셈블리 코드로 변환시켜줌으로써 컴파일러는 컴퓨터가 이해할 수 있는 언어와 최대한 가까운 프로그램으로 만들어 준다.
컴파일이라는 용어는 소스 코드에서 오브젝트 코드로 변환하는 전체 과정을 통들어 일컫기도 하지만, 구체적으로 전처리한 소스 코드를 어셈블리 코드로 변환시키는 단계를 말하기도 한다.
어셈블(Assemble)
어셈블 단계는 어셈블리 코드를 오브젝트 코드로 변환시키는 것이다.
컴퓨터의 중앙처리장치가 프로그램을 어떻게 수행해야 하는지 알 수 있는 명령어 형태인 연속된 0과 1들로 바꿔주는 작업이다.
이 변환작업은 어셈블러라는 프로그램이 수행한다.
소스 코드에서 오브젝트 코드로 컴파일 되어야 할 파일이 딱 한 개라면, 컴파일 작업은 여기서 끝이 난다. 그러나 그렇지 않은 경우에는 링크라 불리는 단계가 추가된다.
링크(Link)
만약 프로그램이 (math.h나 cs50.h와 같은 라이브러리를 포함해) 여러 개의 파일로 이루어져 있어 하나의 오브젝트 파일로 합쳐져야 한다면 링크라는 컴파일의 마지막 단계가 필요하다.
링커는 여러 개의 다른 오브젝트 코드 파일을 실행 가능한 하나의 오브젝트 코드 파일로 합쳐준다.
예를 들어, 컴파일을 하는 동안에 CS50 라이브러리를 링크하면 오브젝트 코드는 GetInt()나 GetString() 같은 함수를 어떻게 실행할 지 알 수 있게 된다.
2) 디버깅
디버깅에 대해 알기전에 먼저 버그에대해 알아보자
(1) 버그
버그는 코드에 들어있는 오류이다.
버그로 인해 프로그램의 실행에 실패하거나 프로그래머가 원하는 대로 동작하지 않게 된다.
개발자는 개발을 하면서 수많은 버그를 마주할텐데 과연 이를 어떻게 대처할까?
(2) 디버깅의 기본
코드에 있는 버그를 식별하고 고치는 과정을 디버깅이라고 한다.
이 디버깅은 디버거라고 불리는 프로그램을 사용하여 가능하다.
프로그램은 일반적으로 인간보다 훨씬 빠르게 연산을 수행한다. 그래서 프로그램을 실행시켜보는 것만으로는 무엇이 잘못되었는지 찾아내기 어렵다. 디버거는 프로그램을 특정 행에서 멈출 수 있게 해주기 때문에 버그를 찾는데 도움이된다.
프로그래머는 멈춰진 그 지점에서 무슨 일이 일어나는지 볼 수 있다.
프로그램이 멈추는 특정 지점을 중지점이라고 한다.
또한 프로그래머가 프로그램을 한번에 한 행씩 실행할 수 있게 해준다.
이로써 프로그래머는 프로그램이 내리는 모든 결정들을 단계별로 따라갈 수 있게 된다.
(3) GDB(GNU Debugger)
GDB는 프러그램의 실행 과정을 추적하고 디버깅하는데 사용되는 강력한 커맨드 디버깅 도구이다.
C, C++, Ada, Objective-C, Fortran, Go 등 다양한 프로그래밍 언어를 지원한다.
GDB는 프로그램의 실행 중에 중단점을 설정하고, 변수의 값, 스택 상태, 레지스터 값 등을 확인하여 프로그램의 동작을 분석할 수 있다.
디버깅 중에는 프로그램의 실행을 일시 중지하고 원하는 시점부터 단계별로 코드를 실행하며 상태를 검사할 수 있다.
GDB는 커맨드 라인 인터페이스를 제공하기 때문에 초기 학습 곡선이 있을 수 있지만, 많은 기능과 유연성을 제공하여 복잡한 디버깅 시나리오를 다룰 수 있다.
GDB 주요 기능
1. 중단점 설정
2. 변수 및 메모리 검사 : 변수의 값을 확인하고, 프로그램이 사용하는 메모리 영역을 검사할 수 있다.
3. 스택 추적 : 프로그램의 호출 스택을 추적하여 함수 호출 관계와 인수 값을 확인할 수 있다.
4. 조건부 중단점: 특정 조건을 만족할 때만 중단점이 동작하도록 설정할 수 있다.
5. 단계별 실행 : 프로그램을 단계별로 실행하면서 각각의 단계에서 변수와 상태를 확인할 수 있다.
6. 백트레이스 : 프로그램이 중단된 지점부터 역추적하여 이전의 호출 스택 상태를 확인할 수 있다.
3) 배열
C에는 아래와 같은 여러 자료형이 있고, 각각의 자료형은 서로 다른 크기의 메모리를 차지한다.
- bool: 불리언, 1바이트
- char: 문자, 1바이트
- int: 정수, 4바이트
- float: 실수, 4바이트
- long: (더 큰) 정수, 8바이트
- double: (더 큰) 실수, 8바이트
- string: 문자열, ?바이트
#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);
}
이런식으로 배열을 사용하면 여러가지 장점이 생긴다.
1. 문자열의 길이를 저장하는 변수와 문자여르이 저장하는 배열의 크기를 직접제어 가능하다.
2. 필요한 메모리를 정확하게 할당할 수 있고, 낭비되는 메모리를 최소화할 수 있다.
3. 메모리 상에 연속적으로 저장되기 때문에 접근시간이 빠르고 메모리 캐시의 지역성을 활용하여 더 빠른 성능을 제공한다.
4. 하나의 변수로 관리가 가능하여 유지보수와 가독성이 올라간다.
이제 이 코드에서 조금씩 고쳐보자
먼저 scores[3] 부분과 printf( ~~ / 3); 에서 맨 마지막 3부분이 같은 숫자라는 것은 몇일, 몇주, 몇년뒤에 다시본다면
분명 까먹게되고 버그의 원인이 될 수 있다.
이럴때는 const를 통해 전역변수를 선언해서 관리해주면 좋다.
#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);
}
하지만 아직 부족하다.
만약 아까 말했듯이 지금은 3개의 점수만 있어서 관리가 편하지만 만약 만개의 점수라면 괜찮을까?
그리고 점수의 개수가 자주 변하면 매번 고쳐야하니 힘들지 않을까?
이제는 동적으로 코드를 구현해보자
#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;
}
이 코드는 배열의 크기를 사용자에게 직접 입력 받고, 배열의 크기 만큼 루프를 돌면서 각 인덱스에 해당하는 값을 역시 사용자에게 동적으로 입력 받아 저장한다.
이와 같은 방법을 통해서 임의의 점수 개수와 점수 배열에 대해서 동적으로 평균값을 구하는 프로그램을 작성할 수 있다.
퀴즈 복습
리졸빙(resolving)
리졸빙은 컴퓨터 네트워크에서 호스트 이름을 IP주소로 변환하는 과정을 의미한다. 호스트 이름은 사람이 이해하기 쉬운 형태의 문자열이지만, 실제통신은 IP주소를 사용하여 이루어진다. 따라서 호스트 이름을 IP주소로 변환해야 정확한 통신이 가능하다.
리졸빙은 일반적으로 DNS(Domain Name System) 서버를 사용하여 수행된다. DNS 서버는 호스트 이름과 해당 호스트의 IP주소를 매핑하는 역할을 한다. 호스트 이름을 리졸빙하기 위해 클라이언트 DNS 서버에 호스트 이름을 포함한 쿼리를 보내고, DNS 서버는 해당 호스트 이름에 대한 IP 주소를 응답으로 전송한다.
체킹(Checking)
체킹은 네트워크 통신에서 주어진 호스트가 온라인 상태인지 확인하는 과정을 말한다.
호스트의 온라인 상태를 확인하기 위해 일반적으로 ICMP(Internet Control Message Protocol)를 사용하는데, 가장 대표적인 예는 'Ping'이라고 알려진 도구이다. Ping은 ICMP Echo 요청을 보내어 해당 호스트로 부터 응답을 받으면 호스트가 온라인 상태로 간주된다.
체킹은 네트워크 상에서 호스트의 가용성을 확인하고, 장애나 문제가 있는 경우에 대비하여 대처하는 데 사용된다. 예를 들어, 서버의 상태를 체크하여 장애가 발생하면 즉시 대응하거나 특정 서비스의 가용성을 모니터링하는 등의 용도로 활용된다.
C언어에서는 리졸빙과 체킹을 수행하기 위해 네트워크 관련 라이브러리나 API를 사용한다.
edwith[5] 는 다 섯개의 값을 하나의 변수에 저장하기 위한 공간을 의미한다. 0~4까지는 문자가 저장되고 5에 종단문자가 저장된다.
ex) edwith[3]
#include <cs50.h>
#include <stdio.h>
int main(void)
{
// Scores
char edwith[3] = "abc";
// Print average
printf("Average:%c,%c,%c,%p\n", edwith[0], edwith[1], edwith[2],&edwith[3]);
}
형식지정자 %c 를 사용해서 edwith[3] 을 해도 에러가난다. 단지 주소값만 볼 수 있다.
#include <cs50.h>
#include <stdio.h>
int main(void)
{
// Scores
char edwith[3];
edwith[0] = 'a';
edwith[1] = 'b';
edwith[2] = 'c';
// Print average
printf("Average:%c, %c, %c\n", edwith[0],edwith[1],edwith[2]);
}
주의 : 문자열은 "" (큰따옴표)사용 , 문자를 저장 할때는 작은따옴표('')사용하기!
그리고 문자를 더한다면 자동으로 아스키코드로 바뀌어서 계산된다.
#include <cs50.h>
#include <stdio.h>
int main(void)
{
// Scores
char edwith[3] = "abc";
// Print average
printf("Average:%i\n", (edwith[0] + edwith[1]));
}
중단문자때문에 문자 개수 + 1이다.
[참고]
cs50 2019
https://bradbury.tistory.com/226
'CS > CS50' 카테고리의 다른 글
[CS50] 1. 컴퓨팅 사고 (1) | 2023.12.31 |
---|---|
[CS50] 6. 자료구조 (0) | 2023.06.17 |
[CS50] 5. 메모리 (1) | 2023.06.01 |
[CS50] 4. 알고리즘 (0) | 2023.05.31 |
[CS50] 2. C언어 (0) | 2023.05.25 |