이번에는 C++에서 가장 많이 사용하는 vector를 구현해보았다. 완벽하게 똑같이 만들고싶은 욕심이 있었지만 한계가 있었다. 우선 소스코드는 아래 링크에 있다.

https://github.com/Seo-sang/C-_Standard_Template_Library

 

GitHub - Seo-sang/C-_Standard_Template_Library: make C++ STL myself

make C++ STL myself. Contribute to Seo-sang/C-_Standard_Template_Library development by creating an account on GitHub.

github.com

1. 벡터 동작 방식

    벡터는 크기가 부족할 경우 크기를 두 배씩 늘리는 동적배열이다. 따라서 초기 배열의 크기를 10으로 잡았다. 처음 myvector를 선언할 경우 배열의 크기는 10으로 설정된다. push_back을 했을 때 공간이 부족한 경우 길이 2배짜리 배열을 만든 후 원래 배열의 내용을 옮겨 담는다. 반대로 pop_back도 마찬가지이다. 현재 배열을 사용하는 공간이 이전 배열의 1/2보다 작은 경우 배열의 크기를 1/2로 줄이고 내용을 복사한다.

 

2. iterator

    벡터뿐 아니라 다른 STL에서도 iterator 기능은 굉장히 많이 사용한다. 그렇기 때문에 이 기능은 뺄 수 없었다. iterator가할 수 있는 연산은 ++, --, *, !=, == 총 5가지를 구현하였다. iterator를 구현하며 클래스 자체를 리턴할 수 있음을 알 수 있었다. 현재 가리키는 포인터를 하나 두어 상대적인 연산을 하도록 만들었다. iterator class의 코드는 다음과 같다.

class myiterator {
        T* current;
    public:
        myiterator(T* node = 0) : current(node) {};

        myiterator& operator++() {
            current++;
            return *this;
        }
        
        myiterator& operator--() {
            current--;
            return *this;
        }

        T& operator*() {
            return *current;
        }

        bool operator !=(const myiterator& cmp) {
            return (current != cmp.current);
        }

        bool operator ==(const myiterator& cmp) {
            return (current == cmp.current);
        }
    };

클래스의 멤버 변수는 하나가 있다 current라는 변수로 벡터 배열의 원소를 가리키는 포인터이다. iterator는 항상 begin부터 시작하거나 end부터 시작한다. 그렇기 때문에 begin, end 코드를 보면 다음과 같다.

    myiterator begin() {
        return myiterator(head);
    }

    myiterator end() {
        return myiterator(tail);
    }

생성자를 이용하여 head와 tail을 현재 포인터로 하는 생성자를 만들었다. begin()을 호출할 경우 iterator는 가장 첫 번째 원소를 가리킨다. end()를 호출할 경우 iterator는 가장 마지막원소 다음을 가리킨다. 벡터에서 end에는 원소가 존재하지 않고 마지막을 나타낸다. 그렇기 때문에 나도 크기가 10인 경우 9개의 원소만 넣고 10번째는 항상 빈 상태로 유지하였다.

 

3. 메소드

    우리가 가장 많이 사용하는 push_back, pop_back을 포함하여 front, back, size, empty, clear, resize, at을 구현하였고 iterator연산에 빠질 수 없는 begin, end도 구현하였다. 코드의 길이가 꽤 길기 때문에 소스코드는 github링크를 통해 확인하기 바란다. 가장 중요한 부분이 push_back과 pop_back에서의 길이를 2배로 늘리거나 1/2로 줄이는 일이었다. 처음에 memcpy를 이용해봤으나 생각한대로 동작하지 않아 for문을 이용해 일일히 복사하는 수 밖에 없었다. resize도 마찬가지로 크기 n을 인자로 받았을 경우 10부터 2배씩 늘려 n을 포함할 수 있는 크기로 배열의 크기를 재설정하였다.

 

    사용만하던 벡터를 직접 구현하면서 벡터에 대한 이해가 더 깊어진 것 같고 특히 iterator를 직접 구현해보고 연산자를 구현해보면서 C++에 대한 이해가 더 발전한 것 같다.

'프로젝트 > C++ STL만들기' 카테고리의 다른 글

[C++ STL 만들기] priority_queue 구현  (0) 2021.08.19
[C++ STL 만들기] queue 구현  (0) 2021.08.02
[C++ STL 만들기] list 구현  (0) 2021.07.31
[C++ STL 만들기] stack 구현  (0) 2021.07.29

메모리에서 변수를 저장하는 부분은 static 영역과 dynamic 영역으로 나뉜다. static영역은 프로그램이 시작될 때부터 종료될 때까지 메모리 공간을 차지하고 있는 전역변수가 저장되어 있다. dynamic 영역에는 local 변수들이 저장되어 있다. dynamic 영역은 stack 영역과 heap 영역으로 나뉜다. 그렇다면 어떤 변수들이 stack영역으로, 어떤 변수들이 heap 영역에 저장될까?

 

1. Stack-dynamic variable

 

    스택 다이나믹 변수는 프로그램에서 함수가 호출되어 실행될 때 아래의 그림과 같이 함수의 지역 변수들이 stack 영역에 저장된다.

func1에서 func2를 호출하고 func2에서 func3을 호출한 경우 위의 그림과 같이 stack영역에 저장된다. func3함수가 종료한 경우 stack영역에서 pop하게 된다. 그 다음 func2가 종료할 경우 stack 영역에는 func1의 지역 변수만 남게 된다. stack-dynamic 변수는 선언문이 수행될 때 storage binding이 일어난다. 즉 컴파일하는 시간에 메모리가 할당되는 것이 아니라 런타임에 storage binding이 일어난다는 것이다. 하지만 type binding은 프로그램이 실행되기 전에 static하게 일어난다.

 

 

2. Heap-dynamic variable

 

    힙 다이나믹 변수에는 두 가지가 있다. 첫 번째는 explicit heap-dynamic variable이고 다른 하나는 implicit heap-dynamic variable이다.

    Java의 경우 모든 객체를 heap 영역에 저장하므로 implicit(암묵적) 힙 다이나믹을 사용한다. implicit heap dynamic은 말 그대로 메모리를 할당이 자동적으로 된다. 배정문(assignment statement)가 실행될 때 heap storage에 binding된다.

    explicit(명시적) heap-dynamic 변수의 경우 프로그래머가 직접 메모리를 할당해주게 된다. 아래는 C언어의 explicit heap-dynamic의 예시이다.

char* explicit = (char*(malloc(sizeof(char)*10);
explicit = "hello\n";
printf("%s", explicit);
free(explicit);

malloc을 이용해 explicit 변수가 사용할 힙 영역을 크기 10만큼 동적할당한다. 저 동적할당문이 실행될 때 메모리가 할당된다. 프로그래머는 동적할당을 받은 변수를 사용한 다음 명시적으로 메모리를 반환해줘야한다. C언어에서는 free를 이용하여 반환해줬다. 만약 free하지 않고 메모리를 가리키는 포인터를 없애는 경우 Garbage 메모리가 생길 수 있다. 또 메모리를 해제하였지만 그 메모리를 가리키고 있는 또 다른 포인터는 그대로 있는 경우가 발생할 수 있다. 그런 포인터를 dangling pointer라고 한다. dangling pointer의 경우 가리키고 있는 엉뚱한 메모리를 읽거나 써서 프로그램에 문제를 발생시킬 수 있으므로 굉장히 조심해야 한다.

    변수의 Life time은 프로세스가 실행되었을 때 데이터인 변수가 메모리를 할당받고 저장되어 공간을 차지하고 있는 시간을 말한다. 전역변수는 프로그램 어디에서든 접근이 가능해야한다. 따라서 프로그램의 시작부터 종료까지 메모리를 차지하고 있어야하고 프로그램이 시작될 때부터 종료될 때까지가 전역변수의 life time이다. 지역 변수는 전역 변수와 달리 함수나 블록 내부에서 선언된다. 그렇기 때문에 그 블록을 나가게 되면 더 이상 필요가 없기 때문에 메모리를 반환하고 사라지게 된다. 따라서 지역변수의 life time은 함수나 블록 내부에서 선언되어 메모리에 할당되는 순간부터 그 지역변수를 감싸는 블록이 끝나 메모리에서 해제되는 순간까지이다. 

 

    변수의 Scope는 변수에 접근할 수 있는 범위를 말한다. 이는 변수의 Life time과 밀접하게 연관된다. 메모리에 변수가 존재해야 접근할 수 있기 때문이다. 변수의 종류는 크게 2가지가 있다. 하나는 Global 변수, 다른 하나는 Local 변수이다. 이 2가지에 추가하여 언어가 함수의 nesting을 허용을 하는 경우 non-local이 존재하기도 한다. 함수의 nesting은 함수 안에 함수를 선언하는 것을 말한다. 아래의 python코드와 같이 말이다.

def out_func(int a):
	a += 10
	def in_func(int b):
    	b += a

이 코드에서 out_func 안에 in_func이 존재한다. 이때 out_func는 부모가 되고 in_func은 자식이 된다. 자식 함수는 부모 함수의 지역변수를 접근할 수 있다. 이때 부모 함수의 지역변수를 non-local 변수라고 부른다.

    변수의 Scope는 크게 두 가지가 있다. 하나는 static scope 다른 하나는 dynamic scope이다. static scope는 프로그램의 구조상 결정이 된다. static scope에서 접근할 수 있는 변수는 전역변수, 지역변수, nesting이 허용된 언어의 경우 non-local변수까지 존재한다. 지역변수는 그 함수나 블록 내에서만 사용 가능하고 전역변수는 모든 함수에서 접근 할 수 있다. 우리는 코드를 보고 어떤 변수는 어디서 접근 가능한지 알 수 있다. 위의 코드를 보고 우리는 구조적으로 in_func은 out_func의 자식 함수이므로 out_func의 지역변수에 접근할 수 있다는 것을 알 수 있다. 우리가 아는 언어들 C언어, C++, python, Java는 모두 static scope이다. 

    dynamic scope는 우리가 코드를 보고 구조적으로 알 수 없다. dynamic scope는 프로그램이 실행되는 runtime에 결정된다. 예를 들어 아래와 같은 실행이 있다고 하자. 

        1. main -> func1 -> func2 -> func3

        2. main -> func3

static scope에서는 1번과 2번 모두 func3가 사용할 수 있는 변수가 동일하다. nesting이 허용 되지 않았다고 한다면 전역번수, func3의 지역변수. 하지만 dynamic scope에서는 1번과 2번에서 func3가 사용할 수 있는 변수의 범위가 다르다.

1번에서는 전역변수, main 지역변수, func1 지역변수, func2 지역변수, func3 지역변수를 사용 가능하고 2번에서는 전역변수, main 지역변수, func3 지역변수를 사용할 수 있다. 즉 자신을 호출한 함수의 변수까지 사용할 수 있는 것이다. 그렇기 때문에 우리는 프로그램 구조상 접근할 수 있는 변수를 알지 못한다. 접근할 수 있는 변수는 runtime에 결정되기 때문이다.

    전역 변수에 대하여 더 이야기 하자면 static scope에서 전역변수는 가급적 사용을 지양한다. 전역변수로 선언 시 프로그램 전체에서 쉽게 접근할 수 있어 편리하지만 module간의 coupling 즉 결합도가 높아지면 전역변수를 공유함으로써 의도와는 다르게 다른 module에 영향을 주어 결과 값이 예상과 다를 수 있기 때문이다. 마지막으로  지역변수와 전역변수가 같은 이름으로 생성될 수도 있다. 이러한 경우 전역변수를 hidden variable이라고 부른다. 이렇게 지역변수와 같은 이름의 다른 변수를 사용하고자 하는 경우 scope operator를 사용할 수 있다. C++에서는 scope operator로 :: 기호를 사용한다.

    그렇다면 C언어에서 static으로 선언한 변수는 어떻게 될까? 그런 변수는 scope는 지역 변수 성격을 가지고 있지만 life time은 전역 변수 성격을 가지고 있는 변수가 된다.

    Binding이란 attribute와 entity의 결합을 말한다. 예를 들면 변수와 type이나 value가 결합되는 것이나 연산자와 기호가 결합되는 것이 있다. 이때 Binding이 언제 이루어지냐에 따라 compile binding, run time binding으로 나눌 수 있다.

compile binding은 static binding이라고도 부르며 프로그램을 컴파일 하는 과정에서 binding이 이루어지고 프로그램이 실행하는 동안 바뀌지 않는 것을 말하고 run time bindng 은 dynamic binding이라고도 부르며 프로그램이 실행되고 있는 runtime에 binding이 이루어지고 프래그램이 실행되는 동안 변할 수 있는 것을 말한다. static binding을 하는 언어들은 C기반 언어 등이 있고 dynamic binding을 하는 언어로는 python, ruby, javascript, php등이 있다.

 

    변수는 프로그램에서 언급되기 전에 data type에 bind되어야 한다. Type binding은 다음 두 가지에 의해 결정된다.

1. 어떻게 type이 명시되어 있는 지

2. 언제 binding이 이루어지는지

    static binding에선 변수의 type을 선언한다. 선언된 type은 프로그램이 끝날 때까지 변하지 않는다. C언어의 다음 예시를 생각해보자.

#include <stdio.h>

int main() {
    int a;
    a = 10;
    //a = "hello";
    printf("%d", a);
    
    return 0;
}

우리는 변수 a를 정수형으로 사용하기 위해서는 위와 같이 a를 integer형으로 선언해야한다. 만약 int형으로 선언한 a에 주석처리를 한 것과 같이 문자열을 넣는다면 컴파일 에러가 날 것이다. integer형으로 선언한 a는 프로그램이 끝날 때까지 int형으로만 사용할 수 있다.

    dynamic binding에서는 변수의 type을 선언하지 않는다. 이런 변수는 어떤한 type의 값도 설정할 수 있다. 변수의 type은 변수에 값을 넣는 assignment statement(배정문)에서 결정되고 bind된다. 다음 python 코드를 보자

b = 10
#b = hello
print(b)

python에서는 변수의 type을 선언하지 않는다 b = 10라는 줄이 실행될 경우 b는 integer형으로 자동적으로 binding된다. 만약 배정문이 b = hello일 경우 b는 str형으로 자동적으로 binding된다. 

 

그렇다면 프로그래머 입장에서는 dynamic type binding이 더 편리한 것 아닐까? 굳이 변수를 타입에 맞춰서 선언할 필요도 없고 편리하다고 생각이 든다. 하지만 dynamic type binding의 가장 큰 단점은 비용이 크다는 것이다. 프로그램이 실행되는 run time에 binding되므로 실행 시간이 static type binding에 비해 길다. 그렇기 때문에 dynamic type binding은 대부분 compiler 방식이 아니라 interpreter 방식을 사용한다. 또한 static type binding의 compiler에 비해 error checking 능력이 떨어진다. 자유로운 형 변환이 가능하기 때문에 integer형 변수였던 i에 double형 값을 중간에 넣고 이를 까먹었을 경우 프로그래머가 원하는 결과가 나오지 않을 수 있다. interpreter는 int형 변수에 double형을 넣은 줄을 보고 에러를 발생시키지 않기 때문에 프로그래머가 이러한 오류를 직접 찾아야한다. 굉장히 큰 프로그램일 경우 이 오류를 찾는 것은 너무 힘든 일일 것이다. 이렇게 dynamic binding은 어떠한 type으로도 쉽게 바꾸고 사용할 수 있는 편리하다는 장점이 있지만 오류가 발생하였을 때 오류를 찾는데 어려움이 있고 실행 시간이 오래걸린다는 단점도 존재한다. 이렇게 장단점이 다르기 때문에 어떤 목적으로 프로그램을 사용할 것인가에 따라 언어를 선택하는 것도 굉장히 중요하다.

    Backus-Naur Form을 알아보기 전에 CFG가 무엇인지 알아야 한다. CFG는 Context-Free Grammar로 단어 그대로 해석해보면 "문맥으로부터 자유롭다"는 것이다. 아래 문장을 봐보자.

 

나는 배를 먹었더니 배가 아프다.

 

    이 문장을 보고 우리는 앞의 배는 먹는 배, 뒤의 배는 우리 신체를 가리키는 것을 문맥을 보고 알 수 있다. "배를 먹는다"로부터 먹는 배를, "배가 아프다"로부터 신체임을 우리는 문맥상으로 파악해야한다. 이러한 언어를 "Context Sensitive 하다"고 한다. 프로그래밍 언어가 이렇게 Context Sensitive하다고 생각해보자. 그렇다면 프로그래밍한 코드를 컴퓨터가 해석하게 만드는 것은 굉장히 어려울 것이다. 그렇기 때문에 프로그래밍 언어는 앞뒤 단어에 따라 의미가 달라지지 않는  CFG를 사용한다.

 

    BNF는 John  Backus가 ALGOL 58언어의 문법을 표기하기 위해 만들었고 이후 Peter Naur가 ALGOL60언어의 내용으로 보완하여 발표하였다. 처음엔 사람들에게 수용되지 않았지만 지금까지도 프로그래밍 언어 구문을 확실하게 표기하는 방법으로 여겨진다. 

    Grammar는 Rule들의 집합이다.  BNF에서 이러한 Rule들은 아래와 같이 3가지 기호로 이루어져 있다.

1. -> : Rule(production)
2. <> : Non-terminal
3. | : Or

    1번째 표현은 규칙을 나타내는 표현으로 rule이나 production으로 불린다. 화살표의 왼쪽은 LHS(Left Hand Side)라고 하며 정의될 개념이 존재한다. 화살표의 오른쪽은 RHS(Right Hand Side)라고 하며 기호와 개념들로 이루어진 정의가 존재한다. 따라서 LHS는 RHS로 정의된다고 할 수 있다.

    2번째 표현은 Non-terminal Symbol로 <>사이에 정의될 원하는 개념을 적는다. nonterminal symbol은 rule의 왼편에 위치하여 lexeme과 token등 terminal이나 또 다른 Non-terminal들에 의해 정의된다. 또한 Non-terminal symbol은 두 개 이상의 정의를 가질 수 있다.

    3번째 표현은 OR로 2번의 Non-terminal이 두 개 이상으로 정의될 때 rule의 오른편에서 이 '|' 기호를 이용해 각 정의들을 구분하여 한 번에 rule을 표현할 수 있게 한다. 예를 들어 아래와 같이 하나의 Non-terminal symbol에 여러 개의 rule이 존재할 경우 아래와 같이 나타낼 수 있다.

<expression> -> <var> + <var>
<expression> -> <var> - <var>
<expression> -> <var> * <var>
<expression> -> <var> / <var>

<expression> -> <var> + <var> | <var> - <var> | <var> * <var> | <var> / <var>

우리가 프로그래밍을 할 때 사칙연산을 사용할 경우 컴파일러는 위의 rule들 중에 동일한 문법이 존재하지 않는지 판단하여 문법이 맞는지 확인할 수 있다. 만약 존재하지 않을 경우 syntax에러를 주며 컴파일 에러가 발생하게 된다.

폰 노이만 아키텍처는 최근 60년 중 가장 인기있고 널리 사용된 컴퓨터 구조이다.

우선 그림으로 나타내자면 다음과 같다.

Von Neumann Architecture

    데이터와 명령어, 프로그램 코드는 모두 같은 메모리(위)에 저장되고 CPU(아래)는 메모리와 분리된 구조이다. 이러한 구조 때문에 메모리에 저장되어있는 instruction과 data는 CPU로 pipelining을 통해 전송된다. 이렇게 전송되는 것을 fetch라고 한다. 그리고 CPU에서 이루어진 연산의 결과는 다시 메모리에 저장된다. CPU와 메모리가 인접해 있고 다음 명령어의 위치를 알려주는 PC(Program Counter)값만 바꾸어 branch하면 되기 때문에 반복되는 연산이 굉장히 빠르다. 따라서 이 구조에서는 재귀보다 반복이 더 빠르다.

    앞서 말한 fetch를 통해 전송된 instruction에 있는 opcode를 CPU에 있는 Decoder가 확인하여 어떤 연산을 수행할지 결정한다. 이러한 실행으로 생기는 것이 바로 아래 그림인 fetch-excution(decode) cycle이다.

fetch-execution(decode) cycle

그렇게 결정된 Control Unit의 값에 따라서 ALU에서 레지스터(register)에 저장된 값을 가지고 연산이 수행된다. 그 결과는 다시 메모리에 저장된다. 이때, 실행되는 명령어의 주소를 가리키는 PC 레지스터는 1이 늘어나 다음 명령어를 실행할 준비를 한다. 만약 instruction이 jump일 경우는 PC 레지스터 값이 jump하여 이동한 명령어의 주소로 바뀌게 된다.

Linux 환경에서 파일 입출력을 다루는 방법에는 두 가지가 있다. 하나는 저수준 파일 입출력이고 다른 하나는 고수준 파일 입출력이다.

    저수준 파일 입출력은 시스템의 커널에서 제공하는 system call 함수를 사용하는 방법이다. 리눅스에서는 예시로 open, read, write 등등이 있다. 당연히 system call 함수를 이용하기 때문에 고수준 파일 입출력보다 더 빠른 파일 처리가 가능하다. 저수준 파일 입출력의 특징중 하나는 "바이트" 단위로 파일을 다룬다는 것이다. 그렇기 때문에 일반 파일뿐 아니라 특수 파일까지 다룰 수 있다. 여기서 특수 파일은 데이터 전송이나 디바이스 접근에서 사용하는 파일을 말한다. 마지막으로 파일을 다룰 때 handler인 파일 디스크립터를 사용한다.

    고수준 파일 입출력은 fopen, fread, fputs와 같은 표준 입출력 라이브러리를 사용하는 방법이다. 이 방법은 바이트 단위로 입출력하는 저수준 파일 입출력과 달리 버퍼를 이용하여 한번에 파일을 읽고 쓸 수 있다. 고수준 파일 입출력함수는 저수준 파일 입출력을 이용하여 만들어진다. 그렇기 때문에 저수준 파일 입출력이 고수준 파일 입출력보다 빠르다.

    저수준 파일 입출력에서 사용하는 파일 디스크립터는 프로세스가 실행될때 각 프로세스마다 가지고 있는 파일 디스크립터 테이블에 할당되게 된다. 파일 디스크립터 테이블은 파일 디스크립터의 목록을 관리하는 자료구조이다. 파일 디스크립터 테이블에는 현재 이 프로세스에서 오픈한 파일의 목록들을 알 수 있다. 파일 디스크립터 값은 정수형인 integer 값이다. 이 값은 파일 디스크립터에게 부여된 번호이다. 프로세스가 생성될 경우 0, 1, 2번 파일 디스크립터는 자동적으로 할당되게 된다. 0번은 표준입력(STDIN), 1번은 표준출력(STDOUT), 2번은 표준 에러를 나타낸다. 따라서 프로세스가 생성될 경우 시스템에서 파일 디스크립터 테이블에 자동으로 0, 1, 2번을 할당한다. 그 다음 프로그래머가 직접 파일을 열 경우 3번부터 순서대로 할해당 파일에 접근할 수 있는 파일 디스크립터가 부여되게 된다.

    고수준 파일 입출력에서는 파일 포인터를 사용한다. fopen의 리턴값인 FILE형 구조체 변수를 가리키는 포인터를 통해 파일에 접근할 수 있다. FILE 구조체에는 오픈한 파일을 처리할 수 있는 정보가 기록된다.  당연히 fopen을 통해 파일을 열어도 파일 디스크립터는 생성이 된다. 파일 포인터로부터 파일 디스크립터를 알고 싶다면 fileno(FILE*)라는 함수를 통해 파일포인터에 해당하는 파일디스크립터를 얻을 수 있다. 파일 포인터 구조체는 다음과 같다.

typedef struct {
    int cnt; //버퍼에 남은 문자 수
    unsigned char *base; //버퍼 시작 주소
    unsigned char *ptr //버퍼의 현재 포인터 주소
    unsigned flag //파일의 접근 권한
    int fd; //파일 디스크립터
} FILE;

    대표적인 저수준 파일 입출력 함수와 고수준 파일 입출력 함수를 이용하여 간단한 예제를 실행해 보겠다. 아래는 파일을 열고 읽고 쓰는데 필요한 시스템 호출 함수와 표준 입출력 함수이다.

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    //저수준 파일 입출력
    char fdbuffer[10];
    int fd = open("file.txt", O_RDWR);
    read(fd, fdbuffer, 10);
    write(STDOUT_FILENO, fdbuffer, 10);
    close(fd);
    
    printf("\n");
    
    //고수준 파일 입출력
    char fpbuffer[10];
    FILE* fp = fopen("file.txt", "r+");
    fread(fpbuffer, 10, 1, fp);
    fwrite(fpbuffer, 10, 1, stdout);
    fclose(fp);
}

위 코드는 file.txt라는 파일이 있을 때 저수준 파일 입출력 방법과 고수준 파일 입출력 방법으로 각각 10바이트를 읽어와 화면에 출력하는 함수이다. 실행 결과는 다음과 같다

file.txt파일에는 I'm shyeon이라는 길이 10의 문자열이 들어있다. 이때 위의 코드로 파일 디스크립터와 파일 포인터를 이용해 읽어올 경우 위와 같이 나타나게 된다.

이 프로그램은 bash shell script언어를 이용하여 월, 일을 입력받아 해당 날짜가 무슨 요일인지 출력하는 프로그램이다.

1900년 1월 1일이 월요일이기 때문에 그 때부터 입력받은 날짜까지 지난 일 수를 구하여 7로 나누었을 때의 나머지를 통해 요일을 구할 수 있다. 소스코드는 다음과 같다.

#!/bin/bash
echo -n "type month date : "
read month date
let year=2021-1900
let sum=$year*365
let sum+=$year/4
case $month in
	"1") let sum+=0;;
	"2") let sum+=31;;
	"3") let sum+=59;;
	"4") let sum+=90;;
	"5") let sum+=120;;
	"6") let sum+=151;;
	"7") let sum+=181;;
	"8") let sum+=212;;
	"9") let sum+=243;;
	"10") let sum+=273;;
	"11") let sum+=304;;
	"12") let sum+=334;;
esac
let sum+=$date
let sum%=7
case $sum in
	"0")echo "2021년 $month월 $date일은 일요일입니다.";;
	"1")echo "2021년 $month월 $date일은 월요일입니다.";;
	"2")echo "2021년 $month월 $date일은 화요일입니다.";;
	"3")echo "2021년 $month월 $date일은 수요일입니다.";;
	"4")echo "2021년 $month월 $date일은 목요일입니다.";;
	"5")echo "2021년 $month월 $date일은 금요일입니다.";;
	"6")echo "2021년 $month월 $date일은 토요일입니다.";;
esac

우선 올해 연도 2021에서 1900을 뺀 값에 365를 곱한다. 하지만 우리는 2월달이 29일이 되는 윤년도 생각해야한다. 윤년은 4년마다 오기 때문에 1900년부터 구하고자하는 연도를 뺀 값을 4로 나눈 값을 더해준다. 그 다음 입력한 월의 전달까지의 지난 날짜를 더한다. 그리고 입력한 달의 날짜를 더하면 1900년 1월 1일부터 2021년 입력한 날짜까지의 총 일 수가 구해지게 된다. 이 값을 7로 나누어 나누어 떨어질 경우 일요일 나머지가 1씩 증가할 때마다 월~토까지 나오게 된다.

아래 그림은 실행 결과다.

오늘 날짜와 생일, 1월 1일을 실행해 보았다.

 

결과는 wsl2 ubuntu-20.04에서 실행하였다.

+ Recent posts