Chương trình điền vào một mảng c. Lập trình trong C: Sử dụng mảng dữ liệu trong C. Biển chỉ dẫn

Mảng trong C là gì?

Cách khai báo mảng trong C?

Làm cách nào để khởi tạo mảng trong C?

Mảng trong C dành cho người giả.

Mảng trong C

Mảng trong C là tập hợp các phần tử cùng loại có thể được truy cập bằng chỉ mục. Các phần tử của mảng trong C lần lượt được đặt trong bộ nhớ của máy tính.

Một ví dụ đơn giản về tạo và điền một mảng trong C:

// @author Subbotin B.P..h> void main(void) ( int nArr; nArr = 1; nArr = 2; nArr = 3; printf("\n\tArray\n\n"); printf("nArr\t =\t%d\n", nArr); printf("nArr\t=\t%d\n", nArr); printf("nArr\t=\t%d\n", nArr); return 0 ; )

Chúng tôi nhận được:

Trong ví dụ này, chúng ta khai báo một mảng chứa các phần tử kiểu int:

ở đây tên mảng là nArr, số phần tử mảng là 3, kiểu phần tử mảng là int.

Mảng là một tập hợp các phần tử. Mỗi phần tử của mảng có thể được gọi bằng số của nó. Con số này thường được gọi là chỉ số. Các phần tử mảng được đánh số bắt đầu từ số 0. Hãy gán một giá trị cho phần tử đầu tiên của mảng và phần tử đầu tiên có chỉ số bằng 0:

Hãy gán một giá trị cho phần tử thứ hai của mảng và phần tử thứ hai có chỉ mục một:

Hãy gán một giá trị cho phần tử thứ ba của mảng và phần tử thứ ba có chỉ số hai:

Khi hiển thị các phần tử mảng trên màn hình, chúng ta sẽ nhận được giá trị của chúng. Như thế này:

printf("nArr\t=\t%d\n", nArr);

Để lấy một phần tử của mảng, bạn cần chỉ định tên mảng và chỉ mục của phần tử:

Đây là phần tử đầu tiên của mảng vì phần tử đầu tiên có chỉ số bằng 0.

Hãy gán giá trị của phần tử thứ ba của mảng cho biến int a:

chỉ số của phần tử thứ ba của mảng là hai, vì các chỉ số được tính từ 0.

Bây giờ quy tắc chung để khai báo mảng trong C: khi khai báo một mảng, bạn cần cho biết tên mảng, loại phần tử và số lượng phần tử. Số phần tử là số tự nhiên, tức là toàn bộ là tích cực. Số 0 không thể là số phần tử. Bạn không thể chỉ định số lượng phần tử mảng có thể thay đổi. Dưới đây là ví dụ về khai báo mảng trong C:

int nArr; // Một mảng đã được khai báo để chứa một trăm số nguyên;
phao fArr; // Một mảng được thiết kế để lưu trữ 5 số float đã được khai báo;
char cArr; // Một mảng đã được khai báo để lưu trữ hai ký tự;

Sẽ là một sai lầm khi khai báo một mảng có số phần tử thay đổi:

Int varElem;
int nArr; // Lỗi! Số lượng phần tử không thể được đặt thành một biến;

Nhưng bạn có thể đặt số phần tử có giá trị không đổi: số nguyên dương trực tiếp 1, 2, 3... hoặc hằng số:

Const int arrayLength = 3;
int nArr;

Khi khai báo một mảng trong C, bạn có thể khởi tạo ngay:

int nMassiv = (1, 2, 3);

Bạn có thể bỏ qua số phần tử mảng trong ngoặc vuông nếu tất cả các phần tử mảng được khởi tạo:

int nMassiv = (1, 2, 3);

số phần tử sẽ được xác định tự động trong trường hợp này.

Bạn chỉ có thể định nghĩa một phần các phần tử của mảng khi khai báo nó:

int nMassiv = (1, 2);

trong ví dụ này, hai phần tử đầu tiên của mảng được khởi tạo, nhưng phần tử thứ ba không được xác định.

Ví dụ về mảng ký tự:

char cArr = ("S", "B", "P");

Khi khai báo một mảng, bạn không thể chỉ định số phần tử của một biến. Nhưng bạn có thể sử dụng các biến khi truy cập các phần tử mảng:

Int ind = 0;
char cr = cArr;

Điều này được sử dụng khi làm việc với các vòng lặp. Ví dụ:

// @author Subbotin B.P..h> void main(void) ( const int arrayLength = 3; int nArr; for(int inn = 0; inn< 3; inn++) { nArr = inn + 1; } printf("\n\tArray\n\n"); for(int inn = 0; inn < 3; inn++) { printf("nArr[%d]\t=\t%d\n", inn, nArr); } return 0; }

Trong ví dụ, trong vòng lặp đầu tiên, chúng ta điền vào mảng các phần tử kiểu int và trong vòng lặp thứ hai, chúng ta hiển thị các phần tử này trên màn hình.

  • Hướng dẫn

Trong bài đăng này, tôi sẽ cố gắng hiểu các khái niệm tinh tế như vậy trong C và C++ như con trỏ, tham chiếu và mảng. Cụ thể mình sẽ trả lời câu hỏi mảng trong C có phải là con trỏ hay không.

Ký hiệu và giả định

  • Tôi sẽ cho rằng người đọc hiểu rằng, ví dụ, C++ có tài liệu tham khảo, nhưng C thì không, vì vậy tôi sẽ không liên tục nhắc tôi đang nói đến ngôn ngữ nào (C/C++ hoặc C++), người đọc sẽ hiểu điều này từ ngữ cảnh;
  • Ngoài ra, tôi cho rằng người đọc đã biết C và C++ ở mức cơ bản và biết, chẳng hạn như cú pháp khai báo một liên kết. Trong bài đăng này, tôi sẽ phân tích tỉ mỉ những điều nhỏ nhặt;
  • Tôi sẽ biểu thị các loại như cách khai báo biến TYPE của loại tương ứng. Ví dụ mình sẽ ký hiệu kiểu “mảng có độ dài 2 int” là int TYPE;
  • Tôi sẽ giả định rằng chúng ta chủ yếu xử lý các loại dữ liệu thông thường như int TYPE , int *TYPE , v.v., trong đó các toán tử =, &, * và các toán tử khác không bị ghi đè và biểu thị những thứ thông thường;
  • "Đối tượng" sẽ luôn có nghĩa là "bất cứ thứ gì không phải là tham chiếu", không phải "một thể hiện của một lớp";
  • Ở mọi nơi, ngoại trừ những nơi được nêu cụ thể, C89 và C++98 đều được ngụ ý.

Con trỏ và liên kết

Biển chỉ dẫn. Tôi sẽ không nói cho bạn biết con trỏ là gì. :) Giả sử rằng bạn biết điều này. Hãy để tôi nhắc bạn những điều sau (tất cả các ví dụ mã được giả định là nằm bên trong một số hàm, ví dụ: main):

Int x; int *y = // Bạn có thể lấy địa chỉ từ bất kỳ biến nào bằng cách sử dụng thao tác lấy địa chỉ "&". Thao tác này trả về một con trỏ int z = *y; // Một con trỏ có thể được hủy đăng ký bằng cách sử dụng thao tác hủy đăng ký "*". Thao tác này trả về đối tượng được trỏ tới bởi con trỏ

Tôi cũng xin nhắc bạn điều sau: char luôn chính xác là một byte và trong tất cả các tiêu chuẩn C và C++ sizeof (char) == 1 (nhưng các tiêu chuẩn không đảm bảo rằng một byte chứa chính xác 8 bit :)). Hơn nữa, nếu bạn thêm một số vào một con trỏ thuộc loại T nào đó, thì giá trị số thực của con trỏ này sẽ tăng theo số này nhân với sizeof (T) . Nghĩa là, nếu p thuộc loại T *TYPE , thì p + 3 tương đương với (T *)((char *)p + 3 * sizeof (T)) . Những cân nhắc tương tự cũng áp dụng cho phép trừ.

Liên kết. Bây giờ về các liên kết. Liên kết cũng giống như con trỏ, nhưng có cú pháp khác và một số khác biệt quan trọng khác, sẽ được thảo luận sau. Đoạn mã sau không khác gì đoạn mã trước, ngoại trừ việc nó sử dụng các liên kết thay vì con trỏ:
int x; int &y = x; int z = y;

Nếu có một tham chiếu ở bên trái dấu gán thì không có cách nào để biết liệu chúng ta muốn gán cho chính tham chiếu đó hay cho đối tượng mà nó đề cập đến. Do đó, phép gán như vậy luôn gán cho một đối tượng chứ không phải cho một tham chiếu. Nhưng điều này không áp dụng cho việc khởi tạo liên kết: tất nhiên, chính liên kết đó đã được khởi tạo. Do đó, một khi một tham chiếu được khởi tạo, không có cách nào để thay đổi chính nó, tức là tham chiếu luôn tồn tại lâu dài (nhưng không phải là đối tượng của nó).

Giá trị. Những biểu thức có thể được gán đó được gọi là giá trị trong C, C++ và nhiều ngôn ngữ khác (đây là viết tắt của “giá trị bên trái”, tức là ở bên trái của dấu bằng). Các biểu thức còn lại được gọi là giá trị. Tên biến rõ ràng là giá trị, nhưng chúng không phải là tên duy nhất. Các biểu thức a, some_struct.some_field, *ptr, *(ptr + 3) cũng là các giá trị.

Thực tế đáng ngạc nhiên là theo một nghĩa nào đó, các tham chiếu và giá trị là giống nhau. Hãy suy đoán. Giá trị là gì? Đó là thứ có thể chiếm đoạt được. Nghĩa là, đây là một loại vị trí cố định trong bộ nhớ, nơi bạn có thể đặt thứ gì đó. Đó là, địa chỉ. Đó là, một con trỏ hoặc một liên kết (như chúng ta đã biết, con trỏ và liên kết là hai cách khác nhau về mặt cú pháp để thể hiện khái niệm địa chỉ trong C++). Hơn nữa, nó giống một liên kết hơn là một con trỏ, vì liên kết có thể được đặt ở bên trái của dấu bằng và điều này có nghĩa là một phép gán cho đối tượng mà liên kết trỏ tới. Vì vậy, lvalue là một tài liệu tham khảo.

Được rồi, nhưng (hầu hết mọi) biến cũng có thể ở bên trái dấu bằng. Vì vậy, (như vậy) một biến là một tham chiếu? Hầu hết. Một biểu thức đại diện cho một biến là một tham chiếu.

Nói cách khác, giả sử chúng ta đã khai báo int x . Bây giờ x là biến int TYPE và không có gì khác. Đó là một int và thế là xong. Nhưng nếu bây giờ tôi viết x + 2 hoặc x = 3 , thì trong các biểu thức này, biểu thức con x thuộc loại int &TYPE . Bởi vì nếu không thì x này sẽ không khác gì, chẳng hạn như 10, và không thể gán gì cho nó (chẳng hạn như mười).

Nguyên tắc này (“biểu thức là biến là tham chiếu”) là phát minh của tôi. Tức là tôi chưa thấy nguyên tắc này trong bất kỳ sách giáo khoa, tiêu chuẩn nào, v.v. Tuy nhiên, nó đơn giản hóa rất nhiều và thuận tiện để coi là chính xác. Nếu tôi đang triển khai một trình biên dịch, tôi sẽ chỉ coi các biến trong biểu thức là tham chiếu và đây hoàn toàn có thể là điều mà các trình biên dịch thực sự phải làm.

Nguyên tắc “bất kỳ giá trị nào cũng là một tham chiếu” cũng là phát minh của tôi. Nhưng nguyên tắc “bất kỳ tham chiếu nào cũng là giá trị” là một nguyên tắc hoàn toàn hợp pháp, được chấp nhận rộng rãi (tất nhiên, liên kết phải là tham chiếu đến một đối tượng có thể thay đổi và đối tượng này phải cho phép gán).

Bây giờ, có tính đến các quy ước của chúng ta, chúng ta sẽ xây dựng các quy tắc nghiêm ngặt để làm việc với các tham chiếu: nếu, giả sử, int x được khai báo, thì bây giờ biểu thức x có loại int &TYPE . Nếu bây giờ biểu thức này (hoặc bất kỳ biểu thức nào khác của loại liên kết) nằm ở bên trái của dấu bằng thì nó được sử dụng chính xác như một liên kết trong hầu hết các trường hợp khác (ví dụ: trong tình huống x + 2) x là tự động được chuyển đổi sang kiểu int TYPE (bằng một thao tác khác, bên cạnh tham chiếu không được chuyển đổi thành đối tượng của nó là &, như chúng ta sẽ thấy sau). Ở bên trái dấu bằng chỉ có thể là một liên kết. Chỉ một tham chiếu mới có thể khởi tạo một tham chiếu (không phải hằng).

Hoạt động * và &. Các quy ước của chúng ta cung cấp cho chúng ta một cách mới để xem xét các phép toán * và &. Bây giờ điều sau đây đã rõ ràng: thao tác * chỉ có thể được áp dụng cho một con trỏ (điều này luôn được biết cụ thể) và nó trả về một tham chiếu đến cùng loại. & luôn được áp dụng cho một tham chiếu và trả về một con trỏ cùng loại. Vì vậy, * và & biến các con trỏ và tham chiếu lẫn nhau. Trên thực tế, nghĩa là họ không làm gì cả và chỉ thay thế bản chất của cú pháp này bằng bản chất của cú pháp khác! Do đó, & nói chung, việc gọi thao tác lấy địa chỉ là không hoàn toàn chính xác: nó chỉ có thể được áp dụng cho một địa chỉ đã tồn tại, nó chỉ đơn giản là thay đổi cách thể hiện cú pháp của địa chỉ này.

Lưu ý rằng con trỏ và tham chiếu được khai báo là int *x và int &x . Như vậy, nguyên tắc “khai báo nhắc sử dụng” một lần nữa được khẳng định: khai báo con trỏ nhắc cách biến nó thành một liên kết, còn khai báo liên kết thì làm ngược lại.

Cũng lưu ý rằng &*EXPR (ở đây EXPR là một biểu thức tùy ý, không nhất thiết phải là một mã định danh duy nhất) ​​tương đương với EXPR bất cứ khi nào nó có ý nghĩa (tức là bất cứ khi nào EXPR là một con trỏ) và *&EXPR cũng tương đương với EXPR bất cứ khi nào nó có nghĩa (tức là khi EXPR là một liên kết).

Mảng

Vì vậy, có một kiểu dữ liệu như vậy - một mảng. Mảng được định nghĩa, ví dụ như thế này:
int x;
Biểu thức trong ngoặc vuông phải là hằng số thời gian biên dịch trong C89 và C++98. Trong trường hợp này, phải có số trong ngoặc vuông; không được phép có dấu ngoặc vuông trống.

Giống như tất cả các biến cục bộ (hãy nhớ rằng, chúng tôi giả sử tất cả các ví dụ mã đều nằm bên trong các hàm) đều nằm trên ngăn xếp, các mảng cũng nằm trên ngăn xếp. Nghĩa là, đoạn mã trên đã dẫn đến việc phân bổ một khối bộ nhớ khổng lồ có kích thước 5 * sizeof (int) trực tiếp trên ngăn xếp, nơi chứa toàn bộ mảng của chúng ta. Bạn không cần phải nghĩ rằng đoạn mã này đã khai báo một loại con trỏ nào đó trỏ tới bộ nhớ nằm ở đâu đó xa trên heap. Không, chúng tôi đã khai báo một mảng, một mảng thực sự. Ở đây trên ngăn xếp.

sizeof (x) sẽ bằng bao nhiêu? Tất nhiên, nó sẽ bằng kích thước của mảng của chúng tôi, tức là 5 * sizeof (int) . Nếu chúng ta viết
struct foo(int a; int b; );
sau đó, một lần nữa, không gian cho mảng sẽ được phân bổ hoàn toàn ngay bên trong cấu trúc và sizeof từ cấu trúc này sẽ xác nhận điều này.

Bạn có thể lấy địa chỉ (&x) từ mảng và đây sẽ là một con trỏ thực sự tới vị trí của mảng này. Loại biểu thức &x , để dễ hiểu, sẽ là int (*TYPE) . Ở đầu mảng, phần tử 0 của nó được đặt, do đó địa chỉ của chính mảng và địa chỉ của phần tử 0 của nó giống nhau về mặt số. Nghĩa là, &x và &(x) bằng nhau về mặt số lượng (ở đây tôi đã viết biểu thức &(x một cách nổi tiếng), trên thực tế, không phải mọi thứ trong đó đều đơn giản như vậy, chúng ta sẽ quay lại vấn đề này sau). Nhưng những biểu thức này có các loại khác nhau - int (*TYPE) và int *TYPE , vì vậy việc so sánh chúng bằng == sẽ không hiệu quả. Nhưng bạn có thể sử dụng thủ thuật void *: biểu thức sau sẽ đúng: (void *)&x == (void *)&(x) .

Được rồi, giả sử rằng tôi đã thuyết phục bạn rằng mảng chỉ là một mảng chứ không phải cái gì khác. Vậy tất cả sự nhầm lẫn giữa con trỏ và mảng này đến từ đâu? Thực tế là tên của mảng được chuyển đổi thành một con trỏ tới phần tử 0 của nó trong hầu hết mọi thao tác.

Vì vậy, chúng tôi đã khai báo int x . Nếu bây giờ chúng ta viết x + 0, thì điều này sẽ chuyển đổi x của chúng ta (thuộc loại int TYPE, hay chính xác hơn là int (&TYPE)) thành &(x), tức là một con trỏ tới phần tử 0 của mảng x. Bây giờ x của chúng tôi thuộc loại int *TYPE .

Việc chuyển đổi tên mảng thành void * hoặc áp dụng == cho nó cũng chuyển đổi trước tên đó thành con trỏ thành phần tử đầu tiên, vì vậy:
&x == x // lỗi biên dịch, các loại khác nhau: int (*TYPE) và int *TYPE (void *)&x == (void *)x // true x == x + 0 // true x == &( x) // đúng

Hoạt động. Ký hiệu a[b] luôn tương đương với *(a + b) (để tôi nhắc bạn rằng chúng tôi không xem xét việc định nghĩa lại toán tử và các phép toán khác). Vậy ký hiệu x có nghĩa như sau:

  • x tương đương với *(x + 2)
  • x + 2 đề cập đến các thao tác chuyển đổi tên của một mảng thành một con trỏ thành phần tử đầu tiên của nó, vì vậy điều này xảy ra
  • Tiếp theo, theo giải thích của tôi ở trên, x + 2 tương đương với (int *)((char *)x + 2 * sizeof (int)) , tức là x + 2 có nghĩa là “dịch chuyển con trỏ x đi hai int”
  • Cuối cùng, một thao tác dereference được lấy từ kết quả và chúng ta truy xuất đối tượng nằm ở con trỏ đã dịch chuyển này

Các loại biểu thức tham gia như sau:
x // int (&TYPE), sau khi chuyển đổi kiểu: int *TYPE x + 2 // int *TYPE *(x + 2) // int &TYPE x // int &TYPE

Tôi cũng lưu ý rằng không nhất thiết phải có một mảng ở bên trái dấu ngoặc vuông; có thể có bất kỳ con trỏ nào ở đó. Ví dụ: bạn có thể viết (x + 2) và nó sẽ tương đương với x. Tôi cũng sẽ lưu ý rằng *a và a luôn tương đương nhau, cả trong trường hợp a là một mảng và khi a là một con trỏ.

Bây giờ, như tôi đã hứa, tôi sẽ quay lại &(x) . Bây giờ rõ ràng là trong biểu thức này, đầu tiên x được chuyển đổi thành một con trỏ, sau đó thuật toán trên được áp dụng cho con trỏ này để tạo ra một giá trị kiểu int &TYPE và cuối cùng nó được chuyển đổi thành kiểu int *TYPE bằng cách sử dụng & . Do đó, việc sử dụng biểu thức phức tạp này (trong đó đã thực hiện chuyển đổi mảng sang con trỏ) để giải thích khái niệm đơn giản hơn một chút về chuyển đổi mảng sang con trỏ là một điều hơi gian lận.

Và bây giờ là câu hỏi cuối cùng: &x + 1 là gì? Chà, &x là một con trỏ tới toàn bộ mảng, + 1 dẫn đến một bước tới toàn bộ mảng. Nghĩa là, &x + 1 là (int (*))((char *)&x + sizeof (int )), nghĩa là, (int (*))((char *)&x + 5 * sizeof (int )) ( ở đây int (*) là int (*TYPE)). Vậy &x + 1 về mặt số bằng x + 5, không phải x + 1 như bạn nghĩ. Có, cuối cùng chúng ta trỏ đến bộ nhớ nằm ngoài mảng (ngay sau phần tử cuối cùng), nhưng ai quan tâm chứ? Rốt cuộc, trong C vẫn chưa có sự kiểm tra nào về việc vượt quá giới hạn của một mảng. Ngoài ra, hãy lưu ý rằng biểu thức *(&x + 1) == x + 5 là đúng. Bạn cũng có thể viết nó như thế này: (&x) == x + 5 . Nó cũng sẽ đúng *((&x)) == x , hoặc, điều tương tự, (&x) == x (tất nhiên trừ khi chúng ta gặp lỗi phân đoạn khi cố gắng truy cập các giới hạn của bộ nhớ: )).

Không thể truyền mảng làm đối số cho hàm. Nếu bạn viết int x hoặc int x trong tiêu đề hàm thì nó sẽ tương đương với int *x và một con trỏ sẽ luôn được truyền tới hàm (kích thước của biến được truyền sẽ giống với kích thước của con trỏ). Trong trường hợp này, kích thước mảng được chỉ định trong tiêu đề sẽ bị bỏ qua. Bạn có thể dễ dàng chỉ định int x trong tiêu đề và chuyển một mảng có độ dài 3 vào đó.

Tuy nhiên, trong C++ có một cách để truyền tham chiếu mảng tới hàm:
void f (int (&x)) ( // sizeof (x) ở đây là 5 * sizeof (int) ) int main (void) ( int x; f (x); // OK f (x + 0); // Không thể int y; f(y); // Không thể, sai kích thước)
Với việc chuyển như vậy, bạn vẫn chỉ truyền một liên kết chứ không phải một mảng, tức là mảng đó không được sao chép. Nhưng bạn vẫn nhận được một số khác biệt so với việc truyền con trỏ thông thường. Một tham chiếu đến mảng được truyền. Thay vào đó, bạn không thể chuyển con trỏ. Bạn cần truyền chính xác mảng có kích thước đã chỉ định. Bên trong hàm, một tham chiếu đến một mảng sẽ hoạt động giống hệt như một tham chiếu đến một mảng, ví dụ, nó sẽ có sizeof giống như một mảng.

Và điều thú vị nhất là việc chuyển tiền này có thể được sử dụng như thế này:
// Tính độ dài của mảng mẫu size_t len ​​​​(t (&a)[n]) ( return n; )
Hàm std::end trong C++ 11 cho mảng được triển khai theo cách tương tự.

"Con trỏ tới mảng". Nói đúng ra, “con trỏ tới một mảng” chỉ là một con trỏ tới một mảng và không có gì khác. Nói cách khác:
int(*a); // Đây là một con trỏ tới một mảng. Thực tế nhất. Nó có kiểu int (*TYPE) int b; int *c = b; // Đây không phải là con trỏ tới một mảng. Nó chỉ là một con trỏ. Con trỏ tới phần tử đầu tiên của mảng int *d = new int; // Và đây không phải là con trỏ tới một mảng. Đây là một con trỏ
Tuy nhiên, đôi khi cụm từ “con trỏ tới một mảng” có nghĩa một cách không chính thức là một con trỏ tới vùng bộ nhớ chứa mảng đó, ngay cả khi kiểu của con trỏ này không phù hợp. Theo cách hiểu không chính thức này, c và d (và b + 0) là các con trỏ tới mảng.

Mảng đa chiều. Nếu int x được khai báo thì x không phải là mảng có độ dài 5 của một số con trỏ trỏ đến đâu đó ở xa. Không, x bây giờ là một khối nguyên khối 5 x 7 được đặt trên một ngăn xếp. sizeof(x) bằng 5 * 7 * sizeof(int) . Các phần tử được sắp xếp trong bộ nhớ như sau: x, x, x, x, x, x, x, x, v.v. Khi chúng ta viết x, các sự kiện sẽ diễn ra như thế này:
x // int (&TYPE), sau khi chuyển đổi: int (*TYPE) x // int (&TYPE), sau khi chuyển đổi: int *TYPE x // int &TYPE
Điều tương tự cũng áp dụng cho **x . Lưu ý rằng trong các biểu thức, chẳng hạn như x + 3 và **x + 3, trên thực tế, việc truy xuất bộ nhớ chỉ xảy ra một lần (mặc dù có hai dấu hoa thị), tại thời điểm chuyển đổi tham chiếu cuối cùng của loại int &TYPE đơn giản thành int KIỂU . Nghĩa là, nếu chúng ta xem mã hợp ngữ được tạo từ biểu thức **x + 3, chúng ta sẽ thấy trong đó hoạt động truy xuất dữ liệu từ bộ nhớ chỉ được thực hiện ở đó một lần. **x + 3 cũng có thể được viết khác thành *(int *)x + 3 .

Bây giờ chúng ta hãy xem tình huống này:
int **y = int mới *; for (int i = 0; i != 5; ++i) ( y[i] = new int; )

Bây giờ y là gì? y là một con trỏ tới một mảng (theo nghĩa không chính thức!) của các con trỏ tới mảng (một lần nữa, theo nghĩa không chính thức). Không nơi nào xuất hiện một khối kích thước 5 x 7 ở đây cả, có 5 khối kích thước 7 * sizeof (int) có thể cách xa nhau. y là gì?
y // int **&TYPE y // int *&TYPE y // int &TYPE
Bây giờ, khi chúng ta viết y + 3 , quá trình tìm nạp bộ nhớ xảy ra hai lần: một lần tìm nạp từ mảng y và một lần tìm nạp tiếp theo từ mảng y, có thể ở xa mảng y. Lý do cho điều này là không có sự chuyển đổi tên mảng thành con trỏ tới phần tử đầu tiên của nó, không giống như ví dụ về mảng x nhiều chiều. Vì vậy **y + 3 ở đây không tương đương với *(int *)y + 3 .

Tôi sẽ giải thích nó một lần nữa. x tương đương với *(*(x + 2) + 3) . Và y tương đương với *(*(y + 2) + 3) . Nhưng trong trường hợp đầu tiên, nhiệm vụ của chúng ta là tìm “phần tử thứ ba ở hàng thứ hai” trong một khối có kích thước 5 x 7 (tất nhiên, các phần tử được đánh số từ 0, vì vậy phần tử thứ ba này sẽ có nghĩa là thứ tư :)). Trình biên dịch tính toán rằng phần tử mong muốn thực sự nằm ở vị trí thứ 2 * 7 + 3 trong khối này và trích xuất nó. Nghĩa là, x ở đây tương đương với ((int *)x) hoặc tương đương *((int *)x + 2 * 7 + 3) . Trong trường hợp thứ hai, trước tiên nó lấy phần tử thứ 2 trong mảng y và sau đó là phần tử thứ 3 trong mảng kết quả.

Trong trường hợp đầu tiên, khi chúng ta thực hiện x + 2 , chúng ta sẽ dịch chuyển ngay lập tức 2 * sizeof (int ) , tức là bằng 2 * 7 * sizeof (int) . Trong trường hợp thứ hai, y + 2 là sự dịch chuyển 2 * sizeof (int *) .

Trong trường hợp đầu tiên (void *)x và (void *)*x (và (void *)&x !) là cùng một con trỏ, trong trường hợp thứ hai thì không.

Một loại mảng C# khác là mảng mảng, còn được gọi là mảng lởm chởm. Một mảng các mảng như vậy có thể được coi là một mảng một chiều có các phần tử là mảng, các phần tử của nó có thể lại là mảng, v.v. đến một mức độ lồng nhau nào đó.

Trong những tình huống nào thì nhu cầu về cấu trúc dữ liệu như vậy có thể phát sinh? Các mảng này có thể được sử dụng để biểu diễn các cây trong đó các nút có thể có số lượng nút con tùy ý. Ví dụ, đây có thể là một cây gia phả. Đỉnh của cấp độ đầu tiên - Người cha, đại diện cho người cha có thể được cho bởi mảng một chiều, do đó Người cha[ Tôi] - Cái này Tôi bố. Các đỉnh của cấp độ thứ hai được biểu diễn bằng một mảng các mảng - Những đứa trẻ, Vì thế Những đứa trẻ[ Tôi] - đây là một mảng trẻ em Tôi-người cha, và Những đứa trẻ[ Tôi][ j] - đây là đứa con thứ j Tôi bố. Để đại diện cho cháu bạn sẽ cần có cấp độ thứ ba, vì vậy Cháu[ Tôi][ j][ k] sẽ đại diện ĐẾN cháu trai jđứa trẻ Tôi bố.

Có một số đặc thù trong việc khai báo và khởi tạo các mảng như vậy. Nếu khi khai báo loại mảng nhiều chiều, dấu phẩy được sử dụng để biểu thị thứ nguyên, thì đối với mảng lởm chởm, một ký hiệu rõ ràng hơn được sử dụng - một tập hợp các cặp dấu ngoặc vuông; Ví dụ, int[ ] chỉ định một mảng có các phần tử là mảng một chiều gồm các phần tử kiểu int.

Việc tự tạo các mảng và khởi tạo chúng sẽ khó khăn hơn. Bạn không thể gọi hàm tạo ở đây mớiint, vì nó không chỉ định một mảng lởm chởm. Trên thực tế, bạn cần gọi hàm tạo cho từng mảng ở mức thấp nhất. Đây chính là khó khăn khi khai báo các mảng như vậy. Hãy bắt đầu với một ví dụ chính thức:

// mảng mảng - ví dụ hình thức
// khai báo và khởi tạo
int[ ] lời nói tục tĩu= mớiint[ ] {
int mới [] {5, 7, 9, 11},
int mới [] {2, 8},
int mới [] {6, 12, 4}
};

Mảng lời nói tục tĩu chỉ có hai cấp độ. Chúng ta có thể coi nó có ba phần tử, mỗi phần tử là một mảng. Với mỗi mảng như vậy, bạn cần gọi hàm tạo mới, để tạo một mảng nội bộ. Trong ví dụ này, các phần tử của mảng bên trong nhận giá trị của chúng bằng cách được khởi tạo rõ ràng dưới dạng mảng không đổi. Tất nhiên, tuyên bố sau đây cũng được chấp nhận:

int[ ] lời nói tục tĩu1 = mớiint[ ] {
mớiint,
mớiint,
mớiint
};

Trong trường hợp này, các phần tử mảng sẽ nhận giá trị 0 trong quá trình khởi tạo. Việc khởi tạo thực tế sẽ cần phải được thực hiện theo chương trình. Điều đáng chú ý là trong hàm tạo cấp cao nhất, hằng số 3 bạn có thể bỏ qua nó và chỉ cần viết mớiint[ ] . Lệnh gọi tới hàm tạo này có thể được bỏ qua hoàn toàn - nó sẽ được ngụ ý:

int[ ] lời nói tục tĩu2 = {
int mới,
int mới,
int mới
};

Nhưng các nhà xây dựng cấp thấp hơn là cần thiết. Một lưu ý quan trọng khác - mảng động cũng có thể được sử dụng ở đây. Nói chung, ranh giới ở bất kỳ cấp độ nào cũng có thể là những biểu thức phụ thuộc vào các biến. Hơn nữa, các mảng ở cấp độ thấp hơn được phép có nhiều chiều.

Hãy tiếp tục tìm hiểu những điều cơ bản về C++. Trong bài viết này chúng ta sẽ xem xét mảng.

Mảng cho phép bạn lưu trữ lượng lớn dữ liệu ở định dạng thuận tiện. Trong thực tế, mảng là biến lưu trữ nhiều giá trị dưới một tên nhưng mỗi giá trị được gán chỉ mục riêng. là danh sách các giá trị mà chỉ mục dùng để truy cập.

Giới thiệu về mảng

Bạn có thể hình dung một mảng như thế này:

Đây là tập hợp một số giá trị được lưu trữ lần lượt dưới một tên. Để có được những giá trị này, bạn không phải tạo biến mới, bạn chỉ cần chỉ ra chỉ mục mà giá trị được lưu trữ trong mảng. Ví dụ: bạn cần chia một bộ năm lá bài để chơi poker, bạn có thể lưu trữ các lá bài này trong một mảng và chỉ thay đổi số chỉ mục để chọn một lá bài mới, thay vì sử dụng một biến mới. Điều này sẽ cho phép bạn sử dụng cùng một mã để khởi tạo tất cả các thẻ và chuyển từ cách viết như sau:

Thẻ1 = getRandomCard(); Card2 = getRandomCard(); Card3 = getRandomCard(); Card4 = getRandomCard(); Thẻ5 = getRandomCard();

Với (int i = 0; tôi< 5; i++) { card[i] = getRandomCard(); }

Bây giờ hãy tưởng tượng sự khác biệt nếu có 100 biến!

Cú pháp

Để khai báo một mảng, bạn cần chỉ định hai thứ (ngoài tên): loại và kích thước của mảng:

Int my_array[ 6 ];

Dòng này khai báo một mảng gồm sáu giá trị nguyên. Lưu ý rằng kích thước mảng được đặt trong dấu ngoặc vuông sau tên mảng.

Bạn sử dụng dấu ngoặc vuông để truy cập các phần tử mảng, nhưng lần này bạn chỉ định chỉ mục của phần tử bạn muốn truy cập:

My_array[ 3 ];

Bạn có thể hình dung quá trình này như sau:


my_array đề cập đến toàn bộ mảng, trong khi my_array chỉ đề cập đến phần tử đầu tiên, my_array đề cập đến phần tử thứ tư. lưu ý rằng lập chỉ mục các phần tử trong mảng bắt đầu từ 0. Do đó, việc truy cập các phần tử mảng sẽ luôn xảy ra với một offset, ví dụ:

Int my_array[ 4 ]; // khai báo mảng my_array[ 2 ] = 2; // đặt giá trị của thứ ba (cụ thể là thứ ba!) thành 2

Khai báo mảng nhiều chiều trong C++

Mảng cũng có thể được sử dụng để biểu diễn dữ liệu đa chiều, chẳng hạn như bàn cờ hoặc bàn cờ tic-tac-toe. Khi sử dụng dữ liệu đa chiều, nhiều chỉ mục sẽ được sử dụng để truy cập các phần tử mảng.

Để khai báo mảng hai chiều, bạn phải xác định chiều của hai chiều:

Int tic_tac_toe_board;

Trực quan hóa một mảng với các chỉ số của các phần tử của nó:

Để truy cập các phần tử của một mảng như vậy, bạn sẽ cần hai chỉ mục - một cho hàng và một cho cột. Hình ảnh hiển thị các chỉ mục cần thiết để truy cập từng phần tử.

Sử dụng mảng

Khi sử dụng mảng, bạn không thể thiếu . Để chạy qua một vòng lặp, bạn chỉ cần khởi tạo một biến về 0 và tăng biến đó cho đến khi nó vượt quá kích thước của mảng—một mẫu vừa phải cho một vòng lặp.

Chương trình sau đây minh họa cách sử dụng vòng lặp để tạo bảng nhân và lưu kết quả vào mảng hai chiều:

#bao gồm sử dụng không gian tên std; int main() ( int array; // Khai báo một mảng giống bàn cờ cho (int i = 0; i< 8; i++) { for (int j = 0; j < 8; j++) { array[i][j] = i * j; // Задаем значения каждого элемента } } cout << "Multiplication table:\n"; for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { cout << "[ " << i << " ][ " << j << "] = "; cout << array[i][j] << " "; cout << "\n"; } } }

Truyền mảng cho hàm

Như bạn có thể thấy, các phần tử khác nhau của ngôn ngữ C++ tương tác với nhau. Giống như vòng lặp, mảng có thể được sử dụng kết hợp với .

Để truyền một mảng cho một hàm, chỉ cần chỉ định tên của nó:

Giá trị int[10]; sum_array(giá trị);

Và khi khai báo một hàm, hãy chỉ định một mảng làm đối số:

Int sum_array(giá trị int);

Xin lưu ý rằng chúng tôi không chỉ định thứ nguyên của mảng trong các đối số của hàm, điều này là bình thường; đối với mảng một chiều thì không cần chỉ định thứ nguyên. Kích thước phải được chỉ định khi khai báo mảng , bởi vì Trình biên dịch cần biết cần phân bổ bao nhiêu bộ nhớ. Khi truyền vào một hàm, chúng ta chỉ cần truyền một mảng hiện có; không cần chỉ định kích thước, bởi vì Chúng tôi không tạo ra một cái mới. Bởi vì chúng ta truyền một mảng cho hàm, bên trong hàm chúng ta có thể thay đổi, không giống như các biến đơn giản được truyền theo giá trị và việc thay đổi giá trị này bên trong hàm sẽ không ảnh hưởng đến biến ban đầu dưới bất kỳ hình thức nào.

Vì chúng ta không biết kích thước của mảng bên trong hàm nên chúng ta cần truyền thứ nguyên làm đối số thứ hai:

Int sumArray(int value, int size) ( int sum = 0; for (int i = 0; i< size; i++) { sum += values[ i ]; } return sum; }

Khi truyền mảng nhiều chiều, chúng ta phải chỉ định tất cả các chiều ngoại trừ chiều đầu tiên:

Int check_tic_tac_toe(int board);

Tất nhiên, bạn có thể chỉ định thứ nguyên đầu tiên nhưng nó sẽ bị bỏ qua.

Chủ đề này sẽ được thảo luận chi tiết hơn trong bài viết về con trỏ.

Bây giờ, hãy viết một hàm tính tổng các phần tử của mảng:

#bao gồm sử dụng không gian tên std; int sumArray(int value, int size) ( int sum = 0; // vòng lặp sẽ dừng khi i == size, vì chỉ số của phần tử cuối cùng = size - 1 for (int i = 0; i< size; i++) { sum += values[i]; } return sum; } int main() { int values; for (int i = 0; i < 10; i++) { cout << "Enter value "<< tôi <<": "; cin >> giá trị[i]; ) cout<< sumArray(values, 10) << endl; }

Sắp xếp một mảng

Hãy giải bài toán sắp xếp mảng 100 số do người dùng nhập vào:

#bao gồm sử dụng không gian tên std; int main() ( int value[ 100 ]; for (int i = 0; i< 100; i++) { cout << "Enter value "<< tôi <<": "; cin >> giá trị[ i ]; ) )

Xong, tất cả những gì còn lại là sắp xếp mảng này :) Mọi người thường sắp xếp mảng như thế nào? Họ tìm kiếm phần tử nhỏ nhất trong đó và đặt nó lên đầu danh sách. Sau đó, họ tìm giá trị tối thiểu tiếp theo và đặt nó ngay sau giá trị đầu tiên, v.v.

Toàn bộ điều này trông giống như một vòng lặp: chúng ta chạy qua mảng, bắt đầu từ phần tử đầu tiên và tìm giá trị nhỏ nhất trong phần còn lại, rồi hoán đổi các phần tử này. Hãy bắt đầu bằng cách viết mã để thực hiện các thao tác sau:

Sắp xếp trống (mảng int, kích thước int) ( for (int i = 0; i< size; i++) { int index = findSmallestRemainingElement(array, size, i); swap(array, i, index); } }

Bây giờ bạn có thể nghĩ đến việc triển khai hai phương thức trợ giúp, findSmallestRemainingElement và swap. Phương thức findSmallestRemainingElement phải lặp qua mảng và tìm phần tử nhỏ nhất, bắt đầu từ chỉ mục i:

Int findSmallestRemainingElement(int array, int size, int index) ( int index_of_smallest_value = index; for (int i = index + 1; i< size; i++) { if (array[ i ] < array[ index_of_smallest_value ]) { index_of_smallest_value = I; } } return index_of_smallest_value; }

Cuối cùng, chúng ta cần thực hiện chức năng trao đổi. Vì hàm sẽ sửa đổi mảng ban đầu nên chúng ta chỉ cần hoán đổi các giá trị bằng biến tạm thời:

Trao đổi trống(int mảng, int first_index, int thứ hai_index) ( int temp = mảng[ first_index ]; mảng [ first_index ] = mảng [ giây_index ]; mảng [ giây_index ] = temp; )

Để kiểm tra thuật toán, hãy điền vào mảng các số ngẫu nhiên và sắp xếp nó. Tất cả mã chương trình:

#bao gồm #bao gồm #bao gồm sử dụng không gian tên std; int findSmallestRemainingElement(int array, int size, int index); void swap(int array, int first_index, int two_index); void sắp xếp(int mảng, kích thước int) ( for (int i = 0; i< size; i++) { int index = findSmallestRemainingElement(array, size, i); swap(array, i, index); } } int findSmallestRemainingElement(int array, int size, int index) { int index_of_smallest_value = index; for (int i = index + 1; i < size; i++) { if (array[ i ] < array[ index_of_smallest_value ]) { index_of_smallest_value = i; } } return index_of_smallest_value; } void swap(int array, int first_index, int second_index) { int temp = array[ first_index ]; array[ first_index ] = array[ second_index ]; array[ second_index ] = temp; } // вспомогательная функция для вывода массива void displayArray(int array, int size) { cout << "{"; for (int i = 0; i < size; i++) { // если элемент не первый выведем запятую if (i != 0) { cout << ", "; } cout << array[ i ]; } cout << "}"; } int main() { int array[ 10 ]; srand(time(NULL)); for (int i = 0; i < 10; i++) { array[ i ] = rand() % 100; } cout << "Original array: "; displayArray(array, 10); cout << "\n"; sort(array, 10); cout << "Sorted array: "; displayArray(array, 10); cout << "\n"; }

Thuật toán sắp xếp mà chúng ta vừa xem xét được gọi là sắp xếp chèn , đây không phải là thuật toán nhanh nhất nhưng rất dễ hiểu và dễ thực hiện. Nếu bạn phải sắp xếp lượng lớn dữ liệu, tốt hơn nên sử dụng các thuật toán phức tạp hơn và nhanh hơn.

Khai báo một mảng trong C
Mảng là kiểu dữ liệu thứ cấp. Mảng trong C là tập hợp các phần tử có kích thước rõ ràng thuộc một loại cụ thể. nghĩa là, không giống như mảng trong Ruby, mảng trong C có cùng loại (chúng lưu trữ dữ liệu chỉ có một loại) và có độ dài (kích thước) được xác định trước.

Trong C, mảng có thể được chia đại khái thành 2 loại: mảng số và mảng ký tự. Tất nhiên, việc phân chia như vậy là hoàn toàn tùy ý vì các ký hiệu cũng là số nguyên. Mảng ký tự cũng có cú pháp hơi khác một chút. Dưới đây là ví dụ về khai báo mảng:

Int mảng; int a = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); char ch = ("R","u","b","y","D","e","v",".","r","u"); char ch2 = "trang web";

Trong trường hợp đầu tiên, chúng ta khai báo một mảng số nguyên (4 byte cho mỗi số) có kích thước 100 phần tử. Chính xác hơn, chúng ta dành bộ nhớ để lưu trữ 100 phần tử kiểu int.

Trong trường hợp thứ hai, chúng ta xác định một mảng gồm 10 phần tử số nguyên và gán ngay giá trị cho các phần tử mảng.

Trong trường hợp thứ ba, chúng ta xác định một mảng ký tự. Không có chuỗi trong C nhưng có các mảng ký tự thay thế chuỗi.

Trong trường hợp sau, chúng tôi cũng khai báo một mảng ký tự bằng cú pháp đặc biệt, ngắn gọn hơn. Mảng ch và ch2 gần như giống nhau nhưng có một điểm khác biệt. Khi chúng ta sử dụng cú pháp hằng chuỗi để tạo một mảng, ký tự \0 sẽ tự động được thêm vào cuối mảng ký tự. Khi sử dụng cú pháp khai báo mảng tiêu chuẩn, chúng ta phải tự thêm \0 làm phần tử cuối cùng của mảng ký tự. Ký tự \0 (null) được sử dụng để xác định phần cuối của dòng. Chúng ta sẽ nói về strakes chi tiết hơn trong một bài viết riêng.

Truy cập các phần tử mảng trong C

Trong C, việc truy cập các phần tử mảng khá đơn giản và tương tự như cách thực hiện trong hầu hết các ngôn ngữ lập trình khác. Sau tên biến tham chiếu mảng, ta chỉ ra chỉ số (còn gọi là khóa) của phần tử trong ngoặc vuông. Ví dụ dưới đây cho thấy cách chúng ta truy cập phần tử đầu tiên của mảng:

#bao gồm int main() ( int arr; int a = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); char ch = ("R","u","b"," y","D","e","v",".","r","u") ; char ch2 = "site"; printf("%d\n", arr); printf(" %c\n", ch); )

Mã printf("%d\n", a); sẽ in 2, không phải 1 vì việc lập chỉ mục các mảng bắt đầu từ 0 và một xác nhận khác về điều này là dòng printf("%c\n", ch); , nó sẽ in ký tự "R" - phần tử thứ 0 của mảng ch.

Nói chung, một khai báo mảng có cú pháp như sau:

kiểu dữ liệu tên biến [<количество_элементов>] = <список, элементов, массива>

Số phần tử mảng và danh sách phần tử là các thuộc tính bắt buộc của một khai báo mảng; chính xác hơn là bắt buộc phải có một trong số chúng, nhưng không phải cả hai cùng một lúc.

Để hiểu được cấu trúc của mảng, bạn cần làm quen với khái niệm con trỏ trong C.

Con trỏ trong C
Các kiểu dữ liệu là cần thiết để có thể phân bổ một phần bộ nhớ có kích thước nhất định để lưu trữ dữ liệu và xác định đó là loại dữ liệu nào vì nếu không có định nghĩa rõ ràng thì không rõ liệu tập hợp số 0 và số 1 có phải là số hay không, một biểu tượng, hoặc một cái gì đó khác. Trong trường hợp này, biến là tham chiếu đến một phần bộ nhớ có kích thước và loại nhất định, ví dụ: biến int đề cập đến vùng bộ nhớ 4 byte cụ thể trong đó một số nguyên được lưu trữ và biến char đề cập đến một Vùng bộ nhớ 1 byte trong đó một ký tự được lưu trữ (mã ký tự).

Để lấy địa chỉ mà một biến tham chiếu tới, chúng ta sử dụng toán tử đặc biệt & - toán tử địa chỉ, ví dụ:

#bao gồm int main() ( int arr; int a = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); char ch = ("R","u","b"," y","D","e","v",".","r","u") ; char ch2 = "site"; int num = 100500; printf("%p\n", &arr ; )

Dòng printf("%p\n", &arr); sẽ in 0xbfbbe068. 0xbfbbe068 là biểu diễn thập lục phân của địa chỉ bộ nhớ nơi lưu trữ số 100500.

Con trỏ là loại biến đặc biệt không lưu trữ các giá trị thông thường mà lưu trữ địa chỉ của chúng trong bộ nhớ.

#bao gồm int main() ( int a, b; b = a = 10; printf("A: %p\n", &a); printf("B: %p\n", &b); )

$./chương trình
Đáp: 0xbfe32008
B: 0xbfe3200c

Trong ví dụ trên, chúng ta gán cho biến a và b cùng một giá trị - số 10, nhưng biến a và b đề cập đến hai vùng bộ nhớ khác nhau, tức là chúng ta lưu số 10 vào bộ nhớ hai lần. Nếu ta thay đổi giá trị của biến b thì không ảnh hưởng đến biến a và ngược lại. Điều này khác với cách chúng ta làm việc với các biến trong Ruby, trong đó các biến là tham chiếu đến các đối tượng được lưu trong bộ nhớ và khi gán theo kiểu a = b = 10, chúng ta nhận được một đối tượng - số 10 và hai tham chiếu đến nó.

#bao gồm int main() ( int a = 10; int * b = printf("A:\n\taddress: %p\n\tvalue: %d\n",&a, a); printf("B:\n\ địa chỉ: %p\n\tvalue: %d\n",b, *b); )

Kết quả thực hiện:

$./chương trình
MỘT:
địa chỉ: 0xbfed0fa8
giá trị: 10
B:
địa chỉ: 0xbfed0fa8
giá trị: 10

Con trỏ và mảng

Trên thực tế, C không có mảng theo nghĩa mà nhiều người quen thuộc. Bất kỳ mảng nào trong C chỉ đơn giản là một tham chiếu đến phần tử 0 của mảng. Ví dụ:

#bao gồm int main() ( int a = (10,20,30); printf("a-Địa chỉ:%p\n", &a); printf("a-Địa chỉ:%p\n", &a); printf( "a-Value:%d\n", a); printf("a-Size:%d\n", sizeof(a));

Kết quả:

$./chương trình
a-Địa chỉ: 0xbfc029b4
a-Địa chỉ: 0xbfc029b4
a-Giá trị: 10
a-Kích thước: 12

Như bạn có thể thấy, tôi không lừa bạn, một biến tham chiếu một mảng thực sự chỉ đề cập đến phần tử 0 của nó, nghĩa là nó là một con trỏ tới địa chỉ lưu trữ của phần tử đầu tiên.

Khi chúng ta chạy một chương trình, hệ điều hành sẽ cung cấp cho chương trình hai lượng bộ nhớ đặc biệt - ngăn xếp và vùng heap. Chương trình của chúng tôi chỉ sử dụng một ngăn xếp. Một ngăn xếp lưu trữ các giá trị một cách có thứ tự. Khi tạo một mảng, thực tế là chúng ta đang tạo một con trỏ tới phần tử 0 của một tập hợp các phần tử và dành bộ nhớ cho N số phần tử. Trong ví dụ trên, chúng tôi đã tạo một tập hợp gồm 3 phần tử kiểu int, tức là Mỗi phần tử chiếm 4 byte bộ nhớ. Khi chúng ta sử dụng hàm sizeof(), hàm này trả về kích thước tính bằng byte của đối số được truyền cho nó, chúng ta nhận được giá trị 12, tức là. mảng chiếm 12 byte bộ nhớ: 3 phần tử * 4 byte. Vì ngăn xếp được sử dụng để lưu trữ các phần tử của bộ sưu tập nên các phần tử được lưu trữ theo thứ tự, nghĩa là chúng chiếm các vùng liền kề của ngăn xếp, điều đó có nghĩa là chúng ta có thể điều hướng qua bộ sưu tập khi biết vị trí của phần tử và kích thước của bộ sưu tập. Ví dụ:

#bao gồm int main() ( int a = (10,20,30,40,10), i; for(i = 0; i<= sizeof(a)/sizeof(int); i++) printf("a[%d] has %d in %p\n", i, a[i], &a[i]); }

Kết quả:

$./chương trình
a có 10 trong 0xbfbeda88
a có 20 trong 0xbfbeda8c
a có 30 trong 0xbfbeda90
a có 40 trong 0xbfbeda94
a có 10 trong 0xbfbeda98
a có 5 trong 0xbfbeda9c

Chương trình in cho chúng ta thông tin về mảng gồm 5 phần tử: số phần tử, giá trị và địa chỉ bộ nhớ. Hãy chú ý đến địa chỉ của các phần tử - đây là những gì tôi đã nói với bạn. Các địa chỉ nằm trong một hàng và mỗi địa chỉ tiếp theo lớn hơn địa chỉ trước đó 4. Phần tử thứ 5 của bộ sưu tập, mà chúng tôi thực sự không khai báo, lưu trữ tổng số phần tử của bộ sưu tập. Điều thú vị nhất là chúng ta có thể sử dụng con trỏ một cách tương tự để duyệt mảng. Ví dụ:

#bao gồm int main() ( int a = (10,20,30,40,10), i; int * b = a; for(i = 0; i<= sizeof(a)/sizeof(int); i++) printf("a[%d] has %d in %p\n", i, *(b + i), b + i); }

Ghi chú

1. Xin lưu ý rằng chúng ta gán con trỏ b không phải địa chỉ của mảng a mà là giá trị của chính biến a, vì a trên thực tế là một con trỏ.

2. Việc sử dụng dấu ngoặc vuông biểu thị chỉ mục của các phần tử mảng là một cú pháp trong C để truy cập thuận tiện và dễ hiểu hơn vào các phần tử của bộ sưu tập.

3. Như tôi đã nói, không có mảng truyền thống trong C nên tôi gọi chúng là tập hợp để nhấn mạnh tính năng này của C.

4. Địa chỉ 1 của phần tử mảng lớn hơn địa chỉ 0 của phần tử mảng theo lượng bộ nhớ được phân bổ để lưu trữ phần tử thuộc loại này. Chúng ta làm việc với các phần tử kiểu int, mỗi phần tử sử dụng 4 byte để lưu trữ. Địa chỉ của một phần tử mảng trong bộ nhớ và của bất kỳ dữ liệu nào nói chung là địa chỉ của byte bộ nhớ đầu tiên được phân bổ để lưu trữ nó.

5. Để dễ hiểu hơn, hãy tưởng tượng bộ nhớ của máy tính là một rạp chiếu phim khổng lồ, trong đó các ghế được đánh số từ 0 đến 1_073_741_824. Dữ liệu của loại char có mông có kích thước bình thường và chúng vừa với một chiếc ghế (chiếm một byte), và những khách truy cập béo thuộc loại dài gấp đôi có mông rất lớn và mỗi người trong số họ chỉ vừa với 10 chỗ ngồi. Khi những người xem phim béo được hỏi số ghế, họ chỉ nói số ghế đầu tiên, còn số lượng và số lượng của tất cả các ghế khác có thể dễ dàng tính toán dựa trên kiểu dáng (kiểu dữ liệu) của khách truy cập. Mảng có thể được biểu diễn dưới dạng các nhóm khách xem phim cùng loại, ví dụ: một nhóm vũ công ba lê gầy loại char gồm 10 người sẽ chiếm 10 chỗ vì char vừa với một chiếc ghế và một nhóm những người yêu thích bia gồm 5 người gõ long int sẽ mất 40 byte.

6. Toán tử & và * có một số tên phổ biến nhưng bạn có thể gọi chúng là Vasya và Petya. Điều chính cần nhớ là:

& — hiển thị số ghế đầu tiên mà một khách xem phim đã ngồi. Tức là địa chỉ của byte bị chiếm dụng đầu tiên.

* — cho phép bạn xưng hô với một vị khách đang ngồi ở một nơi nhất định. Nghĩa là, nó cho phép bạn lấy một giá trị được lưu trữ tại một địa chỉ cụ thể trong bộ nhớ.

Bài viết đến đây là kết thúc, nhưng chủ đề về mảng và con trỏ, chứ đừng nói đến việc học toàn bộ ngôn ngữ C, vẫn chưa kết thúc.

Phản hồi

  1. ẩn danh nói:

    Kiểm tra xem hai mảng này có thực sự giống nhau không:
    char ch = ('R',"u',"b',"y',"D',"e',"v','.',"r',"u');
    char ch2 = "trang web";
    Trong trường hợp thứ hai, mảng chứa thêm một phần tử, /0, được sử dụng làm dấu phân cách khi in, sao chép chuỗi, v.v.

  2. quản trị viên nói:

    Trên thực tế, cả hai mảng đều chứa ký tự \0 là phần tử thứ 10 nên chúng thực sự giống nhau, nhưng tôi sẽ nói về ký tự \0 trong một bài viết riêng dành riêng cho mảng ký tự và chuỗi.

  3. ẩn danh nói:

    Vâng, bạn đã đúng, tôi đã viết nhận xét đó trước khi tự mình kiểm tra mã này trong GCC:
    #bao gồm
    #bao gồm

    int chính(void)
    {
    char ch = ('R',"u',"b', 'y', 'D', 'e', ​​'v', '.', 'r', 'u');
    char ch2 = "trang web";

    printf("%x\n", ch[ strlen(ch) ]);

    trả về 0;
    }
    In số không.

  4. quản trị viên nói:

    Điều thú vị nhất là nếu bạn tin vào đặc tả ANSI C thì bạn đúng vì nó không nói gì về việc tự động thêm ký tự null vào cuối một mảng ký tự được tạo theo cách tiêu chuẩn cho mảng (và trong K&R điều này được thực hiện rõ ràng trong cả hai phiên bản). Tôi nghĩ rằng đây là một sự khác biệt trong C99 hoặc trong trình biên dịch, vì các nhà sản xuất trình biên dịch triển khai hầu hết các khả năng của C99 một phần và một số thêm một số thứ của riêng họ. Bây giờ đã rõ tại sao việc lựa chọn trình biên dịch lại quan trọng đến vậy. Tôi sẽ cần giải quyết vấn đề này sau và viết một bài về sự khác biệt giữa các trình biên dịch C, sự hỗ trợ của chúng đối với C99 và sự khác biệt giữa ANSI C và C 99.

  5. quản trị viên nói:

    Tôi đã tiến hành điều tra nhưng tôi vẫn thông tin sai cho bạn. Trong cú pháp truyền thống, \0 không được thêm vào, chỉ là trùng hợp ngẫu nhiên khi ký tự tiếp theo trong ngăn xếp là \0, nhưng nó không phải là một phần của mảng ký tự. Nếu sử dụng strlen(), bạn có thể thấy rõ sự khác biệt 1 ký tự giữa cú pháp tạo mảng truyền thống. trong đó các ký tự được liệt kê đơn giản và sử dụng hằng chuỗi. Ký tự null chỉ được tự động thêm vào cuối mảng ký tự được tạo bằng hằng chuỗi.

  6. andr nói:

    Rất nhiều tuyên bố sai lệch. Những bài viết như vậy chỉ làm hư hỏng những lập trình viên mới làm quen. :)
    Ví dụ: “Phần tử thứ 5 của bộ sưu tập, mà chúng tôi thực sự không khai báo, lưu trữ tổng số phần tử của bộ sưu tập.” - đây là những câu chuyện chưa từng có. Ngôn ngữ C không lưu trữ độ dài của mảng ở bất kỳ đâu. Trong ví dụ này, mảng 'a' nằm ngoài giới hạn vì đối với 5 phần tử của một mảng, chỉ mục cuối cùng == 4 và bạn lập chỉ mục cho nó là 5. Do đó, bạn truy cập vào địa chỉ của biến i, biến mà trình biên dịch đặt trong bộ nhớ ngay sau mảng, do đó ở vòng lặp cuối cùng (khi i == 5), kết quả là bạn nhận được 5.

    “Như tôi đã nói, không có mảng truyền thống trong C nên tôi gọi chúng là bộ sưu tập để nhấn mạnh tính năng này của C.” - đây là một điều hoàn toàn không thể hiểu được. "Mảng truyền thống" là gì? Nhân tiện, bộ sưu tập là một thuật ngữ rộng hơn. Mảng, danh sách, ma trận, ngăn xếp và thậm chí cả bảng băm phù hợp với định nghĩa về bộ sưu tập. Tại sao lại đưa ra những thuật ngữ không phù hợp và đánh lừa người đọc?

  7. quản trị viên nói:

    cảm ơn vì đã lưu ý andr. Tôi mới bắt đầu học C gần đây và đây là những giả định của tôi. C hơi bất thường đối với tôi, đó là lý do tại sao tôi gặp phải những lỗi như vậy. Tôi sẽ sửa nó sớm.

  8. Faustman nói:

    Nói rất hay về những vũ công ba lê gầy gò và một nhóm những người yêu thích bia!))

  9. Myname nói:

    Và tôi có gcc a, như bạn nói, lưu trữ số phần tử, tạo ra giá trị 32767.