C语言使用结构体面向对象编程举例讲解

示例1:验证器

例1来自于 百度文库 C语言实现面向对象编程,现在本文给他大家提供了可以自由学习的版本。

validator.h

#ifndef ZIYU_LEARN_C_VALIDATOR_H
#define ZIYU_LEARN_C_VALIDATOR_H

#include <stdbool.h>

typedef struct Validator_t {
    bool (* const validate)(struct Validator_t *this, int value);
} Validator;

typedef struct {
    Validator proxy;
    int min;
    int max;
} RangeValidator;

typedef struct {
    Validator proxy;
    bool isEven; //
} OddEvenValidator;

bool isInRange(Validator* this, int value); // 范围校验函数
bool isEvenOrElseOdd(Validator* this, int value); // 奇偶校验函数

#define newRangeValidator(min, max) {{isInRange}, (min), (max)}
#define newOddEvenValidator(isEven) {{isEvenOrElseOdd}, (isEven)}

#endif //ZIYU_LEARN_C_VALIDATOR_H

易错点:Validator proxy; 不要写成 Validator* proxy(加入星号表示指针),否则会让宏定义变得更复杂!稍后 Animal 的例子中会讲。

本文准备详细讲解的是宏定义的这段语法:
首先,min,max, isEven 都是用到了带参数的宏

带参数的宏定义 使用宏的代码 预处理器替换后
#define MAX(x,y) ((x)>(y) ? (x) :(y)) i = MAX ( j+k, m-n ); i = (( j+k ) > ( m-n ) ? ( j+k ) : ( m-n ));
#define IS_EVEN(n) ((n)%2==0) if (IS_EVEN( i )) i++; if (( ( i ) % 2 == 0 )) i++;

解析 #define newXXX(arg0) { func0 , (arg0)} 语法

预处理器会将宏定义的参数“原封不动”进行替换。因此,我们需要关注的是 { },及它所代表的 struct 结构体变量整体赋值的语法:

#include <stdio.h>

typedef struct {
  int age;
  void (* swim)();
} Animal;

void practice(); // 声明一个表示练习游泳的函数
int main() {
  // 定义结构体变量a的同时,进行整体赋值。
  Animal a = {11, practice};

  printf("%d\n", a.age);
  a.swim();
  return 0;
}

void practice() {
  printf("practising the swim.\n");
}

可以尝试将结构体Animal整体赋值的部分写成宏定义,如下表所示:

带参数的宏定义 使用宏的代码 预处理器替换后
#define newAnimal(age) {(age), practice} Animal a = newAnimal( i ); Animal a = {( i ), practice};

因此,上面的代码可以改写成:

#include <stdio.h>

typedef struct {
  int age;
  void (* swim)();
} Animal;

void practice(); // 声明一个表示练习游泳的函数

#define newAnimal(age) {(age), practice}

int main() {
  Animal a = newAnimal(11);

  printf("%d\n", a.age);
  a.swim();
  return 0;
}

void practice() {
  printf("practising the swim.\n");
}

解析 #define newXXX(arg0) &(XXX){ func0 , (arg0)} 语法

接下来,某种意义上讲,是我自己把问题搞复杂了,但是也算给大家提个醒吧:

Animal* pa = newAnimal(11); // 我误加了星号,引出了后面的讨论

编译时出错,我截取了关键的报错信息:

/home/geekziyu/CLionProjects/ziyu-learn-jvm-c/learnc/main.c:11:25: warning: initialization of ‘Animal *’ {aka ‘struct <anonymous> *’} from ‘int’ makes pointer from integer without a cast [-Wint-conversion]
   11 | #define newAnimal(age) {(age), practice}
...
/home/geekziyu/CLionProjects/ziyu-learn-jvm-c/learnc/main.c:11:32: warning: excess elements in scalar initializer
   11 | #define newAnimal(age) {(age), practice}

第一个报错没搜索出有用的解决方案,但是第二个报错搜出来了:https://stackoverflow.com/questions/50614424/excess-elements-in-scalar-initializer-for-struct

因此,代码修改为:

#include "validator.h"
#include <stdio.h>

typedef struct {
  int age;
  void (* swim)();
} Animal;

void practice(); // 声明一个表示练习游泳的函数

#define newAnimal(age) &(Animal){(age), practice}

int main() {
  // 注意这里pa是指针,而非结构体变量
  Animal* pa = newAnimal(11);

  printf("%d\n", pa->age);
  pa->swim();
  return 0;
}

void practice() {
  printf("practising the swim.\n");
}

宏定义的预处理替换结果如下表所示:

带参数的宏定义 使用宏的代码 预处理器替换后
#define newAnimal(age) &(Animal){(age), practice} Animal* a = newAnimal( i ); Animal* a = &(Animal){( i ), practice};

validator.c

#include "validator.h"

bool isInRange(Validator* this, int value) {
  RangeValidator* validator = (RangeValidator*) this;
  return (*validator).min > value && validator->max < value;
}

bool isEvenOrElseOdd(Validator* this, int value) {
  OddEvenValidator* validator = (OddEvenValidator*) this;
  if (validator->isEven) { // 偶数
    return value % 2 == 0;
  } else {
    return value % 2 == 1;
  }
}

main.c

#include "validator.h"
#include <stdio.h>

int main() {

    // 测试范围校验器
    RangeValidator rangeValidator = newRangeValidator(1, 10);
    Validator* rangeProxy = &rangeValidator.proxy;

    int inputVal;
    printf("Please input an integer :");
    scanf("%d", &inputVal);
    printf("\n");

    if (rangeProxy->validate(rangeProxy, inputVal)) {
        printf("The integer %d is in the range from 1 to 10.", inputVal);
    } else {
        printf("The integer %d is NOT in the range from 1 to 10.", inputVal);
    }

    // 测试奇偶校验器
    OddEvenValidator evenValidator = newOddEvenValidator(true);
    Validator* evenProxy = &evenValidator.proxy;

    printf("\nPlease input an integer :");
    scanf("%d", &inputVal);
    printf("\n");

    if (evenProxy->validate(evenProxy, inputVal)) {
        printf("The integer %d is an even number.", inputVal);
    } else {
        printf("The integer %d is NOT an even number.", inputVal);
    }
    return 0;
}

本例小节

  1. C语言中没有 class 关键字(那是 C++ 才有的),因此使用结构体及其关键字 struct 表示“类”;
  2. C语言中也没有构造器,带参数的宏(例如 #define newXXX(arg0, arg1) {(arg0) , (arg1)}} 作用类似于 “类的构造器”;
  3. C语言中也没有类的继承,运用结构体内嵌套其他结构体的方法,可以实现“组合”,效果类似于“继承”;
    (例如 typedef struct { Validator proxy; ... } RangeValidator;
  4. C语言中也没有多态,运用函数指针(指向函数的指针)作为结构体的组成部分,通过传入不同的函数,可以实现“多态”的效果;
    (例如 typedef struct { void (* swim)();} Animal;

示例2:Dog继承Animal

转载自 电子发烧友 C语言要如何面向对象编程?,给大家提供一个看起来更友好的版本。

父类Animal

animal.h :

#ifndef ZIYU_LEARN_JVM_C_ANIMAL_H
#define ZIYU_LEARN_JVM_C_ANIMAL_H

// 定义父类结构
typedef struct {
  int age;
  int weight;
} Animal;
// 构造函数声明
void Animal_Init(Animal *this, int age, int weight);
// 获取父类属性声明
int Animal_GetAge (Animal *this);
int Animal_GetWeight (Animal *this);

#endif //ZIYU_LEARN_JVM_C_ANIMAL_H

animal.c :

#include "animal.h"

void Animal_Init(Animal *this, int age, int weight) {
  this->age = age; 
  this->weight = weight;
}

int Animal_GetAge (Animal *this){
  return this->age;
}

int Animal_GetWeight(Animal *this) {
  return this->weight;
}

animal.c 中实现了 animal.h 中声明而未定义的方法。
与C++对比:在C++的方法中,隐含着第一个参数 this 指针。当调用一个对象的方法时,编译器会自动把对象的地址传递给这个指针。
所以,在 animal.h 中函数我们就模拟一下,显示的定义这个this指针,在调用时主动把对象的地址传递给它,这样的话,函数就可以对任意一个 Animal 对象进行处理了。

测试代码:main.c :

#include <stdio.h>
#include "animal.h"

int main() {
  // 在栈上创建一个Animal对象
  Animal a;
  // 构造对象
  Animal_Init(&a, 1, 3);
  printf("age = %d, weight = %d \n", Animal_GetAge(&a), Animal_GetWeight(&a));
  return 0;
}

可以简单的理解为:在栈中有一块空间,存储着a对象;在代码段有一块空间,存储着可以处理Animal对象的函数。

与C++对比:在C++的方法中,隐含着第一个参数this指针。当调用一个对象的方法时,编译器会自动把对象的地址传递给这个指针。
所以,在 animal.h 中函数我们就模拟一下,显示定义this指针,在调用时主动把对象的地址传递给它,这样的话,函数就可以对任意一个 Animal 对象进行处理了。

子类Dog

dog.h

#ifndef ZIYU_LEARN_JVM_C_DOG_H
#define ZIYU_LEARN_JVM_C_DOG_H
#include "animal.h"
// 定义子类结构
typedef struct {
  Animal parent;// 第一个位置放置父类结构
  int legs;// 添加子类自己的属性
} Dog;

// 子类构造函数声明
void Dog_Init(Dog *this, int age, int weight, int legs);

// 子类属性声明
int Dog_GetAge(Dog *this);

int Dog_GetWeight(Dog *this);

int Dog_GetLegs(Dog *this);

#endif //ZIYU_LEARN_JVM_C_DOG_H

dog.c

#include "dog.h"
// 子类构造函数实现

void Dog_Init(Dog *this, int age, int weight, int legs) {
    // 首先调用父类构造函数,来初始化从父类继承的数据
    Animal_Init(&this->parent, age, weight);
    // 然后初始化子类自己的数据
    this->legs = legs;
}

int Dog_GetAge(Dog *this) {
  // age属性是继承而来,转发给父类中的获取属性函数
  return Animal_GetAge(&this->parent);
}

int Dog_GetWeight(Dog *this) {
  return Animal_GetWeight(&this->parent);
}

int Dog_GetLegs(Dog *this) {
  // 子类自己的属性,直接返回
  return this->legs;
}

测试类 main.c :

#include <stdio.h>
#include "dog.h"

int main() {
  // 在栈上创建一个Dog对象
  Dog d;
  Dog_Init(&d, 6, 9, 4);
  printf("age = %d, weight = %d, legs = %d \n", Dog_GetAge(&d), Dog_GetWeight(&d), Dog_GetLegs(&d));

  return 0;
}

在代码段有一块空间,存储着可以处理 Dog 对象的函数;
在栈中有一块空间,存储着d对象;由于 Dog 结构体中的第一个参数是 Animal 对象,所以从内存模型上看,子类就包含了父类中定义的属性。

Dog 的内存模型中开头部分就自动包括了 Animal 中的成员,也即是说 Dog 继承了 Animal 的属性。

利用函数指针(虚函数)实现多态

animal.h

#ifndef ZIYU_LEARN_JVM_C_ANIMAL_H
#define ZIYU_LEARN_JVM_C_ANIMAL_H

struct AnimalVTable; // 父类虚表的前置声明

// 定义父类结构
typedef struct {
  struct AnimalVTable *vptr; // 虚表指针
  int age;
  int weight;
} Animal;

// 父类中的虚表
struct AnimalVTable {
  void (*run)(Animal *this); // 虚函数指针
};
// 父类中实现的虚函数
void Animal_Run(Animal *this);
// 构造函数声明
void Animal_Init(Animal *this, int age, int weight);
// 获取父类属性声明
int Animal_GetAge (Animal *this);
int Animal_GetWeight (Animal *this);

#endif //ZIYU_LEARN_JVM_C_ANIMAL_H

animal.c

#include <assert.h>
#include "animal.h"
// 父类中虚函数的具体实现
static void _Animal_Say(Animal *this) {
  // 因为父类Animal是一个抽象的东西,不应该被实例化。
  // 父类中的这个虚函数不应该被调用,也就是说子类必须实现这个虚函数。
  // 类似于C++中的纯虚函数。
  assert(0);
}

void Animal_Init(Animal *this, int age, int weight) {
  // 首先定义一个虚表
  static struct AnimalVTable animal_vtbl = {_Animal_Say};
  // 让虚表指针指向上面这个虚表
  this->vptr = &animal_vtbl;
  this->age = age;
  this->weight = weight;
}

// 测试多态:传入的参数类型是父类指针
void Animal_Run(Animal *this) {
  // 如果this实际指向一个子类Dog对象,那么this->vptr这个虚表指针指向子类自己的虚表,
  // 因此,this->vptr->run将会调用子类虚表中的函数。
  this->vptr->run(this);
}

int Animal_GetAge (Animal *this){
  return this->age;
}

int Animal_GetWeight(Animal *this) {
  return this->weight;
}

dog.h

#ifndef ZIYU_LEARN_JVM_C_DOG_H
#define ZIYU_LEARN_JVM_C_DOG_H
#include "animal.h"
// 定义子类结构
typedef struct {
  Animal parent;// 第一个位置放置父类结构
  int legs;// 添加子类自己的属性
} Dog;

// 子类构造函数声明
void Dog_Init(Dog *this, int age, int weight, int legs);

// 子类属性声明
int Dog_GetAge(Dog *this);

int Dog_GetWeight(Dog *this);

int Dog_GetLegs(Dog *this);

#endif //ZIYU_LEARN_JVM_C_DOG_H

dog.c

#include <stdio.h>
#include "dog.h"
// 子类中虚函数的具体实现
static void Dog_Run(Animal *this) {
  printf("Dog run");
}

// 子类构造函数实现
void Dog_Init(Dog *this, int age, int weight, int legs) {
  // 首先调用父类构造函数,来初始化从父类继承的数据
  Animal_Init(&this->parent, age, weight);
  // 定义子类自己的虚函数表
  static struct AnimalVTable dog_vtbl = {Dog_Run};
  // 把从父类中继承得到的虚表指针指向子类自己的虚表
  this->parent.vptr = &dog_vtbl;
  // 然后初始化子类自己的数据
  this->legs = legs;
}

int Dog_GetAge(Dog *this) {
  // age属性是继承而来,转发给父类中的获取属性函数
  return Animal_GetAge(&this->parent);
}

int Dog_GetWeight(Dog *this) {
  return Animal_GetWeight(&this->parent);
}

int Dog_GetLegs(Dog *this) {
  // 子类自己的属性,直接返回
  return this->legs;
}

测试类 main.c

#include <stdio.h>
#include "dog.h"

int main() {
  Dog d;
  Dog_Init(&d, 6, 9, 4);
  printf("age = %d, weight = %d, legs = %d \n", Dog_GetAge(&d), Dog_GetWeight(&d), Dog_GetLegs(&d));
  Animal_Run((Animal*) &d);
  return 0;
}

内存模型如下:

对象d中,从父类继承而来的虚表指针vptr,所指向的虚表是dog_vtbl。

参考文档

  1. 电子发烧友 C语言要如何面向对象编程?
  2. 百度文库 C语言实现面向对象编程
  3. waring: excess elements in scalar initializer
  4. 干货 | C语言实现面向对象编程(附代码)
  5. C语言 第八章 函数、指针与宏
posted @ 2022-05-19 10:58  极客子羽  阅读(750)  评论(0编辑  收藏  举报