IV. Mảng và con trỏ trong C
A. Mảng trong C
1. Khái niệm về mảng
Mảng là một cấu trúc dữ liệu trong lập trình, được sử dụng để lưu trữ nhiều giá trị cùng loại. Mỗi phần tử trong mảng được đánh số thứ tự và được truy cập bằng chỉ số của nó.
2. Khai báo mảng
Cấu trúc của một mảng được xác định bởi kiểu dữ liệu của phần tử và số lượng phần tử trong mảng. Khi khai báo mảng, ta phải chỉ định kiểu dữ liệu và số lượng phần tử. Cú pháp khai báo mảng trong C như sau:
<data_type> <array_name>[<array_size>];
Trong đó:
<data_type>
là kiểu dữ liệu của các phần tử trong mảng
<array_name>
là tên mảng
<array_size>
là số lượng phần tử trong mảng
Ví dụ:
int numbers[5];
Trong ví dụ trên, chúng ta đã khai báo một mảng số nguyên có tên là numbers với 5 phần tử.
3. Truy cập phần tử trong mảng
Phần tử trong mảng được truy cập bằng chỉ số của nó. Chỉ số của phần tử đầu tiên trong mảng là 0. Ví dụ: numbers[0]
là phần tử đầu tiên trong mảng numbers
.
Để truy cập các phần tử trong mảng, ta sử dụng chỉ số của phần tử. Chỉ số bắt đầu từ 0 và kết thúc tại số lượng phần tử trừ đi 1.
Ví dụ:
int numbers[5] = {1, 2, 3, 4, 5};
printf("%d", numbers[0]); // output: 1
printf("%d", numbers[2]); // output: 3
Trong ví dụ trên, chúng ta đã khai báo mảng số nguyên numbers
có 5 phần tử và gán giá trị cho từng phần tử. Sau đó, chúng ta sử dụng chỉ số của phần tử để truy cập và in ra giá trị của phần tử tương ứng.
4. Các phép toán trên mảng
Các phép toán được thực hiện trên mảng bao gồm gán giá trị cho phần tử, sao chép mảng, so sánh mảng, sắp xếp mảng, tìm kiếm trong mảng,...
ví dụ về các phép toán trên mảng như sau :
a.Gán giá trị cho phần tử trong mảng
int arr[5];
arr[0] = 10; // gán giá trị 10 cho phần tử đầu tiên của mảng
b.Lấy giá trị của phần tử trong mảng
int arr[5] = {10, 20, 30, 40, 50};
int x = arr[2]; // lấy giá trị của phần tử thứ 3 trong mảng (30)
c.Tính tổng các phần tử trong mảng
int arr[5] = {10, 20, 30, 40, 50};
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += arr[i];
}
d.Tìm phần tử lớn nhất và nhỏ nhất trong mảng
int arr[5] = {10, 20, 30, 40, 50};
int max = arr[0];
int min = arr[0];
for (int i = 1; i < 5; i++) {
if (arr[i] > max) {
max = arr[i];
}
if (arr[i] < min) {
min = arr[i];
}
}
e.Sắp xếp các phần tử trong mảng
int arr[5] = {50, 40, 30, 20, 10};
for (int i = 0; i < 5; i++) {
for (int j = i + 1; j < 5; j++) {
if (arr[i] > arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
Lưu ý rằng đây chỉ là một số phép toán cơ bản trên mảng trong C, và còn rất nhiều các phép toán khác có thể thực hiện trên mảng, tùy thuộc vào yêu cầu của bài toán.
5. Mảng đa chiều
Mảng đa chiều là một mảng có thể chứa nhiều mảng con. Ví dụ: int matrix[3][4];
khai báo một mảng đa chiều matrix gồm 3 hàng và 4 cột kiểu int.
Mảng đa chiều là một cấu trúc dữ liệu trong ngôn ngữ lập trình C cho phép lưu trữ các giá trị liên quan đến nhau dưới dạng ma trận hoặc bảng. Ví dụ, một mảng đa chiều có thể được sử dụng để lưu trữ các điểm ảnh trong hình ảnh hoặc các thông tin về các đối tượng trong một trò chơi.
Cú pháp khai báo một mảng đa chiều như sau:
int myArray[3][4]; // Mảng đa chiều 3 hàng x 4 cột
Để truy cập các phần tử trong mảng đa chiều, ta cần sử dụng các chỉ số tương ứng cho hàng và cột. Ví dụ, để truy cập phần tử ở hàng thứ hai và cột thứ ba của mảng myArray, ta có thể sử dụng câu lệnh như sau:
int element = myArray[1][2]; // Lấy phần tử ở hàng 2, cột 3
Mảng đa chiều cũng hỗ trợ các phép toán tương tự như mảng một chiều, bao gồm các phép toán gán, cộng, trừ, nhân và chia. Ví dụ, để cộng hai mảng đa chiều array1 và array2, ta có thể sử dụng vòng lặp như sau:
int array1[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int array2[3][3] = {{9, 8, 7}, {6, 5, 4}, {3, 2, 1}};
int sumArray[3][3];
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
sumArray[i][j] = array1[i][j] + array2[i][j];
}
}
Trong ví dụ trên, ta khai báo ba mảng đa chiều array1, array2 và sumArray, với kích thước 3 hàng x 3 cột. Sau đó, ta sử dụng vòng lặp để duyệt qua từng phần tử của hai mảng array1 và array2, thực hiện phép toán cộng tương ứng, và lưu kết quả vào mảng sumArray.
Ví dụ sử dụng mảng 2 chiều để tính một định thức của một ma trận 3x3:
Để tính định thức của ma trận vuông cấp 3x3, chúng ta có thể sử dụng công thức sau:
det(A) = a11(a22a33 - a23a32) - a12(a21a33 - a23a31) + a13(a21a32 - a22a31)
Trong đó, aij
là phần tử ở hàng i
, cột j
của ma trận A.
Chúng ta có thể sử dụng mảng hai chiều để lưu trữ ma trận A.
#include <stdio.h>
int main() {
int A[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int det = A[0][0] * (A[1][1] * A[2][2] - A[1][2] * A[2][1]) -
A[0][1] * (A[1][0] * A[2][2] - A[1][2] * A[2][0]) +
A[0][2] * (A[1][0] * A[2][1] - A[1][1] * A[2][0]);
printf("Determinant of matrix A: %d\n", det);
return 0;
}
Ở đây, chúng ta khai báo mảng A có kích thước 3x3 và gán giá trị cho các phần tử của mảng. Sau đó, chúng ta tính định thức của ma trận A bằng cách sử dụng công thức đã nêu ở trên và in kết quả ra màn hình.
Kết quả sẽ là:
Determinant of matrix A: 0
Vì định thức của ma trận A bằng 0.
B.Con trỏ trong C
Con trỏ là một trong những tính năng quan trọng nhất của ngôn ngữ lập trình C. Nó cho phép ta thao tác trực tiếp với vùng nhớ trong bộ nhớ của chương trình. Con trỏ là một biến đặc biệt trong C, chứa địa chỉ bộ nhớ của một biến khác. Việc sử dụng con trỏ giúp ta tối ưu hoá về mặt bộ nhớ và thực hiện các thao tác phức tạp như truyền đối số tham chiếu và cấp phát động bộ nhớ. Trong phần này, chúng ta sẽ tìm hiểu về cú pháp và cách sử dụng con trỏ trong C.
1. Khái niệm về con trỏ
Con trỏ là một biến đặc biệt trong C, nó lưu trữ địa chỉ bộ nhớ của một biến khác trong chương trình. Nhờ vào đó, ta có thể truy cập và thay đổi giá trị của biến đó thông qua con trỏ
2. Địa chỉ và toán tử địa chỉ
Địa chỉ là một giá trị duy nhất được gán cho mỗi ô nhớ trong bộ nhớ máy tính. Toán tử địa chỉ "&" được sử dụng để lấy địa chỉ của một biến.
Ví dụ:
#include <stdio.h>
int main() {
int a = 10;
printf("Địa chỉ của a: %p", &a); // %p để in ra giá trị địa chỉ
return 0;
}
Output: Địa chỉ của a: 0x7ffeefbff49c
Ở đây, biến a có giá trị 10 và được lưu trữ tại địa chỉ 0x7ffeefbff49c trên bộ nhớ.
Toán tử * được sử dụng để truy cập giá trị của một biến thông qua con trỏ.
Ví dụ:
#include <stdio.h>
int main() {
int a = 10;
int *ptr = &a; // Khai báo con trỏ và gán địa chỉ của biến a cho con trỏ ptr
printf("Giá trị của a: %d", *ptr); // Truy cập giá trị của biến a thông qua con trỏ ptr
return 0;
}
Output: Giá trị của a: 10
Ở đây, ta đã khai báo một con trỏ *ptr và gán địa chỉ của biến a cho nó. Khi muốn truy cập giá trị của biến a thông qua con trỏ ptr, ta sử dụng toán tử * để giải tham chiếu đến giá trị tại địa chỉ được lưu trữ trong con trỏ ptr.
3. Khai báo con trỏ
Để khai báo một con trỏ, ta sử dụng toán tử "*" trước tên biến.
Ví dụ: int *p; khai báo một con trỏ kiểu int tên p.
int *ptr; // khai báo con trỏ ptr kiểu int
float *fptr; // khai báo con trỏ fptr kiểu float
char *cptr; // khai báo con trỏ cptr kiểu char
Trong ví dụ trên, ta khai báo ba con trỏ ptr
, fptr
và cptr
với các kiểu dữ liệu khác nhau, tương ứng là int, float và char. Chúng ta có thể thấy cấu trúc của một câu lệnh khai báo con trỏ là <kiểu dữ liệu> *<tên con trỏ>;
. Ở đây, toán tử *
được sử dụng để chỉ định rằng biến được khai báo là một con trỏ, và <kiểu dữ liệu>
là kiểu dữ liệu của đối tượng mà con trỏ sẽ trỏ tới.
4. Truy cập giá trị và địa chỉ của biến thông qua con trỏ
Toán tử "*
" được sử dụng để truy cập giá trị của biến mà con trỏ đang trỏ tới. Toán tử "&" được sử dụng để lấy địa chỉ của biến.
5. Các phép toán trên con trỏ
Phép cộng: con trỏ có thể được tăng hoặc giảm một số lượng byte để trỏ tới một vùng nhớ khác.
Phép trừ: con trỏ cũng có thể được trừ một số lượng byte để trỏ tới một vùng nhớ khác.
Ví dụ về phép cộng và phép trừ:
int a = 10;
int *ptr = &a;
// Thực hiện phép cộng và trừ với con trỏ
ptr = ptr + 1;
ptr = ptr - 1;
// In ra giá trị và địa chỉ của biến a và con trỏ ptr
printf("a = %d, dia chi a = %p\n", a, &a);
printf("ptr = %p, gia tri cua con tro ptr = %d\n", ptr, *ptr);
Kết quả đầu ra:
a = 10, dia chi a = 0x7ffd0bf56abc
ptr = 0x7ffd0bf56ac0, gia tri cua con tro ptr = 10
Phép so sánh: ta có thể so sánh hai con trỏ để kiểm tra xem chúng có trỏ tới cùng một vùng nhớ hay không.
Ví dụ :
int a = 10;
int b = 20;
int *ptr1 = &a;
int *ptr2 = &b;
if(ptr1 == ptr2) {
printf("Hai con tro tro toi cung mot vi tri nho\n");
} else {
printf("Hai con tro tro toi vi tri khac nhau\n");
}
Kết quả đầu ra:
Hai con tro tro toi vi tri khac nhau
Phép dịch trái (<<): Di chuyển các bit sang trái theo số bít được chỉ định bởi toán tử bên phải của phép dịch. Ví dụ: a << 2 di chuyển các bit của biến a sang trái 2 bit.
Phép dịch phải (>>): Di chuyển các bit sang phải theo số bít được chỉ định bởi toán tử bên phải của phép dịch. Nếu biến là số dương, bit cao được điền vào 0. Nếu biến là số âm, bit cao được điền vào 1. Ví dụ: a >> 2 di chuyển các bit của biến a sang phải 2 bit.
Phép AND bit (&): Thực hiện phép AND bit cho hai toán hạng. Kết quả là một số nguyên chứa bit 1 tại vị trí nào cả hai toán hạng có bit 1. Ví dụ: a & b trả về giá trị của biến a được AND với biến b.
Phép OR bit (|): Thực hiện phép OR bit cho hai toán hạng. Kết quả là một số nguyên chứa bit 1 tại vị trí nào cả hai toán hạng có bit 1 hoặc nếu chỉ có một toán hạng có bit 1. Ví dụ: a | b trả về giá trị của biến a được OR với biến b.
Phép XOR bit (^): Thực hiện phép XOR bit cho hai toán hạng. Kết quả là một số nguyên chứa bit 1 tại vị trí nào chỉ có một trong hai toán hạng có bit 1. Ví dụ: a ^ b trả về giá trị của biến a được XOR với biến b.
Để hiểu rõ hơn về phép toán XOR trên con trỏ, chúng ta có thể xem xét ví dụ sau:
#include <stdio.h>
int main() {
int a = 10;
int b = 12;
int *ptr1 = &a;
int *ptr2 = &b;
// XOR operation on two pointers
int *result = (int*)((uintptr_t)ptr1 ^ (uintptr_t)ptr2);
printf("The result of XOR operation is %p", result);
return 0;
}
Trong ví dụ này, chúng ta khai báo hai biến a và b có giá trị lần lượt là 10 và 12. Sau đó, chúng ta khai báo hai con trỏ ptr1 và ptr2 trỏ tới hai biến này.
Sau đó, chúng ta thực hiện phép toán XOR trên hai con trỏ ptr1 và ptr2. Để thực hiện phép toán này, trước hết chúng ta phải chuyển đổi các con trỏ về kiểu uintptr_t, sau đó thực hiện phép toán XOR giữa hai giá trị này. Kết quả của phép toán XOR là một giá trị con trỏ mới. Cuối cùng, chúng ta in kết quả của phép toán này ra màn hình.
Lưu ý rằng phép toán XOR trên hai con trỏ sẽ trả về một giá trị con trỏ ngẫu nhiên, không liên quan gì tới địa chỉ của các biến mà hai con trỏ trỏ tới. Tuy nhiên, phép toán này vẫn có ứng dụng trong một số trường hợp, như trong việc tạo ra các giá trị con trỏ ngẫu nhiên để sử dụng trong các thuật toán tạo số ngẫu nhiên.
6. Con trỏ và mảng
Một con trỏ có thể được sử dụng để trỏ tới một phần tử của một mảng. Ta có thể sử dụng con trỏ để truy cập các phần tử của mảng và thực hiện các phép toán trên mảng.
Dưới đây là một ví dụ về sử dụng con trỏ để truy cập và thay đổi các phần tử trong một mảng:
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // khai báo con trỏ ptr trỏ tới mảng arr
// in ra các phần tử trong mảng bằng cách sử dụng con trỏ ptr
printf("Cac phan tu trong mang la: ");
for (int i = 0; i < 5; i++) {
printf("%d ", *ptr); // in ra giá trị mà con trỏ ptr đang trỏ tới
ptr++; // di chuyển con trỏ ptr đến phần tử tiếp theo trong mảng
}
// thay đổi giá trị của phần tử đầu tiên trong mảng bằng cách sử dụng con trỏ ptr
*arr = 10;
// in lại các phần tử trong mảng để kiểm tra xem giá trị của phần tử đầu tiên đã được thay đổi hay chưa
printf("\nCac phan tu trong mang sau khi thay doi la: ");
ptr = arr; // trỏ lại con trỏ ptr tới đầu mảng arr
for (int i = 0; i < 5; i++) {
printf("%d ", *ptr);
ptr++;
}
return 0;
}
Output:
Cac phan tu trong mang la: 1 2 3 4 5
Cac phan tu trong mang sau khi thay doi la: 10 2 3 4 5
Trong ví dụ trên, ta khai báo một mảng arr có 5 phần tử và một con trỏ ptr trỏ tới đầu mảng. Sau đó, ta sử dụng con trỏ ptr để truy cập và in ra các phần tử trong mảng bằng cách di chuyển con trỏ ptr qua từng phần tử trong mảng. Cuối cùng, ta sử dụng con trỏ ptr để thay đổi giá trị của phần tử đầu tiên trong mảng và in lại các phần tử trong mảng để kiểm tra xem giá trị của phần tử đầu tiên đã được thay đổi hay chưa.