C++/Study

[C++] 함수 오버로딩

MoongStory 2024. 12. 13. 20:46
반응형

출처 - http://cafe.naver.com/cppmaster/2601

작성자 : 강석민(smk6809@yahoo.co.kr)

출처 : cafe.naver.com/cppmaster, cafe.naver.com/ioacademy

// 마음 대로 퍼 가셔도 되지만 출처는 반드시 밝혀 주시기 바랍니다.

함수 오버로딩 ( Function Overloading )

C++은 아주 복잡한 언어입니다. C가 가진 대부분의 것을 지원 하려고 했고, 추가로 객체 지향과 Generic 기법을 지원 하므로서 괴물처럼 커져 버린 언어입니다.

때문에 많은 분들이 C++로 입문 할 때 어려움을 겪고 있습니다.

하지만 C++이 어려운 만큼 C++을 어느정도 잘하게 되면 Java, C# 등의 새로운 언어를 아주 쉽게 배울수 있습니다.

C++ 문법 속에는 Java, C#의 대부분의 특징이 숨어 있습니다.

기존의 Java, C# 프로그램을 사용하시는 분들도 C++을 통해서 Java 와 C#에 대한 통찰력을 얻을수 있습니다.

하지만 C++을 배우는 많은 분들이 아직은 C++의 깊이를 제대로 이해하지 못하고 있는 것 같습니다.

이번 시리즈를 통해서 C++의 가장 기본 적인 문법부터 체계적으로 정리해 보도록 하겠습니다.

글 중간 중간에 어셈블리 코드를 언급하고 있습니다.

C++을 잘하기 위해 굳이 어셈블리언어를 알 필요는 없습니다.

단지 좀 더 명확하게 원리를 설명하기 위해서 사용했습니다.

또한, 이와 같은 어셈블리 코드는 컴파일러마다 다르기 때문에 외우거나 할 필요는 전혀 없습니다.

첫 번째 순서는 함수 오버로딩입니다.

글 순서

1. 기본 개념

2. 함수 오버로딩의 원리

3. 함수 오버로딩과 C/C++의 호환성 문제

4. __cplusplus

5. 함수 찾는 순서

6. 마무리

실습 환경 : VC++2008 또는 VC++ 2008 Express

1. 기본 개념

C++에서는 인자의 갯수나 타입이 다를 경우에는 동일한 이름의 함수를 여러개 만들수 있습니다.

흔히 함수 오버로딩(Function Overloading) 이라고 합니다.

오버로딩 덕분에 다양한 타입에 대해서 일관된 인터페이스를(함수 이름이 같다는 의미) 제공할 수 있습니다.

#include <iostream>

using namespace std;

int square(int a) // 1
{
	return a * a;
}

double square(double a) // 2
{
	return a * a;
}

int main()
{
	cout << square(3) << endl;	 // call square( int )
	cout << square(3.3) << endl; // call square( double )
}

 

이와 같은 함수 오버로딩에 몇가지 주의 할 점은 있습니다.

* 리턴 타입만 다른 함수는 오버로딩 될 수 없습니다.

* 디폴트 인자가 있는 경우도 주의가 필요 합니다.

* 혼란을 초래할 수도 있습니다.

// 리턴 타입만 다른 경우는 오버로딩 될 수 없습니다.
int foo(int a);
double foo(int a); // error

// 디폴트 인자가 있을 경우는 주의 해야 합니다.
void foo(int a);
void foo(int a, int b = 0); // error

// 혼란을 초래 할 수 도 있습니다.
void foo(int a);   // 1
void foo(char *s); // 2

foo(0);			   // 1번, 2번 중 몇 번을 호출 할까요?

 

그렇다면 이와 같은 함수 오버로딩의 원리는 무엇일까요 ?

2. 함수 오버로딩의 원리 - name mangling

C++ 컴파일러는 함수 오버로딩을 지원하기 위해서 컴파일시에 함수의 이름을 변경합니다.

아래 예제는 C++ 코드와 컴파일 했을 때 생성되는 어셈블리 코드입니다.

(VC++ 2008 Express 설치후, 명령 프롬프트에서 cl Test.cpp /FAs 로 빌드한 결과입니다. 결과물을 단순화하기 위해 최적화 옵션을 지정 하지 않았습니다.)

int square(int a)
{
	return a * a;
}

double square(double a)
{
	return a * a;
}

int main()
{
	square(3);
	square(3.3);
}

 

; square(3)을 호출 하는 기계어 코드 입니다.
	push	3
	call	?square@@YAHH@Z		; square(3)

	; square(3.3)을 호출하는 기계어 코드 입니다.
	add	esp, -4			; fffffffcH
	fld	QWORD PTR __real@400a666666666666
	fstp	QWORD PTR [esp]
	call	?square@@YANN@Z		; square(3.3)
	fstp	ST(0)
	add	esp, 8

 

결국 컴파일 하고 나면 두 함수의 이름이 달라지게 되는 것입니다.

int square(int) ==> ?square@@YAHH@Z

double square(double) ==> ?square@@YANN@Z

이처럼 C++ 컴파일러가 오버로딩을 지원하기 위해 함수의 이름을 컴파일 시간에 변경하는 것을 name mangling 이라고 합니다.

이와 같은 name manglng 현상을 간단하게 확인해 볼 수도 있습니다.

아래 코드를 입력한 후에 빌드해 보세요.

#include <iostream>

using namespace std;

int square(int a); // 선언만있고 구현은 없습니다.
double square(double a);

int main()
{
	square(3); // 구현이 없으므로 Link 에러가 발생 합니다.
	square(3.3);
}

 

이 경우 함수의 선언만 있고 구현이 없기 때문에 해당 함수를 찾을 수 없다는 링커 에러 메시지가 나옵니다.

 

그때 에러 메시지를 자세히 살펴 보면 name mangling 현상을 볼 수 있습니다.

1>Linking...
1>1.obj : error LNK2019: unresolved external symbol "double __cdecl square(double)" (?square@@YANN@Z)
referenced in function _main
1>1.obj : error LNK2019: unresolved external symbol "int __cdecl square(int)" (?square@@YAHH@Z)
referenced in function _main

 

그런데, 이와 같은 name mangling 현상은 C로 만들어진 함수를 C++에서 호출할 때 문제가 됩니다.

3. 함수 오버로딩과 C/C++의 호환성 문제

name mangling 때문에 C와 C++ 사이에는 호환성의 문제가 발생합니다.

이 문제는 직접 실습해 보는 것이 좋습니다.

VC++에서 프로젝트를 하나 만들고 아래의 3개 파일을 추가 한후 빌드해 보세요.

파일 확장자 (.c 또는 .cpp)가 중요합니다.

// square.h
int square( int a );
// square.c
int square(int a)
{
	return a * a;
}
// main.cpp
#include "square.h"

int main()
{
	square(3);
}

 

VC++ 컴파일러는 확장자가 .c 인 경우는 C문법으로 컴파일을 하고 .cpp 인 경우는 C++ 문법으로 컴파일을 하게 됩니다.

따라서 square.c 안에 square 함수는 name mangling 이 적용되지 않습니다.

하지만 main.cpp의 main 함수에서는 square 함수를 C++ 함수라고 생각하고 mangling 된 버전을 찾게 됩니다.

결국 mangling 된 버전의 square 함수를 찾을수 없다는 Link 에러가 발생되게 됩니다.

다음은 square.c 와 main.cpp 의 어셈블리 입니다.

PUBLIC _square
_TEXT SEGMENT
_a$ = 8 ; size = 4
_square	PROC
	push	ebp
	mov	ebp, esp
	mov	eax, DWORD PTR _a$[ebp]
	imul	eax, DWORD PTR _a$[ebp]
	pop	ebp
	ret	0
_square	ENDP
_TEXT	ENDS
END
PUBLIC	_main
_TEXT	SEGMENT
_main	PROC
	push	ebp
	mov	ebp, esp
	push	3
	call	?square@@YAHH@Z
	add	esp, 4
	xor	eax, eax
	pop	ebp
	ret	0
_main	ENDP
_TEXT	ENDS
END

 

이 경우 C++ 컴파일러에서 square 함수가 C의 함수임을 알려주면 됩니다.

square.h 파일을 다음처럼 수정하고 다시 빌드해보면 됩니다.

// square.h
extern "C" int square( int a );

 

extern "C" 는 해당 함수가 C의 규약을 따른다고 C++ 컴파일러에게 알려주는 지시어입니다.

이제 아무 문제 없이 빌드에 성공합니다. 다 된 걸까요?

아직 하나의 문제가 남아 았습니다.

4. __cplusplus 매크로

위에서 만든 main.cpp 소스의 확장자를 main.c 로 변경하면 또 다른 문제가 발생합니다.

(VC++의 경우 main.cpp 파일에서 오른쪽 버튼을 누르면 파일의 이름을 변경할 수 있습니다.)

// square.h
extern "C" int square( int a );
// square.c
int square( int a )
{
	return a * a;
}
// main.c  이제 확장자는 .c입니다.
#include "square.h"

int main()
{
	square( 3 );
}

 

이 경우는 무엇이 문제가 될까요?

extern "C" 라는 키워드는 C++ 이 제공하는 키워드입니다.

하지만 sqaure.h를 포함(include)하고 있는 파일은 main.c입니다.

확장자가 .c 이므로 C 문법으로 컴파일을 하게 됩니다.

당연히 extern "C"를 알수가 없다고 에러가 나오게 됩니다.

어떻게 해야 할까요?

다시 extern "C" 지워야 할까요? 아님 C용 헤더와 C++용 헤더를 따로 만들어야 할까요?

이와 같은 문제를 해결하기 위해서 C++에서는 다음과 같은 규칙이 있습니다.

"모든 C++ 컴파일러에는 __cplusplus 라는 매크로가 정의 되어 있다."

즉, 아래 처럼 조건부 컴파일을 하도록 헤더 파일을 만들면 C에서 include 하거나 C++에서 include 해도 아무런 문제가 없게 됩니다.

(C++에서 include 하면 extern "C" 가 붙고 C에서 include 하면 extern "C"가 붙지 않게 됩니다.)

// square.h
#ifdef __cplusplus
extern "C" {
#endif
	int square( int a );
#ifdef __cplusplus
}
#endif
// square.c
int square( int a )
{
	return a * a;
}
// main.c 또는 main.cpp
// C 또는 C++ 모두에서 square.h를 사용할 수 있습니다.
#include "square.h"
int main()
{
	square( 3 );
}

 

우리가 printf() 함수를 사용하려면 <stdio.h> 파일을 사용 합니다.

우리는 이 헤더를 include 할 때 소스파일의 확장자가 .c 인지 .cpp인지 생각하지 않고 사용합니다.

그것은 바로 <stdio.h> 안에 아래와 같은 구분을 포함하고 있기 때문입니다. (VC++ 2008 에 있는 stdio.h에서 발췌 했습니다.)

/***
* stdio.h - definitions/declarations for standard I/O routines
*
*	Copyright (c) Microsoft Corporation. All rights reserved.
*
* Purpose:
*	This file defines the structures, values, macros, and functions
*	used by the level 2 I/O ("standard I/O") routines.
*	[ANSI/System V]
*
*	[Public]
*
****/

#if _MSC_VER > 1000
#pragma once
#endif

#ifndef _INC_STDIO
#define _INC_STDIO

#include <crtdefs.h>
#ifdef _MSC_VER
/*
* Currently, all MS C compilers for Win32 platforms default to 8 byte
* alignment.
*/
#pragma pack(push,_CRT_PACKING)
#endif /* _MSC_VER */

#ifdef __cplusplus
extern "C" {
#endif

// ......

 

5. 함수 찾는 순서

되도록 간결하게 쓰려고 했는데 약간 길어지는 군요.

이제 마지막으로 약간 다른 이야기를 해보도록 하겠습니다.

아래와 같은 코드가 있을 때 (A) 는 몇 번을 호출하게 될까요?

void foo( int )	{ cout << "int" << endl; }	// 1
void foo( float )	{ cout << "float" << endl; }	// 2
void foo( double )	{ cout << "double" << endl; }	// 3
void foo( ... )	{ cout << "..." << endl; }	// 4

int main()
{
	foo( 3.4f ); // (A)
}

 

3.4f는 당연히 float 타입이므로 2번을 호출하게 됩니다.

그렇다면 2번을 주석 처리 한다면 몇 번이 호출될까요?

float는 double 변할 때 data의 손실이 발생되지 않으므로 3번이 호출됩니다.

그럼, 3번도 주석처리 한다면?

동일한 이름의 함수가 여러개 있을 때 C++ 컴파일러는 다음의 순서로 함수를 찾게 됩니다.

(1) Exactly Matching - 정확한 타입을 가지는 함수가 있다면 제일 먼저 선택됩니다. 위의 경우 2번

(2) Promotion - 정확한 타입이 없을 때 data의 손실이 발생하지 않은 방향으로 암시적 변환이 가능한 함수를 찾습니다. 위의 경우 3번

(3) Standard Conversion - Data 손실이 발생하더라도 표준 타입끼리는 암시적 변환이 허용됩니다. 위의 경우 1번

(4) 변환 연산자, 변환 생성자 등을 사용해서 암시적 변환이 될 수 있는 함수를 찾습니다. 위 코드에는 해당 되지 않습니다.

(5) 가변 인자를 가지는 함수가 있다면 호출 됩니다. 위의 경우 4번

6. 마무리

이번 글을 통해서 함수 오버로딩에 대해서 살펴 보았습니다. 중요한 점을 정리해 보면

- C++에서는 인자의 갯수나 타입에 따라 동일한 함수를 여러개 만들수 있습니다.

- 일관된 인터페이스 작성에 도움이 됩니다.

- 오버로딩의 원리는 컴파일시에 함수의 이름을 변경하는 name mangling 현상입니다.

- name mangling 때문에 C/C++ 사이에는 호환성 문제가 발생합니다.

- C로 작성된 함수를 C++에서 사용하기 위해서 헤더는 조건부 컴파일(__cplusplus)로 작성해야 합니다.

반응형

'C++ > Study' 카테고리의 다른 글

[C++] C++의 형변환 연산자  (0) 2024.12.13
[C++] 증감연산자(전위형 후위형)  (0) 2024.12.13
[C++] namespace  (0) 2024.12.13
[C++] 컨테이너의 요소를 출력하는 Helper 함수  (0) 2024.12.13
[C++] 메모리 할당 이야기  (0) 2024.12.13