Understanding how to implement encapsulation, inheritance, and polymorphism in C#
1. Introduction#
Encapsulation, inheritance, and polymorphism are important features of object-oriented programming. They allow us to make our code more cohesive, less coupled, and increase its safety, readability, and maintainability when combined with some design patterns. So, can we implement encapsulation, inheritance, and polymorphism in C, a procedural programming language, to improve the quality of our code? The answer is obvious. In the process of learning the Linux kernel code, we can analyze it carefully and find that it extensively uses C to implement encapsulation, inheritance, and polymorphism. This article will analyze how to implement these object-oriented features in C, laying the foundation for reading and writing high-quality code.
2. Encapsulation#
Encapsulation is the process of packaging the properties and corresponding methods of an abstract object into a class and changing the internal state through internal methods. The essence of encapsulation is the process of hiding information, making the internal state of an object not directly accessible or modifiable by the outside world.
Encapsulation has the following advantages:
- It improves the safety of the code by allowing data to be accessed only in specified ways, avoiding misuse and illegal access.
- It improves code reusability by allowing the same or similar data types to be used multiple times.
- It improves code maintainability by only requiring modifications in one place when the data type changes.
In C, there is no concept of classes, but we can use structures to implement encapsulation. The important significance of encapsulation is that it integrates functions (methods) and data (attributes) together. This way, we can access all the data and operate all the functions through a simple structure pointer.
Here is a specific example of encapsulation:
#include <stdio.h>
typedef struct human
{
int age;
char sex;
void (*set_age)(struct human *p, int age);
int (*get_age)(struct human *p);
void (*set_sex)(struct human *p, char sex);
char (*get_sex)(struct human *p);
} Human;
void set_age(Human *p, int age)
{
p->age = age;
}
int get_age(Human *p)
{
return p->age;
}
void set_sex(Human *p, char sex)
{
p->sex = sex;
}
char get_sex(Human *p)
{
return p->sex;
}
int main()
{
Human p;
p.set_age = set_age;
p.set_age(&p, 18);
p.set_sex = set_sex;
p.set_sex(&p, 'M');
p.get_age = get_age;
printf("age: %d\n", p.get_age(&p));
p.get_sex = get_sex;
printf("sex: %c\n", p.get_sex(&p));
return 0;
}
We defined a structure called "human" that contains the members "age" and "sex", as well as the functions "set_age", "get_age", "set_sex", and "get_sex" for setting and getting the age and sex. This is encapsulation. The data and functions of the structure can implement the attributes and method operations of a human. In addition, the structure's data can only be modified through the operation functions in the structure.
3. Inheritance#
Inheritance is the process of creating a new class (subclass or derived class) based on an existing class (parent class or base class). The subclass or derived class can access the data and functions of the parent class, thus avoiding duplicate code. The subclass can also add its own properties and data.
Inheritance has the following advantages:
- It improves code reusability by avoiding duplicate code.
- It improves code extensibility by supporting adjustments based on existing classes.
- It improves code readability by making the code more concise and clear.
In C, we can implement class inheritance (single inheritance, without considering multiple inheritance) through nested structures, but we need to ensure that the reference to the parent class structure is placed in the first position of the child class structure member. This way, there will be no problems with data access or type casting.
Here is a specific example of inheritance:
#include <stdio.h>
#include <stdlib.h>
typedef struct human {
int age;
char sex;
} Human;
typedef struct person{
Human human;
char *name;
} Person;
Person* create_person(int age, char sex, char *name) {
Person* cperson = (Person*) malloc(sizeof(Person));
cperson->human.age = age;
cperson->human.sex = sex;
cperson->name = name;
return cperson;
}
int main() {
Person* cperson = create_person(18, 'w', "lucy");
printf("(%d, %c) - name: %s\n", cperson->human.age, cperson->human.sex, cperson->name);
free(cperson);
return 0;
}
In the above code, we defined two structures, "Human" and "Person". "Person" contains the "Human" structure and the member variable "name". We construct a "Person" structure using the "create_person()" function and assign values to its "Human" member and "name" member. When we need to use the features of inheritance elsewhere, we can use a similar nested structure approach to implement it.
4. Polymorphism#
Polymorphism is the most important concept in object-oriented programming. It allows us to perform the same operation on different objects, thus achieving flexible operation processing.
Polymorphism has the following advantages:
- It improves code extensibility by supporting different types of objects, making the program more flexible.
- It improves code maintainability by only requiring modifications to the corresponding classes when types change.
- It improves code readability by making the code more concise and easy to read.
In C, we can implement polymorphism by using function pointers and using the same interface to handle different data. Functions with different functionalities can use the same function name, allowing different functionalities to be called with one function name.
Here is a specific example of polymorphism:
#include <stdio.h>
typedef struct shape {
void (*draw)(struct shape*);
} Shape;
typedef struct {
int x;
int y;
int radius;
Shape base;
} Circle;
typedef struct {
int x1;
int y1;
int x2;
int y2;
Shape base;
} Line;
void drawCircle(Shape* shape) {
Circle* circle = (Circle*) shape;
printf("Circle at (%d, %d) with radius %d\n", circle->x, circle->y, circle->radius);
}
void drawLine(Shape* shape) {
Line* line = (Line*) shape;
printf("Line from (%d, %d) to (%d, %d)\n", line->x1, line->y1, line->x2, line->y2);
}
int main() {
int i;
Circle circle = {
.x = 1,
.y = 5,
.radius = 10,
.base = { .draw = drawCircle }
};
Line line = {
.x1 = 2,
.y1 = 3,
.x2 = 7,
.y2 = 9,
.base = { .draw = drawLine }
};
/* //The above two code segments are equivalent to the following operations
Circle circle;
circle.x = 1;
circle.y = 5;
circle.radius = 10;
circle.base.draw = drawCircle;
Line line;
line.x1 = 2;
line.y1 = 3;
line.x2 = 7;
line.y2 = 9;
line.base.draw = drawLine;
*/
Shape* shapes[2];
shapes[0] = (Shape*)&(circle.base);
shapes[1] = (Shape*)&(line.base);
for (i = 0; i < 2; i++) {
shapes[i]->draw(shapes[i]);
}
return 0;
}
In the above example, we defined a "Shape" structure and its member function "draw". We also defined two child structures, "Circle" and "Line", derived from "Shape", and implemented their own "draw" functions.
By declaring the "draw" function as a function pointer in "Shape" that points to a function with a "void*" parameter, we can dynamically handle different types of child structures in the "draw" function.
We used "typedef" to name the structures as "Shape" so that we can use them in the "Circle" and "Line" structures. Finally, we created a "Circle" and a "Line" object in the main function and stored them in an array of type "Shape*". We used a loop to iterate through the array and call their own "draw" functions for processing, achieving the effect of polymorphism.