1. Con trỏ ký tự (Character Pointer)
1.1. Trỏ tới biến đơn lẻ
Con trỏ ký tự thường được sử dụng để lưu trữ địa chỉ của một biến ký tự. Thông qua con trỏ này, chúng ta có thể truy cập và thay đổi giá trị của biến gốc.
int main() {
char symbol = 'A';
char *ptr = &symbol;
*ptr = 'B'; // Thay đổi giá trị thông qua con trỏ
return 0;
}
1.2. Trỏ tới chuỗi hằng
Khi gán một chuỗi ký tự cho con trỏ, thực chất con trỏ đang lưu địa chỉ của ký tự đầu tiên trong vùng nhớ tĩnh. Dữ liệu này là hằng số và không nên bị sửa đổi. Sử dụng từ khóa const giúp trình biên dịch phát hiện lỗi sớm hơn nếu có cố gắng ghi vào vùng nhớ này.
#include <stdio.h>
int main() {
const char *stringPtr = "hello world";
printf("%s\n", stringPtr);
return 0;
}
1.3. Phân biệt vùng nhớ mảng và con trỏ
Một điểm quan trọng cần lưu ý là sự khác biệt về vùng nhớ giữa mảng ký tự và con trỏ ký tự. Mảng được cấp phát trên stack và mỗi lần khai báo là một vùng nhớ độc lập. Trong khi đó, các con trỏ trỏ tới cùng một chuỗi hằng sẽ chia sẻ cùng một địa chỉ trong vùng nhớ tĩnh.
#include <stdio.h>
int main() {
char buf1[] = "hello world";
char buf2[] = "hello world";
const char *ptr1 = "hello world";
const char *ptr2 = "hello world";
if(buf1 == buf2)
printf("buf1 and buf2 are same\n");
else
printf("buf1 and buf2 are not same\n");
if(ptr1 == ptr2)
printf("ptr1 and ptr2 are same\n");
else
printf("ptr1 and ptr2 are not same\n");
return 0;
}
/*
Kết quả:
buf1 and buf2 are not same
ptr1 and ptr2 are same
*/
2. Con trỏ mảng (Array Pointer)
2.1. Cú pháp khai báo
Cần phân biệt rõ giữa mảng con trỏ và con trỏ mảng. Ưu tiên toán tử quyết định ý nghĩa của khai báo. Dấu ngoặc đơn () giúp ưu tiên liên kết con trỏ trước.
int* p[10]; // Mảng chứa 10 con trỏ kiểu int
int (*p)[10]; // Con trỏ trỏ tới mảng gồm 10 phần tử kiểu int
2.2. Ứng dụng với mảng hai chiều
Con trỏ mảng thường hữu ích khi làm việc với mảng hai chiều. Con trỏ cấp 2 (double pointer) không thể thay thế hoàn toàn do cơ chế tính toán địa chỉ khác nhau khi tăng con trỏ.
#include <stdio.h>
void showTable(int (*matrix)[5], int row, int col) {
for(int i = 0; i < row; i++) {
for(int j = 0; j < col; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
}
int main() {
int data[3][5] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
showTable(data, 3, 5);
return 0;
}
2.3. Khai báo phức tạp
Xét ví dụ về một mảng chứa các con trỏ mảng. Việc hiểu cú pháp này giúp nắm vững thứ tự ưu tiên toán tử.
int (*p[10])[5]; // Mảng 10 phần tử, mỗi phần tử là con trỏ trỏ tới mảng 5 số nguyên
int arr[5] = {1, 2, 3, 4, 5};
p[0] = &arr; // Gán địa chỉ mảng arr cho phần tử đầu tiên của p
printf("%d", *(*p[0])); // Truy cập phần tử đầu tiên của arr
3. Con trỏ hàm (Function Pointer)
3.1. Phân biệt con trỏ hàm và hàm trả về con trỏ
Vị trí của dấu ngoặc đơn và dấu sao quyết định bản chất của khai báo.
void* func1(); // Hàm trả về con trỏ void
void (*func2)(); // Con trỏ trỏ tới hàm trả về void
3.2. Ví dụ đặc biệt
Con trỏ hàm có thể được ép kiểu từ một số nguyên, ví dụ như địa chỉ 0, dù việc gọi hàm tại địa chỉ này thường dẫn đến lỗi runtime trong hệ điều hành hiện đại.
// Ép kiểu số 0 thành con trỏ hàm và gọi
(*(void (*)())0)();
// Khai báo hàm signal phức tạp
void (*signal(int, void(*)(int)))(int);
3.3. Mảng con trỏ hàm
Chúng ta có thể lưu trữ nhiều địa chỉ hàm vào một mảng để dễ dàng quản lý bảng vectơ hoặc menu lệnh.
int (*p[10])(); // Mảng 10 con trỏ, mỗi con trỏ trỏ tới hàm trả về int
4. Hàm gọi ngược (Callback Function)
4.1. Định nghĩa
Callback là kỹ thuật truyền con trỏ hàm làm tham số cho một hàm khác. Hàm nhận tham số sẽ gọi lại hàm được truyền vào tại một thời điểm thích hợp.
void taskA() {}
void taskB(void (*callback)()) {
callback(); // Gọi lại hàm A
}
int main() {
taskB(&taskA);
return 0;
}
4.2. Thực hiện sắp xếp tổng quát mô phỏng qsort
Áp dụng con trỏ hàm để xây dựng hàm sắp xếp có thể làm việc với nhiều kiểu dữ liệu khác nhau thông qua hàm so sánh tùy chỉnh.
#include<stdio.h>
void genericSort(void* base, int count, int size, int (*compare)(void*, void*));
int compareInt(void* a, void* b);
void swapBytes(char* x, char* y, int width);
int main() {
int dataset[] = { 10, 2, 8, 4, 6, 1, 9, 3, 7, 5 };
int len = sizeof(dataset) / sizeof(int);
genericSort(dataset, len, sizeof(int), compareInt);
for (int i = 0; i < len; i++) {
printf("%d, ", dataset[i]);
}
return 0;
}
void swapBytes(char* x, char* y, int width) {
for (int i = 0; i < width; i++) {
char temp = *x;
*x = *y;
*y = temp;
x++;
y++;
}
}
int compareInt(void* a, void* b) {
int val1 = *(int*)a;
int val2 = *(int*)b;
return val1 - val2;
}
void genericSort(void* base, int count, int size, int (*compare)(void*, void*)) {
for (int i = 0; i < count - 1; i++) {
for (int j = 0; j < count - 1 - i; j++) {
char* elem1 = (char*)base + j * size;
char* elem2 = (char*)base + (j + 1) * size;
if (compare(elem1, elem2) > 0) {
swapBytes(elem1, elem2, size);
}
}
}
}