Làm thế nào để xóa [] biết đó là một mảng?


136

Được rồi, tôi nghĩ tất cả chúng ta đều đồng ý rằng những gì xảy ra với đoạn mã sau là không xác định, tùy thuộc vào những gì được thông qua,

void deleteForMe(int* pointer)
{
     delete[] pointer;
}

Con trỏ có thể là tất cả các loại khác nhau, và do đó thực hiện một điều kiện vô điều kiện delete[]trên nó là không xác định. Tuy nhiên, hãy giả sử rằng chúng ta thực sự đang vượt qua một con trỏ mảng,

int main()
{
     int* arr = new int[5];
     deleteForMe(arr);
     return 0;
}

Câu hỏi của tôi là, trong trường hợp con trỏ một mảng, ai là người biết điều này? Ý tôi là, từ quan điểm của ngôn ngữ / nhà biên dịch, không biết liệu có phải arrlà một con trỏ mảng so với một con trỏ đến một int không. Heck, nó thậm chí không biết liệu arrđã được tạo ra động. Tuy nhiên, nếu tôi làm như sau thay vào đó,

int main()
{
     int* num = new int(1);
     deleteForMe(num);
     return 0;
}

HĐH đủ thông minh để chỉ xóa một int và không thực hiện một số loại 'giết người' bằng cách xóa phần còn lại của bộ nhớ ngoài điểm đó (tương phản với strlenvà một \0chuỗi không bị hủy - nó sẽ tiếp tục cho đến khi đạt 0).

Vì vậy, công việc của ai là ghi nhớ những điều này? Hệ điều hành có giữ một số loại bản ghi trong nền không? (Ý tôi là, tôi nhận ra rằng tôi đã bắt đầu bài đăng này bằng cách nói rằng những gì xảy ra là không xác định, nhưng thực tế là, kịch bản 'giết người' không xảy ra, vì vậy trong thế giới thực tế có ai đó đang nhớ.)



6
nó biết từ dấu ngoặc vuông sau khi xóa
JoelFan

"Con trỏ là một mảng". Không, con trỏ không bao giờ là mảng. Họ thường chỉ đến phần tử đầu tiên của mảng, nhưng đó là một điều khác.
Aaron McDaid

Câu trả lời:


99

Trình biên dịch không biết đó là một mảng, nó tin tưởng vào lập trình viên. Xóa một con trỏ đến một single intvới delete []sẽ dẫn đến hành vi không xác định. main()Ví dụ thứ hai của bạn là không an toàn, ngay cả khi nó không sụp đổ ngay lập tức.

Trình biên dịch phải theo dõi xem có bao nhiêu đối tượng cần phải xóa bằng cách nào đó. Nó có thể làm điều này bằng cách phân bổ quá mức đủ để lưu trữ kích thước mảng. Để biết thêm chi tiết, xem C ++ Super FAQ .


14
Trên thực tế, sử dụng xóa [] để xóa một cái gì đó được tạo bằng mới là có thể khai thác. taossa.com/index.php/2007/01/03/ từ
Rodrigo

23
@Rodrigo Liên kết trong bình luận của bạn bị hỏng, nhưng may mắn là máy wayback có một bản sao của nó tại replay.web.archive.org/20080703153358/http://taossa.com/
David Gardner

103

Một câu hỏi mà các câu trả lời đưa ra cho đến nay dường như không giải quyết được: nếu các thư viện thời gian chạy (không phải HĐH, thực sự) có thể theo dõi số lượng thứ trong mảng thì tại sao chúng ta cần delete[]cú pháp? Tại sao một deletehình thức duy nhất không thể được sử dụng để xử lý tất cả các xóa?

Câu trả lời cho điều này quay trở lại nguồn gốc của C ++ như một ngôn ngữ tương thích với C (mà nó không còn thực sự phấn đấu nữa.) Triết lý của Stroustrup là lập trình viên không cần phải trả tiền cho bất kỳ tính năng nào mà họ không sử dụng. Nếu họ không sử dụng mảng, thì họ không cần phải mang chi phí của mảng đối tượng cho mỗi khối bộ nhớ được phân bổ.

Đó là, nếu mã của bạn chỉ đơn giản là

Foo* foo = new Foo;

sau đó, không gian bộ nhớ được phân bổ fookhông nên bao gồm bất kỳ chi phí phụ nào cần thiết để hỗ trợ các mảng Foo.

Vì chỉ phân bổ mảng được thiết lập để mang thông tin kích thước mảng bổ sung, nên bạn cần thông báo cho các thư viện thời gian chạy để tìm thông tin đó khi bạn xóa các đối tượng. Đó là lý do tại sao chúng ta cần sử dụng

delete[] bar;

Thay vì chỉ

delete bar;

nếu thanh là một con trỏ đến một mảng.

Đối với hầu hết chúng ta (bao gồm cả bản thân tôi), sự băn khoăn về một vài byte bộ nhớ có vẻ không ổn trong những ngày này. Nhưng vẫn còn một số tình huống trong đó việc lưu một vài byte (từ số khối bộ nhớ rất cao) có thể quan trọng.


20
"Sự băn khoăn về một vài byte bộ nhớ ngày nay dường như không còn nữa". May mắn thay, với những người như vậy, các mảng trần cũng bắt đầu trông kỳ lạ, vì vậy họ chỉ có thể sử dụng một vectơ hoặc boost :: mảng và quên xóa [] mãi mãi :-)
Steve Jessop

28

Có, HĐH giữ một số thứ trong 'nền'. Ví dụ: nếu bạn chạy

int* num = new int[5];

HĐH có thể phân bổ thêm 4 byte, lưu trữ kích thước của phân bổ trong 4 byte đầu tiên của bộ nhớ được phân bổ và trả về một con trỏ bù (nghĩa là nó phân bổ các không gian bộ nhớ 1000 đến 1024 nhưng con trỏ trả về các điểm 1004, với các vị trí 1000- 1003 lưu trữ kích thước của phân bổ). Sau đó, khi xóa được gọi, nó có thể nhìn vào 4 byte trước khi con trỏ chuyển đến nó để tìm kích thước của phân bổ.

Tôi chắc chắn rằng có nhiều cách khác để theo dõi quy mô phân bổ, nhưng đó là một lựa chọn.


26
+1 - điểm hợp lệ nói chung ngoại trừ thông thường thời gian chạy ngôn ngữ chịu trách nhiệm lưu trữ siêu dữ liệu này, không phải HĐH.
sharptooth

Điều gì xảy ra với kích thước của mảng hoặc kích thước của một đối tượng có mảng được xác định? Nó có hiển thị thêm 4 byte khi bạn thực hiện sizeof trên đối tượng đó không?
Shree

3
Không, sizeof chỉ hiển thị kích thước của mảng. Nếu thời gian chạy chọn thực hiện theo phương pháp mà tôi đã mô tả, đó hoàn toàn là một chi tiết triển khai và theo quan điểm của người dùng, điều đó cần được che dấu. Bộ nhớ trước con trỏ không 'thuộc về' người dùng và không được tính.
bsdfish

2
Quan trọng hơn, sizeof sẽ không trả về kích thước thật của mảng được phân bổ động trong mọi trường hợp. Nó chỉ có thể trả về kích thước được biết tại thời gian biên dịch.
bdonlan

Có thể sử dụng siêu dữ liệu này trong một vòng lặp for để lặp chính xác trên mảng không? ví dụ for(int i = 0; i < *(arrayPointer - 1); i++){ }
Sam

13

Điều này rất giống với câu hỏi này và nó có nhiều chi tiết bạn đang tìm kiếm.

Nhưng đủ để nói, nó không phải là công việc của HĐH để theo dõi bất kỳ điều này. Đó thực sự là các thư viện thời gian chạy hoặc trình quản lý bộ nhớ cơ bản sẽ theo dõi kích thước của mảng. Điều này thường được thực hiện bằng cách phân bổ thêm bộ nhớ lên phía trước và lưu trữ kích thước của mảng ở vị trí đó (hầu hết sử dụng nút đầu).

Điều này có thể xem được trên một số triển khai bằng cách thực thi đoạn mã sau

int* pArray = new int[5];
int size = *(pArray-1);

sẽ làm việc này? Trong windows & linux, chúng tôi không làm việc này.
bạn thân

1
size_t size = *(reinterpret_cast<size_t *>(pArray) - 1)thay vào đó hãy thử

9

deletehoặc delete[]có lẽ cả hai sẽ giải phóng bộ nhớ được cấp phát (bộ nhớ được chỉ ra), nhưng sự khác biệt lớn là deletetrên một mảng sẽ không gọi hàm hủy của từng phần tử của mảng.

Dù sao, trộn new/new[]delete/delete[]có lẽ là UB.


1
Rõ ràng, ngắn gọn và câu trả lời hữu ích nhất!
GntS

6

Nó không biết đó là một mảng, đó là lý do tại sao bạn phải cung cấp delete[]thay vì cũ thông thường delete.


5

Tôi đã có một câu hỏi tương tự như thế này. Trong C, bạn cấp phát bộ nhớ với malloc () (hoặc một chức năng tương tự khác) và xóa nó bằng free (). Chỉ có một malloc (), chỉ đơn giản phân bổ một số byte nhất định. Chỉ có một free (), chỉ đơn giản lấy một con trỏ làm tham số.

Vậy tại sao trong C bạn chỉ có thể trao con trỏ miễn phí, nhưng trong C ++, bạn phải cho nó biết đó là một mảng hay một biến duy nhất?

Câu trả lời, tôi đã học được, phải làm với các hàm hủy lớp.

Nếu bạn phân bổ một thể hiện của một lớp MyClass ...

classes = new MyClass[3];

Và xóa nó bằng xóa, bạn chỉ có thể nhận được hàm hủy cho phiên bản đầu tiên của MyClass được gọi. Nếu bạn sử dụng xóa [], bạn có thể yên tâm rằng hàm hủy sẽ được gọi cho tất cả các thể hiện trong mảng.

Đây là sự khác biệt quan trọng. Nếu bạn chỉ đơn giản làm việc với các loại tiêu chuẩn (ví dụ int), bạn sẽ không thực sự thấy vấn đề này. Ngoài ra, bạn nên nhớ rằng hành vi sử dụng xóa trên mới [] và xóa [] trên mới là không xác định - nó có thể không hoạt động theo cùng một cách trên mọi trình biên dịch / hệ thống.


3

Tùy thuộc vào thời gian chạy chịu trách nhiệm phân bổ bộ nhớ, giống như cách bạn có thể xóa một mảng được tạo bằng malloc trong tiêu chuẩn C bằng cách sử dụng miễn phí. Tôi nghĩ mỗi trình biên dịch thực hiện nó khác nhau. Một cách phổ biến là phân bổ một ô phụ cho kích thước mảng.

Tuy nhiên, thời gian chạy không đủ thông minh để phát hiện xem đó là mảng hay con trỏ, bạn phải thông báo cho nó và nếu bạn nhầm, bạn không xóa chính xác (Ví dụ: ptr thay vì mảng) hoặc cuối cùng bạn lấy một giá trị không liên quan đến kích thước và gây ra thiệt hại đáng kể.


3

Một trong những cách tiếp cận cho trình biên dịch là phân bổ thêm một chút bộ nhớ và lưu trữ số lượng phần tử trong phần tử head.

Ví dụ về cách nó có thể được thực hiện: Tại đây

int* i = new int[4];

trình biên dịch sẽ phân bổ sizeof (int) * 5 byte.

int *temp = malloc(sizeof(int)*5)

Sẽ lưu trữ 4trong sizeof(int)byte đầu tiên

*temp = 4;

và thiết lập i

i = temp + 1;

Vì vậy, ichỉ vào mảng của 4 phần tử, không phải 5.

delete[] i;

sẽ được xử lý theo cách sau

int *temp = i - 1;
int numbers_of_element = *temp; // = 4
... call destructor for numbers_of_element elements if needed
... that are stored in temp + 1, temp + 2, ... temp + 4
free (temp)

1

Về mặt ngữ nghĩa, cả hai phiên bản toán tử xóa trong C ++ đều có thể "ăn" bất kỳ con trỏ nào; tuy nhiên, nếu một con trỏ tới một đối tượng được đưa ra delete[], thì UB sẽ có kết quả, có nghĩa là bất cứ điều gì có thể xảy ra, bao gồm cả sự cố hệ thống hoặc không có gì cả.

C ++ yêu cầu lập trình viên chọn phiên bản phù hợp của toán tử xóa tùy thuộc vào đối tượng xử lý: mảng hoặc đối tượng đơn.

Nếu trình biên dịch có thể tự động xác định xem một con trỏ được truyền cho toán tử xóa có phải là một mảng con trỏ hay không, thì sẽ chỉ có một toán tử xóa trong C ++, điều này đủ cho cả hai trường hợp.


1

Đồng ý rằng trình biên dịch không biết nó có phải là một mảng hay không. Đó là tùy thuộc vào các lập trình viên.

Trình biên dịch đôi khi theo dõi có bao nhiêu đối tượng cần phải xóa bằng cách phân bổ quá mức đủ để lưu trữ kích thước mảng, nhưng không phải lúc nào cũng cần thiết.

Để biết thông số kỹ thuật đầy đủ khi phân bổ thêm dung lượng, vui lòng tham khảo C ++ ABI (cách trình biên dịch được triển khai): Itanium C ++ ABI: Trình điều khiển mảng mới Cookies


Tôi chỉ muốn mọi trình biên dịch quan sát một số tài liệu ABI cho C ++. +1 cho liên kết mà tôi đã truy cập trước đây. Cảm ơn.
Don Wakefield

0

Bạn không thể sử dụng xóa cho một mảng và bạn không thể sử dụng xóa [] cho một mảng không.


8
Tôi nghĩ bạn không nên , vì trình biên dịch trung bình của bạn sẽ không phát hiện ra sự lạm dụng.
Don Wakefield

0

"hành vi không xác định" chỉ đơn giản có nghĩa là thông số ngôn ngữ làm cho không có người bảo vệ nào về những gì sẽ xảy ra. Điều đó không có nghĩa là điều gì đó xấu sẽ xảy ra.

Vì vậy, công việc của ai là ghi nhớ những điều này? Hệ điều hành có giữ một số loại bản ghi trong nền không? (Ý tôi là, tôi nhận ra rằng tôi đã bắt đầu bài đăng này bằng cách nói rằng những gì xảy ra là không xác định, nhưng thực tế là, kịch bản 'giết người' không xảy ra, vì vậy trong thế giới thực tế có ai đó đang nhớ.)

Thông thường có hai lớp ở đây. Trình quản lý bộ nhớ cơ bản và triển khai C ++.

Nói chung, trình quản lý bộ nhớ sẽ ghi nhớ (trong số những thứ khác) kích thước của khối bộ nhớ được phân bổ. Điều này có thể lớn hơn khối triển khai C ++ yêu cầu. Thông thường, trình quản lý bộ nhớ sẽ lưu trữ siêu dữ liệu của nó trước khối bộ nhớ được phân bổ.

Việc triển khai C ++ nói chung sẽ chỉ nhớ kích thước của mảng nếu nó cần làm như vậy cho mục đích riêng của nó, điển hình là vì kiểu này có hàm hủy không phải là trival.

Vì vậy, đối với các loại có hàm hủy tầm thường, việc thực hiện "xóa" và "xóa []" thường giống nhau. Việc thực hiện C ++ chỉ đơn giản là chuyển con trỏ đến trình quản lý bộ nhớ bên dưới. Cái gì đó như

free(p)

Mặt khác, đối với các loại có hàm hủy không tầm thường "xóa" và "xóa []" có thể khác nhau. "xóa" sẽ là một cái gì đó giống như (trong đó T là loại mà con trỏ trỏ đến)

p->~T();
free(p);

Trong khi "xóa []" sẽ là một cái gì đó như.

size_t * pcount = ((size_t *)p)-1;
size_t count = *count;
for (size_t i=0;i<count;i++) {
  p[i].~T();
}
char * pmemblock = ((char *)p) - max(sizeof(size_t),alignof(T));
free(pmemblock);

-1

lặp qua mảng các đối tượng và gọi hàm hủy cho từng đối tượng. Tôi đã tạo phù thủy mã đơn giản này làm quá tải các biểu thức [] và xóa [] mới và cung cấp một hàm mẫu để phân bổ bộ nhớ và gọi hàm hủy cho mỗi đối tượng nếu cần:

// overloaded new expression 
void* operator new[]( size_t size )
{
    // allocate 4 bytes more see comment below 
    int* ptr = (int*)malloc( size + 4 );

    // set value stored at address to 0 
    // and shift pointer by 4 bytes to avoid situation that
    // might arise where two memory blocks 
    // are adjacent and non-zero
    *ptr = 0;
    ++ptr; 

    return ptr;
}
//////////////////////////////////////////

// overloaded delete expression 
void static operator delete[]( void* ptr )
{
    // decrement value of pointer to get the
    // "Real Pointer Value"
    int* realPtr = (int*)ptr;
    --realPtr;

    free( realPtr );
}
//////////////////////////////////////////

// Template used to call destructor if needed 
// and call appropriate delete 
template<class T>
void Deallocate( T* ptr )
{
    int* instanceCount = (int*)ptr;
    --instanceCount;

    if(*instanceCount > 0) // if larger than 0 array is being deleted
    {
        // call destructor for each object
        for(int i = 0; i < *instanceCount; i++)
        {
            ptr[i].~T();
        }
        // call delete passing instance count witch points
        // to begin of array memory 
        ::operator delete[]( instanceCount );
    }
    else
    {
        // single instance deleted call destructor
        // and delete passing ptr
        ptr->~T();
        ::operator delete[]( ptr );
    }
}

// Replace calls to new and delete
#define MyNew ::new
#define MyDelete(ptr) Deallocate(ptr)

// structure with constructor/ destructor
struct StructureOne
{
    StructureOne():
    someInt(0)
    {}
    ~StructureOne() 
    {
        someInt = 0;
    }

    int someInt;
};
//////////////////////////////

// structure without constructor/ destructor
struct StructureTwo
{
    int someInt;
};
//////////////////////////////


void main(void)
{
    const unsigned int numElements = 30;

    StructureOne* structOne = nullptr;
    StructureTwo* structTwo = nullptr;
    int* basicType = nullptr;
    size_t ArraySize = 0;

/**********************************************************************/
    // basic type array 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( int ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor. value assigned to basicType pointer
    // will be the same as value of "++ptr" in new expression
    basicType = MyNew int[numElements];

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( int ) * numElements"
    MyDelete( basicType );

/**********************************************************************/
    // structure without constructor and destructor array 

    // behavior will be the same as with basic type 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( StructureTwo ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor value assigned to structTwo pointer
    // will be the same as value of "++ptr" in new expression
    structTwo = MyNew StructureTwo[numElements]; 

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( StructureTwo ) * numElements"
    MyDelete( structTwo );

/**********************************************************************/
    // structure with constructor and destructor array 

    // place break point check size and compare it with size passed in
    // new expression size in expression will be larger by 4 bytes
    ArraySize = sizeof( StructureOne ) * numElements;

    // value assigned to "structOne pointer" will be different 
    // of "++ptr" in new expression  "shifted by another 4 bytes"
    structOne = MyNew StructureOne[numElements];

    // Place break point in template function to see the behavior
    // destructors will be called for each array object 
    MyDelete( structOne );
}
///////////////////////////////////////////

-2

chỉ cần định nghĩa một hàm hủy bên trong một lớp và thực thi mã của bạn bằng cả hai cú pháp

delete pointer

delete [] pointer

theo đầu ra u có thể tìm ra giải pháp


sử dụng xóa [] khi bạn mới tạo một kiểu mảng. ví dụ int * a = new int; int * b = new int [5]; xóa a; xóa [] b;
Lineesh K Mohan

-3

Câu trả lời:

int * pArray = new int [5];

int size = * (pArray-1);

Đăng ở trên là không chính xác và tạo ra giá trị không hợp lệ. "-1" đếm các phần tử Trên HĐH Windows 64 bit, kích thước bộ đệm chính xác nằm trong địa chỉ Ptr - 4 byte

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.