C/C++ 학습 · 9 min read · Oct 10, 2025
C/C++ 단계별 학습 - 페이지 16
16. 단계별 C/C++ — C++ 프로그래밍 - 다형성
다형성
| | 1. 함수 오버로딩
- 다형성
- 다형성의 종류
- 포인터로 접근하는 일반 멤버 함수
- 가상 함수
- 순수 함수
- 대입 및 복사 초기화
- 복사 생성자
- ‘this’ 포인터 |
1. 함수 오버로딩
인수의 동작이 다른 함수의 이름이 다르면 이를 함수 다형성 또는 함수 오버로딩이라고 합니다.
| | // 함수 오버로딩의 사용을 보여주는 예제 프로그램
#include
using namespace std;
void printline()
{
for(int i=0;i<=80; i++) cout << “-“;
}
void printline(int n)
{
for(int i =0 ;i<=n;i++) cout << “-“;
}
void printline(int n,char ch)
{
for(int i=0;i<=n; i++) cout << ch;
}
int main()
{
printline();
printline(5);
printline(10, ‘*’);
return 0;
}
|
다형성
다형성은 OOP의 중요한 기능 중 하나입니다. 이는 간단히 말해 하나의 이름, 여러 형태를 의미합니다. 우리는 이미 오버로딩된 함수와 연산자를 사용하여 다형성 개념이 어떻게 구현되는지 보았습니다. 오버로딩된 멤버 함수는 인수의 유형과 수를 일치시켜 호출을 위해 선택됩니다. 컴파일러는 컴파일 시간에 이 정보를 알고 있으므로 특정 호출에 적합한 함수를 컴파일 시간에 선택할 수 있습니다. 이를 조기 바인딩 또는 정적 바인딩 또는 정적 링크라고 합니다. 또한 컴파일 시간 다형성으로 알려져 있으며, 조기 바인딩은 객체가 컴파일 시간에 함수 호출에 바인딩된다는 것을 의미합니다.
이제 기본 클래스와 파생 클래스 모두에서 함수 이름과 프로토타입이 동일한 상황을 고려해 보겠습니다. 예를 들어, 다음 클래스 정의를 고려하십시오.
| | #include
using namespace std;
class A
{
int x;
public : void show();
};
class B : public A
{
int y;
public : void show();
};
int main()
{
B b;
b.show();
return 0;
} |
show( ) 멤버 함수를 사용하여 클래스 A와 B의 객체 값을 인쇄하려면 어떻게 해야 합니까? show( )의 프로토타입이 두 곳에서 동일하므로 함수는 오버로딩되지 않으며 따라서 정적 바인딩이 적용되지 않습니다. 사실, 컴파일러는 무엇을 해야 할지 모르고 결정을 연기합니다.
프로그램이 실행되는 동안 적절한 멤버 함수를 선택할 수 있다면 좋을 것입니다. 이를 런타임 다형성이라고 합니다. 어떻게 가능할까요? C++는 런타임 다형성을 달성하기 위해 가상 함수라는 메커니즘을 지원합니다. 런타임에 어떤 클래스 객체가 고려되고 있는지 알게 되면 적절한 버전의 함수가 호출됩니다.
함수가 컴파일 후 훨씬 나중에 특정 클래스와 연결되므로 이 과정을 늦은 바인딩이라고 합니다. 이는 적절한 함수의 선택이 런타임에 동적으로 이루어지기 때문에 동적 바인딩 또는 동적 링크로도 알려져 있습니다.
3. 다형성의 종류
다형성은 두 가지 유형이 있습니다.
| 1 | 컴파일 시간 다형성
또는 조기 바인딩
또는 정적 바인딩
또는 정적 링크 다형성. 객체는 컴파일 시간에 함수 호출에 바인딩됩니다. |
| 2 | 런타임 다형성
또는 늦은 바인딩
또는 동적 바인딩
또는 동적 링크 다형성. 선택 및 적절한 함수는 런타임에 동적으로 수행됩니다. |
| |
|
동적 바인딩은 C++의 강력한 기능 중 하나입니다. 이는 객체에 대한 포인터를 사용해야 합니다. 우리는 객체 포인터와 가상 함수가 동적 바인딩을 구현하는 데 어떻게 사용되는지 자세히 논의할 것입니다.
4. 포인터로 접근하는 일반 멤버 함수
아래 프로그램은 기본 클래스를 포함합니다.
| | / 일반 **함수 포인터에서 접근 /
/ 클래스와의 다형성 (가상 다형성 사용하지 않음) /
#include
using namespace std;
class BASE
{
public :
void disp() { cout << “\n당신은 BASE 클래스에 있습니다 “; }
};
class DERIVED1 : public BASE
{
public :
void disp() { cout << “\n당신은 DERIVED1 클래스에 있습니다”; }
};
class DERIVED2 : public BASE
{
public :
void disp() { cout << “\n당신은 DERIVED2 클래스에 있습니다”; }
};
int main()
{
DERIVED1 d1; // 파생 클래스 1의 객체
DERIVED2 d2; // 파생 클래스 2의 객체
BASE *b; // 기본 클래스에 대한 포인터
b=&d1; // 포인터 b에 d1의 주소 할당
b->disp(); // disp() 호출
b=&d2; // 포인터 b에 d2의 주소 할당
b->disp(); // disp() 호출
return 0;
} |
위 프로그램은 다음을 보여줍니다:
| | • BASE 클래스
• BASE에서 파생된 DERIVED1, DERIVED2 클래스
• 파생 클래스 객체 (d1,d2)
• BASE 클래스 포인터 b | | | *출력 당신은 BASE 클래스에 있습니다
당신은 BASE 클래스에 있습니다 |
5. 가상 함수
가상은 실제로 존재하지만 현실에서는 존재하지 않는 것을 의미합니다.
멤버 함수를 가상 함수로 만들려면 멤버 함수 앞에 virtual 키워드를 붙이면 됩니다.
| | / 클래스와의 다형성 (가상 다형성) /
#include
using namespace std;
class B
{
public :
void show(){ cout << “\n클래스 B 메서드 Show() “; }
virtual void disp() { cout << “\n클래스 B 메서드 disp()”; }
};
class D : public B
{
public :
void show(){cout << “\n클래스 D 메서드 Show() “; }
void disp(){ cout << “\n클래스 D 메서드 disp()”; }
};
int main()
{
D d1;
d1.show();
d1.disp(); // 기본 클래스 멤버
B b;
D d;
B *Bptr;
Bptr = &b;
Bptr->show();
Bptr->disp(); // 기본 클래스 멤버
Bptr=&d;
Bptr->show(); // 파생 클래스 멤버
Bptr->disp(); // 기본 클래스 멤버
return 0;
} | | | 출력 클래스 D 메서드 Show()
클래스 D 메서드 disp() |
6. 순수 함수
기본 클래스에서 정의되고 파생 클래스와 관련된 정의가 없는 함수를 순수 함수라고 합니다. 간단히 말해 순수 함수는 본체가 없는 가상 함수입니다.
| | #include
using namespace std;
class B
{
public :
void show(){ cout << “\n클래스 B 메서드 Show() “; }
virtual void disp() = 0; // 순수 가상 함수
};
class D : public B
{
public :
void show(){cout << “\n클래스 D 메서드 Show() “; }
void disp(){ cout << “\n클래스 D 메서드 disp()”; }
};
int main()
{
D d1;
d1.show(); // O/P : 클래스 D 메서드 show()
d1.disp(); // O/P : 클래스 D 메서드 disp()
D d;
B *Bptr;
Bptr=&d;
Bptr->show(); // O/P : 클래스 B 메서드 show()
Bptr->disp(); // O/P : 클래스 D 메서드 disp()
return 0;
} |
Bptr -> show() *는 기본에서 실행 가능한 기본 함수입니다. Bptr -> disp()는 기본에서 실행 가능한 기본 함수이지만 가상 순수 함수로 선언되었으므로 런타임에 파생 클래스의 disp()*가 호출됩니다.
| | / 순수 가상 함수의 장점을 보여주는 프로그램 /
#include
using namespace std;
enum boolean { false, true };
class NAME
{
protected : char name[20];
public :
void getname()
{ cout << “이름 입력 :”; cin >> name; }
void showname()
{ cout << “\n이름은 “<< name; }
boolean virtual isGradeA() = 0; // 순수 가상 함수
};
class student : public NAME
{
private : float avg;
public :
void getavg()
{
cout << “\n학생 평균 입력 :”;
cin >> avg;
}
boolean isGradeA()
{ return (avg>=80) ? true : false ; }
};
class employee : public NAME
{
private : int sal;
public :
void getsal()
{ cout << “\n급여 입력 “; cin >> sal; }
boolean isGradeA()
{ return (sal>=10000) ? true : false ; }
};
int main()
{
NAME names[20]; // 이름에 대한 포인터 수
student s; // 학생에 대한 포인터
employee *e; // 직원에 대한 포인터
int n = 0; // 목록의 이름 수
char choice;
do{
cout << “학생 또는 직원 입력 (s/e) “;
cin >> choice;
if(choice==’s’)
{
s = new student; // 새로운 학생 만들기
s->getname();
s->getavg();
names[n++]=s;
}
else
{
e = new employee; // 새로운 직원 만들기
e->getname();
e->getsal();
names[n++]=e;
}
cout << “또 입력하시겠습니까 (y/n) ?”; // 또 다른 것
cin >> choice;
} while(choice==’y’);
for(int j=0; j
names[j]->showname( );
if(names[j]->isGradeA( )==true)
cout << “그는 1급 인물입니다”;
}
return 0;
} |
7. 대입 및 복사 초기화
C++ 컴파일러는 항상 여러분을 대신하여 바쁘게 일하고 있으며, 여러분이 신경 쓰지 않아도 되는 일을 하고 있습니다. 여러분이 책임을 지면, 컴파일러는 여러분의 판단에 따릅니다. 그렇지 않으면 컴파일러는 자신의 방식으로 일을 처리합니다. 이 과정의 두 가지 중요한 예는 대입 연산자와 복사 생성자입니다.
여러분은 아마도 대입 연산자를 여러 번 사용했을 것이며, 아마도 그것에 대해 깊이 생각하지 않았을 것입니다. a1과 a2가 객체라고 가정해 보겠습니다. 여러분이 컴파일러에게 다른 방법을 지시하지 않는 한, 다음 문장은 다음과 같은 결과를 초래합니다.
a2 = a1; // a2를 a1의 값으로 설정
이는 대입 연산자 =의 기본 동작입니다.
여러분은 또한 변수를 초기화하는 것, 즉 객체를 다른 객체로 초기화하는 것에 익숙할 것입니다. 예를 들어
alpha a2(a1); // a2를 a1의 값으로 초기화
이는 유사한 동작을 초래합니다. 컴파일러는 새로운 객체 a2를 생성하고 a1의 데이터를 멤버별로 복사합니다. 이는 복사 생성자의 기본 동작입니다.
이 두 가지 기본 활동은 컴파일러에 의해 무료로 제공됩니다. 만약 멤버별 복사가 원하는 것이라면, 추가적인 조치를 취할 필요가 없습니다. 그러나 대입 초기화가 더 복잡한 작업을 수행하기를 원한다면, 기본 함수를 재정의할 수 있습니다. 우리는 대입 연산자와 복사 생성자를 오버로딩하는 기술에 대해 별도로 논의할 것입니다.
대입 연산자 오버로딩
| | // 대입 ( = ) 연산자 오버로딩
#include
using namespace std;
class alpha
{
private:
int data;
public:
alpha() { } // 인자가 없는 생성자
alpha( int d )
{ data = d; } // 인자가 하나인 생성자
void display()
{ cout << data; } // 데이터 표시
alpha operator =(alpha & a) // 오버로딩된 = 연산자
{
data = a.data; // 자동으로 수행되지 않음
cout << “\n 대입 연산자 호출됨 “;
return alpha(data);
}
};
int main()
{
alpha a1(37);
alpha a2;
a2 = a1; // 오버로딩된 = 호출
cout << “\n a2 = “; a2.display(); // a2 표시
alpha a3 = a2; // = 호출하지 않음
cout << “\n a3 = “; a3.display(); // a3 표시
return 0;
}
| | | 출력: a2 = 37
a3 = 37 |
8. 복사 생성자
우리가 논의한 바와 같이, 우리는 두 가지 종류의 문으로 다른 객체의 값을 가진 객체를 정의하고 동시에 초기화할 수 있습니다:
alpha a3(a2); // 복사 초기화
alpha a3 = a2; // 복사 초기화, 대체 구문
두 가지 정의 스타일 모두 복사 생성자를 호출합니다. 즉, 인수를 새로운 객체로 복사하는 생성자입니다. 컴파일러가 모든 객체에 대해 자동으로 제공하는 기본 복사 생성자는 멤버별 복사를 수행합니다. 이는 대입 연산자가 수행하는 것과 유사합니다. 차이점은 복사 생성자가 새로운 객체도 생성한다는 것입니다.
다음 예제는 복사 생성자를 보여줍니다.
| | #include
using namespace std;
class alpha
{
private :
int data;
public:
alpha( ) { } // 인자가 없는 생성자
alpha(int d) { data = d; } // 인자가 하나인 생성자
alpha(alpha& a) // 복사 생성자
{
data = a.data;
cout << “\n복사 생성자 호출됨”;
}
void display( )
{ cout << data; }
void operator = (alpha& a) // 오버로딩된 = 연산자
{
data = a.data;
cout << “\n대입 연산자 호출됨”;
}
};
int main()
{
alpha a1( 37 );
alpha a2;
a2 = a1; // 오버로딩된 = 호출
cout << “ a2 = “; a2.display(); // a2 표시
alpha a3( a1 ); // 복사 생성자 호출
// alpha a3 = a1; // a3의 동등한 정의
cout << “ a3 = “; a3.display(); // a3 표시
return 0;
} |
위 프로그램은 대입 연산자와 복사 생성자를 모두 오버로딩합니다.
오버로딩된 대입 연산자는 이전 예제와 유사합니다.
9. ‘this’ 포인터
C++는 this라는 고유한 키워드를 사용하여 멤버 함수를 호출하는 객체를 나타냅니다. this는 this 함수가 호출된 객체를 가리키는 포인터입니다.
이 포인터는 단순히 자신 객체를 반환하는 작업을 수행합니다.
다음 프로그램은 i, j 객체를 정의하고 i는 5의 값을 할당받고 i의 전체 객체는 그 멤버 함수에 의해 j에 할당됩니다.
| | #include
using namespace std;
class A
{
int a;
public:
A() { }
A(int x)
{ a = x; }
void display()
{
cout << a;
}
A get()
{
return *this; // 자신 반환
}
};
int main()
{
A i(5);
A j;
j = i.get();
j.display();
return 0;
} |
참고: Turbo C++의 객체 지향 프로그래밍: 로버트 라포르
새 게시물을 받은 편지함에서 받기
스팸은 없습니다. 언제든지 구독 해지 가능합니다.