[전문가를 위한 C] Chapter 8. 상속과 다형성

YUSHIN KIM·2026년 9월 4일

전문가를 위한 C

목록 보기
8/14

Chapter 8. 상속과 다형성

8.1 상속

상속 관계는 to-be 또는 is-a 관계이다. 기존의 객체나 클래스에 속성과 행위를 추가한다는 의미에서 확장 관계(extension relationship)이라고도 한다.

코드 박스 8-1 Person 클래스와 Student 클래스 속성 구조체

typedef struct {
	char first_name[32];
    char last_name[32];
    unsigned int birth_year;
} person_t;

typedef struct {
	char first_name[32];
    char last_name[32];
    unsigned int birth_year;
    char student_number[16];		// additional attribute
    unsigned int passed_credits;	// additional attribute
} student_t;

위 코드에서 student_t는 person_t의 속성 집합을 확장하고 있다. person_t는 일반적으로 슈퍼타입(supertype) 또는 베이스 타입(base type), 또는 부모 타입(parent type)이라고 한다. student_t는 자식 타입(child type) 또는 상속받은 서브타입(inherited subtype)이라고 한다.


8.1.1 상속의 본질

상속 관계는 본질적으로 서브타입 객체가 슈퍼타입 객체를 비공개 속성으로 갖고 있는 일대일 합성 관계이다.

코드 박스 8-2 Person과 Student 클래스 속성 구조체(중첩됨)

typedef struct {
	char first_name[32];
    char last_name[32];
    unsigned int birth_year;
} person_t;

typedef struct {
	person_t person;
    char student_number[16];		// additional attribute
    unsigned int passed_credits;	// additional attribute
} student_t;

구조체 중첩을 사용하면 student_t 포인터를 person_t로 변환하는 업캐스팅(upcasting)을 쉽게 수행할 수 있다.

코드 박스 8-3 [예제 8-1] Student와 Person의 객체 포인터 사이의 업캐스팅

#include <stdio.h>

typedef struct {
    char first_name[32];
    char last_name[32];
    unsigned int birth_year;
} person_t;

typedef struct {
    person_t person;
    char student_number[16];        // additional attribute
    unsigned int passed_credits;    // additional attribute
} student_t;

int main(int argc, char** argv) {
    student_t s;
    student_t* s_ptr = &s;
    person_t* p_ptr = (person_t*) &s;
    printf("Student pointer points to %p\n", (void*) s_ptr);
    printf("Person pointer points to %p\n", (void*) p_ptr);
    return 0;
}

셀 박스 8-1 [예제 8-1]의 출력 결과

$ gcc 8_1.c -o 8_1.out
$ ./8_1.out
Student pointer points to 0x7ffeb12e9bc0
Person pointer points to 0x7ffeb12e9bc0

s_ptr, p_ptr은 같은 주소를 가리킨다. 즉, student_t 형 구조체 변수의 메모리 레이아웃에서 person_t 구조체를 상속하고 있다. 그러므로 student 객체를 가리키는 포인터로 Person 클래스의 행위를 재사용할 수 있다.

코드 박스 8-4 컴파일되지 않는 상속 관계 만들기

struct person_t;

typedef struct {
	struct person_t person;
    char student_number[16];
    unsigned int passed_credits;
} student_t;

불완전한 형식(incomplete type)으로 변수를 생성할 수 없다. 그러므로 상속 구현을 위해 서브타입 구조체는 슈퍼타입 정의를 알아야 한다. 하지만 슈퍼타입 구조체의 정의는 비공개여야 하기 때문에 다음 두 가지 접근법이 사용된다.

  • 자식 클래스가 베이스 클래스에 대한 비공개 구현(정의)에 접근할 수 있도록 하기
  • 자식 클래스가 베이스 클래스의 공용 인터페이스에만 접근할 수 있도록 하기

C의 상속에 관한 첫 번째 접근법

첫 번째는 자식의 속성 구조체 내부에 부모의 구조체 변수를 두는 방법이다.

코드 박스 8-5 [예제 8-2] Person 클래스의 공용 인터페이스

#ifndef EXTREME_C_EXAMPLES_CHAPTER_8_2_PERSON_H
#define EXTREME_C_EXAMPLES_CHAPTER_8_2_PERSON_H

// forward declaration
struct person_t;

// memory allocator
struct person_t* person_new();

// constructor
void person_constructor(struct person_t*,
                        const char*     /* first name */,
                        const char*     /* last name*/,
                        unsigned int    /* birth year */);


// destructor
void person_destructor(struct person_t*);

// behavior function
void person_get_first_name(struct person_t*, char*);
void person_get_last_name(struct person_t*, char*);
unsigned int person_get_birth_year(struct person_t*);

#endif

속성 구조체 person_t는 불완전하기 때문에 Student 클래스는 이 헤더 파일로 상속 관계를 구현할 수 없다. 하지만 이 헤더 파일에 구조체 정의를 포함하면 목적을 넘어서는 행위이기 때문에, 비공개 헤더 파일을 사용해야 한다.

코드 박스 8-6 [예제 8-2] person_t의 실제 정의를 포함하는 비공개 헤더 파일

#ifndef EXTREME_C_EXAMPLES_CHAPTER_8_2_PERSON_P_H
#define EXTREME_C_EXAMPLES_CHAPTER_8_2_PERSON_P_H

// private definition
typedef struct {
    char first_name[32];
    char last_name[32];
    unsigned int birth_year;
} person_t;

#endif

이 헤더 파일은 Person 클래스의 일부이므로 비공개 상태여야 하지만 제한적으로 Student 클래스에는 공개되어야 한다.

코드 박스 8-7 [예제 8-2] Person 클래스에 대한 정의

#include <stdlib.h>
#include <string.h>

// this header file defines person_t structure
#include "8_2_person_p.h"

// memory allocator
person_t* person_new() {
    return (person_t*) malloc(sizeof(person_t));
}

// constructor
void person_constructor(person_t* person,
                        const char* first_name,
                        const char* last_name,
                        unsigned int birth_year) {
    strcpy(person->first_name, first_name);
    strcpy(person->last_name, last_name);
    person->birth_year = birth_year;
}

// destructor
void person_destructor(person_t* person) {
    // no need to do anything
}

// behavior function
void person_get_first_name(person_t* person, char* buffer) {
    strcpy(buffer, person->first_name);
}

void person_get_last_name(person_t* person, char* buffer) {
    strcpy(buffer, person->last_name);
}

unsigned int person_get_birth_year(person_t* person) {
    return person->birth_year;
}

코드 박스 8-8 [예제 8-2] Student 클래스의 공용 인터페이스

#ifndef EXTREME_C_EXAMPLES_CHAPTER_8_2_STUDENT_H
#define EXTREME_C_EXAMPLES_CHAPTER_8_2_STUDENT_H

// forward declaration
struct student_t;

// memory allocator
struct student_t* student_new();

// constructor
void student_constructor(struct student_t*,
                        const char*     /* first name */,
                        const char*     /* last name */,
                        unsigned int    /* birth year */,
                        const char*     /* student number */,
                        unsigned int    /* passed credits */);

// destructor
void student_destructor(struct student_t*);

// behavior function
void student_get_student_number(struct student_t*, char*);
unsigned int student_get_passed_credits(struct student_t*);

#endif

student 생성자는 student 내 person 속성을 설정할 책임이 있다. student 객체에서도 Person 클래스의 행위 함수를 사용할 수 있기 때문에 Student 클래스에는 2개의 행위 함수만 정의되어 있다.

코드 박스 8-9 [예제 8-2] Student 클래스의 비공개 정의

#include <stdlib.h>
#include <string.h>

#include "8_2_person.h"

// this header file defines person_t structure
#include "8_2_person_p.h"

// forward declaration
typedef struct {
    // at this point all attributes of the person class are inherited
    person_t person;
    char* student_number;
    unsigned int passed_credits;
} student_t;

// memory allocator
student_t* student_new() {
    return (student_t*) malloc(sizeof(student_t));
}

// constructor
void student_constructor(student_t* student,
                        const char* first_name,
                        const char* last_name,
                        unsigned int birth_year,
                        const char* student_number,
                        unsigned int passed_credits) {
    // call the constructor of the parent class
    person_constructor((struct person_t*) student, first_name, last_name, birth_year);
    student->student_number = (char*) malloc(16 * sizeof(char));
    strcpy(student->student_number, student_number);
    student->passed_credits = passed_credits;
}

// destructor
void student_destructor(student_t* student) {
    // needed to destruct the child object
    free(student->student_number);
    person_destructor((struct person_t*) student);
}

// behavior function
void student_get_student_number(student_t* student, char* buffer) {
    strcpy(buffer, student->student_number);
}

unsigned int student_get_passed_credits(student_t* student) {
    return student->passed_credits;
}

student_t 구조체 정의 시 반드시 person_t 구조체 변수를 가장 먼저 선언해야 한다. 그래야만 업캐스팅 후 객체 사용 시 같은 시작 주소로부터 person_t 객체의 범위만큼 접근할 수 있다.

코드 박스 8-10 [예제 8-2]의 메인 시나리오

#include <stdio.h>
#include <stdlib.h>

#include "8_2_person.h"
#include "8_2_student.h"

int main(int argc, char** argv) {
    // create and construct a student object
    struct student_t* student = student_new();
    student_constructor(student, "John", "Doe", 1987, "TA5667", 134);

    // call person's behavior function to read its attributes from the student object
    char buffer[32];

    // upcast to the parent type's pointer
    struct person_t* person_ptr = (struct person_t*) student;

    person_get_first_name(person_ptr, buffer);
    printf("First name: %s\n", buffer);

    person_get_last_name(person_ptr, buffer);
    printf("Last name: %s\n", buffer);

    printf("Birth year: %d\n", person_get_birth_year(person_ptr));

    // read the attributes limited to the student object
    student_get_student_number(student, buffer);
    printf("Student number: %s\n", buffer);

    printf("Passed credits: %d\n", student_get_passed_credits(student));

    // destruct and free the student object
    student_destructor(student);
    free(student);

    return 0;
}

Person, Student 클래스 모두에 대한 공용 인터페이스를 포함했지만, student 객체 하나만 생성하여 해당 객체를 통해 person 객체의 행위에 접근한다.

셀 박스 8-2 [예제 8-2]를 빌드하고 실행하기

$ gcc -c 8_2_person.c -o person.o
$ gcc -c 8_2_student.c -o student.o
$ gcc -c 8_2_main.c -o main.o
$ gcc person.o student.o main.o -o 8_2.out
$ ./8_2.out
First name: John
Last name: Doe
Birth year: 1987
Student number: TA5667
Passed credits: 134

C의 상속에 관한 두 번째 접근법

두 번째는 자식의 속성 구조체 내부에 부모의 구조체 변수에 대한 포인터를 두는 방법이다.

이 방식에서 Student 클래스는 Person 클래스의 공용 인터페이스에만 의존할 뿐, 비공개 정의에는 의존하지 않는다. 클래스가 분리(decouple)되기 때문에 자식 클래스의 구현을 변경하지 않고 부모 클래스의 구현을 유연하게 변경할 수 있다.

코드 박스 8-11 [예제 8-3] Student 클래스의 새로운 공용 인터페이스

#ifndef EXTREME_C_EXAMPLES_CHAPTER_8_3_STUDENT_H
#define EXTREME_C_EXAMPLES_CHAPTER_8_3_STUDENT_H

// forward declaration
struct student_t;

// memory allocator
struct student_t* student_new();

// constructor
void student_constructor(struct student_t*,
                        const char*     /* first name */,
                        const char*     /* last name */,
                        unsigned int    /* birth year */,
                        const char*     /* student number */,
                        unsigned int    /* passed credits */);

// destructor
void student_destructor(struct student_t*);

// behavior function
void student_get_first_name(struct student_t*, char*);
void student_get_last_name(struct student_t*, char*);
unsigned int student_get_birth_year(struct student_t*);
void student_get_student_number(struct student_t*, char*);
unsigned int student_get_passed_credits(struct student_t*);

#endif

student_t 포인터를 person_t 포인터로 업캐스팅할 수 없기 때문에 Student 클래스는 Person 클래스에서 선언한 모든 행위 함수를 반복하도록 행위 함수를 수정해야 한다.

코드 박스 8-12 [예제 8-3] Person 클래스의 새로운 구현

#include <stdlib.h>
#include <string.h>

// private definition
typedef struct {
    char first_name[32];
    char last_name[32];
    unsigned int birth_year;
} person_t;

// memory allocator
person_t* person_new() {
    return (person_t*) malloc(sizeof(person_t));
}

// constructor
void person_constructor(person_t* person,
                        const char* first_name,
                        const char* last_name,
                        unsigned int birth_year) {
    strcpy(person->first_name, first_name);
    strcpy(person->last_name, last_name);
    person->birth_year = birth_year;
}

// destructor
void person_destructor(person_t* person) {
    // no need to do anything
}

// behavior function
void person_get_first_name(person_t* person, char* buffer) {
    strcpy(buffer, person->first_name);
}

void person_get_last_name(person_t* person, char* buffer) {
    strcpy(buffer, person->last_name);
}

unsigned int person_get_birth_year(person_t* person) {
    return person->birth_year;
}

person_t의 비공개 정의가 소스 파일 내부에 있으므로 더 이상 비공개 헤더 파일은 사용되지 않는다. 이는 다른 클래스로 정의를 공유하지 않고 은닉하겠다는 의미이다.

코드 박스 8-13 [예제 8-3] Student 클래스의 새로운 구현

#include <stdlib.h>
#include <string.h>

// public interface for person class
#include "8_3_person.h"

// forward declaration
typedef struct {
    char* student_number;
    unsigned int passed_credits;
    // at this line, a pointer is needed as person_t is the incomplete type
    struct person_t* person;
} student_t;

// memory allocator
student_t* student_new() {
    return (student_t*) malloc(sizeof(student_t));
}

// constructor
void student_constructor(student_t* student,
                        const char* first_name,
                        const char* last_name,
                        unsigned int birth_year,
                        const char* student_number,
                        unsigned int passed_credits) {
    // allocate memory for the parent object
    student->person = person_new();
    person_constructor(student->person, first_name, last_name, birth_year);
    student->student_number = (char*) malloc(16 * sizeof(char));
    strcpy(student->student_number, student_number);
    student->passed_credits = passed_credits;
}

// destructor
void student_destructor(student_t* student) {
    // at first, need to destruct the child object
    free(student->student_number);
    // and then, call the destructor of the parent class
    person_destructor(student->person);
    // finally, release the memory allocated for the parent object
    free(student->person);
}

// behavior function
void student_get_first_name(student_t* student, char* buffer) {
    // need to call person's behavior function
    person_get_first_name(student->person, buffer);
}

void student_get_last_name(student_t* student, char* buffer) {
    // need to call person's behavior function
    person_get_last_name(student->person, buffer);
}

unsigned int student_get_birth_year(student_t* student) {
    // need to call person's behavior function
    return person_get_birth_year(student->person);
}

void student_get_student_number(student_t* student, char* buffer) {
    strcpy(buffer, student->student_number);
}

unsigned int student_get_passed_credits(student_t* student) {
    return student->passed_credits;
}

Student 클래스의 생성자에서는 부모 클래스의 생성자를 호출하고, 소멸자에서는 부모 클래스의 소멸자를 호출한다. 즉, 합성 관계와 같이 컨테이너 객체가 포함된 객체의 생명주기를 제어한다.

Student 클래스는 상속받은 속성 및 비공개 속성을 노출하기 위해 자신의 행위 함수들을 노출해야 한다. 이때 부모인 person 객체의 비공개 속성을 노출하기 위해 래퍼(wrapper) 함수를 노출한다.

코드 박스 8-14 [예제 8-3]의 메인 시나리오

#include <stdio.h>
#include <stdlib.h>

#include "8_3_student.h"

int main(int argc, char** argv) {
    // create and construct a student object
    struct student_t* student = student_new();
    student_constructor(student, "John", "Doe", 1987, "TA5667", 134);

    // need to call Student's behavior function as the student's pointer cannot be casted to the person's pointer
    char buffer[32];
    student_get_first_name(student, buffer);
    printf("First name: %s\n", buffer);

    student_get_last_name(student, buffer);
    printf("Last name: %s\n", buffer);

    printf("Birth year: %d\n", student_get_birth_year(student));

    student_get_student_number(student, buffer);
    printf("Student number: %s\n", buffer);

    printf("Passed credits: %d\n", student_get_passed_credits(student));

    // destruct and release the student object
    student_destructor(student);
    free(student);

    return 0;
}

student_t, person_t 포인터를 상호 변환할 수 없기 때문에 main 함수에서 더이상 Person의 공용 인터페이스를 사용할 수 없다. 그러므로 이를 포함하지 않는다.

셀 박스 8-3 [예제 8-3]을 빌드하고 실행하기

$ gcc -c 8_3_person.c -o person.o
$ gcc -c 8_3_student.c -o student.o
$ gcc -c 8_3_main.c -o main.o
$ gcc person.o student.o main.o -o 8_3.out
$ ./8_3.out
First name: John
Last name: Doe
Birth year: 1987
Student number: TA5667
Passed credits: 134

두 가지 접근법 비교하기

  • 두 접근법은 본질적으로 합성 관계이다.
  • 첫 번째 접근법은 자식의 속성 구조체 안에 구조체 변수를 두므로 부모 클래스의 비공개 구현에 의존한다. 하지만 두 번째 접근법은 부모의 속성 구조체(불완전 자료형)에 대한 포인터를 두므로 부모 클래스의 비공개 구현에 의존하지 않는다.
  • 첫 번째 접근법에서 부모와 자식의 자료형은 매우 의존적이다. 하지만 두 번째 접근법에서 클래스들은 독립적이며 부모의 구현은 자식으로부터 은닉되어 있다.
  • 첫 번째 접근법에서는 단일 상속(single inheritance)만 가능하다. 하지만 두 번째 접근법에서는 다중 상속(multiple inheritance)이 가능하다.
  • 첫 번째 접근법에서 부모의 구조체 변수는 자식 클래스 속성 구조체의 첫 번째 필드여야 한다. 하지만 두 번째 접근법에서 부모 객체 포인터의 위치는 제한이 없다.
  • 첫 번째 접근법에서는 부모 클래스의 행위 함수를 직접 사용할 수 있다. 하지만 두 번째 접근법에서는 자식 클래스가 래퍼 함수를 통해 부모의 행위 함수를 제공해야 한다.

8.2 다형성

8.2.1 다형성 소개

다형성은 같은 공용 인터페이스(또는 행위 함수의 집합)를 통해 상이한 행위를 갖는다는 의미이다.

코드 박스 8-15 Animla, Cat, Duck 자료형에 대한 객체 세 개를 생성하기

struct animal_t* animal = animal_malloc();
animal_constructor(animal);

struct cat_t* cat = cat_malloc();
cat_constructor(cat);

struct duck_t* duck = duck_malloc();
duck_constructor(duck);

다형성이 없다면 세 클래스가 공유하는 sound라는 함수는 다음과 같이 호출되었을 것이다.

코드 박스 8-16 생성된 객체에서 행위 함수를 호출하기

animal_sound(animal);
cat_sound(cat);
duck_sound(duck);

셀 박스 8-4 함수 호출에 대한 출력 결과

Animal: Beeep
Cat: Meow
Duck: Quack

다음은 다형성이 있을 때의 코드 호출 방식이다.

코드 박스 8-17 세 개의 객체 모두에서 같은 행위 함수 sound를 호출하기

animal_sound(animal);
animal_sound((struct animal_t*) cat);
animal_sound((struct animal_t*) duck);

셀 박스 8-5 함수 호출에 대한 출력 결과

Animal: Beeep
Cat: Meow
Duck: Quack

다형성 메커니즘을 활용하려면 C에서 상속을 구현하는 첫 번째 접근법을 취해야 한다.

코드 박스 8-18 Animal, Cat, Duck 클래스의 속성 구조체 정의

typedef struct {
	...
} animal_t;

typedef struct {
	animal_t animal;
    ...
} cat_t;

typedef struct {
	animal_t animal;
    ...
} duck_t;

duck_t, cat_t 포인터는 각각 animal_t 포인터로 변환할 수 있다. 이후 두 자식 클래스에 대한 같은 행위 함수를 사용할 수 있다.

코드 박스 8-19 아직 다형적이지 않은 animal_sound

void animal_sound(animal_t* ptr) {
	printf("Animal: Beeeep");
}

animal_sound(animal);
animal_sound((struct animal_t*) cat);
animal_sound((struct animal_t*) duck);

셀 박스 8-6 [코드 박스 8-19]의 함수 호출에 대한 출력 결과

Animal: Beeep
Animal: Beeep
Animal: Beeep

위와 같은 구현은 다형성을 활용한다고 할 수 없다.


8.2.2 다형성이 필요한 이유

다형성이 필요한 이유는 어떤 슈퍼타입에 관한 여러 서브타입을 둘 때 코드는 그대로 두기를 원하기 때문이다. 다형성은 변경이 필요할 때 그 범위를 좁히는 역할을 한다.

추상화(abstraction) 또한 다형성이 필요한 이유 중 하나이다. 추상적인 클래스는 자식 클래스에서 오버라이딩 되어야 하는 추상적인 행위를 갖는다. 다형성은 이를 구현하기 위한 핵심 방법이다.


8.2.3 C에서 다형적 행위를 갖는 방법

다형적 행위를 구현하기 위해 함수 포인터(function pointer)를 속성 구조체의 필드로 둘 수 있다.

코드 박스 8-20 [예제 8-4] Animal 클래스의 공용 인터페이스

#ifndef EXTREME_C_EXAMPLES_CHAPTER_8_4_ANIMAL_H
#define EXTREME_C_EXAMPLES_CHAPTER_8_4_ANIMAL_H

// forward declaration
struct animal_t;

// memory allocator
struct animal_t* animal_new();

// constructor
void animal_constructor(struct animal_t*);

// destructor
void animal_destructor(struct animal_t*);

// behavior function
void animal_get_name(struct animal_t*, char*);
void animal_sound(struct animal_t*);

#endif

Animal 클래스는 두 개의 행위 함수를 갖는다. animal_sound 함수만 다형성이 필요하고 자식 클래스에서 오버라이딩될 수 있다. animal_get_name은 그렇지 않다.

코드 박스 8-21 [예제 8-4] Animal 클래스의 비공개 헤더

#ifndef EXTREME_C_EXAMPLES_CHAPTER_8_4_ANIMAL_P_H
#define EXTREME_C_EXAMPLES_CHAPTER_8_4_ANIMAL_P_H

// a function pointer type needed to point the other morphs of animal_sound function
typedef void (*sound_func_t)(void*);

// forward declaration
typedef struct {
    char* name;
    // this member is a pointer to the function which behaves sound function
    sound_func_t sound_func;
} animal_t;

#endif

모든 자식 클래스는 animal_sound 함수에 대한 자신만의 버전을 제공할 수 있다. 그러므로 animal_t의 각 인스턴스는 animal_sound 행위 전용 함수 포인터를 갖고, 그 포인터나 클래스 내 다형적 함수에 대한 실제 정의를 가리킨다.

코드 박스 8-22 [예제 8-4] Animal 클래스에 대한 정의

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#include "8_4_animal_p.h"

// the basic definition of animal_sound at the level of the parent
void __animal_sound(void* this_ptr) {
    animal_t* animal = (animal_t*) this_ptr;
    printf("%s: Beeeep\n", animal->name);
}

// memory allocator
animal_t* animal_new() {
    return (animal_t*) malloc(sizeof(animal_t));
}

// constructor
void animal_constructor(animal_t* animal) {
    animal->name = (char*) malloc(10 * sizeof(char));
    strcpy(animal->name, "Animal");
    // set the function pointer to point to the basic definition
    animal->sound_func = __animal_sound;
}

// destructor
void animal_destructor(animal_t* animal) {
    free(animal->name);
}

// behavior function
void animal_get_name(animal_t* animal, char* buffer) {
    strcpy(buffer, animal->name);
}

void animal_sound(animal_t* animal) {
    // call the function, which is pointed by the function pointer
    animal->sound_func(animal);
}

다형적 행위는 animal_sound 함수 내부에서 발생한다. 서브 클래스가 오버라이딩하지 않는다면 비공개 함수 __animal_sound가 기본 행위가 되어야 한다.

코드 박스 8-23 [예제 8-4] Cat 클래스의 공용 인터페이스

#ifndef EXTREME_C_EXAMPLES_CHAPTER_8_4_CAT_H
#define EXTREME_C_EXAMPLES_CHAPTER_8_4_CAT_H

// forward declaration
struct cat_t;

// memory allocator
struct cat_t* cat_new();

// constructor
void cat_constructor(struct cat_t*);

// destructor
void cat_destructor(struct cat_t*);

// all behavior functions are inherited from the animal class

#endif

코드 박스 8-24 [예제 8-4] Cat 클래스의 비공개 구현

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "8_4_animal.h"
#include "8_4_animal_p.h"

typedef struct {
    animal_t animal;
} cat_t;

// define a new behavior function for the sound of a cat
void __cat_sound(void* ptr) {
    animal_t* animal = (animal_t*) ptr;
    printf("%s: Meow\n", animal->name);
}

// memory allocator
cat_t* cat_new() {
    return (cat_t*) malloc(sizeof(cat_t));
}

// constructor
void cat_constructor(cat_t* cat) {
    animal_constructor((struct animal_t*) cat);
    strcpy(cat->animal.name, "Cat");
    // sound_func points to the new behavior function and this is the line where the overriding occurs
    cat->animal.sound_func = __cat_sound;
}

// destructor
void cat_destructor(cat_t* cat) {
    animal_destructor((struct animal_t*) cat);
}

코드 박스 8-25 [예제 8-4] Duck 클래스의 공용 인터페이스

#ifndef EXTREME_C_EXAMPLES_CHAPTER_8_4_DUCK_H
#define EXTREME_C_EXAMPLES_CHAPTER_8_4_DUCK_H

// forward declaration
struct duck_t;

// memory allocator
struct duck_t* duck_new();

// constructor
void duck_constructor(struct duck_t*);

// destructor
void duck_destructor(struct duck_t*);

// all behavior functions are inherited from the animal class

#endif

코드 박스 8-26 [예제 8-4] Duck 클래스의 비공개 구현

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "8_4_animal.h"
#include "8_4_animal_p.h"

typedef struct {
    animal_t animal;
} duck_t;

// define a new behavior function for the sound of a duck
void __duck_sound(void* ptr) {
    animal_t* animal = (animal_t*) ptr;
    printf("%s: Quacks\n", animal->name);
}

// memory allocator
duck_t* duck_new() {
    return (duck_t*) malloc(sizeof(duck_t));
}

// constructor
void duck_constructor(duck_t* duck) {
    animal_constructor((struct animal_t*) duck);
    strcpy(duck->animal.name, "Duck");
    // sound_func points to the new behavior function and this is the line where the overriding occurs
    duck->animal.sound_func = __duck_sound;
}

// destructor
void duck_destructor(duck_t* duck) {
    animal_destructor((struct animal_t*) duck);
}

위와 같이 부모 객체가 갖고 있는 sound_func 포인터를 자식 객체의 행위 함수에 대한 포인터로 설정하여 오버라이딩을 구현한다. 이것이 C++에서 다형성이 도입된 방식이다.

코드 박스 8-27 [예제 8-4]의 메인 시나리오

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// include only public interfaces
#include "8_4_animal.h"
#include "8_4_cat.h"
#include "8_4_duck.h"

int main(int argc, char** argv) {
    struct animal_t* animal = animal_new();
    struct cat_t* cat = cat_new();
    struct duck_t* duck = duck_new();

    animal_constructor(animal);
    cat_constructor(cat);
    duck_constructor(duck);

    animal_sound(animal);
    animal_sound((struct animal_t*) cat);
    animal_sound((struct animal_t*) duck);

    animal_destructor(animal);
    cat_destructor(cat);
    duck_destructor(duck);

    free(duck);
    free(cat);
    free(animal);

    return 0;
}

main 함수는 Animal, Cat, Duck 클래스의 공용 인터페이스에만 의존하고 구현에는 의존하지 않는다.

셀 박스 8-7 [예제 8-4]의 컴파일, 실행과 출력 결과

$ gcc -c 8_4_animal.c -o animal.o
$ gcc -c 8_4_cat.c -o cat.o
$ gcc -c 8_4_duck.c -o duck.o
$ gcc -c 8_4_main.c -o main.o
$ gcc animal.o cat.o duck.o main.o -o 8_4.out
$ ./8_4.out
Animal: Beeeep
Cat: Meow
Duck: Quacks

가상 함수(virtual function)는 자식 클래스에서 오버라이딩할 수 있는 행위 함수이다. C++ 같은 언어에서는 특정 함수가 다형적인 함수임을 나타내는 특별한 키워드를 사용하여 가상 함수를 지정한다. 이는 컴파일러가 추적하여 오버라이딩 시 실제 정의를 가리킬 수 있게 해야 한다.

profile
안녕하세요

0개의 댓글