Làm thế nào có thể lưu trữ các loại dữ liệu hỗn hợp (int, float, char, v.v.) trong một mảng?


145

Tôi muốn lưu trữ các loại dữ liệu hỗn hợp trong một mảng. Làm thế nào một người có thể làm điều đó?


8
Có thể và có trường hợp sử dụng, nhưng đây có thể là một thiết kế thiếu sót. Đó không phải là mảng dành cho.
djechlin

Câu trả lời:


244

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 typeviê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 intphầ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 switchtuyê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 typethành viên luôn tương ứng với giá trị cuối cùng được lưu trữ trong union.


23
+1 Đây là hàm ý của nhiều ngôn ngữ phiên dịch được viết bằng C
texasbruce

8
@texasbruce cũng được gọi là "liên minh được gắn thẻ". Tôi cũng đang sử dụng kỹ thuật này bằng ngôn ngữ của mình. ;)

Wikipedia sử dụng một trang định hướng cho " liên minh phân biệt đối xử " - "liên minh rời rạc" trong lý thuyết tập hợp và, như @ H2CO3 đã đề cập, "liên minh được gắn thẻ" trong khoa học máy tính.
Izkata

14
Và dòng đầu tiên của trang liên kết được gắn thẻ Wikipedia cho biết: Trong khoa học máy tính, một liên minh được gắn thẻ, còn được gọi là biến thể, hồ sơ biến thể, liên minh phân biệt đối xử, liên minh tách rời hoặc loại tổng hợp, ... Nó đã được phát minh lại rất nhiều lần tên (loại giống như từ điển, băm, mảng kết hợp, vv).
Barmar

1
@Barmar Tôi đã viết lại thành "liên kết được gắn thẻ" nhưng sau đó đọc bình luận của bạn. Quay trở lại chỉnh sửa, tôi không có ý phá hoại câu trả lời của bạn.

32

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ố.


21

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.


8

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 valcho 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 . 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 .tagkhở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_.valkhô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?
MM

5

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 enumgiá trị.


bạn cũng có thể lưu trữ thông tin loại trong chính con trỏ
phuclv

3

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ỏ intphải là bội số của 4 (giả sử intlà 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 longuint64_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.

https://en.wikipedia.org/wiki/Tagged_pulum#Examples

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 doublelà 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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.