혹시나 문제가 된다면 바로 비공개 처리하겠습니다. 지적이나 댓글 환영합니다!
각 항목 아래 더보기를 클릭하시면 더 자세한 설명을 펼쳐서 볼 수 있습니다~
어느덧 42서울 본과정생(카뎃)이 된 지 2주가 지났다. 0서클 첫 과제로 받게된 C언어 라이브러리를 구현하는 과제를 2주간 수행하였다. 라피신 때보다 급박하지 않아서 여유롭게 할 수 있었다.
현재 코드들을 예외처리도 다 끝내고, 테스트가 잘 돌아가게 끔 완성한 상태이다. 일단 내가 공부한 내용을 정리하는게 동료평가 때 설명하기도 좋고 복습에도 효율적이라 블로그에 정리하기로 했다.
먼저, mem 관련 함수 먼저 정리하겠다
사실, 다른 함수들을 구현하는 것은 라피신 때의 내용과 겹치는 것들이 많아 괜찮았지만, mem 관련 함수는 생소해서 공부할 것이 많았던 것 같다.
참고로, 내가 정의한 libft.h 헤더에는 <unistd.h>와 <stdlib.h>가 include 되어있다. 따라서 libft.h를 호출하면, 따로 정의하지 않고도 <unistd.h> 에 정의된 size_t 타입과 <stdlib.h>의 malloc/free를 사용할 수 있다.
(1) memset : 메모리의 내용(값)을 원하는 크기만큼 특정 값으로 설정하는 함수
- memory + setting
- 어떤 배열 등을 특정 값으로 모두 초기화 할 때 유용하다
- 매뉴얼(영문번역) :
이름 : memset –- byte string을 바이트 값으로 채운다
라이브러리 : 표준 C 라이브러리 (libc, -lc)
시놉시스 :
#include <string.h>
void *memset(void *b, int c, size_t len);
설명 : memset() 함수는 (unsigned char 형으로 변환된) 값 c를 string b에 len 바이트만큼 채운다
리턴값 : memset() 함수는 그것의 첫번째 argument(매개변수)를 리턴한다
- b라는 바이트 배열에 c라는 값을 채운다.
- (void *) 형식으로 값을 받는 이유 ?
: (void *) 형식으로 프로토타입을 선언하면, int 형이던 char 형이던, 그 외에 어떤 타입이던 포인터 배열을 매개변수로 받을 수 있다.
: 예를 들어, (char *) 형으로 매개변수 형식을 지정하면, (int *) 타입의 int형 포인터 배열은 값으로 받지 못하게 된다.
- (void *)형의 매개변수를 (unsigned char *)형으로 캐스팅해서 연산을 수행한 이유?
: 메모리 주솟값, 하드웨어 제어를 위한 비트조작, 바이트 데이터 처리 등에서는 unsigned char를 사용하는 것이 기본이다. 메모리 주솟값에는 부호가 없고, 바이트 데이터 처리도 마찬가지다. 이러한 관례를 따르는 게 좋다. 또 unsigned char 은 메모리를 8비트, 즉 1바이트씩 끊어서 접근한다.
만일, 메모리 주솟값을 부호가 있는 값으로 처리하면 많은 문제가 생길 수 있다. 부호, 비부호, 무엇을 쓰든 비트 패턴 자체는 동일하지만, 값을 해석하는 방식이 다르기 때문에 부호 있는 값으로 처리하면 프로그램 중단이 발생할 것이다.
- (void) 포인터로 연산하면 안되는 이유?
: 음수를 저장할 때, 2의 보수 형태로 저장하게 된다. 단순히 부호 여부가 아니라 데이터를 저장하는 방식이 달라진다. 따라서 비부호인 unsigned char 형식으로 캐스팅할 경우가제일 안전하다. 부호 여부는 비트 연산에서 심각하게 다른 차이를 만들기 때문이다.
ex)
10000000 >> 1 = 01000000 (128 / 2 = 64)
10000000 >> 1 = 11000000 (-128 / 2 = -64)
- (unsigned char) 타입이란?
: char 타입은 단일 문자나 단일 바이트의 메모리를 저장하는 데 사용된다.
이때, unsigned char은 8비트의 메모리를 부호 비트 없이 모두 차지한다. 따라서 unsigned char 의 범위는 0 ~ 255가 된다. signed char 의 경우는 첫번째 비트를 부호 비트로 사용하므로 data를 저장할 수 있는 공간은 7비트 밖에 되지 않는다. 이때, signed char 의 범위는 -127 ~ 128 이다. (이런한 이유에서, 256개의 문자를 표현할 수 있는 확장아스키코드의 경우에는 unsigned char 형을 써야한다.)

- 구현 코드 예시 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#include "libft.h"
void *ft_memset(void *b, int c, size_t len)
{
size_t i;
unsigned char *tmp;
tmp = b;
i = 0;
while (i < len)
{
tmp[i] = (unsigned char)c;
i++;
}
return (b);
}
|
cs |
- size_t 형인 len과 비교하며 인덱스 변수인 i 를 증가시키기 때문에 i의 타입도 size_t 형으로 맞춰주었다
- (unsigned char *)타입의 포인터 배열인 tmp 에 c의 값을 할당할 때, c도 unsigned char로 캐스팅하여 할당해 주었다.
- 참고) size_t 란 무슨 타입인가? (주의 ! 엄밀히 unsigned int 타입이 아니다)
size_t는 '이론상 가장 큰 사이즈를 담을 수 있는 unsigned 데이터 타입'으로 정의된다. 즉, 32비트 머신에서는 32비트 사이즈의 unsigned 정수형(int가 아니라 그냥 '정수'를 의미함), 64비트 머신에서는 64비트 사이즈의 unsigned 정수형(unsigned long long)이다. 향후 등장할 지도 모르는 128비트 머신이라던가 더 큰 머신이 존재한다면 그에 따라 더 큰 사이즈가 될 것이다.
따라서 unsigned int로 착각하고 int나 unsigned int로 형변환을 해서 사용하다가 범위가 벗어나는 버그가 발생할 가능성이 있으니 유의해두는 게 좋다. 특히 큰 데이터나 큰 용량을 가진 파일을 처리할 때 주의해야할 것이다.
- 참고) memset 사용시 주의할 점
memset은 메모리 블록을 채운다. 이 때 메모리 블록을 채우는 기준은 1byte(8bit) 이기 때문에, 0이 아닌 다른 값으로 메모리를 초기화하고자할 때 문제가 발생할 수 있다.
예를 들어, 위 배열을 1로 채우기 위해서 memset(array, 1, sizeof(array)) 를 사용하게 되면 unsigned char는 1바이트, int는 4바이트이기 때문에 다음과 같이 채워질 것이다.
arr pointer | 0x00000001 | 0x0000001 | 0x000001 | ... // 사용자가 기대한 메모리 초기화 ----------------------------------- 0x01010101 | 0x01010101 | 0x01010101 | ... // memset이 수행한 메모리 결과
결과적으로, array 배열은 다른 값으로 채워지게 된다. 만약 array 배열이 char 또는 unsigned char형이었다면, 올바른 값이 들어가게 된다.
또한, memset의 2번째 인자는 내부적으로 unsigned char로 해석된다고 언급되어있다. 따라서 2번째 인자를 255보다 큰 값을 집어넣게 되면 제대로된 초기화가 수행될 수 없다. 만약, 257(0x0101)로 값을 초기화하려고 한다면, 255 이상의 값은 무시되고 1(0x0001)로 초기화되게 된다.
또, 바이트(8비트 = unsigned char 크기) 단위로 초기화를 하는 만큼, int형 배열이나 long long형 배열과 같은 경우, 배열에 memset함수로 0, -1은 넣을 수 있는데, 2, 3과 같은 건 넣을 수 없다.
(2) bzero : 바이트 스트링을 0으로 채운다
- memset과 bzero는 공통적으로, 어떤 배열(공간)을 특정 값으로 '초기화'하는 데 이용되는 함수라 할 수 있다.
- memset은 bzero와 다르게, 0이외의 다른 값을 채울 수 있고, "byte"단위로 값을 채운다는 데에서 차이가 있다.
- 반면, bzero는 해당 메모리 공간에 0만 채울 수 있다.
- 또한 memset은, 아무것도 리턴하지 않는 bzero와 다르게, 메모리 block의 주소를 반환한다.
- 매뉴얼(영문번역) :
이름 : bzero -- byte string을 0으로 채운다
라이브러리 : 표준 C 라이브러리 (libc, -lc)
시놉시스 :
#include <string.h>
void bzero(void *s, size_t n);
설명 : bzero() 함수는 string s에 n개의 0인 바이트들을 채운다.
n이 0일 경우, bzero()는 아무것도 수행하지 않는다.
- 구현 코드 예시 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#include "libft.h"
void ft_bzero(void *s, size_t n)
{
size_t i;
unsigned char *p;
p = s;
i = 0;
while (i < n)
{
p[i] = 0;
i++;
}
}
|
cs |
- (void *)형 바이트 스트링을 unsigned char로 형변환하여, n개의 바이트에 0을 채워 초기화하였다.
(3) memcpy : 메모리 복사 함수
- 메모리를 n 바이트 만큼 복사한다
- memory + copy
- 매뉴얼(영문번역) :
이름 : memcpy -- 메모리 데이터를 복사한다
라이브러리 : 표준 C 라이브러리 (libc, -lc)
시놉시스 :
#include <string.h>
void *memcpy(void *restrict dst, const void *restrict src, size_t n);
설명 : memcpy()함수는 메모리 영역 src에서 메모리 영역 dst로 n개의 바이트를 복사한다.
src와 dst의 메모리 영역이 겹칠 경우의 동작은 정의되지 않는다.
dst와 src가 겹칠 경우에 적용하려면, 이 함수 대신에 memmove 함수를 대신 쓰기를 권장한다.
리턴값 : memcpy() 함수는 dst의 원래 값을 리턴한다
- "src와 dst의 메모리 영역이 겹칠 경우의 동작은 정의되지 않는다" 의 의미?
만일 char s[10] = "Hello-World"를 선언했다고 가정하자. 이때 memcpy(s, s+ 5);를 실행할 경우, 복사하고자 하는 메모리 영역이 겹쳐, 해당 영역에 값을 읽음과 동시에 값을 수정하게 된다. 원본의 값이 아닌, 다르게 수정된 값을 dst에 복사하게 될 위험이 있다. 즉, 이런 상황에서 원본의 값을 제대로 복사하기 위한 동작이 따로 정의 되지 않았다는 뜻이다.
- 구현 코드 예시 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include "libft.h"
void *ft_memcpy(void *dst, const void *src, size_t n)
{
size_t i;
unsigned char *pdst;
unsigned char *psrc;
if (src == dst)
return (dst);
pdst = (unsigned char *)dst;
psrc = (unsigned char *)src;
i = 0;
while (i < n)
{
pdst[i] = psrc[i];
i++;
}
return (dst);
}
|
cs |
- dst와 src가 둘다 NULL일 경우, dst(NULL)을 리턴한다.
이유 : 둘중 하나라도 null이면 주소가 0인 메모리를 참조할 수 없어서 segfault가 발생한다. 또한 dst가 null이기 때문에 null을 리턴한다. 이때, null이 src와 dst 중 하나만 들어오면 seg fault가 반환되는 게 올바른 값이며, null이 2개 들어오면 null을 반환한다. 그래서 if문에서 || 이 아닌, &&를 사용하였다.
만일, 이때 위에서 if문으로 처리를 하지 않고 while 문이 돌게 되면 segfault가 난다.
dst == src 일 때 dst가 반환하도록 예외처리를 해줘도 같은 결과가 된다. 또한, 주소만 같다면 굳이 복사하지 않고 dst를 반환해주는게 성능상 더 빠르고 효율적이기도 하다.
- 프로토타입에 선언된 restrict형은 구현에 사용하지 않았다.
- restrict 타입이란? (매뉴얼의 프로토타입에 선언된 매개변수의 자료형)
최적화 키워드 중의 하나.
restrict 를 쓰면, 그 포인터가 가르키는 객체는 다른 포인터가 가르키지 않는다는 것을 나타낸다
restrict를 사용할 시, 내부적으로 같은 메모리 공간을 가리키는지, 메모리가 겹치는지 모두 확인을 할 필요가 없어지기 때문에, 성능이 더 높아진다.
(4) memccpy : 메모리 복사함수2 (memcpy와 차이 있음)
- 메모리 데이터를 복사하다가, 특정 문자가 발견되면 복사를 중단한다. 그리고 해당 위치의 다음 dst 주소를 리턴한다.
- 매뉴얼(영문번역) :
이름 : memccpy -- 메모리 데이터를 특정 문자가 발견될 때 까지 복사한다
라이브러리 : 표준 C 라이브러리 (libc, -lc)
시놉시스 :
#include <string.h>
void *memccpy(void *restrict dst, const void *restrict src, int c, size_t n);
설명 : memccpy()함수는 string src에서 string dst로 바이트들을 복사한다.
만일 (unsigned char 형으로 변환된) 문자 c가 문자열 src에서 발견되면, 복사가 중단된다.
그리고 c가 발견된 위치 다음의 byte의 dst 포인터 주소가 리턴된다.
그렇지 않으면, n 바이트가 복사되며 null 포인터가 반환된다.
src와 dst의 메모리 영역이 겹칠 경우의 동작은 정의되지 않는다.
- 구현 코드 예시 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#include "libft.h"
void *ft_memccpy(void *dst, const void *src, int c, size_t n)
{
size_t i;
unsigned char *pd;
unsigned char *ps;
pd = (unsigned char *)dst;
ps = (unsigned char *)src;
i = 0;
while (i < n)
{
pd[i] = ps[i];
if (ps[i] == (unsigned char)c)
return (dst + i + 1);
i++;
}
return (0);
}
|
cs |
- unsigned char 형으로 변환된 문자 c가 src에서 발견될 경우, 복사가 중단된다. 그리고 해당 위치 다음의 dst의 포인터를 리턴한다.
(5) memmove : 메모리 복사함수3 (memcpy와 중요한 차이 있음)
- memory + move
- 실질적인 동작은 메모리의 이동이 아닌 메모리의 복사
- 매뉴얼(영문번역) :
이름 : memmove -- byte string을 복사한다
라이브러리 : 표준 C 라이브러리 (libc, -lc)
시놉시스 :
#include <string.h>
void *memmove(void *dst, const void *src, size_t len);
설명 : memmove()함수는 string src에서 string dst로 len만큼의 바이트들을 복사한다.
2개의 string의 메모리 영역은 겹칠 수 있다;
그렇지만 복사는 항상 비파괴적인 방식으로 이루어진다
리턴값 : memmove() 함수는 dst의 원래 값을 리턴한다
- memcpy와의 중요한 차이점!!!
메모리 영역이 중첩될 수 있는 객체 간 복사를 허용한다.
예를 들어, char str[] = "Hello-world"이고, memcpy(str, str + 3)을 실행해서 메모리 영역이 겹치는 경우를 가정하자.
이런 경우에는 원본 문자열(src)의 수정과 복사가 동시에 이루어지기 때문에 부정확한 값이 복사될 위험이 있다. 그러나 이때 memmove함수는 이러한 위험이 없다. 임시 버퍼에 src의 문자열을 우선 저장한 뒤, 저장된 값을 dst에 다시 붙여넣어 원본 값의 손상 없이 dst에 붙여넣는다. 그렇기 때문에 memcpy보다 속도가 떨어지나 더 안전하다.
다른 방식으론, 문자열의 주솟값을 비교하여 복사하는 방법이 있다. dst의 주소가 sr보다 뒤에 있는 경우(dst > src 인 경우), src의 뒷부분부터 dst의 뒷부분에 한칸씩 복사하며 앞으로 이동할 수 있다. 그렇지 않고 dst의 주소가 src보다 앞에 있는 경우(dst < src), 앞에서부터 복사를 수행한다.
- 구현 코드 예시 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#include "libft.h"
void *ft_memmove(void *dst, const void *src, size_t len)
{
unsigned char *psrc;
unsigned char *pdst;
psrc = (unsigned char *)src;
pdst = (unsigned char *)dst;
if (src == dst)
return (dst);
else if (src > dst)
{
while (len--)
*(pdst++) = *(psrc++);
}
else
{
psrc += len - 1;
pdst += len - 1;
while (len--)
*(pdst--) = *(psrc--);
}
return (dst);
}
|
cs |
- src와 dst가 같을 경우에는, dst의 문자열이 이미 src와 같은 것이므로, 복사를 수행하지 않고 바로 dst를 리턴한다
- overlapping 을 처리하기 위해, dst의 주소가 sr보다 뒤에 있는 경우(dst > src 인 경우), src의 끝부분부터 복사한다. 그렇지 않고 dst의 주소가 src보다 앞에 있는 경우(dst < src), 앞에서부터 복사를 수행한다.
- overlapping 을 처리하는 방식을 이해하기 쉬운 시각 자료

1. src의 첫번째 byte부터 순차적으로 dest에 한 btye씩 복사하게 되는 경우

2. src의 마지막 byte부터 한 btye씩 순차적으로 dest에 복사하는 경우
이미지 출처 : https://hand-over.tistory.com/47
memmove 사용법 및 구현 - C 메모리 이동
사용법 #include void *memmove(void *dest, const void *src, size_t n); 정의 memmove() 함수는 src 메모리 영역에서 dest 메모리 영역으로 n byte 만큼 복사합니다. src 배열은 src와 dest 의 메모리 영역과 겹..
hand-over.tistory.com
(6) memchr : 메모리 속 문자 찾는 함수
- 메모리 블록에서의 문자를 찾는다.
- s가 가리키는 메모리의 처음 n 바이트 중에서 처음으로 c와 일치하는 값의 주소를 리턴한다.
- 매뉴얼(영문번역) :
이름 : memchr -- byte string 내의 byte의 위치를 찾는다
라이브러리 : 표준 C 라이브러리 (libc, -lc)
시놉시스 :
#include <string.h>
void *memchr(const void *s, int c, size_t n);
설명 : memchr() 함수는 string s 속에서 (unsigned char형으로 변환된) c가 처음 발견되는 위치를 찾는다
리턴값 : memchr() 함수는 c를 찾아낸 해당 바이트가 위치한 곳의 포인터를 반환한다.
만약 n개의 바이트 중에서 해당 바이트를 찾을 수 없다면, null을 반환한다.
- 구현 코드 예시 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include "libft.h"
void *ft_memchr(const void *s, int c, size_t n)
{
size_t i;
unsigned char *p;
p = (unsigned char *)s;
i = 0;
while (i < n)
{
if (p[i] == (unsigned char)c)
return ((void *)(p + i));
i++;
}
return (0);
}
|
cs |
- 바이트 단위의 메모리 접근을 위해 (unsigned char *)형으로 변환한 포인터를, 다시 (void *)형으로 형변환 해준 뒤 리턴하였다.
(7) memcmp : 메모리 비교 함수
- 두 개의 메모리 블록을 비교한다
- 메모리 블록의 데이터를 n 만큼 비교하여, 같다면 0 리턴. 다르다면 0이 아닌 값을 리턴
- 매뉴얼(영문번역) :
이름 : memcmp -- byte string을 비교한다
라이브러리 : 표준 C 라이브러리 (libc, -lc)
시놉시스 :
#include <string.h>
int memcmp(const void *s1, const void *s2, size_t n);
설명 : memcmp() 함수는 byte string s1을 byte string s2와 비교한다.
2개의 string들은 모두 n 바이트 길이라고 가정한다.
리턴값 : memcmp() 함수는 두개의 문자열(string)이 동일할 경우 0을 리턴한다.
그렇지 않으면, 처음으로 일치하지 않는 부분의 바이트들의 차이값을 리턴한다.
(unsigned char의 값으로 취급되므로, 예를 들어, '\200'은 '\0'보다 더 크다)
길이가 0인 문자열들 또한 동일한 문자열로 취급된다.
이 동작은 C에 요구되지 않으며, portable 코드가 오직 리턴값의 부호에 의해서만 결정되야한다.
- unsigned char 값으로 비교된다는 것을 주의!
- 구현 코드 예시 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#include "libft.h"
int ft_memcmp(const void *s1, const void *s2, size_t n)
{
size_t i;
unsigned char *ptr1;
unsigned char *ptr2;
ptr1 = (unsigned char *)s1;
ptr2 = (unsigned char *)s2;
i = 0;
while (i < n)
{
if (ptr1[i] != ptr2[i])
return ((int)(ptr1[i] - ptr2[i]));
i++;
}
return (0);
}
|
- 길이가 0인 문자열일 경우에는 동일한 것으로 취급되므로 0을 리턴한다.
- 두 문자열이 다를 경우, 다르게 나온 첫번째 바이트의 값 차이를 리턴한다.
이때, unsigned char 형의 차이값을 int형으로 캐스팅하여 리턴해주었다.
참고자료 출처:
https://dojang.io/mod/forum/discuss.php?d=1459
C 언어 코딩 도장: void 포인터 연산시 왜 unsigned char포인터를 써야하나요?
"void 포인터 연산은 호환성을 위해 사용하지 않는 것이 좋습니다. 즉, 표준을 지켜야 호환성이 좋은 코드가 됩니다. 만약 메모리를 1바이트씩 접근하려면 unsigned char *를 사용하면 됩니다." https:/
dojang.io
https://www.geeksforgeeks.org/unsigned-char-in-c-with-examples/
unsigned char in C with Examples - GeeksforGeeks
A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.
www.geeksforgeeks.org
https://yaaam.tistory.com/entry/CC-bzero-%EC%99%80-memset%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90
[C/C++] bzero 와 memset의 차이점
1. bezro #include void bzero(void *s, size_t n); 이름에서도 알 수 있듯이 'zero' 값을 채움 0x00의 값을 s 영역에 n 크기만큼 초기화 -> 다른 값은 사용 불가능 deprecated된 함수. 실제로 'man bzero'를 실행..
yaaam.tistory.com
memset 사용시 주의할 점
C 또는 C++ 언어에서, 구조체 또는 배열을 초기화할 때 memset 함수를 사용하는 것을 종종 볼 수 있습니다. #ifdef CPP // C++에서는 cstring 헤더를 사용합니다(string.h 를 사용해도 됩니다) #include #elif #in..
minusi.tistory.com
c언어 memset : 어떠한 수들만 초기화 가능할까?
memset 함수는 시작 주소값부터 sz 바이트만큼, 바이트 단위로 초기화를 해 주는 함수입니다. 보통 2번째 인자에 넣는 값이 0, -1인 경우가 상당히 많은데요. 0x3f나 0x7f 등도 ps에는 꽤 많이 쓰입니
codingdog.tistory.com
https://karupro.tistory.com/17
[C99] 포인터 최적화를 위한 restrict 키워드
이 키워드가 추가됨에 따라 , 에서도 알 수 있듯 대부분의 표준 라이브러리 함수들에 restrict가 붙었습니다. 최적화 키워드 중 하나인데, restrict을 쓰면 그 포인터가 가르키는 객체는 다른 포인
karupro.tistory.com
https://hand-over.tistory.com/47
memmove 사용법 및 구현 - C 메모리 이동
사용법 #include void *memmove(void *dest, const void *src, size_t n); 정의 memmove() 함수는 src 메모리 영역에서 dest 메모리 영역으로 n byte 만큼 복사합니다. src 배열은 src와 dest 의 메모리 영역과 겹..
hand-over.tistory.com
'IT > 42Seoul' 카테고리의 다른 글
[Libft] C 언어 라이브러리 구현_Part2_추가함수 구현1 (0) | 2021.05.23 |
---|---|
42서울 : 라피신 후기 + 본과정 합격 후기 (4기 2차 : 3 /22 ~ 4/16) (0) | 2021.05.20 |
[Libft] C 언어 라이브러리 구현_Part1_malloc을 사용한 함수 (0) | 2021.05.20 |
[Libft] C 언어 라이브러리 구현_Part1_문자 판별 + 변환 관련 함수 (0) | 2021.05.20 |
[Libft] C 언어 라이브러리 구현_Part1_문자열 관련 함수 (0) | 2021.05.20 |