Bài học này là tùy chọn, dành cho những độc giả nâng cao muốn tìm hiểu thêm về C ++. 

Không có bài học nào trong tương lai được xây dựng dựa trên bài học này.

Một con trỏ tới một con trỏ chính xác là những gì bạn mong đợi: một con trỏ giữ địa chỉ của một con trỏ khác.

1. Con trỏ tới con trỏ

Một con trỏ bình thường đến một int được khai báo bằng một dấu hoa thị:

int *ptr; // pointer to an int, one asterisk

Một con trỏ tới một con trỏ tới một int được khai báo bằng cách sử dụng hai dấu hoa thị

int **ptrptr; // pointer to a pointer to an int, two asterisks

Một con trỏ tới một con trỏ hoạt động giống như một con trỏ bình thường – bạn có thể thực hiện chuyển hướng qua nó để truy xuất giá trị được trỏ tới. Và bởi vì bản thân giá trị đó là một con trỏ, bạn có thể thực hiện chuyển hướng qua nó một lần nữa để đến giá trị cơ bản. Các chuyển hướng này có thể được thực hiện liên tiếp:

int value = 5;
 
int *ptr = &value;
std::cout << *ptr; // Indirection through pointer to int to get int value
 
int **ptrptr = &ptr;
std::cout << **ptrptr; // first indirection to get pointer to int, second indirection to get int value

Kết quả

5
5

Lưu ý rằng bạn không thể đặt trực tiếp một con trỏ tới một con trỏ thành một giá trị:

int value = 5;
int **ptrptr = &&value; // not valid

Điều này là do địa chỉ của toán tử (operator &) yêu cầu một lvalue, nhưng & value là một rvalue.

Tuy nhiên, một con trỏ tới một con trỏ có thể được đặt thành null:

int **ptrptr = nullptr; // use 0 instead prior to C++11

2. Mảng con trỏ

Con trỏ đến con trỏ có một vài công dụng. Cách sử dụng phổ biến nhất là cấp phát động một mảng con trỏ:

int **array = new int*[10]; // allocate an array of 10 int pointers

Điều này hoạt động giống như một mảng được cấp phát động chuẩn, ngoại trừ các phần tử của mảng có kiểu “con trỏ tới số nguyên” thay vì số nguyên.

3. Mảng được cấp phát động hai chiều

Một cách sử dụng phổ biến khác cho con trỏ tới con trỏ là tạo điều kiện cho các mảng đa chiều được phân bổ động (xem 9.5 – Mảng đa chiều để xem xét các mảng nhiều chiều).

Không giống như mảng cố định hai chiều, có thể dễ dàng khai báo như thế này:

int array[10][5];

Việc cấp phát động một mảng hai chiều khó hơn một chút. Bạn có thể bị cám dỗ để thử một cái gì đó như thế này:

int **array = new int[10][5]; // won’t work!

Nhưng nó sẽ không hoạt động.

Có hai giải pháp khả thi ở đây. Nếu thứ nguyên mảng ngoài cùng bên phải là hằng số thời gian biên dịch, bạn có thể thực hiện điều này:

int (*array)[5] = new int[10][5];

Dấu ngoặc đơn được yêu cầu ở đây để đảm bảo mức độ ưu tiên thích hợp. Trong C ++ 11 hoặc mới hơn, đây là một nơi tốt để sử dụng loại trừ tự động:

auto array = new int[10][5]; // so much simpler!

Thật không may, giải pháp tương đối đơn giản này không hoạt động nếu bất kỳ thứ nguyên mảng nào không nằm ngoài cùng bên trái không phải là hằng số thời ta biên dịch. Trong trường hợp đó, chúng ta phải phức tạp hơn một chút. Đầu tiên, chúng tôi cấp phát một mảng con trỏ (như trên). Sau đó, chúng tôi lặp qua mảng con trỏ và cấp phát một mảng động cho mỗi phần tử mảng. Mảng hai chiều động của chúng ta là mảng một chiều động của mảng một chiều động!

int **array = new int*[10]; // allocate an array of 10 int pointers — these are our rows
for (int count = 0; count < 10; ++count)
    array[count] = new int[5]; // these are our columns

Sau đó, chúng ta có thể truy cập vào mảng của mình như bình thường:

array[9][4] = 3; // This is the same as (array[9])[4] = 3;

Với phương pháp này, vì mỗi cột mảng được cấp phát động độc lập, nên có thể tạo mảng hai chiều được cấp phát động không phải là hình chữ nhật. Ví dụ, chúng ta có thể tạo một mảng hình tam giác:

int **array = new int*[10]; // allocate an array of 10 int pointers — these are our rows
for (int count = 0; count < 10; ++count)
    array[count] = new int[count+1]; // these are our columns

Trong ví dụ trên, hãy lưu ý rằng mảng [0] là mảng có độ dài 1, mảng [1] là mảng có độ dài 2, v.v.

Việc cấp phát một mảng hai chiều được cấp phát động bằng phương pháp này cũng yêu cầu một vòng lặp:

for (int count = 0; count < 10; ++count)
    delete[] array[count];
delete[] array; // this needs to be done last

Lưu ý rằng chúng ta xóa mảng theo thứ tự ngược lại mà chúng ta đã tạo nó (trước tiên là các phần tử, sau đó là chính mảng). Nếu chúng ta xóa mảng trước các phần tử của mảng, thì chúng ta sẽ phải truy cập vào bộ nhớ được phân bổ để xóa các phần tử mảng. Và điều đó sẽ dẫn đến hành vi không xác định.

Bởi vì việc cấp phát và giải quyết các mảng hai chiều là phức tạp và dễ lộn xộn, nên việc “san phẳng” mảng hai chiều (có kích thước x x y) thành mảng một chiều có kích thước x * y thường dễ dàng hơn:

// Instead of this:
int **array = new int*[10]; // allocate an array of 10 int pointers — these are our rows
for (int count = 0; count < 10; ++count)
    array[count] = new int[5]; // these are our columns
 
// Do this
int *array = new int[50]; // a 10x5 array flattened into a single array

Sau đó, phép toán đơn giản có thể được sử dụng để chuyển đổi chỉ số hàng và cột cho mảng hai chiều hình chữ nhật thành một chỉ mục duy nhất cho mảng một chiều:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

int getSingleIndex(int row, int col, int numberOfColumnsInArray)
{
     return (row * numberOfColumnsInArray) + col;
}
 
// set array[9,4] to 3 using our flattened array
array[getSingleIndex(9, 4, 5)] = 3;

4. Chuyển con trỏ theo địa chỉ

Giống như chúng ta có thể sử dụng một tham số con trỏ để thay đổi giá trị thực của đối số cơ bản được truyền vào, chúng ta có thể chuyển một con trỏ tới một con trỏ đến một hàm và sử dụng con trỏ đó để thay đổi giá trị của con trỏ mà nó trỏ tới (bạn nhầm lẫn chưa?) .

Tuy nhiên, nếu chúng ta muốn một hàm có thể sửa đổi đối số con trỏ trỏ đến, điều này thường được thực hiện tốt hơn bằng cách sử dụng tham chiếu đến một con trỏ thay thế. Vì vậy, chúng ta sẽ không nói thêm về nó ở đây.

Chúng ta sẽ nói thêm về chuyển theo địa chỉ và chuyển bằng tham chiếu trong phần tiếp theo.

Trỏ tới một con trỏ tới một con trỏ tới…

Cũng có thể khai báo một con trỏ tới một con trỏ tới một con trỏ:

int ***ptrx3;

Chúng có thể được sử dụng để cấp phát động một mảng ba chiều. Tuy nhiên, làm như vậy sẽ yêu cầu một vòng lặp bên trong một vòng lặp và cực kỳ phức tạp để làm đúng.

Bạn thậm chí có thể khai báo một con trỏ tới một con trỏ tới một con trỏ tới một con trỏ:

int ****ptrx4;

Hoặc cao hơn, nếu bạn muốn.

Tuy nhiên, trong thực tế, những điều này không được sử dụng nhiều vì bạn không thường xuyên cần chuyển hướng nhiều như vậy.

5. Phần kết luận

Chúng tôi khuyên bạn nên tránh sử dụng con trỏ tới con trỏ trừ khi không có tùy chọn nào khác vì chúng phức tạp để sử dụng và có khả năng nguy hiểm. Thật dễ dàng để thực hiện chuyển hướng thông qua một con trỏ rỗng hoặc treo bằng con trỏ thông thường – điều này thật dễ dàng gấp đôi với một con trỏ tới một con trỏ vì bạn phải thực hiện chuyển hướng kép để đến giá trị cơ bản!

Cài ứng dụng cafedev để dễ dàng cập nhật tin và học lập trình mọi lúc mọi nơi tại đây.

Nguồn và Tài liệu tiếng anh tham khảo:

Tài liệu từ cafedev:

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!