Step by step analysis: how object-oriented programming in C language

This is Brother Dao's original 009

I. Introduction

In embedded development, the C/C++ language is the most popular. Before the C++11 version, their syntax is relatively similar, except that C++ provides an object-oriented programming method.

Although the C++ language was developed from the C language, today's C++ is no longer an extension of the C language back then. Starting from the 2011 version, it is more like a brand new language.

So I never thought, why did you want to extend C++ in the first place? What are the shortcomings of the C language that led to the emergence of C++?

C++ has solved these problems very well, but with the gradual expansion of language standards, the difficulty of learning C++ language has gradually increased. Not developed several projects, are embarrassed to say that they learned C ++, those left value and right value, templates, template parameters, template arguments variable concept pile, etc., that is not the use of two or three years will be proficient Mastered.

However, the C language also has many advantages:

In fact, the last advantage is the most important: the more people use it, the stronger the vitality. Just like today's society, it is not the survival of the superior, but the survival of the fittest.

In this article, we will talk about how to use object-oriented ideas to program in the C language. Maybe you can not, but it is strongly recommended that you look at the project with, because I had to quit when asked this question twice.

Second, what is object-oriented programming

There is such a formula: program = data structure + algorithm .

Process-oriented programming is generally used in C language, which is to analyze the steps required to solve the problem, and then use the function to call these steps step by step , and process the data structure in the function (execute the algorithm), that is to say, the data structure and algorithm are Separate .

The C++ language encapsulates data and algorithms to form a whole. Whether it is operating its properties or calling its behavior, it is executed through an object. This is the idea of ​​object-oriented programming.

If C language is used to simulate such a programming method, three problems need to be solved:

  1. Data encapsulation
  2. inherit
  3. Polymorphism

The first question: packaging

Encapsulation describes the organization of data, which is to organize all attributes (data) belonging to an object together. The structure type in the C language inherently supports this.

The second question: inheritance

Inheritance describes the relationship between objects. By inheriting the parent class, the subclass automatically owns the properties and behaviors (that is, methods) in the parent class. This problem is not a problem as long as you understand the memory model of the C language. As long as you place a parent structure variable in the position of the first member variable in the subclass structure, then the subclass object inherits the properties of the parent class .

Another point to add: To learn any language, you must understand the memory model!

The third problem: polymorphism

Taken literally, polymorphism means "multiple states" and describes a dynamic behavior . In C++, polymorphism occurs only when virtual functions are called through base class references or pointers, that is to say, polymorphism occurs during runtime, and C++ internally implements polymorphism through a virtual table. Then in C language, we can also follow this idea to achieve.

If a language only supports classes and does not support polymorphism, it can only be said to be object-based, not object-oriented.

Since there is no problem with the idea, let's implement one simply.

Third, first implement a parent class to solve the problem of encapsulation

Animal.h

#ifndef _ANIMAL_H_
#define _ANIMAL_H_

// 定义父类结构
typedef struct {
    int age;
    int weight;
} Animal;

// 构造函数声明
void Animal_Ctor(Animal *this, int age, int weight);

// 获取父类属性声明
int Animal_GetAge(Animal *this);
int Animal_GetWeight(Animal *this);

#endif

Animal.c

#include "Animal.h"

// 父类构造函数实现
void Animal_Ctor(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;
}

have a test:

#include <stdio.h>
#include "Animal.h"
#include "Dog.h"

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

It can be simply understood as: there is a space in the code segment to store the functions that can handle the Animal object; there is a space in the stack to store the a object.

Contrast with C++: In the C++ method, the first parameter this pointer is implied. When calling a method of an object, the compiler will automatically pass the address of the object to this pointer.

Therefore, we simulate the function in Animal.h, define the this pointer displayed, and actively pass the address of the object to it when calling it. In this way, the function can process any Animal object.

Fourth, implement a subclass to solve the problem of inheritance

Dog.h

#ifndef _DOG_H_
#define _DOG_H_

#include "Animal.h"

// 定义子类结构
typedef struct {
    Animal parent; // 第一个位置放置父类结构
    int legs;      // 添加子类自己的属性
}Dog;

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

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

#endif

Dog.c

#include "Dog.h"

// 子类构造函数实现
void Dog_Ctor(Dog *this, int age, int weight, int legs)
{
    // 首先调用父类构造函数,来初始化从父类继承的数据
    Animal_Ctor(&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;
}

have a test:

int main()
{
    Dog d;
    Dog_Ctor(&d, 1, 3, 4);
    printf("age = %d, weight = %d, legs = %d \n", 
            Dog_GetAge(&d),
            Dog_GetWeight(&d),
            Dog_GetLegs(&d));
    return 0;
}

There is a space in the code segment to store functions that can handle Dog objects; there is a space in the stack to store d objects. Since the first parameter in the Dog structure is the Animal object, from the memory model point of view, the subclass contains the attributes defined in the parent class.

The beginning part of Dog's memory model automatically includes the members in Animal, which means that Dog inherits the attributes of Animal.

5. Use virtual functions to solve polymorphic problems

In C++, if a virtual function is defined in a parent class, the compiler will open up a space in this memory to place a virtual table. Each item in this table is a function pointer, and then in the memory of the parent class Put a virtual table pointer in the model to point to the above virtual table.

The above description is not very accurate, it mainly depends on the processing methods of various compilers, but most C++ processors do this, we can think of it as such.

After the subclass inherits the parent class, it will open up a space in the memory to place the subclass's own virtual table, and then let the inherited virtual table pointer point to the subclass's own virtual table.

Since C++ does this, we use C to manually simulate this behavior: creating virtual tables and virtual table pointers.

1. Animal.h is the parent class Animal, add virtual table and virtual table pointer

#ifndef _ANIMAL_H_
#define _ANIMAL_H_

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

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

// 父类中的虚表
struct AnimalVTable{
    void (*say)(Animal *this); // 虚函数指针
};

// 父类中实现的虚函数
void Animal_Say(Animal *this);

#endif

2. Animal.c

#include <assert.h>
#include "Animal.h"

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

// 父类构造函数
void Animal_Ctor(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_Say(Animal *this)
{
    // 如果this实际指向一个子类Dog对象,那么this->vptr这个虚表指针指向子类自己的虚表,
    // 因此,this->vptr->say将会调用子类虚表中的函数。
    this->vptr->say(this);
}

> A virtual function table animal_vtbl is defined in the stack space. Each item in this table is a function pointer. For example, the function pointer say points to the function _Animal_Say() in the code segment. > The first member vptr of object a is a pointer to this virtual function table animal_vtbl.

3. Dog.h remains unchanged

4. Define the subclass's own virtual table in Dog.c

#include "Dog.h"

// 子类中虚函数的具体实现
static void _Dog_Say(Dog *this)
{
    printf("dag say \n");
}

// 子类构造函数
void Dog_Ctor(Dog *this, int age, int weight, int legs)
{
    // 首先调用父类构造函数。
    Animal_Ctor(&this->parent, age, weight);
    // 定义子类自己的虚函数表
    static struct AnimalVTable dog_vtbl = {_Dog_Say};
    // 把从父类中继承得到的虚表指针指向子类自己的虚表
    this->parent.vptr = &dog_vtbl;
    // 初始化子类自己的属性
    this->legs = legs;
}

5. Test it

int main()
{
    // 在栈中创建一个子类Dog对象
    Dog d;  
    Dog_Ctor(&d, 1, 3, 4);

    // 把子类对象赋值给父类指针
    Animal *pa = &d;
    
    // 传递父类指针,将会调用子类中实现的虚函数。
    Animal_Say(pa);
}

The memory model is as follows:

In the object d, the virtual table pointer vptr inherited from the parent class, the virtual table pointed to is dog_vtbl.

When executing Animal_Say(pa) , although the parameter type is a pointer to the parent class Animal , the actual pa passed in is an object pointing to the subclass Dog , and the virtual table pointer vptr in this object points to itself in the subclass The defined virtual table dog_vtbl, the function pointer say in this virtual table points to the redefined virtual function _Dog_Say in the subclass , so this->vptr->say(this) finally calls the function _Dog_Say.

Basically, the object-oriented development idea in C is the above.
This code is very simple, just tap it yourself. If you want to be lazy, please leave a message in the background and I will send it to you.

Six, the use of C object-oriented ideas in the project

1. Linux kernel

Take a look at several structures about sockets:

struct sock {
    ...
}

struct inet_sock {
    struct sock sk;
    ...
};

struct udp_sock {
    struct sock sk;
    ...
};

Sock can be regarded as a parent class. The first members of inet_sock and udp_sock are all sock types, which is equivalent to inheriting all the attributes in sock from the memory model.

2. glib library

Take the simplest string processing function as an example:

GString *g_string_truncate (GString *string, gint len)
GString *g_string_append (GString *string, gchar *val)
GString *g_string_prepend (GString *string, gchar *val)

The first parameter of the API function is a pointer to a GString object, which points to the string object that needs to be processed.

GString *s1, *s2;
s1 = g_string_new("Hello");
s2 = g_string_new("Hello");

g_string_append(s1," World!");
g_string_append(s2," World!");

3. Other items

There are also some projects, although from the point of view of the function parameters, it seems that they are not object-oriented, but from the point of view of the design of the data structure, they are also object-oriented ideas, such as:

Modbus protocol open source library libmodbus is
used for home automation wireless communication protocol ZWave
Long ago Qualcomm mobile phone development platform BREW


[Original Statement]

OF: Columbia Road (public No.: The IOT of town things )

know almost: Columbia Road

station B: Share Columbia Road

Denver: Columbia Road share

CSDN: Columbia Road Share

If you think the article is good, please forward it and share it with your friends.


I will summarize and share the actual combat experience of projects in embedded development for more than ten years , I believe you will not be disappointed!

Reprint: Welcome to reprint, but without the consent of the author, this statement must be retained, and the original link must be given in the article.

Recommended reading

[1] It turns out that the underlying debugging principle of gdb is so simple.
[2] The double buffering technology in the producer and consumer mode.
[3] In- depth LUA scripting language, so that you can thoroughly understand the principle of debugging

Guess you like

Origin blog.csdn.net/u012296253/article/details/111450689
Recommended