경일 메타버스 20220506 5주차 4일 수업내용. C언어 함수, 함수 스택 프레임, 함수 호출 규약, 빌드, 전처리, 전처리기, 매크로, 링크, 함수 포인터
Stack : 정적 할당 영역 ⇒ 함수 ⇒ 이미 얼마나 메모리를 사용해야 할지 알고 있다.
Heap : 동적 할당 영역 ⇒ malloc()
Data : 정적 데이터
Code : 프로그램 명령어
정적 : 한번 정해놓으면 변하지 않고 계속 유지되는 성질
- 동적 : 프로그램 실행 중, 끊임없이 변하는 성질
스택은 할당할수록 메모리 주소가 감소
- 힙은 할당할수록 메모리 주소가 증가
Instructions → Code
bp (base pointer) : 스택의 시작 지점
sp (stack pointer) : 할당 받은 영역 끝
ebp : x86 아키텍처의 bp. 함수가 끝난 후 돌아갈 지점이 된다.
esp : x86 아키텍처의 sp. 함수가 할당 받는 만큼 뒤(아래)로 이동한다.
e : x86, r : x64
호출 규약(Calling Convention)
함수가 어떻게 호출자로부터 인자를 받을 것이며, 결과값을 호출자에게 어떻게 돌려줄 것인지 규정해 놓은 것
인자 전달 / 반환값 받는 것 / 스택 포인터 정리에 대한 규칙
- 함수가 사용하는 메모리 : 스택 (Stack)
함수와 관련된 내용(스택 프레임)
- 얼마나 메모리를 사용할 것인지 ⇒ 임시 데이터
- 매개변수(인자)
- 반환 주소
- 스택 관리는 bp, sp를 이용함.
- bp : 함수가 사용하는 메모리의 시작 주소
- sp : 함수가 사용할 수 있는 메모리의 끝 주소
호출 규약(Calling Convetion)
- 인자를 어떻게 전달할 것이며, 반환값을 어떻게 돌려줄 것인가
x86
- __cdecl : 기본 호출 규약, 인자는 오른쪽부터 왼쪽 순서대로 스택에 넣고, 스택 정리는 호출자가 한다. 가변 인자 사용 가능
- __stdcall : 인자를 오른쪽부터 왼쪽 순서대로 스택에 넣고, 스택 정리는 피호출자가 한다. 가변 인자 사용 불가능
- __fastcall : (가능하다면) 첫 번째 인자와 두 번째 인자를 레지스터에 저장하고, 나머지 인자는 오른쪽에서 왼쪽 순서대로 스택에 넣음. 그리고 스택 정리는 피호출자가 한다.
x64
- 공식적으로 한 가지 호출 규약만 있다.
- 4번째 인자까지 레지스터에 저장하고, 스택 정리는 피호출자가 한다.
// <>는 표준 라이브러리 혹은 써드파티 라이브러리를 포함하는 데 사용한다.
#include <stdio.h>
// ""는 내가(혹은 팀원이) 작성한 헤더 파일을 포함하는 데 사용한다.
#include "MyCode.h"
매크로는 기호 상수처럼 사용할 수 있다.
함수처럼 사용할 수도 있다.
2-1. 함수처럼 사용할 때는 매개변수에 꼭 ()를 사용하자.
매개변수에 #을 붙이면 문자열 리터럴로 인식한다.
매개변수에 ##을 붙이면 문자끼리 잇는다.
가변 인자도 사용할 수 있다.
5-1. 똑같이문자열 리터럴로 인식한다.
5-2. __VA_ARGS__는 ...이 들어갈 곳이다.
#undef를 이용하면 정의했던 매크로를 해제할 수 있다.
MSVC에서 미리 정의한 매크로도 있는데, 그 중 유용한 건
7-1. __FILE__, __LINE__, __DATE__, __TIME__ 정도가 있다.
MSVC에서 미리 정의해 놓은 매크로는 다음과 같다.
#include <stdio.h>
// 매크로는 기호 상수처럼 사용할 수 있다.
#define SPEED_OF_LIGHT 299792458
// 함수처럼 사용할 수도 있다.
// 함수처럼 사용할 때는 매개변수에 꼭 ()를 사용하자.
#define MIN(a, b) ((a) < (b) ? a : b)
// 매개변수에 #을 붙이면 문자열로 인식한다.
#define PRINT(msg) puts(#msg)
// 매개변수에 ##을 붙이면 문자끼리 잇는다.
#define DECLARE_MYTYPE(typename) struct MyType##typename
// 가변 인자도 사용할 수 있다.
// __VA_ARGS__는 ...이 들어갈 곳이다.
#define SIMPLE_PRINT(...) puts(#__VA_ARGS__)
#define FORMAT_PRINT(fmt, ...) printf(fmt, __VA_ARGS__)
int main(void)
{
printf("빛의 속도는 %d m/s이다.\n", SPEED_OF_LIGHT); // 전처리 과정 때 SPEED_OF_LIGHT 매크로는 299792458로 대체된다.
if (MIN(10, 20))
{
printf("10은 20보다 작다.\n");
}
PRINT(안녕하세요);
DECLARE_MYTYPE(Student)
{
int a;
} a;
SIMPLE_PRINT(1, 'a', "Hello");
FORMAT_PRINT("Hello Macro : %d, %c, %s", 12, 'c', "Hello");
return 0;
}
// #undef를 이용하면 정의했던 매크로를 해제할 수 있다.
#include <stdio.h>
#define SPEED_OF_LIGHT 299792458
#undef SPEED_OF_LIGHT // 매크로 해제
int main(void)
{
printf("빛의 속도는 %d m/s이다.\n", SPEED_OF_LIGHT); // 컴파일 오류
return 0;
}
#include <stdio.h>
#include "MyHeaderFile.h"
// 매크로는 기호 상수처럼 사용할 수 있다.
#define SPEED_OF_LIGHT 299792458
// 함수처럼 사용할 수도 있다.
// 함수처럼 사용할 때는 매개변수에 꼭 ()를 사용하자.
#define MIN(a, b) ((a) < (b) ? (a) : (b))
// #을 붙여서 문자열 리터럴로 인식한다.
#define PRINT(msg) puts(#msg);
// 매개변수에 ##을 붙이면 문자끼리 잇는다.
#define DECLARE_MYTYPE(typename) struct MyTape##typename
// 가변 인자도 사용할 수 있다.
// 똑같이 문자열 리터럴로 인식이 된다.
// __VA_ARGS__는 ...이 들어갈 곳이다.
#define SIMPLE_PRINT(...) puts(#__VA_ARGS__)
#define FORMAT_PRINT(fmt, ...) printf(fmt, __VA_ARGS__)
// #undef를 이용하면 정의했던 매크로를 해제할 수 있다.
// #undef SPEED_OF_LIGHT
// MSVC에서 미리 정의한 매크로도 있는데, 그 중 유용한 건
// __FILE__, __LINE__, __DATE__, __TIME__ 정도가 있다.
#define PRINT_ERROR(msg) printf("[%s:%d] %s\n", __FILE__, __LINE__, msg)
// 매크로를 잘 써야 C++도 C#처럼 생산성이 나온다.
int main(void)
{
printf("빛의 속도는 %d m/s이다.\n", SPEED_OF_LIGHT);
if (MIN(10 - 20, 20 < 4))
{
printf("10과 20 중 작은 수는 10입니다.");
}
PRINT(안녕하세요);
DECLARE_MYTYPE(Student)
{
int a;
} a;
SIMPLE_PRINT(1, 'a', Hello);
FORMAT_PRINT("Hello Macro : %d, %c, %s", 12, 'c', "Hello");
printf("FILE : %s\n", __FILE__); // 파일 이름
printf("FILE : %d\n", __LINE__); // 줄 번호
printf("FILE : %s\n", __DATE__); // 날짜
printf("FILE : %s\n", __TIME__); // 시간
if (1)
{
PRINT_ERROR("오류가 발생했습니다.");
}
int a_ = 10;
int b_ = 20;
printf("Add_result = %d", Add(a_, b_));
return 0;
}
// MSVC에서 미리 정의해 놓은 매크로도 있다.
식이 참이라면 #endif까지의 내용을 포함한다.
1-1. #if 류를 썼다면 반드시 #endif로 맺어야 한다.
1-2. defined()를 이용해 매크로가 정의되었는지 체크할 수 있다.
1-3. #else로 #if의 식이 만족하지 않았을 때의 내용을 만들 수 있다.
1-4. 앞에 !를 붙여 부정도 가능하다. : !defined
#if defined를 #ifdef로 줄일 수 있다.
2-1. 마찬가지로 #if !defined를 #ifndef로 줄일 수 있다.
#elif는 else if 쓰는 것과 비슷하다.
#include <stdio.h>
int main(void)
{
// 식이 참이라면 #endif까지의 내용을 포함한다.
#if 1
puts("1. 이건 실행됨");
// #if 류를 썼다면 반드시 #endif로 맺어야 한다.
#endif
// defined()를 이용해 매크로가 정의되었는지 체크할 수 있다.
#if defined(TEST)
puts("2. 이건 실행 안됨");
// #else로 #if의 식이 만족하지 않았을 때의 내용을 만들 수 있다.
#else
puts("2. 이건 실행됨");
#endif
// 앞에 !를 붙여 부정도 가능하다.
#if !defined(TEST)
puts("3. 이건 실행됨");
#endif
// #if defined를 #ifdef로 줄일 수 있다.
#define TEST
#ifdef TEST
puts("4. 이건 실행됨");
#endif
// 마찬가지로 #if !defined를 #ifndef로 줄일 수 있다.
#define T 10
#ifndef TEST
puts("5. 이건 실행 안됨");
// #elif는 else if 쓰는 것과 비슷하다.
#elif defined(T) && T > 2
printf("5. 이건 실행됨");
#else
printf("5. 이건 실행 안됨");
#endif
return 0;
}
각 컴파일러마다 만들어 놓은 전처리기를 실행할 수도 있다.
#pragma를 이용한다.
구현체 정의 동작
MSVC의 전처리기
// A.h
#include "B.h"
struct A { int n; };
// A.h End
// B.h
#include "A.h"
struct B { int n; };
// B.h End
위와 같은 상황이면 서로가 서로를 포함하게 돼 전처리 과정에서 수없이 많은 포함이 생기게 되고, 결국 빌드에 실패하게 된다.
이런 이유 때문에 헤더 인클루드 가드(Header Include Guard)라는 기법을 활용했다.
// A.h
#ifndef __A_H__
#define __A_H__
#include "B.h"
struct A { int n; };
#endif
// A.h End
// B.h
#ifndef __B_H__
#define __B_H__
#include "A.h"
struct B { int n; };
#endif
// B.h End
// A.h
#pragma once
#include "B.h"
struct A { int n; };
#endif
// A.h End
// B.h
#pragma once
#include "A.h"
struct B { int n; };
#endif
// B.h End
함수는 암시적으로 포인터로 변환 됨. ex) Foo;
함수 포인터는 왜 필요한가? => 콜백(callback) 함수 때문
콜백 : 함수 안에서 또다른 함수를 실행하는 것
- 빌드 : 프로그램을 만드는 과정
전처리 : 컴파일 전에 하는 처리
- 조건부 컴파일 / 매크로 대체 / 다른 파일 포함 등
- 컴파일 : 사람의 언어로 작성된 소스 코드를 컴퓨터가 알 수 있는 기계어로 번역
- 링크 : 식별자가 의미하는 메모리 주소를 연결
함수 포인터 pointer to function
함수는 암시적으로 포인터로 변환 됨.
- return-type (*identifier)(parameter-list);
함수 포인터는 왜 필요한가? => 콜백(callback) 함수 때문
- 콜백 : 함수 안에서 또다른 함수를 실행하는 것