본문 바로가기
Study/c,c++

c++에서 class의 private 멤버를 일반적이지 않은 방법으로 읽기

by 개발새-발 2021. 4. 25.
반응형

 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*) &target;
            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*) &target;
            
            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

댓글