본문 바로가기

컴퓨터공학/C, C++

C,C++> 캡슐화

프렌드

프렌드 함수

예외로 지정한 대상에 모든 멤버를 공개할 수 있는데 이를 프렌드 지정이라고 한다. 

프렌드는 전역 함수, 클래스, 멤버 함수 세 가지 수준에서 지정할 수 있다. 

 

프렌드 전역 함수는 전역 함수가 마치 클래스 소속 멤버 함수인 것처럼 이 클래스 모든 멤버를 자유롭게 액세스할 수 있다.

private, public 영역이든 어떤 멤버 변수든지 읽고 쓸 수 있고 함수를 호출할 수 있다.

 

전역 함수인 경우 클래스 선언문에서 전역 함수를 friend로 명시하면 된다. 

 

프랜드 클래스 

함수뿐만 아니라 클래스 통째로 프렌드로 지정할 수 있다. 

class Some {
	friend class Any;
    ...
};

이렇게 하면 Any의 모든 멤버 함수는 Some 클래스 모든 멤버를 마음대로 액세스할 수 있다. 

 

프렌드 멤버 함수

클래스 멤버 함수 수가 많아지면 모든 멤버함수들이 대상 클래스 멤버를 액세스할 수 있기 때문에 허용 범위가 넓어져서 위험해진다. 

class Some {
	...
    friend void Any::func(Some &S);
};

Any 클래스의 func 멤버 함수는 Some 클래스의 모든 멤버를 액세스할 수 있다. 

 

 

프렌드 특성

1. 프렌드 지정은 단방향이며 지정한 대상만 프렌드가 된다. 

2. 프렌드 지정은 전이되지 않으며 친구의 친구 관계는 인정하지 않는다. 

3. 한 번에 하나씩만 프렌드 지정을 할 수 있다.

4. 프렌드 관계는 상속되지 않는다. 

 

 

정적 멤버

this 의미

예를 들어서 Simple 클래스 객체 A와 B를 생성한다고 가정해보자. 

메모리를 보면 두 인스턴스 멤버 값은 각자 할당되고 함수는 두 객체가 공유한다. 

호출하는 곳에서는 A와 B로 구분된다. 그러나 함수 입장에서는 자신을 호출한 문장 앞에 붙어 있는 A. , B. 를 알 수 없다. 

하나의 함수를 두 객체가 공유하는데 함수 입장에서는 누가 불렀는지 두 객체를 어떻게 구분할 수 있을까? 

결국 멤버 함수가 호출한 객체를 구분하기 위해서 객체 정보가 함수 인수로 전달되어야 한다. 

C++ 컴파일러는 호출문 객체를 함수 인수로 몰래 전달한다. 

따라서 A.OutValue() 호출문은 컴파일러에 다음과 같이 해석한다. 

A.OutValue() -> OutValue(&A)

이 때 전달 받은 숨겨진 인수를 this라고 한다. 호출한 객체 번지를 가리키는 포인터 상수이다. 

일반적으로 Class형 멤버 함수들은 Class * const this를 받아서 객체 고유 멤버를 액세스할 수 있다.

OutValue함수는 다음과 같이 해석한다.

void OutValue(Simple * const this){
	printf("value=%d\n", this->value);
}

 

정적 멤버 변수

정적 멤버 변수는 클래스 바깥에 선언되어 있지만 클래스에 속하며 객체별로 할당되지 않고 모든 객체가 공유하는 멤버이다. 

전역 변수로 모두 공유하면 좋겠지만 문제가 있다.

1. 클래스 관련 중요 정보가 클래스 바깥에 선언돼 있으면 정보를 완전히 캡슐화하지 못했으니 이 클래스는 부품으로 부적절하다.

2. 전역변수가 있어야 동작할 수 있어서 재사용하려면 항상 전역변수와 함께 배포해야 한다. 

3. 전역변수는 은폐할 수 없어서 외부 누구나 건드릴 수 있다. 

캡슐화, 정보 은폐, 추상성 등 모든 OPP원칙에 어긋난다. 

 

 

따라서 클래스 멤버로 포함시키고 앞에 static 키워드를 붙인다. 

그리고 선언문은 메모리 할당을 하지 않기 때문에 별도로 외부에서 선언 및 초기화해야 한다. 

어떤 클래스 소속인지 ::연산자와 함께 소속을 밝혀야 한다. 

 

정리하면 클래스 내부 선언은 멤버 변수가 클래스 소속이며 정적 멤버 변수라는 걸 밝히고,

외부 정의는 정적 멤버를 생성하고 초기화한다는 뜻이다. 외부 정의에 의해 메모리가 할당되며 초기값을 줄 수 있기 때문이다.

 

관습에 따라 클래스를 헤더 파일에 선언하고 멤버 함수를 구현 파일에 작성할 때 정적 멤버 외부 정의는 통상 클래스 구현 파일에 작성한다. 

이렇게 선언하면 지정한 초기값으로 한 번만 초기화된다.

다시 한 번 말하지만 정적 멤버 변수를 소유하는 주체는 객체가 아니라 클래스이다. 

 

정적 멤버 함수 

정적 멤버 함수도 static이 붙는다.

클래스 선언부에 인라인 형식으로 작성하거나 외부에 따로 정의할 수 있다.

외부 작성 시 static 키워드는 생략한다. 

정적 멤버 함수는 특정한 객체에 의해 호출되는 건 아니라서 숨겨진 인수 this가 전달되지 않는다.

클래스 작업을 하기 때문에 어떤 객체가 자신을 호출했는지 구분할 필요없다. 

따라서 호출 객체 정보도 필요없다. 

정적 멤버 함수는 정적 멤버한 액세스할 수 있고 일반 멤버는 참조할 수 없다. 

그 이유는 일반 멤버 앞에는 암식적으로 this->가 붙지만 정적 멤버 함수는 this를 전달받지 않기 때문이다.

 

상수 멤버

상수 멤버

상수는 대입을 받을 수 없기 때문에 반드시 생성자 초기화 리스트에서 초기화해야 한다. 

이 내용은 앞에서 이미 한 번 다뤘다. 

상수 멤버가 모든 객체에 항상 같은 값을 가진다면 객체를 생성할 때마다 매번 초기화할 필요 없이 정적 멤버로 선언 후 딱 한 번만 초기화할 수 있다. 

 

 

다음과 같이 상수 멤버를 정의한다. 

첫 번째 방법은 생성자 초기화 리스트에서 초기화한다. 

 

두 번째 방법은  클래스 외부에서 한 번 더 다시 정의해야 하며 초기값을 준다. 

일반 정적 멤버와 다르게 상수 멤버는 선언할 때 초기값을 반드시 지정해야 한다. 

정적이면서 상수라는 성질이 있어서 정의할 때 초기화하지 않으면 다시 초기화할 기회가 없다. 

 

세 번째 방법은 클래스 선언문내 멤버 선언문에 아예 초기식을 같이 줄 수 있다. 

정적 멤버는 객체 소속이 아니라 클래스 소속이라서 클래스 선언 시 초기화할 수 있다.

 

 

다른 방법으로 사용하는 상수가 정수 타입이면 상수 멤버 대신 열거 멤버를 사용할 수 있다. 

열거형 정의 문법에 따라서 열거 멤버 다음  초기값을 줄 수 있기 때문에 이걸 이용할 수 있다. 

열거 멤버는 컴파일러가 컴파일 중에만 사용하며 실제 메모리를 차지하지 않기 때문에 선언문 내에서 값을 정의할 수 있다. 

또는 매크로 상수 정의문을 두는 것도 가능하다. 

대신 프로젝트 전체 걸쳐 유일한 이름을 주어야 한다는 제약이 있다. 

 

 

상수 멤버 함수 

상수 멤버 함수는 멤버값을 변경할 수 없는 함수이다. 

멤버값을 단순히 읽기만 한다면 이 함수는 개체 상태를 바꾸지 않는다는 의미로 상수 멤버 함수로 지정하는 것이 좋다. 

클래스 선언문 함수 원형 뒤쪽에 const 키워드를 붙이면 상수 멤버 함수가 된다. 

앞 쪽에는 리턴값 타입을 지정하기 때문에 const를 함수 뒤에 붙이는 특이한 표기법이다.

 

상수로 선언된 객체는 상수 멤버 함수만 호출할 수 있다.

객체 값을 변경하는 함수는 상수 멤버 함수로 지정하지 말아야 한다. 

그러나 상수 멤버 함수라도 정적 멤버 변수 값은 변경할 수 있다. 왜냐하면 정적 멤버는 개체 소속이 아니고 객체 상태를 나타내지도 않기 때문이다. 

 

비상수 멤버 함수가 받는 객체 포인터 this는 Position * const형이며 this 자체는 상수이지만 this가 가리키는 대상은 상수가 아니다. 반면 상수 멤버 함수가 받는 객체 포인터 this는 const Position * const형이다. 

상수 멤버 함수 끝에 붙는 const는 이 함수로 전달되는 숨겨진 인수 this의 상수성을 지정한다. 

 

상수함수와 비상수 함수는 오버로딩할 수 있다. 

 

mutable

mutable로 지정된 멤버는 상수 함수나 상수 객체에서도 값을 변경할 수 있다. 

mutable을 빼면 삳수 함수에서는 멤버값을 변경할 수 없다는 에러가 된다. 

변경을 허용하는 지정인데 왜 필요할까? 

상수성을 주는 이유는 객체 상태가 우발적으로 변경되는 걸 금지하여 안정성을 높이려는 이유이다. 

근데 때로 객체 멤버이면서 객체 상태에 포함되지 않는 멤버가 존재한다. 

예를 들어 값 교환을 위한 임시 변수가 이에 해당한다. 

또는 루프 제어 변수도 객체 상태라고 볼 수 없고 디버깅을 위해 임시 추가된 멤버도 mutable이어야 한다. 

예를 들어 객체 상태를 출력하기 위해 문자열 버퍼를 멤버로 잠시 선언했으면 이 버퍼는 객체 주요 멤버 변수에 포함되지 않는다. 

 

const 키워드는 컴파일러가 컴파일할 때만 참조하므로 결과 프로그램 크기나 성능에 아무런 영향을 주지 않는다. 

컴파일러에게 상세한 정보를 제공하면 실행 중 발생할 수 있는 에러를 컴파일할 때 미리 알 수 있다. 

 


프로그래밍을 하기 위해서 많은 자료가 필요하다. 개발자가 모든 걸 외울 수는 없어서 대충 개념만 파악하고 필요할 때 참고 자료를 보면서 개발을 진행한다. 특히 웹 개발은 이런 현상이 심하다. 비슷한 코드가 반복되고 사용하는 기법이 뻔해서 처음부터 만드는 사람은 거의 없고 그럴 필요 없다. 이런 상황에서 개발자는 개발을 하는 건지 코드 끼워 맞추기를 하는 건지 정체성 혼란이 오고 현타가 온다.  

하지만 베껴 쓰는 것도 알아야 제대로 베끼지 아무나 할 수 있는 건 아니다. 적재적소 맞는 소스를 찾아서 끼워 넣는 것도 기술이다. 현대 개발자는 모든 걸 아는 것보다 어떤 코드 조각이 어디에 있는 지 잘 파악하는 능력이 아주 중요하다. 

베껴 쓰는 코드라도 의미와 구조를 잘 파악해야 하며 필요할 경우 직접 작성할 수 있어야 한다. 그래야 입맛대로 뜯어 고칠 수 있는 응용력을 발휘할 수 있다. 이런 능력을 보유하기 위해서는 문법 전반을 이해해야 한다. 그래서 C++를 배우고 있는 것이다. 

그렇다면 좋은 정보는 어디에 있을까? 인터넷과 책은 너무 많아서 정보 가치가 떨어지는 경향이 있다. 그래서 가장 좋은 정보는 직접 사용한 적이 있는 코드이다. 그 중 가장 훌륭한 소스는 이미 프로젝트에 사용해 본 코드이다. 

개발자가 보유한 프로젝트는 재산이며 경험의 보고이다. 따라서 가급적 많고 다양한 예제를 만들어 보고 재사용 가능한 소스를 확보하는 것이 좋다. 소스를 그냥 모아 놓기만 하는 게 아니라 재사용하기 쉽게 관리해야 한다. 어떤 프로젝트에 갖다 붙여도 잘 작동할 수 있는 일반성과 어떤 상황에서도 견고한 에러 처리 능력, 설명이 없어도 사용 방법을 알 수 있는 직관적인 구조를 유지해야 한다. 

 

'컴퓨터공학 > C, C++' 카테고리의 다른 글

C,C++> 상속  (0) 2022.08.19
C,C++> 연산자 오버로딩  (0) 2022.08.18
C,C++> 생성자  (0) 2022.08.08
C,C++> 클래스  (0) 2022.08.04
C,C++> 기타 내용  (0) 2022.08.03