C++/C++ 기초

[C++] static 멤버 / const 멤버

Song 컴퓨터공학 2023. 7. 10. 15:13

오늘은 클래스 내에서 static 으로 지정한 static 멤버와 const 멤버에 대해 알아보겠습니다. 먼저 static 부터 알아볼건데 그전에 static을 왜 쓰느냐를 알면 더 좋겠죠? static 은 객체 지향 언어의 특징 중 하나인 Encapsulation, 캡슐화에 중요한 역할을 합니다. static 을 통해 거의 모든 함수나 변수를 class 안에 선언하도록 할 수 있습니다. 쉽게 생각하면 static 변수나 함수는 전역 변수, 전역 함수와 비슷한 역할을 하는데 그 범위를 class 로 제한하는 키워드입니다.

 

static 은 변수와 함수의 생명 주기와 사용 범위를 지정하는 방식 중 하나로, static 으로 선언된 변수와 함수의 생명 주기와 사용 범위는 아래와 같습니다.

 

  • 생명 주기(life cycle) : 프로그램이 시작할 때 생성되고 프로그램이 종료될 때 소멸
  • 사용 범위(scope) : 변수나 함수가 선언된 범위 내에서 사용. 전역(global) 혹은 지역(local) 으로 구분

 

모든 변수와 함수는 static 지정자로 선언할 수 있습니다. 클래스 위주로 생각해서 평소 따로 static 으로 선언하지 않은 변수와 함수를 non-static 멤버라고 합니다.non-static 멤버는 객체가 생성될 때 생성되고, 각 객체마다 별도로 생성됩니다. 즉, 객체와 같은 생명 주기를 가집니다. 그러나 static 멤버를 클래스 내에 선언하면 멤버 자체는 객체의 멤버가 맞지만 객체가 생기기 이전에 이미 생성되어 있고, 객체가 소멸한다고 static 멤버까지 소멸되지 않습니다. 또한 static 멤버는 모든 객체들의 공통된 멤버로 객체 사이에 공유됩니다.

 

non-static 멤버는 각 객체마다 별도로 생성되므로 인스턴스(instance) 멤버 라고 부르며, static 멤버는 클래스 당 하나만 생기고 모든 객체들이 공유하므로 클래스(class) 멤버 라고 부릅니다.

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 멤버 변수는 외부에 전역 변수로 선언해야 한다.

 

static 지정자를 앞에 붙여서 static 멤버를 선언할 수 있습니다. 또한 static 멤버 변수는 외부에 전역변수로 선언되어야만 합니다. 까먹기 좋은 내용입니다. 위처럼 클래스 바깥에 전역 변수로 선언하지 않으면 링크 오류가 발생합니다. 위에서 다룬 내용들이 많으니 표로 간략히 정리하면

 

항목 non-static 멤버 static 멤버
선언 사례 class Sample{
    int n;
    void f();
};
class Sample{
    static int n;
    static void f();
}
공간 특성 멤버는 객체마다 별도로 생성 -> 인스턴스 멤버 멤버는 클래스 당 하나 생성 -> 클래스 멤버
- 멤버는 객체 내부가 아닌 별도 공간에 생성
시간적 특성 객체와 생명을 같이한다.
- 객체 생성 시에 멤버 생성
- 객체 소멸 시 함께 소멸
- 객체 생성 후 멤버 사용 가능
프로그램과 생명을 같이한다.
- 프로그램 시작 시 멤버 생성
- 객체가 생기기 전에 이미 존재
- 객체가 사라져도 여전히 존재
- 프로그램이 종료될 때 함께 소멸
공유 특성 공유 X -> 멤버는 객체 별로 따로 공간 유지 동일한 클래스의 모든 객체들에 의해 공유

 

 

위 같은 특성 때문에 static 멤버 함수는 오직 static 멤버들만 접근할 수 있습니다. static 은 객체를 생성하지 않고도 사용할 수 있기 때문에 객체가 생성되어야만 존재하는 non-static 멤버에는 접근할 수 없는 것이죠. 또한 이와 같은 특성 때문에 객체 자신의 주소를 반환하는 this 포인터는 static 멤버 함수에서 사용할 수 없습니다. 객체가 생성되기 이전부터 static 멤버는 존재하기 때문에 this 포인터를 사용하면 오류가 발생할 수 있는 것이죠.

 

아래 처럼 코드를 작성하면 컴파일 오류가 발생합니다.

 

class PersonEror{
	int money;
public:
	static int get<oney() { return money; } // non-static 멤버에 접근. 컴파일 오류
    ...

 

그러나 반대로 non-static 멤버 함수는 당연히 static 멤버에 접근해도 괜찮습니다. 객체가 생성되는 시점에 무조건 static 멤버가 생성되어 있기 때문이죠.


 

클래스의 static 멤버에 접근하는 방법은 크게 2가지가 있습니다.

1. 객체의 멤버로 접근하는 방법

2. 클래스명과 :: 로 접근하는 방법

// 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; // static 멤버 변수는 외부에 전역 변수로 선언해야 한다.

int main() {
	Person::sharedMoney = 1000; // 클래스명과 :: 로 static 멤버 접근

	cout << "sharedMoney = " << Person::sharedMoney << endl;

	Person song;
	song.money = 100;
	song.sharedMoney += 200; // 객체의 멤버로 static 멤버 접근

	cout << "sharedMoney = " << song.sharedMoney << endl;
}

클래스 만으로도 객체 생성전 static 변수에 접근할 수도 있고, 객체를 선언한다면 객체의 멤버로도 static 멤버에 접근할 수 있습니다. static 멤버를 사용하는 경우는 보통 class 내로 묶어 전역 함수를 줄이거나, 클래스 내의 객체에 공유 변수를 만들고자 할 때 많이 활용됩니다.

 

// static 멤버를 활용해 전역 함수를 줄이는 예
#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) ? a : b; }
};

int main() {
	cout << Math::abs(-1) << endl;
	cout << Math::max(2, 5) << endl;
	cout << Math::min(5, 2) << endl;
}

이렇게 비슷한 기능 혹은 범주에 있는 함수들을 한 클래스로 묶고 static 멤버로 선언하면 굳이 객체를 생성하지 않고도 구분하여 클래스를 통해 접근할 수 있기 때문에 코드의 가독성도 좋아지고, 전역 함수의 선언을 줄일 수 있습니다.

 

// 객체 사이 공유 변수를 사용하는 예
#include <iostream>
using namespace std;

class Circle {
private:
	static int numOfCircles;
	int radius;
public:
	Circle(int r = 1) { radius = r; numOfCircles++; }
	~Circle() { numOfCircles--; }
	double getArea() { return 3.14 * radius * radius; }
	static int getNumOfCircles() { return numOfCircles; }
};

int Circle::numOfCircles = 0;

int main()
{
	Circle* p = new Circle[10];
	cout << "Circle 객체의 개수 : " << Circle::getNumOfCircles() << endl;

	delete[] p;
	cout << "Circle 객체의 개수 : " << Circle::getNumOfCircles() << endl;

	Circle a;
	cout << "Circle 객체의 개수 : " << Circle::getNumOfCircles() << endl;

	Circle b;
	cout << "Circle 객체의 개수 : " << Circle::getNumOfCircles() << endl;
}


 

 const 멤버와 const 객체

 

const 는 constant 의 약자로 상수, 즉 변함없는 이라는 뜻을 가지는 키워드입니다. 이 키워드를 변수 앞에 붙이면 값을 변경할 수 없고 그 변수를 상수처럼 취급하게 됩니다.

 

이를 객체와 멤버로 확장해보면 어떤 특징이 있는지 알아봅시다.

class Circle {
	double radius;
	const double pi;
public:
	Circle(double r = 0) : radius(r), pi(3.14) {} // const 변수 pi 초기화
	void SetRadius(double r) { radius = r; }
	double const GetArea() { return (pi * radius * radius); }
};

int main(void) {
	Circle c(1);
	cout << "면적 : " << c.GetArea() << endl;
}

pi 는 원주율이죠. 따라서 바뀌면 안되는 값입니다. 이처럼 멤버 변수에 대해서 const 키워드를 사용하면 상수취급을 하기 때문에 변화시킬 수 없는 값이 됩니다. 유의점으로 const 멤버 변수는 객체 생성과 동시에 초기화를 꼭 해줘야 합니다.

 

그 다음으로 GetArea() 라는 함수에도 const 키워드가 붙어있습니다. const 멤버 함수는 멤버 변수의 값을 읽을 수는 있으나 변경할 수 없는 함수 입니다. 또한 멤버 변수의 주소를 반환할 수 없고, 비 const 멤버 함수의 호출 또한 할 수 없습니다. 왜냐면 비 const 멤버가 만약 멤버 변수의 값을 바꾸는 함수라면 const 함수 호출만으로 값이 변경될 수 있기 때문입니다. 

 

즉 const 키워드를 사용하면 값을 못 바꾼다 라고 인식하고 사용하면 됩니다. 값을 바꾸면 안되는 상황에 const 키워드를 자주 사용합니다.