BÀI GIẢNG HỌC PHẦN
KỸ THUẬT LẬP TRÌNH
CHƯƠNG 6: CON TRỎ
Nội dung
2
6.1. Con trỏ và cú pháp khai báo 6.2. Các phép toán trên biến con trỏ 6.3. Con trỏ và hàm 6.4. Con trỏ và dữ liệu kiểu mảng, xâu ký tự, cấu trúc 6.5. Cấp phát bộ nhớ động
6.1. Con trỏ và cú pháp khai báo
3
• Khái niệm • Cú pháp khai báo • Con trỏ kiểu void
Khái niệm
• Kiểu con trỏ là kiểu dữ liệu dùng để chứa địa chỉ • Biến con trỏ (gọi tắt là con trỏ) dùng để chứa địa chỉ
• Con trỏ thường được dùng trong các trường hợp:
của một đối tượng (biến hoặc hàm)
- Trả về nhiều giá trị từ hàm (thông qua cơ chế
truyền tham số theo địa chỉ trong hàm) - Truyền mảng và xâu ký tự giữa các hàm - Tạo các cấu trúc dữ liệu phức tạp (danh sách liên
4
kết, cây nhị phân, …)
Cú pháp khai báo (1)
• Cú pháp:
kiểu_dữ_liệu *tên_con_trỏ;
int x,y,*px,*py;
Ví dụ:
x, y là các biến nguyên px, py là các con trỏ kiểu int (cấp phát các vùng nhớ có tên là px, py dùng để lưu địa chỉ của các đối tượng kiểu int)
*px là nội dung của px (giá trị của đối tượng mà px
*py là nội dung của py (giá trị của đối tượng mà py
lưu địa chỉ)
5
lưu địa chỉ)
Cú pháp khai báo (2)
ta nói: px trỏ tới x và py trỏ tới y *px tương đương với x, *py tương đương với y
6
Khi sử dụng các lệnh: px = &x; //gán địa chỉ của biến x cho con trỏ px py = &y; //gán địa chỉ của biến y cho con trỏ py
Ví dụ (1)
int x = 4,y = 5,*px,*py;
• Khai báo:
Biến Biến
Nội dung Nội dung
x px
4
y py
5
7
Địa chỉ vùng nhớ 1201 1202 1203 1204 1205 1206 1207 Địa chỉ vùng nhớ 2010 2011 2012 2013 2014 2015 2016
Ví dụ (2)
px = &x; py = &y;
• Thực hiện các lệnh gán:
Biến Biến
Nội dung Nội dung
x px
4 1201
y py
5 1205
8
Địa chỉ vùng nhớ 1201 1202 1203 1204 1205 1206 1207 Địa chỉ vùng nhớ 2010 2011 2012 2013 2014 2015 2016
Ví dụ (3)
• Thực hiện các lệnh gán: *px += 10; *py += 10;
Biến Biến
Nội dung Nội dung
px x
14 1201
py y
15 1205
9
Địa chỉ vùng nhớ 1201 1202 1203 1204 1205 1206 1207 Địa chỉ vùng nhớ 2010 2011 2012 2013 2014 2015 2016
Con trỏ kiểu void
• Là dạng con trỏ đặc biệt (con trỏ không kiểu), có
thể nhận bất kỳ địa chỉ kiểu nào
void *tên_con_trỏ;
• Cú pháp khai báo:
Ví dụ: void *p;
• Con trỏ void thường được dùng làm tham số để nhận bất kỳ địa chỉ kiểu nào từ tham số thực. Khi đó, trong thân hàm phải sử dụng phép ép kiểu để chuyển sang dạng địa chỉ cần xử lý
10
float a[20][30]; p=a;
6.2. Các phép toán trên biến con trỏ (1)
Có 4 phép toán cơ bản: Phép gán, phép tăng/giảm địa chỉ, phép truy nhập bộ nhớ, phép so sánh
• Phép gán giá trị: - Các con trỏ phải cùng kiểu, muốn gán các con trỏ
khác kiểu nên dùng phép ép kiểu Ví dụ:
11
int x; char *p; p = (char*)(&x);
6.2. Các phép toán trên biến con trỏ (2)
• Phép tăng/giảm địa chỉ: - Ví dụ 1:
float x[30],*p; p = &x[10];//p trỏ tới x[10]
Giá trị kiểu float lưu trong 4 byte các phép tăng/giảm địa chỉ được thực hiện trên 4 byte
p+i trỏ tới x[10+i], p-i trỏ tới x[10-i] - Ví dụ 2:
float y[20][30];
y trỏ tới đầu dòng thứ nhất y[0][0] y+1 trỏ tới đầu dòng thứ hai y[1][0]
y là một mảng gồm các dòng có 30 phần tử thực Kiểu địa chỉ của y là 30*4 = 120 byte
12
…
6.2. Các phép toán trên biến con trỏ (3)
- Ví dụ:
• Nguyên tắc truy nhập bộ nhớ: - Con trỏ float truy nhập tới 4 byte, con trỏ int truy nhập tới 2 byte, con trỏ char truy nhập tới 1 byte
Giả sử pf trỏ tới byte 10001 thì *pf biểu thị vùng nhớ 4 byte từ 10001 đến 10004 Giả sử pi trỏ tới byte 10001 thì *pi biểu thị vùng nhớ 2 byte từ 10001 đến 10002 Giả sử pc trỏ tới byte 10001 thì *pc biểu thị vùng nhớ 1 byte 10001
13
float *pf; int *pi; char *pc;
6.2. Các phép toán trên biến con trỏ (4)
float *p1,*p2;
• Phép so sánh: - Áp dụng với các con trỏ cùng kiểu - Ví dụ:
Khi đó:
+ p1
14
• Lưu ý: Các phép tăng/giảm địa chỉ, truy nhập bộ nhớ và phép so sánh không dùng trên con trỏ void
6.3. Con trỏ và hàm (1)
• Thông thường, hàm được dùng để trả về một kết quả thông qua tên hàm. Khi cần trả về nhiều kết quả, cần sử dụng các tham số đầu ra dạng con trỏ • Ví dụ: Hàm trả về nghiệm của phương trình bậc hai ax2 + bx + c = 0
• Tham số của hàm có thể được chia làm 2 loại: Tham số đầu vào (chứa các giá trị đã biết) và tham số đầu ra (chứa các kết quả mới nhận được)
15
Tham số đầu vào: a, b, c Tham số đầu ra: x1, x2 Lưu ý: Khi sử dụng các tham số đầu ra là các con trỏ, các tham số thực sự tương ứng trong lời gọi hàm phải là các địa chỉ
6.3. Con trỏ và hàm (2)
• Chương trình giải phương trình bậc 2:
#include
{
16
float a,b,c,x1,x2; int kt; printf("Nhap vao cac he so:\n"); printf("a = ");scanf("%f",&a); printf("b = ");scanf("%f",&b); printf("c = ");scanf("%f",&c);
6.3. Con trỏ và hàm (3)
• Chương trình giải phương trình bậc 2: (tiếp)
if (kt == -1)
kt = ptb2(a,b,c,&x1,&x2);
printf("Delta < 0, phuong trinh vo nghiem!");
else if (kt == 0)
printf("Delta = 0, phuong trinh co nghiem kep: x1 = x2 = %6.2f",x1);
else
printf("Delta > 0, phuong trinh co 2 nghiem: x1 = %6.2f, x2 = %6.2f",x1,x2);
return 0;
17
}
6.3. Con trỏ và hàm (4)
• Chương trình giải phương trình bậc 2: (tiếp)
int ptb2(float a, float b, float c, float *x1,float *x2)
{
float delta; delta = b*b-4*a*c; if (delta < 0)
return -1;
{
else if (delta == 0)
}
18
*x1 = -b/(2*a); return 0;
6.3. Con trỏ và hàm (5)
• Chương trình giải phương trình bậc 2: (tiếp)
else
{
}
*x1 = (-b+sqrt(delta))/(2*a); *x2 = (-b-sqrt(delta))/(2*a); return 1;
19
}
6.4. Con trỏ và dữ liệu kiểu mảng, xâu ký tự, cấu trúc
20
• Con trỏ và mảng một chiều • Con trỏ và mảng nhiều chiều • Con trỏ và xâu ký tự • Con trỏ và cấu trúc • Mảng con trỏ
Con trỏ và mảng một chiều (1)
• Nhắc lại: Phép lấy địa chỉ áp dụng được với mảng
một chiều • Xét khai báo:
float a[10];
Khi đó:
- Tên mảng biểu thị địa chỉ đầu của mảng, tức là: a tương đương với &a[0]
- Mảng a được lưu trữ trong 10 khoảng nhớ liên tiếp
a+i tương đương với &a[i] và *(a+i) tương đương với a[i]
21
(mỗi khoảng 4 byte) nên:
Con trỏ và mảng một chiều (2)
• Nếu con trỏ pk trỏ tới phần tử a[k] thì: - pk+i trỏ tới phần tử thứ i sau a[k] tức là trỏ tới a[k+i] - pk-i trỏ tới phần tử thứ i trước a[k] tức là trỏ tới a[k-i] - *(pk+i) tương đương với pk[i]
• Xét khai báo:
float a[10],*p; Nếu thực hiện lệnh gán:
p = a;
22
4 cách viết sau là tương đương với nhau: *(a+i) *(p+i) p[i] a[i]
Con trỏ và mảng một chiều (3)
• Chương trình tính tổng, trung bình cộng cho dãy số
a1, a2 , …, an được viết theo 4 cách:
Cách 1: (không sử dụng biến con trỏ)
#include
float a[50],s,tb; int i,n; printf("Nhap so phan tu n = ");scanf("%d",&n);
23
{
Con trỏ và mảng một chiều (4)
Cách 1: (tiếp)
for(i=0;i } for(s=0,i=0;i %6.2f",s,tb); tb=s/n;
printf("Tong = %6.2f. Trung binh cong = return 0; 24 } Cách 2:
- Thay lệnh nhập dữ liệu cho phần tử mảng trong chương trình ở Cách 1:
Từ scanf("%f",&a[i]); thành scanf("%f",a+i); - Thay lệnh tính tổng s: 25 Từ s+=a[i]; thành s+=*(a+i); Cách 3: (có sử dụng biến con trỏ) #include { 26 float a[50],*p,s,tb;
int i,n;
p=a;
printf("Nhap so phan tu n = ");scanf("%d",&n); Cách 3 (tiếp): for(i=0;i } for(s=0,i=0;i %6.2f",s,tb); tb=s/n;
printf("Tong = %6.2f. Trung binh cong = return 0; 27 } Cách 4: (có sử dụng biến con trỏ)
- Thay lệnh nhập dữ liệu cho phần tử mảng trong chương trình ở Cách 3:
Từ scanf("%f",&p[i]); thành scanf("%f",p+i); - Thay lệnh tính tổng s: 28 Từ s+=p[i]; thành s+=*(p+i); • Nếu lời gọi hàm có tham số thực sự là tên mảng một chiều kiểu int (float, double, …) Khi xây dựng hàm, tham số hình thức phải được
khai báo là một con trỏ có kiểu dữ liệu tương ứng
int (float, double, …), ví dụ: int *p; float *p; double *p; … hoặc có thể khai báo như một mảng hình thức: int p[]; float p[]; double p[]; …
• Ví dụ: Xây dựng chương trình tính tổng các phần tử
của dãy số kiểu float, sử dụng hàm tongmang với:
- Tham số đầu vào: mảng a, số phần tử của mảng n
- Kết quả trả về qua tên hàm: Tổng các phần tử của 29 mảng • Chương trình: #include { float a[50];
int i,n;
printf("Nhap so phan tu n = ");scanf("%d",&n);
for(i=0;i printf("a[%d] = ",i);
scanf("%f",a+i); 30 } • Chương trình: (tiếp) printf("Tong = %6.2f",tongmang(a,n));
return 0; } float tongmang(float *p,int n) { return s; 31 } • Việc xử lý mảng nhiều chiều phức tạp hơn so với • Nhắc lại: Phép lấy địa chỉ không áp dụng được với
các phần tử của mảng nhiều chiều. Trong nhiều
trường hợp câu lệnh &a[i][j] không hợp lệ và gây
lỗi (mảng 2 chiều nguyên có thể dùng &a[i][j]) mảng một chiều • Xét mảng 2 chiều: (mảng một chiều của mảng) a là mảng một chiều gồm 2 phần tử, mỗi phần tử
của nó là một dãy gồm 3 số thực (tương ứng với
một hàng) float a[2][3]; 32 a trỏ tới đầu hàng thứ nhất - phần tử a[0][0]
a+1 trỏ tới đầu hàng thứ hai - phần tử a[1][0], … • Sử dụng con trỏ để duyệt mảng 2 chiều: float a[2][3],*p;
p = (float*)a; Xét ví dụ: Khi đó: Chương trình nhập dữ
liệu cho mảng:
#include p trỏ tới a[0][0]
p+1 trỏ tới a[0][1]
p+2 trỏ tới a[0][2]
p+3 trỏ tới a[1][0]
p+4 trỏ tới a[1][1]
p+5 trỏ tới a[1][2] scanf("%f",p+i); return 0; 33 } • Có thể thay thế việc sử dụng con trỏ p bằng cách sử
dụng phép ép kiểu địa chỉ ngay trong hàm scanf #include float a[2][3];
int i;
for(i=0;i<6;i++) scanf("%f",(float*)a+i); } 34 return 0; m*n không sử dụng con trỏ p: • Chương trình nhập dữ liệu cho mảng số thực cấp #include 35 float a[20][30];
int i,j,m,n;
printf("Nhap so dong m = ");scanf("%d",&m);
printf("Nhap so cot n = ");scanf("%d",&n); m*n không sử dụng con trỏ p (tiếp) • Chương trình nhập dữ liệu cho mảng số thực cấp for(i=0;i for(j=0;j printf("a[%d][%d] = ",i,j);
scanf("%f",(float*)a+i*30+j); return 0; } 36 } liệu cho mảng nhiều chiều • Lưu ý: Có thể sử dụng biến trung gian để nhập dữ Chương trình:
#include 37 float a[20][30],x;
int i,j,m,n;
printf("Nhap so dong m = ");scanf("%d",&m);
printf("Nhap so cot n = ");scanf("%d",&n); Chương trình (tiếp) for(i=0;i for(j=0;j return 0; } 38 } • Lời gọi hàm có tham số thực sự là tên mảng nhiều chiều:
Giả sử a là mảng 2 chiều: float a[20][30];
Có 2 cách để sử dụng tên mảng 2 chiều a trong lời
gọi hàm: - Cách 1: Dùng tham số con trỏ kiểu float[30] được khai báo theo một trong 2 cách: Hoặc float (*p)[30]; //dùng để khai báo con trỏ
float p[][30]; //dùng để khai báo tham số dùng cú pháp p[i][j] 39 Trong thân hàm, để truy nhập đến phần tử a[i][j] ta - Cách 2: Dùng 2 tham số Trong thân hàm, để truy nhập đến phần tử a[i][j] ta float*p; //biểu thị địa chỉ đầu của mảng a
int N; //biểu thị số cột của mảng a 40 dùng cú pháp *(p+i*N+j) • Xét khai báo: char *p; • Tương tự như tên mảng, tên xâu ký tự là một hằng
địa chỉ biểu thị địa chỉ của byte nhớ đầu tiên trong
vùng nhớ lưu xâu 41 Phép gán p = "Tran Ngoc Anh" là thực hiện được,
lúc này p sẽ có nội dung là địa chỉ đầu của vùng nhớ
lưu xâu ký tự "Tran Ngoc Anh". Khi đó có thể thực
hiện các lệnh puts(p) và gets(p) bình thường • Xét đoạn lệnh: char *p, s[15];
s = "Tran Ngoc Anh"; // *
gets(p); //** 42 Lệnh * không thực hiện được vì s là một hằng địa
chỉ, không thể gán một hằng địa chỉ này cho một
hằng địa chỉ khác
Lệnh ** không sai về mặt cú pháp song không nên
sử dụng vì nội dung của con trỏ p chưa xác định
(chưa trỏ tới vùng nhớ nào) • Xét chương trình sau:
#include char *p,s[30];
p=s;
puts("Nhap ho ten cua ban: ");
gets(p);
puts("Xin chao");
puts(p);
return 0; { } 43 Kết quả struct ngaythang ngayden,*p, a[10];
p là con trỏ cấu trúc lưu địa chỉ của biến cấu trúc
Các phép gán sau là hợp lệ:
p = &ngayden;
p = &a[0];
p = a; 44 • Xét khai báo: trỏ: • Truy nhập thành phần của cấu trúc thông qua con - Cách 1: tên_con_trỏ ->tên_thành_phần
- Cách 2: (*tên_con_trỏ).tên_thành_phần p->ngay
(*p).ngay 45 Ví dụ: • Các phép gán thông qua con trỏ, phép cộng địa chỉ
đối với con trỏ cấu trúc được áp dụng tương tự như
các con trỏ khác đương: • Khi con trỏ p trỏ tới đầu mảng cấu trúc a thì:
- Các cú pháp truy nhập thành phần sau là tương - Các cách viết sau là tương đương: a[i].thành_phần
p[i].thành_phần
(p+i)->thành phần 46 a[i] p[i] *(p+i) • Tương tự như các kiểu dữ liệu khác, kiểu cấu trúc
có thể sử dụng để khai báo cho các tham số trong
hàm:
- Tham số hình thức là biến cấu trúc tham số - Tham số hình thức là con trỏ cấu trúc tham số thực sự là giá trị cấu trúc thực sự là địa chỉ của biến cấu trúc • Hàm cũng có thể trả về giá trị dạng: - Tham số hình thức là mảng cấu trúc hoặc con trỏ
cấu trúc tham số thực sự là tên mảng cấu trúc 47 - Giá trị cấu trúc
- Con trỏ cấu trúc • Mỗi phần tử của mảng có thể chứa được một địa chỉ
• Cú pháp khai báo: Ví dụ: float *p[50]; kiểu_dữ _liệu *tên_mảng[kích_thước]; • Lưu ý:
- Mảng con trỏ dùng để lưu địa chỉ, không dùng để - Trước khi sử dụng mảng con trỏ cần gán cho mỗi
phần tử của nó một giá trị (địa chỉ của biến hoặc của
một phần tử mảng). Các phần tử của mảng kiểu char
có thể được khởi tạo giá trị bằng các xâu ký tự 48 lưu dữ liệu vùng nhớ cho chúng • Trước khi sử dụng các biến con trỏ, nên cấp phát • Để sử dụng các hàm quản lý việc cấp phát vùng
nhớ, cần khai báo tệp tiêu đề stdlib.h (hoặc alloc.h) • Các hàm cấp phát vùng nhớ:
- Hàm cấp phát vùng nhớ cho n đối tượng: calloc(n,m);//m=sizeof(đối_tượng) 49 Cấp phát vùng nhớ có kích thước n*m byte. Nếu
thành công, hàm trả về con trỏ lưu địa chỉ đầu vùng
nhớ được cấp và khởi tạo cho mọi đối tượng giá trị
0; nếu không đủ bộ nhớ để cấp, hàm trả về NULL • Các hàm cấp phát vùng nhớ: (tiếp)
- Hàm cấp phát vùng nhớ n byte: malloc(n); 50 Cấp phát vùng nhớ có kích thước n byte. Nếu thành
công, hàm trả về con trỏ lưu địa chỉ đầu vùng nhớ
được cấp, nếu không đủ bộ nhớ để cấp, hàm trả về
NULL • Lưu ý: Khi sử dụng các hàm calloc() và malloc()
phải ép kiểu vì nguyên mẫu các hàm này trả về con
trỏ kiểu void • Ví dụ: int *pa,*pb;
pa = (int*)calloc(10,sizeof(int)); //Cấp phát 51 vùng nhớ lưu được 10 số nguyên kiểu int
pb = (int*)malloc(sizeof(int)); //Cấp phát vùng
nhớ lưu được 1 số nguyên kiểu int • Hàm cấp phát lại vùng nhớ: realloc(p,n); trong đó:
- p: là con trỏ trỏ đến một vùng nhớ đã được cấp phát từ trước - n: số byte cấp phát lại 52 Khi thành công, hàm trả về địa chỉ đầu tiên của
vùng nhớ mới gồm n byte được cấp lại cho p (có thể
khác với địa chỉ của lần cấp phát trước đó, khi đó
nội dung của vùng nhớ cũ sẽ được chuyển tới vùng
nhớ mới và có thể tiếp tục sử dụng), ngược lại hàm
trả về NULL
Ví dụ: realloc(pb,6); free(p); • Hàm giải phóng vùng nhớ đã cấp phát: trong đó: p là con trỏ trỏ đến một vùng nhớ đã được
cấp phát bởi calloc() hoặc malloc()
Hàm giải phóng vùng nhớ do p quản lý
Lúc này nên gán lại giá trị cho con trỏ p = NULL Ví dụ: 53 free(pa); pa=NULL;
free(pb); pb=NULL; max trong dãy số a1, a2, …, an:
#include { • Ví dụ về mảng động: Chương trình tìm giá trị min, 54 int *a;
int n,i,max,min;
printf("Nhap so phan tu n = ");
scanf("%d",&n);
a = (int*)calloc(n,sizeof(int)); { • Ví dụ về mảng động: (tiếp)
for(i=0;i printf("a[%d] = ",i);
scanf("%d",&a[i]); for(max=a[0],min=a[0],i=1;i } printf("max = %d, min = %d",max,min);
free(a);
a=NULL;
return 0; 55 } • Viết chương trình cho phép người dùng nhập vào từ
bàn phím một dãy số nguyên: a1, a2, …, an, sử dụng
mảng động để lưu trữ. Sau đó xây dựng hàm tìm
max, min cho mảng vừa nhập. Thông báo kết quả ra
màn hình 56 • Viết chương trình cho phép người dùng nhập vào từ
bàn phím một dãy số nguyên: a1, a2, …, an, sử dụng
mảng động để lưu trữ. Sau đó xây dựng hàm sắp
xếp lại mảng vừa nhập theo chiều tăng dần. In dãy
sau khi sắp xếp ra màn hìnhprintf("a[%d] = ",i);
scanf("%f",&a[i]);
Con trỏ và mảng một chiều (5)
Con trỏ và mảng một chiều (6)
Con trỏ và mảng một chiều (7)
printf("a[%d] = ",i);
scanf("%f",&p[i]);
Con trỏ và mảng một chiều (8)
Con trỏ và mảng một chiều (9)
Con trỏ và mảng một chiều (10)
Con trỏ và mảng một chiều (11)
float s;
int i;
for(s=0,i=0;i
Con trỏ và mảng nhiều chiều (1)
Con trỏ và mảng nhiều chiều (2)
float a[2][3],*p;
int i;
p = (float*)a;
for(i=0;i<6;i++)
Con trỏ và mảng nhiều chiều (3)
Con trỏ và mảng nhiều chiều (4)
Con trỏ và mảng nhiều chiều (5)
Con trỏ và mảng nhiều chiều (6)
Con trỏ và mảng nhiều chiều (7)
printf("a[%d][%d] = ",i,j);
scanf("%f",&x);
a[i][j]=x;
Con trỏ và mảng nhiều chiều (8)
Con trỏ và mảng nhiều chiều (9)
Con trỏ và xâu ký tự (1)
Con trỏ và xâu ký tự (2)
Con trỏ và xâu ký tự (3)
Con trỏ và cấu trúc (1)
Con trỏ và cấu trúc (2)
Con trỏ và cấu trúc (3)
Con trỏ và cấu trúc (4)
Mảng con trỏ
6.5. Cấp phát bộ nhớ động (1)
6.5. Cấp phát bộ nhớ động (2)
6.5. Cấp phát bộ nhớ động (3)
6.5. Cấp phát bộ nhớ động (4)
6.5. Cấp phát bộ nhớ động (5)
6.5. Cấp phát bộ nhớ động (6)
6.5. Cấp phát bộ nhớ động (7)
6.5. Cấp phát bộ nhớ động (8)