책정리/혼자 연구하는 C,C++ 1

16장 함수 고급

GONII 2015. 2. 20. 18:38

16.1 호출 규약

16.1.1 스택

호출 규약(Calling Convention)이란 함수를 호출하는 방식에 대한 일종의 약속

인수는 어떻게 전달하며 리턴값은 어떻게 반환하고 인수 전달을 위해 사용한 메모리는 누가 정리할 것인지 등을 규정

호출 규약을 이해하기 위해서는 스택에 대해 알아야 하며 스택은 기계어 수준에서 동작하기 때문에 어셈블리 언어에 대한 개념도 필요

스택(Stack)은 시스템이 사용하는 메모리 공간

CPU가 임시적인 정보를 저장할 필요가 있을 때 이영역을 사용

만약 힙과 스택이 만나게 되면 메모리가 부족한 상태가 됨

스택에 값을 저장하는 동작을 push(민다)라고 하며 저장된 값을 빼내는 동작을 pop(당긴다)라고 함

스택에 저장된 값들을 LIFO(Last In First Out)의 원칙에 따라 가장 최후에 들어간 값이 가장 먼저 나옴

  • 인수도 함수 호출 중에만 생명이 유지되는 일종의 지역변수이다. 인수의 초기화 시점은 함수가 호출될 대 이며 함수 내부에서만 통용된다.
  • 지역변수를 많이 선언하는 것과 함수의 실행 속도와는 직접적인 상관이 없다. 지역변수 영역은 esp를 필요한 양만큼 감소시켜 생성하는데 10을 빼나 20을 빼나 연산 속도는 일정하다.
  • 지역 변수를 많이 쓴다고 해서 프로그램이 커지는 것도 아니다. 어차피 지역변수는 스택에 임시적으로 생겼다가 함수가 끝나면 사라지므로 프로그램의 크기와는 무관하다.
  • 지역변수를 위해 esp를 위로 올려 공간만 만들 뿐이므로 별도의 초기식이 없으면 지역변수는 초기화되지 않는다. 이 때 원래 공간에 들어있던 값이 바로 쓰레기값이다. 지역변수를 초기화하면 이 때는 초기화하는 시간만큼 느려지고 필요한 코드만큼 프로그램의 크기도 늘어난다.
  • 함수를 호출할 때마다 스택 프레임이 생성되었다가 사라지는 복잡한 과정을 거치므로 함수 호출에는 오버헤더가 있다.

   

16.1.2 스택 프레임

함수가 호출될 때 스택에는 함수로 전달되는 인수, 실행을 마치고 돌아올 복귀 번지, 지역변수 등의 정보들이 저장

스택에 저장되는 함수의 호출 정보를 스택 프레임(Stack Frame)이라고 함

함수 실행 중에도 필요한 경우 임시적인 정보 저장을 위해 스택을 사용하되 이때 푸시 회수와 팝 회수는 일치하므로 함수가 리턴하면 정확하게 호출전의 상태로 돌아가 항상성을 유지

   

16.1.3 호출 규약

호출 규약은 호출원과 함수간의 약속이므로 양쪽이 다른 형태로 약속을 할 수 도 있다

호출 규약

인수 전달

스택 정리

이름 규칙

__cdecl

오른족 먼저

호출원

_함수명

__stdcall

오른쪽 먼저

함수

_함수명@인수크기

__fastcall

ECX, EDX에 우선 전달, 나머지는 오른쪽 먼저

함수

@함수명@인수크기

thiscall

오른쪽 먼저 this 포인터는 ecx 레지스터로 전달된다

함수

C++ 이름 규칙을 따름

naked

오른쪽 먼저

함수

없음

   

16.1.4 호출 규약 불일치

컴파일러는 함수의 호출 규약에 맞게 스택 프레임을 작성하고 관리

   

16.2 재귀 호출

16.2.1 자신을 호출한다.

재귀 호출(Recursive Call) : 자기 자신을 호출하는 형식

문제가 너무 거대해 한 번의 함수 호출로 해결하기 힘들 때 큰 문제를 작게 나누어 각 호출시마다 점진적으로 문제를 해결하는 방법(Divde and Conquer:분할 점령)을 쓴다. 이때 작게 만든 문제가 원래 문제와 같은 구조를 가진다면 재귀호출이 필요해진다. 또는 다루는 자료 자체가 재귀적인 구조를 가지고 있을 때도 사용됨

  • 예제 Factorial

#include <stdio.h>

   

int factorial( int n )

{

if ( n <= 1 )

{

return 1;

}

else

{

return n*factorial(n-1);

}

}

   

void main()

{

printf("5! = %d\n", factorial(5) );

}

int facotrial( int n )

{

int i, m = 1;

for ( i = 1 ; i <= n ; i++ )

{

m *= i ;

}

return m;

}

대부분의 경우는 재귀함수를 사용하지 않을 때가 속도나 구조상에 훨씬 유리하다.

16.2.2 재귀호출이 가능한 이유

함수가 호출되면 함수에 대한 정보들이 스택에 생성되는데 이를 스택 프레임이라고 한다. 스택 프레임은 복귀 번지, 인수, 지역변수 등의 정보들이 저장된다. 이 정보들은 각각의 함수 호출시마다 매번 생성되며 각각의 호출 인스턴스의 스택 프레임은 독립적이다. 같은 함수가 호출되더라도 각 호출에 사용되는 인수나 지역변수들은 호출별로 따로 기억됨

지역변수의 개념이 없는 언어에서는 재귀호출이 지원되지 않는다.

   

재귀 호출은 각 호출 인스턴스마다 스택 공간을 소모하며, 스택 공간이 소진 되는 사태(Stack Overflow)가 발생 할 수 있으므로 주의해야 한다. 스택은 유한한 메모리 공간이다.

16.2.3 디렉토리 검색

재귀 호출이 꼭 필요한 가장 대표적인 예는 디렉토리를 검색할 때이다.

  • 예제 FileList

#include <stdio.h>

#include <windows.h>

void FileList( char *path )

{

HANDLE hSrch;

WIN32_FIND_DATA wfd;

BOOL bResult = TRUE;

char drive[_MAX_DRIVE];

char dir[MAX_PATH];

char newpath[MAX_PATH];

   

printf("\n검색 경로 = \%s\n", path ) ;

hSrch = FindFirstFile( path, &wfd ) ;

if ( hSrch == INVALID_HANDLE_VALUE )

{

return ;

}

   

_splitpath( path, drive, dir, NULL, NULL );

while( bResult )

{

if( wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY )

{

if( strcmp( wfd.cFileName, ".") && strcmp(wfd.cFileName, "..") )

{

sprintf(newpath, "%s%s%s\\*.*", drive, dir, wfd.cFileName);

FileList(newpath);

}

}

else

{

printf("%s%s%s\n", drive, dir, wfd.cFileName);

}

bResult = FindNextFile(hSrch, &wfd) ;

}

FindClose(hSrch);

}

   

void main(void)

{

char path[MAX_PATH];

   

GetWindowsDirectory(path, MAX_PATH);

strcat(path, "\\*.*");

FileList(path);

}

16.2.4 계층적인 자료 표현

하나의 부모 아래에 복수 개의 자식이 포함될 수 있는 구조를 계층적(Hierarchical)이라고 한다

  • 예제 CityTree

#include <stdio.h>

   

struct tag_city

{

int parent;

char *name;

};

   

tag_city arCity[] =

{

-1,"대한민국",

0, "서울특별시",

1, "동대문구",

2, "청량동",

2,"회기동",

2,"신설동",

1,"한강구",

6,"퐁당동",

6,"월척동",

0,"경기도",

9,"화성시",

9,"금성시",

9,"목성시",

};

   

bool haveChild(int idx)

{

int i;

for ( i = 0 ; i < sizeof(arCity)/sizeof(arCity[0]) ; i++ )

{

if ( arCity[i].parent == idx )

{

return true;

}

}

return false;

}

   

void printCity( int parent, int indent )

{

int i, j;

   

for ( i = 0 ; i < sizeof(arCity)/sizeof(arCity[0]) ; i++ )

{

if( arCity[i].parent == parent )

{

for ( j = 0 ; j < indent ; j++ )

{

printf(" ");

}

printf("%s\n", arCity[i].name );

   

if( haveChild(i) )

{

printCity( i, indent+2 );

}

}

}

}

   

void main(void)

{

printCity( -1, 0 );

}

  • 예제 CityTree2

#include <stdio.h>

   

struct tag_city

{

int id;

int parent;

char *name;

};

   

tag_city arCity[] =

{

0,-1,"대한민국",

1, 0, "서울특별시",

2, 1, "동대문구",

3, 2, "청량동",

4, 2,"회기동",

5, 2,"신설동",

6, 1,"한강구",

7, 6,"퐁당동",

8, 6,"월척동",

9, 0,"경기도",

10, 9,"화성시",

11, 9,"금성시",

12, 9,"목성시",

};

   

bool haveChild(int idx)

{

int i;

for ( i = 0 ; i < sizeof(arCity)/sizeof(arCity[0]) ; i++ )

{

if ( arCity[i].parent == idx )

{

return true;

}

}

return false;

}

   

void printCity( int parent, int indent )

{

int i, j;

   

for ( i = 0 ; i < sizeof(arCity)/sizeof(arCity[0]) ; i++ )

{

if( arCity[i].parent == parent )

{

for ( j = 0 ; j < indent ; j++ )

{

printf(" ");

}

printf("%s\n", arCity[i].name );

   

if( haveChild(arCity[i].id))

{

printCity( arCity[i].id, indent+2 );

}

}

}

}

   

void main(void)

{

printCity( -1, 0 );

}

16.3 인라인 함수

16.3.1 인라인 함수

인라인 함수 : 함수는 함수이되 호출될 때 함수가 있는 곳으로 점프하지 않고 함수의 본체 코드를 호출부 자리에 바로 삽입하는 방식의 함수, 함수 정의부 앞에 inline 키워드를 붙여준다

   

  • 예제 randfunc

#include <stdio.h>

#include <stdlib.h>

   

int randfunc( int n )

{

return rand()%n;

}

   

void main()

{

int i, j, k ;

   

i = randfunc(10);

j = randfunc(100);

k = randfunc(50);

   

printf("난수 = %d, %d, %d\n", i, j, k ) ;

}

  • 함수 호출 시 일어나는 과정
    • 인수를 전달하기 위해 인수 값을 순서대로 스택에 밀어 넣는다.
    • 호출원은 바로 다음 번지를 스택에 기록함으로써 함수가 복귀할 번지를 저장한다.
    • 함수가 정의되어 있는 번지로 점프하여 제어권을 함수에게 넘긴다
    • 함수는 스택에 자신의 지역변수를 위한 공간을 만든다
    • 함수의 코드를 수행한다
    • 리턴값을 넘긴다
    • 복귀 번지로 리턴한다
    • 인수 전달에 사용한 스택을 정리한다

         

    함수 실행 시간보다 호출에 걸리는 시간이 너무 크기 때문에 호출부에 바로 삽입 하는 것이 훨씬 이득이다.

    이런 개념이 바로 인라인(inline) 함수이다.

    컴파일러는 randfunc 함수의 본체 코드 rand()%n 을 기억하고 있다가 인라인 함수가 호출되는 곳에 이 코드를 바로 삽입한다.

  • 인라인 함수 장단점

    인라인 함수는 실제로 호출되지 않으므로 호출에 걸리는 시간이 필요없어 전체적인 실행 속도가 대단히 빠르다.

    복귀 번지를 기록하거나 인수, 리턴값을 전달하는 시간만큼을 절약할 수 있다.

    인라인 함수가 호출되는 곳마다 함수의 본체가 삽입되므로 실행 파일의 크기가 커지는 단점이 있다

       

  • 인라인 함수 특징

    함수가 인라인이 될 것인가 아닌가는 프로그래머가 지정하지만 최정 결정은 컴파일러가 함

    재귀 호출 함수는 스택을 기반으로 동작하기 때문에 인라인이 될 수 없다

    인라인 함수는 번지를 가질 수 없다

    클래스 선언에 코드가 작성되어 있는 멤버 함수는 자동 인라인 속성을 가진다.

    16.3.2 매크로 함수와 다른 점

  1. 인라인 함수는 타입을 인식한다. 매크로 함수가 전달된 인수를 기계적으로 단순히 치환하는 것과는 비교된다
  2. 인라인 함수는 함수의 형태를 띄고 있기 때문에 필요할 경우 지역 변수를 사용할 수 있다.
  3. 매크로는 컴파일 이전 단계인 전처리 단계에서 기계적으로 인수를 치환하기 때문에 괄호를 싸지 않으면 예상치 못한 부작용의 위험이 높다.

16.4 디폴트 인수

16.4.1 인수의 기본값

함수에서 디포트 인수는 기본값이 정의되어 있는 인수이다. 호출부에서 이 인수에 대해별다른 지정을 하지않으면 미리 정의되어 있는 기본값이 적용된다. 별도의 값을 제공하면 디폴트는 무시된다.

  • 예제 DefPara

#include <stdio.h>

   

void outChar( int x, int y, char c='-', int n = 10);

   

void main()

{

outChar(0,1);

outChar(0,2,'=');

outChar(0,3,'>',30);

}

   

void outChar( int x, int y, char c, int n )

{

printf("%d, %d, %c, %d\n", x, y, c, n );

}

16.4.2 디폴트 인수 작성법

함수의 원형에만 지정할 수 있으며 정의부에서는 중복 지정할 수 없다.

디폴트 인수는 오른쪽부터 순서대로 지정할 수 있으며 가운데 인수들은 기본값을 지정할 수 없다.

void outChar( int x, int y = 5, char c = '-', int n = 10 ); // 가능

void outChar( int x, int y = 5, char c , int n = 10 ); // 불가능

void outChar( int x = 0, int y = 0, char c = '-', int n = 10 ); // 가능

void outChar( int x = 0, int y = 0, char c , int n ); // 불가능

void outChar( char c, int n, int x = 0, int y = 0 ); // 가능

디폴트 인수를 가지는 함수를 호출할 때도 오른쪽부터 순서대로 생략 가능하며 중간에 한 인수만 생략 할 수 없다.

outChar(1,2); // 가능

outChar(1,2,'$"); // 가능

outChar(1,2,,5); // 불가능

outChar(1,2,'-',5); // 가능

  • 예제 DefParaPtr

#include <stdio.h>

   

void f(int a=3)

{

printf("%d\n",a);

}

   

void main()

{

void (*pf)(int=3);

void (*pf2)(int=4);

   

pf=f;

pf2=pf;

   

pf();

pf2();

}

16.4.3 디폴트 인수 활용

가끔 다른 가밧도 지정할 수 있도록 하고 싶을 때 디폴트 인수를 사용한다.

다양한 옵션을 지정할 수 있도록 하되 인수가 너무 많으면 호출하기 번거로우므로 변화가 많지 않은 옵션은 뒤쪽으로 이동시킨 후 기본값을 개발자가 미리 제공하는 것이다.

   

void outEnemy( int x, int y, int enemy ) ; 라는 몬스터를 출력하는 함수가 있다고 할때 적 캐릭터에 유령을 위해 투명 처리를 할 필요가 생겼다고 하자.

void outEnemy( int x, int y, int enemy, bool bTrans = false ); 라고 처리 하면 디폴트 인수에 의해 기본적인 몬스터들은 그대로 표시될 것이다. 그리고 투명처리가 필요한 몬스터에 대해서만 함수 호출 시 bTrans를 true로 호출하게 되면된다.

16.5 오버로딩

16.5.1 함수의 중복

오버로딩(Overloading)이란 같은 이름으로 함수를 중복 정의하는 것이다.

똑같은 일을 하되 인수의 형식이나 구현 방식이 조금 다른 함수들을 만들어야 한다면 동일한 이름으로 함수들을 오버로딩 할 수 있다.

  • 예제 Overload

#include <stdio.h>

   

int add( int a, int b );

int add( int a, int b, int c ) ;

double add( double a, double b ) ;

   

void main ( void )

{

printf("1+2 = %d\n", add(1,2));

printf("1+2+3 = %d\n", add(1,2,3));

printf("1.1+2.2 = %f\n", add(1.1, 2.2) );

}

   

int add( int a, int b )

{

return a + b;

}

int add( int a, int b, int c )

{

return a + b + c;

}

double add( double a, double b )

{

return a + b;

}

세 개의 add함수가 같은 이름으로 정의되어 있지만 받아들이는 인수의 개수나 타입이 각각 다르다

16.5.2 중복이 안 되는 경우

  1. 리턴 타입만 다른 경우

    int func( int a, double b );

    double func( int a, double b ) ;

  2. 레퍼런스와 일반 변수

    int add( int a, int b );

    int add( itn &a, int &b ) ;

  3. const 지정자가 있는 경우와 없는 경우

    // 가능

    int strlength( char *s ) ;

    int strlength( const char *s ) ;

    포인터가 가리키는 문자열이 상수인지아닌지는 컴파일러가 수비게 판단할 수 있으므로 전달되는 인수의 타입으로 호출할 함수를 결정할 수 있다. 단 포인터 자체 상수인 경우와 그렇지 않은 경우는 구분되지 않는다.

    // 불가능

    int strlength( char * const s ); // 이 경우 char* s를 취하는 인수와 동일한 함수로 취급

  4. 인수의 논리적 의미만 다른 경우

    int findStudent( char *depart, int depnum );

    int findStdent( char *name, int stnum ) ;

  5. 디폴트 인수에 의해 같아질 수 있는 경우

    add( int a, int b, int c = 0 );

    add(1,2)호출문이 add(int, int)인지 add(int, int, 0 )인지 구분되지 않는다

  6. 달라 보이지만 실제로 같은 타입인 경우

   

  • 오버로딩 정리

    함수의 매개변수의 타입이나 개수가 달라야 오버로딩이 성립한다.

    함수를 호출 할 때 호출하려는 함수가 무엇인지 명확해야 구분되어야 오버로딩이 가능하다

  • 예제 constOverload

#include <stdio.h>

   

int strlength( char *s )

{

printf("char *\n");

return 0;

}

   

int strlength( const char *s )

{

printf("const char*\n");

return 0;

}

   

//int strlength( char * const s )

//{

//        printf("char* const\n");

//        return 0;

//}

   

void main()

{

char str1[] = "123";

const char *str2 = "1234";

char* const str3 = "abcd";

   

strlength(str1);

strlength(str2);

strlength(str3);

}

16.5.3 오버로딩 활용

함수 오버로딩은 특정 동작을 하는 함수의 인터페이스를 여러 벌로 정의할 수 있는 아주 멋진 기능이다. 같은 동작을 다른 방식으로 지정할 수 있을 때 각방식별로 함수를 중복 정의해 놓으면 사용할 때 편리하다.

void drawRect( int x1, int y1, int x2, int y2 ) ;

void drawRect( POINT leftTop, POINT rightBottom ) ;

void drawRect( RECT *bound ) ;

   

   

반응형

'책정리 > 혼자 연구하는 C,C++ 1' 카테고리의 다른 글

18장 C 고급 문법  (0) 2015.02.22
17장 파일 입출력  (0) 2015.02.22
15장 포인터 고급  (0) 2015.02.19
13장 구조체  (0) 2015.02.19
12장 문자열 함수  (0) 2015.02.19