Fear is a habit. I'm not afraid.
This is CS50 (3) Arrays 본문
Keyword
Compiling
Debugging
help50 and printf
debug50
check50 and style50
Data Types
Memory Arrays
Strings
Command-line arguments
Readability
Encryption
1. 컴파일링 compiling
#include <cs50.h>
#include <stdio.h>
int main(void)
{
string name = get_string("What's your name?\n");
printf("hello, %s\n", name);
}
1) int main(void)
- main이라는 함수
- 프로그램의 시작점으로써 실행 버튼을 클릭하는 것과 같음
2) #include <stdio.h>
- 라이브러리, 누군가 이미 작성해둔 코드
- 정확하게 말하면 stdio.h는 헤더 파일로 C언어로 작성되어 있으며 파일명이 .h로 끝나는 파일
- 이 파일에는 printf 함수의 프로토타입이 있어서 Clang 컴파일러가 프로그램을 컴파일할 때 printf가 무엇인지 알려주는 역할
3) ; (세미콜론)
- 새 줄을 의미
- 커서가 다음 줄로 넘어가도록 한다.
4) #include <cs50.h>
- cs50 라이브러리를 사용한 프로그램을 컴파일할 때는 clang에 또 하나의 프로그램이 필요함: lcs50
clang -o hello hello.c -lcs50
./hello
5) clang
- 소스코드를 머신코드로 변경해주는 실행어
6) -o hello hello.c
- hello.c 파일을 컴파일하고, hello라는 이름의 실행파일을 생성
7) -lcs50
- clang에게 cs50 라이브러리에 있는 모든 0과 1을 연결하라는 것을 의미
make hello
./hello
8) make hello
위의 긴 명령어와 의미가 동일함.
[ 컴파일링 compiling 단계 ]
1. preprocessing
2. compiling
3. assembling
4. linking
1. 전처리 preprocessing
- 소스코드가 있을 때, 맨 위의 두 줄은 두 라이브러리 파일을 추가하라고 말하고 있음.
- # hash 기호로 시작하는 이 두 줄은 해당 파일의 실제 코드로 대체된다.
- 즉, cs50을 추가하는 첫 줄 대신 clang이 cs50.h 파일에 직접 들어가 해당되는 코드를 가져와서 hello.c라는 파일에 붙여넣는 것
ex)
...
string get_string(string prompt);
int printf(string format, ...);
...
int main(void)
{
string name = get_string("What's your name?\n");
printf("hello, %s\n", name);
}
2. 컴파일링 compiling
- 소스 코드를 어셈블리어로 바꾸는 단계, 더 구체적인 단계를 뜻함.
- 프로그램이 컴파일될 때 어셈블리 코드로 바뀜
...
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
...
- clang에 의해 컴파일되면 C로 작성된 소스 코드는 어셈블리 코드라는 중간 단계로 바뀌고, CPU가 실제로 이해할 수 있는 언어에 가까워짐.
3. 어셈블링 assembling
- 실제 0과 1로 이루어진 머신 코드로 변경하는 단계
4. 연결 linking
- 3개의 다른 파일들을 clang이 컴파일해야 함
- 0과 1들을 하나의 큰 파일로 합치는 단계
Q. 만약 컴파일링 과정을 거치지 않기 위해 바로 머신코드로 우리가 원하는 프로그램을 작성하려고 한다면 어떤 문제가 있을까요?
컴파일러 vs 인터프리터
인터프리터란?
- 프로그램 생성 -> 파일 연결이나 머신 코드 생성이 필요하지 않음 -> 소스를 하나씩 실행함
컴파일러의 장점
- 컴파일러는 코드가 미리 머신 코드로 변환되기 때문에 최적화되고 효율적인 코드 실행이 된다. 즉, 프로그램 실행 속도가 빨라지고 런타임 오버헤드가 줄어든다. 또한 컴파일된 프로그램은 소스 코드를 공개하지 않고 배포할 수 있다.
- 더 작은 메모리를 사용한다.
- CPU 사용률이 낮다.
컴파일러의 단점
- 컴파일 단계에서 사전 지연이 발생하고 변경 사항은 적용하려면 다시 컴파일해야 한다.
- 실행 중에 사람이 읽을 수 있는 소스 코드가 없기 때문에 컴파일된 코드를 디버깅하기가 어렵다.
인터프리터의 장점
- 디버깅하기 쉽다
- 메모리를 자동으로 관리할 수 있어 메모리릭에 대한 위험이 줄어든다.
- 인터프리터로 작성된 언어는 컴파일러보다 더 유연하다.
인터프리터의 단점
- 해당 인터프리터된 프로그램만 실행할 수 있다.
- 인터프리터로 작성된 코드는 컴파일러로 작성된 코드보다 느리게 실행된다.
속도?
- 프로세스를 고려할 때 인터프리터는 컴파일러보다 빠르다. 그러나 프로그램이 이미 컴파일된 경우에는 컴파일된 프로그램의 실행이 인터프리터로 작성된 프로그램보다 빠르다.
4-5. 배열 Array
동적 배열의 단점
1. 동적 배열은 크기를 조정하는 속도가 느릴 수 있다.
- 동적 배열의 요소 수가 증가하여 배열의 크기를 조정해야 하는 경우 전체 배열을 메모리의 새 위치로 이동해야 하므로 시간과 리소스 측면에서 비용이 많이 듦.
2. 동적 배열은 필요할 때 더 많은 요소를 저장할 수 있도록 추가 공간을 할당해야 하므로 꼭 필요한 것보다 더 많은 메모리를 할당해야 하는 경우가 많다. 이로 인해 메모리가 낭비될 수 있다.
3. 동적 배열은 서로 다른 유형의 요소를 저장할 수 없다.
- 모든 배열과 마찬가지로 동적 배열은 같은 데이터 타입에 대한 데이터만 저장할 수 있으므로 동일한 배열에 서로 다른 유형의 요소를 저장할 수 없다. 서로 다른 유형의 요소를 저장해야 하는 경우에는 다른 데이터 구조를 사용해야 한다.
6. 문자열과 배열 Strings
- 실제로는 바이트가 아니라 배열로 저장이 된다.
- string은 정해진 크기를 가질 수 없다.
- 길이가 3인 문자열은 사실 4바이트를 차지한다. (\0의 null문자를 포함하여)
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]);
- 첫 번째는 이름들의 배열의 인덱스를 나타내고, 배열의 인덱스는 어떤 배열의 특정 위치를 의미한다.
Q. 널 종단 문자는 왜 필요할까요?
널 문자가 없으면 strcat, strncpy와 같은 함수는 문자열이 끝나는 위치를 알지 못하고 인접 메모리를 계속 읽어서 정의되지 않은 동작이 발생할 수 있다.
널 문자는 문자열의 경계를 표시하여 버퍼 오버플로 및 메모리 액세스 위반을 방지하는 데 도움이 된다.
7. 문자열의 활용 Strings
- 소문자를 대문자로 바꾸는 코드 (ASCII 규칙을 이용함)
#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");
}
8. 명령행 인자 Command-line arguments
#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");
}
}
main 함수는 0을 반환한다. (0 = 문제없음)
argv[0]에는 처음 입력하는 프로그램 이름이 들어간다.
Q. 명령행 인자는 프로그램의 확장성에 어떤 도움이 될까요? 구체적인 예시를 떠올려보세요.
- 유연성 향상: 명령행 인자를 사용하면 사용자가 프로그램 실행 시 다양한 옵션을 설정할 수 있습니다. 예를 들어, 파일 경로, 구성 설정, 출력 형식 등을 명령행 인자로 받아 프로그램을 더 유연하게 실행할 수 있습니다.
- 재사용성 증가: 다양한 설정과 옵션을 명령행 인자로 받아들이면, 동일한 프로그램을 여러 가지 상황에 맞게 재사용할 수 있습니다. 이렇게 하면 중복 코드를 줄이고 프로그램의 재사용성을 높일 수 있습니다.
- 자동화 및 스크립트 통합: 명령행 인자는 프로그램을 스크립트와 쉽게 통합할 수 있게 해줍니다. 이를 통해 복잡한 작업을 자동화할 수 있으며, 배치 작업이나 크론 작업 등에서 유용하게 사용할 수 있습니다.
- 테스트 용이성: 명령행 인자는 다양한 입력 시나리오를 테스트하기 쉽게 만듭니다. 여러 가지 입력 값을 빠르게 변경하면서 프로그램의 동작을 확인할 수 있기 때문에, 테스트를 효율적으로 수행할 수 있습니다.
- 사용자 편의성 향상: 프로그램 사용자에게 더 많은 제어 권한을 제공합니다. 사용자는 명령행 인자를 통해 프로그램의 동작 방식을 쉽게 조정할 수 있습니다. 이는 사용자 경험을 향상시키는 중요한 요소입니다.
- 확장성: 새로운 기능이나 옵션을 추가할 때 명령행 인자를 통해 쉽게 확장할 수 있습니다. 코드 구조를 크게 변경하지 않고도 새로운 인자를 추가하는 방식으로 프로그램을 확장할 수 있습니다.
'basic' 카테고리의 다른 글
메모리 아키텍처에 대하여 (폰 노이만 아키텍처, 하버드 아키텍처) (0) | 2025.04.30 |
---|---|
This is CS50 (4) Algorithms (0) | 2024.06.06 |
This is CS50 (2) C (0) | 2024.05.30 |
심화-캐리와 오버플로우의 차이점 (1) | 2024.05.30 |
This is CS 50 (1) Computational Thinking, Scratch (0) | 2024.05.28 |