0

con trỏ void*

Con trỏ void* là gì

Con trỏ void * thường được gọi là con trỏ đa năng hoặc con trỏ chung. Đây là một quy ước trong ngôn ngữ C liên quan đến địa chỉ thuần túy. Khi một con trỏ là con trỏ void *, đối tượng mà nó trỏ đến không thuộc bất kỳ kiểu dữ liệu nào. Vì con trỏ void * không thuộc bất kỳ kiểu dữ liệu nào, các phép toán số học không thể được thực hiện trên chúng, chẳng hạn như tăng giá trị con trỏ; compiler không biết phải tăng bao nhiêu. Ví dụ, con trỏ char * tăng 1, trong khi con trỏ short * tăng 2.

Trong C/C++, bạn có thể sử dụng các loại con trỏ khác nhau để thay thế con trỏ void *, hoặc sử dụng con trỏ void * để thay thế các loại con trỏ khác bất cứ lúc nào. Những đặc điểm này có thể dẫn đến nhiều kỹ thuật hữu ích. Bản chất của con trỏ là giá trị của nó là một địa chỉ.

Khi một biến con trỏ được khai báo bằng từ khóa void, nó sẽ trở thành một biến con trỏ đa năng. Địa chỉ của bất kỳ biến nào thuộc bất kỳ kiểu dữ liệu nào (char, int, float, v.v.) đều có thể được gán cho một biến con trỏ void *.

Để hủy tham chiếu một biến con trỏ, hãy sử dụng toán tử gián tiếp *. Tuy nhiên, khi sử dụng con trỏ void *, bạn cần ép kiểu biến con trỏ để hủy tham chiếu. Điều này là do con trỏ void * không có kiểu dữ liệu liên quan. Compiler không thể biết kiểu dữ liệu mà con trỏ void trỏ đến. Do đó, để lấy dữ liệu được trỏ đến bởi con trỏ void *, bạn cần thực hiện ép kiểu bằng cách sử dụng kiểu dữ liệu được lưu trữ tại vị trí con trỏ void *.

#include <stdio.h>

void printValue(void *ptr, char type) {
    // Ép kiểu dựa vào "type" mà người dùng cung cấp
    switch(type) {
        case 'i': // int
            printf("Int: %d\n", *(int*)ptr);
            break;
        case 'f': // float
            printf("Float: %.2f\n", *(float*)ptr);
            break;
        case 'c': // char
            printf("Char: %c\n", *(char*)ptr);
            break;
        default:
            printf("Unknown type\n");
    }
}

int main() {
    int   a = 42;
    float b = 3.14f;
    char  c = 'X';

    void *p; // con trỏ void có thể trỏ tới bất kỳ loại nào

    p = &a;  // trỏ tới int
    printValue(p, 'i');

    p = &b;  // trỏ tới float
    printValue(p, 'f');

    p = &c;  // trỏ tới char
    printValue(p, 'c');

    return 0;
}

Output

Input for the program ( Optional )
STDIN
Output:

Int: 42
Float: 3.14
Char: X

Đặc điểm

Con trỏ void* không thể thực hiện các toán tử số học con trỏ (pointer arithmetic) như +, -, ++, -- và toán tử truy cập mảng [].

Lí do ?

void* là một con trỏ không kiểu. Khi thực hiện một phép toán số học trên con trỏ, trình biên dịch cần biết kích thước của kiểu dữ liệu mà con trỏ đó đang trỏ tới để có thể tính toán đúng địa chỉ.

  • Ví dụ, nếu ta có một con trỏ int* p, khi viết p++, trình biên dịch sẽ dịch chuyển con trỏ đi một khoảng bằng sizeof(int) (thường là 4 byte) để trỏ đến phần tử int tiếp theo trong bộ nhớ.
  • Tuy nhiên, với void*, trình biên dịch không biết nó đang trỏ đến kiểu dữ liệu gì (có thể là int, char, float, hoặc một struct nào đó). Do đó, nó không thể xác định được bước nhảy (step size) là bao nhiêu, dẫn đến error.

Để có thể sử dụng các toán tử này, ta phải ép kiểu (type cast) con trỏ void* sang một kiểu dữ liệu cụ thể trước.

Một ví dụ: Lưu giá trị vào mảng dùng con trỏ void *

  • data_size: uint8_t -> kích thước 1 byte.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

int main()
{
  uint8_t value = 1;
  
  size_t data_size = sizeof(uint8_t);
  int max_size = 5;
  void *buffer;
  
  // mảng buffer có 5 phần tử, mỗi phần tử có kích thước là 1 byte.
  buffer = (void *)malloc(data_size * max_size);
  
  // Lưu trữ một giá trị vào phần tử đầu tiên của buffer: buffer[0]
  memcpy((uint8_t*)buffer + 0 * data_size, &value, data_size);

  printf("đia chỉ con trỏ buffer: %p\n", buffer);
  printf("đia chỉ buffer[0]: %p\n", (uint8_t*)buffer + 0 * data_size);
  printf("đia chỉ buffer[1]: %p\n", (uint8_t*)buffer + 1 * data_size);
  printf("đia chỉ buffer[2]: %p\n", (uint8_t*)buffer + 2 * data_size);
  printf("đia chỉ buffer[3]: %p\n", (uint8_t*)buffer + 3 * data_size);
  printf("đia chỉ buffer[4]: %p\n", (uint8_t*)buffer + 4 * data_size);
}

Output

đia chỉ con trỏ buffer: 0x559a94a122a0
đia chỉ buffer[0]: 0x559a94a122a0
đia chỉ buffer[1]: 0x559a94a122a1
đia chỉ buffer[2]: 0x559a94a122a2
đia chỉ buffer[3]: 0x559a94a122a3
đia chỉ buffer[4]: 0x559a94a122a4

image.png

Tôi nghĩ các bạn sẽ thấy khó hiểu nhất chỗ này: (uint8_t*)buffer + 0 * data_size. Theo như ở trên, thì ta cần phải ép kiểu con trỏ void *, thì mới có thể thực hiện được các phép tính.

  • Lúc này, khi thao tác thì địa chỉ tăng tương ứng với giá trị ép kiểu: uint8_t *. Để dễ hiểu hơn thì có thể hình dung như này.
    • Giả sử địa chỉ lúc đầu mà buffer đang trỏ tới là 0x000.
    • Với (uint8_t *)buffer + 1, thì địa chỉ buffer trỏ tới tiếp theo là 0x001.
    • Với (uint16_t *)buffer + 1, thì địa chỉ buffer trỏ tới tiếp theo là 0x002.

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í