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 ?
Vì 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ếtp++
, trình biên dịch sẽ dịch chuyển con trỏ đi một khoảng bằngsizeof(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ộtstruct
nào đó). Do đó, nó không thể xác định được bước nhảy (step size) là bao nhiêu, dẫn đếnerror
.
Để 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
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
.
- Giả sử địa chỉ lúc đầu mà buffer đang trỏ tới là
All rights reserved