문제를 해결한다는 것
- 컴퓨터 과학(Computer Science)에서는 문제를 해결하는 과정을
- 입력(Input) → 처리(Processing) → 출력(Output) 이라는 흐름으로 다룬다.
- 주어진 문제를 컴퓨터가 이해할 수 있는 방식으로 정의하고,
- 입력을 받아 연산/처리하여 원하는 결과를 출력하는 것이 기본 구조다.
정보의 표현 방법
- 컴퓨터는 정보를 표현할 때 2진법(Binary) 을 사용한다.
- 우리가 일상적으로 쓰는 10진법(Decimal) 과 컴퓨터가 사용하는 2진법(Binary) 은 표현 방식이 다르다.
예시:
- 10진법 123은
- 1 × 10² + 2 × 10¹ + 3 × 10⁰ = 123
- 2진법 101은
- 1 × 2² + 0 × 2¹ + 1 × 2⁰ = 4 + 0 + 1 = 5 (10진법)
비트(Bit)와 바이트(Byte)
비트 (Bit)
- Bit는 컴퓨터가 다룰 수 있는 가장 작은 정보 단위이다.
- 1비트는 0 또는 1 둘 중 하나의 값만 가질 수 있다.
- 이는 전기가 흐른다/흐르지 않는다, 참/거짓, 켜짐/꺼짐 같은 상태를 표현할 수 있다.
바이트 (Byte)
- 1 Byte = 8 Bit
- 바이트는 비트보다 훨씬 많은 정보를 표현할 수 있게 한다.
- 1 Byte로 표현할 수 있는 경우의 수는 2⁸ = 256개이다.
- (0부터 255까지 표현 가능)
- 만약 부호(양수/음수)를 구분한다면, 보통
- -128 ~ +127 범위 (2⁷ = 128, 양/음수 각각 128개)로 나눈다.
2진수, 8진수, 16진수: 왜 16진수를 많이 사용하는가?
2진수(Binary)
- 컴퓨터 내부에서는 모든 데이터를 2진수(0과 1) 로 처리한다.
- 그러나 사람이 2진수를 읽고 쓰기는 너무 길고 비효율적이다.
8진수(Octal)
- 2진수 3자리를 1개의 8진수 숫자로 변환할 수 있다.
- 예: 2진수 101 110 → 8진수 5 6
- 2진수보다 짧게 표현할 수 있어서 과거 일부 시스템(예: 옛날 유닉스 등)에서 사용했다.
16진수(Hexadecimal)
- 2진수 4자리를 1개의 16진수 숫자로 변환할 수 있다.
- 예: 2진수 1101 1110 → 16진수 DE
- 16진수는 숫자 0~9, 그리고 A(10), B(11), C(12), D(13), E(14), F(15)를 사용한다.
왜 16진수가 많이 쓰이는가?
- 4비트(2⁴ = 16)를 딱 1개로 표현할 수 있어서 CPU 구조와 잘 맞는다.
- 2진수보다 짧고 가독성이 좋다 (예를 들어 주소값, 메모리 덤프를 볼 때)
- 컴퓨터의 기본 단위(바이트, 워드) 와 호환성이 매우 뛰어나다.
→ 요약:
2진수를 간결하고 구조적으로 표현하기에 16진수가 가장 효율적이다.
비트 연산: 비트 단위로 계산하는 이유와 효율성
비트 연산이란?
- AND, OR, XOR, NOT 같은 연산을 비트 단위로 수행하는 것.
- 각각 0과 1의 조합에 따라 결과를 만드는 간단하고 빠른 연산이다.
왜 비트 연산을 많이 사용할까?
- 빠르다
- CPU는 0과 1 신호(전기)만 이해한다.
- 비트 단위 연산은 하드웨어 수준에서 거의 즉시 처리할 수 있다.
- 메모리와 자원을 아낀다
- 하나의 8비트(1바이트) 데이터에 여러 정보를 “압축”해서 저장하고 관리할 수 있다.
- 예) 8개의 true/false 값을 하나의 바이트로 표현
- 제어가 쉽다
- 특정 비트를 켜거나 끄는 것(Set/Reset)이 매우 직관적이다.
- 예) 특정 권한 플래그를 설정할 때
- 네트워크/통신 최적화
- 프로토콜이나 데이터 포맷이 비트 수준에서 정의되는 경우가 많다.
→ 요약:
비트 연산은 속도, 메모리 효율, 제어 편의성 면에서 최고의 성능을 낸다.
메모리와 저장 단위: 1KB = 1024B, 1MB = 1024KB 같은 개념
기본 단위
- 1 Bit (b): 0 또는 1
- 1 Byte (B): 8 Bits
메모리 크기 단위
| 1 Kilobyte (KB) | 1024 Bytes |
| 1 Megabyte (MB) | 1024 Kilobytes |
| 1 Gigabyte (GB) | 1024 Megabytes |
| 1 Terabyte (TB) | 1024 Gigabytes |
주의: 1KB = 1000B가 아니라 1024B이다.
왜냐하면 2진법 기반(2¹⁰ = 1024)이기 때문이다.
최근의 혼동
- 디스크 제조업체들은 10진법(1KB = 1000B) 기준을 쓸 때도 있는데,
- 컴퓨터 운영체제는 여전히 2진법(1KB = 1024B) 기준을 주로 사용한다.
- 이 차이로 인해 실제 저장 용량이 표기보다 약간 작게 보일 수 있다.
→ 요약:
메모리 단위는 2진법 기반으로 1024를 기준으로 증가한다.
부호 비트(Signed bit): 정수를 표현할 때 맨 앞 비트를 부호로 사용하는 방법
부호 없는 정수 (Unsigned Integer)
- 모든 비트를 숫자 자체를 나타내는 데 사용한다.
- 예) 8비트에서 0 ~ 255까지 표현 가능 (2⁸ = 256)
부호 있는 정수 (Signed Integer)
- 가장 왼쪽 비트(최상위 비트, MSB) 를 부호(Sign) 로 사용한다.
- 0이면 양수
- 1이면 음수
- 나머지 비트는 수의 크기를 나타낸다.
8비트 예시
비트 | 패턴값
| 0000 0000 | 0 |
| 0111 1111 | +127 |
| 1000 0000 | -128 |
| 1111 1111 | -1 |
→ 부호 비트를 사용하면 표현 범위가 절반으로 줄어든다.
- 부호 없는 8비트: 0 ~ 255
- 부호 있는 8비트: -128 ~ +127
왜 부호 비트를 사용하는가?
- 실수는 플러스/마이너스 양쪽 모두를 다루어야 할 때가 많다.
- 실세계 숫자 모델링(온도, 속도, 금융 등)에 자연스럽게 대응할 수 있다.
→ 요약:
부호 비트는 양수와 음수를 구분하기 위해 최상위 비트를 따로 사용하는 방법이다.
컴퓨터는 어떻게 문자를 이해하고 표현할까?
문자 표현: ASCII와 유니코드
- 컴퓨터는 숫자(0과 1)만 이해할 수 있다.
- 문자를 표현하기 위해, 각 문자에 고유한 숫자 코드를 대응시키는 약속을 만들었다.
ASCII (American Standard Code for Information Interchange)
- ASCII는 미국 표준 정보 교환 코드이다.
- 대문자 A는 숫자 65로 정해져 있으며,
- 65는 2진수로 01000001로 표현된다.
- 예를 들어:
- 72 → ‘H’
- 73 → ‘I’
- 33 → ‘!’
- 72, 73, 33 이라는 숫자 배열은 “HI!” 라는 문자를 나타낸다.
키보드로 문자를 입력할 때도, 컴퓨터는 숫자를 읽어 문자로 해석하는 것이다.
한계와 확장: 유니코드 (Unicode)
- ASCII는 8비트(1바이트)를 사용해 총 256개(0~255) 문자를 표현할 수 있다.
- 그러나 전 세계 언어, 다양한 기호, 이모티콘 등을 모두 표현하기에는 부족하다.
- 그래서 더 많은 문자를 표현할 수 있도록 유니코드가 만들어졌다.
- 예를 들어:
- 😂 (웃으며 눈물 흘리는 이모티콘)은 128514라는 코드로 표현된다.
- 이 숫자는 2진수로 11111011000000010 이다.
유니코드는 훨씬 더 많은 문자를 0과 1로 표현할 수 있게 해준다.
그림과 사진: RGB 색상과 픽셀
- 사진이나 그림은 수많은 점(픽셀) 으로 구성되어 있다.
- 컴퓨터는 각 점을 RGB 색상으로 표현한다.
RGB 색상
- RGB는 Red(빨강), Green(초록), Blue(파랑) 의 약자이다.
- 각각의 색에 대해 0~255 범위의 값을 설정하여 색을 만든다.
- 예) (255, 0, 0) → 순수한 빨간색
- 예) (0, 255, 0) → 순수한 초록색
- 예) (0, 0, 255) → 순수한 파란색
- 예) (255, 255, 255) → 흰색
- 각 픽셀은 빨강/초록/파랑 값 각각을 따로 저장하므로,
- 1 픽셀을 표현하려면 최소 3개의 값이 필요하다.
어떻게 표현하나?
- RGB 각각은 8비트(1바이트)로 표현한다.
- 즉, 하나의 픽셀은 총 24비트(3바이트)로 저장된다.
GIF 파일: 움직이는 이미지는 어떻게 만들어질까?
- GIF 파일은 여러 장의 사진을 빠르게 교체해서 움직이는 것처럼 보이게 만든다.
- 1초에 5~10장 정도의 이미지를 반복해서 보여주는 방식이다.
- 각각의 이미지는 여전히 점(픽셀)들의 색상 정보로 구성되어 있다.
Unicode 인코딩 방식: UTF-8과 UTF-16
UTF-8 (가장 널리 쓰이는 방식)
- 8비트 단위로 문자를 저장하는 방식
- 영어(ASCII 범위)는 1바이트로 저장하고,
- 다른 문자들은 2~4바이트를 사용한다.
- 기존 ASCII 파일과 호환되면서 전 세계 문자도 지원할 수 있다.
예시:
- ‘A’ → 1바이트 (01000001)
- ‘가’ → 3바이트
- 😂 → 4바이트
웹 페이지, 서버 통신 등 대부분 UTF-8을 기본으로 사용한다.
UTF-16
- 16비트(2바이트) 단위로 문자를 저장하는 방식
- 대부분의 문자(한글 포함)는 2바이트에 저장되고,
- 아주 복잡한 이모티콘 같은 문자는 4바이트로 저장된다.
예시:
- ‘A’ → 2바이트
- ‘가’ → 2바이트
- 😂 → 4바이트
Windows 운영체제 내부는 주로 UTF-16을 사용한다.
이미지 압축 방식: JPEG, PNG
비압축 이미지의 문제
- 사진은 수많은 점(픽셀)으로 구성된다.
- 각 픽셀은 빨강(R), 초록(G), 파랑(B) 값으로 색상을 저장한다.
- 아무 압축 없이 저장하면 파일 크기가 엄청 커진다. (수십 MB~수백 MB)
압축의 필요성
- 용량을 줄이고 저장 및 전송을 빠르게 하려면 압축이 필요하다.
- 압축에는 두 종류가 있다:
- 무손실 압축(Lossless): 원본 100% 복원 가능
- 손실 압축(Lossy): 일부 정보를 버리지만 사람이 보기엔 거의 차이 없음
PNG (Portable Network Graphics)
- 무손실 압축을 사용하는 이미지 포맷
- 픽셀 하나하나의 색상 정보를 모두 유지한다.
- 배경 투명(알파 채널)을 지원한다.
특징:
- 품질이 매우 뛰어나지만 파일 크기는 다소 크다.
- 그래픽, 로고, 아이콘, 투명한 이미지에 적합하다.
JPEG (Joint Photographic Experts Group)
- 손실 압축을 사용하는 이미지 포맷
- 사람이 구분하기 어려운 부분의 정보를 제거해 파일 크기를 대폭 줄인다.
- 품질을 어느 정도 손해 보더라도 저장 공간을 절약할 수 있다.
특징:
- 사진, 풍경, 인물 사진 등에 적합하다.
- 품질(압축률)을 조절할 수 있다.
- 압축을 심하게 걸면 화질이 눈에 띄게 깨질 수 있다. (“깨짐” 현상)
출력(Output)
입력에서 출력까지: 문제 해결의 과정
- 컴퓨터가 문제를 해결하는 과정은 기본적으로
- 입력(Input) → 처리(Processing) → 출력(Output) 으로 이루어진다.
- 이 과정에서 어떻게 처리할지를 정해주는 것이 바로 알고리즘(Algorithm) 이다.
알고리즘이란?
- 알고리즘이란 “문제를 해결하기 위한 일련의 단계적인 절차“를 의미한다.
- 입력을 받아 처리하고 원하는 출력을 얻기까지 필요한 과정을 구체적으로 설명하는 방법이다.
전화번호부에서 ‘Smith’를 찾는 알고리즘
1. 가장 단순한 방법: 선형 탐색 (Linear Search)
- 전화번호부 1페이지부터 한 장씩 넘기며 ‘Smith’를 찾는다.
- 시간은 전화번호부 크기에 비례해서 늘어난다. (최악의 경우 1024장 모두 찾아야 한다)
이것도 엄연히 하나의 알고리즘이다. (다만 비효율적이다.)
2. 더 효율적인 방법: 이진 탐색 (Binary Search)
- 전화번호부를 항상 절반씩 나눠가면서 원하는 페이지를 좁혀나가는 방법.
- 예를 들어:
- 1024장의 전화번호부 → 중간(512쪽)을 펼쳐본다.
- ‘Smith’가 앞쪽이면 앞 절반으로 이동,
- 뒷쪽이면 뒤 절반으로 이동.
- 이렇게 매번 범위를 절반으로 줄이면 10단계(2¹⁰ = 1024) 이내에 찾을 수 있다.
→ 페이지 수가 커져도, 검색 시간은 조금만 증가한다!
직관적인 의사코드 (Pseudocode)
의사코드란, 사람이 이해하기 쉽게 실제 코드가 아닌 자연어 스타일로 알고리즘을 표현하는 것이다.
정리된 의사코드
1. 전화번호부를 든다.
2. 전화번호부의 중간 페이지를 연다.
3. 페이지를 살펴본다.
4. 만약 'Smith'를 찾았다면:
- 전화를 건다.
5. 그렇지 않고, 'Smith'가 앞쪽에 있다면:
- 앞쪽 절반의 중간 페이지를 연다.
- 3단계로 돌아간다.
6. 그렇지 않고, 'Smith'가 뒷쪽에 있다면:
- 뒷쪽 절반의 중간 페이지를 연다.
- 3단계로 돌아간다.
7. 만약 전화번호부에 'Smith'가 없다면:
- 탐색을 종료한다.
의사코드 요소
- 함수(Function): 어떤 작업을 수행하는 독립적인 블록으로 동사를 나타낸다.
- 예) '든다', '연다', '본다', '건다', '종료한다'는 함수처럼 하나의 동작을 의미
- 조건문(Condition): 상황에 따라 다른 동작을 하게 만드는 문장
- 예) 만약 ~면, 그렇지 않고 ~이면
- 불리언(Boolean): 참(True) 또는 거짓(False) 값을 가지는 표현
- 예) ‘Smith가 있다’ → 참(True)
- 반복문(Loop): 조건이 만족될 때까지 같은 작업을 반복하는 구조
- 예) loop: 3단계로 돌아간다.
- 변수(Variable): 데이터를 저장하는 공간
- 스레드(Thread): 프로그램의 흐름 단위 (초급에서는 깊게 다루지 않아도 괜찮습니다)
- 이벤트(Event): 특정 행동(클릭, 키 입력 등)이 발생했을 때 반응하는 구조
- 이 외에도 여러 요소가 있다.
왜 이런 구조로 설명할까?
- 컴퓨터는 0과 1로 동작하지만,
- 사람은 절차적 사고(순서, 조건, 반복) 로 문제를 이해한다.
- 의사코드는 사람과 컴퓨터 사이의 다리(Bridge) 역할을 한다.
- 이런 사고방식을 익히면 프로그래밍 언어(Java, Python, C 등)를 배울 때 훨씬 수월하다.
Scratch란?
- Scratch는 MIT Media Lab에서 만든
- 초등학생부터 성인까지 누구나 쉽게 프로그래밍을 배울 수 있도록 설계된 시각적 프로그래밍 언어이다.
- 퍼즐 조각처럼 생긴 블록들을 끌어다 붙이면서 프로그램을 완성한다.
- 문법 오류가 없고,
- 구조적 사고(절차, 반복, 조건 등) 를 자연스럽게 배울 수 있도록 만들어졌다.
공식 사이트:
Scratch - Imagine, Program, Share
Scratch is a free programming language and online community where you can create your own interactive stories, games, and animations.
scratch.mit.edu
Scratch의 블록 색깔과 모양: 의미 정리
Scratch에서는 블록의 색깔, 모양, 연결 방식이 각각 특정 역할을 뜻합니다.
1. 색깔별 블록 종류
| 파랑 (Motion) | 움직임 | 스프라이트를 이동, 회전 |
| 보라 (Looks) | 외형 | 스프라이트의 모양, 말풍선, 숨기기 등 |
| 분홍 (Sound) | 소리 | 소리 재생, 음량 조절 |
| 노랑 (Events) | 이벤트 | 클릭, 키보드 입력 등 시작 신호 처리 |
| 주황 (Control) | 제어 | 반복(Loop), 조건(If), 기다리기(Wait) |
| 초록 (Sensing) | 감지 | 터치 감지, 키 입력 감지, 마우스 위치 등 |
| 청록 (Operators) | 연산 | 수학 연산, 비교, 논리 연산 |
| 연보라 (Variables) | 변수 | 값 저장, 변수 만들기 |
| 연한 청록 (My Blocks) | 사용자 정의 블록 | 자신만의 블록(함수) 만들기 |
2. 모양별 의미
| 퍼즐 조각처럼 끼우는 블록 (Command) | 명령을 수행한다 | move 10 steps, say "Hello" |
| 육각형 모양 블록 (Boolean) | 참/거짓을 반환한다 | touching edge?, key pressed? |
| 마름모꼴 모양 블록 (Reporter) | 값을 반환한다 | x position, mouse x, pick random 1 to 10 |
| 모서리가 둥근 블록 (Hat) | 프로그램을 시작한다 | when green flag clicked, when key pressed |
3. 중요한 개념 추가 정리
- 변수(Variable):예를 들어 “점수”, “속도” 같은 값을 저장하고 읽을 수 있다.
- 정보를 저장하는 “그릇”이다.
- 조건문(Condition):Scratch에서는 육각형(Boolean 블록)과 함께 사용된다.
- 어떤 상황이 “참”일 때만 실행된다.
- 반복문(Loop):Scratch에서는 repeat, forever 같은 주황색 블록이 반복문이다.
- 명령을 여러 번 반복하게 만든다.
- 이벤트(Event):예) ‘녹색 깃발’을 클릭하면 프로그램이 시작된다.
- 특정 사건이 발생했을 때 프로그램이 시작되는 것.
- 연산자(Operator):
- 수학 계산(+, -, *, /)이나 논리 비교(>, =, <)를 한다.
C 언어 시작하기
1. C 프로그램의 기본 구조
- C 프로그램을 작성할 때는 항상 다음과 같은 기본 구조를 사용한다:
#include <stdio.h> // 표준 입력/출력 함수 사용 선언
int main(void) // 프로그램의 시작 지점
{
printf("hello, world"); // 화면에 "hello, world" 출력
}
- int main(void)
- 프로그램의 “시작”을 알리는 부분이다.
- 스크래치에서 녹색 깃발(시작 버튼) 과 같은 역할이다.
- printf()
- “출력(print)“을 의미한다.
- 함수 이름의 f는 “formatted(형식화된)” 를 뜻한다.
- 단순히 글자를 출력하는 것뿐만 아니라, 숫자나 변수를 특정 형식으로 출력할 수 있다.
- 스크래치에서는 “say 블록” 에 해당한다.
- 파일 확장자:
- C 프로그램 파일은 반드시 .c 확장자를 가진다. (예: hello.c)
2. C 프로그램 실행 방법
C 프로그램을 컴퓨터가 이해할 수 있게 만들려면?
- 컴퓨터는 0과 1(기계어) 만 이해할 수 있다.
- 우리가 작성하는 소스코드(C, C++, Java 등)는 그대로는 실행할 수 없다.
- 따라서 “컴파일러” 라는 프로그램이 필요하다.
컴파일러: 소스코드를 기계어(0과 1)로 번역하는 프로그램
clang
- clang은 C 코드를 컴파일하는 프로그램이다.
- 무료이며, 대부분의 운영체제(특히 macOS)에서 쉽게 사용할 수 있다.
- clang 외에도 GCC, MSVC, TCC 등 다양한 컴파일러가 있다.
3. 터미널에서 프로그램 실행하기
1단계: 컴파일하기
- 터미널(명령줄)에 다음 명령어를 입력한다:
clang 파일명.c
- 예를 들어:
clang hello.c
- 그러면 a.out 이라는 파일이 생성된다.
- a.out은 컴퓨터가 이해할 수 있는 2진법(기계어) 로 번역된 실행 파일이다.
2단계: 실행하기
- 터미널에 다음을 입력해 실행한다:
./a.out
- ./는 “현재 폴더에 있는 실행 파일”을 뜻한다.
- 결과:
hello, world%
4. 왜 % 기호가 붙을까?
- printf("hello, world"); 뒤에 줄바꿈이 없기 때문이다.
- 출력 후 커서가 바로 다음에 붙어서 % 표시가 이어서 나타난다.
- 해결 방법:
- 줄바꿈 문자(\n) 를 출력에 추가한다.
#include <stdio.h>
int main(void)
{
printf("hello, world\n");
}
- 이렇게 하면 출력 결과가 깔끔하게 다음 줄로 넘어간다.
5. 프로그램을 수정하고 다시 실행하려면?
- 소스 코드를 고쳤다면 다시 컴파일하고 다시 실행해야 한다.
clang hello.c
./a.out
- 고친 결과를 보기 위해서는 항상 컴파일 → 실행 순서를 따라야 한다.
6. 컴파일 시 실행 파일 이름 직접 정하기
- a.out 대신 원하는 이름으로 실행 파일을 만들 수도 있다:
clang -o hello hello.c
- 이 명령은 hello라는 이름의 실행 파일을 생성한다.
- 실행 방법:
./hello
7. 터미널 명령어
명령어 | 의미 | 설명
| ls | List | 현재 폴더(디렉터리) 안의 파일 목록을 보여준다 |
| rm 파일명 | Remove | 파일을 삭제한다 |
| mkdir 폴더명 | Make Directory | 새 폴더를 만든다 |
| rmdir 폴더명 | Remove Directory | 빈 폴더를 삭제한다 |
외울 필요는 없지만, 기본적인 명령어를 익혀두면 터미널에서 컴퓨터를 빠르고 효율적으로 조작할 수 있다.
- C 프로그램은 int main(void) {} 로 시작한다.
- printf() 함수로 출력을 수행한다.
- 컴파일러(clang) 를 사용해 소스 코드를 기계어로 변환한다.
- 변환된 실행 파일(a.out 또는 지정 이름)을 터미널에서 실행한다.
- 줄바꿈(\n) 을 주의해서 깔끔한 출력을 만든다.
- 기본적인 터미널 명령어도 함께 익혀두면 좋다.
C 언어에서 사용자 입력 받기
1. Scratch의 ask와 C의 대응
- 스크래치에서 ask [질문] and wait 블록은
- → 사용자에게 질문하고, 답변을 입력받는 역할을 한다.
- C 언어에서는 비슷한 기능을 get_string 함수로 구현할 수 있다.
- get_string은 사용자에게 문자열 입력을 받는다.
2. 할당(Assignment) 연산자
- 수학에서는 = 기호가 “같다”는 의미이지만,
- C 언어에서는 = 기호가 “값을 할당한다” 는 의미이다.
- 그래서 =를 할당 연산자(Assignment Operator) 라고 부른다.
예시:
int x = 5; // 변수 x에 5를 할당
3. 서식 지정자 (Format Specifier)
- printf() 함수를 사용할 때
- 입력값을 원하는 형태로 출력할 수 있도록 서식 지정자를 사용한다.
주요 서식 지정자:
| %s | 문자열 (string) 출력 |
| %d | 정수 (decimal) 출력 |
| %f | 실수 (floating point) 출력 |
- \n은 줄바꿈(New Line) 문자이다.
예시:
printf("Hello, %s!\n", name);
- %s%s 같이 연속해서 사용하면 변수도 2개 입력해야 한다:
printf("%s %s\n", firstName, lastName);
4. 문자열(String)과 문자열 입력 문제
- string은 쌍따옴표 안에 있는 0개 이상의 문자들의 집합을 말한다.
5. C 프로그램 예시: CS50 라이브러리를 사용할 경우
#include <stdio.h>
#include <cs50.h> // get_string 함수를 사용하기 위해 필요
int main(void)
{
string answer = get_string("What's your name?\n");
printf("hello, %s\n", answer);
}
- 여기서 string 타입은 cs50.h 라이브러리 안에 정의되어 있다.
- 따라서 #include <cs50.h>를 반드시 추가해야 한다.
만약 #include <cs50.h>를 빼먹으면?
에러 메시지:
error: use of undeclared identifier 'string'
(컴파일러가 string이 뭔지 모른다는 의미)
cs50 라이브러리 설치 방법(macOS, Linux)
macOS 및 Linux:
- CS50 라이브러리 다운로드: GitHub의 CS50 libcs50 저장소 릴리스 페이지에서 최신 버전의 라이브러리 압축 파일(.zip 또는 .tar.gz)을 다운로드합니다.
- 압축 해제: 다운로드한 압축 파일을 원하는 위치에 압축 해제합니다.
- 설치: 터미널을 열고 압축을 해제한 폴더로 이동한 후 다음 명령어를 실행합니다.이 명령어는 라이브러리를 시스템 디렉터리에 설치하여 컴파일러가 찾을 수 있도록 합니다. 비밀번호를 입력하라는 메시지가 나타나면 관리자 비밀번호를 입력합니다.
-
cd libcs50-* sudo make install - VS Code 설정 (선택 사항): VS Code에서 C/C++ 확장 프로그램을 사용하고 있다면, 컴파일러가 헤더 파일을 찾을 수 있도록 include 경로를 설정해야 할 수 있습니다. c_cpp_properties.json 파일을 열어 include 경로에 /usr/local/include (일반적인 설치 경로)를 추가해 보세요.
1. .vscode 폴더 확인 또는 생성:
- VS Code에서 현재 작업 중인 프로젝트 폴더가 열려 있는지 확인하세요.
- 프로젝트 폴더 내에 .vscode라는 이름의 폴더가 있는지 확인합니다.
- 만약 .vscode 폴더가 없다면, VS Code의 탐색기 (왼쪽 사이드바)에서 프로젝트 폴더를 마우스 오른쪽 버튼으로 클릭하고 "새 폴더"를 선택한 후 .vscode라고 이름을 지정하여 폴더를 생성합니다.
2. c_cpp_properties.json 파일 생성 (필요한 경우):
- .vscode 폴더가 생성되었거나 이미 있다면, 해당 폴더를 마우스 오른쪽 버튼으로 클릭하고 "새 파일"을 선택합니다.
- 새 파일의 이름을 c_cpp_properties.json으로 지정하고 Enter 키를 누릅니다.
3. c_cpp_properties.json 파일 내용 편집:
- c_cpp_properties.json 파일을 열면 텍스트 편집기가 나타납니다. 여기에 JSON 형식으로 설정을 입력해야 합니다.
- 기본적인 c_cpp_properties.json 파일 내용은 다음과 같은 형태를 가질 수 있습니다. 만약 파일에 이미 내용이 있다면, includePath 부분을 찾아서 수정하면 됩니다.
{
"configurations": [
{
"name": "Mac", // 또는 "Linux", "Win32" 등 개발 환경에 맞는 이름
"includePath": [
"${workspaceFolder}/**",
"/usr/local/include" // 여기에 CS50 헤더 파일 경로를 추가
],
"defines": [],
"compilerPath": "/usr/bin/clang", // 사용하는 컴파일러 경로 (본인 환경에 맞게 수정)
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "clang-x64" // 사용하는 IntelliSense 모드 (본인 환경에 맞게 수정)
}
],
"version": 4
}
- includePath 배열: 이 배열은 컴파일러가 헤더 파일을 검색할 디렉터리 목록을 지정합니다.
- ${workspaceFolder}/**: 현재 프로젝트 폴더와 그 하위 폴더를 포함합니다.
- /usr/local/include: 여기에 CS50 라이브러리의 헤더 파일이 설치된 경로를 추가합니다. 만약 다른 경로에 설치했다면 해당 경로로 수정해야 합니다. 여러 개의 include 경로를 추가하고 싶다면 쉼표로 구분하여 배열에 나열하면 됩니다.
- compilerPath: 사용하는 C/C++ 컴파일러의 경로를 지정합니다. gcc 또는 clang의 경로를 확인하여 정확하게 입력해야 합니다. 터미널에서 which gcc 또는 which clang 명령어를 실행하면 경로를 확인할 수 있습니다.
- cStandard 및 cppStandard: 사용할 C 및 C++ 표준을 지정합니다.
- intelliSenseMode: IntelliSense 엔진의 모드를 설정합니다. 사용하는 컴파일러에 맞춰 선택합니다. m2를 사용 중이면, "macos-clang-arm64"
4. 변경 사항 저장:
- c_cpp_properties.json 파일을 편집한 후 Ctrl + S (Windows/Linux) 또는 Cmd + S (Mac) 키를 눌러 변경 사항을 저장합니다.
예시:
만약 macOS 환경에서 CS50 라이브러리를 /usr/local/include에 설치했다면, includePath는 다음과 같이 설정될 수 있습니다.
"includePath": [
"${workspaceFolder}/**",
"/usr/local/include"
],
Linux 환경에서 다른 경로에 설치했다면 해당 경로를 /usr/local/include 대신 사용해야 합니다.
확인:
c_cpp_properties.json 파일을 저장한 후, C 코드를 다시 열어보세요. C/C++ 확장 프로그램이 업데이트되어 CS50 헤더 파일을 인식하고 IntelliSense 기능이 정상적으로 작동하는지 확인해 볼 수 있습니다. 예를 들어 <cs50.h>를 #include 했을 때 오류가 표시되지 않거나, get_string 함수에 대한 자동 완성이 나타나는지 확인해 보세요.
DYLD_LIBRARY_PATH 설정하기 (Zsh):
- ~/.zshrc 파일 열기: 터미널에서 다음 명령어를 사용하여 텍스트 편집기 (예: nano, vim, emacs)로 ~/.zshrc 파일을 엽니다. nano가 가장 사용하기 쉬울 수 있습니다.(만약 다른 편집기를 사용하고 싶다면 nano 대신 해당 편집기 명령어를 사용하세요.)
-
Bash
nano ~/.zshrc - 환경 변수 설정 줄 추가: 파일이 열리면, 파일 내용의 맨 아래에 다음 줄을 추가합니다.이미 DYLD_LIBRARY_PATH 관련 설정이 있다면, 해당 줄을 찾아서 /usr/local/lib:를 추가하거나, 경로가 올바르게 설정되어 있는지 확인하세요. 여러 경로를 추가하고 싶다면 콜론(:)으로 구분합니다.
-
Bash
export DYLD_LIBRARY_PATH="/usr/local/lib:$DYLD_LIBRARY_PATH" - 변경 사항 저장:
- nano: Ctrl + O 를 누르고 Enter 키를 눌러 저장합니다. 그런 다음 Ctrl + X 를 눌러 편집기를 종료합니다.
- vim: Esc 키를 누른 후 :wq 를 입력하고 Enter 키를 누릅니다.
- emacs: Ctrl + X 를 누른 후 Ctrl + S 를 눌러 저장하고, Ctrl + X 를 누른 후 Ctrl + C 를 눌러 종료합니다.
- ~/.zshrc 파일 적용: 변경된 설정을 현재 터미널 세션에 적용하려면 다음 명령어를 실행합니다.또는 현재 터미널 창을 닫고 새로운 터미널 창을 열면 자동으로 적용됩니다.
-
Bash
source ~/.zshrc
확인:
새로운 터미널 창을 열거나 source ~/.zshrc 명령어를 실행한 후, 다음 명령어를 입력하여 DYLD_LIBRARY_PATH 환경 변수가 제대로 설정되었는지 확인할 수 있습니다.
echo $DYLD_LIBRARY_PATH
출력 결과에 /usr/local/lib이 포함되어 있다면 영구 설정이 완료된 것입니다. 이제부터는 ./string 명령어를 실행할 때 DYLD_LIBRARY_PATH를 매번 설정할 필요 없이 CS50 라이브러리를 찾을 수 있을 거예요.
6. 일반적인 C (표준 C) 방식으로 입력 받기
CS50 라이브러리 없이 표준 C 함수만 이용하려면 다음처럼 작성한다:
#include <stdio.h>
#include <string.h> // strlen, strcspn 함수를 사용하기 위해 필요
int main(void)
{
char answer[50]; // 문자 배열 선언 (최대 49글자 + 널 문자)
printf("What's your name?\n");
fgets(answer, sizeof(answer), stdin); // 사용자 입력 받기
answer[strcspn(answer, "\n")] = 0; // fgets가 추가한 줄바꿈 문자를 제거
printf("hello, %s\n", answer);
}
- char answer[50]: 최대 50글자(문자열) 공간을 미리 확보
- fgets(): 문자열을 한 줄 단위로 입력받음
- strcspn(): 문자열 안에서 특정 문자의 인덱스를 찾아주는 함수
- \n 제거: 입력 후 줄바꿈이 남는 문제를 해결
fgets()는 안전하지만 줄바꿈(\n)이 따라오기 때문에 따로 제거하는 작업이 필요하다.
7. 컴파일 방법
CS50 라이브러리를 사용하는 경우
- 컴파일할 때 -lcs50 옵션을 추가해야 한다:
clang -o string string.c -lcs50
- -lcs50은 “cs50 라이브러리를 링크(link, -l)해라” 라는 뜻이다.
make 사용하기
- make는 소스 코드를 컴파일하고 링크하는 과정을 자동화하는 프로그램이다.
- make를 사용하면 명령어가 훨씬 간단해진다.
make string
- 이렇게 입력하면:
- string.c 파일을 찾고,
- 알아서 clang -lcs50 옵션을 붙여 컴파일하고,
- string이라는 실행 파일을 만들어준다.
make는 어떤 시스템에 있나?
- Linux, macOS는 기본적으로 make가 설치되어 있다.
- Windows에서는 WSL(Windows Subsystem for Linux) 또는 MinGW 같은 환경을 설치하면 사용할 수 있다.
변수 설정: Scratch의 set 블록
- 스크래치에서 set [변수] to [값] 블록은
- → C에서는 변수를 선언하고 초기화하는 코드와 같다.
C 코드 예시:
int counter = 0;
- int는 정수(integer) 를 저장하는 자료형이다.
- counter라는 변수에 0을 할당한다.
→ Scratch의 set 블록 == C의 자료형 변수명 = 값;
조건문: if, else if, else
C 코드 예시:
if (x < y)
{
printf("x is less than y\n");
}
else if (x == y)
{
printf("x is equal to y\n");
}
else
{
printf("x is not less than y\n");
}
- if: 첫 번째 조건을 검사한다.
- else if: 첫 번째 조건이 거짓이면 두 번째 조건을 검사한다.
- else: 모든 조건이 거짓이면 실행된다.
왜 else로 마무리할까?
- 여기서 (x > y) 는 사실 앞 조건들이 모두 거짓이면 “남은 경우”라서
- 명시적으로 검사할 필요 없이 else로 처리할 수 있다.
- 불필요한 비교 연산을 줄여서 CPU 사용량을 조금이라도 아낄 수 있다.
- 코드도 깔끔하고 가독성이 좋아진다.
→ 조건문을 쓸 때는 “확정된 나머지 경우”를 else로 처리하는 것이 좋다.
반복문: Scratch의 forever, repeat
- 스크래치에서 반복 구조는
- forever: 무한 반복
- repeat (n): n번 반복
이것은 C의 while 문과 대응된다.
while 문 예시
int i = 0;
while (i < 50)
{
i++; // i += 1; 과 같은 의미
printf("hello\n");
}
- 조건이 참일 동안 계속 반복한다.
- 여기서는 i가 50보다 작을 때 계속 "hello"를 출력하고,
- 매번 i를 1씩 증가시킨다.
for 문: 더 간결한 반복문
C에서는 for 문을 사용하면 같은 동작을 더 적은 코드로 표현할 수 있다.
for 문 예시
for (int i = 0; i < 50; i++)
{
printf("hello\n");
}
- for (초기화; 조건; 반복 후 동작)
- 초기화(int i = 0) → 조건(i < 50) → 동작 후 i 증가(i++) 를 반복한다.
→ 반복 횟수가 정해져 있을 때는 for 문이 더 읽기 쉽고 깔끔하다.
1. 주요 자료형(Data Types)
| bool | 참/거짓 | true 또는 false 값을 가진다. (CS50에서는 <cs50.h>로 제공) |
| char | 문자 | 단 하나의 문자 (예: ‘A’, ‘z’) |
| float | 소수점 | 부동 소수점 숫자 (정밀도 낮음) |
| double | 더 정밀한 소수점 | float보다 훨씬 더 많은 소수점 자리를 가질 수 있음 |
| int | 정수 | 보통 약 -20억 ~ +20억 범위 (4바이트) |
| long | 더 큰 정수 | int보다 훨씬 더 큰 수를 저장할 수 있음 (8바이트) |
| string | 문자열 | 여러 개의 문자의 집합 (실제로는 char 배열) |
2. 포맷 지정자 (Format Specifiers)
printf() 같은 함수에서 자료형에 맞게 값을 출력하려면, 포맷 지정자를 사용해야 한다.
| %c | char | 문자 하나 출력 |
| %f | float, double | 실수 출력 |
| %i | int | 정수 출력 |
| %li | long | 큰 정수 출력 |
| %s | string | 문자열 출력 |
포맷 지정자를 틀리게 쓰면 프로그램이 이상하게 동작하거나 에러가 발생할 수 있다.
3. 코드 예제: 입력 받고 출력하기
긴 코드: 가독성 낮음
printf("You are at least %i days old.\n", get_int("What's your age?\n"));
- 한 줄에 입력과 출력을 다 처리하니까
- 코드가 길고 읽기가 어렵다.
더 나은 코드: 가독성 향상
int age = get_int("What's your age?\n");
printf("You are at least %i days old.\n", age * 365);
- get_int()로 먼저 값을 받아서 저장하고,
- printf()에서 그 값을 사용한다.
- 코드를 분리하면 가독성이 좋아지고, 버깅(오류 찾기) 할 때도 쉬워진다.
또 더 좋은 코드: 의미 있는 변수 사용
int age = get_int("What's your age?\n");
int days = age * 365;
printf("You are at least %i days old.\n", days);
- 계산 결과를 days라는 새로운 변수에 저장했다.
- 변수를 의미 있게 이름 지으면 코드 읽기/관리하기 더 쉬워진다.
4. %f 소수점 자리수 조절하기
- %f는 기본적으로 6자리 소수점까지 출력한다.
- %.숫자f 형태로 소수점 자리수를 조절할 수 있다.
float price = get_float("What's the price?\n");
printf("Your total is %.2f.\n", price * 1.0625);
- %.2f → 소수점 이하 2자리만 출력한다.
자주 사용하는 예시: 금액, 평균 계산, 퍼센트 표시 등에서 소수점 자리수를 제한할 때 사용한다.
5. get_자료형() 함수의 특징 (CS50 라이브러리)
- get_int(), get_float(), get_string() 등 CS50의 get 함수들은
- 사용자가 자료형에 맞는 값을 입력할 때까지 계속 요청한다.
int age = get_int("What's your age?\n");
- 만약 나이 대신 "hello" 같은 글자를 입력하면,
- 프로그램이 오류를 내지 않고 “다시 입력해달라” 고 요청한다.
- 올바른 정수를 입력할 때까지 프로그램이 기다린다.
→ 초보자 프로그램이 쉽게 망가지지 않도록 도와주는 기능이다.
간단한 코드: 같은 문장을 여러 번 출력하기
직접 여러 번 작성
#include <stdio.h>
int main(void)
{
printf("cough\n");
printf("cough\n");
printf("cough\n");
}
- 간단하지만 비효율적이다.
- 같은 코드를 복사-붙여넣기 하는 건 유지보수에 좋지 않다.
for 문으로 반복
#include <stdio.h>
int main(void)
{
for (int i = 0; i < 3; i++)
{
printf("cough\n");
}
}
- for 문을 사용하면 반복을 간단하고 깔끔하게 처리할 수 있다.
함수(Function)를 사용해서 더 깔끔하게
함수를 만들고 호출
#include <stdio.h>
void cough(void);
int main(void)
{
for (int i = 0; i < 3; i++)
{
cough();
}
}
// 함수 정의
void cough(void)
{
printf("cough\n");
}
- void cough(void)는 cough라는 이름의 함수를 정의하는 것.
- main()은 코드의 시작점이고,
- cough()는 필요할 때 호출해서 사용하는 작은 단위다.
함수가 main 아래에 있으면 생기는 문제
에러 발생
- 만약 main() 위에 void cough(void); 선언을 안 쓰고,
- main() 아래에만 void cough(void)를 작성하면
error: call to undeclared function 'cough'; ISO C99 and later do not support implicit function declarations
- 이유:cough()를 호출하는 시점에 cough()가 아직 정의되어 있지 않으면, “모르는 함수”라며 에러를 낸다.
- C 컴파일러는 위에서 아래로 코드를 읽는다.
해결 방법: 함수 선언(프로토타입)
#include <stdio.h>
void cough(void); // 미리 함수의 형태를 알려줌 (함수 프로토타입)
int main(void)
{
for (int i = 0; i < 3; i++)
{
cough();
}
}
void cough(void)
{
printf("cough\n");
}
- void cough(void);를 위에 써주면, 컴파일러가 “아, 이런 함수가 뒤에 나오겠구나!” 라고 인식하게 된다.
함수에 인자(Argument) 넘기기
#include <stdio.h>
void cough(int n);
int main(void)
{
cough(3);
}
void cough(int n)
{
for (int i = 0; i < n; i++)
{
printf("cough\n");
}
}
- cough(int n)은 반복할 횟수(n) 를 입력받아서,
- 그만큼 "cough\n"을 출력한다.
→ 함수를 더 유연하게 사용할 수 있다.
사용자 입력받기 + 검증
get_positive_int() 함수 예시 (양의 정수 입력 받을 때)
#include <cs50.h>
#include <stdio.h>
int get_positive_int(void);
int main(void)
{
int i = get_positive_int();
printf("%i\n", i);
}
int get_positive_int(void)
{
int n;
do
{
n = get_int("Positive integer: ");
}
while (n < 1); // 1보다 작은 값이면 다시 입력 받음
return n;
}
설명
- int get_positive_int(void)는 정수를 반환(return)하는 함수다.
- do-while 문:
- 무조건 한 번은 실행하고, 그다음에 조건을 검사한다.
- (while은 조건을 먼저 검사하고 실행)
- int n:
- 아직 값이 없기 때문에 초기에는 쓰레기 값(garbage value) 을 가지고 있다.
printf로 간단한 출력 만들기 (?), (#)
? 반복 출력
#include <stdio.h>
#include <cs50.h>
int main(void)
{
int n;
do
{
n = get_int("Width: ");
}
while (n < 1);
for (int i = 0; i < n; i++)
{
printf("?");
}
printf("\n");
}
- 입력받은 수만큼 ?를 연달아 출력한다.
# 사각형 출력
#include <stdio.h>
#include <cs50.h>
int main(void)
{
int n;
do
{
n = get_int("Size: ");
}
while (n < 1);
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
printf("#");
}
printf("\n"); // 줄바꿈
}
}
- 이중 for 문을 이용해서 n × n 크기의 # 사각형을 만든다.
컴퓨터 메모리의 한계
- 컴퓨터에는 RAM(메모리) 이 있다.
- 메모리는 컴퓨터가 여러 작업을 잠시 기억하기 위해 사용하는 공간이다.
- 하지만 메모리는 한정된 크기만 가지고 있어서,
- 저장할 수 있는 데이터와 처리할 수 있는 숫자에는 한계가 있다.
부동 소수점(Floating Point)의 부정확성
#include <cs50.h>
#include <stdio.h>
int main(void)
{
float x = get_float("x: ");
float y = get_float("y: ");
printf("x / y = %.50f\n", x / y);
}
실행 결과:
x: 1
y: 10
x / y = 0.10000000149011611938476562500000000000000000000000
왜 1/10이 0.1이 아닐까?
- 1/10은 0.1로 생각할 수 있지만,0.1을 이진수로 완벽하게 표현할 수 없다.
- 컴퓨터는 0과 1만 이용해서 값을 표현하기 때문에
- float 타입은 32비트만 사용하기 때문에
- 소수점을 어느 순간까지만 정확하게 저장하고,
- 그 뒤부터는 오차가 발생한다.
- double 타입(64비트)은 float보다 더 많은 소수점 자리를 표현할 수 있지만,
- 그래도 완전히 정확하게 저장할 수는 없다.
→ 컴퓨터는 유한한 비트로 무한한 소수를 정확히 표현할 수 없다.
정수 오버플로우(Integer Overflow)
#include <stdio.h>
#include <unistd.h>
int main(void)
{
for (int i = 1; ; i *= 2)
{
printf("%i\n", i);
sleep(1);
}
}
- i를 계속 2배씩 곱해가는 프로그램이다.
출력 결과 중간:
...
536870912
1073741824
-2147483648
0
0
...
왜 이런 결과가 나올까?
- int 자료형은 보통 4바이트(32비트)를 사용한다.
- 표현할 수 있는 최대 정수는 약 21억(2³¹-1)이다.
- 최대값을 넘어가면 오버플로우(overflow) 가 발생해서,
- 갑자기 음수(-2147483648) 로 바뀌고, 결국 0이 되어버린다.
→ 비트로 표현할 수 있는 범위를 넘으면 엉뚱한 값이 나온다.
오버플로우 사례: Y2K 문제
- Y2K 문제(Year 2000 Bug):
- 1990년대에는 연도를 2자리만 저장했다. (예: 99 → 1999)
- 1999년 이후 2000년이 되면 컴퓨터가 이를 1900년으로 잘못 해석할 수 있었다.
- 그로 인해 금융, 항공, 전력 시스템 등 많은 분야에서 오류가 발생할 뻔했다.
오버플로우 사례: 보잉 787 버그
- 보잉 787 드림라이너에는전력이 끊기는 치명적 버그가 있었다.
- 248일(약 8개월) 이상 전원을 켜둔 상태로 운항하면
왜 그럴까?
- 전자 시스템이 시간을 1/100초 단위로 32비트 정수로 세고 있었다.
- 1/100초 단위로 2³²(약 43억)번을 세면 오버플로우가 발생한다.
- 계산하면 약 248일이 된다.
해결 방법:
- 보잉은 248일마다 비행기 전원을 껐다 켜는(리셋하는) 방법으로 임시 해결했다.
터미널에서 즐길 수 있는 재미있는 기능 추가
- figlet "this is cs50" → 텍스트를 ASCII 아트로 변환
- say "this is cs50" → 텍스트를 소리로 읽어주기
- cowsay "Hello, world!" → 귀여운 소 그림의 말풍선으로 텍스트 출력
- fortune | cowsay → 소가 농담을 하는 것처럼 출력(fortune은 짧은 격언이나 농담을 출력하는 명령어)
- cmatrix → 영화 매트릭스처럼 초록색 문자 비가 흐르는 화면
- sl →ls를 잘못 쳤을 때 기차가 터미널을 달리는 프로그램
C 프로그램 기본 구조
가장 간단한 프로그램
#include <stdio.h>
int main(void)
{
printf("hello, world\n");
}
- #include <stdio.h>
- stdio.h는 헤더 파일(header file) 이다.
- 표준 입출력 함수(printf, scanf 등)를 사용하기 위해 필요하다.
- 파일 이름이 .h로 끝나면 헤더파일이다.
헤더 파일은 단순한 설명서가 아니라, 어떤 함수가 있고, 어떻게 써야 하는지 알려주는 선언 정보를 가지고 있다.
프로그램 컴파일하고 실행하기
clang -o hello hello.c
./hello
- clang -o hello hello.c
- hello.c를 컴파일해서 hello라는 실행 파일을 만든다.
- ./hello
- 현재 폴더에 있는 hello 실행 파일을 실행한다.
다른 예시: CS50 라이브러리 사용
#include <stdio.h>
#include <cs50.h> // get_string 함수를 사용하기 위해 필요
int main(void)
{
string answer = get_string("What's your name?\n");
printf("hello, %s\n", answer);
}
컴파일할 때는 cs50 라이브러리를 추가로 연결해줘야 한다:
clang -o string string.c -lcs50
./string
- -lcs50은
- “cs50 라이브러리의 0과 1 (기계어 코드)을 연결(link)하라”는 의미다.
컴파일이란 무엇인가?
C 소스 파일을 컴퓨터가 실행할 수 있는 프로그램(기계어) 로 변환하는 과정은 4단계로 이루어져 있다.
컴파일을 한다는 것은 Preprocessing → Compiling → Assembling → Linking, 이 네 단계를 자동으로 거친다는 뜻이다.
컴파일의 네 단계
(1) 전처리 단계 (Preprocessing)
- 소스 코드에서
- #include, #define 등 #으로 시작하는 지시문(directive) 을 처리한다.
- #include <stdio.h>처럼
- 외부 파일의 내용을 가져와서 현재 파일에 복사/붙여넣기 한다.
예를 들어:
#include <stdio.h>가 있으면 컴파일러는 stdio.h 파일 안에 있는 다음 같은 코드들을 현재 파일에 포함시킨다:
int printf(const char *format, ...);
결과:
- 프로그램이 printf를 사용할 수 있게 된다.
(2) 컴파일 단계 (Compiling)
- 이제 전처리가 끝난 C 코드를 어셈블리어(Assembly Language) 로 변환한다.
- 어셈블리어는 사람이 읽을 수 있는 매우 저수준의 명령어이다.
예시 (컴파일된 어셈블리 코드 일부):
main: .cfi_startproc
pushq %rbp
movq %rsp, %rbp
callq printf
...
- pushq, movq, callq 등은 CPU가 직접 수행할 수 있는 매우 구체적인 명령이다.
- 여기에 get_string, printf 같은 이름도 어셈블리어에서 연결되어 나타날 수 있다.
(3) 어셈블링 단계 (Assembling)
- 어셈블리어 코드를 입력으로 받아
- 0과 1로 이루어진 기계어(binary code) 로 변환한다.
- 즉, Object File (.o) 파일이 만들어진다.
(아직 프로그램 실행은 불가능하다. 링킹(linking)까지 해야 실행 파일이 된다.)
(4) 링킹 단계 (Linking)
- 컴파일된 여러 개의 파일을 하나로 합치는 작업이다.
- 예를 들어:
| string.o | 내가 작성한 코드 |
| cs50.o | CS50 라이브러리 코드 |
| stdio.o | printf 같은 표준 함수 코드 |
- 이들을 모아서 최종적으로 하나의 실행 파일을 만든다 (a.out 또는 지정한 이름).
프로그래밍은 단순히 코드를 “적는 것”만으로 끝나지 않는다.
- 실제 프로그래밍에서는
- 코드를 작성하는 것(Writing Code)
- 작성한 코드를 디버깅(Debugging) 하는 것이 모두 중요하다.
- 에러 없이 한 번에 성공하는 코드는 거의 없다.
- 디버깅 능력은 프로그래머의 핵심 역량이다.
“버그(Bug)“의 유래
- Grace Hopper는 컴퓨터 과학의 선구자 중 한 명이다.
- 1940년대에 Mark II 컴퓨터를 개발하는 중, 실제로 기계 안에 벌레(moth) 가 들어가서 시스템이 오작동했다.
- 이 사건 이후로 의도하지 않은 프로그램 오류를 “버그(Bug)” 라고 부르게 되었다.
문제 상황: #include <stdio.h>를 빼먹은 경우
int main(void)
{
printf("hello world\n");
}
컴파일
clang buggy0.c
오류 메시지
buggy0.c:3:3: error: call to undeclared library function 'printf' with type 'int (const char *, ...)'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
buggy0.c:3:3: note: include the header <stdio.h> or explicitly provide a declaration for 'printf'
1 error generated.
- printf가 선언되지 않았다는 오류이다.
- 컴파일러는 printf라는 함수를 모르겠다고 한다.
- 해결 방법은? → #include <stdio.h>를 코드 맨 위에 추가해야 한다.
오류를 쉽게 찾는 방법: help50
- CS50에서 제공하는 help50 명령어를 이용하면 오류 메시지를 쉽게 해석할 수 있다.
예시:
help50 clang buggy0.c
- 그러면 친절한 설명이 나오면서
- “헤더 파일(stdio.h)을 추가하세요” 같은 해결 방법을 제시해준다.
출력값을 이용한 디버깅: printf 디버깅
#include <stdio.h>
int main(void)
{
for (int i = 0; i < 10; i++)
{
printf("#\n");
}
}
- 이 코드는 #를 10번 출력하는 프로그램이다.
그런데 만약 출력이 이상하거나, 원하는 만큼 출력되지 않는다면?
방법:
- printf()를 이용해 변수의 값을 직접 출력하면서 확인한다.
수정:
#include <stdio.h>
int main(void)
{
for (int i = 0; i < 10; i++)
{
printf("i is now %i\n", i + 1); // i 값 출력
printf("#\n");
}
}
이렇게 하면:
- i가 현재 몇 번째 반복인지 알 수 있고,
- 프로그램 흐름을 쉽게 따라갈 수 있다.
→ 이런 방법을 printf 디버깅이라고 부른다.
디버깅 도구 사용하기
debug50
- CS50에서는 debug50이라는 디버깅 도구도 제공한다.
- 프로그램을 한 줄씩 실행하면서
- 변수 값
- 메모리 상태
- 흐름 제어
- 등을 살펴볼 수 있다.
사용 예시:
debug50 ./program
(그 전에 make program으로 컴파일해야 한다.)
IDE(통합 개발 환경) 내장 디버거
- VS Code, CLion, Visual Studio 같은 IDE는
- 중단점(Breakpoint) 설정
- 한 줄씩 실행(Step over, Step into)
- 변수 값 실시간 확인
- 기능을 제공한다.
→ 복잡한 프로그램에서는 IDE 디버거를 쓰는 게 더 효율적이다.
check50
- check50은 CS50에서 제공하는 자동 채점 프로그램이다.
- 작성한 프로그램이 정상적으로 작동하는지,
- 주어진 테스트 케이스를 모두 통과하는지 확인해준다.
- 기능 테스트 중심이다. (출력 결과, 동작 흐름 체크)
사용 방법
check50 사용자명/문제명
예시:
check50 cs50/problems/2023/x/hello
- cs50/problems/2023/x/hello는 hello.c 과제에 대한 테스트를 의미한다.
- 입력 예시, 출력 결과를 자동으로 테스트해준다.
→ 프로그램이 요구사항을 정확히 충족하는지 객관적으로 점검할 수 있다.
style50
- style50은 코드 스타일(형식) 을 자동으로 검사해주는 도구다.
- 구글, 페이스북 같은 기업에서는 코드 스타일 가이드를 따르는 것이 필수다.
- style50은 코드를 읽기 쉽게
- 들여쓰기
- 괄호 위치
- 줄바꿈
- 같은 스타일 규칙을 체크해준다.
사용 방법
style50 파일명.c
예시:
style50 hello.c
- 파일을 검사하고, 필요한 경우 “이렇게 수정하는 것이 더 좋습니다” 라는 제안을 해준다.
→ 코드는 잘 작동하는 것뿐만 아니라 다른 사람도 읽기 쉽게 작성하는 것이 중요하다.
'C' 카테고리의 다른 글
| C 언어의 개요 (1) | 2025.08.27 |
|---|---|
| 모두를 위한 컴퓨터 과학(하버드CS50 2019)(4) (0) | 2025.04.29 |
| 모두를 위한 컴퓨터 과학(하버드CS50 2019)(3) (1) | 2025.04.29 |
| 모두를 위한 컴퓨터 과학(하버드CS50 2019)(2) (0) | 2025.04.28 |
| C (1) | 2025.04.13 |