이번 포스팅에서는 함수의 중복과 static 멤버에 대해서 알아볼 것이다.
💡 함수 중복(function overloading)
C++에서는 C와 다르게 함수를 여러 개를 만들 수 있으며, 이것을 함수 중복(function overloading)이라고 부른다. 함수 중복은 다형성(polymorphism)의 한 사례로 전역 함수(global function)와 멤버 함수(member function) 모두에 적용되며, 상속 관계에도 적용된다.
💡 중복 함수의 조건
1. 중복된 함수들의 이름이 동일해야 한다.
2. 중복된 함수들의 매개변수 타입이나 매개변수 개수가 달라야 한다.
3. 리턴 타입은 고려되지 않는다.
위 조건을 보면 알 수 있겠지만,
함수 이름, 매개변수 타입, 매개변수 개수가 모두 같은 함수들끼리 리턴 타입만 다르게 설정하면, 그건 함수 중복이 아니라 컴파일 오류가 발생한다.
그리고 함수 중복에 대한 판정과 호출은 컴파일 시간에 이루어지기 때문에, 함수 중복으로 인한 실행 시간 저하는 없다.
예시 코드를 살펴보자.
다음 코드는 두 함수의 함수 중복을 구현한 예시이다.
#include<iostream>
using namespace std;
int big(int a, int b) {
if (a > b) return a;
else return b;
}
int big(int a[], int size) {
int res = a[0];
for (int i = 1; i < size; i++) {
if (res < a[i]) res = a[i];
}
return res;
}
int main() {
int array[5] = { 1,9,-2, 4,6 };
cout << big(2, 3) << endl;
cout << big(array, 5) << endl;
}
이번 코드 역시 마찬가지로 두 함수의 중복을 구현한 사례이다.
#include<iostream>
using namespace std;
int sum(int a, int b) {
int s = 0;
for (int i = a; i <= b; i++)
s += i;
return s;
}
int sum(int a) {
int s = 0;
for (int i = 0; i <= a; i++)
s += i;
return s;
}
int main() {
cout << sum(3, 5) << endl;
cout << sum(3) << endl;
cout << sum(100) << endl;
}
💡 객체 생성자, 소멸자의 중복?
- 생성자 함수는 앞서 살펴보았듯 당연히 중복이 가능하다.
- 하지만 소멸자는 오직 한개만 존재하기 때문에, 그리고 매개변수가 존재하지 않기 때문에 근본적으로 중복이 불가능하다.
💡 디폴트 매개변수(default parameter)
함수를 호출할 때 매개변수에 값이 넘어오지 않는다면, 미리 정해진 디폴트 값을 받도록 선언한 매개변수를 디폴트 매개변수 라고 부른다.
매개변수=디폴트 값 형식으로 선언한다.
ex)
void star(int a=5);
위 예시 같은 경우에는 호출 시에는 매개변수를 써도 좋고, 안써도 좋다. 매개변수를 쓰지 않으면 디폴트 값이 들어가기 때문이다.
이렇게 디폴트 매개변수에 디폴트 값 전달은 컴파일러에 의해 자동으로 처리된다.
💡 디폴트 매개변수 선언시 주의할 점
디폴트 매개변수를 가진 함수를 선언할 때는 반드시 오른쪽 끝에 몰아서 선언해야 한다.void calc(int a, int b=5, int c, int d=0); // 컴파일 오류 void sum(int a=0, int b, int c); // 컴파일 오류
💡 디폴트 매개변수의 처리 방법
컴파일러에서 함수의 매개변수를 처리할 때, 호출문에 나열된 실인자 값들을 앞에서부터 순서대로 함수의 매개변수에 전달하고 나머지는 디폴트로 전달한다. 따라서 디폴트 매개변수들은 우측으로 몰아서 작성해야 한다.
예를 들어서 매개변수가 4개 선언되고, 그 중 디폴트 매개변수가 3개라면 여러 방법으로 부를 수 있다.
void g(int a, int b=0, int c=0, int d=0);
g(10);
g(10,5);
g(10,5,20);
g(10,5,20,30);
💡 포인터 매개변수의 디폴트 값
포인터 변수를 디폴트 매개변수로 선언할 때도 디폴트 값을 줄 수 있다.void f(int *p=NULL); void g(int x[]=NULL); void h(const char* s = "Hello");
디폴트 매개변수는 다음과 같은 장점을 제공한다.
하지만 디폴트 매개변수를 가진 함수를 작성할 때는 다음을 주의해야한다.
Class Circle{
...
public:
Circle(){radius=1;}
Circle(int r){radius = r;}
Circle(int r=1){radius = r;} // 중복된 함수 사용 불가
};
예시 코드를 한번 살펴보자.
#include<iostream>
using namespace std;
class MyVector {
int* p;
int size;
public:
MyVector(int n = 100) {
p = new int[n];
size = n;
}
~MyVector() { delete[]p; }
};
int main() {
MyVector* v1, * v2;
v1 = new MyVector();
v2 = new MyVector(1024);
delete v1;
delete v2;
}
위 코드는 다음 코드를 통폐합 해서 간단하게 만든 코드이다.
class MyVector {
int* p;
int size;
public:
MyVector() {
p = new int[100];
size = 100;
}
MyVector(int n) {
p = new int[n];
size = n;
}
~MyVector() { delete[]p; }
};
함수 호출이 모호한 경우 컴파일 오류를 발생한다.
함수 중복으로 인해서 발생할 수 있는 모호성은 다음 3가지가 있다.
💡 함수 중복으로 인한 모호성
- 형 변환으로 인한 모호성
- 참조 매개변수로 인한 모호성
- 디폴트 매개변수로 인한 모호성
double square(double a);
square(3); // 컴파일러에서 형 변환 발생
char -> int -> long -> float -> double
square(float a);
square(double a);
// 이럴 경우 int a에 맞는 함수 중복이 없기 때문에 컴파일 오류 발생
예시 코드를 보자.
#include<iostream>
using namespace std;
float square(float a) {
return a * a;
}
double square(double a) {
return a * a;
}
int main() {
cout << square(3.0);
cout << square(3); // 오류 발생
}
14번째 라인에서 오류가 발생한다.
int add(int a, int b);
int add(int a, int &b);
예시 코드를 한번 살펴보면 다음과 같다.
#include<iostream>
using namespace std;
int add(int a, int b) {
return a + b;
}
int add(int a, int& b) {
b = b + a;
return b;
}
int main() {
int s = 10, t = 20;
cout << add(s, t);
}
15번째 라인에서 오류가 발생한다.
디폴트 매개변수를 가진 함수가 보통의 매개변수를 가진 함수와 중복 작성될 때 모호성이 존재할 수 있다. 예시 코드를 살펴보자.
#include<iostream>
#include<string>
using namespace std;
void msg(int id) {
cout << id << endl;
}
void msg(int id, string s = "") {
cout << id << ":" << s << endl;
}
int main() {
msg(5, "Good Morning");
msg(6);
}
15번째 라인에서 모호성으로 인해 컴파일 오류가 발생한다.
💡 추가 정보
마찬가지로, 포인터 변수 역시 모호함으로 인해 컴파일 오류가 발생할 수 있다. 배열의 이름은 포인터기 때문에 다음과 같이 작성할 수 없다.int add(int *p); int add(int p[]);
💡 static의 정의
static은 변수와 함수의 life cycle과 사용 범위(scope)를 지정하는 방식(storage clasS) 중 하나로, static으로 선언된 변수와 함수는 다음 특징을 가지게 된다.
- life cycle : 프로그램이 시작할 때 생성하고, 프로그램이 종료될 때 소멸
- 사용 범위 : 변수나 함수가 선언된 범위 내에서 사용, global과 local 구분
- 클래스를 포함해 C++의 모든 변수와 함수는 static으로 선언 가능하다.
Non-static 멤버들과 static 멤버들의 차이는 다음과 같다.
Non-static 멤버 : 각 객체와 생명주기를 함께 한다.
static 멤버 : 객체가 생성되기도 전에 생성되서, 객체가 소멸해도 사라지지 않는다.
그렇기 때문에 Non-static 멤버는 인스턴스 멤버 라고 부르고, static 멤버는 클래스당 하나만 생기고, 모든 객체들이 공유하고, 클래스 멤버 라고 부른다.
예를 들면 다음과 같다.
class Person{
public:
int money;
void addMoney(int money){
this->money+=money;
}
static int sharedMoney;
static void addShared(int n){
sharedMoney+=n;
}
};
int Person::sharedMoney = 10;
// 반드시 전역 공간에 생성
#include<iostream>
using namespace std;
class Person {
public:
int money;
void addMoney(int money) {
this->money += money;
}
static int sharedMoney;
static void addShared(int n) {
sharedMoney += n;
}
};
int Person::sharedMoney = 10;
int main() {
Person han;
han.money = 100;
han.sharedMoney = 200;
Person lee;
lee.money = 150;
lee.addMoney(200);
lee.addShared(200);
cout << han.money << ' ' << lee.money << endl;
cout << han.sharedMoney << ' ' << lee.sharedMoney << endl;
}
className::static_member
위의 코드를 기반으로 예시를 들면 다음과 같다.
Person::sharedMoney = 200;
Person::addShared(200);
#include<iostream>
using namespace std;
class Math {
public:
static int abs(int a) { return a > 0 ? a : -a; }
static int max(int a, int b) { return (a > b) ? a : b; }
static int min(int a, int b) { return (a > b) ? b : a; }
};
int main() {
cout << Math::abs(-5) << endl;
cout << Math::max(10, 8) << endl;
cout << Math::min(-3, -8) << endl;
}
static 멤버 함수는 static 멤버 변수에 접근하거나, static 멤버 함수만 호출이 가능하다. 왜냐면 static 멤버들은 객체 생성 전에도 존재하고, 클래스 이름으로도 직접 호출이 가능한 독특한 특징을 가지고 있기 때문에 static 멤버 함수에서 non-static 멤버에 접근하는 것을 허용하지 않는 것이다.
하지만!!
이와 반대로 non-static 멤버들은 static 멤버에 접근하는 것에 대한 제약이 없다.
static 멤버 함수는 객체가 생기기 전부터 호출이 가능하므로 this를 사용할 수 없다.