1619, 1/81 회원가입  로그인  
   sihun1113
   [C기초] 10 - 함수를 제대로 알고 써보자!

http://www.hackerschool.org/HS_Boards/zboard.php?AllArticle=true&no=1746 [복사]


http://blog.naver.com/ani4ever/120032194874 출저= 때릴꺼야
*----------------------------------------------------------------------------------------------------------*

이 카테고리는 그동안 배웠던것이라던가 공부하면서 알게된 내용들을 중심으로 혹여 참고가 될 수 있을까 싶은 내용들을

모아둔 곳입니다. 따라서 이 글들을 대상이 되는 분들은.. 프로그래밍을 갓 시작했거나 개념이 마구마구 흔들리시는 분들

정도가 되겠습니다. 그러니 너무 욕하지는 말아주세요 =_=;;;



혹시 제가 잘못 알고 있는 부분들이라던가 궁금한 것이 있으면 쪽지 남겨주시거나 리플을 달아주시면 감사하겠습니다.

*----------------------------------------------------------------------------------------------------------*



이번 주제는 함수로 정했습니다.

왜? 이유를 대자면 한도 끝도 없지요.(사실 제 맘대로라곤 죽어도 말 못합니..... 퍽!)



어찌되었던 간에, 지난 스터디의 부실(?)을 만회하기 위해서라도 최대한 요점만 짚어서 설명해보려고 합니다.



함수가 뭐냐?.. 제가 자주 참조하는 Ivor Horton의 Beginning Visual C++에는 정의가 내려져 있다기보다는 다음과 같은 내용이 있습니다.



"함수는 특정한 목적을 가지고 있는 독립적인 코드 블록이다. 함수는 이름을 가지는데 이름은 함수를 나타내며, 그 함수가 프로그램 안에서 실행될 수 있도록 함수를 호출하는데 사용된다."



"함수가 제공하는 한 가지의 이점은 그것이 프로그램의 여러 부분에서 필요한 만큼 몇 번이고 실행될 수 있다는 것이다."



그리고, 존경하는 저의 스승님, 김정훈 선생님의 저술서인 '알고리즘과 함께하는 C의 아름다움'이라는 책에는 이렇게 설명이 되어있습니다.



"함수의 가장 큰 목적은 코드의 재사용에 있습니다. 만들어 놓은 코드를 한번 밖에 사용하지 못한다면 동일한 역할을 하는 코드를 수도 없이 만들어야 합니다. 음식물과 같이 사용하고 나면 코드가 없어지는 것도 아니고 성능이 떨어지는 것은 더더욱 아닙니다. 코드는 항상 동일한 위치에 존재하기 때문에 무한정 사용할 수 있습니다"



그렇습니다. 함수란 불필요한 코드의 중복을 줄이고 보다 효율적인 프로그램 제작을 위해 존재하는 것입니다.



그렇다면 함수는 좋은것이란 얘기인데 옛 속담에도 이런 말이 있죠? 구슬이 서말이라도 꿰어야 보배라고. 아무리 좋은 기능을 가졌다 할지라도 그 목적과 용도에 맞지 않는다면 프로그램 전체에 좋은 영향을 줄 수 없겠지요.



예를 들어 내가 필요한 기능은 단순히 0~1000 사이의 숫자를 더하기만 하면 되는데, 어디선가 아주 멋들어진.. 100자릿수까지 계산해주는 사칙연산 함수의 소스를 구했다고 해 봅시다. 그래서 그 함수를 넣는다면? 웃기지 않습니까? 이것은 마치 집 앞 구멍가게에 과자를 사러가기위해 최고급 스포츠카를 들여놓겠다는 것과 다르지 않습니다.



이렇듯 좋은 함수란.. 많은 기능을 가진 함수가 아닌 필요에 맞게 최적화 된 함수를 의미합니다.

때에따라서는 함수를 만드는것보다 소스코드 안에 적재를 시키는 것이 오히려 좋을 수도 있습니다. 이러한 판단 능력은 경험에 의해 만들어지는 것이기에 여기에서 자세한 언급은 피합니다.





일반적인 함수는 아래와 같은 모양을 가집니다.





자료형 함수명( 자료형 파라미터1, 자료형 파라미터2, ... )

{

    데이터 처리;

    return 리턴값;

}





함수의 성격에 따라서 파라미터가 1개 이상일수도 있고, 아예 없을수도 있습니다. 또한, 반환 자료형에 따라서 리턴값이 있을수도 있고, 없을수도 있습니다. 그러나 반드시 함수의 자료형과 일치해야합니다.



제가 이번 스터디에서 가장 핵심으로 짚고 넘어가려는 것은, 함수의 파라미터와 관련한 착각과 오해들에 대한 내용들입니다.



프로그래밍 입문과정 중에 함수를 처음 접하게 될 때 이런 유형의 코드를 많이 연습하게 됩니다.





-----------------------------------------------------------

int AddNumber( int A, int B )

{

    return A+B;

}



int main()

{

    int a=2;

    int b=5;

    int sum;



    sum = AddNumber( a, b );



    printf( "[ %d ]", sum );

}

-----------------------------------------------------------





물론 결과는 [ 7 ]이라고 아무런 문제 없이 출력될겁니다.

그리고 조금 발전한(?) 문제의 경우는





-----------------------------------------------------------

void PrintNumber( int A, int B )

{

    printf( "a+b = %d\n", A+B );

    printf( "a-b = %d\n", A-B );

}



void main()

{

    int a=10, b=3;



    PrintNumber( a, b );

}

-----------------------------------------------------------





결과는





-----------------------------------------------------------

a+b = 13

a-b = 7

-----------------------------------------------------------





이라고 나오겠지요.



여기까지는 너무너무 쉽기 때문에 별 문제가 되지 않습니다. 하지만 문제는 이런 경우에 발생하게 됩니다.





-----------------------------------------------------------

void AddTen( int a ) // a값에 10을 더하는 함수

{

    a += 10;

}



void main()

{

    int a=5;



    AddTen( a );



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

}

-----------------------------------------------------------





초보분들은 대부분 15가 결과로 나오길 기대합니다. 하지만 현실은 어떻습니까?





-----------------------------------------------------------

5
-----------------------------------------------------------





자, 대략 난감해지기 시작합니다. 분명히 a를 넘기고 a를 받아서 숫자를 더했는데 왜 결과는 다른걸까요?



아마 제 스터디를 쭈욱 지켜보아 오신 분들은 변수와 포인터에 대해서 약간이나마 이해를 하고 계실것으로 생각합니다. 다시 복습해 보죠.



변수는 세가지 요소를 가진다고 했는데, 그게 뭐뭐뭐였죠?

네 그렇습니다. 변수는 각기 고유 주소와, 크기, 내용물을 갖는다고 했습니다.

이제 변수의 주소를 잘 관찰 해 봅시다.





-----------------------------------------------------------

#include <stdio.h>



void PrintNum( int a )
{
    printf( "함수 안에서의 a의 값과 주소: %d, %d\n", a, &a );

}



void main()
{
    int a=8;

    PrintNum( a );

    printf( "메인 안에서의 a의 값과 주소: %d, %d\n", a, &a );

}

-----------------------------------------------------------





이 코드를 컴파일 해 보세요 결과가 어떻게 나올까요?





-----------------------------------------------------------

함수 안에서의 a의 값과 주소: 8, 1244672
메인 안에서의 a의 값과 주소: 8, 1244884
-----------------------------------------------------------





분명 이름도 똑같이 a이고 값도 똑같이 8인데 주소가 다르네요? 이 말은 무슨 뜻이냐? 두 변수는 이름과 값은 같을지라도 전혀 다른 별개의 변수라는 의미입니다.

그럼 함수 내부에서 어떤 일이 벌어지는지 알아 볼까요?



우선 함수를 호출합니다.





--------------------------------------------------------------------------------

int a=8; -> 주소 1244884인 a라는 변수가 생성되고 8이 대입됩니다.

PrintNum( a );

--------------------------------------------------------------------------------





PrintNum()함수가 호출되었고, a는 함수의 매개변수로 전달되어집니다.

함수는 매개변수로 a를 받습니다.





--------------------------------------------------------------------------------

PrintNum( int a )  -> 여기서 int a 에 의해 새로운 변수가 생성되어집니다.(주소:1244672)

   --------------------------------

     함수 안으로 들어가기 전에 함수에서 선언 된 a(주소:1244672)에 매개변수로

    넘어온 a(주소:1244884)의 값이 복사됩니다.

    즉, (주소:1244672)::a = (주소:1244884)::a 과정이 수행되는 셈이지요.

   --------------------------------

{  -> 이제부터 복사된 a(주소:1244672)가 사용되어집니다.

    printf( "함수 안에서의 a의 값과 주소: %d, %d\n", a, &a );

}  -> 여기서 a(주소:1244672)는 파괴됩니다.

--------------------------------------------------------------------------------





이제 감이 잡히시나요?

함수 안에서의 변수 a는 매번 함수가 호출 될 때마다 생성되고 함수가 종료될때 파괴되는 임시 변수입니다. 따라서 안에서 아무리 어마어마한 연산을 수행했다고 하더라도 곧 의미가 없어질 변수라는 뜻이지요.



그렇다면 안에서 변경 된 내용들을 외부로 끄집어 낼 수 있는 방법은 정녕 없는 것인가????

대표적으로 return이 그 역할을 하지요. 그러나 이는 단순히 한개의 숫자또는 문자밖에는 전달 할 수 없습니다. 하지만 그렇다고 동시다발적으로 수행되어 변경이 필요한 변수들을 하나씩 변경해야한다면.. 배열같은 경우에는 매우 난감한 상황에 봉착하게 되지요. 이를 해결하기 위한 방법을.. 구지 찾으라고 한다면.. 배열을 가진 구조체나 클래스를 선언하여 그 구조체나 클래스를 리턴하는 함수를 만들면 가능하기는 하지요. 예를 들어





--------------------------------------------------------------------------------

class tmp{

    char chArr[1024];

    int    nArr[1024];

}



tmp MyFunction( tmp A )

{

    ....

    return A;

}

--------------------------------------------------------------------------------





어찌보면 잘 작동 할 것 같습니다. 네.. 잘 작동하기는 합니다. 하지만, 어마어마한 메모리 낭비와 함께, 매번 함수를 호출할때마다 무려 5120 Byte나 되는 변수를 할당했다가 해제했다가를 반복하게 됩니다. 이는 간단한 코드에서는 별로 문제 될게 없을지라도, 속도를 중요시하는 프로그램에서는 어마어마한 시간 낭비를 낳는 결과를 초래합니다.(실제로는 메모리 할당이 문제가 아니라 요소를 복사하는데서 어마어마한 시간을 낭비하게 됩니다.)



그래서 함수에서 중요하게 생각하는 것이 포인터의 활용입니다.



위에서 나왔던 예제를 포인터로 전환하여 해결해 봅시다.





--------------------------------------------------------------------------------

#include <stdio.h>



void PrintNum( int* a )
{
     printf( "함수 안에서의 a의 값과 주소: %d, %d\n", a, &a );
     printf( "a가 가리키는 변수의 값과 주소: %d, %d\n", *a, &(*a) );
}



void main()
{
     int a=8;

     PrintNum( &a );

     printf( "메인 안에서의 a의 값과 주소: %d, %d\n", a, &a );
}

--------------------------------------------------------------------------------





함수에서 받아들이는 매개변수가 int형 포인터로 바뀌었습니다. 그렇다면 주소를 넘겨받게 되겠지요. 또, 내부에서는 *연산자를 통해 해당 주소로 접근이 가능해집니다.

결과를 볼까요?





--------------------------------------------------------------------------------

함수 안에서의 a의 값과 주소: 1244884, 1244672
a가 가리키는 변수의 값과 주소: 8, 1244884
메인 안에서의 a의 값과 주소: 8, 1244884
--------------------------------------------------------------------------------





*연산자를 써야하기때문에 다소 귀찮은 감은 있지만.. 어찌되었던간에 원본 데이터인 (주소:1244884)a 에 접근이 가능합니다. 그렇다면 함수 내부에서는 어떤 일이 벌어졌는지 확인 해 봅시다.





--------------------------------------------------------------------------------

int a=8; -> 주소 1244884인 a라는 변수가 생성되고 8이 대입됩니다.

PrintNum( &a ); -> 이제는 a변수의 주소를 넘겨 줍니다.

--------------------------------------------------------------------------------





PrintNum()함수가 호출되었고, a의 주소가 함수의 매개변수로 전달되어집니다.





--------------------------------------------------------------------------------

PrintNum( int* a )  -> 여기서 int* a 에 의해 새로운 포인터변수가 생성되어집니다.(주소:1244672)

   --------------------------------

     함수 안으로 들어가기 전에 함수에서 선언 된 포인터 a(주소:1244672)에 매개변수로

    넘어온 a(주소:1244884)의 주소값이 복사됩니다.

   --------------------------------

{  -> 이제부터 복사된 a(주소:1244672)가 사용되어집니다.

     // 여기까지는 별다른 차이가 없습니다. 다만 a가 가진 값이 주소라는 차이밖엔..

     printf( "함수 안에서의 a의 값과 주소: %d, %d\n", a, &a );

     // 이제 주소를 참조해서 대상을 살펴보면..
     printf( "a가 가리키는 변수의 값과 주소: %d, %d\n", *a, &(*a) );

}  -> 여기서 a(주소:1244672)는 파괴됩니다.

--------------------------------------------------------------------------------





거의 다 비슷하지만, 주소값이 전달되고 그 주소값을 참조하여 접근이 된다는 점이 다르지요.

이제 우리가 원하는대로의 결과를 얻을 수 있을지 테스트를 해 봅시다.





--------------------------------------------------------------------------------

#include <stdio.h>



void PrintNum( int* a )
{
    printf( "함수 안에서의 a의 값과 주소: %d, %d\n", a, &a );
    printf( "a가 가리키는 변수의 값과 주소: %d, %d\n", *a, &(*a) );

    *a += 10;
}



void main()
{
    int a=8;

    printf( "메인 안에서의 a의 값과 주소: %d, %d\n", a, &a );

    PrintNum( &a );

    printf( "함수 호출 후 변경된 a의 값: %d\n", a );
}

--------------------------------------------------------------------------------





--------------------------------------------------------------------------------

메인 안에서의 a의 값과 주소: 8, 1244884
함수 안에서의 a의 값과 주소: 1244884, 1244672
a가 가리키는 변수의 값과 주소: 8, 1244884
함수 호출 후 변경된 a의 값: 18
--------------------------------------------------------------------------------





드디어 우리가 원하던 결과를 얻을 수 있게 되었습니다. 물론 포인터를 사용하는게 부담스럽지 않아야 한다는 전제가 깔리긴 했지만, 이러한 함수를 통해서 함수를 통한 원본 변수 접근을 활용하는 법을 알게 된 것이죠. 아울러 이 포인터 전달이 가진 최대의 장점은 무엇인가 하면, 위에 언급했던 '변수 복사'의 부담을 덜게 된다는 것이지요.



앞의 예제에서 나왔던 구조체 복사에 들어가는 overhead가 10240바이트(입력복사5120,리턴복사5120)였다면, 포인터로 해결했을시는 입력복사 4바이트(포인터 크기4바이트, 리턴 0)에 불과합니다. 즉, 누적이 될 경우 어마어마한 성능의 차이를 가져 올 수 있다는 것이지요.

구조체가 크면 클 수록, 데이터가 많으면 많을수록 이 차이는 점점 커집니다.



따라서 단순히 변경만을 위한 목적이 아니라 해도, 구조체 전체를 매개변수로 전달하는 것보다 포인터를 전달하는 것이 약간이나마 성능 향상을 기대 할 수 있게 되는겁니다.



일단 이번 스터디는 여기까지 마감하고 다음번 스터디에선 함수의 매개변수로 다차원 배열을 넘길때 생기는 문제점에 대해서 알아보도록 하겠습니다.

  Hit : 8100     Date : 2011/05/01 11:33