접근 지정자
접근 지정자 (Access specifiers)
클래스의 멤버 선언부(member-specification)에서 '이 뒤로 나오는 멤버들은 어디까지 공개한다'를 정하는 게 접근 지정자예요. 상속을 받을 때는 기반 클래스에서 물려받은 멤버의 접근성을 어떻게 이어받을지도 접근 지정자가 결정해요. 이 페이지에서 public / protected / private가 정확히 언제 통용되는지 규칙 기준으로 정리해 볼게요.
출처: cppreference
본문
문법
public : member-declarations (1)
protected : member-declarations (2)
private : member-declarations (3)
public base-class (4)
protected base-class (5)
private base-class (6)
- 접근 지정자 뒤에 선언된 멤버들은 public 멤버 접근을 가져요.
- 접근 지정자 뒤에 선언된 멤버들은 protected 멤버 접근을 가져요.
- 접근 지정자 뒤에 선언된 멤버들은 private 멤버 접근을 가져요.
- public 상속: 지정자 뒤에 나열된 기반 클래스의 public·protected 멤버는 파생 클래스에서도 그 접근을 유지해요.
- protected 상속: 지정자 뒤에 나열된 기반 클래스의 public·protected 멤버는 파생 클래스에서 protected 멤버가 돼요.
- private 상속: 지정자 뒤에 나열된 기반 클래스의 public·protected 멤버는 파생 클래스에서 private 멤버가 돼요.
기반 클래스의 private 멤버는 public / protected / private 상속 어느 것을 쓰든 파생 클래스에서 항상 접근할 수 없어요.
설명
클래스 멤버의 이름(정적·비정적, 함수, 타입 등)은 전부 어떤 '멤버 접근'과 연결돼 있어요. 이름이 프로그램 어디에서든 사용될 때 그 접근이 검사되고, 접근 규칙을 만족하지 않으면 컴파일되지 않아요.
#include <iostream>
class Example
{
public: // 여기서부터의 모든 선언은 public
void add(int x) // 멤버 "add"는 public 접근
{
n += x; // OK: private인 Example::n은 Example::add에서 접근 가능
}
private: // 여기서부터의 모든 선언은 private
int n = 0; // 멤버 "n"은 private 접근
};
int main()
{
Example e;
e.add(1); // OK: public인 Example::add는 main에서 접근 가능
// e.n = 7; // error: private인 Example::n은 main에서 접근 불가
}
접근 지정자는 클래스 작성자에게 '어떤 멤버를 클래스 사용자에게 공개할지(인터페이스), 어떤 멤버를 클래스 내부 전용으로 둘지(구현)'를 정할 권한을 줘요.
자세히
클래스의 모든 멤버 선언(클래스 바깥에서 정의된 것 포함)은 그 클래스가 접근할 수 있는 모든 이름에 접근할 수 있어요. 멤버 함수 안의 지역 클래스도 그 멤버 함수가 접근할 수 있는 모든 이름에 접근할 수 있죠.
class 키워드로 정의한 클래스는 멤버와 기반 클래스가 기본적으로 private 접근이에요. struct 키워드로 정의한 클래스는 기본적으로 public 접근이죠. 공용체(union)는 멤버가 기본적으로 public 접근이에요.
protected·private 멤버에 추가 함수나 클래스의 접근을 허용하려면 friend 선언을 쓰면 돼요.
접근성은 이름의 출처와 무관하게 모든 이름에 적용돼요. 그래서 typedef나 using 선언으로 도입된 이름(상속 생성자 제외)은 그 이름이 가리키는 대상이 아니라 그 이름 자체가 검사돼요.
class A : X
{
class B {}; // B는 A 안에서 private
public:
typedef B BB; // BB는 public
};
void f()
{
A::B y; // error: A::B는 private
A::BB x; // OK: A::BB는 public
}
멤버 접근은 가시성(visibility)에는 영향을 주지 않아요. private이거나 private 상속된 이름도 여전히 보이고, 오버로드 결정(overload resolution)에 고려되며, 접근 불가능한 기반 클래스로의 암시적 변환도 여전히 고려돼요. 멤버 접근 검사는 어떤 언어 구성이 해석된 뒤의 마지막 단계에서 이뤄져요. 이 규칙의 의도는 '어떤 private을 public으로 바꿔도 프로그램의 동작이 절대 바뀌지 않는다'는 거예요.
기본 함수 인자나 기본 템플릿 매개변수에 쓰인 이름의 접근 검사는 사용 지점이 아니라 선언 지점에서 수행돼요.
가상 함수 이름의 접근 규칙은 호출 지점에서, 멤버 함수를 호출할 객체를 나타내는 표현식의 타입을 기준으로 검사돼요. 최종 오버라이더의 접근은 무시돼요.
struct B
{
virtual int f(); // B에서 f는 public
};
class D : public B
{
private:
int f(); // D에서 f는 private
};
void f()
{
D d;
B& b = d;
b.f(); // OK: B::f는 public. 비록 private이어도 D::f가 호출돼요
d.f(); // error: D::f는 private
}
비한정 이름 찾기(unqualified name lookup)에서는 private으로 보이는 이름도, 한정 이름 찾기(qualified name lookup)로는 접근할 수 있을 수 있어요.
class A {};
class B : private A {};
class C : public B
{
A* p; // error: 비한정 이름 찾기는 A를 B의 private 기반으로 찾음
::A* q; // OK: 한정 이름 찾기는 네임스페이스 수준 선언을 찾음
};
상속 그래프에서 여러 경로로 접근 가능한 이름은, 그 경로들 중 접근이 가장 넓은 경로의 접근을 가져요.
class W
{
public:
void f();
};
class A : private virtual W {};
class B : public virtual W {};
class C : public A, public B
{
void f()
{
W::f(); // OK: W는 B를 통해 C에 접근 가능
}
};
클래스 안에 접근 지정자는 몇 개든 어떤 순서로든 나타날 수 있어요.
같은 클래스 안에서 멤버를 다시 선언할 때는 같은 멤버 접근으로 다시 선언해야 해요.
struct S
{
class A; // S::A는 public
private:
class A {}; // error: 접근을 바꿀 수 없음
};
public 멤버 접근
public 멤버는 클래스의 public 인터페이스 일부를 이뤄요(나머지 public 인터페이스는 ADL로 찾는 비멤버 함수들이에요).
클래스의 public 멤버는 어디에서나 접근할 수 있어요.
class S
{
public:
// n, E, A, B, C, U, f는 public 멤버
int n;
enum E {A, B, C};
struct U {};
static void f() {}
};
int main()
{
S::f(); // S::f는 main에서 접근 가능
S s;
s.n = S::B; // S::n과 S::B는 main에서 접근 가능
S::U x; // S::U는 main에서 접근 가능
}
protected 멤버 접근
protected 멤버는 클래스가 파생 클래스에게 제공하는 인터페이스를 이뤄요(클래스의 public 인터페이스와는 구별돼요).
클래스의 protected 멤버는 다음 경우에만 접근할 수 있어요.
- 그 클래스의 멤버와 friend
- 그 클래스의 어떤 파생 클래스의 멤버와 friend — 단, protected 멤버를 접근하는 객체의 타입이 그 파생 클래스이거나 그 파생 클래스의 파생 클래스일 때만:
struct Base
{
protected:
int i;
private:
void g(Base& b, struct Derived& d);
};
struct Derived : Base
{
friend void h(Base& b, Derived& d);
void f(Base& b, Derived& d) // 파생 클래스의 멤버 함수
{
++d.i; // OK: d의 타입은 Derived
++i; // OK: 암시적 '*this'의 타입은 Derived
// ++b.i; // error: Base를 통해 protected 멤버에 접근 불가
// (그렇게 하면 가상의 Derived2 같은 다른
// 파생 클래스의 기반 구현까지 바꿀 수 있게 됨)
}
};
void Base::g(Base& b, Derived& d) // Base의 멤버 함수
{
++i; // OK
++b.i; // OK
++d.i; // OK
}
void h(Base& b, Derived& d) // Derived의 friend
{
++d.i; // OK: Derived의 friend는 Derived 객체를 통해 protected 멤버 접근 가능
// ++b.i; // error: Derived의 friend는 Base의 friend가 아님
}
void x(Base& b, Derived& d) // 비멤버·비friend
{
// ++b.i; // error: 비멤버에서는 접근 불가
// ++d.i; // error: 비멤버에서는 접근 불가
}
protected 멤버에 대한 포인터를 만들 때는 선언에 파생 클래스를 사용해야 해요.
struct Base
{
protected:
int i;
};
struct Derived : Base
{
void f()
{
// int Base::* ptr = &Base::i; // error: Derived를 써서 이름을 붙여야 함
int Base::* ptr = &Derived::i; // OK
}
};
private 멤버 접근
private 멤버는 클래스의 구현을 이루고, 클래스의 다른 멤버들을 위한 private 인터페이스가 되기도 해요.
클래스의 private 멤버는 그 클래스의 멤버와 friend에게만 접근할 수 있어요. 멤버가 같은 인스턴스에 있든 다른 인스턴스에 있든 상관없이요.
class S
{
private:
int n; // S::n은 private
public:
S() : n(10) {} // this->n은 S::S에서 접근 가능
S(const S& other) : n(other.n) {} // other.n은 S::S에서 접근 가능
};
명시적 변환(C 스타일·함수 스타일)은 파생 lvalue에서 private 기반으로의 참조로, 또는 파생에 대한 포인터에서 private 기반에 대한 포인터로 캐스팅하는 것을 허용해요.
상속
public / protected / private 상속의 의미는 파생 클래스 문서에서 다뤄요.
키워드
public, protected, private
더 알아보기
- 파생 클래스와 상속의 종류별 의미를 파생 클래스 문서에서 확인할 수 있어요.
- 접근 지정자의 실제 쓰임은 캡슐화와 인터페이스 설계 예시에서 자주 등장해요.
- cppreference의 접근 지정자 원문에서 결함 보고(defect report) 기록을 더 볼 수 있어요.