2014-12-16 19 views
18

Đối với đoạn mã sau:Tại sao int [n] mới hợp lệ khi int array [n] thì không?

foo(int n){ 
    int array[n]; 
} 

tôi hiểu rằng đây là cú pháp hợp lệ và rằng nó là không hợp lệ vì c tiêu chuẩn ++ đòi hỏi kích thước mảng được đặt ở thời gian biên dịch (mặc dù một số trình biên dịch hỗ trợ cú pháp sau).

Tuy nhiên tôi cũng hiểu được những điều sau đây là cú pháp hợp lệ:

bar(int n){ 
    int *array = new int[n]; 
} 

Tôi không hiểu tại sao điều này được cho phép, không phải là nó giống như tạo ra một mảng mà kích thước được xác định tại thời gian chạy? Có thực hành tốt để làm điều này hay tôi nên sử dụng một véc tơ nếu tôi cần phải làm điều này thay thế?

+3

Vì vậy, được gọi là mảng độ dài biến đổi. Dưới đây là một số câu trả lời tại sao chúng không được phép trong C++ -> https://groups.google.com/forum/#!topic/comp.std.c++/K_4lgA1JYeg Chúng hợp lệ trong C BTW. – doc

+7

Trường hợp thứ hai được phân bổ heap, trong khi trường hợp thứ nhất (có giá trị trong C99) sẽ được phân bổ theo từng ngăn xếp. – Thilo

+1

Trong phiên bản C hỗ trợ VLAs, 'int array [n];' tạo một đối tượng mảng có kiểu thực sự là 'int [n]'. 'new int [n]' không tạo ra một kiểu mảng; nó chỉ sinh ra một con trỏ 'int *' trỏ đến phần tử đầu tiên của mảng được phân bổ. –

Trả lời

26

Đó là vì trước đây được phân bổ trên ngăn xếp và sau đó được phân bổ trên heap.

Khi bạn phân bổ thứ gì đó trên ngăn xếp, biết kích thước của đối tượng là điều cần thiết để xây dựng chính xác nó. C99 cho phép kích thước được xác định tại thời gian chạy, và điều này giới thiệu một số biến chứng trong việc xây dựng và tháo dỡ ngăn xếp nói trên, vì bạn không thể tính toán kích thước của nó tại thời gian biên dịch. Mã máy phải được phát ra để thực hiện việc tính toán trong khi thực hiện chương trình. Đây có lẽ là lý do chính khiến tính năng này không được bao gồm trong tiêu chuẩn C++.²

Ngược lại, heap không có cấu trúc cố định, như tên của nó. Các khối có kích thước bất kỳ có thể được cấp phát không có thứ tự cụ thể, miễn là chúng không chồng lên nhau và bạn có đủ bộ nhớ (ảo)¹. Trong trường hợp này, biết kích thước tại thời gian biên dịch không phải là có liên quan.

Ngoài ra, hãy nhớ rằng ngăn xếp có kích thước giới hạn, chủ yếu là để phát hiện các lần thu thập vô hạn trước khi chúng tiêu thụ tất cả bộ nhớ khả dụng. Thông thường giới hạn được cố định khoảng 1MB và bạn hiếm khi đạt được điều đó. Trừ khi bạn phân bổ các đối tượng lớn, cần được đặt trong heap.

Vì những gì bạn nên sử dụng, có thể là std::vector<int>. Nhưng nó thực sự phụ thuộc vào những gì bạn đang cố gắng làm.

Cũng lưu ý rằng C++ 11 có lớp std::array, có kích thước phải được biết lúc biên dịch.C++ 14 nên đã giới thiệu std::dynarray, nhưng nó đã bị trì hoãn vì vẫn còn nhiều việc phải làm liên quan đến việc phân bổ ngăn xếp kích thước không xác định thời gian biên dịch.


¹ khối thường được phân bổ tuần tự vì lý do hiệu suất, nhưng điều đó không bắt buộc.

² như đã chỉ ra, biết kích thước tại thời gian biên dịch không phải là một yêu cầu khó khăn, nhưng nó làm cho mọi việc đơn giản hơn.

+0

Tôi sẽ chào đón 'std :: dynarray' và nó có thể được phân bổ như' std :: vector' trên heap, bởi vì 'std :: vector' không phải lúc nào bạn cần (nó có thể phát triển, yêu cầu các kiểu di chuyển/có thể sao chép được) nhà thầu vv). – doc

+0

@doc: Có vẻ như bạn muốn 'std :: unique_ptr '. Có thể được tạo với kích thước thời gian chạy, nhưng kích thước không thể thay đổi sau khi tạo, nội dung sẽ không được di chuyển. Tuy nhiên, không hỗ trợ các nhà xây dựng mặc định. –

+0

@Stefano Sanfilippo: "Đó là bởi vì trước đây được phân bổ trên ngăn xếp và thứ hai trên heap." Điều này đơn giản là sai. Các tiêu chuẩn đồng bằng c thậm chí không sử dụng các từ "ngăn xếp" hoặc "đống" trong đó. và C++ chỉ làm giảm chúng cho các chức năng gọi là chuẩn của nó. Nhưng có COULD là một trình biên dịch sẽ làm điều đó ngược lại và nó vẫn sẽ hoàn toàn tôn trọng các tiêu chuẩn. Whats thậm chí còn hợp lý hơn: mã C có thể chạy trong môi trường mà thậm chí không hỗ trợ stack/heap. Vì vậy, có thể nó thậm chí không được tôn trọng. Tôi disscussed nó ở đây: http://stackoverflow.com/q/27298414/2003898 – dhein

5

int array[n] phân bổ mảng cố định trên ngăn xếp cuộc gọi tại thời điểm biên dịch và do đó n cần được biết tại thời gian biên dịch (trừ khi phần mở rộng của trình biên dịch cụ thể được sử dụng để cho phép phân bổ trong thời gian chạy) vẫn còn trên ngăn xếp).

int *array = new int[n] phân bổ mảng động dài trên heap tại thời gian chạy, do đó, n không cần phải biết lúc biên dịch.

8

Trong trường hợp đầu tiên, bạn phân bổ không gian bộ nhớ tĩnh để giữ số nguyên. Điều này được thực hiện khi chương trình được biên dịch và do đó dung lượng lưu trữ không linh hoạt.

Trong trường hợp sau, bạn là động phân bổ dung lượng bộ nhớ để giữ số nguyên. Điều này được thực hiện khi chương trình được chạy và do đó dung lượng lưu trữ cần thiết có thể linh hoạt.

Cuộc gọi thứ hai thực sự là một chức năng nói chuyện với hệ điều hành để đi và tìm một nơi trong bộ nhớ để sử dụng. Quá trình tương tự đó không xảy ra trong trường hợp đầu tiên.

+0

Không tìm thấy phân bổ tĩnh, chỉ có số tiền được xác định tĩnh để phân bổ tự động trên ngăn xếp. – Deduplicator

3

Bạn có thể cấp phát bộ nhớ tĩnh trên stack hoặc động trên heap.

Trong trường hợp đầu tiên của bạn, chức năng của bạn chứa một lời tuyên bố của một mảng với chiều dài biến thể, nhưng điều này là không thể, kể từ mảng thô phải có kích thước cố định tại thời gian biên dịch, bởi vì họ được phân bổ trên ngăn xếp. Vì lý do này, kích thước của chúng phải được chỉ định dưới dạng hằng số, ví dụ: 5. Bạn có thể có một cái gì đó như thế này:

foo(){ 
    int array[5]; // raw array with fixed size 5 
} 

Sử dụng con trỏ, bạn có thể chỉ định kích thước biến cho bộ nhớ sẽ được chỉ vì bộ nhớ này sẽ được phân bổ tự động trên heap. Trong trường hợp thứ hai của bạn, bạn đang sử dụng tham số n để chỉ định dung lượng bộ nhớ sẽ được cấp phát.

Kết luận, chúng ta có thể nói rằng con trỏ không phải là mảng: cấp phát bộ nhớ sử dụng một con trỏ được phân bổ trên đống, trong khi cấp phát bộ nhớ cho một mảng nguyên được phân bổ trên chồng.

Có lựa chọn thay thế tốt để mảng thô, ví dụ như các container tiêu chuẩn vector, mà về cơ bản là một container với kích thước chiều dài thay đổi.

Hãy chắc chắn rằng bạn hiểu rõ sự khác biệt giữa cấp phát bộ nhớ động và tĩnh, các difference giữa bộ nhớ được phân bổ trên chồng và cấp phát bộ nhớ trên đống.

3

Điều này là do ngôn ngữ C++ không có tính năng C được giới thiệu trong C99 được gọi là "mảng độ dài biến đổi" (VLA).

C++ đang tụt hậu trong việc áp dụng tính năng C này vì loại std::vector từ thư viện của nó đáp ứng hầu hết các yêu cầu.

Hơn nữa, tiêu chuẩn năm 2011 của C đã bị đảo ngược và làm VLA là một tính năng tùy chọn.

VLA, trong Tóm lại, cho phép bạn sử dụng một giá trị thời gian chạy để xác định kích thước của một mảng địa phương được phân bổ trong lưu trữ tự động:

int func(int variable) 
{ 
    long array[variable]; // VLA feature 

    // loop over array 

    for (size_t i = 0; i < sizeof array/sizeof array[0]; i++) { 
    // the above sizeof is also a VLA feature: it yields the dynamic 
    // size of the array, and so is not a compile-time constant, 
    // unlike other uses of sizeof! 
    } 
} 

VLA của tồn tại trong phương ngữ GNU C dài trước C99. Trong các phương ngữ của C không có VLA, các tham số mảng trong một khai báo phải là các biểu thức không đổi.

Ngay cả trong các phương ngữ C với VLA, chỉ một số mảng nhất định có thể là VLA. Ví dụ, mảng tĩnh không thể, và không thể mảng động (ví dụ mảng bên trong một cấu trúc, ngay cả khi trường hợp của cấu trúc đó được phân bổ động).

Trong mọi trường hợp, vì bạn đang mã hóa bằng C++, đây là tranh luận!

Lưu ý rằng bộ nhớ được phân bổ với operator new không phải là tính năng VLA. Đây là một cú pháp C++ đặc biệt để phân bổ động, mà trả về một kiểu con trỏ, như bạn đã biết:

int *p = new int[variable]; 

Không giống như một của VLA, đối tượng này sẽ kéo dài cho đến khi nó bị phá hủy một cách rõ ràng với delete [], và có thể được trả lại từ xung quanh phạm vi.

+0

@MooingDuck Làm thế nào? – Kaz

+0

Theo kinh nghiệm của tôi, 'new T [n]' là khá nhiều định nghĩa của một mảng động " ". Bạn nói rằng các mảng động không thể là VLAs, nhưng theo cách tôi nhìn thấy, tất cả các mảng động là VLAs. Câu này cũng ngụ ý rằng các mảng bên trong một cấu trúc là các mảng động, nhưng nó không khớp với định nghĩa mà tôi quen thuộc. Một mảng bên trong một cấu trúc đôi khi có thể có một phạm vi động đôi khi, nhưng thậm chí là đẩy nó. –

+0

@MooingDuck Rõ ràng là các nhận xét của tôi về tính năng mảng biến chiều dài ISO C 1999 và không phải về bất kỳ mảng nào trong bất kỳ ngôn ngữ nào có độ dài bằng cách nào đó biến đổi. – Kaz

5

Câu trả lời duy nhất và hợp lệ cho câu hỏi của bạn là, vì tiêu chuẩn nói như vậy.

Ngược lại với C99, C++ không bao giờ muốn chỉ định các mảng độ dài biến đổi (VLAs), do đó cách duy nhất để nhận mảng có kích thước khác nhau là sử dụng phân bổ động, với malloc, new hoặc một số trình quản lý bộ nhớ khác.

Tính công bằng đối với C++, có phân bổ theo thời gian chạy có kích thước hơi làm phức tạp việc mở ngăn xếp, điều này cũng sẽ làm cho việc xử lý ngoại lệ cho các chức năng sử dụng tính năng do đó khó chịu hơn.

Dù sao, ngay cả khi trình biên dịch của bạn cung cấp tính năng C99 làm tiện ích mở rộng, bạn nên luôn giữ chặt mắt trên mức sử dụng ngăn xếp của mình:
Không có cách nào để phục hồi từ việc thổi giới hạn ngăn xếp và trường hợp lỗi chỉ đơn giản là trái Hành vi không xác định vì một lý do.

Cách đơn giản nhất để mô phỏng Vlas trong C++, mặc dù không thực hiện lợi ích của việc tránh phân bổ động (và nguy cơ thổi quá giới hạn):

unique_ptr<T[]> array{new T[n]}; 
+1

Nó không phải là quá nhiều một vấn đề "không bận tâm" vì VLAs đang tích cực có hại trong một số khía cạnh. –

+0

Vâng, nó làm cho việc giải quyết stack (và do đó cũng xử lý ngoại lệ) đối với những hàm có VLA liên quan nhiều hơn một chút. Đó không phải là quá xấu mặc dù. – Deduplicator

+0

Điều đó và như bạn đã đề cập, không có cách nào để chỉ ra lỗi phân bổ. –

4

Không, thứ hai là không khai báo một mảng. Nó đang sử dụng dạng mảng operator new và đặc biệt cho phép thứ nguyên đầu tiên có thể thay đổi.

4

Bởi vì nó có ngữ nghĩa khác nhau:

Nếu n là một hằng số thời gian biên dịch (không giống như trong ví dụ của bạn):

int array[n]; //valid iff n is compile-time constant, space known at compile-time 

Nhưng xem xét khi n là một giá trị thời gian chạy:

int array[n]; //Cannot have a static array with a runtime value in C++ 
int * array = new int[n]; //This works because it happens at run-time, 
          // not at compile-time! Different semantics, similar syntax. 

Trong C99, bạn có thể có thời gian chạy n cho một mảng và không gian sẽ được tạo trong ngăn xếp khi chạy. Có một số đề xuất cho các phần mở rộng tương tự trong C++, nhưng không có đề xuất nào trong số đó được đưa vào tiêu chuẩn.

4

Trong biểu

new int[n] 

int[n] không phải là loại.C++ xử lý "new với mảng" và "new với mảng không" khác nhau. Các N3337 tiêu chuẩn dự thảo có này để nói về new:

Khi các đối tượng được phân bổ là một mảng (có nghĩa là, các noptr-new-declarator cú pháp được sử dụng hoặc kiểu mới-id hoặc type-id biểu thị loại mảng), biểu thức mới mang lại một con trỏ tới phần tử ban đầu (nếu có) của mảng.

Các noptr-new-declarator đề cập đến trường hợp đặc biệt này (đánh giá n và tạo ra các mảng có kích thước này), xem:

noptr-new-declarator:

        [biểu thức] attribute-specifier-seq chọn

        noptr-new-declarator [liên tục thể hiện] thuộc tính-specifier-seq opt

Tuy nhiên bạn không thể sử dụng này trong tờ khai "bình thường" như

int array[n]; 

hoặc trong typedef

typedef int variable_array[n]; 

Điều này khác với VLGT C99, cả hai đều được phép.

Tôi có nên sử dụng véc tơ thay thế không?

Có, bạn nên làm như vậy. Bạn nên sử dụng vectơ mọi lúc, trừ khi bạn có một lý do rất mạnh để làm khác (có một lần trong 7 năm qua khi tôi sử dụng new - khi tôi đang thực hiện vector để phân định trường).

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