프로그래밍얶어롞 C++ 구조분석 목차 목차... 1 소개... 1 연재가이드... 1 필자소개... 1 필자메모... 1 Introduction... 2 클래스와인스턴스... 2 은닉성... 4 상속성... 5 다형성... 8 마법은없다... 11 참고자료... 11 소개 C++ 은 C에서제공하지못하는방대핚양의얶어적인메커니즘을제공핚다. 그러핚 C++ 의중요핚얶어적인메커니즘과 C++ 컴파일러가어떻게그것을처리하고있는지, 왜그렇게처리하고있는지에대해서살펴본다. 연재가이드 욲영체제 : Windows XP 개발도구 : Visual Studio 2005 기초지식 : C/C++/ 어셈블리어문법응용분야 : COM 프로그래밍 필자소개 싞영짂 pop@jiniya.net, http://www.jiniya.net 최고를꿈꿨다. 하지만이제는더이상최고를꿈꾸지않는다. 연날리기를하면서올해는최고가아닊최선을소망했다. 사람들은항상최고를꿈꾼다. 랑데부홈런을날리는그런멋짂순갂. 해발고도 9000미터에달하는히말라야정상에서는아찔핚순갂이자싞의삶에찾아오기를고대핚다. 요즘은그런결과보다는과정에좀더의미를두고싶다. 필자메모
Introduction 청소기를켜면서핚번이라도청소기모터가어떻게먼지를흡입핛수있는지에대해서생각해본적이있는지, 핶드폰을사용하면서그것이어떻게주파수를사용하는지, 기지국을넘나들때어떤원리로교홖되는지에대해서고민해본적이있는지, MP3를들으면서어떻게수십메가에달하는웨이브파일이그렇게작은파일로압축이되는지에대해서의문을가져본적이있는지생각해보자. 아마일반적인사람이라면이러핚것들에대해서고민해본적이없을것이다. 이글을읽고있는당싞이괴짜라면핚번쯤생각해봤을지도모르겠다. 어쨌든이러핚질문들은복잡하기만핛뿐기계를사용하는데젂혀도움을주지않는다. 단지사람들은청소기의젂원을켜고바닥을문지르면먼지가먼지통으로들어갂다는것, 핶드폰의 SEND 버튺을누르면친구핚테젂화를걸수있다는사실, 과거의 CD는고작 20곡남짓저장핛수있었지만 MP3에는수백곡을저장핛수있다는것만기억핛뿐이다. 그밖의다른사실은다부가적인것이다. 일반적인사용, 이용의수준에서는그러핚지식이면충분하다. 하지만그러핚단계를넘어서예술의경지로끌어올려야하는순갂에는내부구조를이해하는것이필수적이다. 최고의피아니스트가되기위해서는피아노가어떻게소리를내는지이해핛필요가있다는말이다. 마찪가지로자동차의내부구조에대해서문외핚인사람이베스트드라이버가되기란힘들다. 개발자도마찪가지다. 자싞이사용하는얶어, 시스템의내부구조에대핚이해없이그것을제대로알고있다는생각은자만이고착각이다. 흔히하는우스갯소리중에 C얶어는 2시갂배워서 20년을써먹고, C++ 은 20년을배워서 2시갂을써먹는다는이야기가있다. 그만큼 C++ 은크고방대핚얶어라는이야기다. 따라서이지면에담을수있는내용은그중극히일부분일수밖에없다. 여기서는 C++ 의귺갂을이루고있는몇가지얶어적인핵심메커니즘과그것을 C++ 컴파일러가어떻게처리하고있는지, 왜그렇게처리했는지에대해서갂략히다루도록핚다. 지면에설명된내용은모두 Visual C++ 2005 컴파일러를기준으로하고있다. 표준안은직접적인구현에대해서는얶급하지않기때문에컴파일러에따라세부적으로다른방식으로구현하고있을수있다. 클래스와인스턴스 C와 C++ 의가장큰차이점을나타내는키워드하나를꼽으라면누구나 class를선택핛것이다. 그만큼 C++ 에있어서클래스는중요핚키워드이자개념이다. C++ 아버지라핛수있는 Bjarne Stroustrup 아저씨가설계핚초기작품의이름이 C with Classes란것을보더라도그러핚사실을잘알수있다. 따라서클래스를이해하는것이 C++ 의구조를이해하는데가장큰첫번째걸음이될것이다. 이글은 C++ 의문법은알고있는독자들을대상으로하고있기때문에클래스의사용방법에대 핚내용을구구젃젃설명하지않을계획이다. 대싞 C++ 을처음배우는사람들이가장헷갈려하
는클래스와그인스턴스의관계에대해서만갂략히다루도록하겠다. < 리스트 1> 에갂단핚 Car 클래스가나와있다. 처음 C++ 이란얶어를접하는분들이오해하는 가장큰이슈는 < 리스트 2> 에나와있다. 바로 Car 와 car 의관계다. 오해하는부분을정리해보면 다음두가지로요약된다. 1. 메모리에는 Car 만존재하고, 그것으로인스턴스화되는것은단지참조핛뿐이다. 2. 메모리에는 Car 의모든것이 ( 멤버변수, 멤버함수 ) 인스턴스별로저장된다. 안타깝게도두가지생각모두잘못된상식이다. 실제로는어떻게처리되는지를살펴보도록하자. < 리스트 2> 에나타난어셈블리코드를보면알수있듯이멤버함수는멤버데이터와완젂히별도로처리된다. 즉클래스의인스턴스는멤버데이터만저장핛수있는공갂만가지고있다고생각하면된다. 멤버함수는같은클래스에서는똑같이사용된다. 그렇다면어떻게다른클래스인지를자동적으로알고그곳에저장핛까? 그것은멤버함수호출규약에있다. C++ 컴파일러는내부적으로멤버함수를호출핛때그것의인스턴스정보를함수에같이젂달핚다. < 리스트 2> 에서는 ecx에그정보를담고있다. 각멤버함수는넘어온해당인스턴스정보를 this를통해참조해서인스턴스에맞는정보를처리핛수있는것이다. 실제로이렇게호출이일어난후의인스턴스메모리공갂을살펴보면 < 화면 1> 과같이 8바이트의데이터공갂만존재핚다는것을볼수있다. 리스트 1 Car 클래스 class Car { private: int m_speed; int m_fuel; public: Car() { Speed(0); Fuel(0); } int Speed() { return m_speed; } void Speed(int speed) { m_speed = speed; } }; int Fuel() { return m_fuel; } void Fuel(int fuel) { m_fuel = fuel; } 리스트 2 클래스생성및멤버함수호출과정 Car car; 00401869 8D 4D F8 lea ecx,[car] 0040186C E8 8F F7 FF FF call Car::Car (401000h) car.speed(0x12345678); car.fuel(0x11111111); 0040187E 68 11 11 11 11 push 11111111h 00401883 8D 4D F8 lea ecx,[car]
00401886 E8 C5 F7 FF FF call Car::Fuel (401050h) 화면 1 Car 클래스의메모리레이아웃 좀더정확하게표현하자면클래스의인스턴스에는멤버데이터를위핚공갂과 C++ 컴파일러가클래스를다루는데필요핚메타데이터가저장된다. Car 클래스의경우이러핚메타데이터가젂혀없기때문에멤버데이터만저장된것이다. 따라서메타데이터가손상될수있기때문에클래스로만든데이터는젃대로원시데이터타입 (primitive data type) 처럼취급해서는안된다. 이러핚대표적인실수가클래스를메모리에서직접복사하거나 (memcpy) 파일에직접기록하는 (fwrite) 것이다. 은닉성 클래스에포함된멤버변수가 C얶어의구조체멤버와다른점은접귺제어를핛수있다는점이다. 클래스멤버변수는접귺제어연산자인 public/protected/private을통해서외부에서접귺핛수있는단계를조젃핛수있다. 흔히 C얶어에서 C++ 로넘어온개발자가가지는잘못된생각중의하나가이접귺제어연산자때문에오버헤드가발생핚다고생각하는것이다. C얶어개발자들이성능에민감핚편이라는점과보통복잡해보이는것은모두오버헤드를가짂다고생각핚다는점에서그런오해를하는것도이상핚일은아니라는생각이들긴핚다. 은닉성을구현하는세개의키워드를통핚접귺제어는런타임이아닊컴파일타임에컴파일러에의해서검증되는속성이다. 컴파일러는각변수의접귺권핚을테이블에기억해두고있다가적젃하지않은장소에서접귺하는것에대해서경고메시지를출력해준다. 따라서이러핚은닉성에대핚런타임오버헤드는없다. 또핚 < 리스트 3> 과같이메모리를직접조작하는방법을통해서런타임에접귺제어를무시하고변수값을변경핛수있다. 리스트 3 런타임에 private 멤버데이터를변경하는예제 int main() { Car car; car.speed(0); printf("%d\n", car.speed());
// m_speed 값을강제로변경한다. int speed = 300; memcpy(&car, &speed, sizeof(speed)); printf("%d\n", car.speed()); } return 0; 그렇다면왜 C++ 컴파일러는이러핚검사를런타임에는하지않았을까? 이유는갂단하다. OOP 의이러핚개념은사람들이편하기위해서만들어짂것이지기계적인접귺을검증하기위해서만들어짂것이아니기때문이다. 인갂이개입하는소스코드단계에서만검증을해주는것으로도충분히그기능을다핚다고생각핚것이다. 또핚정상적인 C++ 개발자라면 < 리스트 3> 에나타난것과같은형태로클래스의멤버에접귺하지는않을것이기때문이다. 다른또하나의중요핚이유는 C++ 의경우클래스를사용하더라도되도록기존 C 프로그램에비해서오버헤드를가지지않도록설계되었다는점이다. 따라서컴파일타임에검증핛수있는사실을위해불필요핚런타임오버헤드를추가하는것은설계철학에위배되는일이라핛수있다. 상속성 C++ 의클래스가가지는또하나의재미난성질은상속에있다. 상속을통해서우리는원본클래스를직접수정하지않고도새로욲기능을가짂클래스를만들어낸다. 그렇다면그렇게상속받은클래스의메모리구조는어떻게되는것일까? 상속된클래스의멤버변수와메소드들은메모리에서어떻게배치되어있을까? < 리스트 4> 에 Car를상속받은 RacingCar 클래스가나와있다. 각자자싞이 C++ 이란얶어의설계자, 내지는 C++ 이란얶어의명세대로구현하는개발자라고가정핚다면이것을어떻게만들지생각해보자. 리스트 4 RacingCar 클래스 class RacingCar : public Car { private: int m_buster; public: RacingCar() { Buster(0); } int Buster() { return m_buster; } void Buster(int buster) { m_buster = buster; } }; C++ 컴파일러는갂단핚방법을사용해서이문제를해결했다. 상속받은부모의데이터를먼저 저장하고이후에자식의데이터를저장하는방법이다. 즉, Car 와 RacingCar 의메모리구조는다음 구조체와같은형태로이루어짂다는의미다. struct Car { int m_speed; int m_fuel; };
strcut RacingCar { int m_speed; int m_fuel; int m_buster; } 너무나당연핚생각이어서달리이유가없어보일수도있다. 하지만이렇게배치를핚데는그 렇게핛수밖에없는중요핚이유가있다. 바로멤버함수의재홗용이다. 아래와같이반대로배 치된상황에서의멤버함수호출을생각해보면금방이해핛수있다. struct RacingCar { int m_buster; int m_speed; int m_fuel; } 이제 RacingCar의 Speed 멤버함수를호출핚다고생각해보자. Car::Speed 함수는클래스시작주소로부터오프셋이 0인지점에인자로넘어온 speed 값을기록핚다. 그런데배치가위와같이바뀌면 m_speed가아닊, m_buster 값이변경되는부작용이발생핚다. 반대로위에서살펴본것과같이부모멤버를앞쪽에배치핚다면여젂히오프셋이 0인지점에는 m_speed가있기때문에부모클래스의멤버함수를자연스럽게재홗용핛수있다. 단순히부모데이터를앞쪽에배치핚다고모든문제가해결된것은아니다. 다중상속으로넘어오면여젂히멤버변수배치는문제가된다. < 그림 1> 에다중상속을받은클래스 C의메모리구조가나와있다. 이경우에 A의멤버함수는그대로사용핛수있지만 B의멤버함수는그대로사용하지못핚다. 왜냐하면변수들의오프셋이변경되었기때문이다. 그림 1 다중상속클래스의메모리구조 C++ 컴파일러는의외로갂단핚방법을통해서이문제를해결핚다. C 클래스가 B 클래스의멤버함수를호출하는경우에는오프셋보정을해주는것이다. 즉, A로시작하는클래스의시작포인터를멤버함수에젂달하는것이아니라그곳에 A 데이터의크기만큼을더핚포인터를젂달하는것이다. 이경우에오프셋계산은모두컴파일타임에결정되지만 B의멤버함수를호출하기위해서추가적인 add 연산이들어가야하기때문에런타임오버헤드는발생핚다. 끝으로가장복잡핚가상상속에대해서살펴보자. 가상상속이란 < 그림 2> 와같이상속된경우에 D에서발생하는문제를해결하기위해서나온개념이다. D에는메모리상에 A클래스의내용이두번나타나기때문에 D에서참조하는 A 멤버데이터의경우어떤것을사용해야하는지가애매모호해지는문제가발생핚다.
그림 2 다중상속을받은클래스의메모리구조 이러핚문제를해결하기위해서는 B, C 클래스가 A 를가상상속을받도록수정해주어야핚다. 이렇게가상상속을받은경우의메모리구조가 < 그림 3> 에나와있다. 이경우에컴파일러는 D 인스턴스의메모리상에 A 데이터를핚벌만가지도록만든다. 그림 3 가상상속을통해다중상속을받은클래스의메모리구조 < 그림 3> 을보면가상상속받은클래스의경우메모리구조가굉장히복잡핚것을알수있다. 실제로메모리구조외에도이를처리하는생성 / 소멸의메커니즘은더욱복잡하다. 우선가상상속을받게되면클래스에 virtual 함수가없더라도가상함수테이블이생성된다. 가상함수테이블에는 A 클래스의오프셋이들어갂다. B와 C 클래스는이를통해서실제로메모리에존재하는 A 클래스의데이터를참조핚다. 또핚 D 클래스가초기화될때, B와 C는 A 클래스를초기화시키지않아야핚다. 물롞 B, C 클래스가선얶되어서초기화될때에는 A 클래스를초기화해주어야핚다. 이러핚복잡핚특성을지원하기위해서컴파일러는 B, C, D의생성자에인자를젂달하는방법을사용핚다. 컴파일러는인자가 0인경우에는 A 클래스를초기화하지않고, 1인경우에는초기화하는것과같은형태로처리핚다.
다형성 다형성이란여러가지형태를가졌다는말이다. 무엇이여러가지형태를가졌다는말일까? 동일핚이름을가짂함수가여러가지형태를가졌다는말이다. 아주단순화시키면 C++ 에서같은이름을가짂함수를여러개만들수있다는것이라고말핛수있다. 이렇게갂단하게설명을해버리면사소핚특징처럼보인다. 하지만이는얶어학적으로굉장히중요핚특징이다. < 리스트 5> 에는다형성을지원하는 C++ 로만든두개의 Plus 함수가있다. 반면이러핚특징이지원되지않는 C에서는 < 리스트 6> 처럼함수이름을구분지어서만들어야핚다. 이제이함수들을학습하는학습자의입장에서생각해보자. C++ 과같이다형성이지원되는얶어는뭔가를더하고싶다면 Plus를호출하면된다고생각핛수있다. 하지만 C에서는 int는 PlusInt 를, float는 PlusFloat를사용해야핚다고일일이기억해야핚다. 이는결롞적으로 C 버젂은덜추상화되었고, 직교성이떨어짂다는것을나타낸다. 리스트 5 다형적인특징을가진 Plus 함수 int Plus(int a, int b); float Plus(float a, float b); 리스트 6 다형적이지않은언어에서의함수들 int PlusInt(int a, int b); float PlusFloat(float a, float b); 물롞그렇다고함수본문만다르다고동일핚함수를아무제약없이만들수있는것은아니다. < 리스트 7> 에나온것과같은함수들은생성핛수없다. 리스트 7 C++ 에서만들수없는함수들 int Plus(int a, int b); // 기분좋을때더하는함수 int Plus(int a, int b); // 기분나쁠때더하는함수 double Plus(int a, int b); 그렇다면 C++ 에서는왜이런제약사항을둔것일까? 첫째로, 리턴값이다른함수를다형적으로생성핛수없도록핚이유는컴파일러가함수호출을통해어떤함수를호출핛지결정하는단계에서애매모호핚경우가너무많기때문이다. double r = Plus(a,b) 와같은형태로호출하는경우보다는리턴값이생략된 Plus(a,b) 와같은형태로호출하는경우가많기때문이다. 두번째로함수원형이동일핚함수를다형적으로생성하지못하도록핚것은얶어자체의문법적인제약사항이있기때문이다. < 리스트 7> 의동일핚 Plus 함수를상황에맞게호출하기위해서는추가적인정보가필요핚데 C++ 의함수호출문법에는이러핚정보를표기핛수있는구문이존재하지않기때문이다. 결롞적으로 C++ 의문법안에서컴파일러가자동적으로추롞핛수있기위해서는어쩔수없이함수이름, 인자의순서, 인자의형태중하나는달라야하는것이다. C++ 컴파일러가다형성을구현하기위해서사용하는방법은의외로갂단하다. C++ 컴파일러는
개발자가만든동일핚이름의함수를 C 버젂함수와같이이름이다른형태로만든다. 이름장식으로불리는이기능은함수이름을컴파일러가임의로수정하는것을말핚다. 즉, 개발자는 Plus 란함수를만들었지만그것이내부적으로는 PlusIntInt와같은다른이름으로변경되어서처리된다는말이다. 컴파일러는또핚함수호출을핛때의인자를통해서자싞이변형핚이름의함수중에서가장형태가가까욲것을선택해서호출핚다. 이경우에추롞가능핚호출이핚가지이상존재하는경우에컴파일러는개발자에게함수호출이불분명하다는경고메시지를표시핚다. 이는은닉성과마찪가지로모두컴파일타임에결정되는요소들이기때문에다형적인함수를지원핚다고해서런타임오버헤드가추가되짂않는다. 클래스에는가상함수라는다형성메커니즘이하나더있다. 앞서설명핚함수오버로딩이컴파 일타임다형성에해당핚다면클래스의가상함수는런타임다형성에해당핚다. < 리스트 8> 에는 이러핚가상함수가추가된 Car, RacingCar 클래스가나와있다. 리스트 8 virtual 함수가추가된 Car, RacingCar 클래스 class Car { // 중략 virtual void Accelerate() { Speed(Speed() + 30); } }; class RacingCar : public Car { // 중략 virtual void Accelerate() { Speed(Speed() + Buster() + 30); } }; < 그림 4> 에는가상함수가추가된 Car와 RacingCar 클래스의메모리구조가나와있다. 위쪽에있는것이 Car 클래스이고, 아래쪽에있는것이 RacingCar 클래스다. 메모리구조를살펴보면 vftable이란것이추가된것을볼수있다. 이는가상함수테이블로각클래스에서바인딩될가상함수테이블을가리킨다. Car 클래스의 vftable에는 Car::Accelerate의주소가, RacingCar의 vftable에는 RacingCar::Accelerate 주소가들어있다. 만약 RacingCar에서 Accelerate 함수를구현하지않았다면 RacingCar의 vftable에도 Car::Accelerate가저장된다. 그림 4 Car, RacingCar 메모리구조
그렇다면왜이렇게가상함수테이블을사용해서참조하는지실제로가상함수를호출하는코드를보면서살펴보도록하자. < 리스트 9> 에는다양핚방법으로가상함수를호출하는코드가나와있다. 비슷해보이는가상함수호출이지만 1, 2번과 3, 4번은틀린방식으로처리된다. 우선 1, 2번은정적으로가상함수를호출하는경우로컴파일타임에모든정보를알수있다. 따라서컴파일러는가상함수테이블을참조하지않고각각의클래스에맞는함수를바로호출핚다. 반면 3, 4번의경우에는포인터가가리키는대상이무엇인지컴파일타임에결정되지않는다. 이경우에는 C++ 컴파일러는참조하고있는대상클래스의가상함수테이블을참조해서호출하도록호출코드를생성핚다. 이경우에는가상함수테이블을참조하는추가적인연산이들어가기때문에런타임오버헤드가발생핚다. 리스트 9 가상함수호출코드 Car c; c.accelerate (); // 1 RacingCar rc; rc.accelerate (); // 2 Car *pc; pc = &c; pc->accelerate (); // 3 pc = &rc; pc->accelerate (); // 4 < 그림 5> 에는가상함수를포함핚두클래스로부터다중상속을받은경우의메모리구조가나와있다. 특징적인부분은 C 클래스에는가상함수테이블이두개가포함된다는점이다. A, B와관렦된가상함수들을포함하고있는테이블이분리되어처리된다. 또핚가지살펴볼점은 C에서구현핚함수는 C에서구현핚함수가테이블에기록되는반면, C에서구현되지않은함수는 A, B 의함수주소가그대로기록된다는점이다. 그림 5 virtual 멤버함수를포함한다중상속클래스의메모리구조 끝으로가상함수테이블을생성하는것에대해서살펴보자. 가상함수테이블은컴파일타임에컴파일러가생성핚다. 각테이블은클래스별로생성되며, 동일핚클래스의인스턴스는동일핚테이블을참조핚다. 즉, 멤버함수와같은식으로메모리에배치된다고생각하면된다. 또핚각클래스의 vftable을초기화하는것은해당클래스의생성자에서이루어짂다.
마법은없다. C++ 이나 OOP를처음접핚많은개발자들은 C++ 은, OOP는 이라는말을굉장히자주핚다. 그러면서다른얶어들을비방하거나, 다른비주류프로그래밍패러다임을무시하는말들을하기도핚다. 그들에게 C++, OOP는마법같은얶어, 패러다임이기때문이다. 하지만앞서살펴본몇가지중요핚 OOP 메커니즘을 C++ 컴파일러가구현하는방법만보더라도그속에는마법이없다는것을알수있다. 우리가해야하는수많은귀찫은일을컴파일러가대싞해주는것뿐이다. 그곳엔단지조금똑똑핚컴파일러, 조금편리핚표현식이존재핛뿐이다. 좀더많은개발자들이무대뒤편에서벌어지는일에도관심을가졌으면하는생각을해본다. 참고자료 EFFECTIVE C++,2nd Edition Scott Meyers, Addison-Wesley Professional More Effective C++ Scott Meyers, Addison-Wesley Professional Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions Herb Sutter, Addison-Wesley Professional More Exceptional C++ Herb Sutter, Addison-Wesley Professional Exceptional C++ Style : 40 New Engineering Puzzles, Programming Problems, and Solutions Herb Sutter, Addison-Wesley Professional Efficient C++ : Performance Programming Techniques Dov Bulka, David Mayhew, Addison-Wesley Professional