Con Trỏ
(Pointer)
Trịnh Tấn Đạt
Khoa CNTT - Đại Học Sài Gòn
Email:
Website: />
Nội dung
▪
Biến tĩnh vs. Biến động
▪
Con trỏ
▪
Các phép toán trên con trỏ
▪
Con trỏ và mảng một chiều
▪
Cấp phát vùng nhớ động
▪
Con trỏ cấp 2
▪
Con trỏ và mảng nhiều chiều
▪
Mảng con trỏ
▪
Con trỏ hằng, void
▪
Con trỏ hàm (option)
Khai báo biến trong C
❖
▪
▪
▪
Quy trình xử lý của trình biên dịch
Dành riêng một vùng nhớ với địa chỉ duy nhất để lưu biến đó.
Liên kết địa chỉ ơ nhớ đó với tên biến.
Khi gọi tên biến, nó sẽ truy xuất tự động đến ô nhớ đã liên kết với tên biến
Ví dụ: int a = 5;
// Giả sử địa chỉ lưu trữ biến a là 0x6
0x2 0x6 0xA
…
5
int B[3];
// Giả sử địa chỉ lưu trữ biến mảng B là 0x2
0x2 0x6 0xA 0xE
…
Biến tĩnh vs. Biến động
▪ Một số hạn chế có thể gặp phải khi sử dụng các biến tĩnh:
o Cấp phát ơ nhớ dư, gây ra lãng phí ơ nhớ.
o Cấp phát ơ nhớ thiếu, chương trình thực thi bị lỗi.
▪ Biến động:
o Khi chạy chương trình, kích thước của biến, vùng nhớ và địa chỉ vùng nhớ được cấp phát cho
biến có thể thay đổi.
o Sau khi sử dụng xong có thể giải phóng để tiết kiệm chỗ trong bộ nhớ.
▪ Biến (variable): Biến là một ô nhớ đơn lẻ hoặc một vùng nhớ được hệ điều hành cấp
phát cho chương trình C++ nhằm để lưu trữ giá trị vào bên trong vùng nhớ đó.
Ví dụ: int m; // một vùng nhớ có kích thước 4 bytes sẽ được cấp phát.
Virtual memory & Physical memory
❖ Virtual memory & Physical
memory
▪ Chúng ta chỉ có thể trỏ đến
vùng nhớ ảo (virtual
memory) trên máy tính, cịn
việc truy xuất đến bộ nhớ vật
lý (physical memory) từ bộ
nhớ ảo phải được thực hiện
bởi thiết bị phần cứng có tên
là Memory management unit
(MMU) và một chương trình
định vị địa chỉ bộ nhớ gọi
là Virtual address space .
Variable address & address-of operator
▪
Địa chỉ của biến (variable address) mà chúng ta nhìn thấy thật ra chỉ là những giá
trị đã được đánh số thứ tự đặt trên Virtual memory.
▪
Để lấy được địa chỉ ảo của biến trong chương trình, chúng ta sử dụng tốn tử lấy
địa chỉ (address-of operator) ‘&’ đặt trước tên biến.
Ví dụ:
int x = 5;
cout << x; // print the value of variable x
cout << &x; // print the memory address of variable x
Reference
▪
Tham chiếu (Reference): Mục đích của tham chiếu trong C++ là tạo ra một biến
khác có cùng kiểu dữ liệu nhưng sử dụng chung vùng nhớ với biến được tham
chiếu đến.
▪
Ví dụ:
Lưu ý: address-of operator và
reference dùng chung 1 ký hiệu
&
Như vậy, mọi hành vi thay đổi giá trị của i_ref đều tác động trực tiếp đến i1.
Lưu ý: Biến tham chiếu sẽ có địa chỉ cố định sau khi khởi tạo. Chúng ta không thể tham chiếu lại lần nữa.
Dereference operator
▪
▪
Tốn tử trỏ đến (dereference operator) hay cịn gọi là indirection operator (tốn tử
điều hành gián tiếp) được kí hiệu bằng dấu sao "*" cho phép chúng ta lấy ra giá trị
của vùng nhớ có địa chỉ cụ thể.
Ví dụ:
Dereference operator
▪
Ngoài việc truy xuất giá trị trong vùng nhớ của một địa chỉ cụ thể, toán tử trỏ đến
(dereference operator) cịn có thể dùng để thay đổi giá trị bên trong vùng nhớ đó.
Dereference operator
▪
Toán tử trỏ đến cho phép chúng ta thao tác trực tiếp trên Virtual memory mà không
cần thông qua định danh (tên biến).
▪
Khác với tham chiếu (reference), toán tử trỏ đến (dereference operator) không tạo
ra một tên biến khác, mà nó truy xuất trực tiếp đến vùng nhớ có địa chỉ cụ thể
trên Virtual memory.
Con trỏ (Pointer)
▪
Địa chỉ của biến là một con số, có thể tạo biến khác để lưu địa chỉ của biến này.
▪
Con trỏ (pointer) là một biến được dùng để lưu trữ địa chỉ của biến khác.
▪
Khác với tham chiếu, con trỏ là một biến có địa chỉ độc lập so với vùng nhớ mà nó trỏ đến,
nhưng giá trị bên trong vùng nhớ của con trỏ chính là địa chỉ của biến (hoặc địa chỉ ảo) mà nó
trỏ tới.
❖ Kiểu con trỏ cho phép:
o Truyền tham số kiểu địa chỉ
o Biểu diễn các kiểu, cấu trúc dữ liệu động
o Lưu trữ dữ liệu trong vùng nhớ heap
Khai báo biến con trỏ
▪ Khai báo con trỏ trong C/C++: Kiểu con trỏ phải được định nghĩa trên một kiểu cơ
sở đã được định nghĩa trước đó (theo kiểu dữ liệu của biến mà con trỏ trỏ tới)
<kiểu_cơ_sở> *<Tên_con_trỏ>;
▪ Ví dụ:
int x; // x là kiểu int
int *px; // px là con trỏ đến vùng nhớ kiểu int
int *p1, *p2; // // p1 và p2 là con trỏ đến vùng nhớ kiểu int
float *pf; // pf là con trỏ đến vùng nhớ kiểu float
double *pd; // pd là con trỏ đến vùng nhớ kiểu double
char c, d, *pc; // c và d kiểu char ; pc là con trỏ đến vùng nhớ kiểu char
Lưu ý: Dấu sao “*” trong con trỏ khơng phải là tốn tử trỏ đến (dereference
operator) , nó là cú pháp được ngơn ngữ C/C++ qui định
Con trỏ (Pointer)
▪
Lưu ý: Kiểu dữ liệu của con trỏ không mô tả giá trị địa chỉ được lưu trữ bên trong
con trỏ, mà kiểu dữ liệu của con trỏ dùng để xác định kiểu dữ liệu của biến mà nó
trỏ đến trên bộ nhớ ảo.
Con trỏ và toán tử lấy địa chỉ
❖ Con trỏ và toán tử lấy địa chỉ & (address-of operator)
▪
“&”: toán tử lấy địa chỉ của 1 biến
▪
Địa chỉ của tất cả các biến trong chương trình đều đã được chỉ định từ khi khai báo
▪
Gán giá trị cho con trỏ: Giá trị mà biến con trỏ lưu trữ là địa chỉ của biến khác có
cùng kiểu dữ liệu với biến con trỏ.
Do đó, chúng ta cần sử dụng address-of operator để lấy ra địa chỉ ảo của biến rồi mới
gán cho con trỏ được. Lúc này, biến ptr sẽ lưu trữ địa chỉ ảo của biến value.
Con trỏ và tốn tử lấy địa chỉ
▪
Ví dụ:
int a = 5;
int *p; // khai báo biến con trỏ p
p = &a; // gán địa chỉ của biến a cho p ( hay p trỏ tới a)
int *q = &a ; // khởi tạo giá trị cho con trỏ q trỏ tới vùng nhớ của a
Con trỏ (Pointer)
▪ Ví dụ:
Con trỏ (Pointer)
▪ Ví dụ:
Lý do mà chúng ta gán được địa chỉ của biến value cho con trỏ
kiểu int (int *) là vì address-of operator của một biến
kiểu int trả về giá trị kiểu con trỏ kiểu int (int *).
Con trỏ và toán tử trỏ đến
❖ Con trỏ và toán tử trỏ đến *
(dereference operator)
▪ “*”: toán tử truy xuất giá trị của
vùng nhớ được quản lý bởi con trỏ.
#include <stdio.h>
#include <iostream>
using namespace std;
char g = 'z';
int main()
{
char c = 'a';
char *p;
p = &c;
printf("%c\n", *p);
// hoac dung cout
cout<< *p <
p = &g;
cout<< *p <
return 0;
}
Ví dụ:
int a = 5; // giả sử biến a được lưu trữ tại địa chỉ 0xB
int *p; // giả sử biến con trỏ p được lưu trữ tại địa chỉ 0xF
p = &a;
Câu hỏi: kết quả của các đoạn code sau là gì?
cout << a;
cout<< &a;
cout<< p;
cout<<*p;
cout<<&p;
Con trỏ và toán tử gán
❖ Con trỏ và toán tử gán “=”
▪
Khi có hai con trỏ cùng kiểu thì chúng ta có thể gán trực tiếp mà khơng cần sử
dụng toán tử lấy địa chỉ address-of operator.
Con trỏ và tốn tử gán
▪ Ví dụ : Có sự khác biệt rất quan trọng khi thực hiện các phép gán:
int i = 10, j = 14;
int* p = &i;
int *q = &j;
*p = *q;
int i = 10, j = 14;
int *p = &i;
int *q = &j;
p = q;
Lưu ý: Khác với tham chiếu (reference), một con
trỏ có thể trỏ đến địa chỉ khác trong bộ nhớ ảo
sau khi đã được gán giá trị.
Tham chiếu (reference) không thể thay đổi địa chỉ
sau lần tham chiếu đầu tiên.
Địa chỉ của biến mảng
▪ Ví dụ :
Lưu ý
❖ Khởi tạo kiểu con trỏ:
▪ Khi mới khai báo, biến con trỏ được đặt ở địa chỉ nào đó (khơng biết trước).
➔ chứa giá trị khơng xác định
➔ trỏ đến vùng nhớ không biết trước.
▪
Con trỏ trong ngôn ngữ C/C++ vốn khơng an tồn. Nếu sử dụng con trỏ khơng
hợp lý có thể gây lỗi chương trình.
Lưu ý
❖ Con trỏ chưa được gán địa chỉ
▪
Biến con trỏ có thể khơng cần khởi tạo giá trị ngay khi khai báo. Nhưng thực hiện
truy xuất giá trị của con trỏ bằng dereference operator khi chưa gán địa chỉ cụ thể
cho con trỏ, chương trình có thể bị đóng bởi hệ điều hành
Con trỏ NULL
▪
Do đó, khi khai báo con trỏ nhưng chưa có địa chỉ khởi tạo cụ thể, chúng ta nên gán
cho con trỏ giá trị NULL (giá trị NULL trong thư viên <stdlib.h>).
▪
Giá trị đặc biệt NULL để chỉ rằng con trỏ không quản lý vùng nào. Giá trị này
thường được dùng để chỉ một con trỏ không hợp lệ.
#include <stdlib.h>
#include <iostream>
using namespace std;
int main()
{
int *p = NULL;
if (p == NULL)
cout <<"con tro khong hop le" << endl;
else
cout << *p;
return 0;
}