C 클래스를 어떻게 구현합니까? [닫은]


139

C (C ++ 또는 객체 지향 컴파일러 없음)를 사용해야하고 동적 메모리 할당이 없다고 가정하면 클래스를 구현하는 데 사용할 수있는 기술이나 클래스의 근사값은 무엇입니까? "클래스"를 별도의 파일로 분리하는 것이 항상 좋은 생각입니까? 고정 된 수의 인스턴스를 가정하거나 컴파일 시간 전에 각 객체에 대한 참조를 상수로 정의하여 메모리를 사전 할당 할 수 있다고 가정하십시오. 구현해야 할 OOP 개념 (다양)에 대해 자유롭게 가정하고 각각에 가장 적합한 방법을 제안하십시오.

제한 사항 :

  • 임베디드 시스템을위한 코드를 작성하고 컴파일러와 기존 코드베이스가 C에 있기 때문에 OOP가 아닌 C를 사용해야합니다.
  • 동적 할당을 시작하면 메모리가 부족하다고 합리적으로 추정 할 충분한 메모리가 없기 때문에 동적 메모리 할당이 없습니다.
  • 우리가 작업하는 컴파일러는 함수 포인터에 문제가 없습니다.

26
필수 질문 : 객체 지향 코드를 작성해야합니까? 어떤 이유에서든 상관없이 괜찮습니다. 그러나 다소 오르막 한 전투를하게 될 것입니다. C로 객체 지향 코드를 작성하지 않으려는 경우 아마도 최선일 것입니다. 확실히 가능합니다-unwind의 탁월한 답변을 참조하십시오. 가능하지 않을 수 있습니다. 나는 틀릴지도 모른다-나는 너를 논쟁하려고하지 않고, 제시되지 않았을 수도있는 몇 가지 반론을 제시한다.
Chris Lutz

1
엄밀히 말하면, 우리는 필요하지 않습니다. 그러나 시스템의 복잡성으로 인해 코드를 유지할 수 없게되었습니다. 복잡성을 줄이는 가장 좋은 방법은 OOP 개념을 구현하는 것입니다. 3 분 이내에 응답 해 주신 모든 분들께 감사드립니다. 너희들은 미쳤고 빠르다!
벤 가트너

8
이것은 내 겸손한 의견이지만 OOP는 코드를 즉시 유지 관리 할 수있게하지 않습니다. 관리가 쉬워 지지만 유지 관리가 용이하지는 않습니다. C (Apache Portable Runtime은 모든 글로벌 심볼 apr_앞에 접두사를 붙이고 GLib g_는 네임 스페이스를 만들기 위해 접두사를 붙임 )와 OOP가없는 기타 구성 요소에 "namespaces"를 가질 수 있습니다 . 어쨌든 앱을 재구성하려는 경우 좀 더 관리 가능한 절차 구조를 마련하는 데 시간을 투자하는 것이 좋습니다.
Chris Lutz

이것은 이전에 끝없이 논의 된 적이 있습니다. 이전 답변을 보셨습니까?
Larry Watanabe

내 삭제 된 대답했다이 소스는 또한 도움이 될 수 있습니다 planetpdf.com/codecuts/pdfs/ooc.pdf는 그것은 C.에 OO 일을위한 완벽한 방법을 설명합니다
루벤 STEINS

답변:


86

이는 원하는 "개체 지향"기능 세트에 따라 다릅니다. 오버로드 및 / 또는 가상 메소드와 같은 것이 필요한 경우 구조에 함수 포인터를 포함해야합니다.

typedef struct {
  float (*computeArea)(const ShapeClass *shape);
} ShapeClass;

float shape_computeArea(const ShapeClass *shape)
{
  return shape->computeArea(shape);
}

이를 통해 기본 클래스를 "상속"하고 적절한 함수를 구현하여 클래스를 구현할 수 있습니다.

typedef struct {
  ShapeClass shape;
  float width, height;
} RectangleClass;

static float rectangle_computeArea(const ShapeClass *shape)
{
  const RectangleClass *rect = (const RectangleClass *) shape;
  return rect->width * rect->height;
}

물론 함수 포인터가 올바르게 설정되도록 생성자를 구현해야합니다. 일반적으로 인스턴스에 메모리를 동적으로 할당하지만 호출자에게도 그렇게 할 수 있습니다.

void rectangle_new(RectangleClass *rect)
{
  rect->width = rect->height = 0.f;
  rect->shape.computeArea = rectangle_computeArea;
}

여러 개의 다른 생성자를 원하면 함수 이름을 "장식"해야하며 둘 이상의 rectangle_new()함수를 가질 수 없습니다 .

void rectangle_new_with_lengths(RectangleClass *rect, float width, float height)
{
  rectangle_new(rect);
  rect->width = width;
  rect->height = height;
}

사용법을 보여주는 기본 예는 다음과 같습니다.

int main(void)
{
  RectangleClass r1;

  rectangle_new_with_lengths(&r1, 4.f, 5.f);
  printf("rectangle r1's area is %f units square\n", shape_computeArea(&r1));
  return 0;
}

나는 이것이 적어도 당신에게 아이디어를 줄 수 있기를 바랍니다. C에서 성공적이고 풍부한 객체 지향 프레임 워크를 보려면 glib의 GObject 라이브러리를 살펴보십시오 .

또한 위에서 모델링 한 명시적인 "클래스"가 없으며 각 객체에는 일반적으로 C ++에서 찾을 수있는 것보다 약간 더 유연한 고유 한 메서드 포인터가 있습니다. 또한 메모리 비용이 듭니다. class구조체 에서 메서드 포인터를 채워서 각 개체 인스턴스가 클래스를 참조 할 수있는 방법을 만들어서 그로부터 벗어날 수 있습니다 .


객체 지향 C를 작성하지 않아도 보통 인수 를 취 const ShapeClass *하거나 const void *인수 로 사용하는 함수를 만드는 것이 가장 좋습니까? 후자는 상속에 비트 좋을 수 있음을 보일 수있을 것입니다,하지만 난 인수를 두 가지 ... 볼 수 있습니다
크리스 루츠

1
@Chris : 네, 어려운 질문입니다. : | GTK + (GObject 사용)는 적절한 클래스, 즉 RectangleClass *를 사용합니다. 이것은 종종 캐스트를해야하지만 편리한 매크로를 제공하므로 SUBCLASS (p) 만 사용하여 BASECLASS * p를 SUBCLASS *로 캐스트 할 수 있습니다.

1
내 컴파일러는 두 번째 코드 줄에서 실패 합니다. 알 수없는 유형 float (*computeArea)(const ShapeClass *shape);이라고 말합니다 ShapeClass.
DanielSank

@DanielSank는 'typedef 구조체'에 필요한 정방향 선언이 없기 때문에 발생합니다 (주어진 예제에는 표시되지 않음). struct참조 자체 이므로 정의 하기 전에 선언 이 필요합니다 . 이것은 Lundin 's answer의 예제와 함께 설명됩니다 . 정방향 선언을 포함하도록 예제를 수정하면 문제가 해결됩니다. typedef struct ShapeClass ShapeClass; struct ShapeClass { float (*computeArea)(const ShapeClass *shape); };
S. Whittaker

Rectangle에 모든 셰이프가 아닌 기능이있는 경우 어떻게됩니까? 예를 들어, get_corners ()입니다. 원은 이것을 구현하지 않지만 직사각형을 구현할 수 있습니다. 상속받은 부모 클래스의 일부가 아닌 함수에 어떻게 액세스합니까?
Otus

24

숙제도 한 번해야 했어요. 나는이 접근법을 따랐다.

  1. 구조체에서 데이터 멤버를 정의하십시오.
  2. 구조체에 대한 포인터를 첫 번째 인수로 사용하는 함수 멤버를 정의하십시오.
  3. 하나의 헤더와 하나의 c 에서이 작업을 수행하십시오. 구조체 정의 및 함수 선언을위한 헤더, 구현을위한 c

간단한 예는 다음과 같습니다.

/// Queue.h
struct Queue
{
    /// members
}
typedef struct Queue Queue;

void push(Queue* q, int element);
void pop(Queue* q);
// etc.
/// 

이것은 내가 과거에 한 일이지만 필요에 따라 함수 프로토 타입을 .c 또는 .h 파일에 배치하여 가짜 범위를 추가했습니다 (필자가 대답했듯이).
Taylor Leese

나는 이것을 좋아한다. 구조체 선언은 모든 메모리를 할당한다. 어떤 이유로 나는 이것이 잘 작동한다는 것을 잊었다.
벤 가트너

나는 당신이 typedef struct Queue Queue;거기에 필요하다고 생각합니다 .
Craig McQueen

3
또는 typedef struct {/ * members * /} Queue;
Brooks Moses

#Craig : 알림 주셔서 감사합니다.
erelender

12

하나의 클래스 만 원하면 structs 배열을 "객체"데이터로 사용하고 포인터를 "멤버"함수에 전달하십시오. 클라이언트 코드에서 구현을 숨기도록 typedef struct _whatever Whatever선언 struct _whatever하기 전에 사용할 수 있습니다 . 이러한 "객체"와 C 표준 라이브러리 사이에는 차이가 없습니다FILE 객체 .

상속과 가상 함수를 가진 클래스를 두 개 이상 원한다면 구조체의 멤버로 함수에 대한 포인터 또는 가상 함수 테이블에 대한 공유 포인터를 갖는 것이 일반적입니다. G 객체의 라이브러리는 모두 이것과 타입 정의의 트릭을 사용하고 널리 사용된다.

ANSI C를 이용한 온라인 객체 지향 프로그래밍 에 대한 기술에 관한 책도 있습니다 .


1
멋있는! C의 OOP 관련 서적에 대한 다른 권장 사항이 있습니까? 아니면 C의 다른 현대적인 디자인 기술? (또는 임베디드 시스템?)
Ben Gartner

7

GOBject를 살펴볼 수 있습니다. 객체를 수행하는 자세한 방법을 제공하는 OS 라이브러리입니다.

http://library.gnome.org/devel/gobject/stable/


1
매우 흥미 롭다. 라이센스에 대해 아는 사람이 있습니까? 직장에서의 목적을 위해 오픈 소스 라이브러리를 프로젝트에 드롭하는 것은 법적 관점에서 작동하지 않을 것입니다.
벤 가트너

GTK + 및 해당 프로젝트의 일부인 모든 라이브러리 (GObject 포함)는 GNU LGPL에 따라 라이센스가 부여되므로 독점 소프트웨어에서 링크 할 수 있습니다. 그래도 그것이 임베디드 작업에 적합한 지 모르겠습니다.
Chris Lutz

7

C 인터페이스 및 구현 : 재사용 가능한 소프트웨어 작성 기법 , David R. Hanson

http://www.informit.com/store/product.aspx?isbn=0201498413

이 책은 당신의 질문을 다루는 훌륭한 일을합니다. Addison Wesley Professional Computing 시리즈에 있습니다.

기본 패러다임은 다음과 같습니다.

/* for data structure foo */

FOO *myfoo;
myfoo = foo_create(...);
foo_something(myfoo, ...);
myfoo = foo_append(myfoo, ...);
foo_delete(myfoo);

5

나는 C에서 OOP가 어떻게 수행되어야하는지에 대한 간단한 예를 제시 할 것이다. 나는이 주제가 2009 년의 것이지만 어쨌든 이것을 추가하고 싶다는 것을 알고있다.

/// Object.h
typedef struct Object {
    uuid_t uuid;
} Object;

int Object_init(Object *self);
uuid_t Object_get_uuid(Object *self);
int Object_clean(Object *self);

/// Person.h
typedef struct Person {
    Object obj;
    char *name;
} Person;

int Person_init(Person *self, char *name);
int Person_greet(Person *self);
int Person_clean(Person *self);

/// Object.c
#include "object.h"

int Object_init(Object *self)
{
    self->uuid = uuid_new();

    return 0;
}
uuid_t Object_get_uuid(Object *self)
{ // Don't actually create getters in C...
    return self->uuid;
}
int Object_clean(Object *self)
{
    uuid_free(self->uuid);

    return 0;
}

/// Person.c
#include "person.h"

int Person_init(Person *self, char *name)
{
    Object_init(&self->obj); // Or just Object_init(&self);
    self->name = strdup(name);

    return 0;
}
int Person_greet(Person *self)
{
    printf("Hello, %s", self->name);

    return 0;
}
int Person_clean(Person *self)
{
    free(self->name);
    Object_clean(self);

    return 0;
}

/// main.c
int main(void)
{
    Person p;

    Person_init(&p, "John");
    Person_greet(&p);
    Object_get_uuid(&p); // Inherited function
    Person_clean(&p);

    return 0;
}

기본 개념은 '상속 된 클래스'를 구조체의 맨 위에 배치하는 것입니다. 이런 식으로, 구조체에서 처음 4 바이트에 액세스하면 '상속 된 클래스'의 처음 4 바이트에도 액세스합니다 (미친 최적화 가정). 이제 구조체의 포인터가 '상속 된 클래스'로 캐스트되면 '상속 된 클래스'는 정상적으로 멤버에 액세스하는 것과 같은 방식으로 '상속 된 값'에 액세스 할 수 있습니다.

생성자, 소멸자, 할당 및 할당 해제 함수 (init, clean, new, free 권장)에 대한 이것과 일부 명명 규칙은 먼 길을 갈 것입니다.

가상 함수의 경우 구조체에서 함수 포인터를 사용하십시오 (Class_func (...); 래퍼도. (간단한) 템플릿의 경우 size_t 매개 변수를 추가하여 크기를 결정하거나 void * 포인터가 필요하거나 관심있는 기능 만있는 '클래스'유형이 필요합니다. (예 : int GetUUID (Object * self); GetUUID (& p);)


면책 조항 : 모든 코드는 스마트 폰에 작성되었습니다. 필요한 경우 오류 점검을 추가하십시오. 버그를 확인하십시오.
yyny

4

a struct를 사용하여 클래스의 데이터 멤버를 시뮬레이션하십시오. 메소드 범위와 관련하여 개인 함수 프로토 타입을 .c 파일 에 배치 하고 공용 함수를 .h 파일 에 배치하여 개인 메소드를 시뮬레이션 할 수 있습니다 .


4
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <uchar.h>

/**
 * Define Shape class
 */
typedef struct Shape Shape;
struct Shape {
    /**
     * Variables header...
     */
    double width, height;

    /**
     * Functions header...
     */
    double (*area)(Shape *shape);
};

/**
 * Functions
 */
double calc(Shape *shape) {
        return shape->width * shape->height;
}

/**
 * Constructor
 */
Shape _Shape() {
    Shape s;

    s.width = 1;
    s.height = 1;

    s.area = calc;

    return s;
}

/********************************************/

int main() {
    Shape s1 = _Shape();
    s1.width = 5.35;
    s1.height = 12.5462;

    printf("Hello World\n\n");

    printf("User.width = %f\n", s1.width);
    printf("User.height = %f\n", s1.height);
    printf("User.area = %f\n\n", s1.area(&s1));

    printf("Made with \xe2\x99\xa5 \n");

    return 0;
};

3

귀하의 경우 클래스의 근사값은 ADT 일 수 있습니다 . 그러나 여전히 동일하지는 않습니다.


1
누구나 추상 데이터 유형과 클래스 사이에 간단한 차이점을 줄 수 있습니까? 나는 항상 두 가지 개념을 밀접하게 연결되어 있습니다.
벤 가트너

그들은 실제로 밀접하게 관련되어 있습니다. 클래스는 ADT의 구현으로 볼 수 있습니다. 왜냐하면 동일한 인터페이스를 만족시키는 다른 구현으로 대체 될 수 있기 때문입니다. 개념이 명확하게 정의되어 있지 않기 때문에 정확한 차이점을 제시하기는 어렵다고 생각합니다.
Jørgen Fogh

3

내 전략은 다음과 같습니다

  • 클래스의 모든 코드를 별도의 파일로 정의
  • 별도의 헤더 파일에서 클래스의 모든 인터페이스를 정의하십시오.
  • 모든 멤버 함수는 인스턴스 이름을 나타내는 "ClassHandle"을 사용합니다 (o.foo () 대신 foo (oHandle) 호출
  • 생성자는 메모리 할당 전략에 따라 void ClassInit (ClassHandle h, int x, int y, ...) 또는 ClassHandle ClassInit (int x, int y, ...) 함수로 대체됩니다.
  • 모든 멤버 변수는 클래스 파일에 정적 구조체의 멤버로 저장되어 파일에 캡슐화되어 외부 파일이 액세스하지 못하게합니다.
  • 객체는 사전 정의 된 핸들 (인터페이스에 표시) 또는 인스턴스화 할 수있는 고정 된 객체 제한과 함께 위의 정적 구조체 배열에 저장됩니다.
  • 유용한 경우 클래스는 배열을 반복하고 모든 인스턴스화 된 객체의 함수를 호출하는 공용 함수를 포함 할 수 있습니다 (RunAll ()은 각 Run (oHandle)을 호출합니다.
  • Deinit (ClassHandle h) 함수는 동적 할당 전략에서 할당 된 메모리 (배열 색인)를 해제합니다.

이 접근 방식의 변형으로 인한 문제점, 허점, 잠재적 함정 또는 숨겨진 이점 / 단점을 아는 사람이 있습니까? 디자인 방법을 재발 명하고 있어야한다고 생각한다면 그 이름을 알려 주시겠습니까?


스타일에 따라 질문에 추가 할 정보가 있으면이 정보를 포함하도록 질문을 편집해야합니다.
Chris Lutz

malloc에서 큰 힙에서 동적으로 고정 크기 풀에서 동적으로 선택하는 ClassInit ()로 동적으로 이동 한 것 같습니다. 다른 객체를 요청할 때 발생할 일에 대해 실제로 수행하지 않고 하나를 제공 할 리소스가없는 것 .
피트 Kirkham

예, 메모리 관리 부담은 ClassInit ()를 호출하는 코드로 옮겨져 반환 된 핸들이 유효한지 확인합니다. 본질적으로 우리는 클래스 전용 힙을 만들었습니다. 범용 힙을 구현하지 않는 한 동적 할당을 수행하려는 경우이를 피할 수있는 방법을 모르겠습니다. 힙에서 하나의 클래스로 상속되는 위험을 격리하는 것을 선호합니다.
벤 가트너

3

또한 볼 이 답변 하고 이 일을

것이 가능하다. 그것은 항상 좋은 생각처럼 보이지만 나중에는 유지 보수의 악몽이됩니다. 코드가 모든 것을 묶는 코드 조각으로 가득 차 있습니다. 함수 포인터를 사용하면 어떤 함수가 호출되는지 명확하지 않기 때문에 새로운 프로그래머는 코드를 읽고 이해하는 데 많은 문제가 있습니다.

get / set 함수를 사용한 데이터 숨기기는 C에서 구현하기 쉽지만 중지하십시오. 임베디드 환경에서 여러 번 시도했지만 결국 유지 관리 문제입니다.

모든 유지 보수 문제가 있으므로 준비를 분명히해야합니다.


2

내 접근 방식은 주로 관련된struct 모든 것을 이동하는 것입니다 기능을 별도의 소스 파일 "휴대용"으로 사용할 수 있도록하는 것입니다.

컴파일러에 따라 수도 에 기능을 포함 할 수 struct있지만,이 A의 매우 컴파일러 특정 확장하고, 일상적으로 사용되는 표준 I의 마지막 버전과는 아무 상관이 없습니다 :)


2
함수 포인터는 모두 좋습니다. 큰 스위치 문을 조회 테이블로 바꾸는 데 사용하는 경향이 있습니다.
벤 가트너

2

첫 번째 c ++ 컴파일러는 실제로 C ++ 코드를 C로 변환 한 전 처리기였습니다.

따라서 C로 클래스를 가질 수 있습니다. 오래된 C ++ 전처리기를 파헤쳐 서 어떤 종류의 솔루션을 만드는지 알아볼 수 있습니다.


그럴 것이다 cfront; 예외가 C ++에 추가되었을 때 문제가 발생했습니다. 예외 처리는 쉽지 않습니다.
Jonathan Leffler

2

GTK는 전적으로 C를 기반으로하며 많은 OOP 개념을 사용합니다. GTK의 소스 코드를 읽었으며 꽤 인상적이며 읽기 쉽습니다. 기본 개념은 각 "클래스"가 단순한 구조체 및 관련 정적 함수라는 것입니다. 정적 함수는 모두 "인스턴스"구조체를 매개 변수로 받아들이고 필요한 모든 작업을 수행하며 필요한 경우 결과를 반환합니다. 예를 들어 "GetPosition (CircleStruct obj)"기능이있을 수 있습니다. 이 함수는 단순히 구조체를 파고, 위치 번호를 추출하고, 새로운 PositionStruct 객체를 만들고, 새 PositionStruct에 x와 y를 붙이고 반환합니다. GTK는 구조체 안에 구조체를 삽입하여 이런 식으로 상속을 구현합니다. 꽤 영리합니다.


1

가상 방법을 원하십니까?

그렇지 않으면 구조체 자체에 함수 포인터 세트를 정의하기 만하면됩니다. 모든 함수 포인터를 표준 C 함수에 할당하면 C ++에서와 유사한 구문으로 C에서 함수를 호출 할 수 있습니다.

가상 메소드를 원하면 더 복잡해집니다. 기본적으로 각 구조체에 고유 한 VTable을 구현하고 호출되는 함수에 따라 VTable에 함수 포인터를 할당해야합니다. 그런 다음 구조체 자체에 VTable의 함수 포인터를 호출하는 함수 포인터 세트가 필요합니다. 이것은 본질적으로 C ++의 기능입니다.

TBH 그러나 ... 후자를 원한다면 프로젝트를 사용하고 다시 컴파일 할 수있는 C ++ 컴파일러를 찾는 것이 좋습니다. 나는 C ++에 대한 집착이 임베디드에서 사용할 수 없다는 것을 결코 이해하지 못했습니다. 나는 그것을 한 번에 많이 사용했으며 작동 속도가 빠르고 메모리 문제가 없습니다. 물론 당신이하는 일에 대해 조금 더 조심해야하지만 실제로 그렇게 복잡하지는 않습니다.


이미 말했고 다시 말할 것이지만 다시 말할 것입니다. 함수 포인터 또는 C ++ 스타일에서 구조체를 호출하여 C에서 OOP를 생성하는 기능이 필요하지 않습니다 .OOP는 주로 기능과 변수의 상속에 관한 것입니다. 함수 포인터 나 중복 코드없이 C에서 달성 할 수 있습니다.
yyny 2016 년

0

C는 OOP 언어가 아니기 때문에 올바른 클래스를 작성하는 기본 방법은 없습니다. 가장 좋은 방법은 구조체함수 포인터 를 보는 것입니다.이 클래스를 사용하면 대략적인 클래스를 만들 수 있습니다. 그러나 C가 절차 적이므로 더 많은 C와 같은 코드를 작성하는 것이 좋습니다 (예 : 클래스를 사용하지 않고).

또한 C를 사용할 수 있다면 C ++을 사용하고 클래스를 얻을 수 있습니다.


4
나는 공감하지는 않지만 FYI, 함수 포인터 또는 구조체에서 함수를 호출하는 기능 (당신의 의도라고 생각하는 것)은 OOP와 관련이 없습니다. OOP는 주로 기능 및 변수의 상속에 관한 것으로, 둘 다 함수 포인터 나 복제없이 C에서 달성 할 수 있습니다.
yyny 2016 년
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.