C99
C99 이전의 부동 소수점 연산 오류 처리
- 부동 소수점 연산 중 오류가 날 경우 그 이유를 오류 조건이라 함
- C 라이브러리 함수가 오류 조건을 보고하는 경우가 있음
- 이 오류 조건의 일부는 errno를 통해 확인 가능
- 인자가 수학적으로 정의된 범위를 벗어날 경우 : EDOM
- 오버플로가 발생한 경우 : ERANGE
- 언더플로가 발생한 경우 : ERANGE가 설정될 수도 있음(구현에 따라 다름)
C99의 부동 소수점 연산 오류 처리
- 좀 더 세분화된 부동 소수점 전용 오류 보고 기능 추가
- 이것을 '부동 소수점 예외'라고 부름
- 예외라고 하지만 다르 언어에서 말하는 예외는 아님
- 그냥 다른 형태의 오류 코드
- errno에서 찾을 수 없던 오류 조건도 보고 됨
- 구현에 따라 다음 중 하나를 지원
- 여전히 errno을 사용
- 새로운 부동 소수점 예외를 사용
- 둘 다 지원
부동 소수점 연산 오류 처리방법 확인하기
-
구현마다 달리 정의된 math_errhandling 비트 플래그를 확인
-
여전히 errno을 사용 : MATH_ERRNO 플래그가 설정되어 있음
#define MATH_ERRNO 1
-
부동 소수점 예외를 사용 : MATH_ERREXCEPT 플래그
#define MATH_ERREXCEPT 2
-
둘 다 지원 : 둘 다 설정되어 있음
-
-
다음이 참이면 IEEE 부동 소수점 연산(IEC 60559)을 지원하는 구현
math_errhandling & MATH_ERREXCEPT
부동 소수점 예외
- <fenv.h> 헤더에 정의되어 있음
- 실제 정의된 정수 값은 각 구현마다 다름
- 단, 비트 플래그로 쓸 수 있도록 2의 승수여야 함
#define FE_DIVBYZERO // 2의 승수 어떤 값
#define FE_INEXACT // 2의 승수 어떤 값
#define FE_INVALID // 2의 승수 어떤 값
#define FE_OVERFLOW // 2의 승수 어떤 값
#define FE_UNDERFLOW // 2의 승수 어떤 값
#define FE_ALL_EXCEPT FE_DIVBYZERO | FE_INEXACT | \
FE_INVALID | FE_OVERFLOW | \
FE_UNDERFLOW
부동 소수점 예외가 났는지 확인하는 법
int fetestexcept(int excepts);
- excepts로 지정된 비트 플래그들이 설정되었는지 확인
int feclearexcept(int excepts);
- excepts로 지정한 비트 플래그들의 상태를 지움
float div_by_zero = 1.f / 0.f;
if(fetestexcept(FE_DIVBYZERO)){
puts("FE_DIVBYZERO");
}
feclearexcept(FE_ALL_EXCEPT);
double inexact = 1.0 / 10.0;
if(fetestexcept(FE_INEXACT)){
puts("FE_INEXACT");
}
feclearexcept(FE_ALL_EXCEPT);
부동 소수점 예외를 설정해줄 수도 있다
int feraiseexcept(int excepts);
- 부동 소수점 매개변수를 받는 함수 구현 중 오류를 보고할 때 유용
- 그러나 오류 코드를 전역 변수에 설정하는 방식
- 가독성이 그리 좋지 않음
- 전역적으로 오류 코드가 설정된다는 사실을 호출자가 모르기 쉬움
- 이 외에 함수에서 오류 코드를 직접 반환하는 방법도 고려할 것
부동 소수점 연산 환경도 설정 가능
- 역시 <fenv.h>에 들어있는 함수들을 이용
- fegetround();
- fesetround();
- fegetenv();
- fesetenv();
...
허나 지원여부는 불확실
- 다음 #pragma를 사용해야만 작동
#include <fenv.h>
#pragma STDC FENV_ACCESS ON
float div_by_zero = 1.f / 0.f;
if(fetestexcept(FE_DIVBYZERO)){
puts("FE_DIVBYZERO");
}
feclearexcept(FE_ALL_EXCEPT);
- 위 #pragma를 넣으면 대부분 컴파일러에서 컴파일 오류
- 각 컴파일러마다 다른 방식으로 부동 소수점 환경을 지원
Type-Generic 수학
- 제네릭이란 보통 둘 중 하나를 의미
- 모든 자료형을 표현할 수 있는 경우 (ex. void*)
- 각 자료형에 맞게 알아서 동작 (ex. C#의 LIST<>)
- 여기서는 두 번째 의미
- 수학 함수(math.h) 및 복소수(complex.h)용 제네릭 매크로 함수
- 프로그래머는 매크로 함수를 호출
- 그러면 매개변수의 자료형에 맞는 함수가 최종적으로 호출
- 이 매크로 함수들은 <tgmath.h>에 들어 있음
매크로 함수가 최종적으로 호출하는 함수
- 매크로 함수 이름이 XXX이라면 최종적으로 호출되는 함수 형태는?
- 실수
- float : XXXf()
- double : XXX()
- long double : XXXl()
- 복소수
- float : cXXXf()
- double : cXXX()
- long double : cXXXl()
복소수와 실수를 모두 지원하는 매크로 함수
- fabs()
- pow()
- exp(), log()
- sin(), cos(), tan()
- asin(), acos(), atan()
- etc.
- 매개변수에 복소수가 하나라도 있으면 복소수용 함수가 호출됨
실수만 지원하는 매크로 함수
- ceil(), floor(), round()
- fmod()
- fmin(), fmax()
- exp2(), log2()
- etc.
- 복소수형 매개변수를 넣으면 정의되지 않은 결과
복소수만 지원하는 매크로 함수
- carg()
- conj()
- creal()
- cimag()
- cproj()
- 이 경우 매크로 함수 이름은 cXXX 형태
float, long double, double, int
- 다음 순서로 최종 호출할 함수가 결정됨
- 매개변수 중 하나가 long double이면 long double용 함수
- 매개변수 중 하나가 double 또는 int이면 double용 함수
- 그 밖의 경우는 float용 함수
가변 길이 배열(variable length arrays)
- 실행 중에 길이(요소수)가 결정되는 배열
- 흔히 줄여서 VLA라고 부름
int n;
scanf("%d", &n);
int nums[n];
가변 길이 배열에 대한 비판
- C99에 나온 이후로 많은 비판을 받음
- 특히 메모리 관리 측면이 문제
- C에서 자동적으로 해주긴 함
- 그러나 그로 인한 성능 저하가 문제
- 코드에서 곧바로 메모리가 안 보이는 C 답지 못한 코드를 양산하게 됨
- 그 결과 C11에서는 선택사항으로 강등됨
- 리눅스 커널에서도 2018년에 드디어 VLA를 다 제거함
- 고로 여기서는 이런 게 있다는 정도만 알고 넘어가자
가변 길이 배열의 메모리 위치
- 보통 스택에 저장
- 그러나 표준은 메모리 위치가 어디여야 하는지 강제하지 않음
- 스택일 수도 힙일 수도 있음
- 컴파일 시에 크기를 알 수 없는 배열을 스택에 할당할 수 있는 방법!
- 힙 메모리보다 속도가 빠름 -> 장점
- 하지만 실행 중에 내부적으로 뭔가 더 복잡한 일이 일어남 -> 단점1
- 실행 중에 배열을 메모리 어딘가에 저장한 뒤 그 포인터를 기억
- 나중에 배열을 사용할 때무다 그 포인터를 통해 접근
- 또한 스택 오버플로가 눈에 잘 안 띄게 만듦 -> 단점2
sizeof()로 배열 크기도 알 수 있다!
- 가변 길이 배열 크기도 sizeof()를 이용해 실행 중에 확인 가능
int size;
scanf("%d", &size);
int nums[size];
printf("nums size: %d\n", sizseof(nums));
- 한편으로는 훌륭
- 그러나 정말 이게 C에 걸맞은 방법인가?
함수 매개변수로도 사용 가능
int sum(int n, int nums[n])
{
int r = 0;
for(int i = 0; i < n; ++i){
r += nums[i];
}
return r;
}
- 배열에 n개의 요소가 있음을 가정하는 함수임을 나타내기 적합
- 그러나 sizeof(nums)는 여전히 4
베스트 프랙티스 : VLA 쓰지말 것
- 그냥 쓰지 않는 게 좋음
- 그냥 여태까지 사용하던 방법을 사용할 것
- 충분히 크게 정적 배열을 잡아서 쓸 것
- 그보다 큰 데이터가 인자로 들어오면 반복문으로 나눠 읽는 법이 있음
- 그것도 안 되면 동적 메모리 할당을 사용할 것
(복습) 함수에 배열 매개변수 전달하기
int sum(int nums[], size_t count)
{
//...
}
- 여기서 nums는 단순히 int*
- 하지만 포인터를 전달할 때에 비해 몇 가지 제약이 있음
- 배열 매개변수를 int* const처럼 전달할 방법이 없음
- const int nums[]는 const int*
- restrict도 불가능
- 배열 매개변수를 int* const처럼 전달할 방법이 없음
- 함수에 전달될 배열의 요소수를 컴파일 도중 알 방법이 전혀 없음
- 만약 알 수만 있다면 최적화 가능
- 예 : loop unrolling
- C99는 이런 걸 지정하는 것을 허용
배열 색인 안의 static 키워드
매개변수_이름[static 한정자 표현식]
- 배열을 매개변수로 전달할 때만 사용 가능
- (선택) static 키워드 : 배열에 최소 몇 개의 요소가 있는지 알려줌
- (선택) 한정자(qualifier) : 배열 자체(요소가 아님)에 붙는 한정자
- const
- restrict
- etc.
static 키워드
int sum(int nums[static 8], size_t count)
{
assert(count >= 8);
// 합을 구해서 변환하는 코드
}
- nums 배열에 최소 8개의 요소가 들어있다고 알려줌
- 컴파일러는 이 힌트에 맞춰 최적화를 할 수 있음
- 실행 중에 그 보다 작은 배열이 들어오면 정의되지 않은 결과
- assert()를 잘 써서 이런 경우를 빨리 찾는 게 좋은 습관
const 한정자
void copy_nums(int dest[const], const int src[], size_t count)
{
for(size_t i = 0; i < count; ++i){
dest[i] = *src++;
}
dest++; // 컴파일 오류
src[0] = 0; // 컴파일 오류
}
- dest : int* const
- src : const int*
restrict 한정자
void copy_nums(int dest[const restrict],
const int src[restrict], size_t count);
- dest : int* const restrict
- src : const int* restrict
복합 리터럴 (compound literal)
- 어떤 데이터형의 자료를 이름 없이 만들어 한 번 쓰고 버리는 방법
- 주로 struct나 배열에 사용
- 코드는 꼭 초기화 목록(initializer) + 캐스팅처럼 보임
복합 리터럴 사용법
(자료형) {초기화_목록}
(int[]){ 2, 4 };
- 자료형
- 초기화 목록으로 만들려고 하는 데이터의 자료형
- 초기화 목록
- 자료형을 초기화하는데 적합한 데이터들
복합 리터럴 예 : 배열
int nums[2] = {2, 4}; // 일반적인 배열 선언
int* p = (int[]){2, 4}; // int[2] 정적 배열. 따로 변수명 없음
- 배열 변수명이 없다 보니 포인터에 대입해야 할 일이 있음
- 예: 선언 후 데이터를 유지하고 싶은 경우
- 그럴 일이 없으면 포인터에 대입하지 않아도 됨
- 예: 매개변수로 사용하는 경우
복합 리터럴 예 : 구조체int sum(int nums[], int n); sum((int[3]) {100, 200, 300}, 3);
- 예: 매개변수로 사용하는 경우
struct foo{
int a;
char b[2];
};
struct foo structure;
// 다른 코드들
structure = ((struct foo) {x+y, 'a', 0});
- 위 코드는 이것과 같음
struct foo structure;
// 다른 코드들
struct foo temp = {x + y, 'a', 0};
structure = temp;
베스트 프랙티스 : 그냥 쓰지 말것
- 코딩 조금 더 하더라도 명확한 코드가 좋음
가변 인자 매크로(variadic macro)
- 함수에는 가변 인자 매개변수를 쓸 수 있엇음
int printf(const char* format, ...);
int scanf(const char* foramt, ...);
- 하지만 매크로 함수에서는 그게 불가능 했음
- C99 부터는 가능
#define 식별자(매개변수들, ...) 대체_목록
#define 식별자(...) 대체_목록
- 일반 함수와는 달라 매개변수 목록에 가변 인자만 있어도 됨
- ...로 전달받은 가변 인자는 VA_ARGS 매크로로 사용 가능
- 단, 다른 함수에 전달하는 용도로 밖에 사용 못함
- #을 앞에 붙여서 문자열을 만들 수도 있음
다른 함수에 가변인자를 전달하는 용도
- 일반 함수와 달리 가변 인자 속에 있는 각 인자에 접근할 방법이 없음
- 일반 함수의 경우 va_list를 사용했음
- 매크로 함수 내에서는 불가능
- 따라서 매크로의 가변 인자는 다른 함수로 다시 전달하는 용도 정도
#define LOG_ERROR(...) fprintf(stderr, __VA_ARGS__);
LOG_ERROR("failed to load: %s\n", filename);
#명령어랑 같이 쓰는 경우
- #를 매크로 함수의 인자 앞에 붙이면 ""로 감싸는 효과
#define PRINT_LIST(...) puts(#__VA_ARGS__)
PRINT_LIST(); // puts("")로 바뀜
PRINT_LIST(17, "pope", double); // puts("17, \"pope\", double");로 바뀜
유니코드 지원
- 유니코드를 소스 파일에서 사용 가능
- 변수 이름
- 문자 또는 문자열
- 소스 파일을 특정 유니코드 인코딩으로 저장하는 걸 말하는 게 아님
- 예 : UTF-8로 저장한 소스 파일
- 소스 코드 안에 직접 유니코드 코드 포인트를 작성하는 방식
- 이걸 유니버셜 문자이름(UCN)이라 함
UCN 사용법
유니코드 코드 포인트 u+nnnn
/unnnn
유니코드 코드 포인트 U+nnnnnnnn
/Unnnnnnnn
int mathscore_pope = 10;
printf("Pope: %d\n", mathscore_pope);
int mathscore_\ud3ec\ud504 = 10; // \ud3ec: 포, \ud504: 프
const char* pope1 = "포프"; // 파일 자체를 UTF-8로 저장하면 동작함
const char* pope2 = "\ud3ec\ud504"; // 파일 자체를 UTF-8로 저장 안 해도 동작함
printf("%s: %d\n", pope2, mathscore_\ud3ec\ud504);
UCN 지원의 의의
- 아스키 인코딩으로 저장한 경우
- 다시 열면 그 문자가 유지되지 않음
- 특정 언어용 인코딩으로 저장한 경우
- 컴파일러가 제대로 읽지 못해서 컴파일 오류를 내는 경우가 대부분
- UTF-8 인코딩으로 저장한 경우
- 파일을 다시 열어도 문자가 유지됨
- 단, 폰트가 없을 경우 (네모)(네모)로 나올 수 있음
- 예전 컴팡리러에서는 컴파일 오류가 날 수도 있음(아스키만 지원)
- 요즘 컴파일러는 대부분 UTF-8을 지원
멀티바이트 문자
- 컴퓨터에게 있어 문자는 그냥 메모리에 있는 바이트 배열
- 모든 문자는 결국 1개 이상의 바이트(char*)에 저장됨
- 아스키는 1문자 = 1바이트
- 다른 인코딩의 1문자는 1바이트 또는 그 이상
- 어떤 나라의 문자를 바이트에 젖아한느 방법(인코딩)은 다양
- ex) 한글은 조합형, EUC-KR, CP949(통합 완성형), UTF-8 등
C에서의 멀티바이트 문자
- 1개 이상의 바이트로 표현된 문자
- 인코딩에 상관 없음
- (중요) 각 문자마다 바이트 크기가 다를 수 있음
- C에서 멀티바이트 문자는 char*로 표현
- (중요) C에서는 멀티바이트 문자가 기본
- 사용자의 입력과 출력은 모두 멀티바이트 문자를 사용
- C의 라이브러리도 기본적으로 멀티바이트 문자를 사용
멀티바이트 문자가 제대로 작동하는 조건
- 아스키가 아닌 문자열을 사용자로부터 입력받았다고 해보자
- 읽어온 멀티바이트 문자열은 char*에 저장됨
- 인코딩은 사용자 컴퓨터 환경에 설정된 것을 따름
- 이 문자열은 다시 사용자에게 출력해주면 제대로 출력됨
- 사용자 컴퓨터 환경이 이미 인코딩이기 때문
- 아스키 문자열을 입력받거나 출력해도 제대로 작동한다
- 사용자 인코딩 설정과 상관 없음
- 대부분의 멀티바이트 문자열은 아스키 값을 그대로 지원하기 때문
문제가 보이시나요?
- 데이터의 교환이 동일한 인코딩 환경을 가진 사용자에 한정
- 사실 이게 컴퓨터가 처음 나왔을 때 프로그램을 사용하던 모습
- 점차 컴퓨터를 널리 사용함에 따라 그 형태도 바뀜
- 그에 따라 C의 다국적 지원도 같이 발전해 왔음
- 그러나 다른 프로그래밍 언어에 비해 매우 미비한 수준
- 다국어를 제대로 지원하는 C프로그램은 ICU라는 라이브러리를 사용
- 하지만 용량 및 성능 상의 우려 때문에 사용하지 못하는 프로젝트가 존재
다국어를 지원하기 위한 긴 여정
- 어쨌든 C 언어는 컴퓨터 사용환경이 변한 것에 많이 뒤쳐져 있음
- 현재 상황은 데이터 교환에 사용할 수 있는 공통된 표준이 필요
- 사용자의 컴퓨터가 어떤 인코딩을 사용하던 무관
- 현재 대세인 표준은 유니코드
- 그 중에서도 웹의 90%를 차지한느 UTF-8 인코딩
- 유니코드까지 가기까지의 힘겨운 C의 여정을 살펴보자
- 그 힘든 여정의 첫 흔적은 wchar_t
wchar_t
- 각 문자가 고정된 바이트 크기를 가짐
- 즉, 모든 문자의 바이트 크기가 같음(멀티바이트 문자와 다름!)
- 그 바이트 크기는?
- 타겟 플랫폼에서 지원하는 모든 인코딩 중 가장 큰 문자를 담을 수 있어야 함
- 지금 와서 보면 이걸 만족하는 것은 사실상 UTF-32(4바이트)
- 자료형이니 컴파일 시에 이미 인코딩과 바이트 크기가 정해짐
- 단, 컴파일러와 플랫폼마다 다를 수 있음
- 표준은 정확히 몇 바이트에 무슨 인코딩인지 정하지 않음
- 리눅스 : 4바이트(UTF-32)
- 윈도우 : 2바이트(USC-2)
- 역시 아스키 문자들은 기존 코드 값을 그대로 유지
wchar_t를 사용하는 법
const wchar_t* w_pope = L"포프"
printf("%ls\n", w_pope);
- 와이드 문자열의 경우 따옴표 앞에 'L'을 붙임
- 이런 와이드 문자열은 컴파일 중에 적절히 변경됨
- 최종 플랫폼에서 사용하는 wchar_t 인코딩에 맞는 비트패턴으로
wchar_t 전용 함수들(C89)
- wchar_t를 제대로 문자열처럼 다룰 수 있는 함수들으 C95에서 등장
- C89는 다음의 함수들만 제공
#include <stdlib.h>
// 멀티바이트 문자(열) -> 와이드 문자(열)
int mbtowc(wchar_t* pwc, const char* s, size_t n);
size_t mbstowcs(wchar_t* dst, const char* src, size_t len);
// 와이드 문자(열) -> 멀티바이트 문자(열)
int wctomb(char* s, wchar_t wc);
size_t wcstombs(char* dst, const wchar_t* src, size_t len);
멀티바이트 문자열 <--> 와이드 문자열 변환 예
const char* pope = "\ud3ec\ud504";
wchar_t w_buffer[64];
mbstowcs(w_buffer, pope, 64);
cosnt wchar_t* w_pope = L"포프";
char buffer[64];
wcstombs(buffer, w_pope, 64);
printf("%s\n", buffer);
문자열 길이는? (C89)
- 별도의 문자열 길이를 구하는 함수가 없음
- wchar_t가 고정 크기인 점을 이용하면 쉽게 구할 수 있음
- 따라서, 우선 멀티바이트 문자열을 와이드 문자열로 변환
- 그 후 널 문자가 나올 때까지 wchar_t가 몇 개인지 세기만 하면 됨
wchar_t 메시지를 메신저로 보내도 동일한 문제 발생
- 내 컴은 4바이트(리눅스)
- 해외 고객 컴은 2바이트(윈도우)
wchar_t는 2% 모자란 공통표준
- 데이터 교환용 공통 표준은 어느 플랫폼/언어와도 통용돼야 함
- wchar_t가 그게 될 수 있었다
- 문제는 C 표준은 wchar_t의 크기와 인코딩을 갖에하지 않음
C99에서는 언어 자체적인 해결 방법이 없음
- 만능 라이브러리(ICU)를 쓰던가
- 두 플랫폼 사이에 변환하는 라이브러리를 만들던가
- 그러나 UCS-2는 모든 유니코드 표현을 못함
C11
유니코드 - 역사적으로 사용한 방법
- 목표 : A 컴퓨터에서 만든 파일을 B 컴퓨터에서 열기
- 할 수 있는 것
- 와이드 문자 <-> 멀티바이트 문자 간 변환이 가능
- 할 수 없는 것
- A 컴퓨터의 멀티바이트 문자를 B 컴퓨터의 멀티바이트 문자로 바로 변환
- 인코딩이 다를 수 있음
- A 컴퓨터의 멀티바이트 문자를 B 컴퓨터의 멀티바이트 문자로 바로 변환
- 취한 방법
- A 컴퓨터의 멀티바이트 문자를 와이드 문자로 변환해서 저장
- B 컴퓨터에서 와이드 문자를 읽은 뒤 자신의 멀티바이트 문자로 변환 후 출력
- 하지만 두 플랫폼의 와이드 문자의 인코딩 크기가 다르면 망함
- Linux와 Windows에서 이미 일어나고 있는 일...
UTF-16과 UTF-32를 표한하는 자료형
- <uchar.h>에 아래 두 매크로가 선언되어 있으면 사용 가능
#define __STCD_UTF_16__
#define __STCD_UTF_32__
- char16_t: UTF-16으로 인코딩된 값을 저장
- 리터럴: u"문자열", "\unnnn"
- char32_t: UTF-32로 인코딩된 값을 저장
- 리터럴: U"문자열", "\Unnnnnnnn"
UTF-16과 UTF-32 예
char16_t msg1[] = u"포큐";
char16_t msg2[] = u"\uD3EC\uD050\u73E0\u3077";
char16_t msg3[] = u"포큐";
char16_t msg4[] = U"\U0000d3ec\U0000d050.....";
const char* utf16_str = u"포프"; ` // 컴파일 오류
const char16_t utf16_str[] = u"포프"; // 컴파일 됨
바로 출력할 수 있을까?
- 당연히 바로 출력하면 제대로 안 나옴
const char16_t utf16_str[] = u"포프";
printf("utf-16: %s\n", utf16_str); // 잘못된 결과 출력
멀티바이트 문자로 변환 뒤 출력
- c16rtomb(), c32rtomb()
- 한 문자씩 변환 (문자열 변환 함수는 없음)
- 반대로 멀티바이트 문자에서 변환하는 함수도 있음
- mbtoc16()
- mbto32()
멀티바이트 문자로 변환 뒤 출력하기 예
const char16_t utf16_str[] = u"포프";
mbstate_t state = {0, };
char buffer[64];
char* p = buffer;
for (size_t i = 0; i < ARRAY_LENGTH(utf16_str); ++i){
size_t num_bytes = c16rtomb(p, utf16_str[i], &state);
if((size_t)-1 == num_bytes){
break;
}
p += num_bytes;
}
UTF-16, UTF-32을 사용하면?
wchar_t를 사용할 경우
입출력 <- 멀티바이트 문자 - wchar_t -> 파일(wchar_t 인코딩)
char16_t/char32_t를 사용할 경우
입출력 <- 멀티바이트 문자 - UTF-16/UTF-32 -> 파일(UTF-16/32)
-
즉 포팅이 불가능 한 wchar_t를 대체 가능
-
wchar_t 없이 유니코드 문자를 멀티바이트 문자로 곧바로 변환
-
UTF-8은 C22에 넣자고 제안된 상태
-
들어온다면 char8_t가 될 듯
-
역시 STDC_UTF_8 매크로도 정의될 것임
그래도 이건 된다
const char* utf8_str = u8"포프";
const char* utf8_str2 = u8"\U0000d3ec\U0000d504";
-
아직 char8_t가 없으니 u8은 char로 저장
-
그런데 이걸 멀티바이트 문자로 바꾸는 함수는 아직 없음
다국어 지원의 기본 원칙- 기본 원칙
- 사용자에게 보여주지 ㅏㅇㄶ을 문자열은 전부 아스키로 저장하자
- 가장 편한 건 ICU 라이브러리 쓰는 것
- 기본 원칙
- 최상의 시나리오 (C89 이상)
- 사용자 환경을 무조건 UTF-8로 만들자
- 사실상 회사에서 사용할 때만 가능
- 멀티바이트 문자도 UTF-8 인코딩이니 그대로 저장
- 다시 파일을 읽어서 보여줄 때도 UTF-8이므로 그대로 멀티바이트 문자로 출력
- 어느 방향도 변환 없음
입출력 <- 멀티바이트 문자(UTF-8) -> 파일(UTF-8)
- wchar_t가 UTF-32인 경우 (C89 이상)
- 멀티바이트 문자를 와이드 문자로 변환
- 와이드 문자가 UTF-32니 코드 포인트가 그대로 4바이트에 저장되어 있음
- 이걸 직접 작성한 UTF-8 변환 함수를 사용해서 UTF-8로 변환 뒤 파일로 저장
- 파일을 열 때는 그냥 반대 과정을 거침
- 각 방향 당 변환 두 번!
입출력(???) <- 멀티바이트 문자(???) - 와이드 문자(UTF-32) - UTF-8 -> 파일(UTF-8)
- char32_t가 UTF-32인 경우 (C11 이상)
- 멀티바이트 문자를 char32_t로 변환
- UTF-32니 코드 포인트가 그대로 4바이트로 저장되어 잇음
- 이걸 직접 작성한 UTF-8 변환 함수를 사용해서 UTF-8로 변환 뒤 파일로 저장
- 파일을 열 때는 그냥 반대 과정을 거침
- 각 방향 당 변환 두 번!
입출력(???) <- 멀티바이트 문자(???) - char32_t - UTF-8 -> 파일(UTF-8)
- char8_t가 UTF-8인 경우 (C22 이상)
- 멀티바이트 문자를 char8_t로 변환
- 이걸 그대로 파일로 저장
- 파일을 열 때는 그냥 반대 과정을 거침
- 각 방향 당 변환 한 번!
입출력(???) <- 멀티바이트 문자(???) - char8_t -> 파일(UTF-8)
경계 점검(bounds-check) 함수
- 경계 점검: 올바른 메모리에 접근하는지 확인하는 것
- 예:
- 사용할 배열 색인이 실제 배열의 범위 안에 있는지
- 변수에 저장할 값이 변수형이 허용하는 범위 안에 있는지
- 등
- 이것저것 검사하다보니 속도가 느려질 수 있음
- 기존의 C 함수들은 이런 검사를 해주지 않음(성능이 우선)
- C11은 안전한 메모리 접근을 보장하기 위해 경계 점검을 하는 함수를 다수 추가
허나 논란이 많은 기능
- 반드시 구현해야 하는 함수가 아님
- GCC의 경우 구현 안 함
- 심지어 다음 버전에서 퇴출하자는 제안이 올라옴
- 따라서, 일부 함수들만 가볍게 보고 지나가자
경계 점검 함수 사용하기
- 다음의 매크로가 정의되어 있으면 이 기능을 지원하는 컴파일러
#define __STCD_LIB_EXT1__
- 이 함수들을 활성화시키려면 다음의 매크로를 선언해야 함
- 주의: 관련 헤더 파일을 인클루드하기 전에 선언할 것
#define __STCD_LIB_EXT1__ 1
경계 점검 함수들(일부)
- fprintf_s() / printf_s()
- gets_s()
- sprintf_s() / snprintf_s()
- fopen_s()
- strcpy_s() / strncpy_s()
- 그 밖의 함수는 직접 찾아볼 것
경계 점검 관련 변수 및 매크로
- errno_t
- 함수의 반환값으로 사용
- 0이면 성공 아니면 실패
- rsize_t
- size_t와 같은 형을 typedef한 것. 하는 일도 같음
- 스스로 경계를 검사하는 함수라는 사실을 표시하기 위해 사용
- RSIZE_MAX
- 경계 점검 함수에서 허용하는 버퍼의 최대 크기
- 상수일 수도 있고 실행 중에 변하는 변수일 수도 있음
gets() 가 제거됨
- C11에서부터 완전히 제거됨
- 후방 호환성을 중시하는 C에서 함수를 제거하는 일은 드뭄ㄴ 일
- 그만큼 위험한 함수(버퍼 오버플로의 가능성)
- 기본적으로 fgets()를 쓰면 됨
- _s 함수로 좀 더 안전하게 쓰려면 gets_s()
gets_s()
char* gets_s(char* str, rsize_t n);
- str에 n-1개의 문자까지 저장
- 언제나 마지막에 널 문자를 붙여줌
- 실행 중에 다음과 같은 오류들을 감지함
- n이 0이거나 RSIZE_MAX보다 클 경우
- str이 널 포인터인 경우
- n-1개의 문자를 저장한 후에 줄바꿈이나 EOF가 발생하지 않을 때
- 오류가 감지되면?
- stdin으로부터 내용을 읽고 줄바꿈이나 EOF를 만날 때까지 문자를 버림
- 그 뒤에 등록된 핸들러 함수를 호출
sprintf_s(), snprintf_s()
int sprintf_s(char* restrict buffer, rsize_t bufsz,
const char* restrict format, ...);
- snprintf_s()도 매개변수 목록은 같음
- 실행 중에 다음과 같은 오류들을 감지함
- format이나 buffer가 널 포인터일 경우
- bufsz의 크기가 0이거나 RSIZE_MAX보다 클 경우
- format 안에 있는 '%s'에 대응하는 인자가 널 포인터일 경우
- (sprintf_s) buffer에 저장될 문자열의 길이가 bufsz보다 큰 경우
fopen_s()
errno_t fopen_s(FILE* restrict* restrict streamptr,
const char* restrict filename,
const char* restrict mode);
- 이 함수로 파일을 열면("w"나 "a" 모드) 배타적으로 파일 사용
- 즉 다른 프로그램이 동시에 이 파일에 접근 불가
- streamptr: 파일 스트림의 포인터의 포인터
- mode: 새로 추가된 모드
- "x" : "w" 또는 "w+"와 함께 사용. 파일이 이미 있다면 덮어쓰기 대신 그냥 실패
- "u" : "w" 또는 "a"와 함께 사용. 예전처럼 다른 프로그램의 파일 접근을 허용
- 실행 중에 다음과 같은 오류들을 감지함
- streamptr가 널 포인터인 경우
- filename이 널 포인터인 경우
- mode가 널 포인터인 경우
strnlen_s()
size_t strnlen_s(const char* str, size_t strsz);
- 반환 값
- str의 길이 : 성공
- 0 : str이 널 포인터
- strsz : str에서 시작하여 strsz 개를 읽었는데도 널 문자를 못 찾음
- 결과가 정의되지 않음
- str에 널 문자가 없고 str의 실제 길이가 strsz보다 작을 경우
- 소유하지 않은 메모리를 읽게 되니 당연한 결과
strcpy_s()
errno_t strcpy_s(char* restrict dest, rsize_t destsz,
const char* restrict src);
- strcpy()와 다른 점
- 복사 후 남은 dest 공간에 쓰레기 값이 들어있을 수 있음
- 성능 향상을 위해 한 번에 여러 바이트씩 복사할 수도 있기 때문
- 실행 중에 다음과 같은 오류들을 감지함
- src나 dest가 널 포인터일 경우
- destsz가 0 이거나 RSIZE_MAX보다 클 경우
- destsz == strnlen_s(src, destsz). 즉, 널 문자가 들어갈 공간이 없음
- src와 dest의 메모리 공간이 겹칠 때
- 다음 경우, 결과가 정의되지 않음
- dest의 실제 배열 크기 <= strnlen_s(src, destsz) < destsz
strcpy_s()
errno_t strncpy_s(char* restrict dest, rsize_t destsz,
const char* restrict src, rsize_t count);
- 최대 count 개의 문자를 복사 후, 널 문자도 붙여줌
- strncpy()와 다른 점
- strncpy()는 복사 후 남은 공간을 0으로 채워줌
- strncpy_s()는 널 문자 뒤 남은 공간에 쓰레기 값이 들어있을 수 있음
- 실행 중에 다음과 같은 오류들을 감지함
- src나 dest가 널 포인터일 경우
- destsz나 count가 0 이거나 RSIZE_MAX보다 클 경우
- destsz <= strnlen_s(src, destsz)
- src와 dest의 메모리 공간이 겹칠 때
- 다음 경우, 결과가 정의되지 않음
- dest의 실제 배열 크기 < strnlen_s(src, destsz) <= destsz
- src의 실제 배열 크기 < strnlen_s(src, count) < destsz
논란의 여지가 많은 _s 함수
- 메모리를 보호한다고 _s 함수들이 반듣시 올바르다고 볼 수 없음
- 심지어 모든 경우를 다 방어하지도 않음
- 각 함수마다 규칙도 복잡해짐
- 복잡한 규칙은 실수할 여지를 증가
- 무조건 안전한 함수를 쓰자는 주장은 언매니지드 언어 개발자에게 잘 안통하는 논리
- 성능을 포기해야 하기 때문
- 내부 동작원리가 직관적으로 안 보이는 문제도
- 이런 오류들을 핸들러에서 어떻게 처리해줄 예정?
- 실제로 논란의 여지가 많아서 널리 구현되지 않음
안전한 코딩은 프로그래머의 몫
- _s 함수들은 방어한다고 이것저것 내부적으로 추가한 게 많음
- 추가적인 뭔가를 해주면 당연히 성능은 저하
- strcpy()의 순수한 속도가 필요하다면 C11의 함수를 못 쓸 수도
- C 프로그래밍할 때는 널 포인터 관리를 어짜피 잘해야 함
- 함수 몇 개 고친다고 해결될 일이 아님
- 그렇다고 모든 함수 안에서 이런 검사해줄 것임?
- 프로그래머가 할 일을 함수가 전부 다 해줄 필요는 없음
C99의 방향성과 상반되기도...
-
restrict 키워드
- C99
- 메모리 접근에 대한 안전장치를 해제하라고 알려주는 힌트
- 컴파일너는 안전한 메모리 접근을 위한 어셈블리 코드를 생략할 수 있음
- 속도 향상, 그러나 호출자가 실수하면 결과를 예측할 수 없음
-
xxx_s() 함수
- C11
- 여러 안전장치를 붙인 메모리 접근 함수들을 제공
- 속도 하락
어떤 컴파일러는 과잉 친절을 베풀기도 함
- 실무에서는 이걸 오지랖이라 생각함
- 컴파일 중 아예 꺼버리는 경우가 많음
- 컴파일 옵션으로 _CRT_SECURE_NO_WARNINGS를 정의
'프로그래머 > C, C++' 카테고리의 다른 글
[HackerRank] Virtual Functions | 클래스 상속 | 가상함수 | 클래스 정적 멤버변수 (0) | 2021.01.27 |
---|---|
[포프 tv 복습] Type-Generic 함수 만들기, 정적 어서트, 메모리 정렬, 멀티스레딩 (0) | 2020.12.04 |
[포프 tv 복습] 나만의 라이브러리 만들기, C99 (0) | 2020.12.01 |
[포프 tv 복습] 전처리기 (0) | 2020.11.30 |
[면접 대비] C를 사용한 해시 맵 구현 (0) | 2020.11.29 |