개요

재밌는 CS시간

2진법

컴퓨터는 어떻게 정보를 저장하고 보여줄까?
너무 당연하게 사용해와서 쉽게 답하기 어렵다. 마치 마법같기도 하다. 그러나 마법은 없다.

우리가 일상에서 사용하는 숫자 체계는 0~9까지의 숫자를 사용하여 수를 나타내는 10진법이다.
고대 국가들에선 20진법 16진법 60진법등 다양하게 사용해왔다.
하지만 컴퓨터는 이처럼 다양한 숫가 없다.
컴퓨터는 전류를 흐르게 하거나 차단하는 방식으로 동작한다. 이를 위해 트렌지스터라는 반도체 소자를 사용하는데 트랜지스터는 온과 오프 각각 1과 0으로 상태를 구분한다. 전류가 흐르면 1 전류가 흐르지 않으면 0.
전류의 세기로도 다양한 숫자를 표현할 수 있겠지만 이들을 구분하기 위해 복잡한 회로가 필요하고 그만큼 오류가 발생할 가능성도 높아진다.
따라서 컴퓨터는 오직 0과 1로만 데이터를 표현한다. 이를 2진법이라고 한다.

3을 이진법으로 나타내면 011로 나타낼 수 있다. 여기서 각각의 숫자를 표현하는 단위를 비트(bit)라고 한다.
비트는 Binary digit의 약자다.

그러나 비트 한 두개 정도로는 많은 양의 데이터를 나타내기엔 턱없이 부족하다. 따라서 여러 숫자 조합을 컴퓨터에 나타내기 위해 비트열을 사용한다. 이는 바이트라고 부르며 8개의 비트가 모여 만들어진 것이다.
또 바이트가 모여 킬로바이트 그리고 더 많이 모여 기가바이트와 같은 단위로 사용한다.

숫자는 나타낼 수 있다는 걸 알겠는데 그럼 문자는 어떻게 나타내는 것일까?
여기엔 약속이 필요하다.
예를 들어 65라는 숫자는 대문자 A를 의미하는 것으로 하자와 같은 약속이다. 이진법으로는 1000001이 A다.
이렇게 되면 문자의 종류가 많을수록 더 많은 숫자가 필요할 것이다. 노래방 책과 같다. 계속해서 더 많은 노래들이 등장해서 노래방 책은 엄청나게 두껍다.
마찬가지로 우리는 이모티콘을 사용하고 한글을 사용하는 등 정말 여러가지를 사용하고 있다. 따라서 유니코드라는 표준을 만들어 더 많은 비트를 사용하여 처리하고 있다. 예를 들어 눈물 이모티콘의 십진법은 128,514로 약속했다.

그렇다면 그림,영상, 음악과 같은 텍스트가 아닌 데이터는 어떻게 저장하고 어떻게 다룰 수 있을까?
그림은 마치 점묘화같이 작은 점들을 찍어 그림을 표현한다. 이를 픽셀이라고 한다.
RGB라는 기본 색을 서로 다른 비율로 조립해서 점을 나타내고 이 점들은 (72, 72, 33)과 같이 나타낼 수 있고 이들을 모아서 화면에 출력하는 것이다.
영상도 그림을 이어붙인 것이기 때문에 숫자로 표현할 수 있다. 음악도 마찬가지다.

컴퓨터의 내부 구조

스크린샷 2024-12-20 시간: 18 39 33 CPU는 컴퓨터의 뇌에 빗대어 설명할 수 있다.

1초에 약 30억 번씩 다음에 무엇을 해야하는지 물어본다.
CPU뒷면에는 핀들이 있는데 이 핀들을 통해 명령을 보낸다.
메인 메모리에는 명령이 저장되어 있다. 메인 메모리는 아주 빠르고 CPU에 필요한 것들을 공급한다.
만약 CPU가 새로운 명령이 필요하면 메인 메모리에게 어디에 명령이 있는지 물어보고 전달 받은 명령을 실행한다. 위의 그림은 이런 프로세스를 보여주고있다.

우리가 사용하는 소프트웨어는 메인 메모리에 저장되어 소프트웨어를 통해 CPU를 제어한다.
그리고 이들을 연결하는 메인보드 혹은 마더보드가 있다. image 또 다른 핵심적인 부품으로는 보조 기억 장치가 있다.
메인 메모리는 매우 빠른 대신 전원을 끄면 정보가 사라진다.
만약 파일이나 정보를 영구적으로 보관하려면 다른 종류의 기억장치가 필요한데 그게 바로 보조 기억 장치다. 보조 기억 장치는 전원이 꺼져도 영구적으로 메모리를 저장할 수 있다.
하드 디스크는 자기 저장 방식을 사용하는데 디스크 표면에 자성을 띠는 물질이 코팅되어있고 데이터를 저장할 때 자기적 상태를 변경하여 정보를 기록한다.
예를 들어 데이터를 기록하는 동안 디스크의 회전하는 원판에 LP 플레이어처럼 자기 헤드가 자기장을 생성하여 데이터를 쓴다. 0과 1은 자성 입자의 방향으로 표현되며 이러한 상태는 시간이 지나도 유지된다.
이 곳에 운영체제, 파일, 응용 프로그램등이 저장된다.

파일에 코드를 작성한다. -> 이렇게 쓴 코드는 메인 메모리에 로드된다. 그럼 CPU가 명령을 수행한다.

컴퓨터의 역사

컴퓨터는 계산하다는 의미를 가진 라틴어 computare가 어원이 된 단어라 컴퓨터를 계산하는 기계로 정의할 수 있다.
우리가 접해봤던 도구중에 주판이 있다. 주판도 계산을 도와주는 도구로 의미적으로 최초의 컴퓨터라고 할 수 있겠다.

그러던 중 프랑스의 수학자 블레즈 파스칼은 세계최초로 덧셈과 뺄셈이 가능한 톱니바퀴로 이루어진 계산기를 발명하였다.곱셈은 덧셈을 여러번 해서 사용할 수 있었다. image

1673년 독일의 라이프니츠가 파스칼의 계산기를 개량하여 곱셈과 나눗셈도 가능한 계산기를 발명했다.

1822년 찰스 배비지가 설계하였으니 완성은 시키지 못했다고한다.
톱니바퀴와 기어를 사용하여 기억하고 계산을 수행했다고 한다.
전자기계식 컴퓨터는 19세기에 등장했다.

  • 홀러리스: 천공 카드 기반 데이터 처리 시스템
  • 제로니모 제퍼슨: 데이터 처리 자동화 장치

전자식 컴퓨터는 ENIAC을 꼽는다. 미국 펜실베니아 대학에서 개발했고 진공관을 사용하여 10진 연산을 수행했다고 한다.

프로그래밍 언어의 역사

초기 프로그래밍은 1과 0으로 이루어진 코드를 작성하여 명령어를 작성하는 방식이었다.
천공 카드란 19세기 자카르 직조기에서 영감을 받아 만들어졌으며 초기 컴퓨터 입력 장치로 사용되었다.
프로그래머는 천공카드에 구멍을 뚫어 명령어를 입력했다.

명령어는 기계어로 작성되었는데 기계어란 컴퓨터의 뇌라고 불리는 cpu가 별다른 해석 없이 읽을 수 있는 언어로 모든 프로그래밍 언어의 종착라고 말할 수 있다.

기계어는 2진법으로 표현되고 규칙을 가지고 프로그래밍할 수 있다. 그러나 0과 1로 이루어진 명령어는 사람이 읽고 해석하기 불편하다. 가독성이 0으로 수렴한다. 이렇게 기계어로 프로그램을 작성한다면 생산성이 굉장히 낮아질 수 밖에 없다.

이러한 어려움을 극복하기 위해 어셈블리어가 등장했다. 기호와 간단한 명령어로 기계어를 추상화하여 더 이해하기 쉽게 도왔다.
예를 들면 MOV A, 5와 같다. 레지스터 A에 숫자 5를 저장하라는 의미다.
그러다 고급 프로그래밍 언어가 등장했다.
FORTRAN, LISP, COBOL등이 있다.
1970년대 C언어와 객체지향 프로그래밍이 등장했다.
C언어는 유닉스 운영 체제를 개발하기 위해 설계된 언어로 저수준 접근과 고수준 구조적 프로그래밍의 장점을 결합했고 이후 많은 언어의 기초가 되었다. 파이썬도 주로 C언어로 개발된 언어다.
이 시기에 만들어진 프로그래밍 언어는 C, Pascal, Smalltalk이 있다. Pascal이라는 이름은 위에서 살펴본 기계식 계산기를 만든 파스칼의 이름에서 따온 것이라고 한다.
1980~90년대에는 우리가 사용하는 프로그래밍 언어들이 등장했다. C++, 파이썬 자바 등이 있다.

포인터

만약 코드를 작성하며 변수를 선언한다면 어느 곳에 어떻게 저장될까?
변수를 선언하면 자료형을 고려해 적당한 위치에 메모리 공간을 할당하고 그 곳에 값이 저장된다.

이 메모리 공간은 작은 방과 같은 공간들이 이어져있는 구조이다.
1칸의 크기는 1바이트로 이루어져있고 각각의 공간들은 16진수로 이루어진 주소값으로 구분된다.

만약 이 주소를 확인해보고 싶다면 주소 연산자인 & 를 사용해서 출력해보면 된다.

#include <stdio.h>

int main(void)
{
    int n = 50;
    printf("%p\n", &n);
}

실행해보면 0x1234567890과 같은 형식의 값을 확인할 수 있다.

실제 저장되어있는 값은 * 을 사용하여 접근할 수 있다.

앞에서 말한대로 메모리 주소는 16진수로 표현된다.
이런 메모리 주소를 직접 관리하기는 쉽지 않을 수 있다.

이를 메모리 주소를 담는 하나의 변수로 포인터 변수를 사용한다.

포인터는 말 그대로 무언가를 가리키는 것이다. 하이퍼링크에 마우스를 올렸을 때 손가락 모양으로 바뀌는 마우스 커서의 이미지이다.

위와 같이 실제 값이 저장되어있는 주소값을 변수에 할당하고 그 것을 사용하고 싶다면 포인터 변수를 사용하면 된다.

#include <stdio.h>

int main(void)
{
   int n = 50;
   int *p = &n;
   printf("%p\n", p);
   printf("%i\n", *p);
}

int *p에서 * 기호는 이 변수가 포인터라는 의미이고 int는 이 포인터가 int 타입의 변수를 가리킨다는 의미이다.
만약 p와 같이 출력을 해본다면 어떤 값이 출력될까? p는 포인터 변수로 어떤 변수의 주소를 담고있다. 따라서 16진수로 이루어진 주소값을 리턴한다.

*p와 같이 출력해본다면 P가 가리키고있는 변수인 50이 출력된다.

포인터는 하이퍼 링크를 가리키는 손가락 모양의 커서이다. 하이퍼 링크는 그 변수의 주소값이고 클릭시 이동된 페이지는 그 변수의 값이다.

*p를 통해서 p의 하이퍼 링크를 클릭한다. p의 하이퍼 링크는 n으로 설정되어있으니 n페이지가 나타난다. 이렇게 이해하면 쉽다.

이게 필요한 이유는 다음과 같다.

#include <stdio.h>

void swap(int x, int y) {
    int temp = x;
    x = y;
    y = temp;
}

int main(void) {
    int a = 10, b = 20;
    swap(a, b);
    printf("a: %d, b: %d\n", a, b); // a: 10, b: 20 (변경되지 않음)
    return 0;
}

위의 코드는 main에서 변수 a, b를 전달받아 서로 바꾸는 swap함수를 호출한 것이다.
결과는 a와 b가 바뀌지 않는다.
이것은 C는 Call by value를 따르기 때문이다.
이렇게 전달했을 경우 실제 변수를 전달하는 듯 보이지만 실제로는 값을 복사하여 값만 전달한다. 이런 방식을 Call by value라고 한다.
swap내부에서 x와 y라는 변수를 사용하고 있지만 이들은 a, b와는 다른 임시 변수인 것이다.
따라서 의도한대로 main에서 a와 b는 변하지 않는다.

이런 경우 변수를 전달하는 것이 아니라 실제 주소값을 전달하여 이를 실제로 바꾸도록 할 수 있는 것이다.

#include <stdio.h>

void swap(int *x, int *y) {
    int temp = *x;
    *x = *y;
    *y = temp;
}

int main(void) {
    int a = 10, b = 20;
    swap(&a, &b); // a와 b의 주소를 전달
    printf("a: %d, b: %d\n", a, b); // a: 20, b: 10 (값이 변경됨)
    return 0;
}

이렇게 하면 C언어에서도 Call by reference를 사용할 수 있는 것이다.