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 的例子中会讲。
- 如果不明白 struct 语法的,可以 移步 C语言结构体详解
- 如果不明白 typedef struct 语法的,可以 移步 C语言中typedef和指针连用实用讲解
本文准备详细讲解的是宏定义的这段语法:
首先,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;
}
本例小节
- C语言中没有
class
关键字(那是 C++ 才有的),因此使用结构体及其关键字struct
表示“类”; - C语言中也没有构造器,带参数的宏(例如
#define newXXX(arg0, arg1) {(arg0) , (arg1)}
} 作用类似于 “类的构造器”; - C语言中也没有类的继承,运用结构体内嵌套其他结构体的方法,可以实现“组合”,效果类似于“继承”;
(例如typedef struct { Validator proxy; ... } RangeValidator;
) - 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。