추상화 (클래스)
비슷하게 생긴 Cpp과 C를 비교해보자. Cpp는 class를 이용해서 class에서 사용할 상태와 메서드들을 선언할 수 있다. 반면 C의 struct에서는 오직 상태만을 저장할 수 있다. 일반적으로 C에서 struct에 관한 함수들을 작성한다면, 이름으로 특정 구조체와 관련된 함수임을 나타낸다. 예를 들어 x, y, width, height를 변수로 가지는 Rect라는 구조체가 있다면, 아래와 같이 파일을 작성하는 것이 일반적이다.
/* rect.h */
struct Rect {
unsigned int x;
unsigned int y;
unsigned int width;
unsigned int height;
};
void rect_move(struct Rect *rect, unsigned int x, unsigned int y);
void rect_draw(struct Rect *rect);
/* rect.c */
#include <stdio.h>
#include "rect.h"
void rect_move(struct Rect * rect, unsigned int x, unsigned int y) {
/* 사각형 이동 */
rect->x = x;
rect->y = y;
printf("사각형을 움직인다.\n");
}
void rect_draw(struct Rect *rect) {
/* 사각형 그리는 작업 수행 */
printf("사각형을 그린다.\n");
printf("x=%u y=%u w=%u h=%u\n",
rect->x, rect->y, rect->width, rect->height);
}
int main() {
struct Rect rect = { 0, 0, 30, 40 };
rect_move(&rect, 1, 2);
rect_draw(&rect);
}
// 출력
사각형을 움직인다.
사각형을 그린다.
x=1 y=2 w=30 h=40
struct Rect가 상태와 메서드를 함께 소유하고 있지는 않지만, Rect 구조체를 다루는 함수들이 같은 파일에 정의되어 있다. 이 함수들은 첫 번 째 인자로 Rect 구조체를 받아서, Cpp의 this 키워드와 동일하게 사용하고 있다. 따라서 동일 파일 안에 구조체를 다루는 함수를 정의하는 방법은, class에서 메서드를 사용하는 것과 기능적으로 동일하게 작동한다. 여기서 더 나아가서 함수포인터를 사용하면 생김새도 Cpp의 메서드와 유사하게 만들 수 있다. 아래는 함수포인터를 사용하여 rect.c 파일을 다시 작성해 보았다.
/* rect.h */
struct Rect {
void (*move)(struct Rect *rect, unsigned int x, unsigned int y);
void (*draw)(struct Rect *rect);
unsigned int x;
unsigned int y;
unsigned int width;
unsigned int height;
};
struct Rect* rect_init(void);
void rect_move(struct Rect * rect, unsigned int x, unsigned int y);
void rect_draw(struct Rect *rect);
/* rect.c */
#include <stdio.h>
#include <stdlib.h>
#include "rect.h"
/* XXX 편의상 메모리 검사하는 코드는 생략 */
struct Rect* rect_init(void) {
struct Rect *rect = malloc(sizeof(struct Rect));
rect->move = rect_move;
rect->draw = rect_draw;
rect->x = 0;
rect->y = 0;
rect->width = 30;
rect->height = 40;
return rect;
}
void rect_move(struct Rect *rect, unsigned int x, unsigned int y) {
/* 사각형 이동 */
rect->x = x;
rect->y = y;
printf("사각형을 움직인다.\n");
}
void rect_draw(struct Rect *rect) {
/* 사각형 그리는 작업 수행 */
printf("사각형을 그린다.\n");
printf("x=%u y=%u w=%u h=%u\n",
rect->x, rect->y, rect->width, rect->height);
}
int main() {
struct Rect *rect = rect_init();
rect->move(rect, 1, 2);
rect->draw(rect);
free(rect);
}
// 출력
사각형을 움직인다.
사각형을 그린다.
x=1 y=2 w=30 h=40
다형성
위키백과를 보면 다형성은 다음과 같이 정의되어 있다. "프로그램 언어의 다형성(多形性, polymorphism; 폴리모피즘)은 그 프로그래밍 언어의 자료형 체계의 성질을 나타내는 것으로, 프로그램 언어의 각 요소들(상수, 변수, 식, 오브젝트, 함수, 메소드 등)이 다양한 자료형(type)에 속하는 것이 허가되는 성질을 가리킨다.". 주로 객체지향 언어의 오버로딩, 오버라이딩을 사용한다. C언어에서는 typecasting을 통해 오버라이딩을 사용하는 다형성을 구현할 수 있다.
/* shape.h */
struct Shape {
void (*move)(struct Shape *rect, unsigned int x, unsigned int y);
void (*draw)(struct Shape *rect);
};
struct Rect {
void (*move)(struct Rect *rect, unsigned int x, unsigned int y);
void (*draw)(struct Rect *rect);
unsigned int x;
unsigned int y;
unsigned int width;
unsigned int height;
};
struct Rect* rect_init(void);
void rect_move(struct Rect * rect, unsigned int x, unsigned int y);
void rect_draw(struct Rect *rect);
struct Circle {
void (*move)(struct Circle *circle, unsigned int x, unsigned int y);
void (*draw)(struct Circle *circle);
unsigned int x;
unsigned int y;
unsigned int radius;
};
struct Circle* circle_init(void);
void circle_move(struct Circle *circle, unsigned int x, unsigned int y);
void circle_draw(struct Circle *circle);
/* shape.c */
#include <stdio.h>
#include <stdlib.h>
#include "shape.h"
/* XXX 편의상 메모리 검사하는 코드는 생략 */
struct Rect* rect_init(void) {
struct Rect *rect = malloc(sizeof(struct Rect));
/* TODO 메모리가 올바르게 할당되었는지 검사 */
rect->move = rect_move;
rect->draw = rect_draw;
rect->x = 0;
rect->y = 0;
rect->width = 30;
rect->height = 40;
return rect;
}
void rect_move(struct Rect *rect, unsigned int x, unsigned int y) {
/* 사각형 이동 */
rect->x = x;
rect->y = y;
printf("사각형을 움직인다.\n");
}
void rect_draw(struct Rect *rect) {
/* 사각형 그리는 작업 수행 */
printf("사각형을 그린다.\n");
printf("x=%u y=%u w=%u h=%u\n",
rect->x, rect->y, rect->width, rect->height);
}
struct Circle* circle_init(void) {
struct Circle *circle = malloc(sizeof(struct Circle));
circle->move = circle_move;
circle->draw = circle_draw;
circle->x = 3;
circle->y = 4;
circle->radius = 7;
return circle;
}
void circle_move(struct Circle *circle, unsigned int x, unsigned int y) {
/* 원 이동 */
circle->x = x;
circle->y = y;
printf("원을 움직인다.\n");
}
void circle_draw(struct Circle *circle) {
/* 원을 그리는 작업 수행 */
printf("원을 그린다.\n");
printf("x=%u y=%u radius=%u\n",
circle->x, circle->y, circle->radius);
}
int main() {
struct Rect *rect = rect_init();
struct Circle *circle = circle_init();
struct Shape* shapes[2] = {
(struct Shape*)rect,
(struct Shape*)circle,
};
for (int i = 0; i < 2; i++) {
shapes[i]->move(shapes[i], i * 100, i * 200);
shapes[i]->draw(shapes[i]);
}
for (int i = 0; i < 2; i++) {
free(shapes[i]);
}
}
// 출력
사각형을 움직인다.
사각형을 그린다.
x=0 y=0 w=30 h=40
원을 움직인다.
원을 그린다.
x=100 y=200 radius=7
Rect와 Circle이라는 서로 다른 객체가 Shape로 형변환이 되어서 사용되고 있다. C에서는 어떤 방식으로 형변환을 하여도 타입체크를 하지 않기 떄문에, 이처럼 완전히 다른 구조체로 형변환을 하여 다형성을 확보할 수 있다. 이 때 주의할 점은 Shape와 Rect, Circle 모두 함수포인터의 위치가 동일해야 한다는 것이다. 만약 Rect와 Shape간 함수포인터 순서가 바뀐다면 다음과 같이 Segfault가 발생하게 된다.
/* shape.h */
struct Shape {
void (*move)(struct Shape *rect, unsigned int x, unsigned int y);
void (*draw)(struct Shape *rect);
};
struct Rect {
unsigned int x;
unsigned int y;
unsigned int width;
unsigned int height;
/* 위치가 변경되었다. */
void (*move)(struct Rect *rect, unsigned int x, unsigned int y);
void (*draw)(struct Rect *rect);
};
struct Rect* rect_init(void);
void rect_move(struct Rect * rect, unsigned int x, unsigned int y);
void rect_draw(struct Rect *rect);
struct Circle {
void (*move)(struct Circle *circle, unsigned int x, unsigned int y);
void (*draw)(struct Circle *circle);
unsigned int x;
unsigned int y;
unsigned int radius;
};
struct Circle* circle_init(void);
void circle_move(struct Circle *circle, unsigned int x, unsigned int y);
void circle_draw(struct Circle *circle);
// 출력
12345 segmentation fault (core dumped)
다형성의 구성 요소 중 하나인 오버라이딩은 확인하였는데, 오버로딩은 어떨까? 일반적으로 C에서는 중복된 이름의 함수 선언을 허용하지 않기 때문에 오버로딩은 구현하기 쉽지 않다. 굳이 오버로딩을 사용해야 한다면, 가변인자(VA_ARGS)를 이용하면 오버로딩을 구현할 수 있다. 하지만 이 방법은 Gobject에서 사용하는 방법은 아니기 때문에, 이런 방법이 있다 정도만 알고 넘어가려 한다.
상속
상속은 위키백과에 다음과 같이 정의되어 있다. "상속(inheritance)은 객체들 간의 관계를 구축하는 방법이다. 클래스로 객체가 정의되는 고전 상속에서, 클래스는 기반 클래스, 수퍼클래스, 또는 부모 클래스 등의 기존의 클래스로부터 속성과 동작을 상속받을 수 있다. 그 결과로 생기는 클래스를 파생 클래스, 서브클래스, 또는 자식 클래스라고 한다." 간단하게 정리해서 여기서 다루는 상속은 부모 클래스의 속성과 동작를 자식클래스에서 재활용/오버라이딩할 수 있도록 하는 것을 의미한다. C언어에서는 구조체 내부에 부모 객체를 소유하여 상속을 구현할 수 있다.
/* shape.h */
struct Shape {
void (*move)(struct Shape *shape, unsigned int x, unsigned int y);
void (*draw)(struct Shape *shape);
char *type;
};
struct Rect {
/* 부모 구조체는 반드시 첫 번째 변수로 선언 */
struct Shape parent;
unsigned int x;
unsigned int y;
unsigned int width;
unsigned int height;
};
struct Rect* rect_init(void);
void rect_move(struct Shape *shape, unsigned int x, unsigned int y);
void rect_draw(struct Shape *shape);
/* shape.c */
#include <stdio.h>
#include <stdlib.h>
#include "shape.h"
/* XXX 편의상 메모리 검사하는 코드는 생략 */
struct Rect* rect_init(void) {
struct Rect *rect = malloc(sizeof(struct Rect));
/* rect와 rect->parent의 주소가 동일하기 때문에
* 이와 같은 형변환이 가능하다. */
struct Shape *shape = (struct Shape*)rect;
shape->move = rect_move;
shape->draw = rect_draw;
shape->type = "RECT";
rect->x = 0;
rect->y = 0;
rect->width = 30;
rect->height = 40;
return rect;
}
void rect_move(struct Shape *shape, unsigned int x, unsigned int y) {
/* 위와 마찬가지로 shape(rect->parent)와 rect의 주소가 동일하기 때문에
* 이와 같은 형변환이 가능하다. */
struct Rect *rect = (struct Rect*)shape;
/* 사각형 이동 */
rect->x = x;
rect->y = y;
printf("사각형을 움직인다.\n");
}
void rect_draw(struct Shape *shape) {
struct Rect *rect = (struct Rect*)shape;
/* 사각형 그리는 작업 수행 */
printf("사각형을 그린다.\n");
printf("x=%u y=%u w=%u h=%u\n",
rect->x, rect->y, rect->width, rect->height);
}
int main() {
struct Rect *rect = rect_init();
((struct Shape*)rect)->move((struct Shape*)rect, 1, 2);
((struct Shape*)rect)->draw((struct Shape*)rect);
free(rect);
}
// 출력
사각형을 움직인다.
사각형을 그린다.
x=1 y=2 w=30 h=40
부모 구조체를 구조체의 첫 번째 변수로 선언하면, 아주 간단한 형 변환을 통해 사용할 수 있다. 이는 Rect 구조체의 인스턴스 (위의 예에서는 rect) 주소와 해당 인스턴스의 parent의 주소가 일치하기 때문이다. 위의 그림을 보면 좀 더 이해가 쉽다. rect를 shape로 형변환하더라도 구조체의 시작 주소는 0x123480으로 동일하다. 또한 rect와 shape의 멤버들은 주소가 겹치지 않기 때문에 사용하는데 있어 메모리 주소 충돌 등의 문제가 생길 일도 없다.
물론 굳이 부모 구조체를 구조체의 첫 번째 변수로 선언하지 않더라도 rect->parent.draw(rect->parent)처럼 접근하는 것도 가능하다. 하지만 부모 구조체를 첫 번 째 변수로 선언하지 않으면, 부모 구조체에서 다시 자식 구조체로 형을 변환하기 어렵다. 위에서 보이듯이 부모 구조체를 인자로 받는 함수(rect_draw, rect_move)가 있을 때, 자식 구조체의 인자들을 참조하기 어렵다. (번거롭기는 하지만 방법이 있기는 하다. container_of 참조) 반면 부모 구조체를 struct 첫 번째 변수로 선언하여 형 변환하면, 위의 코드와 같이 자식 구조체 -> 부모 구조체, 부모 구조체 -> 자식 구조체로 자유롭게 변환이 가능하다는 장점이 있다.
캡슐화
C는 .c 파일과 .h 파일을 가지고 있다. 외부 .c 파일에서는 헤더파일을 인클루드하여 .h파일에 공개되어 있는 인터페이스를 사용할 수 있지만, 헤더파일에 공개되지 않은 .c 내부의 정보는 접근할 수 없다. (extern 키워드를 사용하면, .h 없이도 .c 내부의 정보에 접근할 수 있기는 하다. 하지만 일반적인 사용법은 아니다.) 이 점을 이용해서 공개해야 하는 정보는 .h에 기술하고, 공개를 원치 않는 정보는 .c에 기술하여 C에서도 캡슐화를 구현할 수 있다.
/* rect.h */
/* C에서는 헤더에서 구조체 타입을 선언만 하고, 실제 정의는 .c 파일에서 할 수 있다. */
struct RectPrivate;
struct Rect {
void (*move)(struct Rect *rect, unsigned int x, unsigned int y);
void (*draw)(struct Rect *rect);
/* 현재는 RectPrivate의 크기를 알 수 없기 때문에 구조체 포인터를 사용한다. */
struct RectPrivate *private;
};
struct Rect* rect_init(void);
void rect_move(struct Rect *rect, unsigned int x, unsigned int y);
void rect_draw(struct Rect *rect);
/* rect.c */
#include <stdio.h>
#include <stdlib.h>
#include "rect.h"
/* 실제 RectPrivate의 정의는 .c 파일에서 이뤄진다. */
struct RectPrivate {
unsigned int x;
unsigned int y;
unsigned int width;
unsigned int height;
};
/* XXX 편의상 메모리 검사하는 코드는 생략 */
struct Rect* rect_init(void) {
struct Rect *rect = malloc(sizeof(struct Rect));
rect->move = rect_move;
rect->draw = rect_draw;
rect->private = malloc(sizeof(struct RectPrivate));
/* 현재 파일에 안에서만 아래 값들에 접근할 수 있다.
* 현재 파일 밖에서는 RectPrivate의 구조를 알 수 없기 때문에,
* 아래 값들에 접근할 수 없다. */
rect->private->x = 0;
rect->private->y = 0;
rect->private->width = 30;
rect->private->height = 40;
return rect;
}
void rect_move(struct Rect *rect, unsigned int x, unsigned int y) {
/* 사각형 이동 */
rect->private->x = x;
rect->private->y = y;
printf("사각형을 움직인다.\n");
}
void rect_draw(struct Rect *rect) {
/* 사각형 그리는 작업 수행 */
printf("사각형을 그린다.\n");
printf("x=%u y=%u w=%u h=%u\n",
rect->private->x, rect->private->y,
rect->private->width, rect->private->height);
}
int main() {
struct Rect *rect = rect_init();
rect->move(rect, 1, 2);
rect->draw(rect);
free(rect);
}
헤더파일에는 Rect 구조체의 메서드만 공개하고, 상태는 .c 파일의 RectPrivate에 저장하면 다른 파일에서 RectPrivate의 값에 직접 접근할 수 없게 된다. 이런 방식으로 데이터를 외부에서 직접 접근 하지 못하도록 은닉할 수 있다.
'프로그래밍 언어 > C++' 카테고리의 다른 글
C++ if/switch statement with initializer (0) | 2022.12.11 |
---|---|
C++ 모듈 (Module) (0) | 2022.11.30 |
C++ call by value, call by reference (0) | 2022.11.17 |
C++ (template, auto, decltype) 타입 추론 Universal reference (0) | 2022.11.14 |
C++ 공용체(union) 개념과 통신에서의 사용 이유 (0) | 2022.11.10 |