안녕하세요 피터입니다.

오늘은 C언어의 함수 (Function)에 대해 알려드리겠습니다.


개요

수학에서는 함수 이런식으로 표기합니다.


y = f(x)

여기에서 x 는 함수의 입력값이 되고, y 출력값이 됩니다.


C언어에서 함수(Function)도 이와 유사하다고 볼 수 있습니다.

입력값을 가질 수 있으며, 이에 대응되는 값을 출력할 수 있습니다.



문법(Syntax)

함수 선언 (Function Declaration)

함수는 다음과 같이 선언할 수 있습니다.


DataType FunctionName(DataType, ...);


위와 같은 문장(문장이기 때문에 ; 세미콜론으로 끝납니다)을 함수 선언 또는 함수 원형이라고 합니다.

맨 앞쪽에 있는 DataTypeReturnType이며 함수의 출력값입니다.

FunctionName에서 함수 이름을 지정할 수 있으며, 함수 이름 뒤의 ( ) 안에는 0개 이상매개변수(Parameter)를 넣을 수 있습니다.

각각의 매개변수들은 , (콤마)로 구분하며 개수의 제한은 없습니다.

다만 너무 많은 매개변수를 넣으면 사용하기가 힘들겠죠.


함수 이름을 정하는 것은 변수 이름을 정하는 것 만큼이나 굉장히 중요합니다.

이름은 고유한 함수의 식별자로 방대한 소스코드 내에서 코드의 흐름(Flow)를 추적하는데 있어서 이정표와 같은 역할을 하기 때문입니다.


때문에 함수 이름이나 변수 이름을 대충 지었다가는 여러분의 코드를 보는 사람들로 하여금 코드의 미로에 갇혀 헤매이게 만들지도 모릅니다.

그러니 함수 이름을 정할 때는 의미가 잘 전달될 수 있도록 신중하게 결정해주시기 바랍니다.


올바르게 함수명과 변수명을 짓는 방법에 대해서는 추후에 포스팅하겠습니다.


함수 선언문 내에는 실제로 동작에 관련된 코드가 들어있지 않기 때문에 함수를 선언(Declaration)했다면 반드시 함수를 정의(Definition)해야합니다.

즉, 선언과 정의가 쌍을 이뤄야 합니다.


선언과 정의를 함수의 헤더(Header)바디(Body)라고도 부릅니다.


선언만 하고 정의를 않은 상태에서 해당 함수를 호출하게 되면 컴파일러에서 Link Error를 선물해줄겁니다.


이렇게 헤더와 바디를 구분해놓는 이유는 라이브러리의 경우 실제 동작 코드는 컴파일이 완료된 lib 파일이나 dll 파일 안에 존재하는데, 함수를 호출하기 위해서는 먼저 컴파일러(Compiler)에게 함수 원형을 알려줘야 합니다.

때문에 라이브러리 함수를 호출하기 전에 해당하는 함수의 원형을 선언해줘야 하는데 라이브러리에 포함된 함수가 한두개가 아니기 때문에 일일히 적기가 곤란합니다.

그래서 함수 원형만 따로 모아놓은 파일이 .h 확장자를 갖는 헤더파일(Header file)이고 이 파일만 #include로 불러오면 라이브러리의 함수를 모두 호출할 수 있게 되는 것입니다.


#include<stdio.h>

위 문장은 printf, scanf 등의 표준 입출력 함수들이 구현되어 있는 Standard Input / Output 라이브러리를 사용하기 위해 표준입출력 헤더파일을 포함한다는 의미입니다.


경우에 따라서는 함수의 선언과 동시에 구현을 하는 경우도 있습니다. 이러한 함수를 인라인 함수(Inline Function)이라고 합니다.

하지만 특별한 경우를 제외하고는 함수의 선언과 정의을 분리해서 사용하는게 좋습니다.


함수 정의

함수의 이름과 입/출력값을 지정하여 선언하였다면 이제 함수를 정의할 차례입니다. 다른 표현으로 함수를 구현한다고도 합니다.


DataType FunctionName(DataType, ...)

{

    // do something

    return value;

}


위와 같이 함수 원형 뒤에 { } 블럭에 실제 함수에서 수행할 코드를 작성해주면 됩니다.


함수 선언시에는 각 매개변수의 데이터 타입만 명시하고 이름을 생략할 수 있지만 함수 정의에서는 매개변수의 데이터 타입과 이름을 모두 명시해야 합니다.



// Declaration
int sum(int, int);

// Definition
int sum(int a, int b)
{
  return a + b;
}


위 예제는 두 개의 숫자를 매개변수로 입력받아서 두 숫자의 합을 반환하는 함수입니다.

이처럼 return 값은 특정 변수만 지정할 수 있는 것이 아니라 수식을 넣을 수도 있습니다. 그렇게 되면 수식식의 최종 결과값이 반환되게 되는 것이죠.


함수 호출

함수 정의까지 마쳤다면 이제 원하는 시점에 함수를 호출(Call) 할 수 있습니다.




아래와 같이 선언했던 함수명을 적고 ( ) 안에 매개변수에 들어갈 변수를 넣어주기만 하면 됩니다.

c = sum(a, b);

그러면 sum 함수로 프로그램이 점프(Jump)해서 실행을 하다가 return 을 만나면 다시 돌아오는 것이죠.
{ } 로 감싸여진 블록 마다 별도의 스택(Stack) 영역이 생긴다고 전에 언급 드린 적이 있었죠.

함수에서도 마찬가지입니다.
sum 함수가 호출되는 순간 sum 함수의 스택이 생성되고 스택 영역의 지역변수(Local variable)로 a와 b가 새롭게 할당됩니다.

그리고 main 스택에서 매개변수로 넣었던 a, b 변수의 값이 복사(Copy)되는데 이 부분이 중요합니다.

변수의 이름은 같지만 서로 다른 스택 영역에 존재하기 때문에 sum 함수에서 a, b 변수의 값을 아무리 변경하여도 main 스택 영역의 변수들의 값은 그대로입니다.

sum 함수의 } 부분에서 스택이 정리되면서 sum 함수 내에 할당되었던 변수 a, b는 소멸됩니다.

함수의 처리 결과를 main 스택에서 얻기 위해서는 반환값(return value)을 받는 수밖에 없습니다.

그런데 이 반환값는 한 개밖에 선언할 수가 없죠?

하지만 프로그램을 개발하다 보면 반환값이 여러 개가 필요한 경우가 종종 발생합니다. 이럴 경우에는 어떻게 해결해야 될까요?


첫 번째로는 구조체(struct)를 이용하는 방법이 있습니다.

반환값을 여러 개의 데이터 타입으로 구성된 구조체로 선언하면 원하는 만큼 다양한 결과값을 묶어서 반환할 수 있습니다.

구조체에 관련된 내용은 나중에 자세히 말씀드리기로 하고 오늘은 두 번째 방법을 소개해드리겠습니다.


바로 앞서 배웠던 포인터(pointer)를 이용해서 해결하는 방법입니다.


함수는 매개변수로 값을 그대로 넘기냐, 값에 대한 참조를 넘기냐에 따라서 다음과 같이 두 가지로 구분할 수 있습니다.

  • Call by value
  • Call by reference


위에 있는 sum 함수 예제는 전형적인 Call by value의 예입니다.


Call by reference를 구현하기 위해서는 매개변수포인터 변수로 선언해야 합니다.

sum 함수 예제를  Call by reference로 바꿔보겠습니다.



// Declaration
void sum(int, int, int*);

void main(void)
{
  int a,b,c;
  a = 10, b = 15, c = 0;
  sum(a, b, &c);
  printf("a + b = %d\n", c);
}

// Definition
void sum(int a, int b, int* c)
{
  *c = a + b;
}


int* 타입의 매개변수 c 를 선언하고 sum 함수를 호출할 때 변수 c주소를 넘깁니다.

그렇게 하면 매개변수 c에는 main 스택에 존재하는 변수 c 의 주소가 복사되겠죠.

결과적으로 sum 함수 실행 시 *c 와 같이 * 연산자를 이용해서 해당 주소의 실제 값을 변경하면 main 스택에 있는 변수 c 의 값이 변경되게 됩니다.

이러한 원리를 이용하면 매개변수의 개수에는 제한이 없기 때문에 원하는 만큼의 반환값을 함수로부터 받아올 수가 있습니다.



-Peter의 우아한 프로그래밍

여러분의 공감과 댓글은 저에게 크나큰 힘이 됩니다. 오류 및 의견 주시면 감사하겠습니다.

블로그 이미지

친절한 Peter Ahn

IT 정보 공유, 프로그래밍 지식 공유

댓글을 달아 주세요

 

 

개요

 

안녕하세요 피터입니다.

오늘은 C언어의 포인터(Pointer)에 대해 알려드리겠습니다.

 

포인터는 C언어를 배우는 많은 사람들이 어려워 하는 개념 중에 하나입니다.

동시에 C언어에서 가장 중요한 개념 중 한가지이기도 합니다.

 

사실 포인터(Pointer)라는 것은 하나의 데이터 타입(DataType)일 뿐이며 실제 값(Value)를 저장하는 대신에

값이 저장되어 있는 변수의 주소(Address)가 저장된다는 것만 기억하면 어렵지 않습니다.

 

 

문법(Syntax)

 

포인터 타입 정의 (Definition)

 

포인터 타입을 정의할 때는 기존에 정의된 데이터 타입 뒤에 * (Asterisk) 를 붙이면 됩니다. (애스터리스크 라고 발음합니다)

예를들면 다음과 같이 정의할 수 있습니다.

 

int*
char*
float*
double*
struct node*

 

기본형 데이터 타입 외에도 파생형 타입도 포인터 타입으로 만들 수 있습니다.

[C언어 강좌-4] 자료형 (DataType)

 

모든 포인터 타입의 변수에는 주소값이 저장되기 때문에 포인터 변수가 차지하는 메모리 크기는 모두 같습니다.

그렇기 때문에 원래 1 Bytechar 형의 데이터 타입도 char* 포인터 타입이 되면 4 Bytes의 크기를 갖게 되는 것입니다.

마찬가지로 8 Bytes의 크기를 차지하는 double의 경우에도 double* 포인터 타입이 되면 4 Bytes가 됩니다.

굉장히 중요하고 헷갈리기 쉬운 부분이니 잘 기억해두세요!

 

 

포인터타입으로 변수 선언 (Declaration)

 

포인터 타입으로 변수를 선언하는 방법은 기존의 변수 선언하는 방법과 동일합니다.

int* a; // or int *a;

 

이렇게 선언된 포인터 변수 a 는 4 Bytes 만큼 메모리 공간을 할당받게 되고 이 공간에 주소값을 저장할 수 있습니다.

일반적으로 변수를 할당하고 아무런 값을 셋팅하지 않으면 가비지(Garbage)값이 들어있기 때문에 초기화를 해줘야 합니다.

 

포인터 변수를 초기화 할 때는 보통 NULL (널) 값으로 초기화 합니다.

이것은 나중에 포인터 변수에 주소값이 할당되었는지 체크하기 위해서 아무런 주소값이 할당되지 않았다는 의미를 갖게됩니다.

따라서 포인터 변수에 접근(access)하기 위해서는 반드시 NULL 인지 체크하는 루틴이 필요합니다.

 

int* a = NULL;
if(a != NULL)
  printf("%d", *a);

NULL이 들어있는 포인터 변수에 접근하게 되면 NullptrException 이라는 무시무시한 에러를 만나실 수 있으니 이 부분은 습관처럼 익숙해지셔야 합니다.

 

NULL은 C에서 일반적으로 0으로 define 되어 있는데 메모리 주소 체계에서 0번지는 시스템 영역이므로 이러한 접근을 차단하기 위해서 Exception(예외)을 발생시키는 것입니다.

 

포인터 변수에 접근할 때에는 당연하게도 주소를 다루기 때문에 일반적인 변수와는 조금 다르게 취급해야 되겠죠?

포인터 변수에 값을 저장하거나 불러올 때는 아래와 같이 특별한 연산자가 사용됩니다.

 

 

& 연산자 (Operator &)

 

int b = 100;
a = &b;

& (Ampersand) 연산자는 변수 앞에 붙는 단항연산자입니다.

(앰퍼샌드라고 발음합니다)

이 연산자는 뒤에오는 변수의 주소값을 반환합니다.

따라서 위 코드의 의미는 int 형 변수 b 의 주소를 얻어서 포인터 변수 a 에 저장한다는 의미입니다.

 

 

* 연산자 (Operator *)

 

printf("address: %d, value: %d\n", a, *a);

* 연산자는 & 연산자와 마찬가지로 단항연산자이지만 정반대의 동작을 수행합니다.

뒤에 오는 포인터변수가 가리키고 있는 주소에 저장되어 있는 실제 값(Value)를 반환합니다.

 

따라서 포인터 변수 a에 int 형 변수 b의 주소가 저장되어 있다면 *a 는 b에 저장되어 있는 값을 의미합니다.

 

ex) 포인터 변수 a 에 int 형 변수 b의 주소를 저장한 뒤 주소값과 실제값을 출력하는 코드

int* a = NULL; // or int *a;
int b = 100;
a = &b;

printf("address: %d, value: %d\n", a, *a);

 

참고

 

Call by reference

 

이후 함수(function) 포스팅에서 자세히 다루게 될 내용이지만 간략하게 소개드리겠습니다.

함수의 매개변수(Parameter)를 포인터 타입으로 사용하게 되면 해당 함수의 외부에서 선언된 변수에 직접 접근하여 값을 저장할 수 있습니다.

따라서 Call by value 방식과는 다르게 함수 호출이 끝나고 return 되어도 포인터 타입으로 선언된 매개변수를 이용해 저장된 값은 그대로 유지가 된다는 의미입니다.

 

배열과 포인터 (Pointer and Array)

 

[C언어 강좌-10] 배열 (Array)

이전 포스트에서 배열의 이름만 쓰게 되면 첫 번째 요소의 주소의 의미가 된다고 말씀드린 적이 있습니다.

 

int cost[10];

즉, 위와 같이 선언된 배열이 있다고 한다면 cost 라고만 쓰면 &cost[0] 과 의미가 같다는 뜻입니다.

내부적으로 배열의 인덱스 연산자 [ ] 도 첫 번째 요소의 포인터 값에 [ ] 안에 들어있는 숫자 만큼 더한다는 의미이기 때문에 배열과 포인터는 매우 밀접한 관련이 있다고 할 수 있습니다.

 

 

-Peter의 우아한 프로그래밍

여러분의 공감과 댓글은 저에게 크나큰 힘이 됩니다. 오류 및 의견 주시면 감사하겠습니다.

 

 

블로그 이미지

친절한 Peter Ahn

IT 정보 공유, 프로그래밍 지식 공유

댓글을 달아 주세요

안녕하세요 피터입니다.

오늘은 C언어를 배운 후 C++을 공부하는데 있어서 굉장히 헷갈리는 개념인 포인터와 레퍼런스의 차이에 대해서 설명드리겠습니다.

 

개요

 

C++ 프로그래밍을 시작하면 레퍼런스(Reference : 참조자)라는 새로운 개념을 접하게 됩니다.

언뜻 보면 C언어를 공부할 때 여러분들을 굉장히 괴롭혔던 포인터(Pointer)와 유사해 보이는데 어떠한 대상을 가리킨다는 점에서는 같습니다.

 

하지만 포인터와 레퍼런스는 여러가지 차이점이 있습니다.

 

그 중에 여러분이 C++ 프로그래밍을 할 때 반드시 알아야 할 두 가지 중요한 차이점을 짚어드리겠습니다.

 
 

1. NULL 허용 여부

우선 NULL값을 허용하는 가에 대한 문제입니다.

포인터는 아시다시피 NULL을 허용하지만 레퍼런스는 NULL이 허용되지 않습니다. 이 부분이 굉장히 중요한데요.

포인터를 다룰 때 수없이 우리를 마주쳤던, ‘Null pointer exception’ 또는 Segmentation Fault’ 에러들의 대부분의 원인은 포인터를 초기화하지 않거나 NULL을 가리키고 있는 포인터에 접근했을 때 발생합니다.

 
struct person
{	
	int birthday;
};

struct person *peter = NULL;

peter->birthday = 1220;

위 코드처럼 NULL 포인터를 참조 하고 있는 peter 변수를 접근 할 때 에러가 발생합니다.

따라서 포인터 변수를 사용할 때에는 반드시 아래와 같이 포인터가 NULL인지 여부를 확인하는 코드(validation) 처리를 해줘야 이런 에러들을 방지할 수 있습니다.

 
if (peter != NULL)
   peter->birthday = 1220;
else
   printf("peter is null\n");
 

하지만 여기에서 포인터 대신 레퍼런스를 사용하면 이런 문제는 발생하지 않습니다. 레퍼런스는 NULL을 할당할 수 없도록 제한되기 때문입니다. 포인터와 목적은 같지만 잘못된 참조로 인해 발생되는 오류를 방지하기 위해 고안되었다고 이해하시면 됩니다.

 

이러한 특성은 함수 매개변수(Function parameter)에 사용될 때에도 동일하게 적용이 되는데요.

 
void isBirthdayByPointer(struct person*); 
void isBirthdayByReference(struct person&);

 

위와 같이 선언된 함수가 있을 때 포인터 매개변수를 갖는 함수는 매개변수에 접근할 때 반드시 NULL 여부를 체크해야 합니다.

반면 레퍼런스 매개변수를 갖는 함수는 NULL을 허용하지 않기 때문에 생략이 가능합니다.

 

매개변수가 NULL을 허용하는지 여부는 프로그램을 설계할 때 굉장히 중요한 부분입니다. 이 차이로 인해 함수의 설계 사상이 완전히 달라질 수 있기 때문입니다.

앞으로 여러분들은 함수를 설계할 때 이러한 차이를 반드시 유념하시기 바랍니다.

 

2. 참조 대상 할당 및 접근

 

앞서 살펴본 NULL 허용 여부는 참조 대상을 할당하는 방법에서 오는 차이라고 볼 수도 있는데요.

포인터는 할당 할 때 참조 대상에 대해 & 연산을 통해 주소값을 할당합니다. 반면 레퍼런스에는 참조 대상을 그대로 할당합니다.

 
int a = 10;
int *p = &a;	// 포인터는 주소값을 할당
int &r = a; 	// 레퍼런스는 대상을 직접 할당

 

따라서 레퍼런스 변수에는 애초에 NULL을 할당 할 수가 없는 것이죠. 또한 레퍼런스는 선언과 동시에 초기화를 하지 않으면 컴파일 오류가 발생합니다.

 

이러한 차이는 함수 호출 시에도 같이 적용됩니다.

 
struct person peter;

isBirthdayByPointer(&peter); // 주소값 입력
isBirthdayByReference(peter); // 직접 입력

 

 

이렇게 넘겨 받은 참조를 사용할 때에도 포인터는 *, -> 등의 포인터 연산자를 통해 접근해야 하지만 레퍼런스는 마치 일반변수처럼 접근할 수 있습니다.  (다만 레퍼런스의 값을 변경하면 레퍼런스가 참조하고 있는 실제 변수의 값이 변경됩니다)

 

 
 

결론

 

레퍼런스는 포인터를 잘못 사용해서 생기는 수많은 재앙과도 같은 문제들을 최소화하기 위해 등장했다고 보시면 되겠습니다. 때문에 무한한 가능성을 열어둔 포인터와 달리 레퍼런스는 위에서 살펴본 것과 같이 여러가지 제약 사항이 존재합니다.

 

하지만 이런 제약사항에도 Call by reference로 활용하는 데에는 전혀 문제가 되지 않기 때문에 보다 안정성이 있는 프로그램을 위해 적극적으로 활용하시길 권해드립니다.

 

C++ FAQ에서 포인터와 레퍼런스에 대해 아래와 같이 설명하고 있습니다.

 

Use references when you can, and pointers when you have to
사용할 수 있다면 참조자를, 어쩔 수 없다면 포인터를 써라

 

여러분의 댓글은 저에게 크나큰 힘이 됩니다. 오류 및 의견 주시면 감사하겠습니다.

- Peter의 우아한 프로그래밍 강의

블로그 이미지

친절한 Peter Ahn

IT 정보 공유, 프로그래밍 지식 공유

댓글을 달아 주세요