본문 바로가기

프로그래머/C, C++

[포프 tv 복습] 나만의 라이브러리 만들기, C99

나만의 라이브러리 만들기


C에서도 라이브러리를 만들 수 있다

  • 오브젝트 파일을 모아 라이브러리로 만듦
  • 다시 컴파일할 필요 없이 코드 재활용이 가능
  • 소스 코드 공개 없이(단, 헤더 파일은 예외) 라이브러리 배포 가능
  • C에서는 두 종류의 라이브러리를 만들 수 있다고 했음
    • 정적 라이브러리
    • 동적 라이브러리

(복습) 정적 라이브러리와 링크

  • 정적 라이브러리와 링크하는 것을 정적 링킹이라고 함
  • 라이브러리 안에 있는 기계어를 최종 실행파일에 가져다 복사함
  • 동적 링킹에 비해
    • 실행 파일의 크기가 커짐
    • 메모리를 더 잡아먹을 수 있음
    • 실행 속도가 빠름

보통 정적 라이브러리를 사용하는 절차

  1. 소스 코드들을 컴파일하여 정적 라이브러리를 만듦
    • 보통 파일 하나
    • 물론 여러 개의 라이브러리를 만들면 파일도 여럿
    • 확장자는 *.lib(윈도우 비주얼 스튜디오) 또는 *.a(리눅스 계열)
    • 참고로 리눅스 계열에서는 아카이브(archive)라고도 함
  2. 다른 소스 코드들을 작성할 때 위 라이브러리의 헤더 파일들을 사용
    • 실행 파일을 만들기 위해 main() 함수를 가지고 있는게 보통
    • 여기의 소스 코드들이 바뀌어도 정적 라이브러리를 다시 만들 필요 없음
  3. 컴파일 할 때 정적 라이브러리와 함께 링킹
  1. simple_math.c 컴파일하기
clang -std=89 -W -Wall -pedantic-errors -c simple_math.c -o simple_math.o
  1. 정적 라이브러리 만들기
  • 플랫폼마다 이용하는 실행파일이 다름
    • 윈도우 : llvm-ar
    • 리눅스 계열 : ar

llvm-ar

llvm-ar -명령어<modifier> 정적_라이브러리_파일 <o파일들>
  • 명령어

    • r : 정적_라이브러리_파일에 o파일들을 추가
    • d : 정적_라이브러리_파일에서 o파일들을 삭제
  • modifier(선택)

    • 각 명령어마다 사용할 수 있는 modifier가 다름
    • c : 정적 라이브러리 파일이 처음 만들어질 때 경고 메시지 출력 안 함
  • 그 밖의 명령어와 modifier가 궁금하면 스스로 찾아 볼 것

  • 아래 예 둘 다 simple_math.lib 파일이 없는 경우

  • 'c'를 사용하지 않을 때

    > llvm-ar -r simple_math.lib simple_math.o
    llvm-ar.exe: warning : create simple_math.lib
  • 'c'를 사용할 때

    > llvm-ar -rc simple_math.lib *.o

llmv-ar으로 정적 라이브러리 만들기

week12\simple_math> llvm-ar -rc ..\lib\simple_math.lib simple_math.o
  1. 정적 라이브러리의 헤더 인클루드하기
// main.c
#include <stdio.h>
#include "simple_math.h"

int main()
{
    printf("Test: %d\n", add(10, 20));
    return 0;
}
  • 정적 라이브러리에 있는 함수를 사용하려면 해당 헤더를 인클루드
  • 이 때 헤더 파일의 경로는 생략하는 게 일반적
  • 아직 함수의 실제 구현은 모름
  1. 정적 라이브러리와 함께 빌드하기
clang -std=c89 -W -Wall -pedantic-errors -I <dir> -L <dir> -l<lib_name> *.c
  • -I dir
    • 인클루드 할 때 헤더파일을 검색할 경로를 추가
  • -L dir
    • 빌드 시 사용할 라이브러리 파일이 있는 폴더
  • -l lib_name
    • lib_name: 빌드 시 사용할 라이브러리
    • 파일명.lib에서 파일명을 -l 다음에 띄어쓰기 없이 붙임

simple_math.lib과 함께 빌드해보자!

week12\program> clang -std=c89 -W -Wall -pedantic-errors -I "../simple_math" -L "../lib" -lsimple_math *.c
  • simple_math.h는 week12\simple_math에 있음

  • 따라서 -I를 이용해 해당 경로를 알려줌

    • 안 그러면 헤더 파일을 찾지 못 함
    • 이게 소스코드에서 경로를 넣어줄 필요가 없던 이유
  • 정적 라이브러리 파일은 week12\lib에 있음

  • 따라서 -L을 이용해 해당 경로를 알려줌

  • 정적 라이브러리 파일명은 'simple_math.lib'

  • 따라서 -l 바로 뒤에 'simple_math'를 붙여줌

  • 실행파일이 제대로 빌드됨

  • 이제 평상시와 같이 실행하면 끝

  • 파일이 많아지면?

  • 비주얼 스튜디오의 프로젝트 사용

  • C 역시 비주얼 스튜디오를 사용하면 이 프로젝트를 사용하게 됨

  • gcc나 clang의 경우 자체적으로 프로젝트 같은 것을 지원하지 않음

  • 그러나 cmake를 이용하면 똑같은 일을 할 수 있음

  • 프로젝트란, 라이브러리를 만들 때, 어떤 파일들을 컴파일해야 하는지, 실행파일을 만들 때, 어떤 소스코드와 어떤 라이브러리를 합쳐야 하는지 등을 적어 두는 파일

(복습) 동적 라이브러리와 링크

  • 동적 라이브러리와 링크하는 것을 동적 링킹이라고 함
  • 실행파일 안에 여전히 구멍을 남겨두는 방법
  • 실행파일을 실행할 때 실제로 링킹이 일어남
    • 이 링킹은 실행 중에 운영체제가 해줌
  • 정적 링킹에 비해
    • 실행파일 크기가 작다
    • 여러 실행파일이 동일한 라이브러리를 공유할 수 있다 -> 메모리 절약
    • 여러 실행파일이 이름은 같지만 버전이 다른 동적라이브러리를 사용한다면 DLL지옥을 맛볼 수 있다.

보통 동적 라이브러리를 사용하는 절차

  1. 소스 코드들을 컴파일하여 동적 라이브러리를 만듦
    • 역시 파일 하나
    • 확장자는 *.dll(윈도우) 또는 *.so(리눅스 계열)
  2. 다른 소스 코드들을 작성할 때 위 라이브러리의 헤더 파일들을 사용
  3. 컴파일 할 때 동적 라이브러리와 함께 링킹
    • 단, 동적 라이브러리에 있는 기계어가 실행파일에 포함되지 않음
    • 실행 중에 동적으로 링킹할 수 있는 정보만 포함
    • 따라서 동적 라이브러리 파일도 같이 배포해야 함

동적 라이브러리와 운영 체제

  • 운영체제마다 실행 파일 및 동적 라이브러리 내부 포맷이 다름
    • 리눅스 계열 : ELF(excutable and linkable format) 포맷
    • 윈도우 : PE(portable executable) 포맷
  • 운영체제의 동적 링커(dynamic linker)
    • 프로그램이 실행될 때 필요한 동적 라이브러리를 로딩 후 링킹 해줌
    • 이러려면 동적 라이브러리 안에 있는 함수들을 메모리에 매핑해줘야 함
    • 메모리에 맵핑할 때 필요한 정보가 위 포맷에 저장되어 있음
    • 따라서 운영체제가 지원하지 않는 포맷이면 정보를 읽어오지 못 함
  • 즉, 동적라이브러리는 운영체제에 종속적
  • 운영체제와 컴파일러마다 동적 라이브러리를 만드는 방법이 다름
  • 보통 사용하는 컴파일러
    • 윈도우는 주로 비주얼 스튜디오를 이용
    • 리눅스 계열은 주로 Clang이나 GCC를 이용
  • Clang이나 GCC를 쓸 경우 윈도우와 리눅스에서 만드는 법이 다름
  • 윈도우의 Clang을 사용할 때도 컴파일러 백엔드에 따라 또 달라짐
    • MinGW
    • 비주얼 스튜디오

      동적 라이브러리 만들기

      동적 라이브러리 링크하기

    • gcc <o파일 + 경로> -L<동적 라이브러리 경로> -l<동적 라이브러리 이름> -o <실행파일 이름>
    • gcc -shard <o파일 + 경로> -o <동적 라이브러리 파일명 + 경로>

동적 라이브러리의 장단점

  • 장점
    • 실행파일을 바꾸지 않고 동적 라이브러리 파일만 업데이트 가능
    • 동적 라이브러리 파일을 바꾸지 않고 실행파일만 업데이트 가능
    • 필요에 따라 동적 라이브러리를 선택적으로 로딩 가능
      • 예: CPU 세대 별로 동적 라이브러리 파일을 만들어 둠
    • 여러 실행파일들이 같은 동적 라이브러리를 소유 가능
  • 단점
    • 해킹 당하기 쉬움(예: DLL 인젝션)
      • 라이브러리 안에 있는 함수의 메모리 주소가 동적으로 링킹 되기 때문
    • DLL 지옥

정적 라이브러리의 장단점

  • 장점
    • 함수의 주소가 공개 안 되니 보다 안전
    • 정확한 버전의 라이브러리가 실행파일 안에 내포되어 잇음
    • 최적화에 유리
  • 단점
    • CPU 세대 별로 실행파일을 만들어서 배포해야 함
    • 라이브러리의 소스코드가 바뀔 때마다 실행파일을 재배포해야 함
      • 실행파일의 소스코드가 바뀔 때도 마찬가지
    • 실행파일의 크기가 커짐
    • 실행 중 다른 실행파일들과 라이브러리 공유 불가

베스트 프랙티스 : 정적라이브러리를 쓰자

  • 일단 기본적으로 정적 라이브러리를 사용할 것
  • 동적 라이브러리가 필요하면 그 때 동적라이브러리로 전환

C99 표준


C99로 빌드하기

- std 옵션을 c89 대신에 c99로
> clang -std=99 -W -Wall -pedantic-errors *.c

(복습) 매크로 함수

  • 함수 호출 형태가 아니라 코드 그 자체를 복붙
  • 함수 호출에 따른 과부하를 막을 수 있음
  • 그러나 디버깅이 아주 매우 엄청나게 힘듦
  • 뿐만 아니라 가독성이 매우 떨어짐

인라인 함수

inline 반환형 함수_이름(매개변수 목록) {}
  • 컴파일러에게 최적화 해달라고 알려주는 '힌트'
    • 보통 매크로 함수처럼 코드를 복붙 해줌
    • 즉, 함수 호출이 사라짐
    • 힌트일 뿐이라 컴파일러가 무시할 수도 있음
    • inlin이 없어도 컴파일러가 알아서 최적화를 해줄 수도 있음
  • 복붙을 할 수 있으려면?
    • 인라인 함수를 호출하는 코드를 컴파일할 때 그 함수의 구현을 알아야 함
    • 따라서 인라인 함수의 구현은 소스 파일이 아니라 헤더 파일에 둠

C++의 인라인과는 다르다!

  • C의 인라인은 C++에서 가져온 것
  • 그러나 C++의 인라인만큼 명확하지 않음
    • C에서 올바르게 작동시키려면 좀 이상한 짓을 해야함
  • 그나마 다행인 점 : C의 인라인 코드는 C++에서 제대로 동작

무식하게 코드를 복붙하지 않는다!

  • 매크로는 전처리기가 코드를 무식하게 토씨 하나 안 틀리게 복붙 함
    • 그러다 보니 연산자 우선순위 문제가 생길 수 있음
    • 매개변수 및 코드를 무조건 괄호로 감싸는 것을 권장
  • 인라인 함수는 컴파일러가 컴파일 중에 함수 호출을 코드로 바꿔줌
    • 사실 결과적으로는 복붙. 좀 더 융통성 있게 잘 복붙할 뿐
    • 함수가 누리는 혜택을 그대로 누림

함수 구현을 알아야 복붙이 가능하다

  • 복붙을 하려면 막하던, 잘하던 간에 함수 구현을 알아야
  • 즉, 트렌스레이션 유닛 안에 인라인 함수의 실제 코드가 있어야 함
  • 이 함수의 구현이 다른 C 파일 안에 있으면 불가능
    • C 파일 별로 따로 컴파일되기 때문
  • 따라서 헤더 파일 안에 실제 코드가 있어야 함
    • 매크로 함수와 마찬가지
    • 전처리기 단계에서 #include가 헤더의 내용을 모두 복붙

헤더에 함수의 구현부를 넣으면?

// simple_math.h
int add(int op1, int op2)
{
    return op1 + op2;
}

// humanoid.c

/*
int add(int op1, int op2)
{
    return op1 + op2;
}
*/

#include "simple_math.h"
void walk(...)
{

}

// bird.c

/*
int add(int op1, int op2)
{
    return op1 + op2;
}
*/

#include "simple_math.h"
void fly(...)
{

}

-> 링킹 오류가 발생함

링킹 오류가 나는 이유

  • 모든 .o 파일에 add()가 들어있음
  • 즉, 동일한 이름의 함수가 2개나 있음
  • 그 중 어떤 add()와 링킹을 해야 하는지 몰라서 오류 발생

inline 키워드로 이 함수의 용도를 표시

  • 컴파일러에게 호출용 함수가 아니라 코드 교체용이라 알려줌
  • 그 결과 링커가 볼 수 있는 함수 심볼을 만들지 않음
    // simple_math.h
    inline int add(int op1, int op2)
    {
      return op1 + op2;
    }
  • 이제 컴파일하면 아까의 링커 오류가 안 남
  • 단, 다른 오류가 날 수도 있음...

inline 키워드는 그저 힌트일뿐

  • 컴파일러가 해당 함수를 인라인화 한다는 보장이 없음
  • 한다면 문제가 없음
  • 안 한다면 문제가 됨
    // simple_math.h
    inline int add(int op1, int op2)
    {
      return op1 + op2;
    }
    

// humanoid.c
#include "simple_math.h"
void walk(...)
{
// 코드 어딘가에서
// add() 호출
}

// bird.c
#include "simple_math.h"
void fly(...)
{
// 코드 어딘가에서
// add() 호출
}


## 인라인이 안 되면 무슨 문제가 일어날까?
- inline 키워드가 붙은 함수가 인라인이 안 됐다는 의미는?
    - 여전히 실행 중에 함수 호출을 한다는 의미
- 따라서, 컴파일 단계에서는 그 함수의 시그니쳐만 기억
- 링커가 실제 함수 구현을 찾아 구멍을 메꿔줌
- 그럼 무슨 일이 일어날까? 링킹 오류가 발생

## 왜 이런 일이 발생하지?
- 아까 inline 키워드를 설명하며 했던 말
    - 컴파일러에게 해당 함수를 '함수'로 쓰지 말라 함
    - 그 대신 '코드 교체용'으로 쓰라고 알려줌
- 그러다보니 **컴파일러는 링커가 볼 수 있는 함수 심볼을 만들지 않음**
- 그러나 문제는 이 함수가 반드시 인라인 된다는 보장이 없음
- 인라인이 안 되면?
    - 안 됐는데 함수 심볼마저 없음
    - 따라서 링커 입장에서는 해당 함수를 찾을 방법이 없음

## 해결법?? : 일반 함수도 따로 만든다
- 인라인 함수와 똑같이 구현된 일반 함수가 어딘가에 존재하면 됨
    - 링커가 찾을 수만 있으면 문제 없음
- 그러나 이런 방법은.. 쓸데없는 코드 중복
```c
// simple_math.h
#ifndef SIMPLE_MATH_H
#define SIMPLE_MATH_H
inline int add(int op1, int op2)
{
    return op1 + op2;
}
#endif

// simple_math.c
#include "simple_math.h"
int add(int op1, int op2)
{
    return op1 + op2;
}

올바른 해결법?? : extern

  • 가장 좋은 방법 : 코드 중복 없이 함수 하나만 있는 것
    • 인라인이 되면 인라인으로 사용
    • 안 되면 일반 함수로 사용
  • 그걸 가능하게 만드는 키워드가 extern
  • extern을 붙이면 링커가 찾을 수 있는 함수도 만들어 줌
    // simple_math.h
    #ifndef SIMPLE_MATH_H
    #define SIMPLE_MATH_H
    extern inline int add(int op1, int op2)
    {
      return op1 + op2;
    }
    #endif
  • 중복 오류 또 나옴

extern을 함수 구현부에 붙일 때 문제점

  • 이 헤더를 인클루드한 .c파일마다 이 함수가 생성됨
  • 즉, inline 안 붙인 함수가 헤더에 있을 때와 마찬가지 문제
  • 프로그램 전체에서 그 함수의 심볼은 딱 하나만 있어야 함

현재까지 아는 사실

  • 인라인 함수가 인라인도, 일반 함수도 될 수 있게 해야 함
  • 일반 함수로 만들기 위해서는 extern 키워드가 필요
  • 단, 일반 함수는 심볼이 딱 한 번만 나와야 함

올바른 해결법(최종)

  • 이 모든 것을 만족하는 가장 좋은 방법
  1. .h파일 안에 인라인 함수를 구현
  2. 그에 대응하는 .c파일을 만듦
  3. 그 파일에서 인라인 함수가 들어있는 .h파일을 인클루드
  4. 그 파일에서 인라인 함수를 extern 인라인 함수로 다시 선언
  • 이러면 그 .c파일 안에서만 함수의 심볼이 나옴 (딱 한 개!)
  • 이제 컴파일 중 인라인이 되면 헤더 파일에 있는 구현을 사용
  • 인라인이 안 되면 링커가 .c 파일에서 나온 심볼을 이용해서 링킹
// simple_math.h
#ifndef SIMPLE_MATH_H
#define SIMPLE_MATH_H

inline int add(int op1, int op2)
{
    return op1 + op2;
}

#endif

// simple_math.c
#include "simple_math.h"

extern inline int add(int op1, int op2);

// hunamoid.c 
#include "simple_math.h"
void walk(...)
{
    // 코드 어딘가에서
    // add() 호출
}

// bird.c
#include "simple_math.h"
void fly(...)
{
    // 코드 어딘가에서
    // add() 호출
}

C++ 인라인과의 차이

  • C의 인라인은 어떻게든 돌아가게 만들려고 이상한 짓을 한 느낌
  • C++에서는 이런 짓을 안 해도 됨
    • 헤더 파일에 구현한 인라인 함수는 자동적으로 extern
    • 따라서 이 헤더 파일을 인클루드 한 .cpp파일마다 함수 심볼이 생김
    • 그러나 표준에 따르면 링커가 이 여러 심볼 중에 하나만 골라서 링킹해야 함

베스트 프랙티스 : 인라인을 쓰자

  • 위와 같은 이유 때문에 매크로 함수보다는 인라인이 좋음
  • 특히 한 줄짜리 코드처럼 매우 간단한 함수일 때 적합
    • 매크로도 마찬가지
  • 그런데 C에서는 인라인보다는 매크로를 더 자주 사용
    • 일단 인라인이 사용하기 매우 불편하고 헷갈림
    • C89 이후 표준에 추가된 기능은 그리 널리 사용되지 않음
  • 이런 문제가 없는 C++에서는 매크로 대신 거의 인라인을 사용

함수 호출자를 100% 제어할 수 없다

  • 문자열 복사시, 두 메모리(원본, 복사될 공간)가 안 겹치는 게 맞음
  • 그러나 호출자가 겹치는 메모리 범위를 인자로 전달하면?
    • 막을 방법이 없음
  • 나눗셈(/)할 때 분모로 0을 사용하는 걸 막을 수 없는 것과 마찬가지

결국 안전의 책임은 컴파일러에게로...

  • 결국 많은 컴파일러가 이런 함수를 방어적으로 구현해둠
    • 특히 c->어셈블리로의 변환 과정에서
  • 이런 안전장치 덕에 코드가 '비교적' 안전하게 실행됨
  • 그러나 그로 인한 성능 저하 문제

restrict 키워드

int printf(const char* restrict format, ...);
int fprintf(FILE* restrict stream, const char* restrict format, ...);
int sprintf(void* restrict dest, const void* restrict src, size_t count);
void* memcpy(void* restrict dest, const void* restrict src, size_t count);
char* strcpy(char* restrict dest, const char* restrict src);
  • 포인터 변수 전용인 컴파일러에게 알려주는 힌트
    • '이 포인터 변수의 메모리는 절대 다른 변수와 겹치지 않는다'
    • 컴파일러가 이 힌드를 무시할 수 도 있음
  • 메모리 범위가 겹치는 걸 막는 키워드가 아님
  • 여전히 범위가 겹치는 포인터 전달 가능. 그 경우 정의되지 않은 결과

restrict를 사용할 때와 안 할 때의 차이

void increse(int* a, int* b, int* x)
{
    *a += *x;
        mov1    (%edx), $esi
        add1    %esi, (%ecx)
    *b += *x;
        mov1    (%edx), $esi
        add1    %esi, (%ecx)
}

void increse(int* restrict a, int* restrict b, int* restrict x)
{
    *a += *x;
        mov1    (%edx), $edx
        add1    %edx, (%ecx)
    *b += *x;
        add1    %edx, (%ecx)
}
  • 두 번째 코드 실행 시, x가 가리키는 값을 다시 레지스터에 읽어오지 않음
  • 즉, 여기서는 어떤 포인터도 서로 같은 메모리를 가리키지 않을 것이라 가정
  • 따라서 컴파일러 최적화를 할 수 있음

그러나.. 호출자는 여전히 무시할 수 있다

  • restrict는 컴파일러에게 '안전 장치 꺼도 됨'이라고 말해주는 것
  • 그러나 프로그래머는 여전히 무시할 수 있음
    • 여전히 메모리 범위가 겹치는 포인터들을 전달할 수 있음
  • 이 과목의 코딩표준 중 매개변수에 '_or_null'을 붙이는 때와 비슷
    • 함수가 NULL 매개변수를 제대로 처리하는 경우만 그렇게 표시
    • 그러나 그렇지 않은 매개변수에 NULL을 전달하는 걸 막을 수는 없음
    • assert()가 유일한 안전 장치

restrict의 필요성

  • 일부 하드웨어의 경우, 매우 빠르게 메모리 복사가 가능(ex. DMA)
  • 이를 위해 #define을 통해 플랫폼 전용 memcpy() 등을 만듦
  • 보통 이러한 함수들에는 여러가지 제약이 있음
    • 가장 대표적인 게 메모리 범위가 겹치지 않아야 한다는 것(즉, restrict)
    • 그 외의 제약으로는 메모리정렬(alignment)도 있음

restrict를 무시할 경우의 위험성

  • inline의 경우, 함수가 인라인화가 안 돼도 큰 문제가 없었음
    • 그냥 일반 함수가 호출됨
  • 그러나 restrict 덕에 최적화 된 코드에 포인터를 잘못 넣으면?
    • 어떻게 될 지 모름
  • C99 표준이라 C에서는 많이 못 쓰지만 매우 중요한 개념
    • C++에서도 매우 흔히 사용함

한 줄 주석

  • // 사용 가능

변수 선언

  • C99에서는 블록 중간에 변수 선언이 가능해짐

(복습) 가변 인자 함수

  • 정해지지 않은 수의 매개변수(가변 인자)를 허용하는 함수
    int add_ints(const size_t count, ...);
    int printf(const char *format, ...);
  • 가변 인자 함수와 관련된 매크로 함수 세 가지를 배움
    • va_start()
      • 가변 인자에 접근하기 전에 반드시 호출하는 하뭇
    • va_arg()
      • 가변 인자 목록으로부터 다음 가변 인자를 가져오는 함수
    • va_end()
      • 가변 인자에 접근을 다 한 후에 반드시 호출하는 함수

va_copy()

va_copy(dest, src)
  • C99에 추가

  • 가변 인자 목록을 복사하는 매크로 함수

  • dest를 다 사용한 후에는 반드시 va_end()를 호출해야

    double get_variance(int count, ...)
    {
      va_list arg_list_avg;
      va_start(arg_list_avg, count);
    
      va_list arg_list_v;
      va_copy(arg_list_v, arg_list_avg);
    
      double avg = 0.0;
      for(size_t i = 0; i < count; ++i){
          double num = va_arg(arg_list_avg, double);
          avg += num;
      }
      avg /= count;
      va_end(arg_list_avg);
    }

sprintf()의 문제점

int sprintf(char* buffer, const char* format, ...);
char buffer[20];
const char* name = "Caterina Hassinger";
int score = 100;

sprintf(buffer, "%s's score: %d\n", name, score);
  • 안전하지 않음
    • buffer의 크기보다 긴 문자열이 들어와도 중간에 멈추지 않음
    • 즉, buffer 범위를 넘어서서 계속 씀

snprintf()

int snprintf(char* restrict buffer, size_t bufsz, const char* format, ...);
  • 최대 bufsz-1개의 문자열을 출력
  • 나머지 하나는 바로 널 문자 용
    • 언제나 붙여준다!
    • strncpy()와는 다르다!
  • 하지만 실무에서는 마지막 요소에 널 문자 넣는 코드가 많이 보임
    • strncpy() 처럼
#include <stdio.h>
#define LENGTH (20)

int main()
{
    char buffer[LENGTH];
    const char* name = "Caterina Hassigner";
    int score = 100;

    snprintf(buffer, LENGTH, "%s's score: %d\n", name, score);

    return 0;
}

왜 널 문자를 넣어야 하나요?

  • C89에서 자기만의 _snprintf()를 제공한 컴파일러가 있었음
  • 이 함수는 널 문자를 안 붙여줬음
  • 이제는 snprintf()를 만들어서 제대로 지원
  • 그러나 여전히 호환 때문에 _snprintf()를 남겨둠
  • 고로 안전을 위해 언제나 마지막 요소에 널 문자를 넣는 코드를 둠
    snprintf(buffer, LENGTH, "%s's score: %d\n", name, score);
    buffer[LENGTH-1] = '\0';
  • 사실 이 방법은 다음에 볼 예외 상황에서도 안전

snprintf()도 위험할 수 있다!

#define LENGTH (20)

char buffer[LENGTH];
snprintf(buffer, 0, "%s's score: $d\n", name, score);
  • bufsz가 0이면 아무것도 안 함
  • 즉, 아무것도 안 썼으니까 널 문자도 안 붙여줌
  • 이 때, buffer을 읽으면 엄한 메모리까지 읽어올 수 있음
    • '\0'을 만날 때까지 계속 읽음
  • 이 밖에도, buffer나 format이 NULL이면 펑펑 터짐

long long int

  • C89에서는 최소 64비트인 정수형은 없었음
  • 그러나 C99에서는 생김
  • 그것이 바로 long long int
    • 최소 64비트이고 long이상의 크기
    • 다른 언어에서는 보통 그냥 long
  • 표준에 상관 없이 보통 안전하게 생각해도 되는 것
  • int 생략 가능

long long int의 리터럴

부호 있는 경우

long long big_num1 = 34534275098;
long long int big_num2 = 7869889796ll;

부호 없는 경우

unsigned long long big_num3 = 34534275098ULL;
unsigned long long int big_num4 = 7869889796U;

long long int의 서식문자

long long big_num1 = 709879807897;
long long int big_num2 = 78097675678ULL;

printf("big_num1: %lli\n", big_num1);
printf("big_num1: %llu\n", big_num2);
  • 부호 있는 경우 : %lli
  • 부호 없는 경우 : %llu

불형

  • 더 이상 #define TRUE (1) 안 해도 됨
  • 두 가지 방법
    • _Bool
    • bool: 헤더 인클루드 필요

_Bool

  • 거짓이면 0, 참이면 1
  • char/int/float과 같은 값을 _Bool에 넣을 경우
    • 0에 해당하는 값이면 0
    • 그 외의 값은 1로 변환
  • 여전히 참과 거짓은 숫자로 표현

bool, true, false

  • <stdbool.h> 헤더에 정의(#define)되어 있음
  • bool : _Bool을 다시 정의
  • true : 1로 정의
  • false : 0으로 정의

개발자들이 자체적으로 만든 bool과 충돌나기 때문에 bool을 기본으로 넣을 수가 없었음

typedef unsigned int bool;
#define TRUE (0)
#define FALSE (1)
//혹은
typedef enum {
    false,
    true
} bool;

같은 자료형인데 크기가 다름

  • int 형을 32비트라 가정하고 코딩하면 16비트인 곳에서 문제
    • 정수형의 크기가 고무줄이 아니면 됨
    • 그러면 프로그램을 어느 플랫폼으로 포팅하더라도 큰 문제 없음

고정 폭 정수형

  • <stdint.h> 헤더에 정의되어 있음
  • int8_t / uint8_t
  • int16_t / uint16_t
  • int32_t / uint32_t
  • in64_t / uint64_t
  • C++에서도 뒤늦게 얘내 가져감

_Imaginary, _Complex

  • _Imaginary
    • 허수를 나타내는 키워드
    • 일부 컴파일러는 지원 안 할 수 있음
  • _Complex
    • 복소수를 나타내는 키워드
    • 일부 컴파일러는 다른 이름을 사용할 수 있음
  • 각각 자료형이 세 개씩 존재
    float _Imaginary
    double _Imaginary
    long long _Imaginary
    

float _Complex
double _Complex
long long _Complex


## <complex.h>
- 허수와 복소수와 관련 있는 헤더 파일
- _Bool과 마찬가지로 _Imaginary와 _Complex를 재정의한 매크로를제공
```c
float imaginary
double imaginary
long long imaginary

float complex
double complex
long long complex
  • I
    • 허수부에서 사용하는 i
  • 허수, 복소수와 관련된 유틸리티 함수 제공

IEEE 754 부동 소수점 정식 지원

  • C99의 주요 기능 중 하나
  • float은 IEEE 754 32비트 부동 소수점
  • double은 IEEE 754 64비트 부동 소수점
  • long double은 IEEE 754 확장 정밀도(extended precision) 부동 소수점
  • 사칙 연산과 제곱근의 올림을 IEEE 754에서 정의한대로 처리