2009-02-20 24 views
10

Giả sử tôi có một chức năng phân bổ bộ nhớ cho người gọi:Cách tốt nhất để giải phóng bộ nhớ sau khi trở về từ một lỗi là gì?

int func(void **mem1, void **mem2) { 
    *mem1 = malloc(SIZE); 
    if (!*mem1) return 1; 

    *mem2 = malloc(SIZE); 
    if (!*mem2) { 
     /* ... */ 
     return 1; 
    } 

    return 0; 
} 

tôi muốn nghe phản hồi của bạn về cách tốt nhất để giải phóng() bộ nhớ được phân bổ trong trường hợp malloc thứ hai() thất bại. Bạn có thể tưởng tượng một tình huống phức tạp hơn với nhiều điểm thoát lỗi hơn và bộ nhớ được cấp phát nhiều hơn.

Trả lời

25

Tôi biết mọi người đều ưa để sử dụng chúng, nhưng đây là tình huống hoàn hảo cho một goto trong C.

int func(void** mem1, void** mem2) 
{ 
    int retval = 0; 
    *mem1 = malloc(SIZE); 
    if (!*mem1) { 
     retval = 1; 
     goto err; 
    } 

    *mem2 = malloc(SIZE); 
    if (!*mem2) { 
     retval = 1; 
     goto err; 
    } 
// ...  
    goto out; 
// ... 
err: 
    if(*mem1) free(*mem1); 
    if(*mem2) free(*mem2); 
out: 
    return retval; 
}  
+0

là tốt trong các tình huống nhất định. Chúng tôi thấy loại dọn dẹp này trong hạt nhân Linux rất nhiều. Nó hoạt động tốt và dễ hiểu. –

+0

@Steve Lazaridis: Chính xác. Làm việc với nhân Linux đã dạy tôi rằng đôi khi, goto là ok. :) – FreeMemory

+4

Điều đó sẽ không hoạt động đáng tin cậy trừ khi bạn đặt trước * mem1 và * mem2. –

0

Độ nghiêng của riêng tôi là tạo một hàm đối số biến để giải phóng tất cả các con trỏ không NULL. Sau đó, người gọi có thể xử lý trường hợp lỗi:

void *mem1 = NULL; 
void *mem2 = NULL; 

if (func(&mem1, &mem2)) { 
    freeAll(2, mem1, mem2); 
    return 1; 
} 
+0

Trên thực tế, không có hình phạt trong việc kêu gọi miễn phí trên một con trỏ NULL. Bạn không cần phải kiểm tra NULL. Nhưng điều quan trọng là khởi tạo NULL. – ypnos

+0

@ypnos: Bạn đang nghĩ đến việc xóa C++. Nó không hợp lệ để chuyển NULL thành free(). –

+1

Tôi đã kiểm tra K & R và Lời Toàn Năng nói rằng miễn phí (NULL) hoàn toàn không có gì. –

1

Người gọi có thực hiện bất kỳ điều gì hữu ích với các khối bộ nhớ được phân bổ chính xác trước khi không? Nếu không, các callee nên xử lý deallocation.

Một khả năng để làm sạch một cách hiệu quả là sử dụng do..while(0), cho phép để break nơi bạn ví dụ return s:

int func(void **mem1, void **mem2) 
{ 
    *mem1 = NULL; 
    *mem2 = NULL; 

    do 
    { 
     *mem1 = malloc(SIZE); 
     if(!*mem1) break; 

     *mem2 = malloc(SIZE); 
     if(!*mem2) break; 

     return 0; 
    } while(0); 

    // free is NULL-safe 
    free(*mem1); 
    free(*mem2); 

    return 1; 
} 

Nếu bạn làm được rất nhiều việc phân bổ, bạn có thể muốn sử dụng chức năng freeAll() của bạn để làm dọn dẹp ở đây.

+0

Không, người gọi chỉ được miễn phí nếu cần và quay trở lại. Mã này có phải là goto mỏng che kín không? Không phải là tôi phản đối việc sử dụng goto cho mục đích này ... –

+0

@FunkyMo: Vâng, về cơ bản nó là một 'goto', nhưng với giới hạn mà bạn chỉ có thể bỏ qua phần còn lại của khối, nghĩa là không thể làm điều xấu [tm]; Cá nhân tôi tìm thấy trình dọn dẹp này hơn là sử dụng 'goto', mà tôi chỉ sử dụng để tách ra khỏi các vòng newsted .... – Christoph

+0

@FunkyMo: Tôi sử dụng mẫu này khá thường xuyên: nó cho phép một số phép thuật vĩ mô tiện lợi: http: // stackoverflow Các câu lệnh goto/fine-questions/569573/what-is-a-good-programming-pattern-for-handling-return-values-from-stdio-file-wri/569659 # 569659 – Christoph

6

Đây là nơi mà một goto là thích hợp, theo ý kiến ​​của tôi. Tôi đã từng làm theo giáo điều chống goto, nhưng tôi đã thay đổi điều đó khi nó được chỉ ra cho tôi rằng {...} trong khi (0); biên dịch thành cùng một mã, nhưng không dễ đọc. Chỉ cần làm theo một số quy tắc cơ bản, như không đi ngược với họ, giữ họ ở mức tối thiểu, chỉ sử dụng chúng cho các điều kiện lỗi, vv ...

int func(void **mem1, void **mem2) 
{ 
    *mem1 = NULL; 
    *mem2 = NULL; 

    *mem1 = malloc(SIZE); 
    if(!*mem1) 
     goto err; 

    *mem2 = malloc(SIZE); 
    if(!*mem2) 
     goto err; 

    return 0; 
err: 
    if(*mem1) 
     free(*mem1); 
    if(*mem2) 
     free(*mem2); 

    *mem1 = *mem2 = NULL; 

    return 1; 
} 
4

Đây là một chút tranh cãi, nhưng tôi nghĩ rằng cách tiếp cận goto được sử dụng trong hạt nhân Linux thực sự hoạt động khá tốt trong trường hợp này:

int get_item(item_t* item) 
{ 
    void *mem1, *mem2; 
    int ret=-ENOMEM; 
    /* allocate memory */ 
    mem1=malloc(...); 
    if(mem1==NULL) goto mem1_failed; 

    mem2=malloc(...); 
    if(mem2==NULL) goto mem2_failed; 

    /* take a lock */ 
    if(!mutex_lock_interruptible(...)) { /* failed */ 
    ret=-EINTR; 
    goto lock_failed; 
    } 

    /* now, do the useful work */ 
    do_stuff_to_acquire_item(item); 
    ret=0; 

    /* cleanup */ 
    mutex_unlock(...); 

lock_failed: 
    free(mem2); 

mem2_failed: 
    free(mem1); 

mem1_failed: 
    return ret; 
} 
+0

Bạn hoàn toàn có thể sử dụng nếu có ở đó. Chỉ cần viết nó như: 'if (mem! = Null) {code ...} miễn phí (mem);' Bạn có thể đọc mã nhanh hơn nhiều + trình biên dịch biết chính xác những gì bạn muốn làm. – zoran404

-2

Tôi hơi sợ hãi bởi tất cả các khuyến nghị cho một tuyên bố về goto!

Tôi thấy rằng việc sử dụng goto dẫn đến mã khó hiểu có nhiều khả năng gây ra lỗi lập trình viên hơn. Sở thích của tôi bây giờ là tránh sử dụng hoàn toàn, ngoại trừ trong những tình huống khắc nghiệt nhất. Tôi gần như không bao giờ sử dụng nó. Không phải vì tính toàn cầu học thuật, nhưng bởi vì một năm hoặc lâu hơn sau này, dường như khó có thể nhớ lại logic tổng thể hơn là với sự thay thế mà tôi sẽ đề nghị.

Là một trong những người yêu thích để tái cấu trúc mọi thứ để giảm thiểu tùy chọn của tôi để quên nội dung (như xóa con trỏ), tôi sẽ thêm một vài hàm trước. Tôi sẽ cho rằng có khả năng tôi sẽ sử dụng lại một chút trong cùng một chương trình. Hàm imalloc() sẽ thực hiện thao tác malloc với con trỏ gián tiếp; ifree() sẽ hoàn tác việc này. cifree() sẽ giải phóng bộ nhớ có điều kiện.

Với điều đó trong tay, phiên bản của tôi về mã (với một arg thứ ba, vì lợi ích của cuộc biểu tình) sẽ là như thế này:

// indirect pointer malloc 
int imalloc(void **mem, size_t size) 
{ 
    return (*mem = malloc(size)); 
} 

// indirect pointer free 
void ifree(void **mem) 
{ 
    if(*mem) 
    { 
    free(*mem); 
    *mem = NULL; 
    } 
} 

// conditional indirect pointer free 
void cifree(int cond, void **mem) 
{ 
    if(!cond) 
    { 
    ifree(mem); 
    } 
} 

int func(void **mem1, void **mem2, void **mem3) 
{ 
    int result = FALSE; 
    *mem1 = NULL; 
    *mem2 = NULL; 
    *mem3 = NULL; 

    if(imalloc(mem1, SIZE)) 
    { 
    if(imalloc(mem2, SIZE)) 
    { 
     if(imalloc(mem3, SIZE)) 
     { 
     result = TRUE; 
     }    
     cifree(result, mem2); 
    } 
    cifree(result, mem1); 
    } 
    return result; 
} 

tôi thích để chỉ có một trở về từ một hàm, tại kết thúc. Nhảy ra ở giữa là nhanh (và theo ý kiến ​​của tôi, loại bẩn).Nhưng đáng kể hơn, cho phép bạn dễ dàng bỏ qua mã dọn dẹp liên quan một cách vô tình.

+0

Chức năng này rò rỉ bộ nhớ nếu nó trả về FALSE và giải phóng bộ nhớ khi nó trả về TRUE, không phải là những gì người hỏi hỏi. –

+0

Đã sửa lỗi đánh máy - sự so sánh của cond phải là (! Cond). – digijock

+0

Dour High Arch - Sẽ rất tuyệt nếu bạn rút lại nhận xét vì nó không chính xác. Sơ đồ cơ bản mà tôi trình bày thực tế hoạt động khá tốt, không bị rò rỉ và không trả về các con trỏ không hợp lệ. – digijock

0

Nếu các báo cáo goto trên khiếp sợ bạn đối với một số lý do nào, bạn luôn có thể làm điều gì đó như thế này:

int func(void **mem1, void **mem2) 
{ 
    *mem1 = malloc(SIZE); 
    if (!*mem1) return 1; 

    *mem2 = malloc(SIZE); 
    if (!*mem2) { 
     /* Insert free statement here */ 
     free(*mem1); 
     return 1; 
    } 

    return 0; 
} 

tôi sử dụng phương pháp này khá thường xuyên, nhưng chỉ khi nó rất rõ ràng những gì đang xảy ra.

2

Đây là một lựa chọn có thể đọc được:

int func(void **mem1, void **mem2) { 
    *mem1 = malloc(SIZE); 
    *mem2 = malloc(SIZE); 
    if (!*mem1 || !*mem2) { 
    free(*mem2); 
    free(*mem1); 
    return 1; 
    } 
    return 0; 
} 
1

Cá nhân; Tôi có một thư viện theo dõi tài nguyên (về cơ bản là một cây nhị phân cân bằng) và tôi có trình bao bọc cho tất cả các chức năng phân bổ.

Tài nguyên (chẳng hạn như bộ nhớ, ổ cắm, bộ mô tả tệp, ẩn dụ, v.v ... - bất kỳ thứ gì bạn phân bổ và deallocate) có thể thuộc về một tập hợp.

Tôi cũng có một thư viện xử lý lỗi, theo đó đối số đầu tiên cho mỗi hàm là một bộ lỗi và nếu xảy ra sự cố, hàm sẽ gặp lỗi khi gửi lỗi vào bộ lỗi.

Nếu bộ lỗi có lỗi, không có chức năng nào thực thi. (Tôi có một macro ở trên cùng của mọi chức năng mà làm cho nó trở lại).

Vì vậy, nhiều malloc trông như thế này;

mem[0] = malloc_wrapper(error_set, resource_set, 100); 
mem[1] = malloc_wrapper(error_set, resource_set, 50); 
mem[2] = malloc_wrapper(error_set, resource_set, 20); 

Không cần để kiểm tra giá trị trả về, vì nếu một lỗi xảy ra, không có chức năng sau đây sẽ thực hiện, ví dụ mallocs sau không bao giờ xảy ra. Vì vậy, khi thời gian đến cho tôi để deallocate tài nguyên (nói ở phần cuối của một chức năng, nơi mà tất cả các nguồn lực được sử dụng nội bộ bởi chức năng đó đã được đặt vào một bộ), tôi deallocate bộ. Nó chỉ là một cuộc gọi hàm.

res_delete_set(resource_set); 

Tôi không cần phải kiểm tra đặc biệt cho lỗi - có không if() s trong mã của tôi kiểm tra giá trị trả lại, mà làm cho nó duy trì; Tôi thấy sự gia tăng của kiểm tra lỗi trong dòng phá hủy khả năng đọc và do đó bảo trì. Tôi chỉ có một danh sách các lệnh gọi hàm.

Đó là nghệ thuật, người đàn ông :-)

Các vấn đề liên quan