배움 저장소

[홍정모의 따라하며 배우는 C언어] 16.전처리기와 라이브러리 본문

Programming Language/C

[홍정모의 따라하며 배우는 C언어] 16.전처리기와 라이브러리

시옷지읏 2021. 12. 9. 00:06

16.1 전처리기가 해주는 일들

전처리기 이름은 컴파일러를 기준으로 만들었다.

Visual Studio에서 "전처리기->컴파일러->링커->실행파일"을 빌드라 부른다. 컴파일이라 부르기도 한다.

 

16.2 전처리를 준비하는 번역 단계

전처리기가 하는일 Translating

Translating
Preprocessing
Compiling
Linking

Executable

  Tranlating을 전처리 이전 단계로 구분하여 보는 관점이 있다. Translating을 전처리 과정에 포함하여 보는 관점이 있다.

 

Deprecated

옛날 키보드는 다양한 문자를 지원하지 않았다. 전처리기가 ?? + 'character" 조합으로 여러 문자를 만들어냈다. 이를 사용하기 위해서 컴파일러 옵션을 설정해줘야 한다.command line에 /Zc:trigraphs 를 입력해주자. 

Trigraph     Replacemnet     Digraph    Equivalent
  ??=            #a            <:          [
  ??/            \             :>          ]
  ??'            ^             <%          {
  ??(            [             %>          }
  ??)            ]             %:          #
  ??!            |
  ??<            {
  ??>            }
  ??-            ~
int arr[3] = { 1,2,3 };
printf("arr??(0??) == %d\n", arr??(0??)); // >>  arr[0]  == 1
printf("arr<:1:> == %d\n", arr<:1:>);     // >> arr<:1:> == 2

참고

 

전처리기가 하는일 1. Two Physical lines vs One Logical Line

가독성을 높이기 위하여 긴 string을 \ 문자를 사용하여 그 다음 열에 표시할 수 있다. 한 줄로 출력된다.

printf("this is very very very very very very very very very \
very very very very very very very long sentence.\n");

 

전처리기가 하는일 2. Tokens

 입력한 코드를 Token으로 쪼갠다. 각 코드를 space, tabs, line breaks, white space character를 기준으로 구분한다. 코드를 해석하기 위해 구분된 token이 필요하다.

int/*a variable to count a number */n = 1;

 

위 코드는 주석을 빈칸으로 바꾼다.

 

16.3 #define 매크로(전처리 지시자)

Preprocessor directive

Preprocessor directives begins with # symbol at the beginning of a line.

Macro
- An instruction that represents a sequence of instructions in abbreviated form.

- 전처리지시자는 #으로 시작한다.

- 여러 명령어를 하나로 축약해놓으면 매크로이다.

Preprocessor directive Macro(name) body(or replacement list)
#define SAY_HELLO printf("Hello, World!");
             Macro -> Body로 대체하는 과정을 Macro expansion이라 한다.

 

 

Object-like macros vs Function-like macros

#define ONE 1
#define SQUARE(X) X*X

전처리기는 SQUARE(X)를 모두 X*X로 대체한다.

 

 

Symbolic constant 기호적 상수

#define PI 3.141592 // Symbolic constants or manifest constants

#define GRAVITY 9.8

#define MESSAGE "The greatest glory in linving lies not in never falling, \
but in rising every time we fall."

#define THREE 3
#define NINE THREE*THREE

#define FORMAT "Number is %d.\n"

다음은 C++에서 허용되는 문법이나 C에서는 clang에서만 허용된다. C++은 문법을 더 유용하게 적용하고 있다.

C에서는 전처리 지시자를 사용하자.

const int LIM = 50;   // Not work in GCC, MSVC
static int arr1[LIM]; // Only work in clang and C++

#define LIMIT 20
static int arr1[LIMIT];

 

  문자열, 문자 모두 사용할 수 있다. 이때 문자열은 끝에 '\0'이 추가됨을 이해하자.

#define MY_CHA 'Z'
#define MY_STR "Z" // { 'Z', '\0' }

 

  중복해서 정의할 수 있다.

#define SIX 2*3
#define SIX 2*3

 

define의 범위

- 기본적으로 file 범위이다. 다른 파일에 정의되어있는 define과 충돌하지 않는다.

- header 파일에 define이 정의되어 있고 c 파일에서 해당 해더를 불러온 상황을 가정하자. header파일의 코드가 모두 c파일에 복사된다. 이때 c 파일에서 header에 정의된 define을 사용할 수 있다.

- 한 c 파일에서 특정 header file을 여러 번 불러오기에 위와 같이 중복해서 정의되는 상황이 나온다.

 

이 때 문제가 될 수 있는 상황을 보자. 다음 코드이다.

#define SIX 2*3
#define SIX 2 * 3

위 SIX에 있는 띄어쓰기는 컴파일러 마다 다르게 해석할 수 있다. 위에 있는 SIX와 아래에 있는 SIX가 다르기에 컴파일러는 Warning message를 준다. 이때 의도적으로 redefine을 할 수 있다.

#define SIX 2*3
#undef SIX
#define SIX 2  *  3

 

매크로 함수에 덧붙인 Semicolon( ; )

#define SAY_HELLO for(int i=0;i<5;++i) printf("Hello there?\n");
SAY_HELLO

 통합환경이 Macro 끝에 붙어있는 ;를 인식하지 못한다. 그 다음줄에 코드를 입력하면 자동으로 Tap을 넣어준다. 따라서 Macro 함수를 사용할 때 ;를 넣어주면 줄바꿈 처리를 편리하게 할 수 있다.

 주의! Semicolon ; 을 넣어주면 에러가 발생하는 경우가 있다. 무조건 적으로 Semicolon ; 을 붙이지 말자.

 

문자열 내에있는 매크로는 대체되지 않는다!

 

 

16.4 함수 같은 매크로

  함수 매크로를 사용할 때 주의할점

1. compiler는 매크로를 확인할 수 없다. 사용하지 말자 되도록

2. 매크로 이름에 빈칸을 넣지말자. name과 body로 구분해버린다.

3. 괄호 ( )를 잘 사용하자

4. 관습적으로 Macro name은 대문자를 사용한다.

 

자주 사용하는 Macro 예제

#define MAX(X,Y) ((X) > (Y) ? (X) : (Y))
#define MIN(X,Y) ((X) < (Y) ? (X) : (Y))
#define ABS(X) ((X) < 0 ? -(X):(X))

 

주의 괄호를 잘 사용하지 않았을 때 매크로 함수는 의도하지 않은 결과를 만든다.

#define ADD_O(X,Y) ((X)+(Y))
#define ADD_X(X,Y) X+Y
#define SQUARE_O(X) ((X)*(X))
#define SQUARE_X(X) X*X

	int sqr = SQUARE(3);

	printf("ADD_O %d \n", 2 * ADD_O(1, 3));         // 2 * X + Y = WRONG!!
	printf("ADD_X %d \n", 2 * ADD_X(1, 3));         // 2 * (X+Y) = CORRECT!
									      
	printf("SQUARE_X %d\n", SQUARE_X(1 + 2));       // X+Y*X+Y =  WRONG!!
	printf("SQUARE_X %d\n", 100 / SQUARE_X(3 + 1)); // 100 / X + Y * X + Y = // WRONG!!
	
	int a = 1;
	printf("SQUARE_X %d \n", SQUARE_X(++a));        // ++a * ++a = 3 * 3 = 9 // DANGEROUS

 

Stringizing Operator #

- converts macro parameters to string literals

매크로 선언 내부에 있는 "# + 변수"를 매크로 함수 Parameter로 교체한다

#define SQAURE(X)     ((X)*(X))
#define PRINT_SQR1(X) printf("The square of %d is %d. \n",X, SQAURE(X))
#define PRINT_SQR2(X) printf("The square of " #X " is %d. \n", SQAURE(X))

PRINT_SQR1(5);
PRINT_SQR2(5);

Output

The square of 5 is 25.
The square of 5 is 25.

 

## Operator

  ##Operator combines two tokens into a single token

  매크로 함수에서 사용하는 변수( N )을 매크로에 활용하기 위한 format specifier로 생각하자.  #n와 x##n처럼 쓴다.

#define XNAME(n)  x ## n
#define PRT_XN(n)printf("Macro func, x" #n " = %d\n", x ## n); 
int XNAME(1) = 1;
int XNAME(2) = 2;
printf("x1 = %d \n", x1); // >> x1 = 1
printf("x2 = %d \n", x2); // >> x2 = 2
PRT_XN(1);                // >> Macro func, x1 = 1
PRT_XN(2);                // >> Macro func, x2 = 2

 

16.5 가변 인수 매크로

1. ellipsis(...)는 여러 Parameter를 넣을 때 사용한다.

2. __VA_ARGS__는 C 언어에서 미리 정의되어 있는 변수다. ellipsis와 짝지어 사용하며 ellipsis에 입력된 값을 가져온다.

// ... : ellipsis
// __VA_ARGS__ : one of the predefined macros.
#define PRINT(X, ...) printf("Message " #X ":" __VA_ARGS__)

PRINT(1, "Your number is %d \n", 1);
PRINT(2, "Your number is %d, %d \n", 1, 2);
PRINT(2, "This is first message \t", "This is Second message."); // WRONG

 

참고) 함수에서 Variadic arguments가변인수를 사용하는 방법은 이후 강의에서 등장한다.<stdarg.h>를 포함하면 된다.

 

16.6 #include와 헤더 파일

표준라이브러리로 정의된 Header는 < >를 사용한다.  <header.h>

Solution -> Properties에서 특정 폴더를 추가할 수 있다. 전처리기가 해당 폴더 내 header를 자동으로 탐색한다.

이때 특정 폴더 내부에 있는 header는 Library 혹은 Module로 표시하기위하여 <header.h> 문법을 허용한다.

 

 

여러 Header파일을 활용하는 방법

 일반적으로 헤더 파일에 변수를 선언한다. c파일에 헤더에서 정의된 변수에 값을 할당한다.

참고) 매크로 함수나 C++의 템플릿의 정의는 헤더파일에 들어간다.

- static varaiable은 파일 스코프를 가진다. 따라서 functions.h가 포함되어 있는 파일에서 사용가능하다.

- static function은 header파일에서도 body를 가질 수 있다. 아래 multiply( ) 함수를 참고하자.

 

 functions.h

#pragma once
static int si = 0;
static int multiply(int a, int b)
{
	return a * b;
}

inline int subtract(int a, int b)
{
	return a - b;
}


extern int status;
void print_status();
void print_address();

// Basically function is extern, you can skip 'extern' keyword
extern int add(int a, int b);

 

functions.c

  c파일에 정의를 적는다.헤더 파일에 선언을 적는다. 

#include "functions.h"
#include <stdio.h>

int status = 0;
int add(int a, int b)
{
	return a + b;
}
void print_status()
{
	printf("\"func.c\" Address = %p, Value = %d \n", &status, status);
}
void print_static_address()
{
	printf("print_address()\n");
	printf("Static function address %p\n", multiply);
	printf("Static variable address %p\n", &si);
	printf("Static variable val     %d\n", si);
	si = 5;
	printf("Static variable val     %d\n", si);
}

 

main.c

  function.c에 정의되어있는 status variable을 main.c에서 사용할 수 있다.

#include "functions.h"
int main()
{
	/*printf("You can use status variable in main \n");
	printf("\"main.c\" Address = %p, Value = %d \n", &status, status);*/

	printf("main()\n");
	printf("Static function address %p\n", multiply);
	printf("Static variable address %p\n", &si);
	printf("Static variable val     %d\n", si);
	si = 2;
	printf("Static variable val     %d\n", si);

	print_static_address();
}

Output

"main.c" Address = 00E8A138, Value = 0
"func.c" Address = 00E8A138, Value = 0

 

main.c 내부에서 다음을 실행해보자.

	printf("main()\n");
	printf("Static function address %p\n", multiply);
	printf("Static variable address %p\n", &si);
	printf("Static variable val     %d\n", si);
	si = 2;
	printf("Static variable val     %d\n", si);

	print_static_address();

Output

main()
Static function address 00BC18E0
Static variable address 00BCA144
Static variable val     0
Static variable val     2
print_address()
Static function address 00BC19C0
Static variable address 00BCA13C
Static variable val     0
Static variable val     5

1. 위 결과값을 통해 static 함수, 변수가 2개 존재함을 알 수 있다.

2. static 변수는 파일 스코프이고 functions.h 내부에 static 변수가 정의되어 있다. 따라서 functions.h를 가져오는 c파일에 static 함수,변수가 모두 복사되고 각각의 값을 가진다.

3. main.c에서 fuctions.h를 포함하였고 function.c에서 functions.h를 포함하였으므로 static 함수 변수는 2개 존재한다.

 

header에서 Static 없이 body를 가지는 함수는 어떤 문제를 가지고 있나?

 static을 붙이지 않은 일반 함수 바디가 header파일 내에 정의된 경우이다. 이 때 모든 c파일에 함수 body가 복사되어 중복정의 하게된다. 에러를 발생시킨다.

Header Guard 헤더가드 (전처리지시자)

  A.h 내부에서 B.h를 포함하고 main.c에서 A.h, B.h를 포함하는 상황이다. main.c는 B.h를 두번 include한다.

이를 방지할 수 있는 #pragma once와 #ifndef이다.헤더파일 안에 포함하여 주면 된다. #pragma once를 추천한다.

#pragma once

// A.h
#pragma once

 

#ifndef #define #endif

// A.h
#ifndef __FUNCTIONS__
#define __FUNCTIONS__

// Do sth ( ... )
// this code only included once

#endif

 

16.7 조건에 따라 다르게 컴파일하기

다음 전처리 지시자를 활용하여 컴파일 때 조건을 바꾸어줄 수 있다.

#define, #undef, #if, #ifdef, #ifndef, #else, #elif, #endif

 

전처리 지시자 #undef의 쓰임

  undef하면 macro가 사라진다. 정의되지 않은 macro를 undef 할 수 있다.

#define LIMIT 400
//#undef LIMIT      // Error! 'LIMIT': undeclared identifier

#undef NON_DEFINED
int main()
{
	printf("%d\n", LIMIT);
}

 

조건문 사용하기 ( #ifndef, #define, #endif ) 사용하기

  Empty Macro : #define은 값 없이 이름만 선언할 수 있다. 아래에 Empty Macro, STATE_1, STATE_2를 선언하였다.

 참고) static 함수는 header에서 정의할 수 있다.

 

Header

// state1.h
#ifndef STATE_1
#define STATE_1

#include <stdio.h>
static void func()
{
	printf("func() in the state1.h\n");
}
#endif


// state2.h
#ifndef STATE_2
#define STATE_2

#include <stdio.h>
static void func()
{
	printf("func() in the state2.h\n");
}
#endif

 

 

main.c

#define TYPE 3
#if TYPE == 1
	#include "state1.h"
#elif TYPE == 2
	#include "state2.h"
#else
void func()
{
	printf("func() in the main\n");
}
#endif
int main()
{
	func();
}

 

Preprocessor Definitions

  아래 예제에서 REPORT가 정의되어 있냐 있지 않냐에 따라 함수의 구현 부분이 달라진다.

//#define REPORT
int sum(int i, int j)
{
	int sum = 0;
	for (int k = i; k <= j; ++k)
	{
		sum += k;
#ifdef REPORT
		printf("sum=%d added num = %d\n", sum, k);
#endif
	}
	return sum;
}

int main()
{
	printf("sum(%d, %d) = %d \n", 4, 5, sum(0, 3));
}

위 와 같이 직접 Uncomment/Comment로 설정하지 말고, Visual Studio가 지원하는 전처리 지시자 명령어를 사용하자.

 

아래 설정에서 명령어 입력 끝에 꼭 Semicolon";"을 입력해주자

참고) 뒤에 WIN32, _DEBUG를 보자. x86/x64 모드, relesase/debug를 전처리기가 설정함을 알 수 있다. 이를 활용해보자 REPORT 명령어를 지우고 다음 코드를 release/debug 모드에서 각각 실행해보자.

int sum(int i, int j)
{
	int sum = 0;
	for (int k = i; k <= j; ++k)
	{
		sum += k;
#ifdef _DEBUG
		printf("sum=%d added num = %d\n", sum, k);
#endif
	}
	return sum;
}

debug 모드에서만 함수내 printf( )가 작동함을 알 수 있다.

 

이를 이용하여 OS 조건에 맞추어 다른 출력값을 만들어보자.

void say_hello()
{
#ifdef _WIN64 // #if defined(_WIN64)
	printf("Hello, WIN64\n");
#elif _WIN32
	printf("Hello, WIN32\n");
#elif __linux__
	printf("Hello, Linux\n");
#endif
}

int main()
{
	say_hello();
}

전처리기는 항상 OS 환경에 맞는 변수를 전처리 지시자로 선언한다. 따라서 위와 같은 함수는 언제나 작동할 것이다.

이때 #ifdef 대신에 #if defined( )를 사용할 수 있다.

 

 

16.8 미리 정의된 매크로들, #line, #error

이 때 __DATE__와 __TIME__은 Compile 시간을 기준으로 결정됨을 유의하자. 프로그램 실행마다 달라지지 않는다.

int main()
{
	printf("__FILE__ : %s \n", __FILE__); // path of this file
	printf("__DATE__ : %s \n", __DATE__); // when preprocessing
	printf("__TIME__ : %s \n", __TIME__); // when preprocessing
	printf("__LINE__ : %d \n", __LINE__); // current code's line num
	printf("__func__ : %s \n", __func__); // function belong to
}

Output

__FILE__ : C:\Users\SJ\source\repos\My\My\main.c
__DATE__ : Dec  8 2021
__TIME__ : 16:19:49
__LINE__ : 11
__func__ : main

 

main( ) 밖 다른 함수를 정의하고 main 내부에서 실행시켜보자

void different_function()
{
	printf("In different_function\n");
	printf("__LINE__ : %d \n", __LINE__); // current code's line num
	printf("__func__ : %s \n", __func__); // function belong to
}
int main()
{
	different_function();
}

Output

In different_function
__LINE__ : 8
__func__ : different_function

 

다른 header에서 정의된 함수를 main 내부에서 실행시켜보자

// DifferentFile.h
#pragma once
#include <stdio.h>
static void different_func_different_file()
{
	printf("In different_func_different_file\n");
	printf("__FILE__ : %s \n", __FILE__); // path of this file
	printf("__LINE__ : %d \n", __LINE__); // current code's line num
	printf("__func__ : %s \n", __func__); // function belong to
}

// main.c
#include "DifferentFile.h"
int main()
{
	different_func_different_file();
}

Output

In different_func_different_file
__FILE__ : C:\Users\SJ\source\repos\My\My\DifferentFile.h
__LINE__ : 7
__func__ : different_func_different_file

 

그외 미리 정의된 메크로

printf("__STDC__ %d\n", __STDC__);  
printf("__STDC__HOSTED__ %d\n", __STDC_HOSTED__); // hosted vs freestanding, hosted take restricted standard c syntax
printf("__STDC__HOSTED__ %ld\n", __STDC_VERSION__);

Output

__STDC__ 1
__STDC__HOSTED__ 1
__STDC__HOSTED__ 201112

 

#line, #error

#line 전처리 지시자는 코드가 속한 줄을 임의적으로 바꾼다. 파일명도 바꿀 수 있다.

 이 때 line 1에서 첫 번째의 기준은 그 다음 코드부터이다. print( ) 함수가 실행되는 곳이 첫 줄이 된다.

#error 전처리 지시자는 Error message를 출력하며 #if ~ #endif 와 함께 쓰인다. 4를 다른 숫자로 바꾸어보자->에러

#line 50
	printf("__LINE__ : %d \n", __LINE__);

#line 1 "Hello.txt"
	printf("__FILE__ : %s \n", __FILE__);
	printf("__LINE__ : %d \n", __LINE__);
    
#if __LINE__ != 4
#error Not line 4
#endif
__LINE__ : 50
__FILE__ : Hello.txt
__LINE__ : 2

 

#error 전처리 지시자를 다음처럼 사용할 수 있다.

  x64 환경에서만 동작한다. 아닐경우 error message를 출력한다.

int main()
{
  //#if defined(_WIN64) != 1
  #ifndef _WIN64
  #error NOT WIN64 Platform
  #endif
}

// or

int main()
{
  #if __STDC_VERSION__ != 201112L
  #error Not C11
  #endif
}

 

 

16.9 #pragma 지시자

Pragma directives

- #Pragma는 전처리기가 아닌 컴파일러와 상호작용한다. 

- 컴파일러마다 다른 directives를 사용할 수 있다. MSVC는 아래와 같다.

https://docs.microsoft.com/en-us/cpp/preprocessor/pragma-directives-and-the-pragma-keyword?view=msvc-170 

 

Pragma directives and the __pragma and _Pragma keywords

Describes the pragma directives available in Microsoft Visual C and C++ (MSVC)

docs.microsoft.com

 

아래는 stdio.h 내부에 정의되어 있는 Header Guard이다.

두 가지 Header Guard 모두 다 사용하고 있다. #pragma once 와 #define _INC_STDIO 이다. 하나만 사용해도 된다.

// stdio.h
#pragma once
#ifndef _INC_STDIO // include guard for 3rd party interop
#define _INC_STDIO
//...
#endif // _INC_STDIO

 

#pragma pack

몇 byte를 기준으로 padding을 실행할지 정한다.

//#pragma pack(1) // padding is done base on 1byte
struct s
{
	int i;    //   4byte
	char ch;  //   1byte
	double d; //   8byte
};            //+= 13byte

int main()
{
	struct s init_s;
	printf("Size of init_s is %zd", sizeof(init_s));
}

Output

// When #pragma pack(1) is commented
>> Size of init_s is 16

When #pragma pack(1) is Uncommented
>> Size of init_s is 13

 

이때 #pragma pack(1)을 _Pragma( )함수로 바꾸어줄 수 있다. 결과는 동일하나 _Pragma는 MSVC에서 미지원이다.

_Pragma("pack(1)") // destringizing : remove first and last quotation mark("),
                   //               : convert black slash qutation(\") -> qutation(")

    #define 매크로를 사용해 위 P_Pragma를 정의할 수도 있다.

#define PACK1 _Pragma("pack(1)")
PACK1

 

#pragma warning을 사용하여 warning message 해제하기

//#pragma warning( disable:4477 )
//#pragma warning( disable:6274 )

//#pragma warning( error:4477 )
//#pragma warning( error:6274 )
struct s
{
	int i;    //   4byte
	char ch;  //   1byte
	double d; //   8byte
};            //+= 13byte

int main()
{
	printf("intended warning is came out {%c}\n", 4.2);
}

위 #pragma warning( disable : {code} )를 입력하면 해당 warning message 알림을 해제한다.

이 때 disable 대신에 error를 입력하면 해당 code가 error message로 변경되고 컴파일할 수 없다.

 

16.10 _Generic 표현식

1. 자료형에 따라 다른 값을 할당할 수 있는 전처리 지시자이다. 

2. generic을 사용하려면 C11을 지원하는 컴파일러를 사용하자. _Generic을 넣어주면 된다.

3. MSVC에서 지원하지 않는다. repl에서 c를 사용하자.

 

사용방법

아래 MYTYPE 함수는 argument의 자료형에 맞는 값을 출력한다. 값으로 string, expression 두 형태 모두 가능하다.

#define MYTYPE(X) _Generic((X),\
  int: "int",\
  float: "float",\
  double: (X+100),\
  long : "long",\
  int* : "int*",\
  default:"other"\
)

int main()
{
  int i = 5;
  double d = 5.5;
  printf("%s \n", MYTYPE(i));  // >> int 
  printf("%s \n", MYTYPE(&i)); // >> int* 
  printf("%f \n", MYTYPE(d));  // >> 105.500000
}

 

16.11 inline 함수

- inline keyword를 사용하면 overhead 없이 코드 자체를 복사 붙여넣기 해준다. 따라서 함수의 주소는 존재하지않는다.

- 작은 함수가 반복적으로 실행될 때 실행속도를 빠르게 하기위해 사용하는 keyword

Function call has overhead
- set up the call, pass arguments, jump to the function code, and return.

inline function specifier
- Suggests inline replacements.

Inline functions should be short.
- A function with internal linkage can be made inline. (GCC, clang)
 in this case, you can't take it's address

함수를 호출할 때 생성되는 Overhead는 다음과 같다.

- 함수를 호출하고 이후 Stack에서 제거하기 위한 overhead

- Arguments 복사

- 함수의 선언과 정의가 각각인 경우 이를 풀기위한 overhead

- 값을 반환하기 위한 overhead

따라서 함수를 호출할 때 필요한 Overhead보다 함수의 body가 더 간단하다면 inline 함수를 사용함이 유리하다.

 

예제를 보자

  아래파일이 inline으로 실행되는지 알아보자

static inline int foo() // msvc allow dec inline function without static
{                       // But clang and gcc needs 'static' for inline func
	return 5;
}

int main()
{
	int ret;
	ret = foo();
}

 

아래 옵션에가서 default를 Disabled로 설정해주고 debugging하자.

ret = foo( ) 에서 break(F5)를 걸고 Alt+8으로 assembly로 확인해보자.

 

 

위 옵션에가서 Only _inline을 설정해주면 call foo가 사라진다

아래는 foo()함수대신에 return 되는 값인 5만 대입해주고 있음을 알 수 있다. 왜인지 모르겠지만 내 컴퓨터에선 안된다. 

 

inline 함수의 사용

  inline 함수는 header 내부에서 사용함이 일반적이다. 함수 body를 다른 곳에서 찾는 overhead를 줄인다. static이라 헤더에서 정의할 수 있다. header에서 부른 함수 내 코드를 호출 위치에 곧바로 붙여넣기 하면 된다.

#pragma once
#include <stdio.h>
inline static int func_inline()
{
	return 123;
}

inline은 컴파일러에게 요청할 수 있지만 inline을 강제할 수는 없다. 남발하여 사용함은 좋지 않다.

 

16.12 라이브러리

Library의 특징 : main( ) 함수가 없다.

OS가 프로그램을 실행하면 main 함수를 실행하고 그 안에 있는 코드를 차례로 작동시킨다.

Library는 다른 함수를 도와주는 역할을 하며 main 함수에서 호출된 변수나 함수를 만든다.

 

Solution 내부에 새로운 Project를 만들어주자

// Library.h
#pragma once
void say_hello();
void say_world();


// Library.c
#include "Library.h"
#include <stdio.h>
void say_hello()
{
	printf("Hello \n");
}

void say_world()
{
	printf("World \n");
}

이 때 Library를 Static library(.llb)로 빌드하면 다른 Project에서 사용할 수 있다.

참고)  Dynamic Library(.dll) : 프로그램 실행 때 가져다 쓴다.

         Static Library(.llb)     : 프로그램 내부에 포함되어 있다.

Project에서 Build를 눌러주자. 아래 그림에서 보듯 Library.lib 파일이 생성되었다. Release/Debug 모두 해주자.

 

위 라이브러리를 사용해보자

  1. Header 파일로 포함하기

    아래 설정을 해주면 #include "..."에서 해당 폴더로 찾아가지 않고 파일 명으로만 헤더파일을 찾을 수 있다.

이 상태에서 아래 코드를 컴파일하면 LINK Error가 생긴다. Build된 .lib 파일을 Visual studio가 Linking 과정에 넣게하자

#include <stdio.h>
#include "../Library/Library.h"
int main()
{
	say_hello();
}

 

2. Build 된 .lib 파일을 포함하기

Linker -> Input -> Additional Dependencies에서 필요한 .lib 파일을 입력해주어야한다.

이후 실행하면 성공적으로 사용할 수 있다.

 

16.13 표준 수학 라이브러리

math.h를 다음과 같이 사용해볼 수 있다.

#include <math.h>
int main()
{
	printf("%f \n", cos(3.141592));

	// double's precision is much higher
	double c = 5.0, b = 4.0, a;
	a = sqrt(c * c - b * b);
	printf("double a = %f \n", a);  // >> double a = 3.000000

	float cf = 5.0f, bf = 4.0f, af;
	af = sqrtf( cf * cf - bf * bf );
	printf("float af = %f\n", af);  // >> float af = 3.000000
}

 

Type Variants : _Generic을 이용하여 입력값의 자료형에 따라 다른 함수를 호출해보자

  아래 코드는 MSVC에서 작동하지 않는다. _Generic을 지원하는 컴파일러를 사용하자.

#include <stdio.h>
#include <math.h>
#define SQRT(X) _Generic((X),\
  long double: sqrtl,\
  default: sqrt,\
  float: sqrtf)(X)

int main()
{
  double d1 = SQRT(2.0f); // sqrtf()
  double d2 = SQRT(2.0);  // sqrt()

  if(d1 == d2)
    printf("Identical\n");
  else
    printf("Not identical\n");
}

d1은 float d2는 double이다. 정밀도(precision)가 다르기 때문에 둘은 같지 않다.

Not identical

 

_Generic( ) 기능이 구현된 Library 사용하기 <tgmath.h>

double sqrt(double, double) 함수 사용

//include <tgmath.h>
  double tgmath_d1 = sqrt(2.0f); // take double return double
  double tgmath_d2 = sqrt(2.0);  // take double return double
  if(tgmath_d1 == tgmath_d2)
    printf("Identical\n");
  else
    printf("Not Identical\n");

  double tgmath_diff_d1 = sqrt(1.0f/3.0f); // take double return double
  double tgmath_diff_d2 = sqrt(1.0/3.0);  // take double return doubl
  if(tgmath_diff_d1 == tgmath_diff_d2)
    printf("Identical\n");
  else
    printf("Not Identical\n");

Output

Identical
Not identical

2.0은 동일하게 출력되지만 1.3333333333333333은 정밀도에 따라 값이 달라지므로 두 값은 다르다.

 

(double)과 2.0 (float) 2.0.f를 비교해보자

  여기서 <tgmath.h>을 포함시키면 위 sqrt 함수가 입력되는 자료형에 맞는 함수를 호출한다. 이 때 출력값은

Not identical
Not identical

(double) 2.0과 (float) 2.0f를 구분함을 알 수 있다.

 

16.14 표준 유틸리티 라이브러리

  atexit( (void*)func_ptr ) 함수

  함수 포인터를 입력받아 내부의 리스트에 저장한다. 프로그램이 끝날 때 해당 함수를 실행한다.

입력값에 따라 달라짐을 알 수 있다. 따라서 런타임에 결정된다.

#include <stdlib.h>
void goodbyte(void)
{
	printf("Goodbyte\n");
}
void thankyou(void)
{
	printf("Thank you");
}

int main()
{
	printf("Purchased?\n");
	if (getchar() == 'y')
		atexit(thankyou);   // call this function at end
	while (getchar() != '\n');

	printf("Goodbyte message?\n");
	if(getchar() =='y')
		atexit(goodbyte);   // call this function at end
}

 

_Noreturn 자료형

_Notreturn 자료형이 반환되는 함수로 정의하여 사용한다. 해당 함수가 호출될 때 프로그램이 반드시 종료된다.

- <stdnoreturn.h>가 필요하다.

- MSVC에서 지원하지 않는다. Clang이나 GCC를 사용하자.

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h> // specifying exiting at return type

// causes undefined behavior if i <= 0
// exits if i>0
_Noreturn void stop_now(int i)
{
  if(i>0) exit(i);
}

int main(void)
{
  puts("Preparing to stop ... ");
  stop_now(2);
  puts("This code is never executed.\n");
}

 

16.15 assert 라이브러리

사용방법

  1. <assert.h>를 포함한다.

  2. 반드시 만족해야 하는 조건을 assert( )에 Arguemnts로 던진다.

  3. Debug 모드로 설정한다. Release 모드에서 assert( ) 코드는 포함되지 않는다.

 

아래 코드를 debug / x64 환경에서 실행해보자. 에러가 발생한다.

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

int divide(int, int);

int main()
{
    int a = 5;
    int b = 0;
    printf("your input is %d, %d\n", a, b);
    printf("a / b = %d \n", divide(a, b));
    return 0;
}

int divide(int a, int b)
{                    // Very useful for debugging
    assert(b != 0); // if this meet true, print this line on console

    //if (b == 0) // this if statements add overhead on this function
    //{
    //	printf("%i is divided by Zero\n", a);
    //	exit(1);
    //}
    return a / b;
}

 

Retry를 클릭해보자

assert가 발생한 지점으로 이동한다.

 

_Static_assert( ) 사용하기

  Static assert( )는 Compile Time에 해당 오류를 잡아준다. MSVC에서 지원하지 않는다.

아래 코드에서 CHAR_BIT = 8로 고치면 잘 작동함을 확인할 수 있다. 다양한 플랫폼을 지원하는 프로그램을 제작한다면 아래 코드가 유용하다.

참고) C++에서 static assert 사용이 가능하다.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <assert.h>
#include <limits.h>
_Static_assert(CHAR_BIT==9, "9-bit char assumed"); // CHAR is always 8bit

int main(void)
{
    printf("work! \n");
    return 0;
}

 

16.16 memcpy()와 memmove()

memcpy( ) 함수는 string.h 내부에 있다. 문자열 복사도 가능하지만 메모리 복사에 많이 사용하고 있다. 

 

memcpy( ) vs memmove( )

  memmove( )는 내부에서 buffer를 사용한다. memcpy( )의 Source와 Desti가 동일한 객체 내부에서 실행될 때 값을 가져오고 붙여넣으면서 생길 수 있는 에러를 방지해준다.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // memcpy(), memmove()

#define LEN 6

void print_Array(int* arr, int n)
{
	for (int i = 0; i < n; ++i)
		printf("%d ", arr[i]);
	printf("\n");
}
int arr1[LEN] = { 1, 3, 5, 7, 9, 11 };

int* arr2 = (int*)malloc(LEN * sizeof(int));
if (arr2 == NULL)exit(1);

//for (int i = 0; i < LEN; ++i)
//	arr2[i] = arr1[i];
memcpy(arr2, arr1, sizeof(int) * LEN);
print_Array(arr2, LEN);

// below code, memmove do this
// { 1, 3, 5, 7,  9, 11 }
//         ^  ^   ^  ^
// { 5, 7, 9, 11, 9, 11 }
//   ^  ^  ^  ^
// this is undefine behavior makes error
// memcopy(arr1, &arr1[2], sizeof(int) * 4);

// memmove use buffer it block some undefined behavior
memmove(arr1, &arr1[2], sizeof(int) * 4);
print_Array(arr1, LEN);

 

16.17 가변 인수

- 가변 인수를 코드에 사용하지 말자. debugging이 힘들다. 저수준(하드웨어와 가까운)으로 코딩할 때,  운영체제를

  코딩할 때, 프로그래밍 언어를 만들 때 필요하다.

Variable Arguments
 - int printf(char const* const _Format, ...);

 1. Provide a function prototype using an ellipsis

void   vaf1(int n, ...);                // OK
int    vaf2(const char* s, int k, ...); // OK  
char   vaf3(char c1, ..., char c2);     // Not OK, ellipsis should be the last.
double vaf4(...);                       // Not OK, no parameter.

 2. Create a va_list type variable in the function definition
 3. Use a macro to initialize the variable to an argument list.
 4. Use a macro to access the argument list.

  vaf4( ... )는 다른 Argument 없이 ellipsis가 단독으로 사용되고 있다. 이를 사용할 수 없다.

  다른 Argument의 주소를 기준으로 ellipsis(가변인수)의 주소를 확인할 수 있기 때문이다. 

  함수에 가변 인수를 넣기 위해서 를 사용하자.

 

사용예제

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h> // for Variable Arguments

double average(int, ...);

int main()
{
	double a, b;
	a = average(3, 1.1, 2.2, 3.3);
	b = average(6, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6);
}

double average(int num, ...) // ellipsis argument should be last
{
	va_list ap;
	double sum = 0.0;
	int i;

	// va_start's second arguments is ellipsis's post argument.
	// if average(int num1, int num2, ...) num2 is proper
	va_start(ap, num); // macro
	for (int i = 0; i < num; ++i)
		sum += va_arg(ap, double); // you must to know what type of ellipsis takes
	va_end(ap);

	return sum / (double)num;
}

 

string과 함께 사용하기

double average_d(char* format_string, ...);

int main()
{
	double a, b;
	
	a = average_d("dd", 1.1, 2.2, 3.3);
	b = average_d("ddd", 1.1, 2.2, 3.3, 4.4, 5.5, 6.6);

	printf("%lf\n", a);
	printf("%lf\n", b);
}

double average_d(char* format_string, ...)
{
	int num = strlen(format_string);

	va_list ap;
	double sum = 0.0;
	int i;

    // va_start's second arguments is ellipsis's post argument.
    // if average(int num1, int num2, ...) num2 is proper
	va_start(ap, format_string);
	for (int i = 0; i < num; ++i)
		sum += va_arg(ap, double);
	va_end(ap);

	return sum / (double)num;
}

 

참고자료

https://en.cppreference.com/w/c

 

C reference - cppreference.com

Null-terminated strings:    byte  −   multibyte  −   wide

en.cppreference.com

 

Comments