1. TRƯỜNG ĐẠI HỌC CÔNG NGHỆ THÔNG TIN
LIÊN CHI ĐOÀN MMT&TT
HỖ TRỢ K7
Môn: Nhập môn lập trình
Con trỏ, các vấn đề liên quan con
trỏ
2. ĐỊA CHỈ CỦA BIẾN
• Khái niệm: Địa chỉ của biến là số thứ tự
của byte đầu tiên trong một dãy các byte
liên tiếp mà máy dành cho biến.
• Phân loại địa chỉ biến: địa chỉ kiểu int,
float, double, …
• Lấy địa chỉ của một biến: sử dụng toán
tử &.
3. Ví dụ:
int x = 5;
x được cấp phát vùng nhớ có kích thước 4
byte liên tiếp. Giả sử tại địa chỉ 1050, ta
có:
Địa chỉ của x: &x = 1050, giá trị của x = 5.
5
……
……
1050 x
1053
4. KHÁI NIỆM BIẾN CON TRỎ
Con trỏ là kiểu dữ liệu mà một thành phần kiểu
này có thể lưu trữ địa chỉ của một thành phần nào
đó (có thể là biến, hằng, hàm), hoặc ta nói nó trỏ
tới thành phần đó.
PHÂN LOẠI CON TRỎ
Một con trỏ lưu trữ địa chỉ của một thành kiểu T
thì ta nói p là con trỏ kiểu T, đặc biệt nếu T là một
kiểu con trỏ, hay nói cách khác, p lưu trữ địa chỉ
của một con trỏ khác thì ta nói p là con trỏ trỏ tới
con trỏ.
5. Cú pháp khai báo con trỏ:
<kiểu dữ liệu> * <tên_con_trỏ>;
Ví dụ:
int *p; // p là con trỏ kiểu int
float * q ; // q là con trỏ kiểu float
char *s ; // s là con trỏ kiểu char hay xâu kí tự
int ** r; // r là con trỏ tới con trỏ kiểu int
6. Ví dụ:
int *p, *q;
int x = 5;
// gán đ/c biến x cho con trỏ p
p = &x;
// lưu đ/c trong p vào con trỏ q
q = p;
Giả sử địa chỉ của x là 1010 thì p = 1010 và
q = 1010
5
1010
1010
……
……
1010 x
1014
1016
p
q
7. Khi giá trị nằm trong p là địa chỉ của a thì ta
nói p trỏ vào a.
Lúc này thì *p hoàn toàn tương đương với a ,
người ta coi *p là bí danh của a , thao tác với
*p cũng như thao tác với a, thao tác với a
cũng như thao tác với *p.
Ví dụ:
Câu lệnh x=2; hoàn toàn tương đương với câu lệnh *p=2
Câu lệnh x++; hoàn toàn tương đương với (*p)++ // chú ý
khác với *p++ nhé, phải cho *p vào trong đóng mở ngoặc vì
toán tử * có độ ưu tiên thấp hơn ++.
Lúc này câu lệnh scanf("%d",&a); ta có thể
thay bằng scanf("%d",p);
8. Để xuất một địa chỉ ô nhớ ra màn hình ta sử
dụng mã định dạng %p.
Ví dụ:
#include <stdio.h>
#include <conio.h>
void main()
{
int a;
int *p;
printf("n Dia chi bien a:%p",&a);
p = &a;
printf("n Dia chi con tro p chua la:
%p", p);
}
9. Các phép toán trên con trỏ
Phép gán:
• Tất cả các loại con trỏ đều có phép gán.
• Phép gán với con trỏ yêu cầu vế trái là 1 con trỏ và vế phải là 1
địa chỉ.
• Phép gán yêu cầu sự tương xứng về kiểu dữ liệu, nếu ko
tương xứng chúng ta phải ép kiểu.
Ví dụ:
p=(int*)8232;
p có kiểu dữ liệu là int*
còn 8232 là 1 hằng số nguyên, nên phải ép kiểu về int* rồi thực
hiện phép gán.
• Phép gán với 1 con trỏ kiểu void ko cần thiết phải tương
xứng hoản toàn về kiểu dữ liệu, void* có thể tương ứng
với tất cả thậm chí là vượt cấp (vượt hẳn 2 cấp).
Ví dụ:
void *p,**q;
p=&q;
10. Phép so sánh
• Phép so sánh ngang bằng dùng để kiểm tra 2 con trỏ
có trỏ vào cùng 1 vùng nhớ hay không, hoặc kiểm tra 1
con trỏ có phải là đang trỏ vào NULL hay không (trong
trường hợp cấp phát động, mở file, mở resource,........).
• Phép so sánh lớn hơn nhỏ hơn : > , < , >= , <= sử dụng
để kiểm tra về độ thấp cao giữa 2 địa chỉ . Con trỏ nào
nhỏ hơn thì trỏ vào địa chỉ thấp hơn.
Được quyền so sánh mọi con trỏ với 0, vì 0 chính là
NULL.
Ví dụ:
int a=197,*p=&a;
double *x;
p==&a;
p==0;
x==0;
Các phép toán trên con trỏ
11. Khi so sánh 2 con trỏ hoặc con trỏ với 1 địa chỉ xác định
(số nguyên) cần có sự tương xứng về kiểu dữ liệu:
Ví dụ:
int a=197,*p=&a;
double b=0,*x=&b;
// so sánh 2 con trỏ
(int)p==(int)x;
p==(int *)x;
(double*)p==x;
(void*)p==(void*)x;
p==(void*)x;
(float*)p==(float*)x;
//so sánh con trỏ với số nguyên
p==(int*)9999;
int(p)==9999;
Con trỏ void có thể đem ra so sánh với tất cả các con trỏ
khác.
Các phép toán trên con trỏ
12. Phép cộng trừ và phép tăng giảm : +, +=, -, -=, ++, --
• Bản chất của việc tăng/ giảm con trỏ p đi 1 đơn vị là
cho p trỏ đến ô nhớ bên cạnh phía dưới/trên.
• Chú ý:
+ Khi tăng giảm con trỏ p đi 1 đơn vị không có nghĩa là
trỏ sang byte bên cạnh.
+ Việc tăng giảm con trỏ đi 1 đơn vị phụ thuộc vào kiểu
dữ liệu và nó trỏ đến, quy tắc là :
p+1 >>> giá trị chứa trong p + sizeof(kiểu dữ liệu của
biến mà p trỏ đến)
+ Không có phép tăng giảm trên con trỏ void.
+ Không có phép cộng 2 con trỏ với nhau.
+ Phép trừ 2 con trỏ trả về độ lệch pha giữa 2 con trỏ.
Các phép toán trên con trỏ
13. Con trỏ với mảng
Khi ta khai báo mảng thì tương đương với : xin cấp
phát 1 vùng nhớ có kích thước như bạn khai báo và
khai báo ra 1 hằng con trỏ trỏ vào đầu vùng nhớ đó
Ví dụ:
int a[100]={0,1,2,3};
0
1
2
3
0
1000
1004
1008
1012
1016
a[0]
a[1]
a[0]
a[2]
a[3]
a[4]
Chú ý : trình biên dịch luôn hiểu a[i] là *(a+i).
• a là 1 hằng con trỏ trỏ vào phần tử thứ 0 của mảng.
• các phép toán nhằm làm a trỏ tới vùng khác (thay đổi giá trị
của a) là không thể (++, --, = ).
• a tương đương với &a[0].
• a+i tương đương với &a[i].
• *a tương đương với a[0].
• *(a+i) tương đương với a[i].
14. Những con trỏ mà chỉ trỏ cố định vào 1
vùng nhớ , những con trỏ này không có khả
năng trỏ vào vùng nhớ khác, không thay đổi
được
Ví dụ:
char buf[] = "bonjour";
char * const p = buf;
p++; <<<<<<<<<<<<<<<<<<<<<<< báo
lỗi tại đây
p[4]++; <<<<<<<<<<<<<<<<<<<<<<<<< ko
vấn đề, hoàn toàn có thể thay đổi giá
trị vùng nhớ mà p trỏ đến
Hằng con trỏ là gì?
Con trỏ với mảng
16. Mảng 2 chiều
Cho khai báo 1 mảng 2 chiều:
int D[n][m]; // n, m là hằng nguyên
Tức là có n×m phần tử kiểu nguyên, như vậy D được cấp phát một
vùng nhớ liên tiếp, trong vùng đó có n vùng con cho n phần tử
(dòng), trong mỗi vùng con có m ô nhớ (mỗi ô là một phần tử). Hay
nói cách khác các phần tử của mảng được cấp phát liên tiếp, đầu tiên
là phần tử của hàng 0, sau đó là phần tử của hàng 1,...
1000
1004
...
1000 + m*i*4 + j*4
d[0][0]
d[0][1]
...
d[ i ][ j ]
d[n-1][m-1] 1000 + m*(n-1)*4 + (m-1)*4
Con trỏ với mảng
17. Cấp phát: để cấp phát bộ nhớ cho một biến con trỏ ta
có thể sử dụng các hàm: malloc, calloc, realloc.
Thu hồi: để thu hồi bộ nhớ đã cấp phát cho một biến con
trỏ ta dùng hàm free.
Chú ý là :
• malloc trả về 1 địa chỉ đến 1 vùng nhớ và coi vùng nhớ
này là void *, nên trong câu lệnh malloc luôn đi kèm với
việc ép kiểu:
contro = (ép kiểu) malloc(...)
• Cấp phát là luôn phải đi kèm với giải phóng, ở đâu cũng
thế, malloc là phải free. Code mà để thoát chương trình
rồi chưa giải phóng cho dù là có hệ thống có tự giải
phóng đi nữa vẫn bị coi là bad!!!!
Cấp phát và thu hồi bộ nhớ
18. Vậy còn new và
delete?
new và delete không phải là hàm mà là toán tử
nên không còn phải khai báo thư viện như
malloc (thư viện stdlib.h).
new không có khả năng realloc.
Ví dụ:
int *x=new(int);
.....
delete x;
19. Con trỏ với hàm
int ham(int *a)
{
*a=2;
a++;
}
void main()
{
int *a;
printf("Truoc : %x",a); //trước và sau khi gọi hàm
ham(a); //con trỏ a trỏ vào đâu
printf("Sau %x",a); // thì nó vẫn trỏ vào đó
getch();
}
Hàm trong C ko hề có tham biến, hàm trong C đều
hoạt động theo nguyên tắc sau :
Khi gọi hàm, 1 bản sao của tham số được tạo ra và
hàm sẽ làm việc với bản sao này.
20. void nhap(int *a,int n)
{
a=(int*)malloc(n * sizeof(int)); //sai lầm
for(int i=0;i<n;i++)
scanf("%d", &a[i]);
}
void main()
{
int *x;
int n=6;
nhap(x,n);
//xuat
free(x); // sản sinh ra lỗi run-time do x chưa trỏ
vào đâu mà đòi giải phóng
}
Con trỏ với hàm
21. Dùng con trỏ cấp cao hơn con trỏ hiện tại
Ví dụ:
void ham(int **a)
{
*a=(int*)malloc(100*sizeof(int));
//a[0]=(int*)malloc(100*sizeof(int));
// 2 cach nay nhu nay
}
void main()
{
int *a;
ham(&a);
free(a);
}
Làm thế nào để mà thay đổi giá
trị của 1 con trỏ qua 1 hàm?
Con trỏ với hàm
22. Con trỏ với cấu trúc
Để truy xuất đến một thành phần của biến con
trỏ cấu trúc ta sử dụng toán tử mũi tên (->)
Ví dụ:
typedef struct tagNode
{
int info;
struct tagNode* pNext;
}NODE;
NODE* tao_pt(int x)
{
NODE* p=new NODE;
if(p==NULL) {
printf("Khong du bo nho");
return p;
}
p->info=x;
p->pNext=NULL;
return p;
}