Câu trả lời:
Bạn có thể làm cho các thành phần mảng trở thành một liên minh phân biệt đối xử, còn được gọi là liên minh được gắn thẻ .
struct {
enum { is_int, is_float, is_char } type;
union {
int ival;
float fval;
char cval;
} val;
} my_array[10];
Thành type
viên được sử dụng để giữ lựa chọn thành viên nào union
được sử dụng cho từng thành phần mảng. Vì vậy, nếu bạn muốn lưu trữ một int
phần tử đầu tiên, bạn sẽ làm:
my_array[0].type = is_int;
my_array[0].val.ival = 3;
Khi bạn muốn truy cập một phần tử của mảng, trước tiên bạn phải kiểm tra loại, sau đó sử dụng thành viên tương ứng của liên minh. Một switch
tuyên bố là hữu ích:
switch (my_array[n].type) {
case is_int:
// Do stuff for integer, using my_array[n].ival
break;
case is_float:
// Do stuff for float, using my_array[n].fval
break;
case is_char:
// Do stuff for char, using my_array[n].cvar
break;
default:
// Report an error, this shouldn't happen
}
Nó tùy thuộc vào lập trình viên để đảm bảo rằng type
thành viên luôn tương ứng với giá trị cuối cùng được lưu trữ trong union
.
Sử dụng một liên minh:
union {
int ival;
float fval;
void *pval;
} array[10];
Tuy nhiên, bạn sẽ phải theo dõi loại của từng yếu tố.
Các phần tử mảng cần phải có cùng kích thước, đó là lý do tại sao điều đó là không thể. Bạn có thể làm việc xung quanh nó bằng cách tạo một loại biến thể :
#include <stdio.h>
#define SIZE 3
typedef enum __VarType {
V_INT,
V_CHAR,
V_FLOAT,
} VarType;
typedef struct __Var {
VarType type;
union {
int i;
char c;
float f;
};
} Var;
void var_init_int(Var *v, int i) {
v->type = V_INT;
v->i = i;
}
void var_init_char(Var *v, char c) {
v->type = V_CHAR;
v->c = c;
}
void var_init_float(Var *v, float f) {
v->type = V_FLOAT;
v->f = f;
}
int main(int argc, char **argv) {
Var v[SIZE];
int i;
var_init_int(&v[0], 10);
var_init_char(&v[1], 'C');
var_init_float(&v[2], 3.14);
for( i = 0 ; i < SIZE ; i++ ) {
switch( v[i].type ) {
case V_INT : printf("INT %d\n", v[i].i); break;
case V_CHAR : printf("CHAR %c\n", v[i].c); break;
case V_FLOAT: printf("FLOAT %f\n", v[i].f); break;
}
}
return 0;
}
Kích thước của phần tử của liên minh là kích thước của phần tử lớn nhất, 4.
Có một phong cách khác nhau để xác định liên kết thẻ (theo bất kỳ tên nào) mà IMO làm cho nó dễ sử dụng hơn nhiều , bằng cách xóa liên kết nội bộ. Đây là kiểu được sử dụng trong Hệ thống X Window cho những thứ như Sự kiện.
Ví dụ trong câu trả lời của Barmar đặt tên val
cho liên minh nội bộ. Ví dụ trong câu trả lời của Sp. Sử dụng liên kết ẩn danh để tránh phải chỉ định .val.
mỗi lần bạn truy cập vào bản ghi biến thể. Thật không may, các cấu trúc và công đoàn "ẩn danh" không có sẵn trong C89 hoặc C99. Đây là một phần mở rộng trình biên dịch, và do đó vốn không có khả năng di động.
Một cách tốt hơn IMO là đảo ngược toàn bộ định nghĩa. Tạo mỗi kiểu dữ liệu theo cấu trúc riêng và đặt thẻ (bộ xác định kiểu) vào mỗi cấu trúc.
typedef struct {
int tag;
int val;
} integer;
typedef struct {
int tag;
float val;
} real;
Sau đó, bạn bọc những thứ này trong một liên minh cấp cao nhất.
typedef union {
int tag;
integer int_;
real real_;
} record;
enum types { INVALID, INT, REAL };
Bây giờ có vẻ như chúng ta đang lặp lại chính mình, và chúng ta là . Nhưng hãy xem xét rằng định nghĩa này có khả năng được tách ra thành một tệp duy nhất. Nhưng chúng tôi đã loại bỏ tiếng ồn của việc chỉ định trung gian .val.
trước khi bạn lấy dữ liệu.
record i;
i.tag = INT;
i.int_.val = 12;
record r;
r.tag = REAL;
r.real_.val = 57.0;
Thay vào đó, nó đi đến cuối cùng, nơi nó ít đáng ghét hơn. : D
Một điều khác cho phép này là một hình thức thừa kế. Chỉnh sửa: phần này không phải là tiêu chuẩn C, nhưng sử dụng phần mở rộng GNU.
if (r.tag == INT) {
integer x = r;
x.val = 36;
} else if (r.tag == REAL) {
real x = r;
x.val = 25.0;
}
integer g = { INT, 100 };
record rg = g;
Đúc lên và đúc xuống.
Chỉnh sửa: Một điều đáng chú ý là nếu bạn đang xây dựng một trong số này với các trình khởi tạo được chỉ định C99. Tất cả các thành viên khởi tạo nên thông qua cùng một thành viên công đoàn.
record problem = { .tag = INT, .int_.val = 3 };
problem.tag; // may not be initialized
Trình .tag
khởi tạo có thể bị bỏ qua bởi trình biên dịch tối ưu hóa, bởi vì trình .int_
khởi tạo theo bí danh cùng một vùng dữ liệu. Mặc dù chúng tôi biết cách bố trí (!), Và nó sẽ ổn thôi. Không, không phải vậy. Thay vào đó, hãy sử dụng thẻ "nội bộ" (nó phủ lên thẻ bên ngoài, giống như chúng ta muốn, nhưng không gây nhầm lẫn cho trình biên dịch).
record not_a_problem = { .int_.tag = INT, .int_.val = 3 };
not_a_problem.tag; // == INT
.int_.val
không bí danh cùng một khu vực mặc dù vì trình biên dịch biết rằng .val
ở mức bù lớn hơn .tag
. Bạn đã có một liên kết để thảo luận thêm về vấn đề bị cáo buộc này?
Bạn có thể thực hiện một void *
mảng, với một mảng riêng biệt size_t.
Nhưng bạn mất loại thông tin.
Nếu bạn cần giữ kiểu thông tin theo một cách nào đó, hãy giữ một mảng thứ ba của int (trong đó int là một giá trị được liệt kê) Sau đó, mã hóa hàm mà tùy thuộc vào enum
giá trị.
Liên minh là cách tiêu chuẩn để đi. Nhưng bạn có giải pháp khác là tốt. Một trong số đó được gắn thẻ con trỏ , liên quan đến việc lưu trữ thêm thông tin trong phần "miễn phí" bit của một con trỏ.
Tùy thuộc vào kiến trúc, bạn có thể sử dụng các bit thấp hoặc cao, nhưng cách an toàn và di động nhất là sử dụng các bit thấp không sử dụng bằng cách tận dụng lợi thế của bộ nhớ căn chỉnh. Ví dụ: trong các hệ thống 32 bit và 64 bit, các con trỏ int
phải là bội số của 4 (giả sử int
là loại 32 bit) và 2 bit có ý nghĩa nhỏ nhất phải là 0, do đó bạn có thể sử dụng chúng để lưu trữ loại giá trị của mình . Tất nhiên bạn cần phải xóa các bit thẻ trước khi hủy bỏ con trỏ. Ví dụ: nếu loại dữ liệu của bạn bị giới hạn ở 4 loại khác nhau thì bạn có thể sử dụng nó như dưới đây
void* tp; // tagged pointer
enum { is_int, is_double, is_char_p, is_char } type;
// ...
uintptr_t addr = (uintptr_t)tp & ~0x03; // clear the 2 low bits in the pointer
switch ((uintptr_t)tp & 0x03) // check the tag (2 low bits) for the type
{
case is_int: // data is int
printf("%d\n", *((int*)addr));
break;
case is_double: // data is double
printf("%f\n", *((double*)addr));
break;
case is_char_p: // data is char*
printf("%s\n", (char*)addr);
break;
case is_char: // data is char
printf("%c\n", *((char*)addr));
break;
}
Nếu bạn có thể đảm bảo rằng dữ liệu là 8-byte aligned (như cho con trỏ trong các hệ thống 64-bit, hay long long
và uint64_t
...), bạn sẽ có một chút nhiều hơn cho thẻ.
Điều này có một nhược điểm là bạn sẽ cần thêm bộ nhớ nếu dữ liệu chưa được lưu trữ ở một biến ở nơi khác. Do đó, trong trường hợp loại và phạm vi dữ liệu của bạn bị giới hạn, bạn có thể lưu trữ các giá trị trực tiếp trong con trỏ. Kỹ thuật này đã được sử dụng trong phiên bản 32 bit của công cụ V8 của Chrome , trong đó nó kiểm tra bit địa chỉ ít quan trọng nhất để xem liệu đó có phải là một con trỏ đến một đối tượng khác (như số kép, số nguyên lớn, chuỗi hoặc một số đối tượng) hay 31 -bit giá trị đã ký (gọi là smi
- số nguyên nhỏ ). Nếu là một int
, Chrome chỉ cần thực hiện dịch chuyển số phải 1 bit để lấy giá trị, nếu không thì con trỏ sẽ bị hủy đăng ký.
Trên hầu hết các hệ thống 64 bit hiện tại, không gian địa chỉ ảo vẫn hẹp hơn nhiều so với 64 bit, do đó các bit có ý nghĩa cao nhất cũng có thể được sử dụng làm thẻ . Tùy thuộc vào kiến trúc, bạn có những cách khác nhau để sử dụng chúng làm thẻ. ARM , 68k và nhiều thiết bị khác có thể được cấu hình để bỏ qua các bit hàng đầu , cho phép bạn sử dụng chúng một cách tự do mà không phải lo lắng về segfault hay bất cứ điều gì. Từ bài viết Wikipedia được liên kết ở trên:
Một ví dụ đáng kể về việc sử dụng các con trỏ được gắn thẻ là thời gian chạy Objective-C trên iOS 7 trên ARM64, đáng chú ý được sử dụng trên iPhone 5S. Trong iOS 7, địa chỉ ảo là 33 bit (được căn chỉnh theo byte), do đó, địa chỉ được liên kết từ chỉ sử dụng 30 bit (3 bit có ý nghĩa nhỏ nhất là 0), để lại 34 bit cho thẻ. Các con trỏ lớp Objective-C được căn chỉnh từ và các trường thẻ được sử dụng cho nhiều mục đích, chẳng hạn như lưu trữ số tham chiếu và liệu đối tượng có hàm hủy hay không.
Các phiên bản ban đầu của MacOS đã sử dụng các địa chỉ được gắn thẻ có tên là Handles để lưu trữ các tham chiếu đến các đối tượng dữ liệu. Các bit cao của địa chỉ cho biết liệu đối tượng dữ liệu đã bị khóa, có thể xóa và / hoặc có nguồn gốc từ một tệp tài nguyên, tương ứng. Điều này gây ra sự cố tương thích khi địa chỉ MacOS nâng cao từ 24 bit lên 32 bit trong Hệ thống 7.
Trên x86_64, bạn vẫn có thể sử dụng các bit cao làm thẻ cẩn thận . Tất nhiên, bạn không cần phải sử dụng tất cả 16 bit đó và có thể bỏ qua một số bit để chứng minh trong tương lai
Trong các phiên bản trước của Mozilla Firefox, họ cũng sử dụng tối ưu hóa số nguyên nhỏ như V8, với 3 bit thấp được sử dụng để lưu trữ loại (int, chuỗi, đối tượng ... vv). Nhưng vì JägerMonkey, họ đã đi một con đường khác ( Đại diện giá trị JavaScript mới của Mozilla , liên kết dự phòng ). Giá trị này luôn được lưu trữ trong biến chính xác kép 64 bit. Khi double
là một chuẩn hóa , nó có thể được sử dụng trực tiếp trong tính toán. Tuy nhiên, nếu 16 bit cao của tất cả là 1 giây, biểu thị cho NaN , thì 32 bit thấp sẽ lưu địa chỉ (trong máy tính 32 bit) vào giá trị hoặc giá trị trực tiếp, 16 bit còn lại sẽ được sử dụng để lưu trữ các loại. Kỹ thuật này được gọi là NaN-Boxinghoặc nữ tu sĩ quyền anh. Nó cũng được sử dụng trong JavaScriptCore của WebKit 64 bit và SpiderMonkey của Mozilla với con trỏ được lưu trữ trong 48 bit thấp. Nếu kiểu dữ liệu chính của bạn là dấu phẩy động, đây là giải pháp tốt nhất và mang lại hiệu suất rất tốt.
Đọc thêm về các kỹ thuật trên: https://wingolog.org/archives/2011/05/18/value-interesentation-in-javascript-im THỰCations