해당 글은 공부를 하면서 적은 글이기 때문에 틀릴 수 있습니다. 참고용으로만 봐주세요~
1. 오버로딩(Overloading) : 함수 중복 정의
오버로딩은 같은 이름의 함수에 매개 변수를 다르게 사용하여 매개 변수에 따라 다른 함수가 실행되는 것이다.
int add(int inum1, int inum2)
{
return inum1 + inum2;
}
int add(int inum1, int inum2, int inum3)
{
return inum1 + inum2 + inum3;
}
float add(float fnum1, float fnum2)
{
return fnum1 + fnum2;
}
위의 예제 코드 처럼 add()라는 같은 이름의 함수를 매개 변수를 다르게 하여 다른 함수를 정의할 수 있다.
이러한 함수 오버로딩(Function Overloading)을 사용하면 같은 동작을 수행하는 함수를 매개 변수를 다르게 받아 대응할 수 있도록 코드를 작성할 수 있다.
오버로딩의 주의사항
오버로딩을 이용하기 위해서 몇 가지 주의사항이 있다.
1. 함수 오버로딩은 매개변수가 다른 것을 기준으로 한다.
int add(int inum1, int inum2)
{
return inum1 + inum2;
}
float add(int inum1, int inum2)
{
return inum1 + inum2;
}
위 예제 코드 처럼 반환타입이 다르더라도 문법에 틀려 사용할 수 없다.
왜냐하면 함수 오버로딩은 기본적으로 매개 변수를 기준으로 하기 때문이다.
만약 위의 예제와 같이 반환값만 다른 함수를 만들어 사용하고 싶다면, 함수명을 다르게해 선언해주거나 참조(Reference)를 이용하면 된다.
// 함수 이름을 다르게 한다.
float addfloat(int inum1, int inum2)
{
return inum1 + inum2;
}
// 참조를 이용한다.
void add(int& iresult, int inum1, int inum2)
{
iresult = inum1 + inum2;
}
2. typedef(타입 재정의)를 이용해 만든 타입은 새로운 타입이 아닌 단순히 재정의이기 때문에 오버로딩 할 수 없다.
typedef int INTEGER;
int add(int inum1, int inum2)
{
return inum1 + inum2;
}
int add(INTEGER inum1, INTEGER inum2)
{
return inum1 + inum2;
}
typedef을 이용해 만든 INTEGER는 결국 int 자료형과 일치되기 때문에 int 매개 변수를 받는 것과 같아서 오버로딩할 수 없다.
3. 함수의 매개 변수간 모호성이 없어야 한다.
모호성이 없어야하는 것을 알기 위해 먼저 오버로드 함수가 호출되면 어떤 프로세스를 통해 오버로드된 함수들 중에서 알맞은 함수를 호출하게 되는지 살펴보자.
오버로드(overload)된 함수를 호출하면 다음 세 가지 결과 중 하나가 발생한다.
1. 일치하는 함수가 있다. (호출이 특정 오버로드된 함수로 해석된다.)
2. 일치하는 함수가 없다. (오버로드된 함수 중에 인수가 일치하는 함수가 없다.)
3. 모호한 함수가 있다. (하나 이상의 오버로드된 함수와 인자가 일치한다.)
오버로드된 함수가 호출되면 C++은 다음 프로세스를 통해 오버로드된 함수들 중 호출할 함수를 결정한다.
1) 먼저 C++은 정확하게 일치하는 함수를 찾으려 한다. 이것은 실제 인수가 오버로드된 함수 중 하나의 매개 변수 타입과 정확히 일치하는 경우다.
int add(int inum1, int inum2)
{
return inum1 + inum2;
}
float add(float fnum1, float fnum2)
{
return fnum1 + fnum2;
}
void main()
{
int iresult1 = add(5, 3);
float iresult2 = add(1.f, 1.5f);
return;
}
iresult1의 줄에서 호출되는 add()는 인자로 int 2개를 받기 때문에 제일 첫 번째 add() 함수와 일치하고,
iresult2의 줄에서 호출되는 add()는 인자로 float2개를 받기 때문에 두 번째 add()함수와 일치합니다.
2) 정확히 일치하는 항목이 없으면 C++은 승격(promotion)을 통해 일치하는 함수를 찾으려고 한다.
char, unsigned char, short는 int로 승격된다.
unsigned short는 int의 크기에 따라 int 또는 unsigned int로 승격된다.
float은 double로 승격된다.
열거형(enum)은 int로 승격된다.
int add(int inum1, int inum2)
{
return inum1 + inum2;
}
float add(double fnum1, double fnum2)
{
return fnum1 + fnum2;
}
void main()
{
short snum1 = 5;
short snum2 = 10;
int iresult1 = add(snum1, snum2);
float iresult2 = add(1.f, 1.5f);
return;
}
short를 매개변수로 받는 오버로딩된 함수가 없기 때문에 int로 승격되어 오버로딩된 함수를 찾는다.
마찬가지로 float을 매개변수로 받는 오버로딩된 함수가 없기 때문에 double로 승격되어 함수를 찾는다.
3) 승격(promotion)이 불가능한 경우 C++은 표준 변환을 통해 일치하는 항목을 찾으려고 한다.
숫자 타입은 다른 숫자 타입으로 변환된다. ( ex) int -> float )
열거형(enum)은 위에서 말한 숫자 공식과 같다. ( ex) enum -> int -> float )
0은 포인터 타입 및 숫자 타입과 일치한다. ( ex) 0 -> char* or 0 -> float)
포인터는 void 포인터와 일치한다.
struct Employee;
void print(float value);
void print(Employee value);
print('a');
이 경우 print(char) 및 print(int)가 없으므로 'a'는 float으로 변환되어 print(float)과 일치한다.
4) C++은 사용자 정의 변환을 통해 일치하는 함수를 찾는다.
클래스는 해당 클래스의 객체와 암시적으로 적용될 수 있는 다른 타입으로 변환을 정의할 수 있다.
class X;
void print(float value);
void print(int value);
X value;
print(value);
예를 들어 클래스 X의 사용자 정의 변환을 int로 정의할 수 있다.
value는 클래스 X타입이지만 사용자가 정의한 int로 변환하므로 print(value)는 print(int)로 결정된다.
다음으로 이러한 프로세스를 진행했을 때 모호성이 발생하는 경우가 무엇이 있는지 살펴보자.
void print(unsigned int value);
void print(float value);
print('a');
print(0);
print(3.14159);
위 코드에서 print('a')의 경우 정확하게 일치하는 함수를 찾을 수 없다. 'a'를 int로 승격시켜 찾아보아도 없고, unsigned int와 float으로 변환할 수 있다. 이 두 표준 변환으로 인해 모호한 일치가 발생한다.
나머지 2 경우도 비슷하게
int 0 -> float & unsigned int
3.14159 - > double (이 경우에는 해당하는 함수가 없음) (C++에서 소수를 사용할때 f를 붙이지 않으면 double로 본다.)
모호한 일치가 발생하게 된다.
이러한 모호한 일치는 컴파일 타입 오류로 간주된다. 따라서 프로그램을 컴파일하기 전에 모호한 일치를 명확히 해야 한다.
1-2 연산자 오버로딩(Operator Overloading)
연산자 오버로딩은 사용자 정의 타입에서 연산자가 다른 기능을 할 수 있도록 만들어주는 것이다.
때문에 기본 자료형인 int, double 등의 타입에 대해서는 직접적으로 연산자 오버로딩을 할 수 없고, 클래스(Class)나 구조체(Struct)에 대한 연산자만을 오버로딩 할 수 있다.
함수 오버로딩, 생성자 오버로딩은 함수명, 생성자 명이 같으나 인자로 전달되는 매개 변수의 자료형이나 수가 다른 함수의 선언을 이용하여 다른 기능을 하는 함수를 만들었다.
이와 동일하게 연산자 오버로딩도 사용자 정의 타입에 한해 연산자가 기존의 기능이 아닌 다른 기능을 수행할 수 있도록 만드는 것이다.
ex) 기존 자료형에서 +는 기존 두 수를 더해준다. 하지만 사용자 정의 자료형에서 연산자 오버로딩을 하면, +의 기능을 두 수를 빼주거나 특정 문자열을 출력하는 등으로 만들 수 있다.
class CTest
{
private:
int m_i = 100;
public:
int operator+(int num)
{
return m_i + num;
}
}
void main()
{
int a = 0;
a += 20; // 0 + 20인 20이 a에 들어간다.
CTest i;
a = i + 20; // CTest 클래스에서 오버로딩된 '+'가 호출되어 120이 a에 들어간다.
return;
}
CTest라는 클래스에서 '+' 연산자를 멤버 변수 m_i와 전달 받은 인자를 더해 반환해 주도록 오버로딩했다.
때문에 a = i + 20;의 '+' 연산자 결과는 120이 반환된다.
추가로 공부할 자료
C++ 강좌 15편. 연산자 오버로딩(Operator Overloading)
1. 연산자 오버로딩(Operator Overloading)이번엔 함수 오버로딩, 생성자 오버로딩도 아닌 연산자 오버로딩입니다. 함수 오버로딩, 생성자 오버로딩은 함수명, 생성자명이 같으나, 인자의 자료형이나
blog.hexabrain.net
2. 오버라이딩(Overriding) : 함수 재정의
오버라이딩은 상속 받았을 때 부모 클래스의 함수를 사용하지 않고 다른 기능을 실행할 때 함수를 자식 클래스에 같은 이름, 매개 변수로 재정의해서 사용하는 것이다.
C++에서 파생 클래스는 상속을 받을 때 명시한 접근 제어 권한에 맞는 기초 클래스의 모든 멤버를 상속 받는다.
이렇게 상속받은 멤버 함수는 그대로 사용해도 되고, 필요한 동작을 위해 재정의하여 사용할 수 있다.
C++에서는 2가지 방법으로 멤버 함수를 오버라이딩 할 수 있다.
1. 파생클래스에서 직접 오버라이딩하는 방법
#include <string>
#include <vector>
#include <iostream>
using namespace std;
class CPet
{
public:
string str_name;
vector<int> vec_pos;
public:
void Cry() { cout << "울음소리를 낸다."; }
public:
CPet();
~CPet();
};
class CCat
: public CPet
{
private:
eCAT_TYPE e_CatType;
public:
void Cry() { cout << "야. 옹."; }
public:
CCat();
~CCat();
};
위 예제에서는 CPet 클래스의 Cry()를 자식 클래스인 CCat 클래스에서 재정의 했습니다.
때문에 CPet의 Cry()를 호출하면 울음소리를 낸다.가 출력되고, CCat의 Cry()를 호출하면 야. 옹.을 출력해 줍니다.
재정의 하지 않았다면 CCat의 Cry()를 호출하면, CPet의 Cry()가 호출됩니다.
또한 CCat으로 CPet의 Cry()를 호출할 수 도 있는데, 그 경우 범위 지정 연산자를 이용하면 됩니다.
void main()
{
CPet* Pet = new CPet;
CCat* Cat = new CCat;
Cat->CPet::Cry();
return;
}
이렇게 범위 지정 연산자를 이용해 함수를 호출하게 되면, CCat의 Cry()가 아닌 CPet의 Cry()가 호출되어 울음소리를 낸다.가 출력됩니다.
2. 가상 함수를 이용해 오버라이딩하는 방법
자식 클래스에서 멤버 함수를 직접 재정의하는 방법은 일반적인 상황에서는 잘 동작한다.
하지만 클래스의 특징 중 하나인 상위 클래스의 포인터 자료형으로 하위 클래스를 받아 사용할 때 문제가 발생 할 수 있습니다.
void main()
{
CPet* Pet = new CPet;
CCat* Cat = new CCat;
CPet* ptr_Pet;
ptr_Pet = Pet;
ptr_Pet->Cry();
ptr_Pet = Cat;
ptr_Pet->Cry();
return;
}
위 코드와 같이 CPet과 CCat으로 만든 객체인 Pet과 Cat을 CPet*로 받아 Cry()를 호출하게 되면 결과는 둘다 울음소리를 낸다.가 출력된다.
왜냐하면 Cat이 CCat 클래스로 만든 객체여도 CPet*이기 때문에 CPet의 Cry()가 호출되기 때문이다.
이전에 공부하기로 이러한 이유는 CCat클래스로 Cat을 만들 때, CPet 클래스에 대한 내용이 먼저 작성이 되고, 자식 객체인 CCat 클래스에 대한 내용이 뒤에 이어 작성된다.
이후 접근할때 CPet*로 접근을 하게되면, 딱 CPet의 내용까지만 보는데, 이러한 접근 방식으로 인해 하위 클래스는 상위 클래스 변수로 담아 사용할 수 있는 특징이 있는 것이다.
(C++ 컴파일러는 포인터 변수가 실제로 가리키는 객체 타입을 기준으로 접근하는 것이 아닌, 해당 포인터의 타입을 기준으로 접근한다.)
이 처럼 파생 클래스들을 한번에 관리하기 위해 상위 클래스 변수로 담아 사용하는 일이 빈번히 일어나는데, 이럴 때는 자식 클래스에서 멤버 함수를 직접 재정의를 해도 의도한대로 동작하지 않는 경우가 발생한다.
이럴 때 가상함수를 이용해 오버라이딩을 하면 된다.
#include <string>
#include <vector>
#include <iostream>
using namespace std;
class CPet
{
public:
string str_name;
vector<int> vec_pos;
public:
virtual void Cry() { cout << "울음소리를 낸다."; }
public:
CPet();
~CPet();
};
class CCat
: public CPet
{
private:
//eCAT_TYPE e_CatType;
public:
void Cry() { cout << "야. 옹."; }
public:
CCat();
~CCat();
};
virtual 키워드는 해당 함수를 파생 클래스에서 재정의할 것으로 기대하는 멤버 함수를 의미합니다.
가상 함수는 자신을 호출하는 객체의 동적 타입에 따라 실제 호출할 함수가 결정됩니다.
이때, 기초 클래스에서 virtual 키워드를 사용해 가상 함수를 선언하면, 파생 클래스에서 재정의된 멤버 함수도 자동으로 가상 함수가 됩니다.
이러한 가상 함수는 동적 바인딩을 하기 때문에 객체의 동적 타입에 따라 실제 호출할 함수를 결정할 수 있습니다.
C++ 컴파일러는 함수를 호출할 때, 어느 블록에 있는 함수를 호출해야 하고, 해당 함수가 저장된 정확한 메모리 위치까지 알아야 합니다.
이처럼 함수를 호출하는 코드에서 어느 블록에 있는 함수를 실행하라는 의미로 해석 되는 것을 바인딩(binding)이라고 합니다.
대부분 함수를 호출하는 코드는 컴파이리 타입에 고정된 메모리 주소로 변환됩니다.
이것을 정적 바인딩(static binding)혹은 초기 바인딩(early binding)이라고 합니다.
C++에서는 가상함수가 아닌 멤버 함수는 모두 이러한 정적 바인딩을 하게 됩니다.
하지만 가상 함수의 호출은 컴파일러가 어떤 함수를 호출해야 하는지 미리 알 수 없습니다.
왜냐하면, 가상 함수는 프로그램이 실행될 때 객체를 결정하므로, 컴파일 타임에 해당 객체를 특정할 수 없기 때문입니다.
따라서 가상 함수의 경우에는 런 타임에 올바른 함수가 실행 될 수 있도록 해야합니다.
이것을 동적 바인딩(dynamic binding) 또는 지연 바인딩(late binding)이라고 합니다.
하지만 가상 함수도 결합하는 타입이 분명할 때에는 일반 함수와 같이 정적 바인딩을 합니다.
이러한 가상 함수는 기초 클래스 타입의 포인터나 참조를 통하여 호출될 때만 동적 바인딩을 하게 됩니다.
C++에서는 가상 함수의 정의와 동작 방식만을 규정하고 있으며, 그에 따른 구현은 컴파일러마다 다릅니다.
하지만 컴파일러가 가상 함수를 다루는 가장 일반적인 방식은 가상 함수 테이블(virtual function table)을 이용하는 것입니다.
C++ 컴파일러는 각각의 객체마다 가상 함수 테이블을 가리키는 포인터를 저장하기 위한 숨겨진 멤버를 하나씩 추가합니다.
이와 함께 가상 함수를 단 하나라도 가지는 클래스에 대해서 가상 함수 테이블을 작성합니다.
이렇게 작성된 가상 함수 테이블에는 해당 클래스의 객체들을 위해 선언된 가상 함수들의 주소가 저장되게 됩니다.
가상 함수를 호출하면, C++ 프로그램은 가상 함수 테이블에 접근하여 자신이 필요한 함수의 주소를 찾아 호출하게 됩니다.
가상 함수를 사용하면 이처럼 함수의 호출 과정이 복잡해지므로, 메모리와 실행 속도 측면에서 약간의 부담을 가지게 됩니다.
따라서 C++에서 기본 바인딩은 정적 바인딩이며, 필요한 경우에만 가상 하마수로 선언하도록 하고 있습니다.
(하지만 파생 클래스가 재정의할 가능성이 있는 함수는 모두 가상 함수로 선언하는 편이 좋습니다.)
추가로 C++에서 클래스의 소멸자들은 반드시 가상으로 선언해야합니다.
왜냐하면 상위 클래스의 소멸자를 호출하게 되면, 하위 클래스 쪽의 소멸자는 호출되지 않아 메모리 해제가 안돼 메모리 누수가 발생하는 불상사가 생길 수 있기 때문입니다.
때문에 virtual 키워드를 사용해 CPet 클래스의 Cry()를 가상 함수로 만들면, CCat 객체를 CPet*로 접근해 Cry()를 호출해도 CPet의 Cry()가 아닌 CCat의 Cry()가 호출되게 됩니다.
추가적으로 CPet은 단순히 기초 클래스이기 때문에 Cry()라는 기능은 필요 없는 기능입니다. 때문에 CPet에 Cry()를 없애고 싶습니다.
하지만 CPet에서 파생된 CCat이나 CDog, CRet의 경우 Cry()기능이 필요하고 상위 클래스 포인터를 이용해 접근하게 되면, CPet에 Cry()함수가 없기 때문에 호출할 수 없습니다.
class CPet
{
public:
string str_name;
vector<int> vec_pos;
public:
CPet();
virtual ~CPet();
};
class CCat
: public CPet
{
public:
void Cry() { cout << "야. 옹."; }
public:
CCat();
virtual ~CCat();
};
void main()
{
CPet* Pet = new CPet;
CCat* Cat = new CCat;
CPet* ptr_Pet;
ptr_Pet = Pet;
ptr_Pet->Cry();
ptr_Pet = Cat;
ptr_Pet->Cry();
return;
}
이 경우 CPet에는 Cry()가 없기 때문에 ptr_Pet으로는 Cry()를 호출 할 수 없습니다.
이럴 때에는 가상 함수를 이용해 CPet에서는 아무 기능을 하지 않는 Cry()를 만들어주고, 하위 클래스에서 기능을 작성해주면 됩니다.
class CPet
{
private:
string str_name;
vector<int> vec_pos;
public:
virtual void Cry() {} // 아무런 기능을 하지 않는다.
public:
CPet();
virtual ~CPet();
};
class CCat
: public CPet
{
public:
void Cry() { cout << "야. 옹."; }
public:
CCat();
virtual ~CCat();
};
추가적으로 순수 가상 함수라는 것이 존재합니다.
class CPet
{
public:
virtual void Cry() = 0; // 순수 가상 함수
public:
CPet();
virtual ~CPet();
};
class CCat
: public CPet
{
private:
string str_name;
vector<int> vec_pos;
public:
void Cry() { cout << "야. 옹."; }
public:
CCat();
virtual ~CCat();
};
하지만 이렇게 순수 가상 함수를 이용해 추상 클래스를 만들면, CPet을 이용한 객체를 만들 수 없습니다.
또 추상 클래스를 만들 때는 멤버 변수 없이 순수 가상 함수로만 이루어진 클래스로 설계하는게 좋다고 합니다.
참고 문헌
C++ 08.07 - 함수 오버로딩 (Function overloading)
함수 오버로딩 (Function overloading) 함수 오버로딩(function overloading)은 다른 매개 변수를 가진 같은 이름의 여러 함수를 만들 수 있는 C++의 기능이다. 다음 함수를 보자. int add(int x, int y) { return x + y; }
boycoding.tistory.com
코딩교육 티씨피스쿨
4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등
tcpschool.com
'프로그래밍 > C++' 카테고리의 다른 글
C++ 메모리 구조 (0) | 2024.01.29 |
---|---|
C++ new와 delete/ malloc()과 free() (0) | 2024.01.29 |
C++ 참조형 변수 (Reference variable) (0) | 2024.01.29 |
C++ 언어의 특징 (1) | 2024.01.26 |
객체지향 프로그래밍 (4) | 2024.01.24 |