우리는 난수를 사용하고 싶을 때가 있습니다. 하지만 이런 기능을 만드는 것은 좀처럼 쉬운 일은 아니지요. 귀찮기도 하구요.

C언어에서는 그러한 프로그래머를 위해서 난수생성함수를 제공하고 있습니다. 바로 rand라는 함수이지요. 외우기도 쉽네요. rand(om)으로 기억하면 되니까요.

 

rand

rand함수를 사용하기 위해서는 stdlib.h 헤더파일을 include해야합니다. rand함수는 0부터 RAND_MAX까지 범위까지 난수를 생성합니다. 함수 원형을 같이보시죠.

#include <stdlib.h>

int rand(void);

보시는 바와 같이 rand함수는 int형을 반환하게 됩니다. 아하, 그러면 rand함수를 쓰게 되면 랜덤인 정수형이 나오겠구나. 알 수 있죠?

이제 이 함수를 이용해서 1부터 100까지 정수 중 10개의 수를 랜덤하게 뽑아내는 프로그램을 짜보도록 하지요.

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

int main() { 
        int i; 
        for (i = 1; i <= 10; i++) 
                printf("%d ", (rand() % 100) + 1); 
        printf("\n");
}
이제 결과를 보도록 할까요??
 
42 68 35 1 70 25 79 59 63 65

 

 

오 랜덤하게 실행이 되는 군요. 100가지의 숫자 중 랜덤한 10개의 숫자를 뽑아냈습니다.

저는 신기해서 한번 더 실행해보겠습니다.

 

42 68 35 1 70 25 79 59 63 65

????

프로그램을 실행할때마다 바뀌지 않는데요? 우리는 이런 랜덤한 값을 원한게 아닙니다. 우리는 프로그램을 실행할때마다 랜덤하게 10개의 수를 출력하는 프로그램을 원하는 건데요. 지금 출력된 것은 단지 일정한 숫자 배열을 출력한 것과 같다고 느껴집니다.

왜 C언어는 우리에게 이런 사기를 치는 것일까요?

 

srand

사실 rand함수는 srand함수에 의존적입니다. srand의 s는 seed라는 뜻으로 이 seed값에 따라 rand의 값이 바뀌게 됩니다. srand는 rand함수와 같이 stdlib.h 헤더파일에 존재합니다.

만일 이 함수를 호출하지 않고 rand함수를 호출한다면 srand(1)을 호출하고 rand함수를 호출한 효과와 같습니다.

함수의 원형은 다음과 같은데요.

#include <stdlib.h>

void srand(unsigned int seed);

양의 정수만 seed로 사용할 수 있습니다. 그렇다면 우리가 srand의 seed값을 2로 주면 위의 결과와 다를까요?

위의 코드에서 for로프 위에 srand(2)를 추가해보세요.

46 17 99 96 85 51 91 32 6 17

 

아까와는 다른 결과를 볼 수 있군요. srand에서 seed를 바꿔서 실행시켜보세요. 나오는 값이 계속 달라짐을 알 수 있습니다. seed값만 바꿔주면 그 seed값에 따라 값을 랜덤하게 뽑아 올 수 있군요.

 

하지만 이것 마저도 아직 우리를 만족시킬 수가 없습니다. 이렇게 되면 프로그램을 실행시킬때마다 seed값을 바꾸고 다시 컴파일하는 과정을 거쳐야하기 때문이죠. 우리는 이런 허접한 코드는 쓰지 말도록 합시다.

 

우리는 이것보다 더 잘할 수 있습니다. 잘 할 수 있고 말고요. 한번 생각해봅시다. 프로그램 실행시 계속 바뀌는 값은 뭐가 있을까요?

주소값? (사실 제가 랜덤함수를 구현한다고 생각해볼때 고려해봤던 것 중 하나입니다.)

바로 시간입니다. 시간은 지금 이 순간에도 항상 바뀌고 있지요. 그래서 소개할 다음 함수가 time이라는 함수입니다.

time

time함수는 이름 그대로 시간에 대한 정보를 얻어오는 함수랍니다. 우선 time함수를 사용하기 위해서는 time.h라는 헤더파일을 include해야하지요.

함수의 원형을 한번 살펴볼까요?

#include <time.h>

time_t time(time_t *tloc);

 

이 함수는 1970년 1월 1일 0시 (UTC)부터 현재까지 흐른 시간을 반환합니다. 반환은 하지만 그 시간이 초단위입니다. 만일 우리가 현재까지 흐른 시간을 구하려면 만약 timeptr에 NULL을 전달하고 반환값을 받거나, 아니면 timeptr에 인자를 전달해서 현재까지 흐른 시간을 초단위로 받을 수 있습니다.

이제 아주 기본적인 사용법을 알게 됐으니 코드로 구현하도록 해봅시다. 

 

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

int main() { 
        int i; 
        srand(time(NULL));

        for (i = 1; i <= 10; i++)  
                printf("%d ", (rand() % 100) + 1);
        printf("\n");
}

 

단지 srand의 인자를 time(NULL)로 바꾼거 밖에 없습니다. time(NULL)을 호출하면 1970/1/1 0시부터 현재(프로그램 실행 시)까지 흐른 시간을 return한다고 했지요? 그러니 이 프로그램을 실행할때마다 srand의 seed값이 바뀌게 되는 겁니다.

이제 확인을 해봅시다.

첫번째 실행

79 61 20 69 3 67 82 24 63 35

두번째 실행

44 53 56 15 86 98 95 14 15 46

어떻습니까? 이제 이 프로그램을 여러번 실행해도 값이 다르게 나온다는 것을 알 수 있습니다.

 

그렇다면 우리가 랜덤한 값을 얻고자 할때는 rand, srand, time함수를 전부 다 써야하나요?

네, 이 세가지 함수들은 묶어서 기억하셔야합니다. 사용법은 어렵지 안잖아요 그쵸??

 

이상으로 여기까지 C언어에서 난수를 생성하는 쉬운 방법을 알아보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

컴파일 과정



Visual Studio에서 우리는 실행할때 F5(또는 Ctrl+F5)를 눌러서 우리가 만든 소스코드를 실행시켜봤죠? 우리는 너무 쉽게 프로그램을 실행시킨다고 생각할 수 있지만 의외로 몇몇 단계를 거치고 있습니다.


이번 시간에는 컴퓨터 실행파일이 어떻게 생겨나는지에 대해서 알아보도록 합시다. 


우리가 실행파일을 생성하는데까지는 아래와 같은 과정을 거치게 됩니다. 어? program.c와 program.exe는 알겠는데 나머지 파일들은 무엇일까요?





이 파일들의 정체를 알아내기 위해서 잠시 리눅스를 사용하도록 하겠습니다. 여러분들은 어떤 파일에 어떤 내용들이 기록되는지에 대해서 눈여겨 보면 될 것 같네요.


다음의 소스코드가 어떻게 실행파일로 변하는지 알아보지요.




#include <stdio.h>
#define A 10
#define B 20
int main(){
        int a=A;
        int b=B;
        int c=a+b;
        printf("%d + %d = %d\n",a,b,c);
}


전처리기(Preprocessor)


전처리기 구문(#으로 시작하는 구문)을 처리하는 것이 바로 전처리기라고 하는데요. 일반적으로 #으로 시작하는 부분을 거의 항상 사용합니다. 그것이 언제냐면 바로 #include지요. 너무나도 소중한 printf를 사용하기 위해서는 항상 #include <stdio.h>를 항상 명시해주어야 하죠.


#include를 통해서 stdio.h의 내용이 그대로 들어오게 됩니다!


또한 위의 코드에서 우리는 #define A 10 과 같은 줄을 볼 수 있는데요. 여기서 전처리기는 A라는 부분을 단순히 10으로 치환합니다.


자. 그렇다면 전처리 과정을 끝낸 program.i는 어떻게 변할까요?


gcc -E program.c -o program.i


위의 명령어로 program.i의 내용을 살펴봅시다.


program.i

# 1 "program.c"

# 1 "<built-in>"

# 1 "<command-line>"

# 1 "/usr/include/stdc-predef.h" 1 3 4

# 1 "<command-line>" 2

# 1 "program.c"

....
extern int printf (const char *__restrict __format, ...);
...

int main(){
 int a=10;
 int b=20;
 int c=a+b;
 printf("%d + %d = %d\n",a,b,c);
}


보세요. stdio.h의 내용이 main위의 그대로 들어오지요? 또한 #define A 10과 같은 내용은 없어지고 A가 10으로 치환된것을 알 수 있습니다.


전처리기는 너무나도 단순한 역할을 하는 군요.


중요한것은 전처리기가 컴파일 단계 맨 처음 단계라는 것을 기억하셔야합니다. 그래야지 전처리를 통한 조건부 컴파일을 이해하게 됩니다.



컴파일러(Compiler)

이제 전처리기를 거쳤으니 컴파일러로 컴파일해줍니다. 컴파일러는 고수준언어를 저수준언어로 나타내는 역할을 수행합니다. 저수준언어라는 것은 기계어와 가장 가까운 언어입니다.


이제 program.i로부터 어떻게 program.s가 생겨나는지 보도록 합시다.


gcc -S program.i -o program.s



program.s

.file   "program.c"

        .section        .rodata

.LC0:

        .string "%d + %d = %d\n"

        .text

        .globl  main

        .type   main, @function

main:

.LFB0:

        .cfi_startproc

        pushq   %rbp

        .cfi_def_cfa_offset 16

        .cfi_offset 6, -16

        movq    %rsp, %rbp

        .cfi_def_cfa_register 6

        subq    $16, %rsp

        movl    $10, -4(%rbp)

        movl    $20, -8(%rbp)

        movl    -8(%rbp), %eax

        movl    -4(%rbp), %edx

        addl    %edx, %eax

        movl    %eax, -12(%rbp)

        movl    -12(%rbp), %ecx

        movl    -8(%rbp), %edx

        movl    -4(%rbp), %eax

        movl    %eax, %esi

        movl    $.LC0, %edi

        movl    $0, %eax

        call    printf

        leave

        .cfi_def_cfa 7, 8

        ret

        .cfi_endproc

.LFE0:

        .size   main, .-main

        .ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-16)"

        .section        .note.GNU-stack,"",@progbits


뭐 저도 잘 모르겠습니다. 그냥 저수준언어로 변한것 밖에는 모르겠네요. 

근데 "%d + %d = %d\n" 는 우리가 printf에 썼던 문자열이라는 것을 알 수 있네요.

이것이 컴파일러가 하는 역할입니다. 이제 파일을 오브젝트파일로 변환하는 어셈블러를 보도록 합니다.


어셈블러(Assembler)

이제 완전히 기계어로 바꾸어 주는 역할을 합니다. 우리가 읽을 수 없거든요. 다음의 명령어를 통해서 기계어 파일을 만들고 확인해보도록 하죠.


gcc -c program.s -o program.o


program.o

^?ELF^B^A^A^@^@^@^@^@^@^@^@^@^A^@>^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@È^B^@^@^@^@^@^@^@^@^@^@@^@^@^@^@^@@^@^M^@

^@UH<89>åH<83>ì^PÇEü

^@^@^@ÇEø^T^@^@^@<8b>Eø<8b>Uü^AÐ<89>Eô<8b>Mô<8b>Uø<8b>Eü<89>Æ¿^@^@^@^@¸^@^@^@^@è^@^@^@^@ÉÃ%d + %d = %d

^@^@GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-16)^@^@^@^@^@^@^@^@^T^@^@^@^@^@^@^@^AzR^@^Ax^P^A^[^L^G^H<90>^A^@^@^\^@^@^@^\^@^@^@^@^@^@^@=^@^@^@^@A^N^P<86>^BC^M^Fx^L^G^H^@^@^@^@.symtab^@.strtab^@.shstrtab^@.rela.text^@.data^@.bss^@.rodata^@.comment^@.note.GNU-stack^@.rela.eh_frame^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^A^@^@^@^D^@ñÿ^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^C^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^C^@^C^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^C^@^D^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^C^@^E^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^C^@^G^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^C^@^H^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^C^@^F^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^K^@^@^@^R^@^A^@^@^@^@^@^@^@^@^@=^@^@^@^@^@^@^@^P^@^@^@^P^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@program.c^@main^@printf^@^@-^@^@^@^@^@^@^@

^@^@^@^E^@^@^@^@^@^@^@^@^@^@^@7^@^@^@^@^@^@^@^B^@^@^@



^@^(못 읽겠지?) 뭔가 비웃는것 같은 문자만 있네요. 네, 몰라요.

컴퓨터만 알고 있습니다.




hexdump로 볼까요?


hexdump -C program.o


00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|

00000010  01 00 3e 00 01 00 00 00  00 00 00 00 00 00 00 00  |..>.............|

00000020  00 00 00 00 00 00 00 00  c8 02 00 00 00 00 00 00  |................|

00000030  00 00 00 00 40 00 00 00  00 00 40 00 0d 00 0a 00  |....@.....@.....|

00000040  55 48 89 e5 48 83 ec 10  c7 45 fc 0a 00 00 00 c7  |UH..H....E......|

00000050  45 f8 14 00 00 00 8b 45  f8 8b 55 fc 01 d0 89 45  |E......E..U....E|

00000060  f4 8b 4d f4 8b 55 f8 8b  45 fc 89 c6 bf 00 00 00  |..M..U..E.......|

00000070  00 b8 00 00 00 00 e8 00  00 00 00 c9 c3 25 64 20  |.............%d |

00000080  2b 20 25 64 20 3d 20 25  64 0a 00 00 47 43 43 3a  |+ %d = %d...GCC:|

00000090  20 28 47 4e 55 29 20 34  2e 38 2e 35 20 32 30 31  | (GNU) 4.8.5 201|

000000a0  35 30 36 32 33 20 28 52  65 64 20 48 61 74 20 34  |50623 (Red Hat 4|

000000b0  2e 38 2e 35 2d 31 36 29  00 00 00 00 00 00 00 00  |.8.5-16)........|

000000c0  14 00 00 00 00 00 00 00  01 7a 52 00 01 78 10 01  |.........zR..x..|

000000d0  1b 0c 07 08 90 01 00 00  1c 00 00 00 1c 00 00 00  |................|

000000e0  00 00 00 00 3d 00 00 00  00 41 0e 10 86 02 43 0d  |....=....A....C.|

000000f0  06 78 0c 07 08 00 00 00  00 2e 73 79 6d 74 61 62  |.x........symtab|

00000100  00 2e 73 74 72 74 61 62  00 2e 73 68 73 74 72 74  |..strtab..shstrt|

00000110  61 62 00 2e 72 65 6c 61  2e 74 65 78 74 00 2e 64  |ab..rela.text..d|

00000120  61 74 61 00 2e 62 73 73  00 2e 72 6f 64 61 74 61  |ata..bss..rodata|

00000130  00 2e 63 6f 6d 6d 65 6e  74 00 2e 6e 6f 74 65 2e  |..comment..note.|

00000140  47 4e 55 2d 73 74 61 63  6b 00 2e 72 65 6c 61 2e  |GNU-stack..rela.|

00000150  65 68 5f 66 72 61 6d 65  00 00 00 00 00 00 00 00  |eh_frame........|

00000160  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000170  00 00 00 00 00 00 00 00  01 00 00 00 04 00 f1 ff  |................|

00000180  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000190  00 00 00 00 03 00 01 00  00 00 00 00 00 00 00 00  |................|

000001a0  00 00 00 00 00 00 00 00  00 00 00 00 03 00 03 00  |................|

000001b0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

000001c0  00 00 00 00 03 00 04 00  00 00 00 00 00 00 00 00  |................|

000001d0  00 00 00 00 00 00 00 00  00 00 00 00 03 00 05 00  |................|

000001e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

000001f0  00 00 00 00 03 00 07 00  00 00 00 00 00 00 00 00  |................|

00000200  00 00 00 00 00 00 00 00  00 00 00 00 03 00 08 00  |................|

00000210  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000220  00 00 00 00 03 00 06 00  00 00 00 00 00 00 00 00  |................|

00000230  00 00 00 00 00 00 00 00  0b 00 00 00 12 00 01 00  |................|

00000240  00 00 00 00 00 00 00 00  3d 00 00 00 00 00 00 00  |........=.......|

00000250  10 00 00 00 10 00 00 00  00 00 00 00 00 00 00 00  |................|

00000260  00 00 00 00 00 00 00 00  00 70 72 6f 67 72 61 6d  |.........program|

00000270  2e 63 00 6d 61 69 6e 00  70 72 69 6e 74 66 00 00  |.c.main.printf..|

00000280  2d 00 00 00 00 00 00 00  0a 00 00 00 05 00 00 00  |-...............|

00000290  00 00 00 00 00 00 00 00  37 00 00 00 00 00 00 00  |........7.......|

000002a0  02 00 00 00 0a 00 00 00  fc ff ff ff ff ff ff ff  |................|

000002b0  20 00 00 00 00 00 00 00  02 00 00 00 02 00 00 00  | ...............|

000002c0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

*

00000300  00 00 00 00 00 00 00 00  20 00 00 00 01 00 00 00  |........ .......|

00000310  06 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000320  40 00 00 00 00 00 00 00  3d 00 00 00 00 00 00 00  |@.......=.......|

00000330  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|

00000340  00 00 00 00 00 00 00 00  1b 00 00 00 04 00 00 00  |................|

00000350  40 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |@...............|

00000360  80 02 00 00 00 00 00 00  30 00 00 00 00 00 00 00  |........0.......|

00000370  0b 00 00 00 01 00 00 00  08 00 00 00 00 00 00 00  |................|

00000380  18 00 00 00 00 00 00 00  26 00 00 00 01 00 00 00  |........&.......|

00000390  03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

000003a0  7d 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |}...............|

000003b0  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|

000003c0  00 00 00 00 00 00 00 00  2c 00 00 00 08 00 00 00  |........,.......|

000003d0  03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

000003e0  7d 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |}...............|

000003f0  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|

00000400  00 00 00 00 00 00 00 00  31 00 00 00 01 00 00 00  |........1.......|

00000410  02 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000420  7d 00 00 00 00 00 00 00  0e 00 00 00 00 00 00 00  |}...............|

00000430  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|

00000440  00 00 00 00 00 00 00 00  39 00 00 00 01 00 00 00  |........9.......|

00000450  30 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |0...............|

00000460  8b 00 00 00 00 00 00 00  2e 00 00 00 00 00 00 00  |................|

00000470  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|

00000480  01 00 00 00 00 00 00 00  42 00 00 00 01 00 00 00  |........B.......|

00000490  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

000004a0  b9 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

000004b0  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|

000004c0  00 00 00 00 00 00 00 00  57 00 00 00 01 00 00 00  |........W.......|

000004d0  02 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

000004e0  c0 00 00 00 00 00 00 00  38 00 00 00 00 00 00 00  |........8.......|

000004f0  00 00 00 00 00 00 00 00  08 00 00 00 00 00 00 00  |................|

00000500  00 00 00 00 00 00 00 00  52 00 00 00 04 00 00 00  |........R.......|

00000510  40 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |@...............|

00000520  b0 02 00 00 00 00 00 00  18 00 00 00 00 00 00 00  |................|

00000530  0b 00 00 00 08 00 00 00  08 00 00 00 00 00 00 00  |................|

00000540  18 00 00 00 00 00 00 00  11 00 00 00 03 00 00 00  |................|

00000550  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000560  f8 00 00 00 00 00 00 00  61 00 00 00 00 00 00 00  |........a.......|

00000570  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|

00000580  00 00 00 00 00 00 00 00  01 00 00 00 02 00 00 00  |................|

00000590  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

000005a0  60 01 00 00 00 00 00 00  08 01 00 00 00 00 00 00  |`...............|

000005b0  0c 00 00 00 09 00 00 00  08 00 00 00 00 00 00 00  |................|

000005c0  18 00 00 00 00 00 00 00  09 00 00 00 03 00 00 00  |................|

000005d0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

000005e0  68 02 00 00 00 00 00 00  17 00 00 00 00 00 00 00  |h...............|

000005f0  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|

00000600  00 00 00 00 00 00 00 00                           |........|

00000608



그냥 보지마세요. 머리만 아픕니다. 

아 그냥 기계만 해석할 수 있는 언어구나~ 아시면 됩니다.




링커(Linker)

링커는 이름이 말해주듯 연결해주는 역할을 합니다. 여러개의 오브젝트파일을 하나로 합치거나 라이브러리를 합칠때 링커가 필요하다는 거지요.


우리는 일반적으로 개발할때 협업을 합니다. 그래서 위와 같이 오브젝트 파일(.o)라던가 라이브러리 파일이 여럿 존재할 수 있는데 하나의 소프트웨어를 만들기 위해서는 위의 파일들을 합쳐야하는 거죠. 이해되셨나요?




이제 실행파일을 만들어봅시다.


gcc program.o -o program.exe


그 후 실행을 시키면 


./program.exe

10 + 20 = 30


정상적으로 실행이 되는 것을 확인할 수 있습니다.


뭔가 복잡해보이지만 이러한 과정을 아는 개발자와 모르는 개발자와는 차이가 있다는 것을 알아두세요.


끝!

반응형
블로그 이미지

REAKWON

와나진짜

,

함수(Function)


함수를 중학생때부터 배우죠? 그렇기 때문에 저는 중학교 시절 수학을 포기했습니다. 여러분들은 저보다 뛰어나시니 그렇지는 않았겠죠?


C언어에서 함수는 아주 필수적이라고 할 수 있습니다. 함수에 대해서 간단히 말씀을 드리면 반복되는 코드를 하나로 묶어 필요할때 가져다가 쓴다는 것입니다.


다음의 코드는 어떻게 생각하시나요?

단순히 세 입력의 펙토리얼(!)을 구하여 곱하는 프로그램이지요.




#include <stdio.h>

int main() {
	int fact_a = 1, fact_b = 1, fact_c = 1;
	int a, b, c;
	int i;
	scanf("%d %d %d", &a, &b, &c);
	
	for (i = 1; i <= a; i++)
		fact_a*=i;
	
	for (i = 1; i <= b; i++)
		fact_b *= i;

	for (i = 1; i <= c; i++)
		fact_c *= i;

	printf("%d! * %d! * %d!= %d\n", a, b, c, fact_a*fact_b*fact_c);

	return 0;
}


프로그램이 잘 동작하는지 실행해보세요!

잘 돌아가지요? 문제 없습니다.


하지만 저는 조금 불만인데요. 펙토리얼을 세번 불러오는데, 세번 다 for루프를 돌려야하기 때문에 손가락이 아픈게 그 이유인데요.


만약 10개의 입력으로 들어오고 10개의 펙토리얼의 곱을 구하게 되면 위의 코드에서 for루프를 7개 더 추가해야한다는 것이 매우 불만이지요.


우리가 팩토리얼의 기능을 하는 하나의 코드를 두고 그 코드를 원할때 마다 불러온다면 어떨까요? 그런 역할을 하는 것이 바로 함수입니다.


함수 구현 방식

함수는 어떻게 생겨먹었을까요? 꽤나 간단하게 이해할 수 있을겁니다.

함수의 형태는 이렇습니다.


반환형 함수이름(매개변수1, 매개변수2, ... ){

         //몸체

         //return 반환값

}


o 반환형 : 반환되는 값의 자료형을 의미합니다. 함수에서는 반환되는 값이 없을 수 있는데요. 그 경우 void를 사용합니다. 만약 반환값이 있다면 return에서 값을 반환시켜 주면 됩니다. 그러니 반환값의 자료형과 반환형이 일치해야됩니다.


o 함수이름 : 함수를 불러올때 사용되는 이름입니다. 여러분이 지어주기 나름인데, 이 이름을 보고 사용하는 사람이 "아~ 이런 기능을 하는 함수겠구나!" 라고 알 수 있도록 잘 지어주어야 합니다.


o 매개변수 : 함수에 대한 input이라고 생각하면 됩니다. 이 매개변수를 토대로 함수의 반환값이 달라질 수 있습니다.


기본적인 매개변수의 동작은 전달받은 인자의 값을 복사하는 것입니다. (단, 포인터와 같은 매개변수는 값의 복사가 아닌 참조를 하게 됩니다.)


o 몸체 : 함수가 어떻게 기능을 할지 로직을 구현하는 부분입니다.


return : 반환값을 반환하는 명령입니다. return은 제어문으로 여러개 올 수 있습니다. 단, return은 한번만 진행하므로 만약 if 조건에서 return 문을 썼는데, if 조건에 걸리게 된다면 이 후의 코드를 실행시키지 않고 반환합니다.


혹은 반환형이 void이지만 그 함수를 어떤 조건에서 끝내고 싶다면 반환값없이 그냥 return을 사용해주면 그 즉시 함수를 끝냅니다.




우리는 위의 허접한 코드를 factorial이라는 함수를 만들어 조금 더 간편하게 바꿔볼 생각입니다.

위의 형식 그대로 사용해서 factorial을 구현한다면 이렇게 생겼겠죠?


int factorial(int n) {
	int ret = 1;
	int i;
	for (i = 1; i <= n; i++)
		ret *= i;
	return ret;
}


반환형태는 int형이면서 매개변수는 정수형 n입니다. 비교해보세요. 반환형과 반환값(ret)의 자료형이 일치하는 것을 알 수 있죠?


그 후 메인에서는 이 함수를 호출해서 쓰기만 하면 된답니다.


#include <stdio.h> int factorial(int n) { int ret = 1; int i; for (i = 1; i <= n; i++) ret *= i; return ret; } int main() { int fact_a = 1, fact_b = 1, fact_c = 1; int a, b, c; int i; scanf("%d %d %d", &a, &b, &c); fact_a = factorial(a); fact_b = factorial(b); fact_c = factorial(c); printf("%d! * %d! * %d!= %d\n", a, b, c, fact_a*fact_b*fact_c); return 0; }


어때요? 메인이 훨씬 간결해졌음을 알 수 있습니다.


호출 과정은 다음과 같습니다.



메인 함수를 실행하다가 factorial함수를 만났습니다. 그러면 factorial 함수를 실행시키고 함수가 끝나면 다시 메인함수로 돌아와서 그 전에 실행했던 것을 계속 진행하게 됩니다.



fact_a는 factorial 함수의 반환값이 저장됩니다. 나머지 fact_b, fact_c도 역시 마찬가지구요.


만약 10개의 입력이 주어진다하더라도 factorial만 10번 호출하면 되지요.

(아 물론 이 경우에는 배열과 반복문을 써야하겠지만)




함수 선언

근데 꼭 위에서만 함수를 정의하고 몸체를 구현해야할까요? 그럴필요는 없습니다. 

함수를 메인 아래에서 정의할 수도 있습니다.


하지만 꼭 위에서 함수 선언을 해주어야만 합니다. 왜 그러냐구요?

C언어는 절차지향언어이기 때문에 위에서 아래로 실행하기 때문이지요. 그래서 함수가 밑에 정의되어있는데 메인함수에서 그 함수를 호출한다고 하면 컴파일러는 그 함수를 본적이 없으니까 컴파일 에러를 토하게 됩니다.


위의 코드를 함수의 선언 방식으로 코딩해보도록 하면 다음과 같이 간단하게 바뀝니다.




#include <stdio.h>

int factorial(int);     //함수선언

int main() {
	int fact_a = 1, fact_b = 1, fact_c = 1;
	int a, b, c;
	int i;
	scanf("%d %d %d", &a, &b, &c);
	
	fact_a = factorial(a);
	fact_b = factorial(b);
	fact_c = factorial(c);
	
	printf("%d! * %d! * %d!= %d\n", a, b, c, fact_a*fact_b*fact_c);

	return 0;
}

int factorial(int n) {
	int ret = 1;
	int i;
	for (i = 1; i <= n; i++)
		ret *= i;
	return ret;
}


함수에 밑에 있군요. 메인함수 위의 선언이 있죠?

선언에서는 매개변수의 자료형만 적어주어도 상관없습니다.


재귀함수


함수에서 자신의 함수를 불러오는 것을 바로 재귀함수라고 합니다. factorial함수는 재귀함수로도 구현할 수 있습니다.

int factorial(int n) {
	if (n <= 1) return 1;
	return n*factorial(n - 1);
}

factorial함수에서 factorial함수를 호출하는 것을 볼 수 있지요? 매개변수 n과 다음 factorial(n-1)의 반환값을 곱하는 과정을 반복하고 있습니다. factorial의 매개변수 n은 하나씩 줄어들어 결국에는 1 이하가 될겁니다. 그때 1을 반환하지요.


결국 n * (n-1) * (n-2) * ... * 1이 되어 n!을 구현하는 함수죠.


그림으로 보면 더 이해가 쉽게 될겁니다.






3!을 구하는 과정을 보여줍니다. factorial(3)은 factorial(2)를 호출하고 factorial(2)는 factorial(1)을 호출하는 과정을 보여주고 있습니다.


이때 factorial(1)은 if조건문에 걸려 1을 반환하여 더이상 자신을 호출하지 않습니다. 

*** 이를 기저 사례라고 합니다.


재귀함수는 시스템의 스택을 사용하고 계속 사용할 경우 stack overflow가 발생할 수 있으므로 되도록이면 반복문을 사용하는 것이 좋습니다.


이상으로 함수에 대해서 기본적인 설명을 해봤습니다.


반응형
블로그 이미지

REAKWON

와나진짜

,

스트림

자바에서도 여러 입출력을 지원하지만 이번에 우리의 관심사는 바로 자바에서 제공하는 파일 입출력입니다.


그 전 우리는 스트림에 대한 이야기를 잠깐 간략하게 하고 넘어가겠습니다. 우선 파일에서 입력과 출력이라는 동작을 하려면 파일로 데이터를 전달하거나 파일로부터 전달 받는 길을 열어주어야합니다.


그러한 길을 스트림이라고 하지요.





파일로부터 입력을 받는 스트림을 입력스트림, 출력을 보내는 스트림을 출력스트림이라고 합니다.

그리고 바이너리 형태로 데이터를 입출력하는 스트림을 이진스트림, 문자형태로 입출력하는 스트림을 텍스트스트림이라고 합니다.




스트림을 알았으니 파일입출력을 알아보도록 합시다.


FileReader와 FileWriter


import java.io.*; public class Main { public static void main(String[] args) throws IOException{ File file=new File("test.txt"); if(!file.exists()) file.createNewFile(); FileWriter fw=new FileWriter(file); char []buf= {'m','e','s','s','a','g','e','\r','\n'}; for(int i=0;i<buf.length;i++) fw.write(buf[i]); fw.close(); FileReader fr=new FileReader(file); int EOF=-1; int c; while((c=fr.read())!=EOF) { System.out.print((char)c); } fr.close(); } }


이 코드는 test.txt파일에 "message"라는 문자열을 기록하고 읽어오는 프로그램이에요. 한줄 한줄씩 보도록 하지요.


우선 File 입출력시에는 IOException이 발생할 수 있고 처리가 귀찮으니 저 멀리 보내버리도록 합시다. 가버렷!


일단 file을 열어줘야겠지요? 절대 경로로 지정하지 않는다면 현재 프로젝트 디렉토리에 파일을 엽니다.


하지만 그 파일이 없다면 새로 생성합니다. 

이것을 다음의 라인이 나타냅니다.


File file=new File("test.txt"); if(!file.exists()) file.createNewFile();


그 후 파일에 "message"라는 문자열을 기록합니다. 그때 사용하는 클래스가 바로 FileWriter라는 클래스이지요.

FileWriter의 메소드 write를 통해서 char 배열의 문자열을 하나씩 기록한 후 스트림을 닫습니다.




닫아주어야 파일에 문자열이 입력이 됩니다! 파일을 닫지 않고 파일에 입력하려면 그렇지 않으면 flush함수를 사용하세요. 


FileWriter fw=new FileWriter(file);
char []buf= {'m','e','s','s','a','g','e','\r','\n'};
for(int i=0;i<buf.length;i++)
	fw.write(buf[i]);
fw.close();

이후 한문자씩 읽어오는데 그 역할을 수행하는 클래스가 바로 FileReader입니다. 파일의 끝은 int형의 -1입니다. 그래서 -1을 만날때까지 한문자 한문자 출력합니다.

int형태로 읽어왔으니 char로 바꿔줘야겠지요?


FileReader fr=new FileReader(file);
int EOF=-1;
int c;
while((c=fr.read())!=EOF) {
	System.out.print((char)c);
}
fr.close();


이제 수행을 해보도록 할게요. 어떤 변화가 있는지..


test.txt라는 파일이 생겼네요! 



이것을 까보면!




그안에 우리가 집어넣은 문자열이 존재하는 군요.


eclipse상의 결과 역시 "message"라는 문자열을 출력하는 군요.


파일에 기록한 문자열을 자바프로그램이 읽어온 것이랍니다.



한글자 한글자 읽어오는 것이 여간 불편한 것이 아니죠?

문자열을 사용해서 쓰는 것이 훨씬 더 편할텐데요.

그리고 읽어올때도 배열을 써서 읽어오는 것이 훨씬 편하구요.




그런 방법이 아래에 나와있습니다.




import java.io.*;


public class Main {
	public static void main(String[] args) throws IOException{
		File file=new File("test.txt");
		if(!file.exists())
			file.createNewFile();
		
		FileWriter fw=new FileWriter(file);
		fw.write("Hello, world!!\r\n");
		
	
		fw.close();
		
		FileReader fr=new FileReader(file);
		
		while(true) {
			char []buf=new char[4];
			int ret=fr.read(buf);
			if(ret==-1) break;
			System.out.print(String.valueOf(buf));
		}
		fr.close();
	}
}


FileWriter는 String을 받는 write메소드가 있어서 문자열로 그대로 파일에 기록할 수 있습니다.


중요한건 FileReader인데요. 우선 buf라는 char형 4개에 문자열을 계속 입력받는 거지요. 만약 파일에서 더 읽어올 것이 없다면 -1을 리턴하게 되니까 그 반환형이 -1이면 while루프를 탈출하면 됩니다.


이제 실행 후 파일을 확인해보고 이클립스에서도 확인해 봅시다.





파일에 제대로 적혀있고 이클립스 출력도 이 문자열이 나오는 것을 확인할 수 있죠?



Line 단위 입출력


아직도 불편하긴 합니다. /r/n을 통해서 개행하는 것도 별로구요. 보통 문자 입출력시에는 라인 단위로 입출력을 하기 때문에 라인별로 입력을 할 수 있었으면 좋겠습니다.


그래서 나온것이 버퍼단위의 입출력을 담당하고 있는 BufferedReader, BufferedWriter입니다.



import java.io.*;


public class Main {
	public static void main(String[] args) throws IOException{
		File file=new File("test.txt");
		if(!file.exists())
			file.createNewFile();
		
		BufferedWriter bw=new BufferedWriter(new FileWriter(file));
		
		bw.write("Hello, world!");
		bw.newLine();
		bw.write("Hello, world!!");
		bw.newLine();
		bw.write("Hello, world!!!");
		bw.newLine();
		bw.close();
		
		BufferedReader br=new BufferedReader(new FileReader(file));
		String line=null;
		while((line=br.readLine())!=null)
			System.out.println(line);
			
		br.close();
	}
}


BufferedWriter와 BufferedReader는 버퍼의 사이즈를 지정할 수도 있습니다. 그렇지 않으면 Default 사이즈로 버퍼에 담습니다.


BufferedWriter의 newLine메소드는 개행을 말합니다. 쉽죠? 별거 없어요.


여기서는 Hello, world!를 3라인에 걸쳐 출력합니다. (느낌표 갯수만 다르고요)


이제 BufferedReader를 통해서 읽어옵니다. 

readLine은 String 형태의 문자열을 반환하는데, 만약 더이상 출력할 문자열이 없으면 null을 반환하죠.


그래서 null을 만나게 되면 while루프를 탈출합니다.


이제 실행후 파일과 이클립스화면을 보면 둘의 결과는 같다는 것을 알 수 있습니다.


test.txt




이클립스 실행결과


Hello, world!

Hello, world!!

Hello, world!!!



어떻습니까?


BufferedReader와 BufferedWriter 정말 편리하죠?

이 두 클래스는 조금 빈번하게 쓰입니다. 기억해두세요.


아직 파일입출력에 대해서는 더 할 이야기가 나왔습니다. 나중에 더 이야기 해보도록 하지요.

반응형
블로그 이미지

REAKWON

와나진짜

,

안녕하세요. 이번 시간에는 클래스 중 조금 특별한 속성을 갖고 있는 추상 클래스(Abstract Class)와 자바에서 등장하는 인터페이스(Interface)에 대해서 알아보는 시간을 갖도록 하겠습니다.


추상클래스(Abstract Class)


름부터가 아주 비호감입니다. 저는 추상이라는 단어를 무척이나 싫어하거든요.


뭔가 있는 듯 없는 듯하면서도.. 만질수 있을 듯 없을 듯하면서도.. 볼수 있을듯 없을 듯한 그 거시기한 거.. 그런건데


아주아주 간단히 말해서 추상메소드가 적어도 0개 있는 클래스가 추상클래스라고 합니다.


장난하냐? 그러지 마시고 일단 추상 메소드가 무엇인지 말씀 드리고 시작하겠습니다.


추상클래스는 메소드의 선언만 되어있을 뿐 정의는 되어있지 않은 것을 말합니다. 몸통이 없다는 것이죠.


몸통이 없이 메소드의 선언만 존재하는 것이 추상 메소드이며 추상 메소드를 0개 이상 갖는다면 추상클래스이다 라고 말할 수 있겠네요. 

0개 이상이라는 말은 추상메소드를 갖지 않아도 추상 클래스가 될 수 있다는  것인데, 이렇게 되면 보통의 클래스를 의미하기 때문에 보통은 한개 이상 추상 메소드를 갖는다면 추상 클래스라고 합니다.




아래와 같이 사용합니다. 우선 추상클래스를 정의하려거든 class앞에 abstract라는 키워드를 붙여줍니다.


추상메소드 역시 마찬가지입니다. abstract 키워드를 붙이고 메소드 구현부만 없으면 됩니다. 

abstract class AbstractClass{
	int x;
	public void NormalMethod() {}
	public abstract void AbstractMethod(); 
}

보통의 클래스와의 차이점은 객체를 직접생성할 수 없다는 점입니다.


그래서 이 추상 클래스를 상속받아 그 상속받은 클래스의 객체로 생성해야합니다. 또는 다형성의 성질을 이용할 수 있지요.


사용법은 아래의 코드와 같습니다.


abstract class Animal{
	public void seeFood() {
		System.out.println("내가 음식을 봤을 때");
	}
	abstract public void cry();
}

class Dog extends Animal{

	@Override
	public void cry() {
		System.out.println("왈!! 왈왈!! 왈왈~!!! 왈월워뤙왈!!! 멍멍!");
	}
	
}

class Cat extends Animal{

	@Override
	public void cry() {
		System.out.println("야옹~~ 옹옹오오오오오옹~~~~~~ 야오우오우오우옹~~~~");
	}
	
}
public class Main {

	public static void main(String[] args) {
		Animal dog=new Dog();
		dog.seeFood();
		dog.cry();
		
		System.out.println();
		
		Animal cat=new Cat();
		cat.seeFood();
		cat.cry();
	}
}




Animal을 상속받은 클래스 Dog와 Cat은 무조건 abstract의 메소드를 오버라이딩해주어야 합니다. 그렇지 않으면 Animal클래스를 상속할 수 없습니다.

결과는 어떻게 될까 예상이 되시나요?


내가 음식을 봤을 때

왈!! 왈왈!! 왈왈~!!! 왈월워뤙왈!!! 멍멍!


내가 음식을 봤을 때

야옹~~ 옹옹오오오오오옹~~~~~~ 야오우오우오우옹~~~~



여기서 추상클래스와 비슷한 인터페이스(Interface)를 소개합니다.



인터페이스(Interface)


인터페이스는 명세라고도 불리는데요. 추상클래스의 가장 극단적인 형태라고 보면 됩니다. 

인터페이스는 전부가 추상메소드로 이루어져 있습니다.

자신의 변수도 쓸수 없고 오로지 메소드의 선언만이 있어야합니다.

아래처럼 말이죠.


interface Interface {
	public void method1();
	public void method2();
}

마치 추상클래스로 표현하자면 이렇게 표현할 수도 있겠네요.


abstract class Interface{
	public abstract void method1(); 
	public abstract void method2(); 
}


인터페이스의 사용 예제는 아래와 같습니다.


interface Animal {
	public void cry();
	public void sleep();
}

class Dog implements Animal{

	@Override
	public void cry() {
		System.out.println("왈!! 왈왈!! 왈왈~!!! 왈월워뤙왈!!! 멍멍!");
	}
	@Override
	public void sleep() {
		System.out.println("자신의 집에서 잠을 잡니다.");
	}
	
}

class Cat implements Animal{

	@Override
	public void cry() {
		System.out.println("야옹~~ 옹옹오오오오오옹~~~~~~ 야오우오우오우옹~~~~");
	}
	@Override
	public void sleep() {
		System.out.println("집사 얼굴 위에서 잠을 잡니다.");
	}
	
}
public class Main {

	public static void main(String[] args) {
		Animal dog=new Dog();
		dog.cry();
		dog.sleep();
		
		System.out.println();
		
		Animal cat=new Cat();
		cat.cry();
		cat.sleep();
	}
}

Dog와 Cat은 Animal 인터페이스를 implements하고 있습니다. 근데, 이제까지 봐왔던 extends가 아니군요. 그 이유는 아래에서 설명하도록 하겠습니다.


결과는 이렇게 됩니다.


왈!! 왈왈!! 왈왈~!!! 왈월워뤙왈!!! 멍멍!

자신의 집에서 잠을 잡니다.


야옹~~ 옹옹오오오오오옹~~~~~~ 야오우오우오우옹~~~~

집사 얼굴 위에서 잠을 잡니다.




추상클래스 VS 인터페이스


한가지 중요한건 이 둘의 차이에 대해서 알아야합니다.


추상클래스는 역시 클래스입니다. 단독으로 객체를 생성할 수만 없지 나머지는 보통의 클래스처럼 생성자, 변수, 메소드를 갖고 있을 수 있습니다. 그렇기 때문에 자식 클래스에서는 변수나 구현된 메소드를 물려받기 때문에 상속(extends)받을 수 있는 것이죠.


인터페이스 역시 직접 객체를 생성할 수 없습니다. 구현되어야 할 메소드만 명시해 놓고 인터페이스를 받는 클래스는 그 인터페이스에 맞는 메소드를 구현해야합니다. 그렇기 때문에 implements(구현하다)라는 키워드로 인터페이스를 전달받을 수 있지요.


클래스와는 다르게 여러개의 인터페이스를 클래스가 구현할 수 있습니다. 클래스로 따지자면 다중상속과 비슷한 개념입니다.

자바는 다중상속을 지원하지 않지만 다중 인터페이스를 통한 상속은 지원하지요.




아주 간단한 예를 들어보도록 하지요.

개발시에 동료에게 아래와 같은 Operator의 인터페이스를 구현하는 클래스를 만들기를 부탁했다고 한다면 여러분의 착한 동료는 그렇게 하겠죠? 


interface Calculator{
	public int sum(int a,int b);
	public int subtraction(int a,int b);
}



그래서 구현해야하는 sum과 subtraction을 호출하는 부분에 대해서만 신경써서 개발하고 sum과 subtraction 내부는 생각하지 않아도 됩니다


단, 동료에게 어떤 기능을 하는 메소드이니 이런 메소드 형식으로 만들어 달라라고 이야기 해주어야 겠죠?


여기서 여러분은 동료에게 이런 기능을 하는 멤버 메소드를 만들어라라고 명시한 겁니다.


여기 Calculator 명세를 너에게 줄건데 너는 꼭 int형 매개변수 2개를 받고, int 반환형을 갖는 sum을 구현하고

마찬가지로 int형 매개변수 2개를 받고, int 반환형을 갖는 subtraction이라는 메소드를 구현하라고 명세를 만들어 준 거에요.




이제 왜 인터페이스는 implements를 써서 받고, 추상클래스는 extends를 써어 받는 지 차이점을 알겠죠?



반응형
블로그 이미지

REAKWON

와나진짜

,

다형성(Polymorphism)


다형성이라는 개념은 OOP에서 아주 중요한 개념이므로 모르면 OOP에 대해서 제대로 안다고 할 수 없는 개념입니다.


각 요소들이 여러 가지 자료형으로 표현될 수 있다는 것을 말하게 되는데, 반댓말로는 단형성이 있습니다. 한가지의 요소는 한가지의 형태로만 매칭된다는 것을 의미합니다.


음... 일단 모르겠어요.. 다형성이 정확히 무엇인지.


암튼 뭐.. 앞에 '다'라는 의미는 '많은 다(多)' 자가 아니겠어요?? 뭔가 여러가지 (자료)형을 말하는 것 같은데 클래스를 만들어가면서 알아보도록 하지요.


여기 People이라는 클래스가 있습니다. 아주 간단하게 정의한 클래스죠. 그 안에는 printInfo라는 멤버메소드가 있군요.




class People{
	
	public void printInfo() {
		System.out.println("나는 사람입니다.");
	}
}



People 클래스에서 printInfo를 호출하게 되면 지가 사람이라는 군요.


그 밑에 Man과 Woman 클래스는 People클래스를 상속합니다.


class Man extends People{}
class Woman extends People{}


이후 메인에서는 이 두 클래스를 객체로 만들어 printInfo를 호출합니다. 


public class Test {

	public static void main(String[] args) {
		Man man=new Man();
		Woman woman=new Woman();
		
		man.printInfo();
		System.out.println();
		woman.printInfo();
	}
}


이후 실행을 하게 되면 아래의 결과가 나오게 되겠죠.


나는 사람입니다.


나는 사람입니다.


두 클래스 Man과 Woman은 People이라는 클래스를 상속받았으므로 printInfo 호출시 People의 printInfo를 호출할 수 있다는 것, 뭐 놀랍지 않군요.


이제 이것을 토대로 다형성을 세세하게 알아보도록 합시다.



Woman, Man은 People이다





UML 다이어그램으로 본다면 위의 그림과 같을 겁니다.

Man과 Woman은 People이라는 클래스를 상속하기 때문이에요.


우리는 이 다이어그램을 이런 관점으로 한번 바라볼 수 있을까요?


Man은 People이다. (남자는 사람이다.)

Woman은 People이다. (여자는 사람이다.)


현실 세계에서 이 다이어그램을 말로 풀어보아도 그 의미가 맞습니다.

그 반대는 어떨까요?


People은 Man이다. (사람은 남자이다.)

People은 Woman이다. (사람은 여자이다.)


사람은 남자인가요? 아니면 사람은 여자인가요?

그렇지 않습니다. 사람은 남자인지, 여자인지 알 수가 없습니다.


그렇기 때문에 반대로 표현하면 모호해진다는 것을 알 수 있지요.


여기서 중요한 점은 Man은 People로 표현할 수 있고, Woman도 People로 표현할 수 있다는 것입니다.


이것이 다형성의 개념이 나오게 됩니다. Man과 Woman은 People이기 때문에 People이라는 자료형으로 받을 수 있습니다.




한번 확인해볼까요?

public class Test {

	public static void main(String[] args) {
		People people=new Man();
		
		people.printInfo();
		System.out.println();
		
		people=new Woman();
		people.printInfo();
		
	}
}


실행을 시켜서 확인해보면 위의 결과와 동일한 것을 알 수 있습니다. 

Man과 Woman은 People(부모클래스)로 받을 수 있다는 점을 기억하세요!



다형성과 오버라이딩

여기서 Man과 Woman은 printInfo를 물려받았고 오버라이딩(Overriding)할 수 있다는 것을 알고 있습니다.


그래서 저는 Man과 Woman의 printInfo를 그 클래스에 맞도록 오버라이딩하고 싶습니다. Man과 Woman 클래스를 다음과 같이 수정해보도록 하지요.



class Man extends People{
	@Override
	public void printInfo() {
		super.printInfo();
		System.out.println("그리고 나는 남자입니다.");
	}
	
}
class Woman extends People{
	@Override
	public void printInfo() {
		super.printInfo();
		System.out.println("그리고 나는 여자입니다.");
	}
}

그리고 실행해본다면 


나는 사람입니다.

그리고 나는 남자입니다.


나는 사람입니다.

그리고 나는 여자입니다.


오버라이딩된 printInfo를 호출한다는 것을 알 수 있습니다. 오...

다형성에서 People은 자식클래스에서 재정의된 메소드를 호출할 수 있다는 것입니다.


그렇다면 Woman과 Man에서 단독으로 정의한 메소드는 어떻게 될까요?

Man과 Woman클래스에서 다음과 같이 메소드를 추가해보도록 합시다.



class Man extends People{
	@Override
	public void printInfo() {
		super.printInfo();
		System.out.println("그리고 나는 남자입니다.");
	}
	
	public void enlist() {
                System.out.println("내일 군대를 갑니다.");
		System.out.println("충성!");
	}
	
}
class Woman extends People{
	@Override
	public void printInfo() {
		super.printInfo();
		System.out.println("그리고 나는 여자입니다.");
	}
	
	public void makeUp() {
                System.out.println("예뻐질 거랍니다.");
		System.out.println("톡톡 촵촵!");
	}
}


그리고 people.enlist를 호출하려한다면 호출이 되지 않습니다. 왜냐하면 People 형이기 때문이죠. People 클래스는 enlist라는 메소드를 갖지 않기 때문에 호출할 수 없습니다.


이런 경우에는 데이터 형에 맞게 캐스팅해주어서 사용해야합니다.

바로 아래처럼요.


public class Test {

	public static void main(String[] args) {
		People people=new Man();
		people.printInfo();
		((Man)people).enlist();
		
		System.out.println();
		
		people=new Woman();
		people.printInfo();
		((Woman)people).makeUp();
		
	}
}


왜 그런걸까요?

People은 자신을 상속한 클래스 중에서 어떤 매소드를 만들지, 어떤 멤버 변수를 만들어낼지 미리 알아낼 수 없기 때문이죠.


때문에 그 메소드가 있는 객체로 직접 캐스팅해주어서 매소드를 사용해야합니다.


이해를 돕기 위한 그림이 아래에 있습니다. People이라는 자료형이 사용가능한 메소드는 printInfo밖에 없습니다. 그래서 Man의 printInfo메소드를 사용할 수 있습니다. 


또한 new가 동적 메모리를 할당하는 역할을 하므로 Man이 실제 메모리에 잡히게 됩니다. 따라서 형변환을 Man으로 하는 것이 가능한 것이죠.






이와 같은 다형성은 어디에서 쓰일까요?


대표적으로 메소드에서 매개변수로 People을 상속하는 클래스를 받을때 사용할 수 있습니다. 


        public static void func(People people) {
		people.printInfo();
	}
	public static void main(String[] args) {
		Man man=new Man();
		Woman woman=new Woman();
		func(man);
		
		System.out.println();
		func(woman);
	}

func의 매개변수 people은 People의 객체이기 때문에 그것을 상속하는 모든 클래스를 받아 낼 수 있어요.


그래서 Object 객체로 모든 객체를 받을 수 있는 것도 바로 이러한 다형성의 속성때문입니다. 




또한 필요에 의해서는 instanceof 연산자를 사용해서 캐스팅할 수 있습니다.




        public static void func(People people) {
		people.printInfo();
		if(people instanceof Man) 
			((Man)people).enlist();
		if(people instanceof Woman)
			((Woman)people).makeUp();
		
	}
	public static void main(String[] args) {
		Man man=new Man();
		Woman woman=new Woman();
		func(man);
		
		System.out.println();
		func(woman);
	}


OOP의 가장 중요한 특징 중 하나 다형성에 대해서 알아보았습니다. 도움이 되었스면 좋겠습니다. 화이팅!


반응형
블로그 이미지

REAKWON

와나진짜

,

파일 입출력 2


간단하게 파일에서 읽고 쓰는 것은 이제 알겠습니다.


하지만 단순히 파일을 처음부터 차례대로 읽는 것이 아니라, 어떤 위치 이후에서 읽고 쓰는 것도 가능할까요?


할 수 있습니다. 바로 fseek함수를 이용해서 말이죠.


fseek

int fseek(FILE *stream, long int offset, int origin);


fseek함수는 파일의 위치를 임의로 조작할 수 있습니다. 파일은 순차적으로 읽고 씁니다. 하지만 특정위치에 있는 데이터를 읽거나 쓸 필요가 있는 것이죠.


stream은 파일을 의미합니다.

offset은 origin에서 얼마만큼 이동할 것인지 나타냅니다.

origin은 기준 위치를 지정합니다.




우리가 fread를 여러번 호출해도 순차적으로 읽어올 수 있는 이유는 파일포인터때문입니다. 파일포인터는 현재 파일의 위치를 기억하고 있습니다. 그렇기 때문에 순차적으로 읽을 수 있는 거지요.






우리는 파일포인터의 위치를 임의로 지정하여 특정 위치의 데이터를 읽고 쓰는 것이 가능합니다. 파일포인터에 관해서는 세가지 매크로가 있는데요.


SEEK_SET : 파일의 처음

SEEK_CUR : 지금 현재 파일포인터 위치

SEEK_END : 파일의 끝


특정 위치에서 읽기

특정 위치에 값을 읽어오는 코드를 한 번 보겠습니다.


text.txt

ABCDEFGHIJ


#include <stdio.h> #include <string.h> int main() { FILE *fp = fopen("text.txt", "r"); char buf[30]; memset(buf, 0, sizeof(buf)); fseek(fp, 5L, SEEK_SET); // fseek(fp, -4L, SEEK_END); fread(buf, sizeof(char), sizeof(buf), fp); printf("%s\n", buf); fclose(fp); return 0; }


파일의 처음위치부터 5를 건너뛰면 파일포인터는 F 앞을 가리키고 있습니다.


결과

FGHIJ



반면에 파일 끝에서 4칸 앞은 offset이 음수가 됩니다. 처음 fseek을 주석처리하고 밑의 주석을 해제하고 실행해보세요. 결과는 이렇습니다.


GHIJ



특정 위치에 쓰기

반면 특정 위치에 쓰는 것도 가능합니다. 단! 파일을 열때 r+모드로 여세요. 


text.txt

ABCDEFGHIJ



#include <stdio.h>
#include <string.h>
int main() {
	FILE *fp = fopen("text.txt", "r+");
	char buf[30]="_INSERT_";

	fseek(fp, -5, SEEK_END);
	fwrite(buf, sizeof(char),strlen(buf), fp);
	
	fclose(fp);
	return 0;
}


파일 끝에서 5칸 앞에 _INSERT_라는 문자열을 추가하는 코드입니다. F 앞의 위치입니다. 

하지만 그 자리를 덮어쓰게 됩니다. 그래서 결과는 이렇습니다.


text.txt

ABCDE_INSERT_




ftell

파일 포인터의 위치를 알아옵니다. 

long int ftell(FILE *stream)


열었던 파일을 매개변수로 넣어주면 끝입니다.


성공시 0을 포함한 양수를 반환합니다.


text.txt

ABCDEFGHIJ


#include <stdio.h>

int main() {
	FILE *fp = fopen("text.txt", "r+");
	int pos;
	fseek(fp, 5, SEEK_SET);
	pos = ftell(fp);
	printf("현재 파일 포인터 : %d\n", pos);
	fclose(fp);
	return 0;
}


결과

현재 파일 포인터 : 5


ftell을 응용해서 파일의 크기도 알아올 수 있습니다.

#include <stdio.h>

int main() {
	FILE *fp = fopen("text.txt", "r+");
	int pos;
	fseek(fp, 0, SEEK_END);
	pos = ftell(fp);
	printf("파일크기 : %d\n", pos);
	fclose(fp);
	return 0;
}


파일 속성에서 알아본 크기는 10바이트입니다. 위의 코드의 결과는 어떨까요?


파일크기 : 10


바이트 단위로 일치한다는 것을 알 수 있네요.


파일에 대해서는 다음에 또 이야기하도록 합시다.

반응형
블로그 이미지

REAKWON

와나진짜

,

포인터배열

포인터라는 것은 조금 알겠는데 포인터배열은 무엇일까요... 포인터도 힘들게 배우는데 말이죠. 정말 산넘어 산입니다.

포인터배열이란 포인터를 원소로 갖는 배열을 의미합니다. 포인터 각각을 배열로 표현한 것이지요.

느낌이 오시나요?

코드와 그림으로 알아보도록 합시다.

 

#include <stdio.h>
int main() { 
        int a = 10; 
        int b = 20; 
        int c = 30; 
        int *pArr[3] = { &a,&b,&c };

        printf("%d\n", *pArr[0]);
        printf("%d\n", *pArr[1]);
        printf("%d\n", *pArr[2]);
}


여기, 포인터배열을 간단하게 알아볼 수 있는 코드입니다. pArr은 평소에 보던 배열과는 다르게 앞에 *(pointer)를 볼 수 있지요?

연산자 우선순위에 의하면 배열첨자([])가 포인터(*)연산보다 먼저입니다. 그렇기 때문에 배열 3개가 있고, 그 배열의 원소는 포인터라는 의미가 됩니다. 

따라서 배열의 원소인 pArr[0]은 a의 주소를 갖고있고, pArr[1]은 b의 주소를 갖고 있고, pArr[2]는 c의 주소를 갖고 있습니다.

이 코드의 상황을 그림으로 나타냈습니다.

 

 

 

만약 pArr[0]을 찍어보면 a의 주소가 나오게 됩니다. a의 값에 접근하고 싶다면 포인터 연산을 해주면 됩니다. 바로 *pArr[0], 이렇게요.

그렇게 포인터 원소를 배열로 나열했기 때문에 포인터배열이라 부릅니다.

 

포인터배열로 배열 가리키기

포인터가 배열의 시작주소를 가리킬 수 있다는 것은 이제 잘 알겁니다. 아닌가? 그렇다면 어떤 배열들을 포인터 배열로 가리킬 수도 있다는 느낌이 오시나요?

코드를 통해 느껴봅시다. 

 

#include <stdio.h>  
int main() { 
        int i, j; 
        int a[5] = { 1,2,3,4,5 };
        int b[6] = { 10,20,30,40,50,60 }; 
        int c[7] = { 100,200,300,400,500,600,700 };
        int *pArr[3] = { a,b,c }; 
        int sub_len[3] = 
        { sizeof(a) / sizeof(int), sizeof(b) / sizeof(int), sizeof(c) / sizeof(int) }; 
        int len = sizeof(pArr) / sizeof(int*);   

        for (i = 0; i < len; i++) { 
                for (int j = 0; j < sub_len[i]; j++) 
                        printf("%d ", pArr[i][j]); 
                printf("\n"); 
        } 
}

a,b,c 배열의 길이는 전부 다릅니다. 하지만 문제없지요. 왜냐면 포인터는 배열의 시작주소만 알면 되기 때문입니다.

각각의 포인터들(pArr[0], pArr[1], pArr[2])은 배열의 시작주소 a, b, c를 가리키고 있습니다.

 

포인터 역시 배열처럼 첨자를 쓸수도 있다는 것을 다들 아실겁니다.

 

 

 

 

 

배열포인터

배열포인터는 무엇일까요? 아까 포인터배열은 포인터를 배열로 나열한 것이라고 설명했으니, 배열포인터는 배열을 가리키는 포인터가 아닐까요?

 

배열포인터는 다음과 같이 정의합니다.

int (*pArr)[3]

 

앞서 본 포인터배열과는 다르게 괄호가 추가 되었죠. 이 한 끗 차이에 의미가 변하게 됩니다. 우선 포인터이긴한데, 길이 3을 갖는 int형 배열만을 가리킬 수 있다는 점입니다. 우선 다음 기본적인 코드를 봅시다. 우리가 아는 내용입니다.

#include <stdio.h> 
int main() { 
        int i = 0; 
        int arr[5] = { 1,2,3,4,5 };
        int *pArr = arr; 

        for (i = 0; i < sizeof(arr)/sizeof(int); i++) 
                printf("%d ", pArr[i]);
        printf("\n");
}  
r


pArr은 일차원배열 arr의 시작주소를 가리키고 있다는 내용입니다. 그래서 배열처럼 인덱싱을 통하여 각 원소를 출력하고 있지요. arr의 길이 5는 신경쓰지 않습니다. 단지 가리키기만 하고 있습니다.

이제 위의 정의를 다시 봅시다. 

 

int arr[행][열] ={ {...}, {...}, {...}};

int (*pArr)[열]= arr;

 

(*pArr)은 arr의 가장 높은 차원의 길이 3(행)은 신경쓰지 않습니다. 단지 그 시작주소만 가리키기만 하면 되거든요. 하지만 그 보다 낮은 차원의 길이4(열)는 알아야만 합니다. 그래야만 다음 행을 구해낼 수 있기 때문이죠.

어떻게??

pArr[0]은 4개의 int배열을 가리키고 있는 포인터입니다. 따라서 sizeof(pArr[0])을 찍어보면 그 길이가 16이라는 것을 알 수 있습니다. 그래서 주소를 계산할때 지금 행의 주소에 16을 더해야 다음 행을 구할 수 있습니다. 위 코드에서는 1차원 배열이고 각 원소의 길이는 단순히 1이니까 쓰지 않는 것입니다.

이제 2차원 배열을 배열포인터로 구현해봅시다.

#include <stdio.h>
int main() { 
        int i,j; 
        int arr[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} }; 
        int(*pArr)[4] = arr; 
        int row = sizeof(arr) / sizeof(arr[0]);
        int col = sizeof(arr[0]) / sizeof(arr[0][0]);

        for (i = 0; i < row; i++) { 
                for (j = 0; j < col; j++) 
                        printf("%d ", pArr[i][j]); 
                printf("\n"); 
        } 
}

 

arr의 각 행 길이와 pArr의 행의 길이를 맞추고 있다는 것을 보세요. 그리고 pArr[0:2]는 각각 사이즈가 16이며 마치 배열처럼 동작이 가능합니다. 

 

이것을 어디다가 활용할 수 있을까요??

 

함수에 전달인자로 배열을 받을때 주로 사용합니다.

 

#include <stdio.h>

void printArr(int(*pArr)[4],int row,int col) {
        int i, j; 
        for (i = 0; i < row; i++) { 
                for (j = 0; j < col; j++) 
                        printf("%d ", pArr[i][j]); 
                printf("\n"); 
        }
} 

int main() { 
        int arr[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12}}; 
        int row = sizeof(arr) / sizeof(arr[0]); 
        int col = sizeof(arr[0]) / sizeof(arr[0][0]);
        printArr(arr,row,col); 
}


매개변수를 받으려면 위와 같이 전달받고, 배열처럼 인덱싱을 편하게 사용할 수 있습니다.

 

더 간편한 방법으로는 int (*pArr)[4]를 int pArr[][4]로 바꿔줘도 실행이 가능합니다. 왜냐면 *pArr은 pArr[]과 거의 같은 의미이기 때문입니다.

void printArr(int pArr[][4],int row,int col) { 
        int i, j; 
        for (i = 0; i < row; i++) { 
                for (j = 0; j < col; j++) 
                        printf("%d ", pArr[i][j]);
                printf("\n"); 
        }
}

이상 끝~~~~~~~~~~~~~~~~~~~~~~~~~~~~

반응형
블로그 이미지

REAKWON

와나진짜

,

C언어 파일 입출력

 

C언어에서 scanf와 printf 함수를 통해서 키보드로 입력을 받고 모니터로 출력해주는 그런 프로그램들을 많이 보았을 겁니다. 이런 키보드나 모니터같은 입출력 장비를 콘솔이라고 합니다. 그래서 콘솔 입출력을 해왔던 것이죠.

여기서는 파일 입출력에 대해서 설명합니다. 사실 콘솔 입출력과는 별로 다를바가 없습니다. 단지 그 대상이 모니터나 키보드가 아닌 파일이기 때문이죠. 본격적으로 파일 입출력을 설명하기 전에 우리는 스트림에 대한 개념을 먼저 알아야합니다. 

> 그전에 왜 C 표준입출력을 사용하나요?

리눅스를 배우셨던 분들은 open, read, write, close를 이용해서 파일을 다뤄보셨을 겁니다. 그때는 open에 필요에 따라 여러 플래그들을 줄 수가 있는데요. 예를 들어 O_RDONLY, O_CREAT 등 말이죠. 이거 구차하게 일일히 헤더 추가한 다음에 파일 디스크립터를 가져와서 write, read하는 것을 C 표준입출력 라이브러리에서는 stdio.h만 포함해서 사용할 수 있습니다.  아주 개꿀이라는 얘기죠. 그리고 변태같은 플래그들을 포함하지 않아도 사용하기에 적합한 플래그들을 미리 조합해놨기 때문에 상큼하게 그걸 사용하면 됩니다. 또한 내부적으로 버퍼를 사용하기 때문에 read, write 함수들을 최적으로 사용하게 됩니다.

 

스트림(Stream)

영어를 그대로 직영하게 되면 흐름이라는 건데요. 비슷하게 생각하시면 됩니다. 프로그램에서 파일이 열리면 C표준입출력은 스트림(stream)이라는 파일과 프로그램 사이의 추상적인 흐름이 일어나는 파이프를 생성합니다. 그래서 파일이 열리게 되면 개념적으로 스트림을 통해서 파일에 기록하거나 읽을 수 있습니다. 만약 파일을 읽기만 하겠다하면 읽기 전용의 스트림을 여는 것이고, 파일을 쓰기만 할 것이라면 쓰기 전용의 스트림을 열어서 거기에 기록을 하면 됩니다. 

그래서 아래와 같이 어떤 프로그램에서 File이라는 이름의 파일을 읽고 쓰기 위해서 스트림을 열면 아래와 같은 상황이 발생하게 됩니다. 그래서 바이트 단위던, 줄 단위던 입력이 흐름이 가능한 상태가 됩니다.

 

기본적으로 보통 프로그램에서는 3개의 스트림이 열려있습니다. 바로 표준 입력 스트림(stdin), 표준 출력 스트림(stdout), 표준 에러 스트림(stderr)입니다. 이 3개는 콘솔에 대해서 열려있는 스트림들입니다. 

 

stdin, stdout, stderr

키보드로 입력받고, 모니터로 출력하는 것도 C표준 입출력에서는 스트림으로 간주하게 됩니다. 그래서 우리가 stdin을 통해서 입력을 받는다면 키보드를 통해서 입력을 받는 것이고, 표준 출력 스트림으로 출력한다면 모니터 화면에다가 출력이 되는 겁니다. 그래서 파일 대신 모니터와 키보드가 스트림 끝에 놓여있는 것을 보세요.

 

 

파일의 종류 ( 텍스트 파일 , 이진 파일)

파일을 사람이 쓰고 읽냐, 컴퓨터가 쓰고 읽냐에 따라서 텍스트 파일(text-file), 이진 파일(binary-file)로 나누게 됩니다. 이와 같은 구분은 문자열로 입출력을 하느냐, 아니면 바이너리로 입출력을 하느냐를 위해서 구분합니다. 맨 처음 파일에 대해서 스트림을 생성할 때 결정이 됩니다.

 

1. 파일 열기 fopen

파일 함수는 표준입출력(stdio.h) 헤더파일에 존재합니다. 파일에 어떤 데이터를 읽고, 쓰고, 추가하려면 일단 파일을 열어야겠지요. 함수를 한번 보시죠.

FILE *fopen(const char *filename, const char *mode);

filename : 파일명을 말합니다. 절대 경로나 상대 경로로 줄 수 있습니다. 상대 경로는 그 프로젝트 위치를 기준으로 합니다.

mode : 파일을 어떤 방식으로 열건지 정합니다. 스트림 방식을 정하는 겁니다. 입력 스트림인지, 출력 스트림인지.

  -동작 모드

 모드  설명 flag 조합
 r(read)  1. 파일을 읽기 전용으로 엽니다. 
 2. 파일이 있어야합니다.
O_RDONLY
 w(write)  1. 파일을 쓰기 전용으로 엽니다.
 2. 주의해야합니다. 파일이 존재한다면 기존의 내용을 지우고 쓰기 때문이죠.

 3. 파일이 없으면 새로 생성합니다.
O_WRONLY | O_CREAT | O_TRUNC
 a(append)  1. 파일이 있으면 파일의 끝에 내용을 추가합니다.
 2. 파일이 없으면 생성해서 내용을 추가합니다.
O_WRONLY | O_CREAT | O_APPEND
 r+  1. 파일을 읽고 쓰기 위해 엽니다.
 2. 파일이 반드시 있어야 합니다.
O_RDWR
 w+  1. 파일을 읽고 쓰려고 엽니다.
 2. r+와 다르게 파일이 있는 경우 내용을 덮어쓰고 없으면 생성해서 데이터를 씁니다. 
O_RDWR | O_CREAT | O_TRUNC
 a+  1. 파일을 읽고 갱신하기 위해 엽니다.
 2. 파일이 없으면 생성해서 데이터를 추가합니다.
O_RDWR | O_CREAT | O_APPEND

 

  - 이진 또는 텍스트 모드(t, b)

텍스트모드가 기본(default)입니다. 이진 모드로 파일을 열려면 b를 추가합니다. 

ex) 이진모드로 읽기 위해 파일을 open -> rb

파일을 여는 데 성공했다면 그 파일에 대한 포인터를 return합니다.

하지만 파일을 여는 데 실패했으면 NULL을 반환하죠.

 

2. 파일 닫기 fclose

무엇이든 열었으면 닫는 것이 원칙이죠. 파일 스트림을 닫으려면 fclose를 사용하시면 됩니다.

int fclose(FILE *stream);

그냥 열었던 파일 포인터를 집어넣으면 됩니다. 성공하면 0을 반환하고 실패하면 EOF(-1)를 반환합니다.

 

3. 텍스트 파일 읽기 함수 

파일은 두 종류의 파일이 있다고 했죠? 사람이 읽을 수 있는 텍스트 형식의 파일과 컴퓨터가 읽고 처리하는 바이너리 파일, 즉 이진 파일이 있습니다. 우선 텍스트 파일을 읽는 함수는 쓰임새에 따라 여러가지가 있습니다.

    3.1 한문자 읽기 : fgetc, getc, getchar

#include <stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);

fgetc와 getc는 같은 기능을 하는 함수입니다. getchar() 함수는 키보드용 한문자 입력을 받는 함수와 같아서 getc(stdin)과 같습니다. 

getc = getc , getc(stdin) = getchar()

stream에서 한 글자를 읽어오는 함수이며, 일반적으로 반환형은 한 글자의 ASCII값인 정수형 값입니다. 파일의 끝에 도달할 시에 EOF를 return합니다. EOF는 End-Of-File로 -1입니다. 이렇게 반환형이 (signed) int인 이유는 이 EOF를 반환받기 위해서입니다. 

    3.2 한 줄 읽기 : fgets

#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);

stream에서 문자 한줄을 읽어올때 사용하는 함수이며 size 이하의 문자 한줄을 s로 읽어옵니다. 이때 개행문자까지 읽어옵니다. 그래서 개행문자('\n') 다음 문자의 끝을 나타내는 문자인 NULL('\0')이 붙습니다. 간단히 사용법을 확인해볼까요? 다음은 stdin으로 콘솔(키보드)로부터 입력을 받는 단순한 예제입니다.

//fgets_test.c
#include <stdio.h>
#include <string.h>

#define BUF_SIZE 32
int main(){

        char buf[BUF_SIZE] = {0,};

        printf("입력:");
        fgets(buf, BUF_SIZE, stdin);

        printf("출력:");
        printf("%s(%ld)", buf, strlen(buf));
}
# ./a.out 
입력:hello world
출력:hello world
(12)#

여기서 보이는 "hello world"의 문자열 길이는 공백을 포함해서 11글자이지만, 개행문자를 포함했기 때문에 12글자가 되고, 문자 길이도 한 줄 밑에 출력이 되었네요. 

    3.3 서식화된 파일 입력 : fscanf

#include <stdio.h>
int fscanf(FILE *stream, const char *format, ...);

키보드 입력에 대해서 입력 포맷팅 함수는 scanf였죠? 파일에 대해서 포맷팅 함수는 fscanf입니다. 

 

4. 텍스트 파일 쓰기 함수  

텍스트 파일에 쓰는 함수는 아래와 같습니다. 위의 텍스트 파일 읽기 함수의 네이밍을 따라갑니다. 함수에 대한 소개만하고 넘어가도록 합시다. 

    4.1 한문자 쓰기 : fputc, putc, putchar

#include <stdio.h>
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);

   putchar(c)는 putc(stdout, c)와 같습니다.

    4.2 한 줄 쓰기 : fputs, puts

#include <stdio.h>
int fputs(const char *s, FILE *stream);
int puts(const char *s);

    4.3 서식화된 파일 출력 : fprintf

#include <stdio.h>
int fprintf(FILE *stream, const char *format, ...);

 

예제 - 텍스트 데이터 저장, 읽어오기

//writer.c

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

#define NUM 3

typedef struct _student{
        char name[16]; //이름
        unsigned int age; //나이 
        unsigned int id; //학번
} student;


int main(){
        int i;
        student s[NUM] = {
                {"park", 18, 1234},
                {"jung", 18, 1235},
                {"kim", 19, 1111}
        };
        FILE *fp;

        //텍스트 파일, 없으면 새로운 파일 생성, 있으면 내용 덮어쓰기(w+)
        fp = fopen("info.txt", "w+");
        if(fp == NULL) {
                printf("fopen error\n");
                return 1;
        }

        for(i = 0;i < NUM; i++){
                fprintf(fp, "%s %d %d\n",
                        s[i].name, s[i].age, s[i].id);
        }
        fclose(fp);
}
//reader.c
#include <stdio.h>

#define NUM 3

typedef struct _student{
        char name[16]; //이름
        unsigned int age; //나이 
        unsigned int id; //학번
} student;

int main(){
        int i;
        FILE *fp;
        student s[NUM]; 

        //테스트 파일 읽기 전용
        fp = fopen("info.txt", "r");
        if(fp == NULL){
                printf("fopen error\n");
                return 1;
        }
        for(i = 0; i < NUM; i++){
                fscanf(fp,"%s %d %d", 
                                s[i].name, &(s[i].age), &(s[i].id));

                printf("[%d]\n", i);
                printf("name : %s, age : %u, id : %u\n",
                                s[i].name, s[i].age, s[i].id);
        }
        fclose(fp);
}
# gcc reader.c -o reader
# ./writer
# cat info.txt
park 18 1234
jung 18 1235
kim 19 1111

writer만 실행해보면 info.txt가 생겨났고 그 내용은 이렇게 적혀있습니다. 사람이 알아볼 수 있죠?

# ./reader 
[0]
name : park, age : 18, id : 1234
[1]
name : jung, age : 18, id : 1235
[2]
name : kim, age : 19, id : 1111

reader라는 프로그램으로도 아주 잘 읽을 수 있습니다.

 

5. 이진 파일 읽기 fread

파일을 읽는 함수는 fread입니다. 프리드라고 읽지마세요 제발. 앞에 f는 모두 file의 f입니다. 앞에 f가 붙은 함수는 거의 다 파일에 대한 함수라는 것을 기억하세요.  

#include <stdio.h>
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);

 

stream으로부터 자료형인 size를 count만큼 읽어서 buffer에 저장합니다. buffer가 void*인 이유는 어떤 자료형이건 받아와야하기 때문입니다.  파일을 읽은 길이(count)만큼 반환합니다. 

만약에 단순 바이너리를 읽는다면, 그러니까 바이트 단위를 읽는다면 size는 1입니다. 그러면 만약 크기 16바이트인 구조체를 3개를 읽는다면 아래와 같이 호출이 됩니다. 

fread(buffer, 16, 3, fp);

6. 이진 파일 쓰기 fwrite

파일에 쓰는 함수입니다. fread와는 반대 기능이죠.

#include <stdio.h> 
size_t fwrite(const void *buffer, size_t size, size_t count, FILE *stream);

buffer에 담긴 내용을 기록하는데 size만큼의 count 만큼 버퍼로부터 stream쪽으로 씁니다. 성공하면 count를 return하고 실패한다면 count가 아닐 수 있습니다.

 

예제 - 이진데이터 구조체 저장, 읽어오기

이진 파일을 사용할 수 있는 가장 큰 장점은 모든 자료를 이진데이터로 쓸 수 있다는 점입니다. 객체(구조체)도 그냉 냅다 쓸 수 있습니다. 모든 것을 이진 데이터로 쓰기 때문이지요. 다음은 구조체를 파일에 쓰고, 그 파일로부터 읽어오는 예제를 보여줍니다.

//info_writer.c
#include <stdio.h>

#define NUM 3

typedef struct _student{
        char name[16]; //이름
        unsigned int age; //나이 
        unsigned int id; //학번
} student;

int main(){
        FILE *fp;
        student s[NUM] = {
                {"kim", 16, 1234},
                {"lee", 16, 1235},
                {"lim", 17, 1111}
        };

        //이진(b)으로 쓰기용, 없으면 만들고 있으면 덮어쓴다(w+)
        fp = fopen("info.bin", "wb+");
        if(fp == NULL){
                printf("fopen error\n");
                return 1;
        }

        if(fwrite(s, sizeof(student), NUM, fp) != NUM){
                printf("fwrite erorr\n");
                fclose(fp);
                return 1;
        }

        printf("Student Information Saved OK \n");
        fclose(fp);
}

 

//info_reader.c
#include <stdio.h>

#define NUM 3

typedef struct _student{
        char name[16]; //이름
        unsigned int age; //나이 
        unsigned int id; //학번
} student;

int main(){
        int i;
        FILE *fp;
        student s[NUM]; 

        fp = fopen("info.bin", "rb"); //읽기 전용
        if(fp == NULL){
                printf("fopen error\n");
                return 1;
        }

        if(fread(s, sizeof(student), NUM, fp) != NUM){
                printf("fread erorr\n");
                fclose(fp);
                return 1;
        }

        for(i = 0; i < NUM; i++){
                printf("[%d]\n", i);
                printf("name : %s, age : %u, id : %u\n",
                                s[i].name, s[i].age, s[i].id);
        }
        fclose(fp);
}

 

# gcc info_writer.c -o writer
# gcc info_reader.c -o reader
# ./writer 
Student Information Saved OK 
# ./reader 
[0]
name : kim, age : 16, id : 1234
[1]
name : lee, age : 16, id : 1235
[2]
name : lim, age : 17, id : 1111

 

7. 버퍼 

버퍼는 C표준입출력에서 입력과 출력을 효율적으로 처리하기 위한 일종의 저장공간입니다. 내부적으로 write, read를 적시에 한번만 호출하기 위한 것이 목적입니다. 그런데 이러한 버퍼의 처리 방식을 잘 모르면 낭패를 볼 수 있는데요. 아래의 코드를 봅시다.

//buffer.c
#include <stdio.h>

int main(){

        char c;

        printf("아무 글자나 하나 입력:");
        scanf("%c", &c);
        printf("입력받은 글자 : %c\n", c);

        printf("다시 입력 : ");
        scanf("%c", &c);
        printf("입력받은 글자 : %c\n", c);
}

 

실행하게 되면 두 번재 scanf에 입력을 주기도 전에 프로그램이 끝나게 됩니다. 분명 scanf를 통해서 한글자 입력을 받는 코드를 작성했으에도 말이죠. 

# ./a.out 
아무 글자나 하나 입력:H
입력받은 글자 : H
다시 입력 : 입력받은 글자 : 
#

 

이 프로그램은 내부적으로 이렇게 동작하게 됩니다. 'H'라는 문자를 입력하면 내부적으로 엔터에 해당하는 개행 문자 '\n'도 입력이 됩니다.

 

결국 버퍼에는 H와 '\n'이 입력이 되게 되며 변수 c에는 'H'가 담기게 되겠죠. 버퍼에 남아있는 건 개행문자 '\n'입니다. 그래서 다음 scanf는 이 개행문자를 입력받아 입력이 끝나게 되는 겁니다. 

 

위는 줄 단위 버퍼링의 사례로 결국에는 남아있는 버퍼를 비워줘야합니다. 버퍼를 비워주는 방법에는 여러 가지 방법이 있는데요.

7.1 버퍼를 비우는 방법들

- 간단한 방법은 단순히 문자 하나 입력받는 거죠. 아래와 같이말이죠. 

printf("아무 글자나 하나 입력:");
scanf("%c", &c);
getchar();
printf("입력받은 글자 : %c\n", c);

printf("다시 입력 : ");
scanf("%c", &c);
getchar();
printf("입력받은 글자 : %c\n", c);

 

- fflush 함수 사용

#include <stdio.h>
int fflush(FILE *stream);

fflush 함수를 사용할 수가 있는데, 이 방법은 표준이 아니므로 권장되지 않습니다. 실제 제 ubuntu시스템에서는 동작하지 않습니다.

 

- scanf에서 공백 사용

scanf("%c", &c) -> scanf(" %c", &c)

 

앞에 공백 문자 하나를 넣어주세요. 

 

- 개행문자가 나올때까지 제거 

while(getchar() != '\n');

어떤 시스템에는 \r\n으로 개행합니다. 그게 윈도우즈인데, 이럴 때는 getchar()만 사용하게 되면 \r만 제거 됩니다. 그래서 아예 \n까지 제거 할 수 있도록 while문을 도는 방식을 사용할 수 있습니다. 약간 고급진 말로 '\r'은 커서를 맨 앞으로 돌리는 CR(Carriage return)이라 하며 '\n'은 커서는 그자리이며 라인만 바꾸는 LF(Line Feed)라고 합니다.  

반응형
블로그 이미지

REAKWON

와나진짜

,

캡슐화


우리는 이제까지 객체나 클래스의 변수나 메소드를 직접 접근하는 프로그래밍을 해왔습니다. 우리가 저질렀던 이런 방법의 프로그래밍은 조금 위험하다는 것을 알아야합니다. 


왜요?

모두가 접근 가능한 변수나 메소드는 제약 조건없이 쉽게 데이터가 변경 가능하기 때문입니다.

우리는 이 데이터가 안전하게 변경되기 위해서 포장, 또는 알맹이처럼 쌓아야하는 의무가 있습니다. 그게바로 객체지향에서 말하는 캡슐화라고 하는 것이죠.




어떻게 캡슐화가 이루어지는 지 상황을 통해서 알아보도록 합시다.


우리는 계좌의 5만원의 잔액을 갖고 있는 클래스 BankAccount가 있다고 칩시다. 간단하게 정의해보겠습니다.

 

class BankAccount{
	int balance=50000;
}


우리는 이 클래스의 객체로부터 인출하는 동작을 하고 싶다는 것입니다.

그럴때 외부에서 직접적으로 balance 변수에 접근하게 된다면 balance가 음수가 될 수도 있다는 겁니다. 인출은 절대 음수가 될 수 없다는 원칙을 깨고 원치않는 프로그램의 오류가 생기게 됩니다. 실제 이렇게 된다면 피해가 막심할 것입니다.


그러니, 우리는 데이터를 제어해야합니다. 

우선 balance라는 변수는 그 클래스 외부에서 절대 접근을 불가하게 만들고 알맞은 로직을 갖고 있는 멤버함수를 두어, 그 balance를 변경하게 만들면 되지 않을까요??


접근 제어자가 우리의 작은 소망을 들어 줄 수 있습니다.


접근지정자


그 전에 우리는 접근 지정자, 또는 접근 제한자라고 하는 녀석들부터 알아야합니다.


자바의 접근 지정자에는 4개가 있습니다. public, protected, default, private라는 녀석들이지요. 이 4개의 접근 지정자들은 멤버 변수나 멤버 메소드를 어떤 범위 내에서 접근하게 허락할 것인지를 정의하게 해줍니다.




다음의 표와 그림이 범위를 보여주고 있습니다. 


 접근 지정자

 오직 클래스

같은 패키지

 자식 클래스

 외부 어디서나

 public

 O

 O

 O

 O

 protected

 O

 O

 O

 

 default

 O

 O

 

 

 private

 O

 

 

 




public > protected > default > private 순으로 범위가 점점 좁아지는 것을 알 수 있습니다.


이 접근 제어자를 통해서 위의 코드의 문제점을 잡아보도록 하지요.


class BankAccount{
	private int balance=50000;
	
	public int withdraw(int m){
		if(balance<m)
			return 0;
		balance-=m;
		return m;
	}
}

변수는 private로 지정해서 BankAccount내에서만 제어가 가능하게 만들어 줍니다.


그리고 메소드를 통해서 m만큼의 돈을 인출하는 withdraw를 정의하는 겁니다. public 지정자로 withdraw메소드를 정의했으니, 누가나 withdraw를 호출할 수 있습니다. 또한, withdraw메소드는 BankAccount 멤버이기 때문에 balance라는 변수에 접근이 가능하며 이 메소드에서 제어를 하고 있습니다. 만일 인출하려는 금액 m이 지금 잔액(balance)보다 크다고 하면 0을 반환하는 것입니다. 그 외에는 그 금액을 인출하는 것이죠. 물론 잔액은 줄어들게 됩니다.


이렇게 원치않는 변경을 막기 위해 접근 지정자를 쓰게 된다면 balance를 보호할 수가 있습니다.


캡슐화가 이해되셨나요?




상속 관계만 데이터 접근 protected

우리는 상속관계에서 부모클래스의 데이터나 메소드를 자식 클래스만 접근을 허락해야 할 때도 있습니다. 이때 사용하는 지정자가 protected입니다.


example이라는 패키지에 두개의 클래스를 A, A를 상속받은 클래스 B를 정의합니다.


package example;
public class A{
	protected int a;
	public A(){}
}


package example;
public class B extends A{
	public B(){
		a=30;
	}
}


B는 A를 상속했으니 a에 대한 변수에 접근이 가능합니다.



같은 패키지의 main함수와 다른 패키지의 main에서 실험해보세요. 같은 패키지에서는 a에 접근이 가능하고, 다른 패키지에는 아래 그림에서 처럼 a가 proposals조차에서도 보이지 않습니다.




왜냐면 protected지정자로 지정된 변수나 메소드는 같은 패키지에서 모두 접근이 가능하기 때문이지요. 


우리는 다른 패키지에 있는 클래스 역시 상속을 할 수 있기 때문에 상속관계에서만 데이터 접근을 허락할 때는 protected 접근 지정자를 사용해야합니다.

반응형
블로그 이미지

REAKWON

와나진짜

,