-
printf 파헤치고 구현하기프로그래밍/42프로젝트 2021. 10. 17. 17:37반응형
stdarg
manuanl을 통해 stdarg 사용법을 알아보자
man stdarg
void va_start(va_list ap, last); type va_arg(va_list ap, type); void va_copy(va_list dest, va_list src); void va_end(va_list ap);
man의 내용을 해석해보자
원문과 비교하면서 읽으면 좋을 것 같다.
stdarg.h 헤더 안에는 va_list라는 타입을 선언하고 있고, 3개의 매크로를 통해(end는 빼고 말하는 듯, 함수가 아니고 매크로로 이루어져 있다.) 갯수와 타입을 모르는 arguments list를 순회할 수 있다.
사용될 함수는 va_list 타입의 오브젝트를 선언해야 하고 이 오브젝트를 va_start(), va_arg(), va_copy(), va_end() 매크로로 사용해야한다. va_start() 매크로가 ap를 init해주고, 가변매개변수가 처리될 때 마다 이 ap가 va_arg()의 매개변수로 사용되어야 한다. va_end()는 더 이상 가변매개변수가 없다고 알리고, ap를 사용할 수 없게 만든다. 같은 함수 안에서는 va_strart()와 va_end()의 짝이 맞아야 한다. last 파라미터는 가변길이 매개변수 리스트가 나오기 전 마지막 파라미터의 이름이다. 즉 호출된 함수가 타입을 알고 있는 마지막 파라미터이다.
last 파라미터의 주소가 va_start() 매크로에서 사용되기 때문에, last 파라미터는 register 변수나, 함수, 배열 타입이면 안 된다.
va_arg() 매크로는 현재 call에서 다음 argument의 type과 value를 가지게 끔 표현을 expend한다. va_arg()는 호출할 때 마다 va_start()로 init된 va_list 타입의 ap가 다음 argument를 가리키도록 변경한다. type parameter는 타입네임이 적혀있는데 여기에 단순히 *을 붙이는 것으로 포인터를 만들어 오브젝트가 정확한 타입을 얻을 수 있도록 한다. (포인터 값을 증가 시킬 때 타입의 크기를 모르면 몇 바이트 만큼 이동해야할지를 모르니까)
다음 argument가 더 없거나, type이실제 다음 argument의 타입과 비교할 수 없으면 랜덤 에러가 발생한다.(default argument promotion에 따라 비교가능해야 함)
va_start()매크로를 사용한 뒤에 va_arg()를 처음 사용하면 last argument 다음 argument를 리턴한다.
va_copy() 매크로는 가변길이 매개변수 리스트의 상태를 복사한다. src는 va_start()에 init된 상태여야 하고, dest는 va_end()의 개입이 없었어야 한다. dest에 반영된 상태는 src에 va_start()를 호출하고 va_arg()를 호출한 것과 동일하다. 카피된 가변길이 매개변수 리스트는 va_arg()에 여러번 사용될 수 있고, 마찬가지로 마지막에 va_end()에 사용될 수 있다.
va_end()에 의해 사용불가처리된 가변길이 매개변수 리스트는 va_start()에 의해 다시 init 될 수 있고, 아니면 va_copy()로 다른 카피본을 만들면 된다.
man에 examples의 예시코드가 뭔가 익숙한 느낌이 든다... format에서 파싱해서 switch문으로 분기를 나눈다? 이 형태를 그대로 차용해서 printf를 만들면 될 것 같다.
void foo(char *fmt, ...) { va_list ap, ap2; int d; char c, *s; va_start(ap, fmt); va_copy(ap2, ap); while (*fmt) switch(*fmt++) { case 's': /* string */ s = va_arg(ap, char *); printf("string %s\n", s); break; case 'd': /* int */ d = va_arg(ap, int); printf("int %d\n", d); break; case 'c': /* char */ /* Note: char is promoted to int. */ c = va_arg(ap, int); printf("char %c\n", c); break; } va_end(ap); ... /* use ap2 to iterate over the arguments again */ ... va_end(ap2); }
코딩도장 가변길이 매개변수을 보면 사용법에 대한 설명이 자세히 나와있다. ap가 argument pointer의 약자라고 한다.
stdarg 주의점 - default argument promotion
주의해야 할 점이 가변길이 매개변수 리스트 중 char tpye을 받을 때 type을 char로 적는 것이 아닌 int를 적어줘야 한다는 것이다.
위에 man의 예시에서도 char를 c = va_arg(ap, char); 가 아닌 c = va_arg(ap, int);처럼 int로 받아오고 있고 주석에도 char is promoted to int라고 char가 int로 승격되었다고 적어져 있다.
15번 줄에 char로 적고 컴파일 하면 다음 같은 워닝을 띄워준다.
char가 promotable 프로모션이 가능한 타입이라, int로 promotion승격 될거기 때문에 undefined behavior라고 한다.
다음과 같이 int로 받아와야 컴파일 워닝을 안 띄운다.
다른 글들을 찾아보니 c표준에서는 va_arg()를 사용할 때 타입의 승격이 일어나기 때문에 큰 타입으로 적어줘야 한다고 한다. 윈도우에서는 표준을 따르지 않아 char type이 승격되지 않아 따로 지정하는 것이 가능하다고 한다.
char, short -> int
float -> double
bool은 기본 type이 아니고 헤더파일에서 _Bool 타입을 사용하는데 sizeof() 했을 때 1byte가 나오므로 아마 char일 것이다 -> 큰 타입인 int로 승격
long과 long long의 경우 운영체제 별로 크기가 다른데
운영체제 / 크기 long long long window 4byte 8byte osx 8byte 8byte 일단 출력하고자 하는 타입과 승격된 argument의 타입이 다르면 컴파일 워닝이 일어난다.
어쨌든 표준에서는 왜 가변길이 매개변수 리스트를 받아올 때 큰 타입을 사용하도록 강제하는 것일까?
일단 승격은 왜 필요한 걸까?
내가 알고있는 일반적인 변수 승격, 정수 승격(promotion) 자체는 하드웨어에서 cpu의 ALU를 type별로 연산이 가능하도록 만들면 char 변수의 덧셈, 뺄셈, 곱셈, 나눗셈, 나머지 연산을 위한 ALU, int 변수를 위한 ALU 등 타입별 하드웨어를 가져야하지만, char, int와 같이 비슷한 형태여서 0bit 몇개만 더 붙여서 큰 타입으로 만들 수 있는 경우(승격) 큰 타입의 ALU만 사용할 수 있기 때문에 하드웨어 설계에서 효율적이라 사용하는 것으로 알고 있다.(하지만 요즘 cpu에서는 의미가 없고 compatibility 때문인 듯)
unsigned char 변수 a=1을 16진수로 표현하면 0x01
unsigned char 변수 b=2를 16진수로 표현하면 0x02
unsigned int 변수 c=3을 16진수로 표현하면 0x00000003
unsigned int 변수 d=4를 16진수로 표현하면 0x00000004
a+b와 c+d를 연산하기 위하여 8bit ALU와 32bit ALU를 따로 2개 구비해두어 연산할 수 도 있지만
그냥 a와 b 앞에 0을 몇개 더 붙여서 int로 promotion 시키면 각각 0x00000001, 0x00000002가 되어 하드웨어적으로 8bit ALU는 만들 필요도 없고, 32bit ALU 하나만 사용할 수 있을 것이다.
아마도 가변길이 매개변수 리스트가 동작하는 기반인 함수 콜에서 스택을 쌓을 때에 매개변수를 승격시키는 이유가 있을거라 생각하고 검색을 해서 스택오버플로우 default argument promotions에 대한 질문글을 발견했다.
c99표준 pdf 83페이지를 가면 자세히 읽어볼 수 있는데 func call이 prototype을 가지지 않은 type을 포함 하면 integer promotion이 일어나고 변수타입이 float이면 double이 되는 승격이 일어나는데 이 전체 과정을 default argument promotions라고 한다고 한다.
스택오버플로 질문글 답변에 왜 이런 처리를 했는지 찾을 수 있었는데
1988년 이전에는 클래식 K&R C에 function prototype 같은 것이 없었었고 default argument promotions를 사용하면 다음과 같은 이점이 있었다고 한다.
(a) 레지스터에 바이트를 넣는 것이 워드를 넣는 것보다 더 많은 비용이 들지 않고(promotion하면 크기가 커진다고 생각할 수 있는데, 오히려 promotion을 안 하면 변수 타입에 대한 정보를 더 줘야 하기 때문에 가변길이매개변수를 넘길 때 워드로 넘겨줘야 하나보다 (추측임 사실 아님))
(b) 결국 줄이지는 못했지만 매개변수 전달에서 잠재적인 오류를 줄이고 싶어 특정 타입만 사용하도록 강제 했다고 한다.
오류를 줄이는 상황의 예시 double을 %f로 출력해도 잘 출력되는 예시를 보여준다.
stdarg의 내부동작
위 글들을 보면 함수를 호출할 때 cdecl(C declaration) 호출 규약을 따르게 되면 스택에 다음 처럼 값들이 쌓이게 된다고 한다. 호출하는 쪽의 지역 변수들 다음에 호출되는 함수의 매개변수가 뒤에서부터 거꾸로 쌓이고, 함수가 호출되기 이전 실행 흐름인 리턴 어드레스가 쌓이고, 함수의 끝을 가리키는 (EBP)Extendeed Base Pointer 레지스터가 들어가고, 호출된 함수의 지역변수들이 들어간다.
^ higher addresses (lower on the stack) | | caller local variables | ... | argument 3 | argument 2 | argument 1 | return address | saved EBP (usually) | callee local variables | v lower addresses (higher on the stack)
typedef char *va_list; #define va_start(ap,parmn) (void)((ap) = (char*)(&(parmn) + 1)) #define va_end(ap) (void)((ap) = 0) #define va_arg(ap, type) (((type*)((ap) = ((ap) + sizeof(type))))[-1])
이렇게 함수를 콜 했을 때 스택에 어떤 정보들이 저장되는지 알고, 매개변수가 어떤식으로 저장되는지 알 수 있는 상황이면, 타입과 주소를 알고있는 매개변수를 기준으로 func call stack에서 주소를 옮겨가며 가변길이 매개변수 리스트에 접근할 수 있다는 것이다.
하지만 cdecl 호출을 사용하는 상황이 아니면 스택이 아니라 레지스터를 쓸 수도 있기 때문에 어떻게 동작할지 장담할 수 없다고 한다. (fastcall을 쓴다던지, 컴파일러에서 inline을 해버린다던지)
어떤 함수호출 규약들이 있는지 추가적으로 읽어보면 좋을 것 같다.
printf
이제 printf에 대한 메뉴얼을 읽어보자, 그냥 man printf를 하면 쉘 명령어가 나오니까 꼭 3을 붙여서 라이브러리 printf를 보자
man 3 printf
printf family는 format에 따라서 아웃풋을 만들어내는데
printf 앞에 다음과 같은 수식어들이 붙을 수 있다. (f, s, sn, as, d, v, vf, vs, vsn, vas, vd)
f 파일 구조체 스트림 *FILE에 출력 s 이미 메모리 할당된 버퍼 string에 출력 sn 이미 메모리 할당된 버퍼 string에 최대 size-1 만큼 출력 as 포인터에 변환된 string을 충분히 담을 수 있을만큼 버퍼를 동적할당하여 출력 d 파일 디스크립터 번호에 맞는 파일에 출력 v 가변길이매개변수 대신 va_list를 인자로 받음 locale(지역)과 관련된 extend 버전을 보려면 printf_l()을 보면 된다. xlocale()도 보면 좋을 듯.
함수는 뒤따라 오는 argument를 어떻게 처리할지 나타내는 format string에 컨트롤 돼서 출력한다. (argument는 가변길이매개변수 도구인 stdarg에 의해 엑세스 되어 아웃풋에 출력되기 위해 변환된다.)
asprintf와 vasprintf는 포인터 *ret을 formatted string을 충분히 담을 수 있는 버퍼로 동적할당 해준다. 이 포인터는 더 이상 필요하지 않을 때 free(3)로 release 해줘야 한다. 충분한 크기로 할당할 수 없을 때에는 -1을 리턴하고, ret은 NULL 포인터가 된다.
snprintf와 vsnprintf는 output string에 최대 size-1 만큼 출력한다. (마지막에 '\0' 문자를 갖는다.) 만약 리턴값이 size보다 크거나 같으면, string이 너무 짧고, 몇 문자들은 무시 됐다는 뜻이다. size가 0이 아닌 이상 output string은 항상 null 문자 ('\0')로 끝난다.
sprintf와 vsprintf는 size를 INT_MAX + 1로 취급한다.user-provided character string에 아웃풋을 쓰는 루틴들(sn, vsn, s, vs)은 그 string과 format string이 겹치면 안 된다.
//#
format string은 ordinary character와 %로 시작하는 conversion specifications로 구성된다. 매개변수는 type promotion이 되고난 후 conversion specifier와 일치해야 한다. (stdarg로 넘긴 상황이 아니라 printf("%lf", 1.0 + 1.0f); 같은 상황 말하는 듯)
% 이후에 다음에서 설명할 내용들이 순서대로 나타난다. positional field, flags, seperator character, mininum field width, precision, modifier, specifier
(중간 부분은 아래에서 따로 목차로 빼서 설명)
(man에서 중간에 건너 뛰고 거의 마지막 부분)
decimal point character는 프로그램의 locale에 따라서 정의된다. (category LC_NUMERIC)
어떤 경우에도 존재하지 않거나 작은 field width가 numeric field를 자를 수 없다. 만약 결과가 field width 보다 크다면 field가 변환된 결과를 포함할 수 있도록 확장된다. (이 내용은 man에서 맨 밑에 있긴 한데 중요해서 먼저 적음)
RETURN VALUES
printf()는 출력된 캐릭터의 갯수를 리턴한다. (아웃풋 string을 끝내기 위한 마지막 \0은 포함 안 됨)
snprintf()와 vsnprintf()는 size가 제한되지 않았으면 출력 됐어야할 캐릭터 수를 리턴한다. (역시 마지막 \0을 포함 안 한다.) (strlcat의 리턴이랑 비슷한 듯) 이 함수들은 에러가 일어나면 음수를 리턴한다.
%[positional field][flags][seperate character][minimum width][precision][length modifier][specifier]
위와 같은 식으로 format string이 구성된다.
언제나 휴일 블로그 이 블로그에 몇 개의 flags와 specifier에 대한 설명이 잘 나와 있다.
positional field
10진수 자릿수 다음에 $가 따라온다. 다음 access할 매개변수를 나타낸다. 이 filed가 없으면 마지막으로 엑세스된 매개변수가 사용된다. 매개변수 번호는 1부터 시작한다. 엑세스된 매개변수와 액세스 되지 않은 매개변수가 산재해 있으면 결과는 알 수 없다.
아마 과제를 하는 많은 사람들이 madatory part만 하고 man을 읽어보고 구현하지 않기 때문에 positional field에 대해 잘 모를 것이다. 구글에 positional specifier in printf로 검색하면 자료들을 찾아볼 수 있다. 이 링크gnu printf ordering에 string을 번역하면서 매개변수 순서가 바뀔 때 positional field를 사용하는 예제가 있다.
중요한 점은 한 번 positional field를 사용했으면 모든 매개변수에 positional field를 사용해야 한다는 것이다.
아래 처럼 사용하면 컴파일 워닝이 안 뜨지만
아래 처럼 하면 positional과 non-positional arguments를 mix할 수 없다고 컴파일 워닝이 뜬다.
flags
flags '#'
값이 alternate form으로 바뀌어야 한다. cdinpsu conversions에는 영향을 안 미친다.
o converions일 때는 output의 첫번째 string이 0이 되도록 number의 precision이 증가하고,
xX conversions의 경우 0인 아닌 결과일 때 0x나 0X가 앞에 붙게 된다.
aAeEfFgG conversions인 경우 결과는 그 뒤에 자릿수가 없더라도 항상 deciaml point를 갖게되고(평상시에는 소수점 자릿수가 있을 때만 decimal point가 붙음),
gG conversions의 경우 뒤에오는 0이 사라지지 않는다.
d specifier에 적용했을 때 영향을 안 미친다고 했으므로
워닝을 띄워주는 것을 볼 수 있고
영향을 미치는 경우엔 positional field보다 앞에 오게 되면 컴파일 워닝을 띄우고
뒤에 오게 되면 워닝을 안 띄우는 걸로 봐선 positional field 다음에 flag field가 와야 하는 선후 관계가 분명히 있다.
flags '0' (zero)
제로패딩. n을 제외한 모든 conversions일 때 왼쪽에 blanks 대신에 0이 채워진다. 만약에 precision이 numeric conversion인 diouxX와 같이 주어지면 0 flag는 무시된다.
flags '-'
converted된 값이 field boundary 안에서 왼쪽 정렬된다. n conversions를 제외하고 왼쪽 말고 오른쪽에 blanks가 채워진다. 0 flag와 - flag가 둘 다 주어지면 0 flag는 무시된다.
flags ' ' (space)
sigend conversion인 경우(aAdieEfFgG) blanks가 양수 앞에 있어야 한다. (아마 이게 default인 듯 하다)
flags '+'
sigend conversion인 경우 부호가 숫자 앞에 온다. spcae flag와 같이 쓰인 경우 덮어쓴다.
flags ''' (apostrophe)
10진수 변환(dui) 이거나 실수 변환(fF)의 정수부분이 그룹화되고 localeconv(3)의 결과물인 seperator로 천 단위로 구분된다.
다른 글들을 보면 12345를 출력하면 12.345와 같이 출력이 돼야하는 것 같은데 clang-1300.0.29.3버전에서는 안 되는 듯 하다... 그냥 테스트 없이 자체적으로 구현해야할 듯
seperator character (, ; : _)
AltiVec나 SSE vector 같은 multiple value를 출력할 때 구분자를 넣어준다. os 버전 마다 behaviour가 다르다. AltiVec Technology Programming Interface Manul을 확인해야 한다.
AVX나 AltiVec는 여러 개의 변수를 한 번에 병렬처리할 수 있게 하는 인터페이스 같은데 cpu 하드웨어에서 지원해줘야만 쓸 수 있는 것 같다.
포프TV에서 cpu에서의 벡터 가속에 대해 얘기한 적이 있는데 영상을 못 찾겠다.. 아시는 분은 댓글 좀아래 처럼 immintrin.h 헤더를 인클루드하면 사용할 수 있다고 한다. 컴파일 할 때 -mavx 옵션을 넣어줘야 한다.
다만 컴파일 워닝을 어떻게 지울 수 있는지는 모르겠다...//# seperator를 안 넣으면 아래 처럼 출력이 되고
seperator를 넣어주면
seperator로 구분되어 출력된다. 위 3개 다 같은 결과가 나온다. 딱히 한 specifier 안 에서 순서는 상관 없는 것 같다.
AltiVec나 SSE vector 같은 부분은 나중에 더 찾아봐야할 것 같다...
mininum field width
10진수 자릿수로 최소 너비를 지정하는 필드이다. 만약 변한된 값이 이 width보다 작다면 flag에 따라 패딩을 채워줄 것이다. (기본은 왼쪽 공백)
width 크기를 어디까지 지원 하는지 궁금했는데 INT_MAX-1 까지는 출력이 된다.
INT_MAX를 사용하면 printf가 -1을 리턴한다. 앞에 argument만 무시되는 것이 아니고 printf 자체가 실패한다.
아래와 같은 경우에
zero flag가 붙는 걸로 봐선
앞에 0을 flag로 인식하고, 뒤에 3을 width로 인식한다.
precision
10진수 자릿수로 표시되는 width 뒤에 오는 . 마침표 형태의 precision이다. width가 생략됐다면 0으로 취급된다.
diouxX conversion일 때는 나타날 자릿수의 최소 갯수이고,
aAeEfF conversion일 때는 decimal point 뒤에 나타나는 자릿수의 갯수이고,
gG conversion일 때는 정확한 자릿수의 최대 갯수이고,
s conversion일 때는 출력될 문자의 최대 갯수이다.
아래는 precision이 3이여서
정확하게 출력할 게 3개라서 d conversion의 특성 때문에 앞에 00이 붙은 것이고 (밑에서 %d 찾자)
아래는 width를 3으로 지정해주어서 기본 flag인 blank가 적용이 된 것이고, precision은 0으로 취급되었다. (원래 숫자만 출력)
아래는 flag가 0이고, width가 3, precision이 생략됐으므로 0으로 취급된다. 0 flag와 precision이 같이 주어져서 0 flag가 무시된다.
flag가 default인 blank, width가 3, precision 0으로 취급된다.
flag 0, width가 3, precision이 2이지만, flag가 무시되어서
blank, width 3, precision 2로 인식 된다.
width3, precision3으로 인식된다.
precision 3으로 인식된다.
width 5, precision 3으로 인식된다.
잘못된 순서의 positional field가 섞이면
컴파일 워닝이 난다.
width와 precision을 positinoal filed와 섞기
width나 precision이 (둘 다 가능) asterisk *나 asterisk 다음에 숫자$로 표시가 되면 (*, *숫자$) 이 경우엔 int argument가 width나 precision으로 사용된다. 음수 width는 왼쪽 정렬 flag로 취급되고, 음수 precision은 없는 것 처럼 취급 된다. positional field (nn$)와 non-positional field가 섞이면 결과는 모른다.
(원래 이 설명이man에서 specefier에 붙어있는데 왜 거기에 있는지는 모르겠다....)아래의 경우 flag 0인 상황에서 width를 asterisk로 받아오는 상황이다.
asterisk로 받아온 width가 5가 되고, specifier d conversion으로 4를 출력한다.
아래의 경우엔 * asterisk를 통해서 width의 값을 2번째 argument 4로 받아오고 specifier인 d conversion의 값으로 5를 출력하도록 의도한 거였는데
positional field (*2$)와 non-positional filed (%d)를 섞어서 썼으므로 컴파일 워닝을 내는 상황이다.
아래 처럼 specifier인 d conversion에 첫번째 인자에서 값을 가져오라고 1$를 붙여주면 컴파일 워닝이 없다.
아래의 경우엔 1개의 argument 만으로 여러 개의 positional field를 커버할 수 있냐를 테스트 한 건데, 문제없이 컴파일 된다.
length modifier
length modifier로 argument의 size를 결정한다.
다음 modifier는 dinouxX conversion에서 유효하다.
Modifier di ouxX n hh signed char unsigend char signed char * h short unsigned short short * l (ell) long unsigned long long * ll (ell ell) long long unsigend long long long long * j intmax_t uintmax_t intmax_t * t ptrdiff_t (ptrdiff_t와 사이즈 같은 unsigned 형) ptrdiff_t * z (size_t와 사이즈 같은 signed 형) size_t (size_t와 사이즈 같은 signed 형을 가리키는 포인터) q (더 이상 사용 안 함) quad_t u_quad_t quad_t * (이 글은 아직 안 읽어봤는데 읽어보면 도움이 될거 같다)c의 헤더파일들은 다음 처럼 찾아볼 수 있다.
cd /Library/Developer/CommandLineTools find SDKs -name inttypes.h
intmax_t는 stdint.h나 inttypes.h 헤더를 인클루드하면 사용할 수 있고 어떤 sigend integer든 표현할 수 있는 변수이다. uintmax_t는 unsigned를 표현한다. 아키텍쳐마다 크기가 다르다. 같은 헤더파일에 정의되어 있는 INTMAX_MAX로 최대값을 알 수 있다.
ptrdiff_t는 stddef.h에 있고, stdint.h나 inttpyes.h에 정의되어있는 PTRDIFF_MAX로 최대값을 알 수 있다. 두 포인터 간의 차이를 저장하기 위한 타입이다.
size_t는 stddef.h, stdio.h, stdlib.h, string.h, time.h, uchar.h, wchar.h 등 많은 헤더에서 정의되는데 sizeof()연산자를 적용하고 난 후 결과를 담기 위한 변수이다.
quad_t는 quad-word를 표현하기 위한 타입이다. %qd의 용도
보면 4개의 타입 intmax_t, ptrdiff_t, size_t, quad_t의 크기가 전부 8byte로 동일해서 왜 굳이 타입을 나눴어야 했는지 의문이 든다.
다른 사람이 printf를 구현한 것을 참고해보면 printf구현참고
typedef나 #define로 타입을 정의하므로 컴퓨터에서 어떤 아키텍쳐를 사용하냐에 따라 타입의 크기가 달라질 수 있다.
printf에서 length modifier를 통해서 length를 정하는 부분이므로 아래에 보면 이 컴퓨터에서 정의된 타입이 long인지 long long인지에 따라 크기를 정하는 것을 볼 수 있다.
intmax_t, ptrdiff_t, size_t, quad_t는 크기가 같더라도 용도에 따라 다르게 쓰면 될 것 같다.
다음 modifier는 aAeEfFgG conversion에서 유효하다.
Modifier aAeEfFgG l (ell) double (원래 동작이랑 같으므로 무시 됨) L long double 다음 modifier는 cs conversion에서 유효하다.
Modifier c s l (ell) wint_t wchar_t * wint_t, wchar_t는 확장아스키 표현을 위한 wide char 변수이다. wchar.h를 인클루드하면 사용할 수 있다. 코딩도장 wchar_t
스택오버플로우 wint_t를 보면 wchar_t는 확장아스키 멀티바이트 캐릭터를 표현하기 위한 변수이고, wint_t는 WEOF(wide end of file)깉은 매크로를 처리하기 위해 더 넓은 크기를 사용한다는 것 같다.
gnu 확장문자를 보면 wchar_t는 멀티바이트 캐릭터 기본 변수이고, wint_t는 parameter promotion 때문에 정의되어야 한다고 한다.
위에서 stdarg 부분에서 설명한 default argument promotion인 상황에서 사용되는 변수가 wint_t라는 얘기인 것 같다.
AltiVec Technology Programming Interface Manul도 5개의 length modifier를 정의한다.
SSE vectors인 경우
v argument를 vector value로 취급, conversion specifier에 의해 길이가 결정된다. (default = 정수면 8-bit units 16개, 소수면 32-bit units 4개) vh, hv argument를 16-bit 유닛 8개 vector로 취급 vl, lv argument를 8-bit 유닛 4개 vector로 취급 SSE2 64-bit units인 경우 다음이 추가 됨
vll, llv argument를 64-bit 유닛 2개 vector로 취급 specifier
specifier는 어떤 conversion이 적용될지 정한다.
specifier - diouxX
di : int(또는 적절한 변형) argument가 sigend 10진수로 변환 된다.
o : unsigned 8진수
u : unsigned 10진수
xX : unsigned 16진수 (x : "abcdef", X : "ABCDEF")
precision을 주었다면 최소한 그 자릿수 만큼은 출력된다. 변환된 값이 자릿수가 더 적다면 0이 왼쪽에 채워진다.
d와 i는 뭐가 다른가? 분명 다르기 때문에 문자를 구분했을텐데 동작이 똑같다. 사실 printf에서 다른게 아니고 scanf에서 다르다.
man 3 scanf를 보자.
d의 경우 signed decimal integer로 취급하고,
i의 경우 0x로 시작하는 16진수면 16진수로, 0으로 시작하면 8진수로, 그 외에는 10진수로 인식한다고 한다.
아래를 보면 a, b 둘 다 scanf 입력에 0x5를 준 상황이다.
d는 문자 x를 인식하지 못해 정상적인 값이 입력이 안 된다.
specifier - DOU
long int argument가 sigend 10진수(D), unsigned 8진수(O), unsigned 10진수(U)로 변환된다. ld, lo, lu 인 것 처럼 취급된다. 이 변환 문자들은 더 이상 사용되지 않으며 결국 없어질 것이다. (DOU 대신 ld, lo, lu를 써라)
specifier - eE
doble argument가 [-]d.ddde+-dd의 스타일로 반올림되어 변환 된다. decimal-point 앞에는 한자리이고, 뒤에는 자릿수가 precision과 같다. precision이 없으면 6으로 취급된다. precision이 0이면 decimal-point가 안 나타난다.
E conversion의 경우 지수부분을 나타내기 위해 e 대신 E를 사용한다.
지수부분은 적어도 2자리수를 차지한다. 만약 값이 0이라면 지수부는 00이 된다.
aAeEfFgG conversion에서 +-무한대가 소문자 변환문자일 때 inf, -inf로 표현되고, 대문자 변환문자일 때 INF, -INF로 표현된다. 비슷하게 NaN, nan도 대소문자에 따라 표현된다.
specifier - fF
double argument가 10진수 [-]ddd.ddd의 스타일로 반올림되어 변환된다. decimal-point 뒤에 자릿수는 precision이랑 같다. precision이 없다면 6으로 취급된다. precision이 0이면 decimal-point가 안 나타난다. decimal-point가 나타난다면 그전에 적어도 한 자릿수는 보인다.
specifier - gG
double argument가 f나 e 스타일로 변환된다. (G의 경우엔 F나 E로) precision은 정확한 자릿수를 나타낸다. precision이 없으면 6자리로 취급된다. precision이 0이면 1자리로 취급된다. 변환 후 지수부분이 -4보다 작거나 precision과 같거나 크다면 e 스타일로 표현된다. 결과물의 소수부분에 뒤따라오는 0들은 지워진다. decimal-point는 뒤에 한자리라도 있게되면 나타난다.
specifier - aA
double argument가 16진수 [-]0xh.hhhp[+-]d의 스타일로 반올림되어 변환된다. hexadecimal-point 뒤에 자릿수는 precision이랑 같다. precision이 없으면 반올림이 안 일어난 부동소수점을 그대로 표현할 수 있도록 충분한 값을 가지고, precision이 0이면 hexadecimal-point가 안 나타난다. 문자 p뒤에는 2의 지수부가 +-sigend과 10진수로 표시되고, A conversion이면 prefix가 0X로 (a는 0x), 16진수를 나타내기 위해 ABCDEF 문자를 쓰고 (a는 abcdef), 지수부분과 가수부분을 구분하기 위해 P문자를 사용한다. (a는 p)
부동소수점이 16진수로 여러 방법으로 표현되는데 주의해야한다. 예를 들어 0x1.92p+1, 0x3.24p+0, 0x6.48p-1, 0xc.9p-2는 모두 같다. 어떤 포멧이 선택될지는 숫자의 내부 표현에 따른다. 하지만 가수부분이 최소화 된다는 걸 보장한다. 0은 항상 가수부분 0(적절한 경우 앞에 -이 붙음)과 지수부분 +0으로 표현된다.
specifier - C
c conversion이 l (ell) modifier랑 같이 있는 걸로 취급된다.
specifier - c
int argument가 unsigend char로 변환된다. (default argument promotion의 영향인 듯) 만약 l (ell) modifier가 사용 됐다면 wint_t 매개변수가 wchar_t 매개변수로 변환되고, shift 시퀀스를 포함한 signle wide cahracter인 시퀀스(multi-byte일 수 있음)가 출력된다.(유니코드 말하는 듯) shift 상태는 다시 원래 상태로 복원된다.
(쉬프트 시퀀스를 보면 참고할 수 있는데 0200이라는 1byte가 나오면 일본어 모드로 들어가게 되고, 0201이면 라틴어 모드로 들어가게 된다고 한다. 같은 0240~0377 값이라도 어느 언어 모드인지에 따라서 의미가 바뀌고 이렇게 의미를 바쑬 수 있는 0200과 0201 같은 byte들을 shift sequence라고 부르는 것 같다.
mblen(), mbtowc(), wctomb(), xlocale() 같은 함수들을 살펴보거나, Internationalization이나 i18n로 검색하면 더 찾아볼 수 있다.)
specifier - S
s conversion이 l (ell) modifier랑 같이 있는 걸로 취급된다.
specifier - s
char * argument가 주어지고 terminating NULL charcter를 포함하지 않는 char array가 출력된다. precision이 주어졌으면 precision을 초과해서 출력되지 않는다. precision이 주어졌으면 문자열이 NULL character로 끝나지 않아도 된다. precision이 안 주어졌거나 precision이 array 크기보다 크다면 array는 NULL character를 포함해야 된다.
l (ell) modifier가 사용됐다면, wchar_t * argument가 사용된 걸로 취급된다. 각각의 wide character(multi-byte일 수 있음)에는 어떤 shfit sequence라도 올 수 있다. shfit sequence가 사용되면 문자열이 끝나고 나서 원래의 sequence state를 복원한다. NULL character가 나올 때 까지(NULL character를 포함하지는 않고) wide character가 출력된다. precision이 주어지면 byte가 shfit sequence를 포함해서 그 숫자 초과해서 출력되지 않는다. Partial character는 절대 출력되지 않는다.(이게 무슨 말인지 모르겠음)//# precision이 주어졌으면 NULL character로 끝날필요는 없다. precision이 안 주어졌거나 precision이 주어진 스트링 byte를 초과하면 NULL character로 끝나야 한다.
specifier - p
void * argument가 16진수로 출력된다. (%#x 나 %#lx가 출력되는 것 처럼)
숫자 0을 출력하려고 하면
void * argument가 아니므로 컴파일 워닝이 뜬다.
specifier - n
여태까지 출력된 문자의 갯수가 int * argument가 가리키는 integer로 저장된다. argument의 변환은 이루어지지 않는다. 이 specifier가 사용됐다면 foramt argument는 write-protected memory에 있어야 한다. (const나 literal 이어야 한다는 듯)
SECURITY CONSIDEATIONS
sprintf()나 vsprintf()는 무지하게 긴 string을 사용한다고 가정하므로, 버퍼오버플로우 공격을 통해 실행되고 있는 프로그램의 기능을 바꾸려는 악의적인 유저들에 의해 쉽게 오용된다. 오버플로우가 일어나지 않게 주의해야하지만 이는 확신하기 어려우므로 snprintf() 사용하자.
%n specifier는 지정된 주소에 임의의 값을 쓸 수 있기 때문에 format argument로 신뢰할 수 없는 값을 짚어 넣으면 안 된다. 이 같은 상황은 printf()에 의해 생성된 format argument를 짚어넣는 방법을 통해, snprintf()로 빌드된 함수라도 발생할 수 있다. 그러므로 format argument가 쓰기 가능한 메모리 영역에 있는데 %n을 포함하고 있으면 신뢰할 수 없다. (mprotect(2)의 PROT_WRTIE을 보자) 따라서 %n은 literal string에서는 사용되지만 스택이나 힙 메모리에서는 사용될 수 없다.
literal string으로 %n이 포함된 format을 만들면 잘 출력된다.
123까지 3개를 출력을 했으니 그 숫자가 %n에 의해 ret에 저장된다.
이렇게 동적할당을 하거나
스택에다가 format을 만들면
아예 abort가 나버린다.
specifier - %
단지 %가 써진다. argument 변환은 안 일어난다.
번외 - 에러도 똑같이 띄우고 싶다
없는 specifier를 넣었을 때 comlie warning 어떻게 띄울 것인가가 궁금했다.
%k 처럼 없는 specifier를 사용하면 컴파일러가 어느 줄에 어느 col에 워닝이 있는지 알려주는데
#error나 #warning 매크로를 사용해도 그냥 매크로를 작성한 위치를 하이라이트할 뿐이지, 소스코드를 분석해서 이 소스 어느 부분에 문제가 있다를 알려주진 않는다.
컴파일러 옵션 중에 __attribute__() 라는 것이 있는데 이를 사용하면 가능한 것 같다.
gnu c extension에서 atrribute를 검색하면 찾아볼 수 있다.
함수마다 적용되는 attribute도 있고, 문법, 변수, 타입마다 적용되는 attribute들이 따로 있다.
예를 들면 변수에 사용되는 unused 같은 애들은 이렇게 쓸 수 있다.
변수를 선언하고 사용하지 않았으므로
아래처럼 -Wall -Werror -Wextra 옵션을 주어 워닝이 조금이라도 나면 컴파일이 안 되게 만들었을 때 컴파일이 안 되야 하는데
아래 처럼 attribute를 사용하면
컴파일이 잘 되는 것을 볼 수 있다.
우리는 함수 attribute 중에 format attribute를 사용하면 된다.
아래 처럼 함수 헤더를 따로 선언하고 함수 헤더에 attribute를 적용한다.
format attribute의 인자는 순서대로 format을 어떤 것을 기준으로 검사할지 (printf를 기준으로 검사), format string이 몇 번째 index에 오는지, 첫 검사할 첫번 째 함수인자의 index 이다.
#include <stdio.h> #include <stdarg.h> void func(const char *format, ...) __attribute__((format(printf, 1, 2))); void func(const char *format, ...) { va_list ap; va_start(ap, format); while (*format) { if (*format == 'c') printf("%c", va_arg(ap, int)); else if (*format == 's') printf("%s", va_arg(ap, char*)); else printf("else\n"); format++; } va_end(ap); } int main(void) { func("k", "1234"); }
내가 만든 함수의 format 파라미터에 대해서 컴파일 워닝을 주는 것을 볼 수 있다.
구현
man에서 string에도 출력하고, 동적할당도 하고, 파일디스크립터에서도 쓰는 다양한 버전의 printf family들을 봤더니 단순히 stdout에만 출력하지말고 모든 부분에 쓸 수 있는 lib로 만든 다음 이를 wrapping해서 과제 요구사항만 만족하게 제출하면 나중에 다른 과제에서 써먹기 편할 것 같다는 생각이 들었다. 또 과제 요구사항에서는 그렇게 많은 부분을 요구하진 않았지만 개인적인 도전
과 앞으로 다시는 printf 따위 거들떠도 보지 않겠다는 일념으로 구현가능한 부분은 모두 구현하고 싶었다.구현 했을 때 나중에 유용하게 쓸 수 있는 함수는 아래 3개라고 생각된다. va_list를 인자로 받는 출력은 흔하지 않을 것 같았고, FILE 구조체를 쓸 일이 생기면 그 때 수정해도 괜찮을 것 같았다.
sprintf(char * str, const char * format, ...)
snprintf(char * str, size_t size, const char * format, ...)
asprintf(char ** ret, const char * format, ...)norm에 걸리지 않으면서 위 3개를 아우를 수 있는 형태를 고민했다.
//#
구현하지 않은 부분
일단 mendatory 부분은 과제에서 시키는대로 먼저 구현하고, bonus를 구현할 때 가능한 모든 사항을 구현하려한다.
외부함수 허용이 안 되므로
seperator character field나 vh, hv, vl, lv와 같이 SSE vector와 관련된 modifier랑 specifier, muti-byte charcter의 처리 빼고는 구현할 수 있을 것 같았다. (다른 헤더에 있는 구조체 사용은 되니까)
고민1
signed unsigned, modifier(size), 진수(base len)를 한 번에 고려할 수 있는가?
libft나 gnl과 같은 이전 과제를 통해서 숫자를 출력할 때 다음처럼 정해진 type size안에서 가장 큰수의 자리수 만큼 10을 곱한 나누는 수를 정해놓고 앞에서 출력하는 방식을 썼었다.
void print_num(int n, int fd) { int denum; char *str; str = "0123456789"; denum = 1000000000; // 음수면 -출력 if (n < 0) { write(fd, "-", 1); // n이 denum보다 크면 맨 앞 한자리 출력 if (n / denum) write(fd, str - n / denum, 1); // 아직 음수이므로 양수로 바꿔줌 n %= denum; n *= -1; denum /= 10; } // 이제 무조건 양수다 n이 denum보다 작을 수 있으므로 // 자리수가 맞을 때까지 denum을 줄임 while (n < denum && 1 < denum) denum /= 10; // 양수 출력 while (denum) { write(fd, str + n / denum, 1); n %= denum; denum /= 10; } }
여기서 만약에 십진수가 아닌 16진수를 출력한다고 해보자 그러면 10으로 나누고, 10으로 나눈 나머지를 저장한 부분을 16으로 바꾸면 전체 로직에는 변화가 없다. 하지만 저장하는 변수가 unsinged인데 음수를 나누는 계산을 하면 unsinged 양수로 취급한 상태에서 계산 하므로 원하는 결과가 안나온다.
음수, 양수를 고려하는 것은 제일 처음에 한 번만 음수인지 체크하면 되므로 간단하다.
근데 출력해야 하는 데이터의 크기가 바뀐다고 해보자 int가 아니라 long을 출력한다고 해보자
signed long은 1비트를 sign, 63비트를 데이터로 쓰므로 2^63-1 = 9223372036854775807이 가장 큰 표현 가능한 숫자고, 10진수자리수에 맞게 * 10을 해주면 denum을 다음과 같이 설정할 수 있다.
long denum = 1000000000000000000;
근데 short의 경우는? 가장 큰 값의 10진수자리수 * 10이 바로 계산이 나오나? 물론 type 별로 if문으로 분기를 나눠서 denum을 초기화 해줄 수 있다. 근데 modifier가 붙으면? size_t는? ptrdiff_t는? 아키텍쳐 마다 크기가 다른 자료형인데? 크기가 달라질 때마다 저장하는 변수를 다르게 하려면, if문이던 함수던 너무 분기가 많아지고 관리가 힘들어진다.
출력할 진법으로 숫자를 계산하고 거기에 맞는 최대자리수를 denum으로 사용하는 이유는 숫자를 앞에서 부터 출력해야 하기 때문이다.
0x1390이라는 숫자가 있다면 이 숫자는 10진수로 1 * 16^3 + 3* 16^2 + 9 * 16 = 1 * 4096 + 3 * 256 + 9 * 16 = 5008 이다.
0x1390을 십진수로 계산하여 4096의 맨 앞 4를 출력하면, 원래 숫자 5008이랑 달라진다. 뒤 자리에서 오는 올림을 고려 안 했기 때문에 다르게 나온다. 원래 진법으로 바꾼 다음에 출력한다면 뒷자리까지 끝까지 계산해야 한다.
denum이 표현 가능한 숫자 10진수자리수 * 10으로 설정된 이유는 출력을 10진수로 하기 때문이다. 출력을 8진수로 한다고 하면 8진수자리수 * 8을 위해 8진수로 변환하는 과정이 필요할 것이다. 이걸 계산하는 방법을 통일할 수 없을까?
자료형의 사이즈를 알고 있으면 1에서부터 오버플로우가 일어나거나, 1에서부터 사이즈 쉬프트한 숫자보다 커지기 전까지, 진수 만큼 계속 곱해주면 최댓값을 특정 진수로 표현했을 때 자리수를 알 수 있다. 문제는 사이즈가 다를 때마다 저장하는 변수의 자료형을 바꿔줘야 한다는 것이다. int의 경우 4byte, long의 경우 8byte 사이즈마다 변수를 다르게 저장해줘야 하는 것이 번거롭다.
제일 큰 자료형에 저장을 하면 되지 않을까? 모든 정수형을 저장할 수 있는 자료형인 intmax_t를 사용했다. 그 중에서도 unsigned인 uintmax_t를 사용하면 절댓값이 음수 범위보다 크기 때문에 음수에서 나누기 연산을 할 때도 유용할거라 생각했다.
uintmax_t cal_denum(const int sign, const int size, const int base_len) { uintmax_t ret; uintmax_t thresh_hold; uintmax_t tmp; thresh_hold = 0x01; if (sign) thresh_hold = thresh_hold << (8 * size - 2); else thresh_hold = thresh_hold << (8 * size - 1); ret = 0x01; tmp = 0x01; while (!(ret < thresh_hold && (tmp > thresh_hold || tmp < ret))) { ret = tmp; tmp = ret * base_len; } return (ret); }
signed 정수 타입의 경우 맨 앞 비트 하나를 sign으로 사용하기 때문에 signed이면 1bit를 빼고 byte_size 만큼 2를 곱하는 쉬프트 연산을 하여 표현 가능한 2의 배수 최대 숫자를 구한다.(thresh_hold)
1부터 시작해서 base_len 만큼 계속 곱해주는데, 분명 곱하기 전에는 thersh_hold보다 작았는데 곱하고 나니, thresh_hold보다 크거나(signed의 경우), unsigned 타입이라서 곱셈 오버플로우가 일어나면 곱하기 전보다 작아질 것이므로 반복을 중단한다.
<, > 비교 연산자를 사용할 때 type이 signed인지 unsigned인지 중요해지는데 signed 타입을 사용하면 unsigned 타입은 비교가 안 되므로 그냥 절댓값을 계산한다고 생각하고 unsigned를 사용하면 될 것 같다.
비트연산 헷갈린다...
맨 앞에 비트 하나를 확인해서 음수인지 확인 하는데 쉬프트 연산을 사용하는데, 결과가 예상하는 거랑 다르게 이상하게 나와서 삽질을 했다.
비트연산자에도 오버플로우가 존재한다.
비트연산을 적용시킬 숫자를 특정 변수에다가 대입을 했는지 상수를 썼는지, 상수를 썼다면 그 숫자가 sigend int, unsinged int 범위 안에 들어가는지에 따라 동작하는게 다르다.
1. 상수 31비트 쉬프트, 출력
2. 상수 31비트 쉬프트, 변수 대입, 출력
3. 8byte 변수 대입, 31비트 쉬프트, 출력
4. 상수 32비트 쉬프트, 출력
5. 상수 32비트 쉬프트, 변수 대입, 출력
6. 8byte 변수 대입, 32비트 쉬프트, 출력
7. 상수 30비트 쉬프트, 변수 대입, 출력
16진수 상수로 표현된 수는 signed int, unsigned int로 4byte 취급된다. 따라서 1번에서는 4byte로 출력 됐던 것이 4번, 5번에서는 4byte안에서 overflow가 일어난 후 결과가 출력된 것이고, 6번에서는 8byte변수에 대입했기 때문에 8byte로 다루어져 overflow가 일어나지 않은 것이다. 컴파일 워닝에서 오버플로우가 일어난다고 경고하고 있다.
2번의 경우가 당황스러운데 비교를 위해서 7번을 보면 30번 쉬프트 때는 0x40000000이 되는데, 한 번 더 쉬프트 해서 31번 했을 때는 0xffffffff80000000이 되는 것이 이상하다. 이런 경우가 있는지 모르고 간과하고 구현하다 계속 삽질을 했는데
2번 결과는 0xffffffff80000000이고 10진수로 -2147483648로 4byte signed int 최소값을 나타낸다.
그냥 -2147483648을 출력해봤더니 같은 16진수를 나타낸다.
7번 에다가 쉬프트 연산이 아닌 2를 곱했을 때 예상하던 결과가 나왔다.
찾아보니 1이 signed로 취급되면 1 << 31은 4byte 범위 안에서 표현이 안되므로 이를 8byte로 처리할지 4byte로 처리할지는 undefined 라고 한다.
밑에 처럼 unsinged라고 숫자 뒤에 U를 명시해주면 원하는데로 동작한다.
다시 고민1로 돌아와서
그래서 결론은 음수이던 양수이던 아키텍쳐에서 최대값 정수를 저장할 수 있는 unsigned 변수 하나만 사용해서 모든 사이즈를 다 취급할 것이다.
이를 위해서 다음과 같은 준비물이 필요하다.
1. size별, sign별 (절대값의 최대값 자리수) * (진수) 를 저장하는 denum (예를 들어 signed, unsigned int 일 때 1000000000)
2. 큰 자료형에다가 변수를 저장할 것이므로 작은 자료형을 다룰 때 overflow가 일어나야 한다. 이를 위한 size크기만큼 0xffff를 저장하고 있는 mask (예를 들어 signed, unsigned int 일 때 0xffffffff)
3. 맨 앞 비트가 1인지 0인지 보고 음수인지 양수인지 사용할 비트마스크
mask를 계산하기 위해서 0에서 반전을 주어 모든 비트를 1로 만들어 0xffffffffffffffff로 만들어준다. 이 때 0을 uintmax_t에 대입한 후 반전을 주지 않으면 결과는 달라진다. (위에서 비트연산 설명함) 그 후 size 만큼만 ff를 남기기 위해 >>로 쉬프트 시켜준다. 매개변수로 넘어온 num는 uintmax_t로 넘어왔으므로 size 만큼의 최대수보다 클 수 있다. mask랑 and 연산을 해주어 mask가 가리키는 부분만 솎아내어 오버플로우가 일어난 것 처럼 계산해준다. num에 오버플로우가 일어난 결과가 uintmax_t에 양수로 저장된다.
비트마스크(31비트 쉬프트하는 거기 때문에 0x01U에서 U 안 붙여주면 결과는 모름)를 이용해서 맨 앞에 비트가 음수인지 체크해준다. 음수인 경우에 -를 출력하고 비트를 반전시키고 + 1을 해주는 걸로 양수로 바꿔준 후(반전비트가 4byte가 아니라 uintmax_t 8byte 단위로 일어났기 때문에) 오버플로우를 위해 mask와 and 연산해서 속아내준다.
그 후로는 num이 음수도 아니고 무조건 양수이므로 양수취급해서 출력해주면 된다.
음수 제일 작은 숫자 -2147483648의 경우도 오버플로우가 일어나 양수 2147483648로 취급된다.
int print_num(uintmax_t num, int sign, const int size, const char *base) { int base_len; uintmax_t denum; int cnt; uintmax_t mask; cnt = sign; base_len = ft_strlen(base); denum = cal_denum(sign, size, base_len); mask = 0x00; mask = ~mask >> (8 * sizeof(uintmax_t) - 8 * size); num = num & mask; if (sign && (num & (0x01U << (8 * size - 1)))) { write(1, "-", 1); num = (~num + 1) & mask; } while (num < denum && 1 < denum) denum /= base_len; while (denum) { write(1, base + num / denum, 1); cnt++; num %= denum; denum /= base_len; } return (cnt); }
고민2
%p == %#x == %#lx 와 같이 conversion specifier와 flag, modifier가 섞이면서 같은 기능을 하는 표현이 많아졌고, 이를 굳이 표현별로 분기를 나눌 필요 없이 한 번에 체크할 수 없는 로직이 없을까 고민했다.
gnl을 풀 때 했던 것 처럼 경우의 수를 나눠서
format 파싱하는 lr파서 스테이트머신
부동소수점
https://velog.io/@sgyoon/2019-09-15-01
부동소수점 실수 탐구 in JavaScript
자바스크립트에서 사용하는 64비트 IEEE 754 형식에 대해 알아보았다.
velog.io
https://0.30000000000000004.com/
Floating Point Math
Floating Point Math Your language isn’t broken, it’s doing floating point math. Computers can only natively store integers, so they need some way of representing decimal numbers. This representation is not perfectly accurate. This is why, more often th
0.30000000000000004.com
반응형