+1

Con Trỏ Hàm - C/C++

Hàm Con Trỏ (Function Pointer) là gì?

  • Hàm con trỏ trong lập trình đề cập đến:
    • Con trỏ tới hàm (A pointer to a function): Đây là biến lưu trữ địa chỉ hàm mà sau này có thể gọi bằng con trỏ này.
    • Hàm trả về một con trỏ (Function returning Pointers): Đây là hàm trả về địa chỉ bộ nhớ trỏ đến một biến hoặc cấu trấu dữ liệu

A pointer to function

Định nghãi và cách sử dụng

Con trỏ tới một hàm cho phép bạn tự động quyết định hàm nào sẽ được gọi trong thời gian chạy, thay vì thời gian biên dịch. Điều này mang lại sự linh hoạt và có thể được sử dụng để triển khai các lệnh gọi lại (callbacks), trình xử lý sự kiện (event handlers), và để triển khai các cấu trúc mã trừu tượng hơn như máy trạng thái (State Machine)


#include <stdio.h>

// khai báo một hàm đơn giản để được trỏ tới
void sayHello(){
    printf("Hello World");
}

// một hàm khác cũng sẽ được trỏ tới
void sayGoodbye(){
    printf("Goodbye, World!\n");
}

int main(){
    // khai báo một con trỏ hàm void
    void (*funcPtr)();
    
    // trỏ tới hàm sayHello
    funcPtr = sayHello;
    // gọi hàm thông qua con trỏ
    funcPtr(); // -> in ra: Hello World
    
    // trỏ tới hàm sayGoodbye
    funcPtr = sayGoodbye;
    funcPtr(); // -> in ra: Goodbye, World!
    
    return 0;
    
Lý do sử dụng Hàm con trỏ (Function pointer)
  • Dynamic Function Call: Chúng cho phép gọi hàm động, nghĩa là bạn có thể chọn hàm nào sẽ thực thi khi chạy
  • Callbacks: được sử dụng trong lệnh gọi lại cho lập trình hướng sự kiện
  • Flexibility (Tính linh hoạt): Cho phép truyền các hàm làm đối số cho các hàm khác, tạo điều kiện thuận lợi cho việc tạo mã linh hoạt hơn và có thể tái sử dụng
  • Efficiency (Hiệu quả): Có thể làm cho việc triển khai state machine hiệu quả hơn bằng cách giảm nhu cầu về nhiều cấu trúc if-else hoặc switch-case.

Function Returning Pointers

Định nghĩa và cách sử dụng

Các hàm này trả về một con trỏ, thường được sử dụng để trả về địa chỉ của một mảng hoặc bộ nhớ được cấp phát động (Dynamically allocated memory)

#include <stdio.h>
#include <stdlib.h>

// hàm trả về một con trỏ
int* allocateArray(int Size){
        int* array = (int*)malloc(size * sizeof(int));
        return array;
}

int main(){
    int size = 5;
    int* array = allocatedArray(size);
    
    // khởi tạo mảng
    for (int i = 0; i < size; i++){
            printf("%d", array[i]);
    }
    
    // giải phóng bộ nhớ động
    free(array);
    
    return 0;

Ứng dụng trong hệ thống nhúng (Embedded System)

Trong các hệ thống nhúng, nơi tài nguyên bị hạn chế và hiệu suất thời gian thực là quan trọng, pointers được sử dụng rộng rãi vì nhiều lý do:


1. Xử lý ngắt (Interrupt Handling):

  • Con trỏ hàm được sử dụng để thiết lập các chương trình phục vụ ngắt (Interrupt Service Routine - ISR). Địa chỉ của ISR được lưu trữ trong bảng vector ngắt (Interrupt vetor table).
#include <stdio.h>

// ISRs prototypes
void UART_ISR();
void Timer_ISR();

// ISR vector table
void (*ISR_VectorTable[2])() = {UART_ISR, Timer_ISR};

void UART_ISR(){
    printf("UART Interrupt\n");
}

void Timer_ISR(){
    printf("Timer Interrupt\n");
}

void SimulateInterrupt(int InterruptType){
    if (InterruptType < 2){
        ISR_VectorTable[InterruptType]();
    }
}

int main(){
    // mô phỏng UART interrupt
    SimulateInterrupt(0);
    // mô phỏng Timer interrupt
    SimulateInterrupt(1);
    
    return 0;
}

2. State Machines:

  • Con trỏ hàm cho phép triển khai các máy trạng thái (state machine) bằng cách cho phép chuyển đổi động giữa các trạng thái, làm cho mã trở nên đồng nhất và dễ duy trì.
#include <stdio.h>

// Nguyên mẫu hàm trạng thái
void stateA();
void stateB();
void stateC();

// định nghĩa cho một hàm trạng thái
typedef void(*StateFunc)();

// Bảng chuyển trạng thái
StateFunc stateTable[3] = {stateA, stateB, stateC};

// biến trạng thái hiện tại
int currentState = 0;

void stateA(){
    printf("State A\n");
    // đổi sang trạng thái B
    currentState = 1;
}

void stateB(){
    printf("State B\n");
    // đổi sang trạng thái C
    currentState = 2;
}

void stateC(){
    printf("State C\n");
    // đổi sang trạng thái A
    currentState = 0;
}

int main(){
    // mô phonge quá trình chuyển đổi trạng thái
    for (int i = 0; i < 6; ++i){
        stateTable[currentState]();
    }
    
    return 0;

3. Callbacks:

  • Được sử dụng trong bộ hẹn giờ, giao tiếp UART, I2C, SPI để xử lý các sự kiện như dữ liệu đã nhận, hoàn tất truyền, v.v
#include <stdio.h>

// Callback type definition
typedef void (*CallbackFunc)(int);

// Function that accepts a callback
void registerCallback(CallbackFunc cb, int eventData) {
    // Invoke the callback with event data
    cb(eventData);
}

// Sample callback function
void eventHandler(int data) {
    printf("Event handled with data: %d\n", data);
}

int main() {
    // Register the callback
    registerCallback(eventHandler, 42);
    return 0;
}


4. Trừu tượng hóa trình điều khiển (Driver Abstraction):

  • Các hoạt động phần cứng sử dụng con trỏ hàm, giúp chuyển đổi phần cứng dễ dàng hơn mà không cần thay đổi mã ứng dụng cấp cao hơn.

5. Quản lý bộ nhớ động (Dynamic Memory Management):

  • Các hàm trả về con trỏ có thể được sử dụng để phân bổ và quản lý bộ nhớ động, điều này rất quan trọng trong mời trường có bộ nhớ hạn chế.

Summary

Sử dụng con trỏ hàm một cách hiệu quả trong các hệ thống nhúng liên quan đến việc hiểu lợi ích của chigns và áp dụng các phương pháp hay nhất để tối đa hóa hiệu quả, khả năng bảo trì và tính linh hoạt của mã.

Lưu ý

  1. Sự rõ ràng

    • Ghi lại rõ ràng mục đích của con trỏ hàm và cách sử dụng dự kiến của chúng
    • Đặt tên con trỏ hàm một cách mô tả để chỉ ra vai trò của chúng rõ ràng
  2. Khởi tạo (Initialization)

    • Luôn khởi tạo các con trỏ hàm trước khi sử dụng để ngăn chặn lỗi Null Pointer Dereferences
    • Xem xét các trình xử lý mặc định cho các con trỏ chưa được khởi tạo
  3. Modular Code

    • Sử dụng con trỏ hàm để tạo mã mô-đun và có thể tái sử dụng bằng cách tách các thành phần.
  4. Hiệu suất

    • Xin lưu ý rằng các lệnh gọi hàm gián tiếp thông qua con trỏ có thể chậm hơn do tình trạng ngừng hoạt động trong các CPU hiện đại. Giảm thiểu việc sử dụng chúng trong các phần quan trọng về hiệu suất.
  5. Quản lý bộ nhớ

    • Khi các hàm trả về con trỏ tới bộ nhớ được cấp phát động, hãy đảm bảo quản lý bộ nhớ thích hợp (phân bổ và giải phóng).

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí