Search
📚

Libft

Created
2020/12/31
tag
42서울
42Seoul
libc
Libft

Subjects

1. 프로젝트 개요 및 팁

이번 프로젝트는 libc에 있는 일부 모듈을 직접 구현하여 정적 라이브러리로 만드는 것이다. 차후에 정적 라이브러리를 이용할 수 있도록 재사용 가능한 함수들은 최대한 이용하여 내부적으로 유지하는 static 함수를 최소화해보자. 정적 라이브러리라는 의미를 최대한 기억하여 구현하는 것을 목표로 한다.

1) 구현 순서 가이드

1.
memsetbzerocalloc
2.
strlcpysubstrstrjoinsplit
3.
memcmpstrnstr
4.
strchrstrtrim
5.
lstnewlstdelonelstclearlstlastlstadd_backlstmap

2) const char * vs char const * vs char * const

int main(void) { // s1, s2 equivalent -> cannot change value, afford to change address // s3 -> afford to change value, cannot change address const char *s1 = "123"; char const *s2 = "456"; char *const s3 = "789"; // upper one error // *s1 = '0'; // ++s1; // upper one error // *s2 = '0'; // ++s2; // lower one error // *s3 = '0'; // ++s3; return (0); }
C
복사

2. libft.a

1) memset

#include <string.h> void* memset(void* dest, int c, size_t n);
C
복사
dest 의 주소부터 size 바이트를 value 값으로 채운다. 이 때, valueunsigned char 로 형변환되고 바이트 단위로 값을 초기화 하게 된다. 따라서 memsetchar형 초기화는 문제가 없으나, int 값으로 초기화는 불가능하다. int 값 중에서도 memset으로 올바르게 형변환 되어 이용할 수 있는 수는 0-1이다.
01 바이트 기준 2진수로 0000000000000000이고, 4 바이트로는 0000000000000000000000000000000000000000000000000000000000000000이다. 1 바이트 단위로 쓰여도 0으로 초기화 된다.
-11 바이트 기준 2진수로 1111111111111111이고, 4 바이트로는 1111111111111111111111111111111111111111111111111111111111111111이다. 1 바이트 단위로 쓰여도 -1으로 초기화 된다.
int로 초기화 하는 것이 불가능하다는 것에 대해 이해해보자.
int main(void) { int memset_with_0[5]; int memset_with_1[5]; memset(memset_with_0, 0, sizeof(memset_with_0)); memset(memset_with_1, 1, sizeof(memset_with_1)); //memset_with_0 출력 printf("memset_with_0 : "); for (int i = 0; i < 5; i++) printf("%d ", *(memset_with_0 + i)); //memset_with_1 출력 printf("\nmemset_with_1 : "); for (int i = 0; i < 5; i++) printf("%d ", *(memset_with_1 + i)); return 0; }
C
복사
위와 같이 두 배열을 01로 초기화 한다면 아래와 같이 출력된다. memset 함수는 바이트 단위로 값을 초기화 하기 때문에 예상과는 다른 결과 값을 확인할 수 있다.
0 0 0 0 0 16843009 16843009 16843009 16843009 16843009
Plain Text
복사
memset1 바이트 단위로 초기화 시킨다. 따라서 4바이트 크기를 갖는 int 데이터에 각 바이트를 1로 초기화 하게 되면 0000001 00000001 00000001 00000001 과 같은 형태를 띄게 된다.
따라서 2진수로 표현된 값을 확인해보면 위처럼 1648300916483009의 값을 가진채로 int 배열이 초기화 된 것을 확인할 수 있다. memset1 바이트 단위의 초기화를 수행한다는 것을 염두에 둬야 하고, 1 바이트 단위의 초기화 때문에 char 형태의 배열에 적합하며 string.h에 들어있는 함수인 것도 이 때문임을 유추할 수 있다.

2) bzero

함수 원형

#include <string.h> void bzero(void* dest, size_t size);
C
복사

함수 설명

memset과 비슷한 역할을 한다. C 언어 비표준이며 deprecated된 함수이므로 사용하지 않는 것이 좋다. bzero, ZeroMemory 대신에 C 언어 표준으로 이용되는 memset을 이용하는 것이 좋다. 함수 이름처럼 str 주소값부터 메모리 공간을 size 크기의 바이트만큼 0 으로 채운다.
deprecated 함수는 아래 링크를 통해 이해할 수 있다.

3) calloc

함수 원형

#include <stdlib.h> void* calloc(size_t count, size_t size);
C
복사

함수 설명

Heap 공간으로부터 count * size만큼 메모리를 할당 후, 그 공간으 모두 0으로 초기화한다.

Return Value

할당된 메모리 주소를 가리키는 포인터를 반환

4) memcpy

함수 원형

#include <string.h> void* memcpy(void* restrict dest, const void* restrict src, size_t size);
C
복사

함수 설명

src가 가리키는 곳 부터 size바이트만큼 dest에 복사한다.

Return Value

dest 를 반환

5) memccpy

함수 원형

#include <string.h> void* memccpy(void* restrict dest, const void* restrict src, int ch, size_t size);
C
복사

함수 설명

src 가 가리키는 곳 부터 size 바이트만큼 dest에 복사한다. 만약 src에서 문자 ch를 만나면 ch까지만 복사를 진행하고 복제를 중단한다.

Return Value

복사된 dest변수에서 복사가 끝난 바로 다음 주소를 반환
문자 ch를 만나지 않았다면, size 바이트를 복사하고 NULL을 반환

6) memmove

함수 원형

#include <string.h> void* memmove(void* dest, const void* src, size_t size);
C
복사

함수 설명

src 가 가리키는 곳 부터 size 바이트 만큼 dest 가 가리키는 곳으로 옮긴다. 버퍼를 이용하므로 destsrc 가 겹쳐도 문제 없다.

Return Value

dest를 반환

7) memchr

함수 원형

#include <string.h> void* memchr(const void* dest, int ch, size_t size);
C
복사

함수 설명

dest 가 가리키는 곳 부터 size 바이트 까지 중 처음으로 ch 와 일치하는 값을 찾는다. chunsigned char로 해석된다.

Return Value

문자 ch 를 만났다면 해당 주소를 반환
문자 ch를 만나지 않았다면 NULL을 반환

8) memcmp

함수 원형

#include <string.h> int memcmp(const void* ptr1, const void* ptr2, size_t size);
C
복사

함수 설명

두 메모리 블록을 비교한다. ptr1이 가리키는 size바이트의 데이터와 ptr2가 가리키는 size바이트의 데이터를 비교한다.

Return Value

두 메모리 블록이 같으면 0을 반환
두 메모리 블록이 같지 않다면 *ptr1 - *ptr2 값을 반환
→ 두 메모리 블록이 다른 곳에서 ptr1의 값이 더 크면 0 보다 큰 값을 반환
→ 두 메모리 블록이 다른 곳에서 ptr2의 값이 더 크면 0 보다 작은 값을 반환

테스트 케이스

const char s1[] = "atoms\0\0\0\0"; // extra null bytes at end const char s2[] = "atoms\0abc";
C
복사

9) strlcpy

함수 원형

#include <string.h> size_t strlcpy(char* dest, const char* src, size_t size);
C
복사

함수 설명

원리는 strncpy와 비슷하지만 strncpy함수보다 오류가 적은 함수이다.
strncpy의 경우 NULL 문자 삽입을 보장하지 않는다.
strlcpysize0이 아닌 경우 size - 1 까지 복사를 진행하고 마지막에 NULL을 삽입해준다.

Return Value

src의 길이를 반환

10) strlen

함수 원형

#include <string.h> size_t strlen(const char* str);
C
복사

함수 설명

문자열 str의 길이를 구한다. 아래와 같은 구문에서는 100을 리턴하지 않고 12를 리턴하게 된다.
char str[100] = "Hello World!";
C
복사
strlen 함수는 문자열의 마지막 NULL 문자에 의해 길이를 결정하기 때문이다.

Return Value

문자열의 길이를 반환

11) strlcat

함수 원형

#include <string.h> size_t strlcat(char* dest, const char* src, size_t size);
C
복사

함수 설명

dest의 마지막 위치에 size - strlen(dest) - 1 만큼 복사하고 끝에 NULL을 삽입한다.

Return Valu e

sizedest의 크기보다 작을 때, strlen(src) + size를 반환
sizedest의 크기보다 클 때, strlen(src) + strlen(dest)를 반환

12) strchr

함수 원형

#include <string.h> char* strchr(const char *s, int c);
C
복사

함수 설명

s가 가리키는 문자열에서 첫 번째 c (char로 변환)를 찾는다. c는 검색할 문자를 의미하고 int로 형 변환 되어서 전달되지만, 함수 내부적으로 다시 char로 처리된다. 마지막 NULL 문자도 문자열의 일부로 간주하기 때문에 이 함수는 문자열의 맨 끝 부분을 가리키는 포인터를 얻기 위해 사용할 수도 있다.

Return Value

s에서 문자열의 첫 부분부터 검색하여 찾고자 하는 문자가 가장 먼저 나타나는 주소를 반환
해당하는 문자가 없다면 NULL을 반환

13) strrchr

함수 원형

#include <string.h> char* strrchr(const char *s, int c);
C
복사

함수 설명

s가 가리키는 문자열에서 마지막 c (char로 변환)를 찾는다. c는 검색할 문자를 의미하고 int로 형 변환 되어서 전달되지만, 함수 내부적으로 다시 char로 처리된다. 마지막 NULL 문자도 문자열의 일부로 간주하기 때문에 이 함수는 문자열의 맨 끝 부분을 가리키는 포인터를 얻기 위해 사용할 수도 있다.

Return Value

s에서 문자열의 끝 부분부터 검색하여 찾고자 하는 문자가 가장 먼저 나타나는 주소를 반환
해당하는 문자가 없다면 NULL을 반환

14) strnstr

함수 원형

#include <string.h> char* strnstr(const char *haystack, const char *needle, size_t len);
C
복사

함수 설명

문자열 haystack에서 NULL로 끝나는 문자열 needle의 첫 번째 부분을 찾는다. 여기서 haystacklen 문자 이하로 검색된다. NULL 문자 뒤에 나타나는 문자는 검색되지 않는다.

Return Value

needle값이 비어 있다면 haystack를 반환
haystack문자열에서 needle 문자열을 찾지 못하면 NULL을 반환
needle 문자열을 찾으면, haystack에서 needle 문자열 시작 부분 위치 주소를 반환

테스트 케이스

#include<string.h> #include<stdio.h> int main() { char *haystack = "This"; char *needle = "is"; char *k = strnstr(haystack, needle, 4); printf("%c", *k); // output : i return (0); }
C
복사
#include<string.h> #include<stdio.h> int main() { char *haystack = "This"; char *needle = "is"; char *k = strnstr(haystack, needle, 3); printf("%c", *k); // output : segmentation fault return (0); }
C
복사
#include <stdio.h> #include <string.h> int main (void) { char str1[10] = "book"; char str2[5] = "oo"; char str3[5] = "o"; char str4[5] = "go"; char str5[5] = "og"; char str5[5] = "ob"; char str6[5] = "ookg"; printf("book - oo : %p\n", strnstr(str1, str2, 4)); printf("book - o : %p\n", strnstr(str1, str3, 4)); printf("book - go : %p\n", strnstr(str1, str4, 4)); printf("book - og : %p\n", strnstr(str1, str5, 4)); printf("book - ob : %p\n", strnstr(str1, str5, 4)); printf("book - ookg : %p\n", strnstr(str1, str6, 4)); printf("book - oo - 0byte : %p\n", strnstr(str1, str2, 0)); return (0); }
C
복사
book - oo : 0x7ffee3c9daef book - o : 0x7ffee3c9daef book - go : 0x0 book - og : 0x0 book - ob : 0x0 book - ookg : 0x0 book - oo - 0byte : 0x0
C
복사

15) strncmp

함수 원형

#include <string.h> int strncmp(const char *s1, const char *s2, size_t n);
C
복사

함수 설명

strncmp 함수는 문자열을 n자 이하로 비교한다. s1, s2는 각각 서로 비교할 문자열들이다. n은 (처음부터) 비교할 최대 문자의 개수이다. 매뉴얼에 쓰여 있는 lexicographically의 뜻은 아래와 같다. 더 자세한 것은 아래 링크를 참고하자.
DESCRIPTION The strcmp and strncmp functions lexicographically compare the null-terminated strings s1 and s2.

Return Value

만일 n개의 문자가 모두 일치한다면, 0을 반환
비교한 n개의 문자 중 최초로 일치하지 않는 문자의 값이 s1이 더 큰 경우 0보다 큰 값을, s2가 더 큰 경우 0보다 작은 값을 반환

테스트 케이스

const char s1[] = "atoms\0\0\0\0"; // extra null bytes at end const char s2[] = "atoms\0abc";
C
복사

16) atoi

함수 원형

#include <stdlib.h> int atoi(const char *str);
C
복사

함수 설명

궁극적인 목표는 숫자로 표현되는 문자열의 초기 부분을 str에서 int로 변환하는 것이다. 숫자로 표현할 수 있는 문자가 나오기 전까지 공백 문자들을 모두 무시한다. 공백 문자를 무시한 후, 주어지는 문자들이 부호 혹은 숫자가 아니라면 변환이 이뤄지지 않는다. 또한 문자열이 공백 문자로만 이루어져 있어도 변환이 이루어 지지 않는다. 변환에 숫자를 이용하고, 더 이상 숫자가 나오지 않는다면 변환을 마치게 된다.

Return Value

부호를 처리한 int형 정수를 반환

테스트 케이스

printf("%d", ft_atoi("22433723768547758107")); printf("%d", ft_atoi("9223372036854775808")); printf("%d", ft_atoi("9223372036854775808"));
C
복사

17) isalpha

함수 원형

#include <ctype.h> int isalpha(int c);
C
복사

함수 설명

c가 알파벳인지 판단한다. 이 때 매개 변수의 자료형이 char가 아닌 int인 이유는 EOF를 처리 해야하기 때문이다. char-128에서 127까지의 범위를 갖고 있으므로, 총 256개의 표현을 할 수 있는 타입이다. 따라서 char를 받게 되면 총 256가지의 경우로 반환을 할 수 있게 되는데, 이에 대해선 아스키 코드 및 확장에 대해서만 표현할 수 있기 때문에 EOF에 대해서는 처리 불가하게 된다. 결국, 256가지 (아스키 코드 및 확장)에 추가로 1가지 (EOF)를 처리하기 위해서는 int 타입이 필요하다.
EOFEnd Of File의 약자로 더 이상 데이터가 없음을 지칭한다. 따라서 파일의 끝을 나타내기 위해 사용되며, 파일의 끝에 도달했을 때 언제나 특별한 값을 반환하도록 만들어져 있다. (보통 -1을 반환한다.)

Return Value

알파벳이 아니라면 0을 반환
알파벳이라면 0이 아닌 정수를 반환 (이는 Implementation Defined Behavior이다. 따라서 대문자인 경우 1을 반환하고, 소문자인 경우 2를 반환하는 식으로 구현하는 것도 가능하다. 이를 통해 c가 소문자인지 대문자인지 한번에 파악하는 것이 가능하기도 하다.)

18) isdigit

함수 원형

#include <ctype.h> int isdigit(int c);
C
복사

함수 설명

매개 변수 c에 들어오는 값이 숫자를 지칭하는 문자인지 판단한다.

Return Value

숫자를 지칭하는 문자가 아니라면 0을 반환
숫자를 지칭하는 문자라면 1을 반환

19) isalnum

함수 원형

#include <ctype.h> int isalnum(int c);
C
복사

함수 설명

c가 알파벳 또는 숫자인지 확인한다.

Return Value

알파벳 또는 숫자이면 0이 아닌 값을 반환
둘 다 해당하지 않으면 0 반환

테스트 케이스

#include <ctype.h> #include <stdio.h> int main(void) { printf("%d\n", isalnum('a')); printf("%d\n", isalnum('!')); printf("%d\n", isalnum('1')); printf("%d\n", isalnum(1)); printf("%d\n", isalnum(49)); return (0); }
C
복사
1 0 1 0 1
C++
복사

20) isascii

함수 원형

#include <ctype.h> int isascii(int c);
C
복사

함수 설명

c0 ~ 127 사이 값인 아스키 문자인지 확인한다.

Return Value

아스키 문자라면 0이 아닌 값을 반환
아스키 문자가 아니라면 0을 반환

테스트 케이스

#include <ctype.h> #include <stdio.h> int main(void) { printf("%d\n", isascii('a')); printf("%d\n", isascii('!')); printf("%d\n", isascii('1')); printf("%d\n", isascii(0)); printf("%d\n", isascii(49)); printf("%d\n", isascii(128)); return (0); }
C
복사
1 1 1 1 1 0
C
복사

21) isprint

함수 원형

#include <ctype.h> int isprint(int c);
C
복사

함수 설명

c가 공백을 포함하여 출력 가능한 문자인지 확인한다. 32 ~ 126 사이의 값을 말한다.

Return Value

출력 가능한 문자이면 0이 아닌 정수를 반환
그렇지 않으면 0을 반환

테스트 케이스

#include <ctype.h> #include <stdio.h> int main(void) { printf("%d\n", isprint('a')); printf("%d\n", isprint('!')); printf("%d\n", isprint('1')); printf("%d\n", isprint(0)); printf("%d\n", isprint(49)); printf("%d\n", isprint(128)); printf("%d\n", isprint(127)); printf("%d\n", isprint(32)); return (0); }
C++
복사
1 1 1 0 1 0 0 1
C
복사

22) toupper

함수 원형

#include <ctype.h> int toupper(int c);
C
복사

함수 설명

c로 들어온 값이 소문자에 해당한다면 이를 대문자로 변환한다. 이 때 매개 변수의 자료형이 char가 아닌 int인 이유는 EOF를 처리 해야하기 때문이다. char-128에서 127까지의 범위를 갖고 있으므로, 총 256개의 표현을 할 수 있는 타입이다. 따라서 char를 받게 되면 총 256가지의 경우로 반환을 할 수 있게 되는데, 이에 대해선 아스키 코드 및 확장에 대해서만 표현할 수 있기 때문에 EOF에 대해서는 처리 불가하게 된다. 결국, 256가지 (아스키 코드 및 확장)에 추가로 1가지 (EOF)를 처리하기 위해서는 int 타입이 필요하다.
EOFEnd Of File의 약자로 더 이상 데이터가 없음을 지칭한다. 따라서 파일의 끝을 나타내기 위해 사용되며, 파일의 끝에 도달했을 때 언제나 특별한 값을 반환하도록 만들어져 있다. (보통 -1을 반환한다.)

Return Value

소문자에 대응하는 대문자
소문자에 대응하는 대문자가 없으면, 매개 변수를 그대로 반환

23) tolower

함수 원형

#include <ctype.h> int tolower(int c);
C
복사

함수 설명

c로 들어온 값이 대문자에 해당한다면 이를 소문자로 변환한다. 이 때 매개 변수의 자료형이 char가 아닌 int인 이유는 EOF를 처리 해야하기 때문이다. char-128에서 127까지의 범위를 갖고 있으므로, 총 256개의 표현을 할 수 있는 타입이다. 따라서 char를 받게 되면 총 256가지의 경우로 반환을 할 수 있게 되는데, 이에 대해선 아스키 코드 및 확장에 대해서만 표현할 수 있기 때문에 EOF에 대해서는 처리 불가하게 된다. 결국, 256가지 (아스키 코드 및 확장)에 추가로 1가지 (EOF)를 처리하기 위해서는 int 타입이 필요하다.
EOFEnd Of File의 약자로 더 이상 데이터가 없음을 지칭한다. 따라서 파일의 끝을 나타내기 위해 사용되며, 파일의 끝에 도달했을 때 언제나 특별한 값을 반환하도록 만들어져 있다. (보통 -1을 반환한다.)

Return Value

대문자에 대응하는 소문자
대문자에 대응하는 소문자가 없으면, 매개 변수를 그대로 반환

3. libft.h

/* ************************************************************************** */ /* */ /* ::: :::::::: */ /* libft.h :+: :+: :+: */ /* +:+ +:+ +:+ */ /* By: jseo <jseo@student.42seoul.kr> +#+ +:+ +#+ */ /* +#+#+#+#+#+ +#+ */ /* Created: 2020/12/22 15:13:53 by jseo #+# #+# */ /* Updated: 2020/12/25 20:20:54 by jseo ### ########.fr */ /* */ /* ************************************************************************** */ #ifndef LIBFT_H # define LIBFT_H # include <stdlib.h> # include <unistd.h> typedef struct s_list { void *content; struct s_list *next; } t_list; int ft_atoi(const char *s); void ft_bzero(void *s, size_t n); void *ft_calloc(size_t cnt, size_t n); int ft_isalnum(int c); int ft_isalpha(int c); int ft_isascii(int c); int ft_isdigit(int c); int ft_isprint(int c); char *ft_itoa(int n); void *ft_memccpy(void *dst, const void *src, int c, size_t n); void *ft_memchr(const void *s, int c, size_t n); int ft_memcmp(const void *s1, const void *s2, size_t n); void *ft_memcpy(void *dst, const void *src, size_t n); void *ft_memmove(void *dst, const void *src, size_t n); void *ft_memset(void *s, int c, size_t n); void ft_putchar_fd(char c, int fd); void ft_putendl_fd(char *s, int fd); void ft_putnbr_fd(int n, int fd); void ft_putstr_fd(char *s, int fd); char **ft_split(char const *s, char c); char *ft_strchr(const char *s, int c); char *ft_strdup(const char *s); char *ft_strjoin(char const *s1, char const *s2); size_t ft_strlcat(char *dst, const char *src, size_t dstsize); size_t ft_strlcpy(char *dst, const char *src, size_t dstsize); size_t ft_strlen(const char *s); char *ft_strmapi(char const *s, char (*f)(unsigned int, char)); int ft_strncmp(const char *s1, const char *s, size_t n); char *ft_strnstr(const char *s1, const char *set, size_t n); char *ft_strrchr(const char *s, int c); char *ft_strtrim(char const *s1, char const *set); char *ft_substr(char const *s, unsigned int start, size_t len); int tolower(int c); int toupper(int c); int ft_lstsize(t_list *lst); void ft_lstadd_back(t_list **lst, t_list *new); void ft_lstadd_front(t_list **lst, t_list *new); void ft_lstclear(t_list **lst, void (*del)(void *)); void ft_lstdelone(t_list *lst, void (*del)(void *)); void ft_lstiter(t_list *lst, void (*f)(void *)); t_list *ft_lstlast(t_list *lst); t_list *ft_lstmap(t_list *lst, void *(*f)(void *), void (*del)(void *)); t_list *ft_lstnew(void *content); #endif
C
복사

4. Makefile

# **************************************************************************** # # # # ::: :::::::: # # Makefile :+: :+: :+: # # +:+ +:+ +:+ # # By: jseo <jseo@student.42seoul.kr> +#+ +:+ +#+ # # +#+#+#+#+#+ +#+ # # Created: 2020/12/22 14:44:25 by jseo #+# #+# # # Updated: 2021/01/01 00:47:04 by jseo ### ########.fr # # # # **************************************************************************** # NAME = libft.a SRCS = ft_atoi.c \ ft_bzero.c \ ft_calloc.c \ ft_isalnum.c \ ft_isalpha.c \ ft_isascii.c \ ft_isdigit.c \ ft_isprint.c \ ft_itoa.c \ ft_memccpy.c \ ft_memchr.c \ ft_memcmp.c \ ft_memcpy.c \ ft_memmove.c \ ft_memset.c \ ft_putchar_fd.c \ ft_putendl_fd.c \ ft_putnbr_fd.c \ ft_putstr_fd.c \ ft_split.c \ ft_strchr.c \ ft_strdup.c \ ft_strjoin.c \ ft_strlcat.c \ ft_strlcpy.c \ ft_strlen.c \ ft_strmapi.c \ ft_strncmp.c \ ft_strnstr.c \ ft_strrchr.c \ ft_strtrim.c \ ft_substr.c \ ft_tolower.c \ ft_toupper.c BNS_SRCS = ft_lstsize.c \ ft_lstadd_back.c \ ft_lstadd_front.c \ ft_lstclear.c \ ft_lstdelone.c \ ft_lstiter.c \ ft_lstlast.c \ ft_lstmap.c \ ft_lstnew.c OBJS = $(SRCS:%.c=%.o) BNS_OBJS = $(BNS_SRCS:%.c=%.o) FLAGS = -Wall -Wextra -Werror $(NAME) : $(OBJS) gcc $(FLAGS) -c $(SRCS) -I./ ar rc $(NAME) $(OBJS) all : $(NAME) bonus : $(NAME) gcc $(FLAGS) -c $(BNS_SRCS) -I./ ar rc $(NAME) $(BNS_OBJS) clean : rm -f $(OBJS) $(BNS_OBJS) fclean : clean rm -f $(NAME) re : fclean all .PHONY : all clean fclean re
Plain Text
복사