C++ Con trỏ (Pointer) toàn thư: Phần 4: Con trỏ "đa cấp". Đánh nhau bằng con trỏ.

Ngày cuối cùng của năm 2018 rét vcđ. Buổi trưa bảo lên ngủ 20p rồi dậy thì thành ra nằm ì trong chăn đến 5hh30 pm mới thèm mò dậy! Thiện tai, thiện tai! 😑 Năm nay trên thành phố quê mình tổ chức Carnaval cũng hoành tráng lắm, nhưng khổ cái là trời lạnh vãi, mò mẫm đi đâu; khổ tập 2 là ko có người yêu; nên thôi ngồi nhà viết nốt bài cuối cùng về Con trỏ mà không liên quan đến OOP. 

2019 này kỷ niệm 100 năm ngày chấm dứt Thế chiến II; hi vọng sẽ có đủ sức khỏe để học đc nhiều nùi kiến thức hơn! 😁

Quay trở lại seri Con trỏ, trong phần Dàn bài, mình đã trình bày:
Trong bài viết này, mình sẽ trình bày về Con trỏ "đa cấp" cũng như tất cả mọi lưu ý, mưu mẹo, lọc lừa và điểm nhấn quan trọng về Con trỏ từ đầu tới giờ. Để trước khi bước sang Con trỏ "thông minh" (Smart Pointer), chúng ta sẽ chắc chắn rằng mình không còn vướng bận j với Con trỏ thông thường (Raw Pointer) nữa.

Con trỏ "đa cấp".

Thật ra để Google search về loại con trỏ này, các bạn hãy search là "Pointer to Pointer". Còn cái tên Con trỏ "đa cấp" là mình ks về hồi năm 2 đại học, khi code mảng 2 chiều trong môn Cấu trúc dữ liệu và giải thuật. 

Nói nôm na thì Con trỏ đa cấp có nghĩa là con trỏ có nhiều *, hoặc con trỏ mà trỏ đến một con trỏ khác. Ví dụ
  • int *a; là con trỏ cấp 1
  • int **b; là con trỏ cấp 2
  • int ***c; là con trỏ cấp 3
  • int ****d; là con trỏ cấp 4
  •  ... vân vân và mây mây. 
Con trỏ đa cấp cũng là con trỏ, do đó nó cũng có các tính chất, các phép toán tương tự như của một con trỏ. Chỉ khác một thứ duy nhất, đó là dữ liệu trỏ đến. Nếu Con trỏ 1* trỏ vào một biến cụ thể; thì con trỏ 2* sẽ trỏ vào 1 con trỏ 1*, rồi con trỏ 1* đó sẽ trỏ vào 1 biến cụ thể. Tương tự như vậy với con trỏ ngàn*. Nói tóm lại Con trỏ cấp cao sẽ trỏ vào con trỏ cấp thấp hơn, tức là có giá trị là địa chỉ lưu trữ con trỏ cấp thấp hơn.

Sử dụng con trỏ đa cấp.

Con trỏ đa cấp được sử dụng để thay đổi giá trị của con trỏ cấp thấp hơn được trỏ bởi con trỏ đa cấp đó. Nói vậy thôi chứ trong thực tế, ta cũng ít khi gặp phải con trỏ nhiều *; tôi thấy đến 3* đã là rất hiếm gặp rồi. Thường thì ta hay sử dụng con trỏ void vì nó có thể trỏ vào mọi thứ (như đã nói ở Phần 1: Căn bản về Con trỏ), chứ cũng ít khi sử dụng nhiều *.

Tuy nhiên thì ứng dụng của con trỏ đa cấp thấp vẫn rất thường xuyên như:
  • Con trỏ 2* (cấp 2) hay được sử dụng để làm việc với mảng 2 chiều.
  • Con trỏ 3* (cấp 3) hay được sử dụng để làm việc với mảng 3 chiều.

Đánh nhau bằng Con trỏ.

Bạn có phân biệt được 4 thể loại: **p, &&p, *&p, &*p ko?

* là toán tử lấy giá trị của một con trỏ; thể hiện rằng thằng đứng sau nó là tên một con trỏ. & là toán tử lấy địa chỉ vùng nhớ của một biến; thể hiện rằng thằng đứng sau nó là tên một biến. Như vậy đã đủ để bạn phân biệt 4 cái ký tự bựa bựa kia chưa nhỉ! 😄

Con trỏ và Tham chiếu là 2 khái niệm trong C++; tuy cùng giúp ta tương tác với biến số, nhưng tính chất lại hoàn toàn khác nhau. Nếu như Con trỏ linh động hơn vì có thể chỉ trỏ lung tung đến mọi thứ miễn là tương thích kiểu dữ liệu và tham số hình thức (riêng con trỏ void sẽ trỏ đến được mọi thứ); thì Tham chiếu lại là một cái tên khác của biến, và bị gắn chặt với biến đó.

Do đó, ta có thể phân biệt chúng như sau:
  • **p nghĩa là một con trỏ p cấp 2, **p sẽ trỏ đến một biến con trỏ cấp 1 khác. Và con trỏ cấp 1 đó sẽ trỏ đến 1 biến cụ thể.
  • &&p nghĩa là một tham chiếu (tên khác) của một biến. Và biến đó lại là 1 tham chiếu (tên khác) của một biến cụ thể. Kiểu như mình tên là Cường, còn tham chiếu của mình là Sơn (vì ở nhà, mọi người trong họ đều gọi mình là Sơn); nhưng mình lại tự xưng là Ngọc Sơn Bùi Văn Cường. Thì nghĩa là &&p tương ứng với "Ngọc Sơn Bùi Văn Cường"; còn &p tương ứng với "Sơn".
  • *&p nghĩa là một con trỏ mà trỏ vào 1 tham chiếu &p. Tham chiếu &p là tên khác của một biến cụ thể.
  • &*p nghĩa là một tham chiếu (tên khác) của con trỏ p (con trỏ có nhiều tên).

Tiếp tục phân biệt 4 thể loại: char *p, const char *p, char *const p, const char *const p?

Để phân biệt được sự khác nhau giữa 4 thể loại này, bạn cần xem lại bài viết nói về Hằng con trỏ và Con trỏ hằng ở Phần 2 của seri. Trong quá trình code thực tế, tôi chắc chắn rằng đa số các bạn sẽ gặp phải lỗi không thể ép kiểu từ const char* về char*! Đầy là lỗi rất phổ biến khi ta làm việc với xâu ký tự. Lỗi này xuất phát từ việc chúng ta chưa hiểu rõ về Hằng con trỏ và Con trỏ Hằng, nói theo thuật ngữ quốc tế thì là "multable pointer" và "immutable pointer" vậy.

4 thể loại kiểu dữ liệu này là sự kết hợp giữa xâu ký tự và con trỏ. Mỗi loại gồm 2 kiểu là "mutable" (sửa được) và "immutable" (ko sửa được). Do đó, để phân biệt 4 kiểu dữ liệu này, ta có thể hiểu như sau:
  • char *p: p là con trỏ thông thường (thay đổi được); và trỏ đến một xâu ký tự thông thường (thay đổi được).
  • const char *p: p là một con trỏ thông thường (thay đổi được); và trỏ đến một hằng xâu ký tự (ko thể thay đổi được).
  • char* const p: p là một Hằng con trỏ (ko thay đổi được); và trỏ đến một xâu ký tự thông thường (thay đổi được).
  • const char* const p: p là một Hằng con trỏ (ko thay đổi được); và trỏ đến một hằng xâu ký tự (ko thay đổi được).
Mẹo là thế này. Cứ thấy const đứng trước cái j thì cái đó (cái ngay sau const) ko thể thay đổi. Ngoài ra, việc ép kiểu tường minh sẽ không khả dụng vì ta không hể ép một Hằng con trỏ trở thành một con trỏ thông thường được!

Vài dòng so sánh giữa các con trỏ.

Sau khi đọc xong hết seri Con trỏ từ đầu đến giờ, tôi nghĩ các bạn đã có thể giải thích được ý nghĩa của những câu lệnh sau rồi.

Sau khi đã tìm hiểu về Con trỏ Hàm rồi, cho dù có bao nhiêu dấu đóng mở ngoặc đi nữa, chúng ta vẫn sẽ hiểu và giải thích được hết! Trời ạ, mình gõ ra mấy dòng trên cho nhiều rồi mà giờ lại ngại viết giải thích quá! 😅 Đến giai đoạn này mà còn phải giải thích từng dòng lệnh thì chắc ngại chết mất. Mình nhìn qua chả thấy dòng nào khó hiểu cả, nhưng thôi cứ giải thích đại mấy dòng vậy:
  • Đoạn câu lệnh đầu tiên chỉ đơn thuần là việc so sánh 2 con trỏ với nhau. Với 2 con trỏ có kiểu dữ liệu khác nhau, ta phải tiến hành ép kiểu tường minh về cùng 1 kiểu dữ liệu con trỏ chung.
  • Đoạn câu lệnh ở giữa là việc so sánh con trỏ với một số nguyên. Đương nhiên rồi, như mình đã nói ban đầu, giá trị của con trỏ là một số nguyên; do đó việc so sánh này hoàn toàn khả thi. Có điều ta không được quên việc ép kiểu tường minh!
  • Với đoạn câu lệnh thứ 3, sau khi đọc xong Phần 3: Con trỏ Hàm; mình nghĩ chẳng có cái khỉ j là khó khăn để hiểu nữa cả. main là một hàm, nó cũng sẽ có địa chỉ; nên đương nhiên việc so sánh là khả thi rồi. Còn cái mớ int(*)() thì nó cũng chỉ đơn giản là kiểu dữ liệu của một con trỏ hàm thôi. Trong câu lệnh này, ta tiến hành ép kiểu của p về kiểu của một con trỏ hàm để tương thích với con trỏ hàm main của hàm main. int tương ứng với int main(); (*) ám chỉ rằng đây là một con trỏ; còn () ám chỉ rằng đây là con trỏ hàm, và danh sách tham số của hàm đó là không có j. Ez operation!

Tổng kết.

Trên đây là toàn bộ những hiểu biết của mình về Con trỏ thông thường (Raw Pointer) của C++. Phần sau, phần cuối cùng, chúng ta sẽ tìm hiểu về Con trỏ thông minh (Smart Pointer). Đây là một khái niệm được xây dựng dựa trên Proxy Pattern của Lập trình hướng đối tương. Điều này cũng như Smart Pointer, mình đã trình bày qua ở bài viết "Smart Pointer trong C++? Nó cũng chỉ là 1 "đối tượng" thôi, dễ hiểu mà!" rồi.

Trong bài viết cuối cùng của seri này, mình sẽ trình bày kỹ càng hơn về cơ chế của Con trỏ thông minh, ưu điểm/hạn chế cũng nhưng những kiểu dữ liệu của Con trỏ thông minh. 

Cuối cùng, tạm biệt 2018. Chào đón 2019 với những thử thách và kiến thức mới! Năm con lợn của mình. Con lợn ở bên dưới có tên là "Snort" nhé các bạn; nhìn ngộ vãi! 😁


Nhận xét

Đăng nhận xét

Bài đăng phổ biến từ blog này

Trên con đường tu đạo luôn cực kỳ theo đuổi!

Vừa ngộ ra sự vi diệu của Padding Oracle Attack thì được tin crush hồi lớp 12 sắp cưới.