본문 바로가기

프로그래머/C, C++

[포프 tv 복습] C99, C11

C99

C99 이전의 부동 소수점 연산 오류 처리

  • 부동 소수점 연산 중 오류가 날 경우 그 이유를 오류 조건이라 함
  • C 라이브러리 함수가 오류 조건을 보고하는 경우가 있음
  • 이 오류 조건의 일부는 errno를 통해 확인 가능
    • 인자가 수학적으로 정의된 범위를 벗어날 경우 : EDOM
    • 오버플로가 발생한 경우 : ERANGE
    • 언더플로가 발생한 경우 : ERANGE가 설정될 수도 있음(구현에 따라 다름)

C99의 부동 소수점 연산 오류 처리

  • 좀 더 세분화된 부동 소수점 전용 오류 보고 기능 추가
    • 이것을 '부동 소수점 예외'라고 부름
    • 예외라고 하지만 다르 언어에서 말하는 예외는 아님
    • 그냥 다른 형태의 오류 코드
  • errno에서 찾을 수 없던 오류 조건도 보고 됨
  • 구현에 따라 다음 중 하나를 지원
    1. 여전히 errno을 사용
    2. 새로운 부동 소수점 예외를 사용
    3. 둘 다 지원

부동 소수점 연산 오류 처리방법 확인하기

  • 구현마다 달리 정의된 math_errhandling 비트 플래그를 확인

    1. 여전히 errno을 사용 : MATH_ERRNO 플래그가 설정되어 있음

      #define MATH_ERRNO      1
    2. 부동 소수점 예외를 사용 : MATH_ERREXCEPT 플래그

      #define MATH_ERREXCEPT   2
    3. 둘 다 지원 : 둘 다 설정되어 있음

  • 다음이 참이면 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 수학

  • 제네릭이란 보통 둘 중 하나를 의미
    1. 모든 자료형을 표현할 수 있는 경우 (ex. void*)
    2. 각 자료형에 맞게 알아서 동작 (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

  • 다음 순서로 최종 호출할 함수가 결정됨
    1. 매개변수 중 하나가 long double이면 long double용 함수
    2. 매개변수 중 하나가 double 또는 int이면 double용 함수
    3. 그 밖의 경우는 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 쓰지말 것

  • 그냥 쓰지 않는 게 좋음
  • 그냥 여태까지 사용하던 방법을 사용할 것
    1. 충분히 크게 정적 배열을 잡아서 쓸 것
    2. 그보다 큰 데이터가 인자로 들어오면 반복문으로 나눠 읽는 법이 있음
    3. 그것도 안 되면 동적 메모리 할당을 사용할 것

(복습) 함수에 배열 매개변수 전달하기

int sum(int nums[], size_t count)
{
    //...
}
  • 여기서 nums는 단순히 int*
  • 하지만 포인터를 전달할 때에 비해 몇 가지 제약이 있음
    • 배열 매개변수를 int* const처럼 전달할 방법이 없음
      • const int nums[]는 const int*
    • restrict도 불가능
  • 함수에 전달될 배열의 요소수를 컴파일 도중 알 방법이 전혀 없음
  • 만약 알 수만 있다면 최적화 가능
    • 예 : 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 컴퓨터에서 와이드 문자를 읽은 뒤 자신의 멀티바이트 문자로 변환 후 출력
  • 하지만 두 플랫폼의 와이드 문자의 인코딩 크기가 다르면 망함
    • 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로 저장

  • 그런데 이걸 멀티바이트 문자로 바꾸는 함수는 아직 없음

    다국어 지원의 기본 원칙
    • 기본 원칙
      1. 사용자에게 보여주지 ㅏㅇㄶ을 문자열은 전부 아스키로 저장하자
    • 가장 편한 건 ICU 라이브러리 쓰는 것
  1. 최상의 시나리오 (C89 이상)
  • 사용자 환경을 무조건 UTF-8로 만들자
    • 사실상 회사에서 사용할 때만 가능
  • 멀티바이트 문자도 UTF-8 인코딩이니 그대로 저장
  • 다시 파일을 읽어서 보여줄 때도 UTF-8이므로 그대로 멀티바이트 문자로 출력
  • 어느 방향도 변환 없음

입출력 <- 멀티바이트 문자(UTF-8) -> 파일(UTF-8)

  1. wchar_t가 UTF-32인 경우 (C89 이상)
  • 멀티바이트 문자를 와이드 문자로 변환
  • 와이드 문자가 UTF-32니 코드 포인트가 그대로 4바이트에 저장되어 있음
  • 이걸 직접 작성한 UTF-8 변환 함수를 사용해서 UTF-8로 변환 뒤 파일로 저장
  • 파일을 열 때는 그냥 반대 과정을 거침
  • 각 방향 당 변환 두 번!

입출력(???) <- 멀티바이트 문자(???) - 와이드 문자(UTF-32) - UTF-8 -> 파일(UTF-8)

  1. char32_t가 UTF-32인 경우 (C11 이상)
  • 멀티바이트 문자를 char32_t로 변환
  • UTF-32니 코드 포인트가 그대로 4바이트로 저장되어 잇음
  • 이걸 직접 작성한 UTF-8 변환 함수를 사용해서 UTF-8로 변환 뒤 파일로 저장
  • 파일을 열 때는 그냥 반대 과정을 거침
  • 각 방향 당 변환 두 번!

입출력(???) <- 멀티바이트 문자(???) - char32_t - UTF-8 -> 파일(UTF-8)

  1. 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가 발생하지 않을 때
  • 오류가 감지되면?
    1. stdin으로부터 내용을 읽고 줄바꿈이나 EOF를 만날 때까지 문자를 버림
    2. 그 뒤에 등록된 핸들러 함수를 호출

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를 정의