封装
所谓的封装,其实就是将多个关联数据打包到一起,作为一个整体来看,也就是所谓的对象
对象是什么?对象是某个具体类型的一个实例化
那在C语言里面,我们有没有什么对关键数据打包的技术么?答案肯定是有
数据打包利器:
struct
, union
既然结构体或者共用体实例化就是对象,那如何面向这个对象编程呢?
不如我们遵从一个约定:针对某个对象的的操作函数,我们始终把这个对象放在参数的第一个位置
struct Student {
const char *name;
int number;
};
void Student_Init(Student *stu, const char *name, int number);
void Student_Print(const Student *stu);
为什么呢?
这么说吧,如果针对某个对象的操作,不需要任何额外参数,那我们的第一个参数必定就是我们的对象参数,因为参数就它一个
void Student_Study(Student *stu);
那如果还有额外参数呢?那我们保持良好的习惯,始终在对象参数的后面增加其他参数,这样就能保持第一个参数始终都是我们的对象参数了
void Student_Play(Student *stu, int minutes); // 学生玩耍几分钟
void Student_Test(Student *stu, int classroom, int kind); // 学生在教室考试
而这些函数所做任何操作,结果都会体现在两处:
- 第一个对象参数
- 返回值或者输出参数
慢慢的我们会形成一种思想,我们的接口函数,都是针对某个对象的,而这个对象,就是第一个参数
实际上,C++就是这么干的,只不过C++替我们做了第一个参数的自动传递,这个参数名字叫:this
继承
C里面没有继承
但我们可以换一种思路:继承的是什么?
首先,继承的肯定有基类的数据成员,然后是基类的操作接口
不如我们这样做:
struct Animal {
// ...
};
void Animal_Bark(const Animal *animal);
在定义一个新结构体来扩展某个现有结构体的功能时,将已有结构体作为新结构体的第一个成员,并命名为parent或者base或者super等表示基类字段
然后我们的其他新增的字段都往下定义
struct Cat {
struct Animal parent;
int age;
};
void Cat_Bark(const Cat *cat);
为什么这样做呢?
因为这样可以保证这个结构的实例化对象的地址,既可以当做新结构体来用,也可以当成基类的结构体来用,因为他们的地址是一样的🤪
Cat xiaohei;
Cat_Bark(&xiaohei);
Animal_Bark((const Animal *)&cat); // 首地址一样,没有问题
Animal_Bark(&cat.parent); // 直接引用parent的地址,也没有问题
这样是不是就可以用“继承”后的新结构体的对象作为参数调用基类的接口了😎
是不是很牛逼
多态
多态,实际上就是函数名一样,基类和子类却干着不同的勾当🧐,没错吧
C里面可以用啥来这么干呢?
嘿嘿,函数指针也可以
shape.h
struct Shape {
double (*area)(const Shape *shape);
};
Shape *Shape_New();
double Shape_Area(const Shape *shape);
void Shape_Delete(Shape *shape);
shape.c
static double Shape_CalcArea(const Shape *shape)
{
// ...
}
Shape *Shape_New()
{
Shape *shape = (Shape *)malloc(sizeof(struct Shape));
if (shape != NULL) {
shape->area = Shape_CalcArea;
}
return shape;
}
double Shape_Area(const Shape *shape)
{
return shape->area(shape);
}
void Shape_Delete(Shape *shape)
{
free(shape);
}
我们可以定义一个结构体,成员里面放上一个函数指针(这个函数指针所对应的函数也要保持第一个参数是当前结构体对象的指针)
在基类的初始化过程中,我们把基类操作的具体函数赋值给这个函数指针,在子类结构体的初始化里面,我们偷偷把这个函数指针指向子类的新函数
rect.h
struct Rect {
struct Shape parent;
int width;
int height;
};
Rect RectMake(int width, int height);
rect.c
static double Rect_Area(const Rect *rect)
{
return rect->width * rect->height;
}
Rect RectMake(int width, int height)
{
Rect rect = { .parent = { .area = (double (*)(const Shape *shape))Rect_Area, .width = width, .height = height};
return rect;
}
然后这个结构体在使用的时候,我们随便调用这个函数指针,实际上基类和子类调用的具体函数实现也不是同一个,相当于用另类方式实现了多态
Rect r = RectMake(1024, 768);
double area = Shape_Area(&r.parent);
// 或者
double area = Shape_Area((const Shape *)&r);
// 或者
double area = r.parent.area(&r.parent);
// 或者
double area = r.parent.area((const Shape *)&r);
泛型
泛型,在C++里面也称为模板
在C里面能用吗?
直接用当然没办法,但我们也可以换一种思路呀
模板类型包括哪三个要素?
或者换个说法:一个具体的对象包括哪三要素?
地址,大小,与类型
在C++里面体现为:
地址,大小,构造函数
那么我们可以使用一个通用的字段表示地址,然后一个字段表示大小,至于构造函数,我们在写具体的对象初始化的时候基本上都会写
地址我们用void *
大小我们用sizeof的返回类型size_t
typedef struct Vector {
size_t size;
size_t length;
size_t capacity;
uint8_t data[0];
} Vector;
Vector * Vector_New(size_t capacity, size_t item_size)
{
Vector *vector = (Vector *)calloc(1, sizeof(Vector) + capacity * item_size);
if (vector != NULL) {
vector->length = 0;
vector->size = item_size;
vector->capacity = capacity;
}
return vector;
}
void Vector_Delete(Vector *vector)
{
free(vector);
}
void Vector_Push(Vector *vector, const void *item)
{
if (vector->length == vector->capacity) {
return;
}
memcpy(vector->data + vector->length * vector->size, item, vector->size);
vector->length++;
}
void *Vector_At(Vector *vector, size_t index)
{
if (vector->length <= index) {
return NULL;
}
return vector->data + vector->size * index;
}
void Vector_Set(Vector *vector, size_t index, const void *item)
{
if (vector->length <= index) {
return;
}
memcpy(vector->data + vector->size * index, item, vector->size);
}
使用如下:
int main()
{
Vector *vec = Vector_New(5, sizeof(int)); // 5个int的存储空间
int number = 1;
Vector_Push(vec, &number); // [1]
number = 2;
Vector_Push(vec, &number); // [1,2]
number = 3;
Vector_Push(vec, &number); // [1,2,3]
//...
int a = *(int *)Vector_At(vec, 2); // 3 for vec[2] == 3
number = 9;
Vector_Set(vec, 0, &number); // vec[0] = 9;
a = *(int *)Vector_At(vec, 0); // 9 for vec[0] == 9
Vector_Delete(vec);
return 0;
}
所以这里我们用地址加大小,就可以表示一个模板参数了,只不过我们用的时候必须要自己有所意识:当前用的这个对象是哪个类型?
当我们写出了一些包含了地址和大小的接口函数时,我们其实提供的是一个相当于模板类型的接口,比如标准库的write接口,我们可以传各种结构体的地址,然后传递sizeof的结果作为大小,这样我们几乎可以对任何类型做相同的操作
常见的有结构体字节信息,管理结构体的内存分配,释放与拷贝等等