출처 - 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 |