3.2. Con trỏ
3.2.4. Các phép toán trên con trỏ
trừ với một số nguyên (int, long) để cho kết quả là một con trỉ cùng kiểu. Xét ví dụ sau: Ví dụ:
int *x =2; *px, *py;
px= &x;
py = px +1; /* py trỏ đến số nguyên nằm sau x trong bộ nhớ */
Việc cộng trừ một con trỏ với một số nguyên có một ý tưởng rõ rệt nếu ta so sánh con trỏ với địa chỉ các ngội nhà là một biến có địa chỉ náo đó, các địa chỉ này được đánh số tăng dần theo một chiều nhất định. Biết địa chỉ nhà này, ta có thể tính địa chỉ của nhà bên cạnh bằng cách cộng thêm 1 hoặc trừ đi 1 vào địa chỉ của căn nhà đã biết, và cứ thế tiếp tục. Điều đó có nghĩa khi cộng hoặc trừ con trỏ với một số nguyrn n,, ta
sẽ được một địa chỉ mới chỉ đến một biến khắc nằm cách biến trước (hoặc sau) đó n vị trí
- Do phép cộng một con trỏ với một số nguyên cho ta một con trỏ, nên phép trừ hai con trỏ được coi là hợp lệ và cho kết quả là một số nguyên biểu thị “khoảng cách” (ở đây bằng số phần tử) giữa hai biến con trỏ
- Phép cộng, nhân, chia hai con trỏ là không hợp lệ (không có ý nghĩa về mặt logic)
- Có thể áp dụng các phép so sánh, phép gán cho các con trỏ với đòi hỏi các toán hạng con trỏ phải có cùng kiểu. Mọi sự chuyển đổi kiểu tự động từ các kiểu khác thành các kiểu con trỏ phải được cân nhắc
- Có thể áp dụng phép toán chuyển đổi kiểu bắt buộc để chuyển đổi kiểu của một con trỏ. Ví dụ: int *addr1; char *addr2; addr1 = (int *) addr2;
3.2.5. Con trỏ kiểu void
Con trỏ kiểu void được khai báo như sau: void *tên_con_trỏ;
đây là con trỏ đặc biệt, con trỏ không kiểu, nó có thể nhận bất kỳ địa chỉ kiểu nào. Chẳng hạn các câu lệnh sau là hợp lệ:
void *px, *py; int x=1;
float y = 0.1; px = &x; py = &y;
- Các phép toán số học, so sánh không dùng được trên con trỏ void
- Con trỏ void được dùng làm đối để nhận bất kỳ kiểu địa chỉ nào từ tham số thực sự trong các lời gọi hàm. Một hàm được khai báo kiểu void không quan tâm đến giá trị trả về.
3.3. Liên hệ giữa con trỏ và mảng
Trong C, khái niệm con trỏ và mảng liên quan mật thiết với nhau. Tất cả các thao tác mà chúng ta thực hiện chỉ thông qua chỉ số trên mảng có thể thực hiện nhờ công cụ con trỏ. Tuy nhiên cùng một ý tưởng thuật toán, chươmg trình sử dụng các con trỏ nói chung nhanh hơn nhưng đồng thời khó hiểu hơn so với chương trình sử dụng các mảng.
3.3.1. Con trỏ và mảng một chiều
Xét câu lệnh, int a[10];
Khai báo này định nghĩa một mảng a kích thước 10, nghĩa là một khối gồm 10 đối tượng liên tiếp: a[0], a[1], ..., a[9]
kí pháp a[i] biểu thị thành phần thứ i+1 của mảng a. Nếu pa là một con trỏ kiểu nguyên
int *pa;
khi đó phép gán: pa = &a[0] (tương đương pa = a)
đem pa trỏ đến phần tử thứ nhất (có chỉ số là 0) của a; nghĩa là pa chứa địa chỉ a[0]
bây giờ, phép gán: x = *pa;
sao chép nội dungcủa a[0] vào biến nguyên x ... pa+i ...
3.3.2. Con trỏ và xâu kí tự
Xâu kí tự là một hằng địa chỉ biểu thị địa chỉ đầu của mảng kí tự chứa xâu. Vì vậy, nếu chúng ta khai báo
char *pc;
là một con trỏ kiểu char thì phép gán:
pc = “Dai hoc sư pham KT hung Yen”
là hợp lệ. Khi đó câu lệnh: puts(pc) sẽ in ra màn hình dòng chữ: Dai hoc sư pham KT hung Yen
Giữa con trỏ kiểu char và mảng kiểu char có sự khác biệt Xét ví dụ sau:
char ch[20]; char *pc;
pc = “Dai hoc sư pham KT hung Yen” /* hợp lệ*/
ch = “Dai hoc sư pham KT hung Yen” /* không hợp lệ vì mảng kí tự là một hằng*/
3.3.3. Con trỏ và mảng nhiều chiều
Việc xử lý mảng nhiều chiều phức tạp hơn so với mảng một chiều. Không phải mọi qui tắc đúng với mảng một chiều đều có thể đem ra áp dụng đối với mảng nhiều chiều
Phép toán lấy địa chỉ nói chung không dùng được đối với các thành phần của mảng nhiều chiều (trừ trường hợp mảng hai chiều số nguyên)
VD: float a[10][20]; ...
a là một hằng con trỏ trỏ đến các dòng của ma trận hai chiều, vì vậy: a trỏ đến dòng thứ nhất
a+ 1 trỏ đến dòng thứ 2 a+ 2 trỏ đến dòng thứ ba
...
để tính toán được địa chỉ của phần tử ở dòng i cột j, chúng ta dùng phép chuyển đổi kiểu bắt buộc đối với a: (float *)a đây là con trỏ trỏ đến thành phần a[0][0] của ma trận. nên thành ohần a[i][j] sẽ có địa chỉ là (float *)a + i*n +j.
Tổng quát, nếu mảng có kiểu type và có kích thước các chiều tương ứng là n1, n2, ..., nk. Địa chỉ thành phần a[0]..[0] là (type*)a và địa chỉ của a[i1][i2]..[ik] được tính: i n i k k j il l k Þ j a type +∑ ∏ + + = − = 1 1 1 *) (
Để vào số liệu cho mảng nhiều chiều có thể dùng biến trung gian: đọc một giá trị và chứa tạm vào biến trung gian, sau đó ta gán biến này cho phần tử mảng
CHƯƠNG 4. CẤU TRÚC 4.1. Cấu trúc
4.1.1. Định nghĩa cấu trúc
Cấu trúc là một kiểu dữ liệu bao gồm nhiều thành phần có thể thuộc nhiều kiểu dữ liệu khác nhau. Các thành phần được truy nhập thông qua tên. Khái niệm cấu trúc trong C có nhiều nét tương tự như khái niệm về bản ghi (record) trong Pascal hay Foxpro
4.1.2. Khai báo cấu trúc
- Cú pháp:
struct[ten_cau_truc] {
Khao báo các thành phần;
} [danh sách các biến cấu trúc];
- trong đó:
struct là từ khoá đứng trước khai báo cấu trúc; tên_cấu_trúc là một tên hợp lệ được dùng làm tên cấu trúc, tên này có thể có hoặc không; danh sách các biến cấu trúc: liệt kê các biến có kiểu cấu trúc vừa khai báo, các biến này sẽ sử dụng trong chương trình, danh sách này có thể có hoặc không, nhưng ít nhất một trong hai hoặc là
tên_cấu_trúc hoặc danh sách các biến cấu trúc phải có mặt trong khai báo Ví dụ:
struct hoc_sinh {
char ho_ten[30]; float diem;
} hs, dshs[100];
Câu lệnh này có thể tác thành hai câu lệnh như sau: struct học_sinh { char ho_ten[30]; float diem; }; struct hoc_sinh hs,dshs[100];
trong trường hợp kiểu hoc_sinh chỉ được sử dụng đúng một lần để khai báo các biến, chúng ta có thể không cần khai báo tên cho nó. Câu lệnh trên lại có thể được viết như sau: struct { char ho_ten[30]; float diem; }hs, dshs[100];
4.1.3. Định nghĩa kiểu bằng typedef
Ngôn ngữ C cho phép ta đặt lại tên kiểu cho riêng mình bằng câu lệnh như sau:
typedef kiểu_đã_có tên_kiểu_mới; trong đó: kiểu_đã_có là kiểu dữ liệu mà ta muốn đổi tên
tên_kiểu_mới là tên mới mà ta muốn đặt Xét câu lệnh sau:
typedef unsigned char byte;
sau câu lệnh này, byte được xem là kiểu dữ liệu tương đương với unsigned char và có thể sử dụng trong khai báo các biến khác
typedef thường được sử dụng để định nghĩa các kiểu dữ liệu phức hợp thành một tên duy nhất để dễ dàng hơn khi viết.
Ví dụ:
typedef struct hoc_sinh
{
char ho_ten[30];
float diem;
} t-học_sinh, *ptr_hoc_sinh;
với câu lệnh này tên mới của cấu trúc struct hoc_sinh sẽ là t_hoc_sinh và một kiểu con trỏ cấu trúc có tên là ptr_hoc_sinh. Khi đó câu lệnh khai báo biến được viết:
t-hoc_sinh hs,dshs[100];
Để khai báo một biến con trỏ cấu trúc ta có thể sử dụng câu lệnh sau: ptr_hoc_sinh ptrhs;
4.1.4. Truy nhập đến các thành phần của cấu trúc
Các thành phần của cấu trúc được truy nhập thông qua tên biến cấu trúc và tên thành phần. Nguyên tắc chung như sau:
tên_biến_cấu trúc. tên_thành_phần
chẳng hạn để truy nhập đến các thành phần của biến hs chúng ta viết như sau: hs.ho_ten
hs.diem
Ví dụ: viết chương trình nhập vào họ tên và điểm thi của các học sinh của một lớp và in ra danh sách các học sinh phải thi lại
#include<stdio.h> #include<conio.h> #include<string.h> main() { clrscr(); struct hoc_sinh { char ho_ten[30]; char lop[7]; float diem; }dshs[100]; int n,k,i,done=0; char hoten[30]; float d; n=0; do {
printf("Ho ten: "); fflush(stdin);gets(hoten); if (strcmp(hoten,"")!=0) { strcpy(dshs[n].ho_ten,hoten); printf("Lop: "); gets(dshs[n].lop); printf("diem: "); scanf("%f",&d); dshs[n].diem=d; n++;
}
} while (strcmp(hoten,"")!=0); k=0;
printf("Danh sach sinh vien thi lai la: \n"); for (i=0; i<n; i++)
if (dshs[i].diem<5)
printf("%3d%20s%6.3f\n",++k,dshs[i].ho_ten,dshs[i].diem); if (!k) printf("Khong co sinh vien thi la \n");
getch(); }
Chú ý:
- không nên sử dụng toán tử & đối với các thành phần cấu trúc (đặc biệt đối với các thành phần không nguyên) trong khi nập dữ liệu vì điều đó dẫn đến việc treo máy
- đối với các biến cấu trúc đơn ví dụ hs có thể sử dụng #define để rút gọn
“đường truy nhập” đến các cấu trúc, ví dụ:
#define ht hs.ho_ten
#define ds hd.diem
hoặc #define dh dshs[i].ho_ten
4.1.5. Thành phần kiểu FIELD (nhóm bit)
Ngôn ngữ C cho phép chúng ta khai thác từng bit như một thành phần riêng biệt của cấu trúc. Một thành phần như vậy được gọi là một field (tạm dịch là một vùng). Khai báo cho một cấu trúc có các thành phần là các field được thực hiện như một cấu trúc bình thường:
struct struct_field {
usigned field1: số_bit1;
int field2:số_bit2;
... }
Số bit không vượt quá 15, hay độ dài tính theo bit của field không được vượt quá 16 bít. Turbo C sẽ cấp phát cho flags n từ (16 bits) bằng tổng các with chi cho 16, nếu dư thì n+1 từ
Xét câu lệnh khai báo sau: struct date
{ unsigned int day:5; /*giá trị từ 1 đến 31*/
unsigned year :5; /*xét từ năm 1980 – 2011*/ int t: 2;
} x; Lưu ý:
- Không cho phép lấy địa chỉ của các thành phần kiểu field
- Không thể xây dựng các mảng có kiểu cấu trúc field
- Khi muốn bỏ qua một số bit nào đó ta bỏ trống tên trường. Các cấu trúc có thành phần kiểu field được sử dụng nhằm:
- Tiết kiện bộ nhớ
- Dùng trong các khai báo kiểu hợp (union) để lấy ra các bits của một từ nào đó
4.2. Kiểu hợp (union)
Một biến kiểu union cũ`ng bao gồm nhiều thành phần giống như một biến cấu trúc, nhưng khác biến cấu trúc ở chỗ: các thành phần của biến cấu trúc được cấp phát ở các vùng nhớ khác nhau, còn các thành phần của union được cấp phát chung một vùng nhớ. Độ dài của vùng nhớ đủ để chứa được thành phần dài nhất của union. Khai báo một union được thực hiện nhờ từ khoá union. Việc định nghĩa một biến kiểu union, mảng các union, con trỏ union, cũng như cách thức truy nhập đến các thành phần của biến union được thực hiện hoàn toàn tương tự như đối với các cấu trúc. Một cấu trúc có thể có thành phần union và ngược lại các thành phần của union lại có thể là cấu trúc.
Ví dụ khai báo union: 1) typedef union
{
unsigned int ival; float fval;
unsigned char ch[2]; } val;
val a, b, x[10];
độ dài của kiểu union val bằng độ dài của trường dài nhất là trường fval và bằng 4 2) struct ng {
int ngay; int thang, nam; }
struct dc {
int sonha; char *tenpho;
}
union u {
struct ng date ; struct dc address; } diachi;
CHƯƠNG 5. HÀM VÀ CẤU TRÚC CHƯƠNG TRÌNH 5.1. Mở đầu:
Một chương trình viết theo ngôn ngữ C là một dãy các hàm trong đó có một hàm chính (hàm main). Các hàm trong chương trình không cần tuân theo thứ tự nhưng một chương trình bao giờ cũng được thực hiện từ hàm main(). Mỗi hàm sẽ thực hiện một phần việc nào đó trong chương trình. Một trong các ưu điểm của C là nó cho phép tổ chức và sử dụng các hàm một cách đơn giản và hiệu quả.
5. 2. Ví dụ đơn giản về chương trình có hàm:
Bài toán: Tìm giá trị lớn nhất của ba số được nhập từ bàn phím.
Phân tích:
Ta tổ chức chương trình thành 2 hàm: hàm main và một hàm max3s để tính giá trị lớn nhất của 3 số giả sử là a,b,c
Chương trình:
#include <stdio.h>#include<conio.h>float max3s(float a,float b,float c); /*Nguyên mẫu của hàm*/
main()
{ float x,y,z; clrscr();
printf("nhap 3 so: "); scanf("%f%f%f",&x,&y,&z);
printf("max (%0.2f,%0.2f,%0.2f) = %0.2f",x,y,z,max3s(x,y,z)); getch();
}
float max3s(float a,float b,float c) {
float max; /* Biến cục bộ dùng trong thân hàm*/ max=a>b?a:b;
return(max>c?max:c); /*giá trị hàm trả về */ }
Chú ý:- Không nhất thiết phải khai báo nguyên mẫu (prototype0 của hàm. Nhưng nên có, vì nó cho phép chương trình biên dịch phát hiện lỗi khi gọi hàm (số đối không đúng) hay tự động việc chuyển dạng
- Trong khai báo nguyên mẫu của hàm có thể bỏ tên các đối 5. 3. Quy tắc xây dựng một hàm
* Khai báo:
Kiểu giá trị của hàm tên_hàm (các tham số hình thức và kiểu của chúng) Lưu ý:
- Cuối dòng không có dấu ;
- Các tham số cách nhau bởi dấu phẩy (,)
- Đối với các hàm không có giá trị (như thủ tục của Pascal) thì dùng kiểu void như sau: void tên_hàm(các tham số hình thức và kiểu của chúng)
- Hàm không có đối, thì ta cũng dùng void để khai báo đối như sau: void tên_hàm (void)
Hoặc: Kiểu giá trị của hàm tên_hàm (void)
* Thân hàm:
Sau dòng tiêu đề là thâm hàm. Thân hàm là nội dung chính của hàm bắt đầu bằng dấu { và kết thúc bởi dấu }. Trong thân hàm chứa các câu lệnh cần thiết để thực hiện một yêu cầu nào đó. Trong thân hàm có thể sử dụng thêm các biến cục bộ, các biến này chỉ có tác dụng trong th ân hàm và không có mối liên hệ gì đến các biến của các hàm khác trong chương trình. Nhưng các đối lại được dùng để trao đổi dữ liệu giữa các hàm.
Trong thân hàm có thể sử dụng một câu lệnh return, có thể dùng nhiều câu lệnh return ở những chỗ khác nhau và cũng có thể không sử dụng câu lệnh này.
Dạng tổng quát: return [biểu thức];
Giá trị của biểu thức trong câu lệnh return sẽ được gán cho hàm 5. 4. Quy tắc hoạt động của hàm:
- Lời gọi hàm có dạng sau: tên_hàm ([ds tham số thực sự]) với chú ý: số tham số thực sự phải bằng số tham số hình thức (đối của hàm) và kiểu của tham số thực phải cùng kiểu với tham số hình thức tương ứng.
- Khi gặp một lời gọi hàm, thì hàm sẽ được thực hiện theo trình tự sau: + Cấp phát bộ nhớ cho các đối và các biến cục bộ
+ Gán giá trị của các tham số thực cho các đối tương ứng + Thực hiện các câu lệnh trong thân hàm
+ Khi gặp câu lệnh return hoặc dấu } cuối cùng của thân hàm thì máy sẽ xoá các đối, các biến cục bộ và thoát khỏi hàm.
Nếu trở về từ một câu lệnh return có chứa biểu thức thì giá trị của biểu thức được gán cho hàm. Giá trị của hàm sẽ được sử dụng trong các biểu thức chứa nó.
5. 5. Cấu trúc tổng quát của chương trình có hàm: + Các #include
+ Các #define
+ Khai báo các đối tượng dữ liệu ngoài (biến, mảng, cấu trúc, hợp...) + Khai báo các nguyên mẫu của các hàm
+ Hàm main
+ Định nghĩa các hàm
Chú ý: Hàm main có thể đặt sau hoặc xen vào giữa các hàm khác 5.6. Xây dựng và sử dụng hàm
5.6.1. Các khái niệm liên quan đến hàm:
- Tên hàm
- Kiểu giá trị của hàm - Đối hay tham số hình thức - Thân hàm
- Khai báo hàm ( khai báo prototype) - Lời gọi hàm
- Tham số thực
5.6.2. Xây dựng hàm:
Các công việc: khai báo kiểu hàm, đặt tên hàm, khai báo các đối và đưa ra các câu lệnh cần thiết để thực hiện yêu cầu đề ra cho hàm. Một hàm được viết theo mẫu sau:
Type ten_ham( khai báo các đối) {
khai báo các biến cục bộ các câu lệnh
[return [bieu thuc]; ] }
• Chú ý 1: Đối với các hàm không có giá trị (như thủ tục trong Pascal) thì dùng kiểu void
• Chú ý 2: Hàm không có đối thì ta cũng dùng void để khai báo đối
• Chú ý 3: Khi gặp một toán tử retủn có chứa biểu thức, thì giá trị của biểu thức sẽ được chuyển kiểu cho phù hợp với kiểu của hàm trước khi nó được gán cho hàm
5.6.3. Sử dụng hàm
Hàm được sử dụng thông qua lời gọi tới nó. Cách viết một lời gọi hàm như sau: ten_ham ([danh sách các tham số thực sự])
trong đó:
+ số tham số thực phải bằng số đối