가상(virtual) 함수는 실제로는 어떤 함수가 실행될지 결정되기도 전에 미리 사용되었다고 해서 가상이라는 이름을 얻었습니다.
가상 함수는 실행시간 다형성을 구현하는 방법으로, 소프트웨어의 재사용을 위한 중요한 도구입니다.
함수를 가상으로 선언하는 것은 개발자가 컴파일러에게 "나는 어떤 함수가 호출되어야 할지 모르겠으니 함수 결정을 미루어라. 실행시간에 실제로 객체가 만들어지고 난 다음 결정하라"라고 알려주는 것입니다.
2022.08.12 - [C++ 프로그래밍/클래스] - 다형성-상속에서의 형 변환
다형성-상속에서의 형 변환
1. 기본 자료형에 대한 형 변환 형 변환이란 특정한 자료형에 속하는 값을 다른 자료형의 값으로 변환하는 것을 말합니다. 다음은 int 값을 double로 변환하는 형 변환이 이루어지는 문장입니다. int
onesside-world.tistory.com
이전글에서 그림을 참고해서 실제로 구현된 그래픽 에디터 코드를 짜면 밑에 코드처럼 되고
move() 멤버 함수는 기준점을 기준으로 하니까 오버라이딩이 필요없고 미리 결정할 수 있지만.
모양이 변해야하는 draw함수는 미리 결정하기 어렵습니다.
#pragma once
#include <iostream>
#include <string>
#define MAXLINES 100
using namespace std;
class Canvas {
string line[MAXLINES]; // 화면 출력을 위한 문자열
int xMax, yMax; // 맵의 크기
public:
Canvas(int nx = 10, int ny = 10) : xMax(nx), yMax(ny) {
for (int y = 0; y < yMax; y++)
line[y] = string(xMax * 2, ' ');
}
void draw(int x, int y, string val) {
if (x >= 0 && y >= 0 && x<xMax && y<yMax)
line[y].replace(x * 2, 2, val);
}
void clear(string val = ". ") { // 디폴트 매개변수: 공백 2개
for (int y = 0; y < yMax; y++)
for (int x = 0; x < xMax; x++)
draw(x, y, val);
}
void print(char *title = "<My Canvas>") {
system("cls");
cout << title << endl;
for (int y = 0; y < yMax; y++)
cout << line[y] << endl;
cout << endl;
}
};
화면을 그리는 Canvas 클래스
#pragma once
#include "Canvas.h"
class Point {
public:
int x, y; // 점의 좌표
Point(int xx = 0, int yy = 0) : x(xx), y(yy) {}
void move(int dx, int dy) { x += dx; y += dy; }
};
class Shape {
protected:
Point p; // 형태의 위치
public:
Shape(int x = 0, int y = 0) : p(x, y) {}
virtual void draw(Canvas& canvas, string color = "★") {
canvas.draw(p.x, p.y, color);
}
void move(int dx, int dy) { p.move(dx, dy); }
};
// 선분과 관련된 클래스들 --------------------------------------------------------
inline int Abs(int x) { return x > 0 ? x : -x; }
inline int Max(int x, int y) { return x > y ? x : y; }
inline int Round(double x) { return (int)(x + 0.5); }
class Line : public Shape {
Point q; // 선분의 다른 쪽 끝점 (한쪽 끝점은 p임)
public:
Line(int x1 = 0, int y1 = 0, int x2 = 0, int y2 = 0)
: Shape(x1, y1), q(x2, y2) { }
void draw(Canvas& canvas, string color = "선") {
int len = Max(Abs(q.x - p.x), Abs(q.y - p.y));
double x = p.x, y = p.y;
double dx = (q.x - p.x) / (double)len, dy = (q.y - p.y) / (double)len;
for (int i = 0; i <= len; i++) {
canvas.draw(Round(x), Round(y), color);
x += dx;
y += dy;
}
}
void move(int dx, int dy) { p.move(dx, dy); q.move(dx, dy); }
};
class HLine : public Line {
public:
HLine(int x = 0, int y = 0, int len = 0) : Line(x, y, x + len, y) { }
};
class VLine : public Line {
public:
VLine(int x = 0, int y = 0, int len = 0) : Line(x, y, x, y + len) { }
};
// 사각형과 관련된 클래스들 --------------------------------------------------------
class Rect : public Shape {
int w, h; // 사각형의 가로와 세로 길이 (시작점은 p임)
public:
Rect(int x = 0, int y = 0, int ww = 0, int hh = 0)
: Shape(x, y), w(ww), h(hh) { }
void draw(Canvas& canvas, string color = "■") {
for (int i = p.x; i <= p.x + w; i++) {
canvas.draw(i, p.y, color); // 사각형의 윗변
canvas.draw(i, p.y + h, color); // 사각형의 아랫변
}
for (int i = p.y; i <= p.y + h; i++) {
canvas.draw(p.x, i, color); // 사각형의 좌변
canvas.draw(p.x + w, i, color); // 사각형의 우변
}
}
};
class Square : public Rect {
public:
Square(int x = 0, int y = 0, int w = 0) : Rect(x, y, w, w) { }
};
// 원 클래스 ----------------------------------------------------------------------------
class Circle : public Shape {
int r; // 원의 반지름 (중심은 p를 사용)
public:
Circle(int x = 0, int y = 0, int rr = 0)
: Shape(x, y), r(rr) { }
void draw(Canvas& canvas, string color = "◎") {
Line(p.x, p.y, p.x, p.y + r).draw(canvas, color);
Line(p.x, p.y, p.x, p.y - r).draw(canvas, color);
Line(p.x, p.y, p.x + r, p.y).draw(canvas, color);
Line(p.x, p.y, p.x - r, p.y).draw(canvas, color);
}
};
여러 도형을 관리하는 Shape 클래스
#include "Shapes.h"
void main()
{
Canvas myCanvas(25, 15);
Shape* list[100];
int nShape = 0;
while (true) {
myCanvas.print(" < 내 마음대로 그릴 수 있는 나의 그래픽 편집기 >");
char str[200], type;
int v[4];
printf("Input ==> ");
gets_s(str);
int ret = sscanf(str, "%c%d%d%d%d", &type, v, v + 1, v + 2, v + 3);
if (type == 'l' && ret == 5)
list[nShape++] = new Line(v[0], v[1], v[2], v[3]);
else if (type == 'v' && ret == 4)
list[nShape++] = new VLine(v[0], v[1], v[2]);
else if (type == 'h' && ret == 4)
list[nShape++] = new HLine(v[0], v[1], v[2]);
else if (type == 'c' && ret == 4)
list[nShape++] = new Circle(v[0], v[1], v[2]);
else if (type == 'r' && ret == 5)
list[nShape++] = new Rect(v[0], v[1], v[2], v[3]);
else if (type == 's' && ret == 4)
list[nShape++] = new Square(v[0], v[1], v[2]);
else if (type == 'q') break;
myCanvas.clear(". ");
for (int i = 0; i < nShape; i++)
list[i]->draw(myCanvas);
}
for (int i = 0; i < nShape; i++)
delete list[i];
}
그래픽 에디터를 사용하는 Main.cpp
Main.cpp에서 list[i]->draw(myCanvas);의 list[i]는 자료형은 Shape*이지만 실제로는 자식 클래스로 생성된 객체들을
가리키고 있습니다.
따라서 만약 실행 시에 list[i]의 실제 클래스를 찾아 해당 클래스에서 draw() 실행할 수 있다면 완벽할 것입니다.
이를 위한 처리 과정은 다음과 같습니다.
- Shape 클래스에서 draw()를 가상 함수로 선언한다.
부모가 가상 함수로 선언한 멤버 함수는 자식 클래스에서도 자동으로 가상 함수가 됩니다. - Shpae의 자식 클래스 Line, Circle 및 Rectangle에서는 draw()를 재정의하고,
해당 클래스의 특징에 따라 화면에 출력하는 함수를 구현합니다. - 실행시간에 list[i]->draw; 문장을 만났을 때, draw()가 가상 함수이면 list[i]가 실제로 어느 클래스에서 생성되었는지를 먼저 확인한다. 그리고 그 클레스에서부터 draw() 함수를 실행합니다.
이러한 방법을 동적 바인딩(dynamic bining) 이라고 합니다.
함수 중복을 포함해서 지금까지 공부한 모든 함수들은 정적 바인딩(static binding),
즉 컴파일 과정에 실행할 함수를 결정한다는 것입니다.
C++에서는 가상 함수가 아닌 모든 함수들에 정적 바인딩을 적용합니다.
▶가상 함수 선언은 매우 간단합니다.
부모 클래스의 Shape의 draw() 선언 시 다음과 같이 맨 앞에 virtual 키워드를 넣어주면 됩니다.
- virtual void draw() { . . . }
▶가상 소멸자
객체가 다양한 클래스로 생성되었으면 소멸도 생성되었던 클래스의 소멸자를 이용해 처리해야합니다.
만약 가상 소멸자를 해주지 않으면 동적 생성된 객체를 제대로 메모리 해제를 해주지 못해,
메모리 누수가 발생하게 됩니다.
그렇기 때문에 반드시 소멸자도 가상 함수가 되어야 합니다.
- virtual ~Shape() { . . . }
'C++ 프로그래밍 > 클래스' 카테고리의 다른 글
다형성-순수 가상 함수와 추상 클래스 (0) | 2022.08.22 |
---|---|
다형성-가상 함수와 객체의 크기 (0) | 2022.08.22 |
다형성-다형성이란? (0) | 2022.08.12 |
다형성-상속에서의 형 변환 (0) | 2022.08.12 |
상속 (0) | 2022.08.08 |