버퍼

C 표준입출력 라이브러리에서는 내부적으로 버퍼를 도입하여 입출력을 효율적으로 처리합니다. printf나 scanf 등의 라이브러리 함수는 결국 입력과 출력을 read, write를 통해서 이루어집니다. 버퍼라는 공간을 두는 이유는 내부적으로 write, read를 적시에 최소한으로만 호출하기 위한 것이 목적입니다. 왜 그런 뻘짓을 하느냐구요? CPU를 많이 사용하지 않기 위해서입니다. 한 글자 씩 계속 입력을 받거나 출력을 하면 그만큼 read, write 콜이 잦아지는데 이러면 CPU에 부담이가 성능에 안좋을 영향을 끼치게 되는 겁니다. 버퍼를 사용하는 것은 라이브러리 함수인면에서 응용 프로그램에서는 신경쓰지 않아도 되지만, 버퍼의 처리 방식을 모르게 되면 낭패를 봅니다.

어떻게 버퍼를 이용하는 것을 버퍼링이라고 하고  따라서 세가지 버퍼링 방식이 있습니다. 

전체 버퍼링(Full buffering)

이러한 버퍼링은 내부 버퍼에 데이터가 꽉 차게 되면 그제서야 입출력이 되는 방식입니다. 그러니까 버퍼가 전부 차기 전에는 이 데이터를 가지고만 있고 입출력은 하지 않는 것이죠. 위에서 얘기했듯이 이러한 버퍼링의 목적은 read, write를 최소한으로 사용하기 위함입니다. 그래서 버퍼가 전부 찰 때까지 기다리고 있죠. 이 때 "버퍼의 크기가 크면 무조건 좋은 거 아닌가?" 라는 물음을 던질 수 있는데, 정도라는 것이 있듯 최적의 버퍼 크기가 정해져있습니다. 이것을 표준 입출력 라이브러리가 정해줍니다. 우리는 개-꿀만 빨면 됩니다.

보통 파일을 디스크로부터 읽을 때의 버퍼링 방식입니다. 

아래와 같은 경우가 전체 버퍼링의 예를 보여줍니다. 붉은 사각형은 비어있는 데이터를 의미하며 파란 사각형은 채워진 데이터를 의미합니다. 현재는 버퍼에 2바이트의 데이터가 모자라서 파일에 기록하지 않고 있습니다. 이때 2바이트가 채워지고 있는 모습입니다. 

전체 버퍼링1

이 때 완전히 버퍼가 채워지면 그제서야 데이터를 한꺼번에 파일로 전송하게 됩니다. 

 

줄 단위 버퍼링(Line buffring)

scanf나 fgets, fgetc 등의 표준 입력 함수나 printf, fputs, putc 등의 함수를 이용한 표준 출력을 사용할 때 이러한 줄 단위 버퍼링이 적용됩니다. 줄 단위 버퍼링은 새 줄 문자('\n')가 나올 때 까지 입력이나 출력을 하는 것입니다. 또한 버퍼가 차게 되면 입출력을 진행합니다. 이 때 버퍼의 크기는 보통 전체 버퍼링의 버퍼 크기보다 작습니다.

아래와 같은 경우가 줄 단위 버퍼링을 보여줍니다. 아직 데이터가 전부 채워지지 않았으며 이 때 개행 문자인 '\n'이 입력이 되고 있는 상황입니다. 

개행 문자를 만나면 버퍼가 채워져있지 않음에도 입출력을 진행하게 됩니다. 

 

비 버퍼링(Unbuffered)

버퍼링은 하지 않는 방식입니다. 왜요? 급하기 때문입니다. 여러분도 급똥이면 장사없듯이 프로그램도 급하면 장사없습니다. 언제가 급할까요? 에러를 출력할때가 그런 상황입니다. 지체없이 에러를 해결해야할 상황이 생기기 때문이지요. 

버퍼링 정보 가져오기

그렇다면 보통의 표준 입력, 표준 출력, 표준 에러나 파일에 대한 스트림은 어떤 버퍼링 방식을 갖고 버퍼 크기는 어떻게 결정이 될까요? 아래의 코드는 상황에 따른 입,출력 버퍼에 대한 정보를 표시해주는 코드입니다.

//buffer_info.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef __GLIBC__
#define _IO_UNBUFFERED 0x0002
#define _IO_LINE_BUF 0x0200
#endif

int main(int argc, char *argv[]){
        FILE *fp;

        char buf[32] = {0, };

        if(argc =! 2){
                printf("Usage : %s stdin | stdout | stderr | file_name\n",
                                argv[0]);
                return 1;
        }


        if(!strcmp(argv[1], "stdin")){
                fp = stdin;
                fgets(buf, sizeof(buf), fp);
        } else if(!strcmp(argv[1], "stdout")){
                fp = stdout;
                printf("stdout\n");
        } else if(!strcmp(argv[1], "stderr")){
                fp = stderr;
                fprintf(fp, "stderr\n");
        } else {
                fp = fopen(argv[1], "r");

                if(fp == NULL){
                        printf("fopen error\n");
                        return 1;
                }

                while(fgets(buf, sizeof(buf), fp) != NULL);
        }

        if(fp->_flags & _IO_UNBUFFERED)
                printf("비버퍼링\n");
        else if(fp->_flags & _IO_LINE_BUF)
                printf("줄단위 버퍼링\n");
        else
                printf("전체 버퍼링\n");

        printf(" 버퍼 사이즈 : %ld\n", fp->_IO_buf_end - fp->_IO_buf_base);

        fclose(fp);
}

 

# ./a.out stdin
hello
줄단위 버퍼링
 버퍼 사이즈 : 1024
# ./a.out stdout
stdout
줄단위 버퍼링
 버퍼 사이즈 : 1024
# ./a.out stderr
stderr
비버퍼링
 버퍼 사이즈 : 1
# ./a.out /etc/group
전체 버퍼링
 버퍼 사이즈 : 4096

 

단순히 stdin, stdout, stderr에 대해서 fgets나 printf를 한번 호출하지 않고서 fp->_flags를 들여다보면 다른 결과가 나올 수 있습니다. 예를 들면 아래와 같이 fgets를 주석 처리하고 실행해보시면 다른 결과를 보실 수 있을 겁니다. 

    if(!strcmp(argv[1], "stdin")){
            fp = stdin;
            //fgets(buf, sizeof(buf), fp);

아래의 결과가 위처럼 fgets를 주석처리한 예인데, 결과가 다르죠?

# ./a.out stdin
전체 버퍼링
 버퍼 사이즈 : 0

이러한 결과를 통해서 알 수 있는 것은 스트림을 열었다고 해서 그 버퍼링이 설정된다는 것이 아니라, read, write를 하는 함수들이 버퍼링을 결정해준다는 사실입니다. 

 

버퍼링 설정 

1. setbuf

#include <stdio.h>

void setbuf(FILE *stream, char *buf);

setbuf 함수를 이용해서 버퍼링 방식을 설정할 수 있습니다. 대신 시스템이 정해준 버퍼인 BUFSIZ로만 사용이 가능합니다. 반대로 버퍼링을 끌 수도 있습니다.

버퍼를 설정하려면 buf[BUFSIZ]의 버퍼를 *buf인자에 전달해야하고, 버퍼를 끄려면 NULL을 전달하면 됩니다.

아래의 예를 한번 볼까요?

//setbuf.c
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

//버퍼링을 키거나 끄는 쓰레드 
void* buf_control(void *arg){
        char buf[BUFSIZ] = {0,};
        int on;
        while(1){
                scanf("%d", &on);
                switch(on){
                        case 0: //OFF
                                setbuf(stdout, NULL);
                                break;
                        case 1: //ON
                                setbuf(stdout, buf);
                                break;
                }
        }

}

//1초마다 "A"를 계속 찍는 쓰레드
void* print_line(void *arg){
        while(1){
                printf("A");
                sleep(1);
        }
}
int main(int argc, char *argv[]){
        pthread_t tid1, tid2;

        printf("[0] 버퍼 동작 X\t [1] 버퍼 동작 O\n");

        pthread_create(&tid1, NULL, buf_control, NULL);
        sleep(1);
        pthread_create(&tid2, NULL, print_line, NULL);

        pthread_join(tid1, NULL);
        pthread_join(tid2, NULL);
}
# ./a.out 
[0] 버퍼 동작 X  [1] 버퍼 동작 O
0
AAAAAAAAAAAAAAA1
0
AAAAAAAAAAAA1A
0
AAAAA1
0
AAA1
0
AAAAA^C

 

pthread의 개념을 몰라도 좋습니다. 단순히 buf_control이라는 함수, print_line이라는 함수가 동시에 수행되는 것만 아시면 됩니다. 

buf_control이라는 함수에서는 버퍼링을 끄거나 키거나 할 수 있는데, 1은 버퍼링을 키는 동작, 0은 버퍼링을 끄는 동작이라는 것을 볼 수 있을 겁니다. 

print_line함수는 한 글자씩 1초마다 printf를 통해서 출력을 해주는 함수입니다. 단 줄바꿈(\n)은 하지 않죠. printf는 디폴트 동작으로는 줄단위 버퍼링 방식을 사용하는 것을 위에서 확인했었죠?? 그래서 버퍼링을 설정하게 되면 줄바꿈이 나오지 않거나 버퍼 크기인 1024바이트가 채워지지 않으면 출력을 하지 않습니다.

2. setvbuf

#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

세 가지의 버퍼링 방식을 설정할 수 있습니다. stream에 대해서 size만큼의 buf를 버퍼링합니다. 이때 mode는 비버퍼링(unbuffered), 줄 단위 버퍼링(line buffering), 전체 버퍼링(full buffering)을 설정할 때 쓰입니다. mode에 대한 설명은 아래를 참고하세요. 

mode 설명 
_IONBF 비버퍼링 모드 
_IOLBF 줄 단위 버퍼링 모드
_IOFBF 전체 버퍼링(블록 버퍼링) 모드

setvbuf는 성공시 0, 실패시 0이 아닌 값을 설정하여 돌려줍니다.

확실히 setbuf 함수보다는 보다 세세한 설정이 가능하죠? 그렇다면 setvbuf함수를 통해서 버퍼링을 설정하는 예를 볼까요? 아래는 stdout을 줄 단위 버퍼링이 아닌 4바이트의 전체 버퍼링으로 바꾼 하나의 예입니다.

//setvbuf.c
#include <stdio.h>
#include <unistd.h>

#ifdef __GLIBC__
#define _IO_UNBUFFERED 0x0002
#define _IO_LINE_BUF 0x0200
#endif

#define BUF_SIZE 4

int main(){

        char buf[BUF_SIZE] = {0,};
        FILE *fp = stdout;

        if(setvbuf(fp, buf, _IOFBF, BUF_SIZE) != 0){
                printf("setvbuf _IOLBF error \n");
                return 1;
        }

        if(fp->_flags & _IO_UNBUFFERED) printf("비버퍼링\n");
        else if(fp->_flags & _IO_LINE_BUF) printf("줄단위 버퍼링\n");
        else printf("전체 버퍼링\n");

        while(1){
                printf("A");
                sleep(1);
        }

}

실행해보면 줄 단위 버퍼링이 아닌 전체 버퍼링으로 설정된 것을 볼 수 있습니다. 그리고 4초마다 버퍼가 꽉 채워지기 때문에 출력이 되는 동작을 확인할 수 있네요.

# ./a.out 
전체 버퍼링
AAAAAAAAAAA^C
반응형
블로그 이미지

REAKWON

와나진짜

,

strftime

strftime은 시간 커스텀할 수 있게 만들어준 문자열 서식화 함수인데요. 문자열에 날짜에 대한 형식을 지정하면 문자열에 시간에 대한 정보가 들어옵니다. strftime의 원형은 다음과 같습니다.

#include <time.h>

size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);

 

time.h를 include해야합니다. 

s : 담을 문자열입니다.

max : 문자열의 최대 길이를 지정합니다.

format : 여기에 서식화할 format문자가 들어갑니다. 

tm : 시간정보를 담은 tm구조체가 들어와야합니다. tm구초제는 아래와 같이 정의되어있습니다.

struct tm {
   int tm_sec;    /* 초 (0-60) */
   int tm_min;    /* 분 (0-59) */
   int tm_hour;   /* 시간 (0-23) */
   int tm_mday;   /* 월의 일 (1-31) */
   int tm_mon;    /* 월(0부터 시작) (0-11) */
   int tm_year;   /* 년 */
   int tm_wday;   /* 주의 일 (0-6, Sunday = 0) */
   int tm_yday;   /* 년의 일 (0-365, 1 Jan = 0) */
   int tm_isdst;  /* Daylight saving time */
};

 

반환 : 성공시 문자열의 크기, 실패시 0이 반환됩니다.

 

format의 문자는 매우 다양합니다. 아래의 표로 정리하긴했지만, 이것보다 훨씬 많습니다. 여기에 나오지 않는 format문자는 인터넷 서치하시거나 매뉴얼 페이지를 보시기바랍니다.

format 문자 설명
%a 짧은 요일 이름 Thu
%A 긴 요일 이름 Thursday
%b, %h 짧은 달 이름 Jan
%B 긴 달 이름 January
%c 날짜 + 시간 Tue Apr  5 15:35:05 2022
%C 두자리 연도(00-99) 20
%d 그달의 일(01-31) 30
%D 날짜(MM/DD/YY) 04/05/22
%e 그 달의 일(1-31) 10
%H 그 날의 시(24시간) 23
%I 그 날의 시(12시간) 11
%m 달(01-12) 02
%M 분(00-59) 55
%p 오전/오후 PM
%S 초(00-60) 30
%T %H:%M:%S 12:30:24
%R %H:%M 12:30
%r 12시간 형식의 로컬 시간 05:40:12 PM

 

아래는 이러한 strftime함수를 사용한 예입니다.

#include <time.h>
#include <stdio.h>

int main() {
    time_t now;
    struct tm* t;
    char buf[128] = { 0, };

    time(&now);
    t = localtime(&now);
    if (strftime(buf, sizeof(buf), "current - %c", t) == 0) {
        printf("strftime 실패\n");
        return -1;
    }
    printf("%s\n", buf);

    return 0;
}

 

 

오늘은 4월 5일로 나무 심으러 가도록 합시다.

strptime

strptime함수도 존재하는데, strftime의 역이라고 생각하시면 됩니다. formatting된 문자열에서 시간 구조체인 tm을 얻을 때 사용합니다.

char *strptime(const char *s, const char *format, struct tm *tm);

 

파라미터 설명은 넘어가도록 하고 사용 예를 보도록 하겠습니다.

#include <stdio.h>
#include <string.h>
#include <time.h>

int main(void){
        struct tm tm;
        char buf[255];

        memset(&tm, 0, sizeof(struct tm));
        strptime("2020-09-21 07:09:30", "%Y-%m-%d %H:%M:%S", &tm);
        printf("%d/%d/%d %d:%d:%d\n",tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
                                        tm.tm_hour, tm.tm_min, tm.tm_sec);
}

 

시간을 나타내는 문자열 서식화 함수 strftime과 strptime을 알아보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

반복문 for

C언어에는 여러 반복문이 존재합니다. for문, while문, do ~ while문이 있지요. 오늘은 가장 흔하게 쓰이는 for문에 대하여 이야기해보도록 하겠습니다. 

for문에 대한 형식은 일반적으로 아래와 같은 형태입니다.

1. 초기화 식이 먼저 수행됩니다. 이는 for문 최초 1회만 수행됩니다.

2. 이후 조건식을 보고 true이며 for문 내부에 있는 코드를 수행하고, false이면 for문을 멈추고 빠져나옵니다.

3. 조건식이 참일 경우 수행되는 내부 부분입니다.

4. 이후 증감식을 수행한 후 다시 2의 조건식에 맞는지 확인합니다. 

이후 2 ~ 4의 반복입니다.

 

예 ) 1~10까지의 합

이러한 기본 형식을 지킨 1부터 10까지 모두 더한 수를 구한 예제입니다.

#include <stdio.h>

void main() {
	int i;
	int sum = 0;
	for (i = 1; i <=10; i++) {
		sum += i;
	}
	printf("1부터 10까지의 합 : %d\n", sum);
}

 

중첩 for문 

for문안에 for문이 등장하여 중복적으로 반복문을 수행할 수도 있습니다. 이를 중첩 for문, 혹은 2중 for문이라고 합니다. 원리는 간단합니다. 수행 부분이 내부 for문으로 바뀌는 거거든요. 아래는 이와 관련된 구구단의 예제입니다.

int i, j;
for (int i = 2; i <= 9; i++) {
	for (int j = 1; j <= 9; j++) {
		printf("%d * %d = %d\n", i, j, i * j);
	}
	printf("\n");
}

 

for문의 제어 - continue, break 

상황에 따라서 이번 수행부분은 건너뛰거나, 아예 for문을 멈춰야하는 상황이 발생할 수 있습니다. 이럴 경우 쓰이는 키워드가 if와 함께 continue와 break입니다.

continue : 이후 for문의 실행부분을 건너뜁니다.

break : break가 있는 for문을 종료시킵니다. 2중 for문의 경우 break가 등장하는 for문을 멈춰버립니다.

예 ) 1~10까지 짝수만 출력

for (int i = 1; i<=10; i++) {
	if (i % 2 == 1) continue;
	printf("%d \n", i);
}

 

예) 특정수와 일치하는 배열의 index 찾기

void main() {
	
	int numbers[] = { 5,4,2,1,9,11 };
	int found_index = -1;
	for (int i = 1; i<sizeof(numbers); i++) {
		if (numbers[i] == 9) {
			found_index = i;
			break;
		}
	}
	if (found_index != -1) {
		printf("9의 배열 index:%d\n", found_index);
	}
	else {
		printf("9를 찾을 수 없음\n");
	}
}

 

for문의 다양한 사용법 

for문은 일반적으로 이런식으로 사용한다는 것을 보여드렸습니다. 초기화식, 조건식, 증감식은 모두 생략이 가능합니다. 아래와 같이 다양하게 사용할 수 있습니다.

for(;;) // 무한 루프
for(i=1;;) //무한 루프
for(;i<5;i++) //초기화 구문 생략
for(;;i++)	//i가 1씩 증가하는 무한루프
for(i=1,j=1;;i++,j++)	//두가지 변수를 ,로 동시에 증감이 가능

 

for문 활용 - 별 출력

for문의 가장 대표적인 예제로는 별찍기가 있는데요. 한번 풀어보도록 합시다. 처음 몇개는 쉬우니까 우선 풀이는 생략하고 코드만 올리도록 하겠습니다.

1. 

*
**
***
****

아래는 코드입니다.

void main() {
	printf("*\n");
	printf("**\n");
	printf("***\n");
	printf("****\n");
}

장난이구요. 

void main() {
	int i, j;
	for (i = 1; i <= 4; i++) {
		for (j = 1; j <= i; j++)
			printf("*");
		printf("\n");
	}
}

 

2.

****
***
**
*
void main() {
	int i,j;
	for (i = 4; i > 0; i--) {
		for (j = 1; j <= i; j++)
			printf("*");
		printf("\n");
	}
}

 

3. 

   * 
  ***
 *****
*******

여기서부터 좀 많이 헤메실수 있어서 풀이방법도 기재해드립니다. 우선 코드분석해보시고 풀이를 봐주세요.

#include <stdio.h>

void main() {
	int i, j, height;
	scanf("%d", &height);
	for (i = 1; i <= height; i++) {
		for (j = 1; j <= height - i; j++) //공백 먼저 출력
			printf(" ");
		
		for (j = 1; j <= i * 2 - 1; j++)  //*출력
			printf("*");
		printf("\n");
	}
}

 

우선 i는 *과 공백을 포함한 한 줄을 의미합니다. 공백과 별을 어떻게 출력하는지는 아래와 같은 규칙을 갖게 됩니다. 아래의 그림은 높이가 4인 삼각형 모양의 별을 나타내었습니다.

i가 1일 경우 ) 우선 공백이 3개 출력되고, 별이 1개 출력이 됩니다.

i가 2일 경우 ) 공백이 2개 출력되고, 별이 3개 출력됩니다.

i가 3일 경우 ) 공백이 1개, 별이 5개 출력됩니다.

i가 4일 경우 ) 공백이 0개, 별이 7개 출력됩니다.

자, 이제 일반화하게 되면, 아래와 같은 규칙을 얻게 되지요.

공백 : height - i 까지 출력

별 : i*2 -1까지 출력

그래서 위의 코드와 같이 쓸 수 있습니다.

4.

*******
 *****
  ***
   *

이거는 i를 처음부터 height값으로 놓고 --i로 거꾸로 돌리면 됩니다.

void main() {
	int i, j, height;
	scanf("%d", &height);
	for (i = height; i >= 0; i--) {
		for (j = 1; j <= height - i; j++) //공백 먼저 출력
			printf(" ");
		
		for (j = 1; j <= i * 2 - 1; j++)  //*출력
			printf("*");
		printf("\n");
	}
}

 

4.

   *
  ***
 *****
*******
 *****
  ***
   *

 

위의 두 코드를 합치면 되는데, 중간에 같은줄이 두개 나오기 때문에 밑에는 height -1로 하여 한번 빼줍시다.

	for (i = 1; i <= height; i++) {
		for (j = 1; j <= height - i; j++) //공백 먼저 출력
			printf(" ");

		for (j = 1; j <= i * 2 - 1; j++)  //*출력
			printf("*");
		printf("\n");
	}

	for (i = height-1; i >= 0; i--) {
		for (j = 1; j <= height - i; j++) //공백 먼저 출력
			printf(" ");

		for (j = 1; j <= i * 2 - 1; j++)  //*출력
			printf("*");
		printf("\n");
	}

 

이상으로 별까지 출력하는 코드로 for문을 활용해보았습니다. for문은 배열과 같이 많이 쓰이기 때문에 반드시 알아야하는 반복문입니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

switch ~ case

switch ~ case 문법은 if문과 비슷하지만 약간은 다른 문법인데요. if 문은 괄호안에 조건식에 따라서 비교를 하고 조건식이 꽤나 복잡해질 수 있습니다. 이럴때 가독성이 조금 떨어지게 되지요. 하지만 switch case 문은 보다 명료하고 간단하게 조건에 따라 실행하는 case들이 나눠지기 때문에 가독성이 뛰어나다는 장점이 있죠. 이제 switch ~ case를 어떻게 사용하는가, 그리고 흔하게 할 수 있는 실수들은 무엇인가에 대해서 설명해보려고 합니다. 

1. 기본 사용법

switch case의 기본 템플릿은 아래와 같습니다.

switch : switch안에는 정수형으로 이 정수의 데이터를 가지고 case 옆의 데이터와 비교하게 됩니다.

case n : 실제 수행 부분입니다. swtich에 전달한 정수형 인자와 일치하면 그 case문을 실행하게 되는거죠.

default : case에 모두 포함되지 않을 경우 default 안쪽의 코드가 실행됩니다. default는 생략할 수도 있습니다.

 

switch(정수형){
case 1: 
//...//
break;
case 2:
//...//
break;
case 3:
//...//
break;
//...//
default:
//...//
}

 

아래는 switch ~ case를 이용한 간단한 예제입니다. 

#include <stdio.h>

void create_new_game() {
	printf("새로운 게임 만들기\n");
}

void continue_game() {
	printf("지난 게임 계속\n");
}
void game_setting() {
	printf("게임 설정\n");
}
void exit_game() {
	printf("게임 종료\n");
}
void main() {

	int selection;
	printf("1. 새게임\n");
	printf("2. 계속\n");
	printf("3. 설정\n");
	printf("4. 종료\n");

	scanf("%d", &selection);
	switch (selection) {
	case 1: 
		create_new_game();
		break;
	case 2:
		continue_game();
		break;
	case 3:
		game_setting();
		break;
	case 4:
		exit_game();
		break;
	default:
		printf("없는 옵션입니다\n");
	}
}

 

switch안에는 정수형 인자를 넣어야한다고는 했지만, 문자도 가능합니다. 문자도 내부적으로 정수로 처리하게 되니까요. 

void main() {

	char selection;
	printf("a. 새게임\n");
	printf("b. 계속\n");
	printf("c. 설정\n");
	printf("d. 종료\n");

	scanf("%c", &selection);
	switch (selection) {
	case 'a': 
		create_new_game();
		break;
	case 'b':
		continue_game();
		break;
	case 'c':
		game_setting();
		break;
	case 'd':
		exit_game();
		break;
	default:
		printf("없는 옵션입니다\n");
	}
}

 

 

단, 문자열의 경우에는 사용할 수 없습니다.

 

2. break가 없는 경우

case 마지막에 break를 항상 입력하는 것을 볼 수가 있죠? 왜 그렇게 될까요? 위 코드에서 break만 없애서 실행을 해보도록 하겠습니다.

void main() {

	char selection;
	printf("a. 새게임\n");
	printf("b. 계속\n");
	printf("c. 설정\n");
	printf("d. 종료\n");

	scanf("%c", &selection);
	switch (selection) {
	case 'a': 
		create_new_game();
	case 'b':
		continue_game();
		
	case 'c':
		game_setting();
	case 'd':
		exit_game();
	default:
		printf("없는 옵션입니다\n");
	}
}

 

저는 b를 선택하였음에도 b 이후 모든 case가 동작하게 됩니다. break라는 것은 곧 멈추라는 의미로 break가 없으면 선택 case 이후의 모든 case를 수행하게 됩니다. 

 

3. case안에서 변수 선언

case안에서 특정 변수를 선언하고 코드를 짜보도록 합시다. 코드를 아래처럼 바꿔서 실행해봅시다.

#include <stdio.h>

void create_new_game(char *new_game_name) {
	printf("새로운 게임 만들기 :%s\n",new_game_name);
}
//... 생략 ...//
void main() {

	char selection;
	printf("a. 새게임\n");
	printf("b. 계속\n");
	printf("c. 설정\n");
	printf("d. 종료\n");

	scanf("%c", &selection);
	switch (selection) {
	case 'a': 
		char new_game_name[32];
		printf("새로운 게임 이름:");
		scanf("%s", new_game_name);
		create_new_game(new_game_name);
		break;
        //... 생략 ...//
	}
}

그리고 비주얼 스튜디오를 보면 빨간줄이 딱 떠있을 겁니다.

 

"선언에는 레이블을 사용할 수 없습니다." 

그러나 그냥 실행해도 문제없이 실행됨을 알 수 있는데, 이건 컴파일러에 따라서 그냥 실행할수 있게 해주는 경우입니다. 그렇지 않은 경우 아래와 같은 오류가 발생하며 컴파일이 안되는 경우가 있죠.

다음은 리눅스에서 위와 완전히 동일한 코드를 gcc를 이용하여 컴파일한 경우입니다. 같은 에러가 발생하면서 컴파일이 안되는 경우를 볼 수가 있죠?

 error: a label can only be part of a statement and a declaration is not a statement

 

해결방법은 매우 간단합니다. 중괄호로 case를 묶으면 에러가 발생하지 않습니다. 간단하죠?

 

이상으로 너무나 사용하기 쉬운 switch ~ case 문법에 대해서 알아보았습니다. 마지막에 대한 switch ~ case에 대한 실수는 많이 발생할 수 있으니, 알아두시면 인터넷 서치없이 바로 해결 가능합니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

Map

자료구조 중 하나인 Map은 키(key)와 값(value)를 쌍으로 갖는 STL입니다. Map의 특징 중 하나는 키 값이 중복되지 않는 다는 것입니다. C++에 있는 Map은 레드블랙트리로 이루어져있으며 검색, 삽입, 삭제가 O(log n)입니다.

코드를 보면서 어떻게 사용할 수 있는지 확인해보도록 하겠습니다. map을 사용하기 위해서는 아래와 같이 map 헤더파일을 include해주어야합니다.

#include <map>

 

1. 데이터 삽입, 조회, 변경

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
	map<string, string> m;
	m["seoul"] = "02";
	m["kyungki"] = "031";
	m["daegu"] = "051";
	m["incheon"] = "032";
	
	//맵의 모든 데이터를 순환
    //first, second를 보아 pair객체를 사용하는 것을 알 수 있다.
	for (auto iter : m) {
		cout << iter.first << "의 지역번호:" << iter.second << endl;
	}
	cout << endl;

	//[]로도 접근 가능
	cout <<"seoul:"<< m["seoul"] << endl;

	//변경
	m["daegu"] = "053";
	cout << "daegu:" << m["daegu"] << endl;

}

배열 인덱스를 다루듯이 사용할 수가 있습니다.

아래는 결과화면입니다.

 

그런데 한가지 유심히 보면 map의 키,값을 순회할때 키가 오름차순으로 나오고 있네요(daegu - incheon - kyungki - seoul).  내림차순으로 map을 구성하고 싶다면 아래와 같이 사용하세요.

map<string, string,greater<string>> m;

그리고 insert()함수를 통해서 삽입할 수도 있습니다. Map은 내부적으로 pair 객체를 이용하여 키와 값을 저장하는데요. first는 키, second는 값이 들어가게 됩니다.

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
	map<string, string> users;
	users.insert({ "010-3343-1111","reakwon" });
	users.insert({ "010-1133-0000","lee" });
	users.insert({ "010-1999-9991","huh" });
	users.insert({ "010-1192-1928","so" });
	users.insert(make_pair<string, string>("010-4444-4455", "kim"));
	
	for (auto it : users)
		cout << "name:" << it.second << ", phone:" << it.first << endl;

}

 

2. 키가 존재하는지 확인

find함수로 키가 존재하는지 확인할 수 있습니다. iterator로 내부적으로 순환하면서 비교하고 찾기 때문에 만약 키가 존재하지 않으면 iterator의 end()를 반환하게 됩니다.

	if (users.find("010-1133-0000") != users.end()) {
		cout << "키가 존재" << endl;
	}
	else {
		cout << "키가 존재하지 않음" << endl;
	}

 

3. 키-값 삭제

삭제 연산은 erase함수로 키과 값 쌍을 삭제할 수 있습니다. 

3-1. 입력된 키와 같은 pair를 삭제

users.erase("010-3343-1111");

 

3-2. 데이터 모두 삭제

users.erase() 혹은 clear() 함수로 데이터를 모두 삭제할 수 있습니다. 아래와 같이 사용합니다.

users.erase(users.begin(),users.end());
users.clear();

 

4. 저장된 키-값 수 

size() 함수를 사용하면 현재 map에 데이터가 저장된 수를 알 수 있습니다.

users.size()

 

Map을 이용한 문제풀이

Map을 사용하면 아래의 문제를 쉽게 풀수가 있습니다. 한번 문제보시면서 풀어보시구요. 정답 코드는 아래에 있습니다.

https://www.acmicpc.net/problem/1764

 

1764번: 듣보잡

첫째 줄에 듣도 못한 사람의 수 N, 보도 못한 사람의 수 M이 주어진다. 이어서 둘째 줄부터 N개의 줄에 걸쳐 듣도 못한 사람의 이름과, N+2째 줄부터 보도 못한 사람의 이름이 순서대로 주어진다.

www.acmicpc.net

#include <iostream>
#include <map>
#include <string>
#include <vector>
using namespace std;

int n, m;
int main() {
	cin >> n >> m;
	//값은 0으로 default로 설정됨
	map<string, int> outsiders;
	vector<string> ans;
	
	for (int i = 0; i < n+m; i++) {	//듣+보 전부 한꺼번에 입력받음(n+m)
		string name;
		cin >> name;		//이름이 2번 등장하면 outsiders[name] = 2가 된다
		outsiders[name]++;	
	}
	//2면 정답
	for (auto it : outsiders) {
		if (it.second == 2) ans.push_back(it.first);
	}
	
	cout << ans.size() << endl;
	for (int i = 0; i < ans.size(); i++)
		cout << ans[i] << endl;

}

 

이상으로 포스팅 마치도록 하겠습니다~

반응형
블로그 이미지

REAKWON

와나진짜

,

Vector

Standard Template Library의 컨테이너로 정의된 클래스인데요. 배열과 비슷한 특징이 있습니다만, 동적으로 계속하여 뒤에 원소를 추가할 수 있습니다. 배열을 다루는 사용자의 불편함을 vector를 사용하면 어느정도 편리하게 사용할 수 있습니다. 이 포스팅에서는 vector의 사용방법에 대해서 다룹니다.

C++에서 vector를 사용하기 위해서는 아래와 같이 vector 헤더파일을 추가시키시면 됩니다.

#include <vector>

 

1. 초기화

배열과 비슷하다고 했습니다만 초기화 방법에서는 약간 차이가 있습니다. 아래의 코드는 초기화 방식을 설명합니다. 

vector<int> v1;			//아무것도 없는 비어있는 vector
vector<int> v2(5);		//5개의 int형을 저장하는 vector(전부 0으로 초기화)
vector<int> v3(5,1);	//5개의 int형을 저장하는 vector(전부 1로 초기화)
vector<int> v4 = { 1,2,3,4,5 };	//배열과 같은 초기화
vector<int> v5(v4);		//v4의 벡터 요소를 복사해서 초기화

 

2. 크기와 용량(size & capacity)

vector는 현재 가지고 있는 데이터의 수를 나타내는 크기(size)와 얼만큼의 데이터를 담을 수 있는지에 대한 용량(capacity)가 있습니다. 만약 용량이 전부 꽉 차게 되면 용량을 동적으로 더 늘려서 데이터를 추가할 수 있습니다. vector의 용량은 항상 size보다 크거나 같습니다. 아래의 그림처럼 capacity가 모자라게 되면 늘리게 되는거죠.

 

코드로 직접 확인해보세요.

#include <iostream>
#include <vector>

using namespace std;

int main() {
	vector<int> v;
	for (int i = 0; i < 20; i++) {
		v.push_back(i + 1);
		cout << " 용량 :" << v.capacity();
		cout << " 크기 :" << v.size();
		cout << " 데이터: " << v[i] << endl;
	}
	return 0;
}

 

 

3. 데이터 읽기

데이터읽는 방법은 배열과 같이 []로 접근하는 방법과 at() 으로 접근하는 방법이 있습니다. 둘은 같은 값을 나타내줍니다. 아래의 코드를 보고 결과가 같은지 확인해봅시다.

int main() {
	vector<int> v = { 1,5,3,6,8 };
	cout << "v[1]:" << v[1] << endl;
	cout << "v.at(1):" << v.at(1) << endl;
	cout << "v[3]:" << v[3] << endl;
	cout << "v.at(3):" << v.at(3) << endl;

	return 0;
}

 

둘은 같은 값을 나타내고 있죠? 하지만 차이는 없을까요? 있겠죠. 만약 배열 접근 기호([])로 10번째 요소를 읽어봅시다. 현재 5번째까지 초기화했고, 10번째는 아직 접근할 수 없기 때문에 아래와 같은 에러를 보이고 종료하고 맙니다. at()통해서도 마찬가지일거에요. 하지만 둘의 차이는 예외를 뜨게해서 처리할수 있게 만들었느냐 아니냐입니다.

아래의 코드를 실행시켜보시고, 바꿔서 윗줄은 주석처리, 아랫줄은 주석 해제하여 실행해보세요. 차이점을 알 수 있습니다.

#include <iostream>
#include <vector>

using namespace std;

int main() {
	vector<int> v = { 1,5,3,6,8 };
	try {
		cout << v[10] << endl;
		//cout << v.at(10) << endl;
	}
	catch (out_of_range& e) {
		cout << "예외 발생 처리 " << endl;
	}

	return 0;
}

 

프로그래밍을 at()으로 하면 더 안전하게 사용할 수 있는 대신 검사때문에 []를 이용하는 방법보다는 느립니다. 두 방식 중 알맞게 선택하여 사용하세요.

4. 데이터쓰기

데이터쓰기는 너무 편합니다. 그냥 배열과 같이 사용하면 됩니다.

	vector<int> v = { 5,3,1,6,7 };
	v[2] = 3;

 

5. 데이터 뒤에 추가 및 뒤에서 삭제

push_back()을 사용하면 아래와 같이 vector 뒤에 차곡차곡 데이터를 추가합니다. 반대로 삭제하려면 pop_back()을 사용하시면 됩니다. pop_back()은 데이터를 return하지는 않고, 단지 꺼내주기만 합니다. 가장 마지막 원소를 가져오려면 back()을 이용하세요.

#include <iostream>
#include <vector>

using namespace std;

int main() {
	vector<int> v;

	v.push_back(10);
	v.push_back(13);
	v.push_back(15);
	v.push_back(20);

	int size = v.size();
	for (int i = 0; i < size; i++) {
		cout << "back():" << v.back() << endl;
		v.pop_back();
	}

	return 0;
}

 

6. Iterator 

Iterator를 통해서 for문을 돌수도 있습니다. vector의 begin()은 vector의 처음 요소를 가리키고 있습니다. vector의 end()는 vector의 마지막 요소 다음을 가리키고 있습니다. 마지막 요소 다음이지 마지막 요소를 가리키는게 아닙니다.

그래서 아래와 같이 for문을 사용하는 방법이 가능합니다.

vector<int> v = { 0,9,21,1,0,29 };
for (vector<int>::iterator it = v.begin(); it != v.end(); it++) 
	cout << *it << endl;

 

너무 복잡하죠? 아래와 같이 auto로 코드를 줄일 수 있습니다.

for (auto it = v.begin(); it != v.end(); it++)
	cout << *it << endl;

 

7. reserve

용량을 지정한 수대로 동적할당을 미리 시켜놓습니다. 

int main() {
	vector<int> v = { 0,9,21,1,0,29,2022 };
	cout << "capacity:" << v.capacity() << endl;
	v.reserve(10);
	cout << "capacity:" << v.capacity() << endl;
	v.reserve(15);
	cout << "capacity:" << v.capacity() << endl;

	return 0;
}

 

8. insert

insert는 iterator를 통해서 원소를 삽입하는 방식입니다. 아래와 같이 사용가능합니다. 

#include <iostream>
#include <vector>

using namespace std;

int main() {
	vector<int> v = { 0,9,21,1,0,29,2022 };
	vector<int>::iterator it=v.begin();	//맨앞
	//v.insert(it, 90);	//맨앞에 90삽입
	v.insert(it + 4, 90);	//4번째 원소에 90삽입

	for (auto it = v.begin(); it != v.end(); it++)
		cout << *it << endl;

	return 0;
}

이 밖에도 insert는 오버로딩이 되어있으므로 찾아보셔서 알맞는 것을 사용하면 됩니다.

 

9. 정렬

vector는 algorithm 헤더파일의 sort()함수로 정렬이 가능합니다. 다만 기본 자료형만 가능하다는 점이고, 클래스같은 경우는 별도로 비교 함수를 사용하여 delegator 방식으로 처리해야합니다.

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main() {
	vector<int> v = { 0,9,21,1,0,29,2022 };
	sort(v.begin(), v.end());
	for (auto it = v.begin(); it != v.end(); it++)
		cout << *it << endl;

	return 0;
}

이 밖에도 insert는 오버로딩이 되어있으므로 찾아보셔서 알맞는 것을 사용하면 됩니다.

 

그외에도 여러가지 vector 관련 함수들이 있는데요. 어렵지 않은 함수들이니까 나중에 직접 찾아서 활용해보시기 바랍니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

조건문 if

C언어를 배우는 중에 조건에 따라 실행흐름을 분기시키는 예약어인 if문에 대해서 자세히 알아보도록 합시다. if문와 else if는 괄호에 조건식을 써주는데요. 이 조건에 따라 실행을 다르게 시킬 목적으로 사용이 됩니다. 아래와 같은 방식으로 사용하게 됩니다. 이제 예제를 보면서 하나씩 이해하도록 합시다.

if(조건 1){
	//실행 1//
} else if(조건 2){
	//실행 2//
} else if(조건 3){
	// ... //
} else if(조건 n){
	//실행 n//
}else{
	//그 외 실행
}

 

1. if 단독 사용법

if문은 다음과 같이 괄호 안에 비교식을 써주면 되는데요. 이때 괄호식 안에 조건이 참이 되는 경우에만, if안의 코드를 실행하게 됩니다. if 안의 문장이 한문장일 경우에 아래의 중괄호는 생략이 될 수 있습니다.

#include <stdio.h>

void main() {
	int a = 10;
	if (a == 10) {	//if안의 구문이 한 줄이면 '{' 생략가능 
		printf("a = 10\n");
	}//if안의 구문이 한 줄이면 '}' 생략가능
	printf("종료\n");
}

위의 코드는 아래와 같이 수행이 됩니다. a가 5라면 if안에 printf는 호출이 되지 않지요.

 

2. if ~ else ~ 구문 예제

if에 조건에 맞지 않는 조건을 수행할 때에는 else 구문을 이용할 수 있습니다. 

#include <stdio.h>

void main() {
	int a = 2;
	if (a == 10) {	
		printf("a의 값은 10.\n");
	}
	else {
		printf("a의 값은 10이 아님.\n");
	}
}

아래는 위 코드의 결과입니다.

 

3. 조건이 여러가지일 경우 else if

조건이 여러 가지가 있을 경우에는 else if를 통해서 여러 조건을 줄 수가 있습니다. if 부터 순차적으로 아래쪽으로 비교해서 그 중 참이 되는 경우에 있는 블록만 실행하고 바로 if문을 빠져나옵니다.

#include <stdio.h>

void main() {
	int a = 3;
	if (a >= 5) {	
		printf("a의 값은 5 이상\n");
	}
	else if (a >= 2) {
		printf("a의 값은 2 이상\n");
	}
	else {
		printf("a의 값은 2 미만\n");
	}
}

#include <stdio.h>

void main() {
	int score = 85;
	if (score >= 90) {
		printf("학점 A\n");
	}
	else if (score >= 80) {
		printf("학점 B\n");
	}
	else if (score >= 70) {
		printf("학점 C\n");
	}
	else if (score >= 60) {
		printf("학점 D\n");
	}
	else {
		printf("학점 F\n");
	}
}

 

4. 조건이 참인 경우 = 0이 아닌 경우

C, C++에서 거짓의 조건은 간단합니다. 0만 false라고 간주한다는 것이죠. 그 외의 모든 수는 전부 참으로 간주합니다. 즉, 여기 if문이나 else if에서 조건이 참인 경우는 0이 아닌 경우입니다. 0외에 숫자는 모두 참으로 간주하게 됩니다. 그래서 아래의 코드중 a와 b가 참이되는 이유입니다.

#include <stdio.h>

void main() {
	int a = 1;
	int b = -1;
	int c = 0;
	if (a) {
		printf("a는 true\n");
	}
	if (b) {
		printf("b는 true\n");
	}
	if (c) {
		printf("c는 true\n");
	}
	printf("종료\n");
}

 

5. &&조건과 || 조건

&&(논리 AND 연산)은 모든 조건이 전부 true여야 true이고, ||(논리 OR 연산)은 모든 조건중에 하나라도 true이면 true입니다. 

#include <stdio.h>

void main() {
	int a = 1;
	int b = 2;
	if (a == 1 && b == 2) {
		printf("a=1이고 b=2이다.");
	}
	else {
		printf("a=1이 아니거나, b=2가 아니다.");
	}
}

6.1 하기 쉬운 실수1 (모든 비교문이 수행이 되지 않음)

다음은 if문이나 조건식에서 흔하게 하기 쉬운 실수입니다. 아래의 코드를 예측해보시기 바랍니다.

#include <stdio.h>

void main() {
	int a = 10;
	int b = 20;
	if (a > 10 && (b++) > 20) {
		printf("실행이 될까요?\n");
	}
	printf("b의 값:%d\n", b);
}

 

조건을 보게 되면 a는 10보다 크지 않죠. 딱 10이니까요. 그렇다면 a>10은 false가 됩니다. 

그리고 다음 조건에서 b를 선증가시킨 후 20과 비교합니다. b는 21의 값이 되겠네요. 그렇다면 b>20의 조건은 true가 되겠네요. 

그렇다면 if문 안의 printf는 실행이 되지 않겠네요. 그렇다면 b의 값은 얼마일까요? 아래 결과 화면입니다.

 

b의 값은 고스란히 20입니다. 조건식에서 비교할때 &&이나 ||이 오게 되면 true나 false가 확정일 경우에는 그 뒤의 식은 보지도 않고 건너 뛰어버립니다. 그래서 b++은 수행되지 않게 되죠. 그래서 b가 21로 나오게 할 경우에는 아래와 같이 수정이 되어야 21이 나오게 됩니다.

#include <stdio.h>

void main() {
	int a = 10;
	int b = 20;
	if (a > 9 && (b++) > 20) {
		printf("실행이 될까요?\n");
	}
	printf("b의 값:%d\n", b);
}

6.2 하기 쉬운 실수 2 (실수로 값 대입)

== 연산자나 != 연산자를 사용할때 흔히 이런 실수를 많이 하게 됩니다. 아래의 코드는 원래 finished == 1일 경우 프로그램이 종료되야하는 조건입니다. finished가 그전에 0이기 때문에 if문을 수행하지 않는것이 프로그래머의 의도입니다. 하지만 코드에서 프로그래머의 실수로 '=' 하나 빠지는 바람에 finished=0에서 finished=1로 대입이 되었고, if문이 실행이 됩니다.

#include <stdio.h>

void main() {
	int finished = 0;
	if (finished = 1) {	//이 식은 문법상 문제가 없는 문장이므로 에러없이 컴파일됨
		printf("finished\n");
	}
}

 

이런 오류들은 컴파일에서 잡아낼 수 없으며 찾는데도 오래 걸리게 됩니다. 따라서 비교식을 쓸때 상수는 왼쪽에 쓰는 것이 이런 휴먼 에러를 막을 수 있습니다.

이렇게 빨간색으로 뜨니까 컴파일단계에서 바로 알 수 있고 아래처럼 올바르게 수정하여 프로그램을 더 안전하게 만들 수 있습니다.

#include <stdio.h>

void main() {
	int finished = 0;
	if (1 == finished) {
		printf("finished\n");
	}
}

 

여기까지 C언어의 if문에 대해서 알아보았습니다. if문은 어렵지는 않지만 비교문이 끝까지 비교가 진행이 되는지 되지 않는지 모르는 것과 아는 것은 다릅니다. 공부하는데 도움이 되었으면 좋겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

ASCII to Hex

간단히 말해서 입력이 String인데, 이것을 16진수 값으로 변환하는 코드를 구현해보도록 하겠습니다.

간단히 "CAFECAFE0102"라는 이런 문자열을 입력을 받았는데 16진수로 변환하고 싶은 것입니다.  즉, 0xCA, 0xFE, 0xCA, 0xFE, 0x01, 0x02의 숫자 배열로 입력을 변환하는 것이죠.

가장 간단하게 구현하기 위해서 매우 정상적인 입력값만 들어온다고 가정하겠습니다. 그러니까 16진수의 범위에 있지 않은 글자('G'를 넘어가는 알파벳)는 들어오지 않는다고 가정하겠습니다.

설명은 아래의 주석으로 대체하도록 하겠습니다.

 

 

#include <stdio.h>
#include <stdint.h>
#include <string.h>


//입력의 str은 모두 대문자인 16진수 변환가능한 문자열이라고 가정
//size는 str의 크기
//hex는 변환된 16진수 배열
unsigned int ascii_to_hex(const char* str, size_t size, uint8_t* hex)
{
    unsigned int i, h, high, low;
    for (h = 0, i = 0; i < size; i += 2, ++h) {
        //9보다 큰 경우 : 알파벳 문자 'A' 이상인 문자로, 'A'를 빼고 10을 더함.
        //9이하인 경우 : 숫자 입력으로 '0'을 빼면 실제 값이 구해짐.
        high = (str[i] > '9') ? str[i] - 'A' + 10 : str[i] - '0';
        low = (str[i + 1] > '9') ? str[i + 1] - 'A' + 10 : str[i + 1] - '0';
        //high 4비트, low 4비트이므로, 1바이트를 만들어주기 위해 high를 왼쪽으로 4비트 shift
        //이후 OR(|)연산으로 합
        hex[h] = (high << 4) | low;
    }
    return h;
}

int main() {
    char str[128] = "CAFEcafe0102";
    uint8_t hex[128] = { 0, };
    size_t size = strlen(str);
    int i;

    //소문자 cafe를 대문자로 만들기 위해 strupr함수 사용
    strupr(str);
    //hex에 실제 16진수의 값이 들어감
    ascii_to_hex(str, size, hex);
    
    //size는 반으로 줄어듦, ex) hex[0]=0xCA;
    for (i = 0; i < (size/2); i++)
        printf("0x%02X\n", hex[i]);
}

 

1바이트의 unsigned int 자료형인 uint8_t를 사용하기 위해서는 stdint.h를 include해야합니다.

결과

 

 

사실 ascii에 대한 개념과 char 자료형이 정수처럼 계산할 수 있다는 사실, 그리고 비트 연산(SHIFT, OR) 연산만 이해하고 알고 있다면 그렇게 어려운 구현은 아니라고 생각이 됩니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

Calendar 클래스

Calendar 클래스는 날짜와 시간을 다루기 위해서 Date와 함께 많이 쓰이는 클래스 중 하나입니다. Calendar를 사용하기 위해서는 java.util.Calendar를 import 시켜야합니다. 

가장 기본적으로 현재 날짜와 시간을 가져올 수도 있고, 특정 시간으로 시간을 되돌리거나 뒤로갈 수도 있습니다. 이는 Calendar에서 제공하는 상수들을, 예를 들면 YEAR, MONTH, DAY_OF_MONTH 등을 이용하여 설정할 수 있습니다. 어떤 필드가 있는지 볼까요? 아래의 필드를 이용해서 get, set 메소드를 통해 값을 얻어오거나 설정할 수 있습니다.

Field(static int) 설명
YEAR 년도를 나타냅니다.
MONTH 월을 나타내는데, 이때 1월을 상수 0으로 대응이 됩니다. 그래서 실제 월을 구할때는 +1을 해주어야합니다.
DATE, DAY_OF_MONTH 월의 날짜를 의미합니다.
DAY_OF_WEEK 일주일에 해당되는 요일을 의미합니다. 일요일부터 시작이며 일요일은 1입니다. 수요일은 4의 값을 갖습니다.
HOUR 시간을 표시하는데 12시간 단위의 시간을 의미합니다.
HOUR_OF_DAY 시간을 표시하는데 24시간 단위의 시간을 의미합니다.
MINUTE 분을 의미하는 필드입니다.
SECOND 초를 의마하는 필드입니다.
MILLISECOND 밀리 세건드 단위를 의미하는 필드입니다.

 

아래의 필드는 get, set으로 얻지는 않고 비교할때 사용할 수 있습니다.

Field(static int) 설명
JANUARY 1월을 나타냅니다. 0의 값을 갖고 있습니다.
월을 나타내는 필드는 전부 대문자입니다. 2월을 FEBURARY, 3월은 MARCH입니다. 각 숫자는 월-1에 값을 갖습니다.
SUNDAY 일요일에 해당하는 값이며 1을 가집니다. 요일을 나타내는 상수도 마찬가지로 전부 대문자로 표시할 수 있으며 SUNDAY의 1부터 SATURDAY의 7까지 나타낼 수 있습니다.

 

이번 포스팅에서는 Calendar클래스를 어떻게 사용하고 다루는지 예를 통해서 설명하도록 하겠습니다.

1. 현재 시간의 정보를 표시하는 예제

    public static void main(String[] args){
    	Calendar cal=Calendar.getInstance();	//getInstance()로 객체 생성
    	System.out.println("현재 날짜:"+cal.get(Calendar.YEAR)+"-"+(cal.get(Calendar.MONTH)+1)+"-"+cal.get(Calendar.DAY_OF_MONTH));
    	System.out.println("일주일 중 오늘은 "+cal.get(Calendar.DAY_OF_WEEK)+"번째 요일 (1은 일요일)");
    	System.out.println("일년 중 오늘은 "+cal.get(Calendar.DAY_OF_YEAR)+"번째 날");
    	System.out.println("현재 시간 "+cal.get(Calendar.HOUR_OF_DAY)+":"+cal.get(Calendar.MINUTE)+":"+cal.get(Calendar.SECOND)+":"+cal.get(Calendar.MILLISECOND));
    	System.out.println("현재 시간 "+cal.get(Calendar.AM_PM)+":"+cal.get(Calendar.MINUTE)+":"+cal.get(Calendar.SECOND));
    	System.out.println("이번 주는 일년 중 "+cal.get(Calendar.WEEK_OF_YEAR)+"번째 주");
    }

 

Calendar객체는 new 키워드로 객체를 생성할 수 없고 getInstance() 메소드로 객체를 생성할 수 있습니다. 위 예제는 현재 날짜와 시간을 나타내며 위에 설명한 표의 데이터가 어떤 값을 나타내는 지 확인하는 예제입니다.

현재 날짜:2021-10-20
일주일 중 오늘은 4번째 요일 (1은 일요일)
일년 중 오늘은 293번째 날
현재 시간 19:36:39:959
현재 시간 1:36:39
이번 주는 일년 중 43번째 주

 

2. set 메소드로 날짜 설정

    public static void main(String[] args){
    	Calendar cal=Calendar.getInstance();	//getInstance()로 객체 생성. 기본 현재 날짜
    	cal.set(Calendar.MONDAY,Calendar.DECEMBER); 	//12월로 설정
    	cal.set(Calendar.HOUR_OF_DAY,14);	//오후 2시로 Calendar 객체 설정
    	System.out.println("설정된 날짜 - "+(cal.get(Calendar.MONTH)+1)+"월 "+cal.get(Calendar.DATE)+"일");
    	System.out.println("설정된 시간 - "+cal.get(Calendar.HOUR_OF_DAY)+":"+cal.get(Calendar.MINUTE)+":"+cal.get(Calendar.SECOND));
    	
    }

 

이번에는 set 메소드로 month와 hour를 설정해보았습니다. 아래처럼 원래의 date와 minute, second는 현재의 시간과 동일하며 month와 hour만 바뀐 것을 알 수 있네요.

설정된 날짜 - 12월 20일
설정된 시간 - 14:43:55

 

이렇게 개별적으로 바꿀 수도 있습니다만, 만약 한꺼번에 바꾸고 싶다고 하면 오버로딩된 set메소드를 활용하시면 됩니다.

    public static void main(String[] args){
    	Calendar newYear=Calendar.getInstance();
    	newYear.set(2022,Calendar.JANUARY,1);	//년, 월, 일 설정
    	System.out.println(newYear.get(Calendar.YEAR)+"년 "+(newYear.get(Calendar.MONTH)+1)+"월 "+newYear.get(Calendar.DATE)+"일");
    	
    	newYear.set(2022,Calendar.JANUARY,1,0,0);	//년, 월, 일, 시, 분 설정
    	System.out.println(newYear.get(Calendar.YEAR)+"년 "+(newYear.get(Calendar.MONTH)+1)+"월 "+newYear.get(Calendar.DATE)+"일");
    	System.out.println(newYear.get(Calendar.HOUR_OF_DAY)+"시 "+newYear.get(Calendar.MINUTE));
    }
2022년 1월 1
2022년 1월 1
0시 0

 

3. 밀리초로 1970년 1월 1일 00시 00분부터 흐른 시간 구하기

    public static void main(String[] args){
    	Calendar today=Calendar.getInstance();	//getInstance()로 객체 생성. 기본 현재 날짜
    	System.out.println("1970년 00시 00분부터 흐른 초 :"+today.getTimeInMillis()/1000);
    	
    	SimpleDateFormat format=new SimpleDateFormat("a hh:mm:ss");	
    	System.out.println("현재시간 "+format.format(today.getTimeInMillis()));	//SimpleDateFormat으로 출력
    	
    	
    	Calendar newYear=Calendar.getInstance();	//현지 시간으로 설정
    	newYear.set(Calendar.YEAR, 2021);
    	newYear.set(Calendar.MONTH, Calendar.OCTOBER);
    	newYear.set(Calendar.DAY_OF_MONTH, 21);
    	
    	long diff=newYear.getTimeInMillis()-today.getTimeInMillis();
    	Calendar dDay=Calendar.getInstance();
    	dDay.setTimeInMillis(diff);	//1년 이내로만 이 코드를 쓸 수 있음
    	System.out.println("남은 날 수 :"+(dDay.get(Calendar.DAY_OF_YEAR)-1));	//오늘이 포함되므로 -1
    	
    	diff=diff/(60*60*24*1000);	//60(분) * 60(1분) * 24(시간) * 1(초) = 하루 
    	System.out.println("남은 날 수 :"+diff);
    }

 

getTimeInMillis를 통해서 밀리초단위로 구할 수 있습니다. 밀리초 단위로 SimpleDateFormat에 설정하여 더 쉽게 볼 수도 있고, 두 시간 사이의 계산도 가능합니다. 

1970년 00시 00분부터 흐른 초 :1634727499
현재시간 오후 07:58:19
남은 날 수 :2
남은 날 수 :1

 

4. 특정 날짜가 현재 날짜보다 전날인지, 이후인지 확인

    public static void main(String[] args){
    	Calendar yesterday=Calendar.getInstance();
    	yesterday.set(Calendar.DATE, yesterday.get(Calendar.DATE)-1);	//현재 날짜 -1로 설정
    	Calendar today=Calendar.getInstance();
    	
    	Calendar tomorrow=Calendar.getInstance();
    	tomorrow.set(Calendar.DATE, tomorrow.get(Calendar.DATE)+1);	//현재 날짜 +1
    	
    	System.out.println("오늘이 어제보다 이전인가? "+today.before(yesterday));
    	System.out.println("오늘이 내일보다 이전인가? "+today.before(tomorrow));
    	
    	System.out.println("오늘이 어제보다 이후인가? "+today.after(yesterday));
    	System.out.println("오늘이 내일보다 이후인가? "+today.after(tomorrow));
    }

위 코드는 어제와 내일로 일단 날짜를 지정한 Calendar객체로 오늘이 앞날인지, 뒷날인지 비교하는 코드입니다. 

오늘이 어제보다 이전인가? false
오늘이 내일보다 이전인가? true
오늘이 어제보다 이후인가? true
오늘이 내일보다 이후인가? false

 

또한 위의 코드는 아래와 같이 add 메소드를 활용하는 방식으로 바꿀 수도 있습니다.

    	Calendar yesterday=Calendar.getInstance();
    	yesterday.set(Calendar.DATE, -1);
    	Calendar today=Calendar.getInstance();
    	
    	Calendar tomorrow=Calendar.getInstance();
    	tomorrow.add(Calendar.DATE,1);

 

이상으로 Calendar 클래스의 활용과 예제를 살펴보았습니다.

 

반응형
블로그 이미지

REAKWON

와나진짜

,

정적(static)

정적 멤버(혹은 변수)와 메소드라는 것은 무엇일까요? 우선 아래와 같은 상황이 있다고 가정해봅시다. Counter라는 클래스가 있으며 Counter는 멤버로 cnt를 갖고 있습니다. 그리고 count라는 메소드가 있는데, 이 count 메소드는 단순히 cnt를 하나 증가하는 역할을 합니다.

그리고 메인에서는 Counter의 두 객체 counter1, counter2를 생성하고 둘은 counter() 메소드를 통해서 cnt를 증가시키고 있습니다. 여기서 저의 의도는 두 객체가 cnt를 같이 증가시키고 싶습니다.

class Counter{
	public int cnt;
	public void count() {
		cnt++;
	}
	
}

public class TestMain {
	
    public static void main(String[] args){
    	Counter counter1=new Counter();
    	Counter counter2=new Counter();
    	counter1.count();
    	counter2.count();
    	System.out.println("counter1:"+counter1.cnt+", counter2:"+counter2.cnt);
    	
    }
}

 

만약 프로그램이 저의 의도대로 동작한다면 counter2까지 왔을때 cnt는 2가 되어야합니다. (물론 그렇게 동작하지 않습니다.) 결과를 보면 아래와 같네요.

counter1:1, counter2:1

 

정적 변수(Static variable)

여기서 저의 희망은 cnt라는 맴버를 공통으로 사용하는 것입니다. 이때 static하나만 써주면 저의 목적이 달성됩니다. Counter 클래스에서 cnt를 선언할때 static만 붙여봅시다.

class Counter{
	public static int cnt;
	public void count() {
		cnt++;
	}
}

public class TestMain {
	
    public static void main(String[] args){
    	Counter counter1=new Counter();
    	Counter counter2=new Counter();
    	counter1.count();
    	counter2.count();
    	System.out.println("counter1:"+counter1.cnt+", counter2:"+counter2.cnt);
    	
    }
}

그리고 결과를 봐야겠죠.

counter1:2, counter2:2

 

제가 말한대로 동작하는 것을 알 수 있습니다.

어떻게 이런것이 가능할까요? static 변수나 메소드는 static 메모리 구역에 따로 고정적(정적)으로 할당되어 관리됩니다. 그래서 이 static 변수와 메소드는 모든 Counter 클래스가 공통적으로 사용할 수 있습니다. 이러한 특징 때문에 클래스 변수, 메소드라고 합니다.

 

static에 대해서 가볍게 아시려면 "공용 변수, 메소드" 라고 쉽게 기억하시면 됩니다.

 

그렇지만 static에 대해서 좀 더 자세히 알려면 여러분은 우선 프로그램이 실행될때의 메모리 구조에 대해서 아셔야합니다. 우리가 항상쓰던 new로 객체를 생성하는 것은 heap 영역에 저장되어 프로그램이 실행하는 중간에 메모리를 할당합니다. 그래서 메소드가 끝나서 쓸일이 없어질때 자바의 Garbage Collector에 의해서 메모리에서 수거가 됩니다.

하지만 static은 프로그램이 실행 전 먼저 메모리에 잡히게 됩니다. 이때 실행 전이라고 하는 것은 프로그램이 메모리에 적재되고 명령어를 수행하기 전을 말합니다. 그리고 프로그램이 종료될때 해제가 되지요. 즉, heap의 영역보다 나중에 정리된다는 뜻입니다.

메모리 구조

 

가장 먼저 메모리에 적재되어 정적(static)으로 존재하기 때문에 static 멤버나 메소드는 클래스의 객체 생성없이 클래스의 이름만가지고도 사용할 수 있습니다.

public static void main(String[] args){
	Counter.cnt++;
    Counter.cnt++;
    System.out.println("cnt:"+Counter.cnt);
}
cnt:2

 

정적 메소드(static method)

이제 정적 변수는 알았는데, 정적 메소드는 그렇다면 무엇일까요? 역시 변수와 개념은 다르지 않습니다. 메모리에 메소드하나를 고정적으로 두어서 사용이 가능합니다. Counter 클래스를 변형시켜서 보도록 합시다. 

class Counter{
	public static int count(int cnt) {
		return cnt+1;
	}
}

public class TestMain {
	
    public static void main(String[] args){
    	Counter counter=new Counter();
    	System.out.println("count 1:"+Counter.count(1));
    	System.out.println("count 2:"+Counter.count(2));
    	System.out.println("counter's count 5:"+counter.count(5));
    }
}

 

그러면 메모리에는 이런식으로 잡히게 됩니다. 마치 counter라는 메소드를 공용으로 사용하듯 말이죠.

 

실행을 시켜보면 아래와 같은 결과를 갖게 됩니다.

count 1:2
count 2:3
counter's count 5:6

 

이제 메모리에 정적변수나 메소드가 다른 동적으로 메모리를 할당(new 키워드로 할당하거나 일반 메소드)하는 시점보다 먼저 메모리에 잡히게 되었다는 것을 알았다면 다음과 같은 규칙을 이해하게 될 것입니다.

 - 정적 메소드에서는 정적 메소드나 정적 변수만 사용할 수 있다. 멤버 변수는 사용할 수 없다.

이러한 이유는 정적 메소드에서는 자신보다 나중에 메모리가 할당되는 멤버 변수나 메소드는 언제 할당되는지 알수가 없습니다. 만약 아래의 코드처럼 count가 먼저 호출되는 상황에서 cnt가 메모리에 할당되지 않았다면요. 그래서 이처럼 빨간줄이 쳐지면서 static 만이 올수가 있다고 메시지를 띄워줍니다.

 

main 메소드에서 static 메소드만 호출 가능했던 이유

main 메소드 역시 static 메소드이기 때문에 여러분들이 클래스를 배우기 전 메소드를 배울때 묻지도 따지지도 않고 우선 static을 붙여서 메소드를 구현해보셨을 것입니다. main 메소드는 프로그램 실행 전 먼저 메모리에 할당되어야하기 때문이고, 메인이 static이기 때문에 호출되는 메소드 역시 static이어야하기 때문입니다.

 

이상으로 자바에서 static 개념에 대한 설명이었습니다. 무엇보다 메모리가 할당되는 시점만 이해한다면 왜 이런 결과가 나오는지 이해하기가 쉬울 것입니다.

반응형
블로그 이미지

REAKWON

와나진짜

,