Con trỏ trong C++

Giới thiệu về con trỏ trong C++

Khái niệm con trỏ

Con trỏ (pointer) là một biến đặc biệt trong ngôn ngữ lập trình C++ và các ngôn ngữ khác, nó được sử dụng để lưu trữ địa chỉ của một biến khác trong bộ nhớ. Một biến con trỏ sẽ chứa địa chỉ bộ nhớ của biến khác, giúp ta truy xuất hoặc thao tác với giá trị của biến đó thông qua con trỏ.

Ví dụ, khi bạn khai báo một biến kiểu int có tên là “a”, nó sẽ được lưu trữ trong một ô nhớ nào đó trong bộ nhớ. Ta có thể sử dụng một biến con trỏ để lưu địa chỉ của biến “a”, và sau đó truy cập đến giá trị của “a” thông qua biến con trỏ đó.

Con trỏ là một khái niệm khá cơ bản và quan trọng trong lập trình, nó cho phép ta sử dụng các cấu trúc dữ liệu động như danh sách liên kết, cây nhị phân, xâu ký tự động, và nhiều thứ khác. Các kiến thức về con trỏ cũng là một phần quan trọng trong việc hiểu và sử dụng các thư viện và framework phức tạp hơn trong lập trình.

Vai trò của con trỏ trong C++

Con trỏ có vai trò rất quan trọng trong C++, bởi vì nó cho phép ta thực hiện các tác vụ như:

  1. Truy xuất và thay đổi giá trị của biến thông qua địa chỉ của nó trong bộ nhớ.
  2. Truy xuất và thao tác với các phần tử của mảng.
  3. Truy xuất và thao tác với các phần tử của các cấu trúc dữ liệu như danh sách liên kết, cây nhị phân, và nhiều thứ khác.
  4. Cấp phát và giải phóng bộ nhớ động để tạo ra các cấu trúc dữ liệu động.
  5. Truyền tham chiếu của các biến hoặc các cấu trúc dữ liệu tới các hàm.

Với những vai trò này, con trỏ đóng vai trò quan trọng trong việc tạo ra và quản lý các cấu trúc dữ liệu phức tạp trong các chương trình C++, từ các ứng dụng đơn giản đến các ứng dụng lớn và phức tạp.

Cách khai báo con trỏ trong C++

Để khai báo một biến con trỏ trong C++, ta sử dụng ký tự * trước tên biến. Ví dụ:
 
int *p; // khai báo một con trỏ kiểu int có tên là p
double *q; // khai báo một con trỏ kiểu double có tên là q
char *r; // khai báo một con trỏ kiểu char có tên là r

Trong các ví dụ trên, các biến p, q, và r được khai báo là các con trỏ tới kiểu int, kiểu double, và kiểu char tương ứng. Sau khi khai báo các biến con trỏ này, chúng ta có thể sử dụng chúng để lưu địa chỉ của các biến khác trong bộ nhớ và truy xuất hoặc thay đổi giá trị của các biến đó thông qua các biến con trỏ.

Sử dụng toán tử & để lấy địa chỉ của biến

Để lấy địa chỉ của một biến trong C++, ta sử dụng toán tử & trước tên biến. Ví dụ:

int a = 10;
int *p = &a; // gán địa chỉ của biến a cho con trỏ p

Trong ví dụ trên, toán tử & được sử dụng để lấy địa chỉ của biến a, sau đó địa chỉ này được gán cho biến con trỏ p. Sau đó, ta có thể sử dụng con trỏ p để truy xuất và thay đổi giá trị của biến a thông qua địa chỉ của nó trong bộ nhớ.

Sử dụng toán tử * để truy xuất giá trị của biến thông qua con trỏ

Để truy xuất giá trị của biến thông qua một con trỏ trong C++, ta sử dụng toán tử * trước tên của con trỏ. Ví dụ:

int a = 10;
int *p = &a; // gán địa chỉ của biến a cho con trỏ p
cout << *p; // in ra giá trị của biến a thông qua con trỏ p

Trong ví dụ trên, ta sử dụng toán tử * trước tên con trỏ p để truy xuất giá trị của biến a thông qua địa chỉ của nó được lưu trữ trong con trỏ p. Kết quả là giá trị của biến a được in ra trên màn hình.

Ngoài ra, ta cũng có thể sử dụng toán tử * để thay đổi giá trị của biến thông qua một con trỏ. Ví dụ:

int a = 10;
int *p = &a; // gán địa chỉ của biến a cho con trỏ p
*p = 20; // thay đổi giá trị của biến a thông qua con trỏ p
cout << a; // in ra giá trị mới của biến a

Trong ví dụ này, ta sử dụng toán tử * trước tên của con trỏ p để thay đổi giá trị của biến a thông qua địa chỉ được lưu trữ trong con trỏ p. Sau khi thay đổi giá trị này, giá trị của biến a được in ra trên màn hình.

Các phép toán trên con trỏ

Phép gán con trỏ

Trong C++, ta có thể gán giá trị của một con trỏ cho một con trỏ khác bằng cách sử dụng toán tử gán (=). Ví dụ:

int a = 10;
int *p = &a; // gán địa chỉ của biến a cho con trỏ p
int *q = p; // gán giá trị của con trỏ p cho con trỏ q

Trong ví dụ trên, ta khai báo một biến con trỏ p và gán địa chỉ của biến a cho p. Sau đó, ta khai báo một biến con trỏ khác q và gán giá trị của con trỏ p cho q. Khi đó, giá trị của q cũng sẽ trỏ tới địa chỉ của biến a trong bộ nhớ, giống như giá trị của p.

Chú ý rằng khi ta thay đổi giá trị của biến thông qua con trỏ q, giá trị của biến thông qua con trỏ p cũng sẽ thay đổi, vì cả hai con trỏ đều trỏ tới cùng một vị trí trong bộ nhớ.

Con trỏ và mảng

Trong C++, con trỏ cũng có thể được sử dụng để truy xuất và thao tác với các phần tử của một mảng.

Khi khai báo một mảng, tên của mảng là một con trỏ tới phần tử đầu tiên của mảng. Ví dụ:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // gán địa chỉ của phần tử đầu tiên của mảng cho con trỏ p

Trong ví dụ trên, ta khai báo một mảng có 5 phần tử và gán giá trị của địa chỉ của phần tử đầu tiên của mảng cho con trỏ p. Do tên của mảng cũng là một con trỏ, nên ta cũng có thể gán trực tiếp địa chỉ của mảng cho con trỏ p, như sau:

int arr[5] = {1, 2, 3, 4, 5};
int *p = &arr[0]; // gán địa chỉ của phần tử đầu tiên của mảng cho con trỏ p

Sử dụng con trỏ, ta có thể truy xuất các phần tử của mảng bằng cách thêm hoặc trừ đi một số nguyên lượng tử để di chuyển đến vị trí của phần tử cần truy xuất. Ví dụ:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // gán địa chỉ của phần tử đầu tiên của mảng cho con trỏ p
cout << *(p + 2); // in ra giá trị của phần tử thứ 3 của mảng (vị trí 2)

Trong ví dụ trên, ta sử dụng toán tử + để di chuyển con trỏ p đến vị trí của phần tử thứ 3 của mảng (tức là phần tử có vị trí thứ 2 trong mảng), sau đó sử dụng toán tử * để truy xuất giá trị của phần tử này thông qua con trỏ p.

Ngoài ra, ta cũng có thể sử dụng toán tử [] để truy xuất giá trị của các phần tử của mảng thông qua con trỏ. Ví dụ:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // gán địa chỉ của phần tử đầu tiên của mảng cho con trỏ p
cout << p[2]; // in ra giá trị của phần tử thứ 3 của mảng (vị trí 2)

Trong ví dụ này, ta sử dụng toán tử [] với con trỏ p để truy xuất giá trị của phần tử thứ 3 của mảng (tức là phần tử có vị trí thứ 2 trong mảng).

Một số ví dụ khác:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // gán địa chỉ của phần tử đầu tiên của mảng cho con trỏ p

for (int i = 0; i < 5; i++) {
  cout << *(p + i) << " "; // in ra giá trị của từng phần tử của mảng thông qua con trỏ p
}

cout << endl;

for (int i = 0; i < 5; i++) {
  cout << p[i] << " "; // in ra giá trị của từng phần tử của mảng thông qua con trỏ p sử dụng toán tử []
}

Trong hai ví dụ trên, ta sử dụng con trỏ p để truy xuất từng phần tử của mảng bằng cách di chuyển con trỏ đến vị trí của từng phần tử. Trong ví dụ thứ nhất, ta sử dụng toán tử + để di chuyển con trỏ p đến vị trí của từng phần tử, sau đó sử dụng toán tử * để truy xuất giá trị của phần tử này. Trong ví dụ thứ hai, ta sử dụng toán tử [] với con trỏ p để truy xuất giá trị của từng phần tử của mảng.

Lưu ý rằng, khi truy xuất giá trị của các phần tử của mảng thông qua con trỏ, ta cần chú ý đến kiểu dữ liệu của con trỏ và kiểu dữ liệu của các phần tử của mảng. Nếu ta khai báo một con trỏ kiểu int nhưng trỏ đến một mảng kiểu double, thì kết quả truy xuất giá trị thông qua con trỏ sẽ không chính xác.

Con trỏ và chuỗi

Trong C++, chuỗi được biểu diễn dưới dạng một mảng các ký tự liên tiếp nhau kết thúc bằng ký tự null (‘\0’). Vì vậy, ta có thể sử dụng con trỏ để trỏ đến các phần tử của chuỗi, hoặc để trỏ đến địa chỉ của chuỗi.

Ví dụ:

char str[] = "Hello world";
char *p = str; // gán địa chỉ của phần tử đầu tiên của chuỗi cho con trỏ p

while (*p != '\0') {
  cout << *p; // in ra từng ký tự của chuỗi thông qua con trỏ p
  p++; // di chuyển con trỏ đến phần tử kế tiếp
}

cout << endl;

char *q = &str[6]; // gán địa chỉ của phần tử thứ 6 trong chuỗi cho con trỏ q

while (*q != '\0') {
  cout << *q; // in ra từng ký tự của chuỗi, bắt đầu từ phần tử thứ 6
  q++; // di chuyển con trỏ đến phần tử kế tiếp
}

Trong ví dụ trên, ta sử dụng con trỏ p để truy xuất từng ký tự của chuỗi, bắt đầu từ phần tử đầu tiên của chuỗi. Ta cũng có thể sử dụng toán tử [] để truy xuất từng phần tử của chuỗi thông qua con trỏ. Ví dụ:

char str[] = "Hello world";
char *p = str;

cout << *(p + 1) << endl; // in ra ký tự 'e'
cout << p[2] << endl; // in ra ký tự 'l'

Ở đây, ta sử dụng toán tử + để di chuyển con trỏ p đến vị trí của phần tử thứ 2 trong chuỗi (ký tự ‘e’), sau đó sử dụng toán tử * để truy xuất giá trị của phần tử này. Ta cũng có thể sử dụng toán tử [] để truy xuất giá trị của từng phần tử của chuỗi thông qua con trỏ.

Con trỏ và hàm

Con trỏ là một trong những công cụ quan trọng trong việc sử dụng hàm trong C++. Ta có thể sử dụng con trỏ để truyền đối số vào hàm hoặc để trả về giá trị từ hàm.

Khi truyền đối số vào hàm thông qua con trỏ, ta sẽ truyền địa chỉ của biến chứa giá trị của đối số đó. Trong hàm, ta sẽ sử dụng con trỏ để truy xuất đến giá trị của biến được truyền vào. Như vậy, khi thay đổi giá trị của biến thông qua con trỏ trong hàm, giá trị của biến được thay đổi ngay cả sau khi hàm đã kết thúc.

Ví dụ:

void increment(int* ptr) {
  (*ptr)++;
}

int main() {
  int num = 5;
  int* ptr = &num;

  cout << "Before increment: " << *ptr << endl;
  increment(ptr);
  cout << "After increment: " << *ptr << endl;

  return 0;
}

Ở đây, ta định nghĩa một hàm increment nhận một con trỏ tới một số nguyên, sau đó tăng giá trị của số nguyên tại địa chỉ được trỏ đến bởi con trỏ đó. Trong hàm main, ta khởi tạo một biến num và một con trỏ ptr trỏ tới biến num. Ta sau đó gọi hàm increment với đối số là con trỏ ptr, kết quả là giá trị của biến num được tăng lên 1.

Ví dụ:

int* createArray(int size) {
  int* arr = new int[size];

  for (int i = 0; i < size; i++) {
    arr[i] = i;
  }

  return arr;
}

int main() {
  int* ptr = createArray(5);

  for (int i = 0; i < 5; i++) {
    cout << ptr[i] << " ";
  }

  delete[] ptr; // giải phóng bộ nhớ đã cấp phát

  return 0;
}

Ở đây, ta định nghĩa một hàm createArray nhận một tham số size và trả về một con trỏ tới một mảng các số nguyên có kích thước size. Trong hàm main, ta gọi hàm createArray với đối số là số 5, và gán giá trị trả về cho con trỏ ptr. Ta sau đó in ra từng phần tử trong mảng thông qua con trỏ ptr. . Cuối cùng, ta giải phóng bộ nhps đã cấp phát thông qua toán tử delete[].

Ngoài ra, con trỏ cũng có thể được sử dụng để trả về nhiều giá trị từ một hàm. Để làm được điều này, ta có thể truyền một hoặc nhiều con trỏ vào hàm, và sau đó thay đổi giá trị tại các địa chỉ được trỏ đến bởi các con trỏ đó.

Ví dụ:

void getMinMax(int arr[], int size, int* minPtr, int* maxPtr) {
  *minPtr = arr[0];
  *maxPtr = arr[0];

  for (int i = 1; i < size; i++) {
    if (arr[i] < *minPtr) {
      *minPtr = arr[i];
    }
    if (arr[i] > *maxPtr) {
      *maxPtr = arr[i];
    }
  }
}

int main() {
  int arr[] = { 5, 2, 8, 3, 9, 1 };
  int size = sizeof(arr) / sizeof(arr[0]);
  int min, max;

  getMinMax(arr, size, &min, &max);

  cout << "Min value: " << min << endl;
  cout << "Max value: " << max << endl;

  return 0;
}

Đoạn code trên định nghĩa một hàm getMinMax có nhiệm vụ tìm giá trị nhỏ nhất và lớn nhất trong một mảng số nguyên và lưu kết quả vào hai con trỏ minPtrmaxPtr truyền vào hàm.

Hàm getMinMax nhận vào một mảng số nguyên arr có kích thước size và hai con trỏ minPtrmaxPtr.

Gán giá trị đầu tiên của mảng arr cho *minPtr*maxPtr để khởi tạo giá trị tối thiểu và tối đa ban đầu.

Duyệt qua các phần tử trong mảng arr từ phần tử thứ hai đến phần tử cuối cùng. Nếu giá trị của phần tử đang xét nhỏ hơn giá trị tối thiểu hiện tại (*minPtr), thì gán giá trị đó cho *minPtr. Nếu giá trị của phần tử đang xét lớn hơn giá trị tối đa hiện tại (*maxPtr), thì gán giá trị đó cho *maxPtr.

Trong hàm main, khởi tạo một mảng số nguyên arr và tính kích thước của mảng. Khởi tạo hai biến minmax để lưu giá trị tối thiểu và tối đa của mảng. Gọi hàm getMinMax và truyền vào mảng arr, kích thước của mảng, và hai con trỏ &min&max. In ra giá trị tối thiểu và tối đa của mảng.

Các phép toán số học trên con trỏ

Trong C++, con trỏ cũng có thể được sử dụng trong các phép toán số học như cộng, trừ, nhân và chia.

  • Phép cộng: Con trỏ và một số nguyên có thể được cộng với nhau, kết quả là một con trỏ mới trỏ đến vị trí bộ nhớ tiếp theo của con trỏ cũ. Ví dụ: ptr = ptr + 1; sẽ dịch con trỏ ptr đến phần tử tiếp theo trong mảng hoặc vùng nhớ được cấp phát.

  • Phép trừ: Hai con trỏ có thể được trừ nhau, kết quả là một giá trị nguyên biểu thị khoảng cách giữa hai con trỏ trong bộ nhớ. Ví dụ: int diff = ptr2 - ptr1; sẽ tính toán khoảng cách giữa hai con trỏ ptr1 và ptr2.

  • Phép nhân và chia: Phép nhân và chia không được hỗ trợ cho con trỏ trực tiếp, nhưng chúng có thể được sử dụng thông qua các biểu thức số học với các số nguyên.

Các phép toán số học trên con trỏ thường được sử dụng trong các thuật toán sắp xếp và tìm kiếm trên mảng hoặc danh sách liên kết.

Sử dụng con trỏ để quản lý bộ nhớ động

Cấp phát động bằng toán tử new

Trong C++, để cấp phát bộ nhớ động, chúng ta có thể sử dụng toán tử new. Toán tử new sẽ cấp phát bộ nhớ cho một biến có kiểu dữ liệu cụ thể và trả về con trỏ tới vùng nhớ mới được cấp phát.

Cú pháp chung của toán tử new như sau:

pointer_variable = new data_type;
pointer_variable = new data_type[size];

Trong đó:

  • pointer_variable là con trỏ sẽ trỏ tới vùng nhớ được cấp phát.
  • data_type là kiểu dữ liệu của biến, mảng hoặc đối tượng được cấp phát.
  • size là số lượng phần tử của mảng cần được cấp phát.

Ví dụ:

int* ptr = new int;  // cấp phát động một biến kiểu int
int* arr = new int[10];  // cấp phát động một mảng 10 phần tử kiểu int

Sau khi sử dụng toán tử new để cấp phát động, chúng ta cần phải giải phóng bộ nhớ đã cấp phát bằng toán tử delete để tránh lãng phí bộ nhớ và gây ra lỗi chương trình.

Giải phóng bộ nhớ động bằng toán tử delete

Để giải phóng bộ nhớ động được cấp phát bằng toán tử new, ta sử dụng toán tử delete. Cú pháp để sử dụng toán tử delete như sau:

delete pointer_variable;
delete[] pointer_variable;

Trong đó:

  • pointer_variable là con trỏ đến vùng nhớ cần giải phóng.

Nếu chúng ta đã sử dụng toán tử new để cấp phát động một biến, ta sử dụng toán tử delete như sau:

int* ptr = new int; // cấp phát động một biến kiểu int
delete ptr; // giải phóng bộ nhớ đã cấp phát

Nếu chúng ta đã sử dụng toán tử new để cấp phát động một mảng, ta sử dụng toán tử delete[] như sau:

int* arr = new int[10]; // cấp phát động một mảng 10 phần tử kiểu int
delete[] arr; // giải phóng bộ nhớ đã cấp phát

Lưu ý rằng, nếu ta không giải phóng bộ nhớ đã cấp phát bằng toán tử delete, sẽ dẫn đến lãng phí bộ nhớ và có thể gây ra lỗi chương trình.

Cấp phát động cho mảng bằng toán tử new[]

Để cấp phát động cho một mảng trong C++ ta sử dụng toán tử new[]. Cú pháp sử dụng như sau:

data_type* array_name = new data_type[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 của mảng.
  • size là số lượng phần tử cần phân bổ cho mảng.

Ví dụ, để cấp phát một mảng gồm 5 phần tử kiểu int, ta sử dụng câu lệnh sau:

int* my_array = new int[5];

Câu lệnh này sẽ cấp phát bộ nhớ động để lưu trữ một mảng gồm 5 phần tử kiểu int. Các phần tử trong mảng sẽ được khởi tạo giá trị mặc định tùy thuộc vào kiểu dữ liệu. Trong trường hợp của kiểu int, các phần tử sẽ được khởi tạo giá trị là 0.

Lưu ý rằng, khi sử dụng toán tử new[] để cấp phát động cho một mảng, chúng ta cần sử dụng toán tử delete[] để giải phóng bộ nhớ đã được phân bổ cho mảng sau khi sử dụng xong.

Giải phóng bộ nhớ động cho mảng bằng toán tử delete[]

Để giải phóng bộ nhớ động đã được cấp phát cho một mảng trong C++, chúng ta sử dụng toán tử delete[]. Cú pháp sử dụng như sau:

delete[] array_name;

Trong đó, array_name là tên của mảng cần giải phóng bộ nhớ.

Ví dụ, để giải phóng bộ nhớ đã được cấp phát cho một mảng my_array kiểu int có 5 phần tử, ta sử dụng câu lệnh sau:

delete[] my_array;

Câu lệnh này sẽ giải phóng bộ nhớ đã được cấp phát cho mảng my_array. Lưu ý rằng, nếu không giải phóng bộ nhớ đã được cấp phát cho mảng khi không sử dụng nữa, sẽ dẫn đến lãng phí bộ nhớ và có thể gây ra vấn đề về hiệu suất hoạt động của chương trình.

Các khái niệm liên quan đến con trỏ

Trong ngôn ngữ lập trình C++, có một số khái niệm quan trọng liên quan đến con trỏ như sau:

  1. Địa chỉ: là địa chỉ vùng nhớ mà biến được lưu trữ trong bộ nhớ của máy tính.

  2. Con trỏ: là một biến đặc biệt, lưu trữ địa chỉ của một biến khác trong bộ nhớ.

  3. Toán tử địa chỉ (&): được sử dụng để lấy địa chỉ của một biến trong bộ nhớ.

  4. Toán tử gián giá trị (*): được sử dụng để truy xuất giá trị của biến thông qua con trỏ.

  5. Cấp phát động: là quá trình cấp phát và giải phóng bộ nhớ trong quá trình thực thi của chương trình.

  6. Mảng và con trỏ: một mảng cũng có thể được xem như là một con trỏ tới phần tử đầu tiên của mảng.

  7. Chuỗi và con trỏ: chuỗi trong C++ được biểu diễn bằng một mảng các ký tự, và có thể được truy xuất thông qua con trỏ.

  8. Con trỏ và hàm: con trỏ được sử dụng để truyền đối số cho một hàm, hoặc để trả về giá trị từ một hàm.

  9. Các phép toán số học trên con trỏ: các phép toán như cộng, trừ, nhân, chia, ++ và — có thể được thực hiện trên con trỏ.

  10. Cấp phát động bằng toán tử new: toán tử new được sử dụng để cấp phát bộ nhớ động trong C++.

  11. Giải phóng bộ nhớ động bằng toán tử delete: toán tử delete được sử dụng để giải phóng bộ nhớ đã được cấp phát động bằng toán tử new.

Tất cả các khái niệm trên đều rất quan trọng trong việc sử dụng con trỏ trong C++, và nắm vững chúng sẽ giúp chúng ta hiểu rõ hơn về cách thức hoạt động của con trỏ và sử dụng chúng một cách hiệu quả.

Ví dụ minh họa

Ví dụ về con trỏ đơn giản

Đây là một ví dụ đơn giản về con trỏ trong C++, nó sẽ hiển thị địa chỉ và giá trị của một biến thông qua con trỏ:

#include <iostream>
using namespace std;

int main() {
  int num = 10;
  int* ptr = &num;  // khai báo con trỏ và gán địa chỉ của biến num

  cout << "Giá trị của biến num là: " << num << endl;
  cout << "Địa chỉ của biến num là: " << &num << endl;

  cout << "Giá trị của biến num thông qua con trỏ là: " << *ptr << endl;
  cout << "Địa chỉ của biến num thông qua con trỏ là: " << ptr << endl;

  return 0;
}

Kết quả sẽ là:

Giá trị của biến num là: 10
Địa chỉ của biến num là: 0x7ffc6c5e6a8c
Giá trị của biến num thông qua con trỏ là: 10
Địa chỉ của biến num thông qua con trỏ là: 0x7ffc6c5e6a8c

Ở đây, chúng ta đã khai báo một biến số nguyên num và một con trỏ kiểu số nguyên ptr, sau đó gán địa chỉ của biến num cho con trỏ ptr. Chúng ta có thể sử dụng toán tử * để truy cập giá trị của biến num thông qua con trỏ ptr, và sử dụng toán tử & để lấy địa chỉ của biến num.

Ví dụ về con trỏ và mảng

Dưới đây là một ví dụ về cách sử dụng con trỏ để truy cập các phần tử của một mảng:

#include <iostream>
using namespace std;

int main() {
  int arr[] = {1, 2, 3, 4, 5};
  int *ptr = arr;

  for (int i = 0; i < 5; i++) {
    cout << "Value at index " << i << " is: " << *(ptr + i) << endl;
  }

  return 0;
}

Trong ví dụ này, chúng ta có một mảng arr chứa các số nguyên từ 1 đến 5. Chúng ta cũng có một con trỏ ptr trỏ đến địa chỉ đầu tiên của mảng arr.

Trong vòng lặp, chúng ta sử dụng con trỏ ptr để truy cập các phần tử của mảng arr. Để truy cập phần tử thứ i, chúng ta sử dụng cú pháp *(ptr + i). Chúng ta có thể thấy rằng việc sử dụng con trỏ cho phép chúng ta truy cập các phần tử của mảng một cách linh hoạt.

Ví dụ về con trỏ và chuỗi

Dưới đây là một ví dụ về cách sử dụng con trỏ để thao tác với chuỗi:

#include <iostream>
using namespace std;

int main() {
  char str[] = "Hello, world!";
  char *ptr = str;

  while (*ptr != '\0') {
    cout << *ptr;
    ptr++;
  }

  return 0;
}

Trong ví dụ này, chúng ta có một chuỗi str được khai báo là một mảng các ký tự. Chúng ta cũng có một con trỏ ptr trỏ đến địa chỉ đầu tiên của chuỗi str.

Trong vòng lặp, chúng ta sử dụng con trỏ ptr để duyệt qua các ký tự của chuỗi str. Để truy cập ký tự hiện tại, chúng ta sử dụng cú pháp *ptr. Chúng ta cũng kiểm tra xem có phải ký tự kết thúc chuỗi \0 hay không để dừng vòng lặp.

Chúng ta có thể thấy rằng việc sử dụng con trỏ cho phép chúng ta thao tác với chuỗi một cách linh hoạt.

Ví dụ về cấp phát và giải phóng bộ nhớ động

Dưới đây là một ví dụ về cấp phát và giải phóng bộ nhớ động bằng toán tử new và delete trong C++:

#include <iostream>
using namespace std;

int main() {
    // Cấp phát một mảng động gồm 5 phần tử kiểu int
    int *arr = new int[5];

    // Gán giá trị cho các phần tử của mảng
    for (int i = 0; i < 5; i++) {
        arr[i] = i + 1;
    }

    // In các giá trị của mảng
    for (int i = 0; i < 5; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;

    // Giải phóng bộ nhớ động đã cấp phát cho mảng
    delete[] arr;

    return 0;
}

Ở đây, ta sử dụng toán tử new để cấp phát một mảng động gồm 5 phần tử kiểu int và gán giá trị cho các phần tử của mảng. Sau đó, ta sử dụng vòng lặp để in ra các giá trị của mảng. Cuối cùng, ta giải phóng bộ nhớ động đã cấp phát cho mảng bằng toán tử delete[].

Ví dụ về con trỏ void

Con trỏ void là một loại đặc biệt của con trỏ trong C++, nó cho phép ta trỏ đến bất kỳ kiểu dữ liệu nào mà ta muốn. Tuy nhiên, để truy xuất giá trị của con trỏ void, ta phải ép kiểu (type cast) về kiểu tương ứng trước khi sử dụng.

Ví dụ dưới đây minh họa cách sử dụng con trỏ void để trỏ đến các kiểu dữ liệu khác nhau và ép kiểu để truy xuất giá trị của chúng:

#include <iostream>
using namespace std;

int main() {
  int i = 5;
  float f = 3.14;
  char c = 'a';

  void* ptr;

  // trỏ đến biến i kiểu int
  ptr = &i;
  cout << "Value of i: " << *((int*) ptr) << endl;

  // trỏ đến biến f kiểu float
  ptr = &f;
  cout << "Value of f: " << *((float*) ptr) << endl;

  // trỏ đến biến c kiểu char
  ptr = &c;
  cout << "Value of c: " << *((char*) ptr) << endl;

  return 0;
}

Kết quả khi chạy chương trình sẽ là:

Value of i: 5
Value of f: 3.14
Value of c: a

Ở ví dụ trên, ta sử dụng con trỏ void để trỏ đến các kiểu dữ liệu khác nhau, sau đó ép kiểu về kiểu tương ứng và truy xuất giá trị của chúng. Ta dùng toán tử * để truy xuất giá trị của con trỏ, và toán tử (type*) để ép kiểu con trỏ về kiểu tương ứng.

Tổng kết

Con trỏ là một khái niệm quan trọng trong ngôn ngữ lập trình C++, cho phép truy xuất và thay đổi giá trị của biến thông qua địa chỉ của nó. Ta có thể tóm tắt lại một số kiến thức chung về con trỏ trong C++ như sau:

  • Con trỏ là một biến đặc biệt lưu trữ địa chỉ của một biến khác.
  • Con trỏ giúp truy cập và thao tác trên vùng nhớ đã được cấp phát động.
  • Các phép toán số học không thể được thực hiện trên các con trỏ, trừ phép cộng với một số nguyên và phép trừ với một con trỏ khác.
  • Toán tử & được sử dụng để lấy địa chỉ của một biến, và toán tử * được sử dụng để truy xuất giá trị của biến thông qua con trỏ.
  • Cấp phát động bằng toán tử new và giải phóng bộ nhớ động bằng toán tử delete.
  • Cấp phát động cho mảng bằng toán tử new[] và giải phóng bộ nhớ động cho mảng bằng toán tử delete[].
  • Con trỏ void là một con trỏ đặc biệt có thể trỏ tới một kiểu dữ liệu bất kỳ.

Dưới đây là những lưu ý quan trọng khi sử dụng con trỏ:

  • Sử dụng con trỏ không đúng cách có thể dẫn đến những lỗi nghiêm trọng như truy cập vào vùng nhớ không được cấp phát, hoặc gây ra lỗi segmentation fault.
  • Việc sử dụng con trỏ cần phải cẩn thận để tránh tràn bộ nhớ và tăng hiệu suất của chương trình.
  • Không nên sử dụng con trỏ trong trường hợp không cần thiết. Có thể sử dụng các cấu trúc dữ liệu khác để thay thế con trỏ như mảng hay vector để tránh gây ra những lỗi không đáng có.
  • Khi sử dụng con trỏ, cần chú ý đến các quy tắc về kiểu dữ liệu và địa chỉ để tránh gây ra những lỗi không đáng có.

Mặc dù con trỏ là một tính năng quan trọng trong C++, tuy nhiên, việc sử dụng con trỏ cũng cần phải có một số lưu ý và hạn chế để tránh những lỗi không mong muốn, đặc biệt là trong các ứng dụng lớn và phức tạp.

Một số khuyến cáo và hạn chế khi sử dụng con trỏ trong C++ bao gồm:

  1. Tránh sử dụng con trỏ một cách vô tội vạ: việc sử dụng con trỏ cần phải được cân nhắc và có kế hoạch, và không nên sử dụng con trỏ chỉ để làm cho code trông ngắn gọn hơn hoặc dễ đọc hơn.

  2. Luôn kiểm tra giá trị con trỏ trước khi sử dụng: trước khi truy cập vào một đối tượng thông qua con trỏ, cần kiểm tra xem con trỏ có trỏ tới một đối tượng hợp lệ hay không. Nếu không, việc truy xuất đến đối tượng sẽ gây ra lỗi truy cập bộ nhớ không hợp lệ.

  3. Tránh sử dụng con trỏ hỗn hợp (mixed-type pointer): việc sử dụng con trỏ hỗn hợp, tức là con trỏ trỏ tới một kiểu dữ liệu khác với kiểu dữ liệu được khai báo cho con trỏ, sẽ gây ra rất nhiều rắc rối và lỗi không mong muốn.

  4. Sử dụng các hàm thư viện chuẩn thay vì tự viết code: việc sử dụng các hàm thư viện chuẩn sẽ giảm thiểu rủi ro gây lỗi khi sử dụng con trỏ.

  5. Luôn giải phóng bộ nhớ động sau khi sử dụng xong: khi sử dụng cấp phát bộ nhớ động, cần nhớ giải phóng bộ nhớ sau khi sử dụng xong để tránh lãng phí bộ nhớ và gây ra lỗi bộ nhớ.

Tóm lại, sử dụng con trỏ trong C++ là một công cụ mạnh mẽ nhưng cũng đầy rủi ro. Việc sử dụng con trỏ cần phải cân nhắc và có kế hoạch, và luôn phải tuân thủ các lưu ý và hạn chế để tránh các lỗi không mong muốn và tối ưu hóa hiệu suất của ứng dụng.

Bài tập thực hành

Để thành thạo về sử dụng con trỏ trong C++. Bạn nên thường xuyên thực hành code để nắm vững các kiến thức cũng như kỹ năng lập trình về con trỏ.

Dưới đây là một số bài tập có thể sử dụng con trỏ trong C++:

  1. Viết chương trình nhập vào một dãy số nguyên và sắp xếp chúng theo thứ tự tăng dần bằng cách sử dụng con trỏ.

  2. Viết chương trình tính tổng của hai ma trận vuông bất kỳ và lưu kết quả vào một ma trận khác sử dụng con trỏ.

  3. Viết chương trình đảo ngược một chuỗi sử dụng con trỏ.

  4. Viết chương trình tìm kiếm một phần tử trong một mảng sử dụng con trỏ.

  5. Viết chương trình đọc dữ liệu từ một tệp tin và hiển thị nội dung lên màn hình sử dụng con trỏ.

  6. Viết chương trình sắp xếp một danh sách liên kết sử dụng con trỏ.

  7. Viết chương trình tạo ra một số ngẫu nhiên và sử dụng con trỏ để hiển thị các giá trị đó theo thứ tự ngược lại.

  8. Viết chương trình đọc dữ liệu từ một tệp tin, đếm số lượng từ trong tệp tin đó và hiển thị kết quả sử dụng con trỏ.

  9. Viết chương trình tính tổng của hai đa thức bất kỳ sử dụng con trỏ.

  10. Viết chương trình tìm kiếm và thay thế một chuỗi con trong một chuỗi sử dụng con trỏ.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *