Như chúng ta đã biết thì C không phải là một một ngôn ngữ dành cho lập trình hướng đối tượng (OOP); nó là ngôn ngữ lập trình thủ tục POP (Procedure Oriented Programming). Tuy nhiên, bạn vẫn có thể triển khai một số khái niệm của OOP trong C thông qua một số kỹ thuật nhất định. Bài viết này nhằm mục đính khám phá và tìm hiểu các đặc điểm nổi bật của OOP, cũng như hiểu rõ được các lợi ích, tiện lợi của nó.
Bạn có thể tham khảo khái niệm OOP qua bài viết này
1. Tổng Quan
Cùng xem thử làm cách nào mà C có thể mô phỏng hành vi giống OOP thông qua việc triển khai struct, function pointer, header row và macro
- TÍnh đóng gói (Encapsulation): có thể thực hiện đóng gói bằng
struct
vàcon trỏ hàm
, sử dụngheader
để ẩn cho tiết triển khai - Tính Kế Thừa (Inheritance): có thể mô phỏng bằng cách nhúng các
structures
trong cácstructures
- Tính đa hình (Polymorphism): có thể sử dụng con trỏ hàm cho các hàm ảo
- Tính trừu tượng (Abstraction): được mô phỏng bằng các function prototypes và interfaces
- Ngoài ra có thể duy trì tính module bằng các tệp C riêng biệt cho các
classes
hoặc chức năng khác nhau
1.1 Sử dụng Structs
Header File
và Function pointer
để mô phỏng tính đóng gói
Bạn có thể đạt được sự đóng gói bằng cách định nghĩa các structure
trong các tệp tiêu đề và sau đó cung cấp các hàm hoạt động trên các cấu trúc đó. Bằng cách giữ các chi tiết triển khai ẩn trong têp nguồn (.c) và chỉ hiển thị giao dienej thông qua tệp tiêu đề, bạn đạt được được một số cấp độ ẩn dữ liệu, đây là nguyên tắc cốt lõi của OOP.
Example: Đóng gói với một Cả "Class" Bạn có thể định nghĩa một cấu trúc cho Car và sau đó cung cấp các hàm để hoạt động trên cấu trúc đó
car.h (Header File)
#ifndef CAR_H
#define CAR_H typedef struct Car { int speed; int fuel;
} Car; // "Constructor" function to initialize a Car
Car* Car_create(int speed, int fuel); // Member functions to operate on the Car
void Car_accelerate(Car* car);
void Car_brake(Car* car);
void Car_refuel(Car* car, int fuelAmount); #endif // #define CAR_H
car.c (triển khai)
#include <stdio.h>
#include <stdlib.h>
#include "car.h" Car* Car_create(int speed, int fuel) { Car* newCar = (Car*) malloc(sizeof(Car)); newCar->speed = speed; newCar->fuel = fuel; return newCar;
} void Car_accelerate(Car* car) { if (car->fuel > 0) { car->speed += 10; car->fuel -= 1; printf("Car is accelerating. Speed: %d, Fuel: %d\n", car->speed, car->fuel); } else { printf("Not enough fuel to accelerate!\n"); }
} void Car_brake(Car* car) { if (car->speed > 0) { car->speed -= 10; printf("Car is braking. Speed: %d\n", car->speed); } else { printf("Car is already stopped.\n"); }
} void Car_refuel(Car* car, int fuelAmount) { car->fuel += fuelAmount; printf("Car refueled. Fuel: %d\n", car->fuel);
}
Ở đây, cấu trúc Car tương tự như một Class
và các hàm hoạt động trên cấu trúc Cả mô phỏng các hàm thành viên. Chi tiết về cách Car hoạt động được đóng gói trông tệp .c, trong khi tệp .h cung cấp giao diện public
1.2 Sử dụng Structure
cho tính kế thừa
Mặc dù C không có tính kế thừa gốc, bạn có thể mô phỏng nó bằng cách nhúng một structure
vào một structure
khác. Điều này tương tự như việc tạo một Class
cơ sở và mở rộng nó bằng một Class
dẫn xuất.
Example:
typedef struct Vehicle { int speed; int fuel;
} Vehicle; typedef struct Car { Vehicle base; // "Inherit" from Vehicle int doors;
} Car; void Vehicle_accelerate(Vehicle* v) { v->speed += 10;
} void Car_accelerate(Car* c) { Vehicle_accelerate(&(c->base)); // Call base "class" function
}
Ở đây, cấu trúc Car chứa cấu trúc Vehicle, mô phỏng sự kế thừa. Function Car_accelerate
tái sử dụng chức năng từ hàm Vehicle_accelerate
, chứng minh cách một Class
được dẫn xuất có thể mở rộng hành vi của lớp cơ sở.
1.3 Sử dụng Con trỏ hàm cho tính đa hình
C cũng có thể mô phỏng đa hình bằng con trỏ hàm. Bạn có thể tạo cấu trúc với con trỏ hàm để mô phỏng các hàm ảo, cho phép các Class
(Structure) khác nhau cùng một giao diện theo cách khác nhau
Example : animal.h
#ifndef ANIMAL_H
#define ANIMAL_H typedef struct Animal { void (*speak)(struct Animal*); // Function pointer to a "virtual function"
} Animal; void Animal_speak(Animal* animal); // Call the speak function (virtual function)
#endif
animal.c
#include "animal.h"
#include <stdio.h> void Animal_speak(Animal* animal) { if (animal->speak) { animal->speak(animal); // Call the "virtual function" }
}
dog.h
#ifndef DOG_H
#define DOG_H #include "animal.h" typedef struct Dog { Animal base; // Inherit from Animal
} Dog; Dog* Dog_create();
void Dog_speak(Animal* animal); #endif
dog.c
#include <stdio.h>
#include <stdlib.h>
#include "dog.h" void Dog_speak(Animal* animal) { printf("Woof! Woof!\n");
} Dog* Dog_create() { Dog* dog = (Dog*) malloc(sizeof(Dog)); dog->base.speak = Dog_speak; // Set the virtual function pointer return dog;
}
Bây giờ, cấu trúc Dog mô phỏng một lớp dẫn xuất ghi đè hàm speak
. Bạn có thể mở rộng mẫu này cho các loài động vật khác như Cat, một lời có triển khai speak
riêng.
Ví dụ Sử dụng:
#include <stdio.h>
#include "dog.h" int main() { Dog* dog = Dog_create(); Animal_speak((Animal*) dog); // Call the "speak" function polymorphically free(dog); return 0;
}
Trong trường hợp này, tính đa hình đạt được bằng cách sử dụng các con trỏ hàm, trong đó Dog triển khai speak
riêng, nhưng vẫn có thể được gọi thông qua hàm Animal_speak
theo cách đa hình.
1.4 Sử dụng Macros để mô phỏng Method
Bạn có thể sử dụng macros
trong C để tạo lối tắt cho việc truy cập Method
trên một cấu trúc, mô phỏng các phương thức lớp trong C++
Example:
#define Car_accelerate(car) Car_accelerateImpl(&(car)) // Macro for "method" call void Car_accelerateImpl(Car* car) { car->speed += 10;
}
Ở đây, macro Car_accelerate
là cách viết tắt để gọi hàm Car_accelerateImpl
, hàm này lấy một con trỏ đến đối tượng Car. Nó mô phỏng cú pháp giống như phương pháp.
1.5 Ví dụ cấu trúc
project/
│
├── src/ # Source files
│ ├── car.c # Implementation of Car class
│ ├── vehicle.c # Implementation of Vehicle class
│ ├── airplane.c # Implementation of Airplane class
│ └── main.c # Entry point
│
├── include/ # Header files (interfaces)
│ ├── car.h # Interface for Car class
│ ├── vehicle.h # Interface for Vehicle class
│ └── airplane.h # Interface for Airplane class
│
├── build/ # Compiled output (binary, object files, etc.)
└── Makefile # Build configuration
2. So sánh sự khác biệt giữa OOP trong C++ và mô phỏng OOP trong C
Trong khi cách tiếp cận này có thể mô phỏng OOP, nó thiếu đi những cú pháp dễ hiểu và tính an toàn của C++. Bạn cần xử lý nhiều khía cạnh, chăng hạn như quản lý bộ nhớ và đảm bảo các con trỏ hàm chính xác
2.1 Hỗ trợ OOP gốc
C++: được thiết kế với OOP trong tâm trí và hỗ trợ trực tiếp các lớp, kế thừa, đa hình, đóng gói và trừu tượng hóa. Các tính năng này được tích hợp vào cú pháp ngôn ngữ.
C: không được thiết kế cho OOP. Bạn cần mô phỏng OOP thủ công bằng các cấu trúc, con trỏ hàm và tổ chức mã cẩn thận.
2.2 Classes vs Structs
C++ có từ khóa Class
tích hợp cho phép định nghĩa các đối tượng với dữ liệu được đóng gói (members) và phương thức (member function). Các lớp trong C++ đi kèm với các tính năng như kiểm soát truy cập (public, private, protected), constructors/ destructors và kế thừa
class Car { private: int speed; public: Car() { speed = 0; } void accelerate() { speed += 10; } };
Trong C, bạn sử dụng struct
để biểu diễn các đối tượng và bạn phải xác định hành vi thông qua các hàm bên ngoài, điều này ít tự nhiên hơn và đòi hỏi nhiều quản lý thủ công hơn (ví dụ: con trỏ hàm cho tính đa hình)
typedef struct Car { int speed; } Car; void accelerate(Car* car) { car->speed += 10; }
2.3 Encapsulation (Data Hiding)
- C++: cung cấp tính năng đóng gói bằng cách cho phép các biến thành viên và hàm được khai báo với các chỉ định truy cập khác nhau
private
,protected
,public
. Điều này thực thi việc ẩn dữ liệu nghiêm ngặt
class Car { private: int speed; public: void setSpeed(int s) { speed = s; } };
- C: Không có cơ chế gốc rễ nào để kiểm soát truy cập trong C. Bạn có thể mô phỏng đóng gói bằng cách tách các khai báo trong tệp tiêu đề và định nghĩa trong tệp nguồn, nhưng bạn không thể thực sự ngăn chặn quyền truy cập vào dữ liệu nội bộ.
typedef struct Car { int speed; // No way to make this private
} Car; // You have to rely on conventions or manual enforcement.
2.4 Constructor và Destructor
- C++: cung cấp các hàm tạo và hàm hủy tự động khi bạn định nghĩa một lớp, xử lý việc khởi tạo và dọn dẹp đối tượng. Trình biên dịch cũng cung cấp một hàm tạo mặc định nếu không có hàm tạo nào được định nghĩa rõ ràng
class Car { public: Car() { /* Constructor */ } ~Car() { /* Destructor */ }
};
- C: không có hàm tạo hoặc hủy. Bạn phải tạo và khởi tạo thủ công các đối tượng bằng các hàm chuyên dụng và giải phóng bộ nhớ bằng
free()
cho bộ nhớ được phân bổ động.
Car* createCar() { Car* car = (Car*) malloc(sizeof(Car)); car->speed = 0; return car;
} void destroyCar(Car* car) { free(car);
}
2.5 Tính kế thừa (Inheritance)
- C++: Kế thừa được hỗ trợ trong C++ bằng cú pháp
:
. Bạn có thể dễ dàng lấy một lướp từ một lớp cơ sở và ghi đè các hàm thành viên, hưởng lợi từ cả kế thừa giao diện và kế thừa triển khai.
class Vehicle { public: void move() { /* ... */ } }; class Car : public Vehicle { public: void move() { /* Overridden method */ } };
- C: Không có hỗ trợ trực tiếp cho việc kế thừa. Bạn phải mô phỏng thủ công bằng cách sử dụng thành phần (tức là nhúng một
struture
vào mộtstructure
khác
typedef struct Vehicle { void (*move)(struct Vehicle*); } Vehicle; typedef struct Car { Vehicle base; // Composition to simulate inheritance } Car;
2.6 Tính đa hình (Polymorphism - Virtual Functions)
- C++: hỗ trợ đa hình thông qua
virtual function
vàinheritance
. Bạn có thể khai báo cácmethods
làvirtual
trong lớp cơ sở, cho phép các lớp dẫn xuất ghi đè chúng. Khi chạy, phương thức thích hợp được gọi trên loại đối tượng thực tế
class Vehicle { public: virtual void move() { /* Default behavior */ } }; class Car : public Vehicle { public: void move() override { /* Car-specific behavior */ } };
- C: Bạn cần mô phỏng đa hình với các con trỏ hàm trong struct. Điều này đòi hỏi phải xử lý thủ công và nỗ lực quản lý nhiều hơn.
typedef struct Vehicle { void (*move)(struct Vehicle*); } Vehicle; typedef struct Car { Vehicle base; } Car; void Car_move(Vehicle* v) { /* Car-specific move */ }
2.7 Nạp chồng toán từ (Operator Overloading)
- C++: cho phép định nghĩa lại các hành vi của toán tử như (
+
,-
,*
, v.v) cho các k
class Complex { public: int real, imag; Complex operator+(const Complex& c) { return Complex(real + c.real, imag + c.imag); }
};
- C: không hỗ trợ điều này, nghĩa là bạn phải định nghĩa các hàm có tên rõ ràng (ví dụ:
addComplex()
) để xử lý các thao tác tùy chỉnh.
typedef struct Complex { int real, imag; } Complex; Complex addComplex(Complex a, Complex b) { Complex result = {a.real + b.real, a.imag + b.imag}; return result; }
2.8 Templates
- C++: hỗ trợ
Templates
, cho phép lập trình chung. Điều này có nghĩa là bạn có thể viết một hàm hoặc lớp hoạt động với bất kỳ kiểu dữ liệu nào, tăng khả năng tái sử dụng mã và tính linh hoạt.
template<typename T>
T add(T a, T b) { return a + b;
}
- C: không có mẫu hoặc kiểu chung. Bạn phải sử dụng macro (có giới hạn) hoặc tạo các hàm riêng cho từng kiểu dữ liệu, điều này có thể dẫn đến trùng lặp mã.
#define add(a, b) ((a) + (b)) // Using macros, but not type-safe
2.9 Quản lý tài nguyên tự động (RAII)
- C++: sử dụng RAII (Resource Acquisition Is Initialization). Khi một đối tượng được tạo, các tài nguyên (như bộ nhớ, xử lý tệp, v.v) sẽ được thu thập tự và giải phóng khi đối tượng nằm ngoài phạm vi (thông qua các hàm Destrcutors). Điều này làm giảm đáng kể nguy cơ rò rỉ bộ nhớ.
class File { public: FILE* handle; File(const char* filename) { handle = fopen(filename, "r"); } ~File() { fclose(handle); } // Destructor automatically frees resources };
- C: Bạn phải quán lý tài nguyên thủ công (mở/ đóng tệp, Phân bổ/ giải phóng bộ nhớ động) bắng cách sử dụng các lệnh gọi hàm rõ ràng. Rò rỉ bộ nhớ và quản lý tài nguyên kém phổ biến hơn vì không có chức năng tự động
FILE* file = fopen("filename", "r"); // Must remember to close the file later fclose(file);
2.10 Thư viện hỗ trợ
-
C++: Thư viện chuẩn C++ cung cấp hỗ trợ tích hợp cho các tính năng OOP như container (
std::vector
,std::map
), thuật toán, v.v. Thư viện này cũng bao gồm các tính năng như con trỏ thông minh để quản lý bộ nhớ an toàn. -
C: Thư viện chuẩn của C bị hạn chế hơn, tập trung vào các hoạt động cấp thấp như quản lý bộ nhớ (
malloc
,free
), thao tác chuỗi và I/O cơ bản. Đối với các cấu trúc dữ liệu và thuật toán nâng cao, bạn cần xây dựng chúng theo cách thủ công hoặc sử dụng các thư viện của bên thứ ba.
3. Tổng Kết
- Cú pháp: C++ có hỗ trợ gốc cho OOP, trong khi C yêu cầu mô phỏng thủ công.
- Tính Đóng gói: C++ có kiểm soát truy cập tích hợp (
private
,public
), trong khi C dựa vào các quy ước. - Tính Kế thừa: C++ hỗ trợ kế thừa thực sự, trong khi C yêu cầu nhúng cấu trúc.
- Tính Đa hình: C++ có các hàm ảo cho đa hình thời gian chạy, trong khi C sử dụng các con trỏ hàm.
- Quản lý bộ nhớ: C++ hỗ trợ quản lý tài nguyên tự động (RAII), trong khi C yêu cầu xử lý bộ nhớ và tài nguyên thủ công.
- Quá tải toán tử và mẫu: C++ hỗ trợ cả hai, trong khi C thiếu các tính năng này.
Tóm lại, mặc dù bạn có thể mô phỏng OOP trong C, C++ cung cấp một bộ công cụ phong phú và thanh lịch hơn nhiều cho lập trình OOP, giúp việc triển khai các thiết kế hướng đối tượng phức tạp, có thể tái sử dụng và bảo trì dễ dàng và an toàn hơn nhiều. So sánh thì C thủ công hơn và dễ xảy ra lỗi hơn.