자바스크립트를 활성화 해주세요

C++의 객체지향 구현: 오버로딩 편

 ·  ☕ 7 min read

일반적으로 C와 C++의 차이로 객체지향을 이야기한다. 하지만 객체지향은 방법론에 불과하며, C언어를 통해서도 객체지향 프로그래밍은 가능하다. 하지만 객체지향의 주요 특성은 프로그래밍 언어 수준에서 제공해줘야 한다. 이번 글에서는 객체지향의 특징 중 다형성, 특히 오버로딩이 C++에서 어떤 식으로 구현되는지 알아보도록 하겠다.

오버로딩의 정의 및 예제

먼저 위키에 있는 함수 오버로드 문서의 핵심 부분을 인용하도록 하겠다.

같은 함수 이름을 가지고 있으나 매개변수, 리턴타입 등의 특징은 다른 여러개의 서브프로그램 생성을 가능하게 한다.

쉽게 설명하면 함수 이름이 같아도 매개변수가 다른 경우를 허용한다는 것이다. 아래 코드와 함께 확인해 보자.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using std::cout;
using std::endl;

int add(int a, int b)
{
    return a + b;
}

double add(double a, double b)
{
    return a + b;
}

int main(void)
{
    cout << add(3, 4) << endl;
    cout << add(3.1, 4.2) << endl;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

double add(double a, double b)
{
    return a + b;
}

int main(void)
{
    printf("%d\n", add(3, 4));
    printf("%lf\n", add(3.1, 4.2));
}
$ g++ add.cc -o add_cpp
$ ./add_cpp
7
7.3

$ gcc add.c -o add_c
add.c:8:8: error: conflicting types for ‘add’; have ‘double(double,  double)’
    8 | double add(double a, double b)
      |        ^~~
add.c:3:5: note: previous definition of ‘add’ with type ‘int(int,  int)’
    3 | int add(int a, int b)
      |     ^~~
add.c: In function ‘main’:
add.c:15:18: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘double’ [-Wformat=]
   15 |         printf("%d\n", add(3, 4));
      |                 ~^     ~~~~~~~~~
      |                  |     |
      |                  int   double
      |                 %f

위 예제를 보면 똑같은 이름의 add() 함수임에도 매개변수의 타입이 int 혹은 double로 다른 경우 두 함수가 동시 존재할 수 있고, 내가 전달한 인자 타입에 따라 자동으로 두 함수 중 하나가 선택된다.

반면 아래와 같은 수준의 오버로딩은 지원되지 않는다. 함수 정의나 호출이 모호하기 때문에 컴파일러가 임의로 결정할 수 없기 때문이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using std::cout;
using std::endl;

int inc(int base, int add = 1)
{
    return base + add;
}

int inc(int base)
{
    return base + 1;
}

int main(void)
{
    cout << inc(3, 1) << endl;
    cout << inc(3) << endl;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using std::cout;
using std::endl;

int divide(int dividend, int divisor)
{
    return dividend / divisor;
}

double divide(int dividend, int divisor)
{
    return (double)dividend / (double)divisor;
}

int main(void)
{
    int quotient = divide(7, 3);
    double div_rate = divide(7, 3);

    cout << quotient << endl;
    cout << div_rate << endl;
}
$ g++ inc.cc -o inc
inc.cc: In function ‘int main()’:
inc.cc:18:20: error: call of overloaded ‘inc(int)’ is ambiguous
   18 |         cout << inc(4) << endl;
      |                 ~~~^~~
inc.cc:5:5: note: candidate: ‘int inc(int, int)’
    5 | int inc(int base, int add = 1)
      |     ^~~
inc.cc:10:5: note: candidate: ‘int inc(int)’
   10 | int inc(int base)
      |     ^~~

$ g++ div.cc -o div
div.cc:10:8: error: ambiguating new declaration of ‘double divide(int, int)’
   10 | double divide(int dividend, int divisor)
      |        ^~~~~~
div.cc:5:5: note: old declaration ‘int divide(int, int)’
    5 | int divide(int dividend, int divisor)
      |     ^~~~~~

위에서 살펴봤던 예시처럼 함수 오버로딩을 할 경우, 함수의 이름이 같기 때문에 서로 표현할 때 헷갈리기 쉽다. 그래서 이런 경우는 함수가 어떤 타입의 인자들을 받고, 어떤 타입으로 결과를 반환하는지를 묶어서 함수 시그니처라 한다. 객체지향 언어의 경우 메서드 시그니처라고 부르기도 한다.

함수 오버로딩 결과 분석

함수 오버로딩을 했을 때, 컴파일된 결과에서 어떤 차이가 있는지 확인해 보기 위해 제일 처음 본 add() 예제에서 cout 등의 출력 관련 함수 호출을 제거하여 분석을 단순화해보자.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int add(int a, int b)
{
    return a + b;
}

double add(double a, double b)
{
    return a + b;
}

int main(void)
{
    add(3, 4);
    add(3.1, 4.2);
}
$ g++ add_bin.cc -o add_bin
$ objdump -d add_bin > add_bin.asm
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
0000000000001120 <frame_dummy>:
    1120:       f3 0f 1e fa             endbr64
    1124:       e9 77 ff ff ff          jmp    10a0 <register_tm_clones>

0000000000001129 <_Z3addii>:
    1129:       f3 0f 1e fa             endbr64
    112d:       55                      push   %rbp
    112e:       48 89 e5                mov    %rsp,%rbp
    1131:       89 7d fc                mov    %edi,-0x4(%rbp)
    1134:       89 75 f8                mov    %esi,-0x8(%rbp)
    1137:       8b 55 fc                mov    -0x4(%rbp),%edx
    113a:       8b 45 f8                mov    -0x8(%rbp),%eax
    113d:       01 d0                   add    %edx,%eax
    113f:       5d                      pop    %rbp
    1140:       c3                      ret

0000000000001141 <_Z3adddd>:
    1141:       f3 0f 1e fa             endbr64
    1145:       55                      push   %rbp
    1146:       48 89 e5                mov    %rsp,%rbp
    1149:       f2 0f 11 45 f8          movsd  %xmm0,-0x8(%rbp)
    114e:       f2 0f 11 4d f0          movsd  %xmm1,-0x10(%rbp)
    1153:       f2 0f 10 45 f8          movsd  -0x8(%rbp),%xmm0
    1158:       f2 0f 58 45 f0          addsd  -0x10(%rbp),%xmm0
    115d:       66 48 0f 7e c0          movq   %xmm0,%rax
    1162:       66 48 0f 6e c0          movq   %rax,%xmm0
    1167:       5d                      pop    %rbp
    1168:       c3                      ret

0000000000001169 <main>:
    1169:       f3 0f 1e fa             endbr64
    116d:       55                      push   %rbp
    116e:       48 89 e5                mov    %rsp,%rbp
    1171:       be 04 00 00 00          mov    $0x4,%esi
    1176:       bf 03 00 00 00          mov    $0x3,%edi
    117b:       e8 a9 ff ff ff          call   1129 <_Z3addii>
    1180:       f2 0f 10 05 80 0e 00    movsd  0xe80(%rip),%xmm0        # 2008 <_IO_stdin_used+0x8>
    1187:       00
    1188:       48 8b 05 81 0e 00 00    mov    0xe81(%rip),%rax        # 2010 <_IO_stdin_used+0x10>
    118f:       66 0f 28 c8             movapd %xmm0,%xmm1
    1193:       66 48 0f 6e c0          movq   %rax,%xmm0
    1198:       e8 a4 ff ff ff          call   1141 <_Z3adddd>
    119d:       b8 00 00 00 00          mov    $0x0,%eax
    11a2:       5d                      pop    %rbp
    11a3:       c3                      ret

C++ 프로그램의 시작점이 되는 main() 함수의 디스어셈블리 내용을 보면, 기존 C++ 코드에서 함수 호출한 것과 같이 call 명령이 두 번 수행된 것을 확인할 수 있다. 처음의 add(int, int)_Z3addii 심볼이며, add(double, double)_Z3adddd인 것으로 유추할 수 있다. (함수 호출 순서, 내부적으로 사용하는 레지스터의 크기, 연산 등에 사용되는 명령어의 종류 등)

분명 C++에서 선언한 함수 이름과는 다르지만, 어딘가 닮은 부분이 있다. 이런 방식을 맹글링이라 한다. 예제처럼 함수만 오버로딩하는 것이 아니라, 다른 클래스들이 같은 이름의 함수를 가질 수도 있기 때문에, 이를 컴파일 과정에서 함수 심볼을 변경하는 것이다.

예제에서 나온 함수는 간단하니, 위키 설명을 참고하며 해석해보자.

_Z3addii_Z, 3add, ii로 나눠 해석할 수 있다.

  • _Z는 맹글링된 심볼이라는 의미의 헤더 역할이다.
  • 3은 바로 뒤에 나올 심볼을 해석할 때, 3글자를 읽어야 한다는 뜻이다.
    그리고 읽을 3글자는 add다.
  • ii는 함수의 시그니처로 제공된 인자의 순서가 int, int라는 뜻이다.
    (바로 아래 _Z3adddd를 보면 dddouble, double임을 알 수 있다.)

참고로 위 예제의 맹글링 결과와 해석 방식은 컴파일러에 따라 다르다. Visual C++에서 맹글링하는 규칙은 여기를 참고하자.

맹글링으로 인해 생기는 빌드 실패 원인

가끔 개발하다 보면 C와 C++을 섞어서 사용해야 하는 경우가 존재한다. 간단한 수준의 프로그램이라면 C 소스를 C++로 강제 컴파일하면 해결할 수 있다. 하지만 엄밀히 말하면 C는 C++의 하위 호환이라기엔 다른 부분이 존재한다. 대표적으로 아래 코드는 C언어에서 허용되지만, C++에서는 오류가 발생하는 문법이다.

int sparse_array[40] = {
    [0] = 4,
    [4] = 10
};

이런 문제로 C 소스를 C++로 강제 컴파일하지 못할 경우, 링킹 과정에서 맹글링으로 인한 오류가 발생할 수 있다. 아래 예시를 참고하자.

1
2
3
4
5
6
#ifndef __ADDER_H__
#define __ADDER_H__

extern int add(int, int);

#endif /* __ADDER_H__ */
1
2
3
4
5
6
#include "adder.h"

int add(int a, int b)
{
    return a + b;
}
1
2
3
4
5
6
#include "adder.h"

int main()
{
    add(3, 4);
}
$ gcc -c adder.c
$ g++ -c main.cc
$ ls *.o
adder.o  main.o

$ g++ -o adder adder.o main.o
/usr/bin/ld: main.o: in function `main':
main.cc:(.text+0x13): undefined reference to `add(int, int)'
collect2: error: ld returned 1 exit status

$ objdump -r main.o

main.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE
0000000000000013 R_X86_64_PLT32    _Z3addii-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE
0000000000000020 R_X86_64_PC32     .text


$ objdump -d adder.o

adder.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <add>:
   0:   f3 0f 1e fa             endbr64
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   89 7d fc                mov    %edi,-0x4(%rbp)
   b:   89 75 f8                mov    %esi,-0x8(%rbp)
   e:   8b 55 fc                mov    -0x4(%rbp),%edx
  11:   8b 45 f8                mov    -0x8(%rbp),%eax
  14:   01 d0                   add    %edx,%eax
  16:   5d                      pop    %rbp
  17:   c3                      ret

먼저, 오브젝트 파일들을 링킹하는 과정(gcc -o adder adder.o main.o)에서 생기는 오류를 보면, 분명 C에서 구현한 add(int, int)가 존재함에도 찾을 수 없다고 한다. 아래 objdump로 분석해보면, main.o에서 _Z3addii 심볼을 호출하고 싶지만, adder.o에는 add란 심볼만 존재하기 때문에 찾을 수 없는 것이다.

이렇게 C++의 맹글링으로 인해 생기는 빌드 문제를 해결하려면 extern "C" 등으로 표기해줘야 한다. 아래 예시를 확인해보자.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#ifndef __CALCULATOR_H__
#define __CALCULATOR_H__

#ifdef __cplusplus
extern "C" { /* C-C++ compatibility */
#endif

extern int add(int a, int b);
extern int mul(int a, int b);

#ifdef __cplusplus
} /* C-C++ compatibility */
#endif

#endif /* __CALCULATOR_H__ */
1
2
3
4
5
6
#include "calculator.h"

int add(int a, int b)
{
    return a + b;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include "calculator.h"

int mul(int num, int times)
{
    int result = 0;
    for (int i = 0; i < times; i++) {
        result = add(result, num);
    }
    return result;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>
#include "calculator.h"

using std::cout;
using std::endl;

int main()
{
    cout << add(3, 4) << ' ' << mul(3, 4) << endl;
}
$ gcc -c adder.c
$ gcc -c multiplier.c
$ g++ -c main.cc
$ ls *.o
adder.o  main.o  multiplier.o

$ g++ -o calculator adder.o multiplier.o main.o
$ ./calculator
7 12

기본적으로 C에서 구현할 함수를 C++에서 호출하려 하면 extern "C" 키워드를 함수 선언 앞에 선언하면 된다. 하지만 C에서는 extern "C" 키워드를 사용하면 문법 오류로 컴파일할 수 없다. 이런 문제를 해결하기 위해 C++ 컴파일러만 선언되는 매크로인 __cplusplus을 기준으로 코드가 삽입되게 하면 된다.


JaeSang Yoo
글쓴이
JaeSang Yoo
The Programmer

목차