c++의 private 접근 지정자는 "동일한 class가 아니라서 접근하실 수 없습니다."라 말한다. "아니 내가 좀 보겠다는데 거 한 번만 봅시다~" 라 말해도 "친구(friend)가 아니라면 만날 수 없습니다"는 말만 돌아올 뿐이다. 하지만 난 같은 class도 아니고 friend도 아니다. 그렇다고 기존에 있던 class의 코드를 수정하고 싶지는 않다. 그렇다면 private 멤버를 읽을 방법이 없는가? 그렇지는 않다. 우리는 몇 가지 편법을 사용해서 private 멤버를 읽고 임의로 수정할 수 있다. 이 글에선 friend를 이용한 일반적인 방법을 한번 본 후 수많은 꼼수들 중 두 가지 방법을 소개할 것이다.
Friend를 사용한 일반적인 방법
만약 내가 이미 접근하려는 class의 friend였다면 private면 멤버에 접근할 수 있었는지 보자. Target의 private 멤버 a, b를 Observer class에서 읽고 수정해 보도록 하겠다.
#include <iostream>
class Target{
private:
int a;
float b;
public:
Target(int a,int b):a(a),b(b) {};
void introduce(){
std::cout<<"From Target.introduce(), a,b : "<<a<<" "<<b<<std::endl;
}
friend class Observer;
};
class Observer{
public:
void see(Target& target){
std::cout<<"From Observer.see(), a,b in target are : "<<target.a<<" "<<target.b<<std::endl;
target.a = -999;
target.b = -99.99;
std::cout<<"But they changed. now, a,b are :"<<target.a<<" "<<target.b<<std::endl;
}
};
int main(void){
Target t(10,20);
Observer o;
t.introduce();
o.see(t);
t.introduce();
}
위 코드에서 Observer가 Target의 private 멤버를 읽을 수 있게 하는 중요한 문장은 friend class Observer; 이다. Observer 클래스를 friend로 하겠다는 의미이다. 클래스가 아닌 함수를 friend로 할 수도 있다. 만약 위 Target에 void func(Target& t);를 friend로 하고자 한다면 friend void func(Target& t); 를 추가해주면 된다. 위처럼 코드를 작성해주면 아래처럼 출력이 된다.
From Target.introduce(), a,b : 10 20
From Observer.see(), a,b in target are : 10 20
But they changed. now, a,b are :-999 -99.99
From Target.introduce(), a,b : -999 -99.99
Target의 private 멤버였던 a, b의 값이 Observer에 의해 변하였다. 이처럼 Observer가 Target의 friend일 때 Target의 private멤버에 간섭할 수 있는 것을 보았다.
이제 friend가 아닐 때의 방법을 보자.
1. 메모리에 직접 접근하는 방법
class가 메모리에 담기는 형태를 안다면, private 멤버의 메모리에 직접 접근하여 조작을 가할 수 있다. Target은 다른 class로부터 상속받지 않으며 virtual함수가 존재하지 않는 class이다. pragma pack과 같이 메모리 정렬 크기를 조절하는 구문도 없다. 그렇다면 &target에서부터 a, b가 각각 4바이트씩 연달아 있을 것이다. 한번 시도해보자.
#include <iostream>
class Target{
private:
int a;
float b;
public:
Target(int a,int b):a(a),b(b) {};
void introduce(){
std::cout<<"From Target.introduce(), a,b : "<<a<<" "<<b<<std::endl;
}
};
class Observer{
public:
void see(Target& target){
int* val_a = (int*) ⌖
float* val_b = (float*) &target+1;
std::cout<<"From Observer.see(), a,b in target are : "<<*val_a<<" "<<*val_b<<std::endl;
*val_a = -999;
*val_b = -99.99;
std::cout<<"But they changed. now, a,b are :"<<*val_a<<" "<<*val_b<<std::endl;
}
};
int main(void){
Target t(10,20);
Observer o;
t.introduce();
o.see(t);
t.introduce();
}
From Target.introduce(), a,b : 10 20
From Observer.see(), a,b in target are : 10 20
But they changed. now, a,b are :-999 -99.99
From Target.introduce(), a,b : -999 -99.99
friend를 사용했을 때와 동일한 출력을 받았다. private 멤버의 위치를 계산하여 직접 접근함으로 private멤버를 friend로 등록되지 않은 Observer에서 바꾸어 주었다. 메모리에 직접 접근하는 방법이 통한다는 것을 볼 수 있었다. 같은 원리로, 어떤 class에서 private멤버들이 이 class 에선 private가 아니라는 점만 빼면 같은 클래스를 만들어 접근할 수도 있다.
#include <iostream>
class Target{
private:
int a;
float b;
public:
Target(int a,int b):a(a),b(b) {};
virtual void virtualfunc(){};
void introduce(){
std::cout<<"From Target.introduce(), a,b : "<<a<<" "<<b<<std::endl;
}
};
class Foo{
public:
int a;
float b;
virtual void virtualfoofunc();
};
class Observer{
public:
void see(Target& target){
Foo* f = (Foo*) ⌖
std::cout<<"From Observer.see(), a,b in target are : "<<f->a<<" "<<f->b<<std::endl;
f->a = -999;
f->b = -99.99;
std::cout<<"But they changed. now, a,b are :"<<f->a<<" "<<f->b<<std::endl;
}
};
int main(void){
Target t(10,20);
Observer o;
t.introduce();
o.see(t);
t.introduce();
}
From Target.introduce(), a,b : 10 20
From Observer.see(), a,b in target are : 10 20
But they changed. now, a,b are :-999 -99.99
From Target.introduce(), a,b : -999 -99.99
Target에선 a,b가 private였지만 Foo에선 public이다. Target포인터를 Foo포인터로 강제로 바꾸어준 뒤 접근한다. 어느 멤버가 메모리상에 어느 위치에 있는지 고려하는 수고가 줄었다. Target에 이전 코드에서 없던 virtual 함수가 생겼다. 만약 이전 코드에서 이 Target에 접근하였다면 원하는 값이 마음대로 변하지 않았을 것이다. vtable로 인해 멤버의 위치가 virtual 함수가 있을 때와 차이가 있기 때문이다. 이 코드에서는 foo기준에서 접근하는 a, b의 위치와 target기준에서 접근하는 a, b의 위치가 동일하였기에 가능하였다.
상속 여부와 virtual 함수 포함 여부 등에 따라서 class 멤버의 메모리상 위치는 달라질 수 있음으로 주의하여야 한다. 만약 첫 코드처럼 '&target이 가리키는 위치에 첫 멤버가 있겠거니~' 해서 접근 후 수정하였는데, 그 위치에 원하는 멤버가 아닌 다른 엉뚱한 멤버나, vtable가 있다면 엉뚱한 값을 수정하는 것이 된다. 그래서 원하는 클래스가 메모리상에 저장되는 구조를 정확하게 파악한 후 접근하여야 한다.
2. Template specializaton (템플릿 특수화)를 사용하는 방법
접근하고자 하는 class에 template function이 있다면 Template specializaton를 통하여 private 멤버를 수정할 수 있다.
#include <iostream>
class Target{
private:
int a;
float b;
public:
Target(int a,int b):a(a),b(b) {}
void introduce(){
std::cout<<"From Target.introduce(), a,b : "<<a<<" "<<b<<std::endl;
}
template<typename T>
void sayHello(T const& p){
std::cout<<"hello "<<&p<<std::endl;
}
};
class Observer{};
template<>
void Target::sayHello<Observer>(Observer const& p){
std::cout<<"sayHello called with Observer"<<std::endl;
a = -999;
b = -99.99;
}
int main(void){
Target t(10,20);
Observer o;
t.introduce();
t.sayHello(100);
t.sayHello(o);
t.introduce();
}
From Target.introduce(), a,b : 10 20
hello 0x7ffd5ebf4c1c
sayHello called with Observer
From Target.introduce(), a,b : -999 -99.99
target의 a,b가 sayHello를 통해 수정되었다. 사실 이상한 방법은 아니다. sayHello는 Target의 function이고, Target의 function이 Target의 private멤버에 접근할 수 있다는 것은 이상한 것이 아니다. 우리는 아무것도 하지 않는 class인 Observer를 만들어 이 클래스에 대해 sayHello를 따로 지정해 주었다. 이 방법으로 sayHello는 우리가 방금 만든 Observer를 제외한 모든 타입에 대해 본래 Target의 개발자가 의도한 대로 동작한다. 그러나 Observer에게만 특별히 작성된 sayHello를 통해 이제 우리는 target에 '어느 짓' 이든 할 수 있다.
만약 어떤 클래스에 add라는 템플릿 function이 있다고 하자. add는 a, b를 받아 a + b를 반환한다. 그런데 add에 char*의 문자열 두 개를 받아 이어 붙인 char*를 반환하고 싶다. char*엔 +가 우리가 원하는 방식으로 정의되어있지 않기 때문에 a + b를 반환하는 add를 곧바로 쓸 수 없다. 이때 template specialization를 통해 char*에 맞게 add를 다시 써 줄 수 있다. 이때는 template specialization를 제대로 사용했다고 할 수 있다. 그러나 위에서 sayHello를 이상하게 사용한 것처럼 add함수의 목적에 맞지 않게 template specialization를 사용한다면 이는 경우에 따라서 프로그램이 의도하지 않은 방향으로 작동할 수 있을 것이다.
이 글은 학교 커뮤니티에 "이미 완성된 class A의 private 멤버를 friend를 사용하지 않고 외부에서 접근할 수 있는 방법이 있나요?"에 대해 두 개의 편법을 생각해본 글이다. 만약 과제를 하거나 프로젝트를 하는데 이 꼼수를 써야 할 것 같은 느낌이 들면 차라리 코드를 엎어버리는 게 좋은 선택지라고 생각한다.
'Study > c,c++' 카테고리의 다른 글
비트마스크 (0) | 2021.05.08 |
---|---|
비트 연산 기본 (0) | 2021.05.05 |
C/C++서 양수 정수 나눗셈 결과의 올림 구하기 (0) | 2021.03.01 |
Scanf의 형식지정자에 대하여 (0) | 2021.02.26 |
댓글