文章目录

本文档描述了在 C 语言中创建类的最简单的代码,它将描述构造函数、实例变量、实例方法、类变量、类方法、继承、多态性、具有别名的命名空间,并将它们放在一个示例项目中。
一、类(C Classes)
类由实例类型和类对象组成:
实例类型是一个包含变量成员(称为实例变量)和函数成员(称为实例方法)的 struct
,实例类型的变量称为实例。
类对象是包含类变量和类方法的全局 const struct
变量,这些成员属于整个类,没有对任何实例的任何引用。
例如,一个名为 “Complex” 的类应将实例类型 struct Complex
和类对象 Complex
命名,并将接口定义放在 “Complex.h” 中,将实现放在 “Complex.c” 中。
以下是 Complex.h 文件:
struct Complex {
...
};
extern const struct ComplexClass {
...
} Complex;
以下是 Complex.c 文件:
#include "Complex.h"
const struct ComplexClass Complex = {...};
[!IMPORTANT]
类对象的成员也可以作为全局变量和函数实现,这甚至可以将构造函数实现为宏函数。 然而,为了设计的简单性,以及定义和使用命名空间的简单性,选择这样的设计会更受欢迎。
二、构造(Constructor)
实例在声明时必须由构造函数初始化,并且构造函数必须是类方法。 构造函数最好返回实例类型,但也可以返回指向实例类型的指针。
例如,Complex 类有两个实例变量 re
和 im
,以及一个名为 new
的构造函数,那么 Complex 类的 .h 文件和 .c 文件应该这样写:
以下是 Complex.h 文件:
struct Complex {
double re, im;
};
extern const struct ComplexClass {
struct Complex (*new)(double real, double imag);
} Complex;
以下是 Complex.c 文件:
#include "Complex.h"
static struct Complex new(double real, double imag) {
return (struct Complex){.re = real, .im = imag};
}
const struct ComplexClass Complex = {.new = &new};
如果要调用 new
函数,可以这样写:
struct Complex c = Complex.new(3., -4.);
三、方法(Methods)
实例方法必须声明为指向所需函数原型的实例类型成员,并且该指针必须由构造函数定义。 一般情况下,方法指针设置为实现文件中定义的静态函数。
为了能够访问实例的数据,实例方法必须接收指向实例的指针作为其第一个参数。 该参数通常被命名为 this
。
例如,添加一个实例方法 abs()
来计算复数的绝对值,.h 文件和 .c 文件可以这样写:
以下是 Complex.h 文件:
struct Complex {
double re, im;
double (*abs)(struct Complex *this);
};
extern const struct ComplexClass {
struct Complex (*new)(double real, double imag);
} Complex;
以下是 Complex.c 文件:
#include "Complex.h"
static double abs(struct Complex *this) {
return sqrt(this->re * this->re + this->im * this->im);
}
static struct Complex new(double real, double imag) {
return (struct Complex){.re = real, .im = imag, .abs = &abs};
}
const struct ComplexClass Complex={.new = &new};
如果要调用 abs
函数,可以这样写:
struct Complex c = Complex.new(3., -4.);
printf("%g\n", c.abs(&c));
[!NOTE]
类方法必须以与实例方法相同的方式初始化,但对原型没有限制。
四、继承(Inheritance)
基类必须表示为与基类本身具有相同名称和类型的成员变量。
子类可以重写基类实例方法指针以提供多态性,子类必须使用相同原型的函数进行重写,并在调用基类的构造函数后在构造函数中设置基类的方法指针。
每当调用重写的实例方法时,必须要保证它是由基类的实例调用的。由于实例方法接收指向基类的指针作为其第一个参数,因此我们可以使用 stddef.h
中的 offsetof()
宏获取子类。
以下是展示继承和多态性的简单示例:
以下是 Employee.h 文件:
struct Employee {
const char *first_name;
const char *family_name;
const char *(*print)(struct Employee *this,
size_t bufsize,char buf[bufsize]);
};
extern const struct EmployeeClass {
struct Employee (*new)(const char *first_name, const char *family_name);
} Employee;
以下是 Employee.c 文件:
#include "Employee.h"
static const char *print(struct Employee *this, size_t bufsize,
char buf[bufsize])
{
snprintf(buf, bufsize, "Name: %s %s", this->first_name, this->family_name);
return buf;
}
static struct Employee new(const char *first_name, const char *family_name)
{
return (struct Employee){
.first_name = strdup(first_name),
.family_name = strdup(family_name),
.print = &print
};
}
const struct EmployeeClass Employee = {.new = &new};
以下是 Manager.h 文件:
#include "Employee.h"
struct Manager {
struct Employee Employee;
int level;
};
extern const struct ManagerClass {
struct Manager (*new)(const char *first_name, const char *family_name,
int level);
} Manager;
以下是 Manager.c 文件:
#include "Manager.h"
static const char *print(struct Employee *base, size_t bufsize,
char buf[bufsize])
{
struct Manager *this = (void *)base-offsetof(struct Manager, Employee);
snprintf(buf, bufsize, "Name: %s %s, level %d",
this->Employee.first_name,
this->Employee.family_name,
this->level);
return buf;
}
static struct Manager new(const char *first_name, const char *family_name,
int level)
{
struct Manager ret = {.level = level};
ret.Employee = Employee.new(first_name, family_name);
ret.Employee.print = &print;
return ret;
}
extern const struct ManagerClass Manager = {.new = &new};
以下是调用的示例:
#include "Manager.h"
int main(void)
{
struct Manager manager = Manager.new("Grayson", "Zheng", 13);
struct Employee employee = Employee.new("Grayson", "Zheng");
struct Employee *polymorph = &manager.Employee;
char buf[50];
printf("%s\n", employee.print(&employee, sizeof(buf), buf));
printf("%s\n", polymorph->print(polymorph, sizeof(buf), buf));
return 0;
}
Manager 类使用 Manager.c ,用下面的这行代码重写了 Employee 的 print()
实例方法:
ret.Employee.print = &print;
五、控制对成员的访问
在面向对象语言中,每个成员都有一个访问属性,编译器将强制执行该访问属性。对于 C 语言中的类,可以使用注释来指定访问属性。
例如:
struct Complex {
...
// protected:
...
// private:
...
};
六、抽象(Abstract)
在面向对象语言中,可以指定一个抽象类来保证该类不能被实例化,抽象方法和接口可以用来保证子类重写方法。
对于 C 语言中的类,只需确保该类的任何用户都理解此类含义,例如:
struct ElementInterface {
...
};
/*interface*/
struct Element {
...
};
/*abstract*/
struct Complex {
...
};
struct Stack {
/*abstract*/
double (*foo)(struct Stack *this);
};
抽象实例方法指针应初始化为 NULL
。
七、命名空间(Namespaces)
命名空间定义了类导出的所有标识符的通用前缀及其头和实现文件的路径。
例如,具有命名空间 org_pvv_hakonhal_utils_Complex
的 Complex
类应在 org/pvv/hakonhal/utils/Complex.c
中拥有其实现文件,在 org/pvv/hakonhal/utils/Complex.h
中拥有其头文件,其中包含:
#ifndef ORG_PVV_HAKONHAL_UTILS_COMPLEX_H
#define ORG_PVV_HAKONHAL_UTILS_COMPLEX_H
struct org_pvv_hakonhal_utils_Complex {
...
};
extern struct org_pvv_hakonhal_utils_ComplexClass {
...
} org_pvv_hakonhal_utils_Complex;
#endif
当要使用该类时,可以使用 #define 指令为标识符添加别名,使它们更易于管理:
#include "org/pvv/hakonhal/utils/Complex.h"
#define Complex org_pvv_hakonhal_utils_Complex
...
struct Complex c = Complex.new();
八、示例项目
这里将以这个简单的堆栈项目作为例子,在这个示例项目中,将通过扩展一个更简单的堆栈实现来创建和测试边界检查堆栈实现。该项目将说明有关 C 语言的类的所有内容,包括构造函数、方法、继承、命名空间和别名。
1. The Libray Project
我们假设这个简单的堆栈项目是从网上下载的,头文件可能被引用为 org/somewhere/someone/stack.h
。堆栈头包含:
Stack.h
#ifndef ORG_SOMEWHERE_SOMEONE_STACK_H
#define ORG_SOMEWHERE_SOMEONE_STACK_H
struct org_somewhere_someone_Stack_ElementI {
};
#define ORG_SOMEWHERE_SOMEONE_STACK_SIZE 100
struct org_somewhere_someone_Stack {
void (*push)(struct org_somewhere_someone_Stack *this,
struct org_somewhere_someone_Stack_ElementI *element);
struct org_somewhere_someone_Stack_ElementI *(*pop)
(struct org_somewhere_someone_Stack *this);
// protected:
int count;
struct org_somewhere_someone_Stack_ElementI *
data[ORG_SOMEWHERE_SOMEONE_STACK_SIZE];
};
extern struct org_somewhere_someone_StackClass {
struct org_somewhere_someone_Stack (*new)(void);
} org_somewhere_someone_Stack;
#endif
org_somewhere_someone_Stack_ElementI
是存储在堆栈中的元素的接口,并且对存储的元素没有任何限制,因为该结构具有空主体。
在唯一的 org_pvv_hakonhal_utils
命名空间创建 org/pvv/hakonhal/utils
目录,将 BStack.c 和 BStack.h 文件放在其中。
BStack.h:
#ifndef ORG_PVV_HAKONHAL_UTILS_BSTACK_H
#define ORG_PVV_HAKONHAL_UTILS_BSTACK_H
#include "org/somewhere/someone/Stack.h"
struct org_pvv_hakonhal_utils_BStack {
struct org_somewhere_someone_Stack org_somewhere_someone_Stack;
};
extern const struct org_pvv_hakonhal_utils_BStackClass {
struct org_pvv_hakonhal_utils_BStack (*new)(void);
} org_pvv_hakonhal_utils_BStack;
#endif
BStack.c:
#include "org/pvv/hakonhal/utils/BStack.h"
#define BStack org_pvv_hakonhal_utils_BStack
#define BStackClass org_pvv_hakonhal_utils_BStackClass
#include "org/somewhere/someone/Stack.h"
#define ElementI org_somewhere_someone_Stack_ElementI
#define Stack org_somewhere_someone_Stack
#define STACK_SIZE ORG_SOMEWHERE_SOMEONE_STACK_SIZE
#include <stdio.h>
#include <stdlib.h>
static void (*base_push)(struct Stack *this, struct ElementI *element);
static void push(struct Stack *base, struct ElementI *element)
{
if (base->count >= STACK_SIZE) {
fprintf(stderr, "%s", "Stack overflow!\n");
exit(1);
}
base_push(base, element);
}
static struct ElementI *(*base_pop)(struct Stack *this);
static struct ElementI *pop(struct Stack *base)
{
if (base->count <= 0) {
fprintf(stderr, "%s", "Stack underflow!\n");
exit(1);
}
return base_pop(base);
}
static struct BStack new(void)
{
struct BStack ret;
ret.Stack = Stack.new();
base_push = ret.Stack.push;
ret.Stack.push = &push;
base_pop = ret.Stack.pop;
ret.Stack.pop = &pop;
return ret;
}
const struct BStackClass BStack = {.new = &new};
2. Testing the Library Project
下面是项目测试:
BStack_test.c:
#include "org/pvv/hakonhal/utils/BStack.h"
#define BStack org_pvv_hakonhal_utils_BStack
#include "org/somewhere/someone/Stack.h"
#define ElementI org_somewhere_someone_Stack_ElementI
#define Stack org_somewhere_someone_Stack
#include <stddef.h>
#include <stdio.h>
struct Integer {
struct ElementI ElementI;
int value;
void (*print)(struct Integer *this, const char *id);
};
static void print(struct Integer *this, const char *id)
{
printf("%s: %d\n", id, this->value);
}
static struct Integer new(int value)
{
return (struct Integer) {
.ElementI = {},
.value = value,
.print = &print,
};
}
static const struct {
struct Integer (*new)(int value);
} Integer = {.new = &new};
int main(void)
{
struct BStack stack = BStack.new();
struct Integer i = Integer.new(10), j = Integer.new(20);
struct Integer *ptr;
stack.Stack.push(&stack.Stack, &i.ElementI);
stack.Stack.push(&stack.Stack, &j.ElementI);
ptr = (void *)stack.Stack.pop(&stack.Stack) - offsetof(struct Integer, ElementI);
ptr->print(ptr, "j");
ptr = ((void *)stack.Stack.pop(&stack.Stack)) - offsetof(struct Integer, ElementI);
ptr->print(ptr, "i");
printf("%s\n", "Will now try to pop an empty stack");
stack.Stack.pop(&stack.Stack);
return 0;
}
这里我定义了一个 Integer
类,它扩展了 org_somewhere_someone_Stack_ElementI
接口,以便它可以添加到 BStack
中。 该类还包含一个 int
和一个用于打印它的实例方法。由于构造的是一个可执行文件,因此 Integer
类不需要具有命名空间。