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 đó: + p1p2 nếu địa chỉ p1 trỏ tới là cao hơn địa chỉ p2 trỏ tới

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 #include int ptb2(float a, float b, float c, float *x1,float *x2); int main(void)

{

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 int main(void)

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

printf("a[%d] = ",i); scanf("%f",&a[i]);

}

for(s=0,i=0;i

%6.2f",s,tb);

tb=s/n; printf("Tong = %6.2f. Trung binh cong =

return 0;

24

}

Con trỏ và mảng một chiều (5)

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);

Con trỏ và mảng một chiều (6)

Cách 3: (có sử dụng biến con trỏ)

#include int main(void)

{

26

float a[50],*p,s,tb; int i,n; p=a; printf("Nhap so phan tu n = ");scanf("%d",&n);

Con trỏ và mảng một chiều (7)

Cách 3 (tiếp):

for(i=0;i

printf("a[%d] = ",i); scanf("%f",&p[i]);

}

for(s=0,i=0;i

%6.2f",s,tb);

tb=s/n; printf("Tong = %6.2f. Trung binh cong =

return 0;

27

}

Con trỏ và mảng một chiều (8)

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);

Con trỏ và mảng một chiều (9)

• 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

Con trỏ và mảng một chiều (10)

• Chương trình:

#include float tongmang(float *p,int n); int main(void)

{

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

}

Con trỏ và mảng một chiều (11)

• Chương trình: (tiếp)

printf("Tong = %6.2f",tongmang(a,n)); return 0;

}

float tongmang(float *p,int n)

{

float s; int i; for(s=0,i=0;i

return s;

31

}

Con trỏ và mảng nhiều chiều (1)

• 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], …

Con trỏ và mảng nhiều chiều (2)

• 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 int main(void) {

float a[2][3],*p; int i; p = (float*)a; for(i=0;i<6;i++)

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

}

Con trỏ và mảng nhiều chiều (3)

• 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 int main(void) {

float a[2][3]; int i; for(i=0;i<6;i++)

scanf("%f",(float*)a+i);

}

34

return 0;

Con trỏ và mảng nhiều chiều (4)

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 int main(void) {

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);

Con trỏ và mảng nhiều chiều (5)

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

}

Con trỏ và mảng nhiều chiều (6)

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 int main(void) {

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);

Con trỏ và mảng nhiều chiều (7)

Chương trình (tiếp)

for(i=0;i

for(j=0;j

printf("a[%d][%d] = ",i,j); scanf("%f",&x); a[i][j]=x;

return 0;

}

38

}

Con trỏ và mảng nhiều chiều (8)

• 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

Con trỏ và mảng nhiều chiều (9)

- 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)

Con trỏ và xâu ký tự (1)

• 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

Con trỏ và xâu ký tự (2)

• 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)

Con trỏ và xâu ký tự (3)

• Xét chương trình sau: #include int main(void)

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ả

Con trỏ và cấu trúc (1)

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:

Con trỏ và cấu trúc (2)

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ụ:

Con trỏ và cấu trúc (3)

• 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)

Con trỏ và cấu trúc (4)

• 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ảng con trỏ

• 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

6.5. Cấp phát bộ nhớ động (1)

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

6.5. Cấp phát bộ nhớ động (2)

• 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

6.5. Cấp phát bộ nhớ động (3)

• 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

6.5. Cấp phát bộ nhớ động (4)

• 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);

6.5. Cấp phát bộ nhớ động (5)

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;

6.5. Cấp phát bộ nhớ động (6)

max trong dãy số a1, a2, …, an: #include #include int main(void)

{

• 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));

6.5. Cấp phát bộ nhớ động (7)

{

• 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;imax) max=a[i]; else if (a[i]

}

printf("max = %d, min = %d",max,min); free(a); a=NULL; return 0;

55

}

6.5. Cấp phát bộ nhớ động (8)

• 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ình