this_call


1. this

객체가 여러번 인스턴스화 한다고 해서 멤버 함수가 생성되는게 아니라 인자가 추가 되는 개념으로 바뀐다.

1. 멤버 함수의 호출 원리

객체가 함수의 1번째 인자(this)로 추가된다. - this call
정확히는 ecx 레지스터로 전달

2. static 멤버 함수는 this가 추가되지 않는다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>

using namespace std;

class Point {
int x, y;
public:
void set(int a, int b) {
//컴파일을 하면서 바뀐다 void set(Point* const this, int a, int b)
x = a; // this->x = a;
y = b; // this->y = b;
}

static void foo(int a) {
// 컴파일을 해도 static function은 바뀌지 않는다.
// void foo( int a)
x = a;
/*
* 컴파일을 하면서 this->x = a로 변한다.
* 그러나 static function은 this가 없다.
* 그래서 static function에서는 멤버 변수 접근이 안된다.
*/
}
};

int main( ) {
Point::foo(10); // static 멤버함수는 객체없이 호출 가능
/*
* push 10
* 보낼 객체가 없다.
* call Pint::foo
*/

Point p1, p2;
p1.set(10, 20); // 이 순간의 원리를 생각해 봅시다.
/* set(&p1, 10, 20)으로 변경됩니다.
* push 20
* push 10 진짜 인자는 스택으로
* mov ecx, &p1 객체 주소는 레지스터에 호출
* call Point::set 그리고 함수 호출!!
*/
}

2. 함수포인터

멤버 함수의 포인터를 만드는 법

1. 일반 함수 포인터에 멤버 함수의 주소를 담을 수 없다.

this때문에..!!

2. 일반 함수 포인터에 static 멤버 함수의 주소를 담을 수 있다.

3. 멤버 함수 포인터를 만들고 사용하는 방법

1
2
3
void(Dialog::*f3)() = &Dialog::Close;
Dialog dlg;
(dlg.*f3)();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>

using namespace std;

class Dialog {
public:
void Close() {
cout << "Dialog Close" << endl;
}
};

void foo() {
cout << "foo" << endl;
}

int main() {
void(*f1)() = &foo;//OK

//void(* f2) () = &Dialog::Close;// 될까요? 잘 생각해 보세요
// error

//멤버 함수 포인터를 만드는 법
void(Dialog::*f3)() = &Dialog::Close; //OK...외우자

// f3(); //될까요??
// error

Dialog dlg;
// dlg.f3(); // ok 결국 dlg.Close() 즉 f3(&dlg)
// 그런데. 이경우 컴파일러는 f3이라는 멤버를 찾게된다. 그래서 error

(dlg.*f3)(); // f3는 함수포인터이므로 *f3하면 함수가 된다.
// .* 연산자 우선순위를 호출함수()보다 높여야 한다.

Dialog *pDlg = &dlg;
//pDlg와 f3를 사용해서 Close를 호출해 보세요.
((*pDlg).*f3)();
(pDlg->*f3)();

return 0;
}

3. this 관리의 어려움

일반적은 쓰레드 관리를 통해서 this 관리의 어려움을 본다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//VC로만 가능
#include <iostream>
#include <windows.h>
#include <conio.h>

using namespace std;

DWORD __stdcall foo(void* p) {
cout << "foo" << endl;
return 0;
}

int main ()
{
CreateThread(0, 0, foo, "A", 0, 0); //쓰레드 생성
_getch();
return 0;
}

4. this 관리의 어려움

일반적은 쓰레드 관리를 통해서 this 관리의 어려움을 본다

핵심 1. C의 callback 함수는 객체 지향으로 디자인 될때 static 멤버함수가 되어야 한다.

핵심 2. static 멤버에는 this가 없으므로 가상함수나 멤버 data에 접근할 수 없다. 다양한 기법으로 this를 사용할 수 있게 하는것이 편리하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//VC로만 가능
#include <iostream>
#include <windows.h>
#include <conio.h>

using namespace std;

// C의 스레드 개념을 c++로 캡슐화 해 봅시다.
// 아래 클래스를 라이브러리 내부 클래스라고 생각합니다.
class Thread {
public:
void Create() {
CreateThread(0, 0, _threadMain, this, 0, 0); // !!
}

static DWORD __stdcall _threadMain(void *p) {
// 다시 가상함수 호출
threadMain();
/* 컴파일을 통해서 바뀌게 된다. this->threadMain();
* 즉 threadMain(this)가 되어야 한다.
* 그러나 threadMain()으로는 사용할수가 없게 된다.
* 방법은 p가 this이므로 캐스팅해서 사용합니다
*/

Thread* self = static_cast<Thread*>(p);
self->threadMain(); // 결국 threadMain(self)
return 0;
}

virtual void threadMain() {
// 컴파일을 통해서 바뀐다. void threadMain(Thread* this)
}
};

// 아래 클래스가 라이브러리 사용자 클래스 입니다.
class MyHtread : public Thread {
public :
virtual void threadMain() {
cout << "MyThread" << endl;
}
};

int main() {
MyHtread t;
t.Create();
return 0;
}

5. NULL 객체 호출 문제

NULL 객체에 대해서 handling을 해준다.
NULL 객체에서 호출하는 문제는 this를 생각하면 쉽게 풀어 나갈 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>

using namespace std;

class Test {
int data;
public:
void f1() {
cout << "f1" << endl;
}

int f2() {
cout << "f2" << endl;
return 0;
}

int f3() {
cout << "f3" << endl;
return data; // this->data
}

// 아래 코드는 왜 만들었을까요 ?
// NULL 객체에 대해서 함수를 호출해도 죽지 않게 하기 위해
int call_f3() { return this ? f3() : 0; }

virtual void f4() { };
};

int main() {
Test *p = 0;// 메모리 할당에 실패해서 0이 나왔다고 가정합니다.

p->f1(); // 어떻게 될까요 ?? 실행하지 말고 생각해 보세요.
//f1(p), f1(0)
p->f2(); // OK

p->f3(); // error : this->data 이기 때문에

p->call_f3();

p->f4(); // 될까요? 0 번지에 가상함수 테이블이 있다고 생각하게 된다.!! run-time error
}

6. 상속과 포인터

상속 순서에 따라서 포인터 주소를 배정 받는다.
다운케스팅을 하면 자동으로 그 주소를 찾아 간다.
부모의 주소를 100번지라고 가정을 하면 상속 받은 순서에 따라서 주소를 배정을 받게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>

using namespace std;

class X {
public:
int x;
};

class Y {
public:
int y;
};

class C : public X, public Y {
public:
int c;
};

int main() {
C c;

cout << &c << endl; //100번지 라고 할 때

X *pX = &c;
Y *pY = &c;

cout << pX << endl; // ? 100
cout << pY << endl; // ? 104

return 0;
}

6. 상속과 포인터

우리가 예상하는 것보다 컴파일러는 주소 값을 잘 찾아서 간다. 컴파일러가 어떻게 이 과정을 찾는지는 모른다.

모든 함수 포인터는 4바이트라고 생각했지만 일반적은 함수 포인터만 4바이트
다중 상속을 하는 경우 포인터의 주소는 8바이트 인다. { 함수주소, this offset }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

using namespace std;

class X {
public:
int x;
void fx() { cout << this << endl; }
};

class Y {
public:
int y;
void fy() { cout << this << endl; }
};

class C : public X, public Y {
public:
int c;
};

int main() {
C c;

cout << &c << endl; // 100번지 라고 할 때

c.fx(); // 100
c.fy(); // 104

void (C::*f)();

//f = &C::fx; // { fx 주소, 0 }
//(c.*f)();// 결과 ? 100 나와야 한다.
//f(&c)

f = &C::fy; // { fy 주소, sizeof(X) 즉, 4 }
(c.*f)(); //f(&c)
// f.함수주소(&c + f.this_offset)

cout << sizeof(f) << endl;

return 0;
}

7. 함수포인터와 가상함수

가상함수의 경우는 가상함수 테이블의 인덱스 번호가 넘어온다.
즉, 가상함수 table의 인덱스, 가상함수의 순서가 나오게 된다.
g++ : 0, 1, 2, 3 등의 숫자가 나오게 됩니다.
vc++ : 주소 비슷하게 나오는데… 그 주소를 따라가면 index가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

using namespace std;

class Base {
public:
virtual void goo() { cout << "Base foo" << endl; }
virtual void foo() = 0;// { cout << "Base foo" << endl; }
};

class Dervied : public Base {
public:
virtual void foo() { cout << "Derived foo" << endl; }
};

int main() {

void (Base::*f)() = &Base::foo;// 잘 생각해 보자.

//cout << f << endl;
printf("%d\n", f);//foo -> 9
//goo -> 1

Base *p = new Dervied;

(p->*f)(); // 오버라이딩의 개념에 맞게 Dervied 클래스의 값을 찾아간다.
}

8. Handler

하나의 클래스를 이용해서 각각의 인스턴스가 다른 역활을 하기 위해서는 Handler를 이용해서 처리한다.
java에서 리스너와 비슷한 처리 방법이다.
c/c++에서는 모든 함수의 주소를 담을 수 있는 도구가 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>

using namespace std;

template<class T>
class Button {
void(*handler)();

public:
void setHandler(void(*f)()) {
handler = f;
}

void click() {
// 버튼이 눌렸다는 사실을 외부에 전달합니다.
// 흔히 "객체가 외부에 이벤트를 발생한다."라고 표현!
handler();
}
};

void Btn1Handler() { cout << "버튼 1 클릭" << endl; }

int main() {
Button b1;
b1.setHandler(&Btn1Handler);
b1.click(); // 사용자가 버튼을 클릭하면 이함수가 호출된다고 가정합니다.

return 0;
}

다음과 같이 처리 하면 모든 클래스에 대해서 처리를 할 수 가 없다.
이럴 때 사용하는 것이 template 이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Dialog {
public:
void Close() {
cout << "Dialog Close" << endl;
}
};

template<typename T>
class Button {
//void(*handler)();
void(T::*handler)();

//멤버 함수를 담기 위해서 만든다.
T *member;
public:
void setHandler(void(T::*f)()) {
handler = f;
}

void click() {
// 버튼이 눌렸다는 사실을 외부에 전달합니다.
// 흔히 "객체가 외부에 이벤트를 발생한다."라고 표현!
(member->*handler)();
}
};

int main() {
Button<Dialog> b1;
b1.setHandler(&Dialog::Close);
b1.click(); // 사용자가 버튼을 클릭하면 이함수가 호출된다고 가정합니다.
}

다음과 같이 코드를 수정하면 모든 클래스에 대해서 처리를 할 수가 있다.

9. function<>

모든 함수의 주소를 담을 수 있는 도구
c,c++ : 문법적으로는 없다.
c# : delegate 라는 문법
objective-c : Selector 라는 문법

c++11 : function<> 모든 함수의 주소를 담을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <functional>

using namespace std;

void foo() { cout << "foo" << endl; }
void goo(int a) { cout << "goo : " << a << endl; }
void hoo(int a, int b) { cout << "hoo : " << a << ", " << b << endl; }

class Dialog {
public:
void Close() {
cout << "Dialog Close" << endl;
}
};

int main() {
function<void()> f = &foo;
//function<리턴값(파라메터)>
f(); // ok..foo() 호출

f = bind(&goo, 5);// 인자를 고정
f();//goo(5)

f = bind(&hoo, 1, 2);
f();//hoo(1,2)

Dialog dlg;
f = bind(&Dialog::Close, dlg);// 객체를 고정
f();//dlg.Close()

return 0;
}

10. Bind

bind에 사용법을 더 자세하게 배워보자

namespace std::placeholders; ``` _1, _2, .... 이용해서 입력하는 인자의 위치를 정해 준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

```c++
#include <iostream>
#include <functional>

using namespace std;
using namespace std::placeholders;

void foo() { cout << "foo" << endl; }

void goo(int a) { cout << "goo : " << a << endl; }

void hoo(int a, int b) { cout << "hoo : " << a << ", " << b << endl; }

class Dialog {
public:
void Close() {
cout << "Dialog Close" << endl;
}
};

void koo(int a, int b, int c, int d) {
printf("%d %d %d %d\n", a, b, c, d);
}

int main() {
function<void(int)> f = &goo;
f(5); //goo(5)

f = bind(&hoo, _1, 3);
f(5); //hoo(5, 3)

function<void(int, int)> f2;
f2 = bind(&koo, _2, 2, 9, _1);
f2(6, 3); // 3, 2, 9, 6

return 0;
}