[C++] 함수 중복과 static 멤버

HY K·2024년 9월 29일

명품 C++ 프로그래밍

목록 보기
24/24

이번 포스팅에서는 함수의 중복과 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; }
};

함수 중복의 모호함(ambiguous)

함수 호출이 모호한 경우 컴파일 오류를 발생한다.
함수 중복으로 인해서 발생할 수 있는 모호성은 다음 3가지가 있다.

💡 함수 중복으로 인한 모호성

  • 형 변환으로 인한 모호성
  • 참조 매개변수로 인한 모호성
  • 디폴트 매개변수로 인한 모호성

형 변환으로 인한 모호성

  • 함수의 매개변수 타입과 호출 문의 실인자 타입이 일치하지 않는 경우 컴파일러에서 형 변환(type conversion)을 실시
double square(double a);
square(3); // 컴파일러에서 형 변환 발생
  • 컴파일러는 작은 크기의 타입에서 큰 크기의 타입으로 형 변환을 지속함
char -> int -> long -> float -> double
  • 하지만, 중복된 함수가 작성되어 있는 경우 적절한 실인자의 타입에 맞는 함수가 없을 경우 컴파일 오류가 발생함
square(float a);
square(double a);

// 이럴 경우 int a에 맞는 함수 중복이 없기 때문에 컴파일 오류 발생
  • 컴파일러는 이를 float 타입으로 변환해야하는지, double 타입으로 변환해야하는지 판정해주지 못함

예시 코드를 보자.

#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의 특성

💡 static의 정의
static은 변수와 함수의 life cycle과 사용 범위(scope)를 지정하는 방식(storage clasS) 중 하나로, static으로 선언된 변수와 함수는 다음 특징을 가지게 된다.

  • life cycle : 프로그램이 시작할 때 생성하고, 프로그램이 종료될 때 소멸
  • 사용 범위 : 변수나 함수가 선언된 범위 내에서 사용, global과 local 구분
  • 클래스를 포함해 C++의 모든 변수와 함수는 static으로 선언 가능하다.

Non-static 멤버들과 static 멤버들의 차이는 다음과 같다.

Non-static 멤버 : 각 객체와 생명주기를 함께 한다.
static 멤버 : 객체가 생성되기도 전에 생성되서, 객체가 소멸해도 사라지지 않는다.

그렇기 때문에 Non-static 멤버는 인스턴스 멤버 라고 부르고, static 멤버는 클래스당 하나만 생기고, 모든 객체들이 공유하고, 클래스 멤버 라고 부른다.

static 멤버 선언

  • 간단히, 선언을 원하는 멤버 함수나 멤버 변수 앞에 static 지정자를 붙히면 된다. 클래스당 하나만 생성한다.
  • 여기서 정말 중요한 것은, 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;
// 반드시 전역 공간에 생성

static 멤버의 사용법

  • 보통 멤버들과 동일하게 객체 이름(.)이나 객체 포인터(->)를 사용하면 된다.
#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;
}
  • static 멤버는 클래스 당 하나만 존재하므로 클래스의 이름으로도 접근이 가능하다.
  • 클래스 이름과 static 멤버 사이의 범위 지정 연산자(::)를 사용하여 접근한다.
className::static_member

위의 코드를 기반으로 예시를 들면 다음과 같다.

Person::sharedMoney = 200;
Person::addShared(200);
  • 이렇게 클래스 이름과 범위 지정 연산자를 사용하면, 어떤 객체를 생성하기도 전에 static 멤버를 활용할 수 있다.

static을 사용하는 이유?

  • 전역 변수나 전역 함수를 클래스에 캡슐화 : 전역 변수와 전역 함수의 사용을 최소화 하고 OOP의 기본 원리를 추구할 수 있다.
#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 멤버 변수에 접근하거나, static 멤버 함수만 호출이 가능하다. 왜냐면 static 멤버들은 객체 생성 전에도 존재하고, 클래스 이름으로도 직접 호출이 가능한 독특한 특징을 가지고 있기 때문에 static 멤버 함수에서 non-static 멤버에 접근하는 것을 허용하지 않는 것이다.

하지만!!
이와 반대로 non-static 멤버들은 static 멤버에 접근하는 것에 대한 제약이 없다.

static 멤버 함수는 this를 사용할 수 없다.

static 멤버 함수는 객체가 생기기 전부터 호출이 가능하므로 this를 사용할 수 없다.

profile
로봇, 드론, SLAM, 제어 공학 초보

0개의 댓글