c cho pic
1.3.1 Ðịnh nghĩa Hàm Chương trình C được chia thành từng đơn vị gọi là hàm. Ðoạn mã mẫu chỉ có duy nhất một hàm main(). Hệ điều hành luôn trao quyền điều khiển cho hàm main() khi một chương trình C được thực thi. Tên hàm luôn được theo sau là cặp dấu ngoặc đơn (). Trong dấu ngoặc đơn có thể có hay không có những tham số (parameters). 1.3.2 Dấu phân cách (Delimiters) Sau định nghĩa hàm sẽ là dấu ngoặc xoắn mở {. Nó thông báo điểm bắt đầu của hàm. Tương tự, dấu ngoặc xoắn đóng } sau câu lệnh cuối trong hàm chỉ ra điểm kết thúc của hàm. Dấu ngoặc xoắn mở đánh dấu điểm bắt đầu của một khối mã lệnh, dấu ngoặc xoắn đóng đánh dấu điểm kết thúc của khối mã lệnh đó. Trong đoạn mã mẫu có 2 câu lệnh giữa 2 dấu ngoặc xoắn. Hơn nữa, đối với hàm, dấu ngoặc xoắn cũng dùng để phân định những đoạn mã trong trường hợp dùng cho cấu trúc vòng lặp và lệnh điều kiện.. 1.3.3 Dấu kết thúc câu lệnh (Terminator) Dòng int i = 0; trong đoạn mã mẫu là một câu lệnh (statement). Một câu lệnh trong C thì được kết thúc bằng dấu chấm phẩy (;). C không hiểu việc xuống dòng dùng phím Enter, khoảng trắng dùng phím spacebar hay một khoảng cách do dùng phím tab. Có thể có nhiều hơn một câu lệnh trên cùng một hàng nhưng mỗi câu lệnh phải được kết thúc bằng dấu chấm phẩy. Một câu lệnh không được kết thúc bằng dấu chấm phẩy được xem như một câu lệnh sai. 1.3.4 Dòng chú thích (Comment) Những chú thích thường được viết để mô tả công việc của một lệnh đặc biệt, một hàm hay toàn bộ chương trình. Trình biên dịch sẽ không dịch chúng. Trong C, chú thích bắt đầu bằng ký hiệu /* và kết thúc bằng */. Trường hợp chú thích có nhiều dòng, ta phải chú ý ký hiệu kết thúc (*/), nếu thiếu ký hiệu này, toàn bộ chương trình sẽ bị coi như là một chú thích. Trong đoạn mã mẫu dòng chữ "This is a sample program" là dòng chú thích. Trong trường hợp chú thích chỉ trên một dòng ta có thể dùng //. Ví dụ: int a = 0; // Biến ‘a’ đã được khai báo như là một kiểu số nguyên (interger) 1.3.5 Thư viện C (Library) Tất cả trình biên dịch C chứa một thư viện hàm chuẩn dùng cho những tác vụ chung. Một vài bộ cài đặt C đặt thư viện trong một tập tin (file) lớn trong khi đa số còn lại chứa nó trong nhiều tập tin nhỏ. Khi lập trình, những hàm được chứa trong thư viện có thể được dùng cho nhiều loại tác vụ khác nhau. Một hàm (được viết bởi một lập trình viên) có thể được đặt trong thư viện và được dùng bởi nhiều chương trình khi được yêu cầu. Vài trình biên dịch cho phép hàm được thêm vào thư viện chuẩn trong khi số khác lại yêu cầu tạo một thư viện riêng. 1.4 Biên dịch và thực thi một chương trình (Compiling and Running) Những bước khác nhau của việc dịch một chương trình C từ mã nguồn thành mã thực thi được thực hiện như sau : Ø Soạn thảo/Xử lý từ Ta dùng một trình xử lý từ (word processor) hay trình soạn thảo (editor) để viết mã nguồn (source code). C chỉ chấp nhận loại mã nguồn viết dưới dạng tập tin văn bản chuẩn. Vài trình biên dịch (compiler) cung cấp môi trường lập trình (xem phụ lục) gồm trình soạn thảo. Ø Mã nguồn Ðây là đoạn văn bản của chương trình mà người dùng có thể đọc. Nó là đầu vào của trình biên dịch C. Ø Bộ tiền xử lý C Từ mã nguồn, bước đầu tiên là chuyển nó qua bộ tiền xử lý của C. Bộ tiền xử lý này sẽ xem xét những câu lệnh bắt đầu bằng dấu #. Những câu lệnh này gọi là các chỉ thị tiền biên dịch (directives). Điều này sẽ được giải thích sau. Chỉ thị tiền biên dịch thường được đặt nơi bắt đầu chương trình mặc dù nó có thể được đặt bất cứ nơi nào khác. Chỉ thị tiền biên dịch là những tên ngắn gọn được gán cho một tập mã lệnh. Ø Mã nguồn mở rộng C Bộ tiền xử lý của C khai triển các chỉ thị tiền biên dịch và đưa ra kết quả. Ðây gọi là mã nguồn C mở rộng, sau đó nó được chuyển cho trình biên dịch C. Ø Trình biên dịch C (Compiler) Trình biên dịch C dịch mã nguồn mở rộng thành ngôn ngữ máy để máy tính hiểu được. Nếu chương trình quá lớn nó có thể được chia thành những tập tin riêng biệt và mỗi tập tin có thể được biên dịch riêng rẽ. Ðiều này giúp ích khi mà một tập tin bị thay đổi, toàn chương trình không phải biên dịch lại. Ø Bộ liên kết (Linker) Mã đối tượng cùng với những thủ tục hỗ trợ trong thư viện chuẩn và những hàm được dịch riêng lẻ khác kết nối lại bởi Bộ liên kết để cho ra mã có thể thực thi được. Ví dụ xét toán hạng có giá trị là 12, toán tử luận lý nhị phân sẽ coi số 12 này như 1100. Toán tử luận lý nhị phân xem xét các toán hạng dưới dạng chuỗi bit chứ không là giá trị số thông thường. Giá trị số có thể thuộc các cơ số: thập phân (decimal), bát phân (octal) hay thập lục phân (hexadecimal). Riêng toán tử luận lý nhị phân sẽ chuyển đổi toán hạng mà nó thao tác thành biểu diễn nhị phân tương ứng, đó là dãy số 1 hoặc là 0. Toán tử luận lý nhị phân gồm &, | , ^ , ~ , vv … được tổng kết qua bảng sau Bitwise AND( x & y) Mỗi vị trí của bit trả về kết quả là 1 nếu bit tại vị trí tương ứng của hai toán hạng đều là 1. Bitwise OR( x | y) Mỗi vị trí của bit trả về kết quả là 1 nếu bit tại vị trí tương ứng của một trong hai toán hạng là 1 Bitwise NOT ( ~ x) Ðảo ngược giá trị các bit của toán hạng (1 thành 0 và ngược lại) Bitwise XOR ( x ^ y) Mỗi vị trí của bit trả về kết quả là 1 nếu bit tại vị trí tương ứng của một trong hai toán hạng là 1 chứ không phải cả hai cùng là 1 Toán tử luận lý nhị phân xem kiểu dữ liệu số như là số nhị phân 32-bit, giá trị số được đổi thành giá trị bit để tính toán trước rồi sau đó sẽ trả về kết quả ở dạng số ban đầu. Ví dụ: Biểu thức 10 & 15 có nghĩa là (1010 & 1111) trả về giá trị 1010 có nghĩa là 10. Biểu thức 10 | 15 có nghĩa là (1010 | 1111) trả về giá trị 1111 có nghĩa là 15. Biểu thức 10 ^ 15 có nghĩa là (1010 ^ 1111) trả về giá trị 0101 có nghĩa là 5. Biểu thức ~10 có nghĩa là ( ~1010 ) trả về giá trị 1111.1111.1111.1111.1111.1111.1111.0101 có nghĩa là -11. 4.5 Biểu thức dạng hỗn hợp & Chuyển đổi kiểu Một biểu thức dạng hỗn hợp là một biểu thức mà trong đó các toán hạng của một toán tử thuộc về nhiều kiểu dữ liệu khác nhau. Những toán hạng này thông thường được chuyển về cùng kiểu với toán hạng có kiểu dữ liệu lớn nhất. Điều này được gọi là tăng cấp kiểu. Sự phát triển về kiểu dữ liệu theo thứ tự sau : char < int <long <float <double Chuyển đổi kiểu tự động được trình bày dưới đây nhằm xác định giá trị của biểu thức: a. char và short được chuyển thành int và float được chuyển thành double. b. Nếu có một toán hạng là double, toán hạng còn lại sẽ được chuyển thành double, và kết quả là double. c. Nếu có một toán hạng là long, toán hạng còn lại sẽ được chuyển thành long, và kết quả là long. d. Nếu có một toán hạng là unsigned, toán hạng còn lại sẽ được chuyển thành unsigned và kết quả cũng là unsigned. e. Nếu tất cả toán hạng kiểu int, kết quả là int. Ngoài ra nếu một toán hạng là long và toán hạng khác là unsigned và giá trị của kiểu unsigned không thể biểu diễn bằng kiểu long. Do vậy, cả hai toán hạng được chuyển thành unsigned long. Sau khi áp dụng những quy tắc trên, mỗi cặp toán hạng có cùng kiểu và kết quả của mỗi phép tính sẽ cùng kiểu với hai toán hạng. char ch; float f; double d; result = (ch/i) + (f*d) – (f+i); Trong ví dụ trên, trước tiên, ch có kiểu ký tự được chuyển thành integer và float f được chuyển thành double. Sau đó, kết quả của ch/i được chuyển thành double bởi vì f*d là double. Kết quả cuối cùng là double bởi vì các toán hạng lúc này đều là double 4.5.1 Ép kiểu (Casts) Thông thường, ta nên đổi tất cả hằng số nguyên sang kiểu float nếu biểu thức bao gồm những phép tính số học dựa trên số thực, nếu không thì vài biểu thức có thể mất đi giá trị thật của nó.Ta xem ví dụ: int x,y,z; x = 10; y = 100; z = x/y; Trong trường hợp này, z sẽ được gán 0 khi phép chia diễn ra và phần thập phân (0.10) sẽ bị cắt bỏ. Do đó một biểu thức có thể được ép thành một kiểu nhất định. Cú pháp chung của cast là: (kiểu dữ liệu) biểu thức Ví dụ, để đảm bảo rằng biểu thức a/b, với a và b là số nguyên, cho kết quả là kiểu float, dòng mã sau được viết: (float) a/b; Ép kiểu có thể áp dụng cho các giá trị hằng, biểu thức hay biến, ví dụ: (int) 17.487; (double) (5 * 4 / 8); (float) (a + 7); Trong ví dụ thứ hai, toán tử ép kiểu không đạt mục đích của nó bởi vì nó chỉ thực thi sau khi toàn biểu thức trong dấu ngoặc đã được tính. Biểu thức 5 * 4 / 8 cho ra giá trị là 2 (vì nó có kiểu là số nguyên nên đã cắt đi phần thập phân), vì vậy, giá trị kết quả với kiểu double cũng là 2.0. Ví dụ: int i = 1, j = 3; x = i / j; /* x = 0.0 */ x = (float) i/(float) j; /* x = 0.33 */ 4.6 Độ ưu tiên của toán tử ( Precedence Độ ưu tiên của toán tử thiết lập thứ tự ưu tiên tính toán khi một biểu thức số học cần được ước lượng. Tóm lại, độ ưu tiên đề cập đến thứ tự mà C thực thi các toán tử. Thứ tự ưu tiên của toán tử số học được thể hiện như bảng dưới đây. Những toán tử nằm cùng một hàng ở bảng trên có cùng quyền ưu tiên. Việc tính toán của một biểu thức số học sẽ được thực hiện từ trái sang phải cho các toán tử cùng độ ưu tiên. Toán tử *, /, và % có cùng đô ưu tiên và cao hơn + và - (hai ngôi). Độ ưu tiên của những toán tử này có thể được thay đổi bằng cách sử dụng dấu ngoặc đơn. Một biểu thức trong ngoặc luôn luôn được tính toán trước. Một cặp dấu ngoặc đơn này có thể được bao trong cặp khác. Ðây là sự lồng nhau của những dấu ngoặc đơn. Trong trường hợp đó, việc tính toán trước tiên được thực hiện tại cặp dấu ngoặc đơn trong cùng nhất rồi đến dấu ngoặc đơn bên ngoài. Nếu có nhiều bộ dấu ngoặc đơn thì việc thực thi sẽ theo thứ tự từ trái sang phải. Tính kết hợp cho biết cách thức các toán tử kết hợp với các toán hạng của chúng. Ví dụ, đối với toán tử một ngôi: toán hạng nằm bên phải được tính trước, trong phép chia thì toán hạng bên trái được chia cho toán hạng bên phải. Đối với toán tử gán thì biểu thức bên phải được tính tr ư ớc rồi gán giá trị cho biến bên trái toán tử. Tính kết hợp cũng cho biết thứ tự mà theo đó C đánh giá các toán tử trong biểu thức có cùng độ ưu tiên. Các toán tử như vậy có thể tính toán từ trái sang phải hoặc ngược lại như thấy trong bảng 4.5. Ví dụ: a = b = 10/2; Giá trị 5 sẽ gán cho b xong rồi gán cho a. Vì vậy thứ tự ưu tiên sẽ là phải sang trái. Hơn nữa: -8 * 4 % 2 – 3 được tính theo trình tự sau: Trình tự Thao tác Kết quả 1. - 8 (phép trừ một ngôi) số âm của 8 2. - 8 * 4 - 32 3. - 32 % 2 0 4. 0-3 -3 Theo trên thì toán tự một ngôi (dấu - ) có quyền ưu tiên cao nhất được tính trước tiên. Giữa * và % thì được tính từ trái sang phải. Tiếp đến sẽ là phép trừ hai ngôi. Thứ tự ưu tiên của các biểu thức con Những biểu thức phức tạp có thể chứa những biểu thức nhỏ hơn gọi là biểu thức con. C không xác định thứ tự mà các biểu thức con được lượng giá. Một biểu thức sau: a * b /c + d *c; bảo đảm rằng biểu thức con a * b/c và d*c sẽ được tính trước phép cộng. Hơn nữa, quy tắc từ trái sang phải cho phép toán nhân và chia bảo đảm rằng a sẽ được nhân với b và sau đó sẽ chia cho c. Nhưng không có quy tắc xác định hoặc a*b /c được tính trước hay sau d*c. Tùy chọn này là ở người thiết kế trình biên dịch quyết định. Quy tắc trái sang phải hay ngược lại chỉ áp dụng cho một chuỗi toán tử cùng độ ưu tiên. Cụ thể, nó áp dụng cho phép nhân và chia trong a*b/c. Nhưng nó không áp dụng cho toán tử + vì đã khác cấp. Bởi vì không thể xác định thứ tự tính toán các biểu thức con, do vậy, ta không nên dùng các biểu thức nếu giá trị biểu thức phụ thuộc vào thứ tự tính toán các biểu thức con . Xét ví dụ sau: a * b + c * b++ ; Có thể trình biên dịch này tính giá trị mục bên trái trước và dùng cùng giá trị b cho cả hai biểu thức con. Nhưng trình biên dịch khác lại tính giá trị mục bên phải và tăng giá trị b trước khi tính giá trị mục bên trái. Ta không nên dùng toán tử tăng hay giảm cho một biến mà nó xuất hiện nhiều hơn một lần trong một biểu thức . Thứ tự ưu tiên giữa những toán tử so sánh (toán tử quan hệ) Ta đã thấy trong phần trước một số toán tử số học có độ ưu tiên cao hơn các toán tử số học khác. Riêng với toán tử so sánh, không có thứ tự ưu tiên giữa các toán tử và chúng được ước lượng từ trái sang phải. Thứ tự ưu tiên giữa những toán tử luận lý Bảng dưới đây trình bày thứ tự ưu tiên cho toán tử luận lý. Khi có nhiều toán tử luận lý trong một điều kiện, chúng được lượng giá từ phải sang trái . Ví dụ, xét điều kiện sau: False OR True AND NOT False AND True Ðiều kiện này được tính như sau: 1. False OR True AND [NOT False] AND True NOT có độ ưu tiên cao nhất. 2. False OR True AND [True AND True] Ở đây, AND là toán tử có độ ưu tiên cao nhất và những toán tử có cùng ưu tiên được tính từ phải sang trái. 3. False OR [True AND True] 4. [False OR True] 5. True Thứ tự ưu tiên giữa các kiểu toán tử khác nhau Do vậy, trong một biểu thức gồm cả ba kiểu toán tử, các toán tử số học được tính trước, kế đến là toán tử so sánh và sau đó là toán tử luận lý . Thứ tự ưu tiên của các toán tử trong cùng một kiểu thì đã được nói tới ở những phần trước. Xét ví dụ sau: 2*3+4/2 > 3 AND 3<5 OR 10<9 Việc thực hiện tính toán sẽ như sau: 1. [2*3+4/2] > 3 AND 3<5 OR 10<9 Ðầu tiên toán tử số học sẽ được tính theo thứ tự ưu tiên như bảng 4.4. 2. [[2*3]+[4/2]] > 3 AND 3<5 OR 10<9 3. [6+2] >3 AND 3<5 OR 10<9 4. [8 >3] AND [3<5] OR [10<9] Kế đến sẽ tính tất cả những toán tử so sánh có cùng độ ưu tiên theo quy tắc tính từ trái sang phải. 5. True AND True OR False Cuối cùng tính toán các toán tử kiểu luận lý. AND sẽ có độ ưu tiên cao hơn OR. 6. [True AND True]OR False 7. True OR False 8. True Dấu ngoặc đơn Thứ tự ưu tiên của các toán tử có thể thay đổi bởi các dấu ngoặc đơn. Khi đó, chương trình sẽ tính toán các phần dữ liệu trong dấu ngoặc đơn trước. Ø Khi một cặp dấu ngoặc đơn này được bao trong cặp khác, việc tính toán thực hiện trước tiên tại cặp dấu ngoặc đơn trong cùng nhất, rồi đến dấu ngoặc đơn bên ngoài. Ø Nếu có nhiều bộ dấu ngoặc đơn thì việc thực hiện sẽ theo thứ tự từ trái sang phải . Xét ví dụ sau: 5+9*3^2-4 > 10 AND (2+2^4-8/4 > 6 OR (2<6 AND 10>11)) Cách tính sẽ là: 1. 5+9*3^2-4 > 10 AND (2+2^4-8/4 > 6 OR (True AND False)) Dấu ngoặc đơn trong cùng sẽ được tính trước tất cả các toán tử khác và áp dụng quy tắc cơ bản trong bảng 4.6 cho tính toán bên trong cặp dấu ngoặc này. 2. 5+9*3^2-4 > 10 AND (2+2^4-8/4 > 6 OR False) 3. 5+9*3^2-4 > 10 AND (2+16-8/4 > 6 OR False) Kế đến dấu ngoặc đơn ở ngoài được xét đến. Xem lại các bảng nói về thứ tự ưu tiên của các toán tử. 4. 5+9*3^2-4 > 10 AND (2+16-2 > 6 OR False) 5. 5+9*3^2-4 > 10 AND (18-2 > 6 OR False) 6. 5+9*3^2-4 > 10 AND (16 > 6 OR False) 7. 5+9*3^2-4 > 10 AND (True OR False) 8. 5+9*3^2-4 > 10 AND True 9. 5+9*9-4>10 AND True Ta tính biểu thức bên trái trước theo các quy tắc 10. 5+81-4>10 AND True 11. 86-4>10 AND True 12. 82>10 AND True 13. True AND True 14. True. 6.1 Tập tin tiêu đề <stdio.h> Trong các ví dụ trước, ta đã từng viết dòng mã sau: #include <stdio.h> Ðây là lệnh tiền xử lý (preprocessor command). Trong C chuẩn, ký hiệu # nên đặt tại cột đầu tiên. stdio.h là một tập tin và được gọi là tập tin tiêu đề (header). Nó chứa các macro cho nhiều hàm nhập và xuất được dùng trong C. Hàm printf() , scanf() , putchar() và getchar() được thiết kế theo cách gọi các macro trong tập tin stdio.h để thực thi các công việc tương ứng. 6.2 Nhập và xuất trong C (Input and Output) Thư viện chuẩn trong C cung cấp hai hàm để thực hiện các yêu cầu nhập và xuất có định dạng. Chúng là: · printf() – Hàm xuất có định dạng. · scanf() – Hàm nhập có định dạng. Những hàm này gọi là những hàm được định dạng vì chúng có thể đọc và in dữ liệu ra theo các định dạng khác nhau được điều khiển bởi người dùng. Bộ định dạng qui định dạng thức mà theo đó giá trị của biến sẽ được nhập vào và in ra. 6.2.1 printf() Chúng ta đã quen thuộc với hàm này qua các phần trước. Ở đây, chúng ta sẽ xem chúng chi tiết hơn. Hàm printf() được dùng để hiển thị dữ liệu trên thiết bị xuất chuẩn – console (màn hình). Dạng mẫu chung của hàm này như sau: printf("control string", argument list); Danh sách tham số (argument list) bao gồm các hằng, biến, biểu thức hay hàm và được phân cách bởi dấu phẩy. Cần phải có một lệnh định dạng nằm trong chuỗi điều khiển (control string) cho mỗi tham số trong danh sách. Những lệnh định dạng phải tương ứng với danh sách các tham số về số lượng, kiểu dữ liệu và thứ tự. Chuỗi điều khiển phải luôn được đặt bên trong cặp dấu nháy kép“”, đây là dấu phân cách (delimiters). Chuỗi điều khiển chứa một hay nhiều hơn ba thành phần dưới đây : § Ký tự văn bản (Text characters) – Bao gồm các ký tự có thể in ra được và sẽ được in giống như ta nhìn thấy. Các khoảng trắng thường được dùng trong việc phân chia các trường (field) được xuất ra. § Lệnh định dạng - Định nghĩa cách thức các mục dữ liệu trong danh sách tham số sẽ được hiển thị. Một lệnh định dạng bắt đầu với một ký hiệu % và theo sau là một mã định dạng tương ứng cho mục dữ liệu. Dấu % được dùng trong hàm printf() để chỉ ra các đặc tả chuyển đổi. Các lệnh định dạng và các mục dữ liệu tương thích nhau theo thứ tự và kiểu từ trái sang phải. Một mã định dạng thì cần thiết cho mọi mục dữ liệu cần in ra. § Các ký tự không in được – Bao gồm phím tab, dấu khoảng trắng và dấu xuống dòng. Mỗi lệnh định dạng gồm một hay nhiều mã định dạng. Một mã định dạng bao gồm ký hiệu % và một bộ định kiểu. Bảng 6.1 liệt kê các mã định dạng khác nhau được hỗ trợ bởi câu lệnh printf() : Ký tự đơn (Single Character) %c Chuỗi (String) %s Số nguyên có dấu (Signed decimal integer) %d Số thập phân có dấu chấm động (Floating point) %f %f hoặc %e Số thập phân có dấu chấm động - Biểu diễn phần thập phân %lf Số thập phân có dấu chấm động - Biểu diễn dạng số mũ %e %f hoặc %e Số thập phân có dấu chấm động (%f hoặc %e, con số nào ít hơn) %g Số nguyên không dấu (Unsigned decimal integer) %u Số thập lục phân không dấu (Dùng “ABCDEF”) (Unsigned hexadecimal integer) %x Số bát phân không dấu (Unsigned octal integer) %o Bảng 6.1: Mã định dạng trong printf () Trong bảng trên, c, d, f, lf, e, g, u, s, o và x là bộ định kiểu. Các quy ước in cho các mã định dạng khác nhau được tổng kết trong Bảng 6.2: %d Các con số trong số nguyên. %f Phần số nguyên của số sẽ được in nguyên dạng. Phần thập phân sẽ chứa 6 con số. Nếu phần thập phân của con số ít hơn 6 số, nó sẽ được thêm các số không (0) bên phải hay gọi là làm tròn phía bên phải. %e Một con số bên trái dấu chấm thập phân và 6 con số bên phải giống như %f Bởi vì các ký hiệu % , \ và “ được dùng đặc biệt trong chuỗi điều khiển, nếu chúng ta cần in các ký hiệu này lên màn hình, chúng phải được dùng như trong Bảng 6.3: \\ In ký tự \ \ “ In ký tự “ %% In ký tự % Ví dụ 6.1 : Ðây là một chương trình đơn giản dùng minh họa cho một chuỗi có thể được in theo lệnh định dạng. Chương trình này cũng hiển thị một ký tự đơn, số nguyên và số thực (a single character, integer, và float). #include <stdio.h> #include <conio.h> main() { int a = 10; float b = 24.67892345; char ch = 'A'; printf(" Integer data = %d", a); printf(" Float Data = %f", b); printf(" Character = %c", ch); printf(" This prints the string"); printf("%s", " This also prints a string"); getch(); } 6.2.2 scanf() Hàm scanf() được sử dụng để nhập dữ liệu. Khuôn dạng chung của hàm scanf() như sau: scanf(<Chuỗi các định dạng>, <Danh sách các tham số>); Ðịnh dạng được sử dụng bên trong câu lệnh printf() cũng được sử dụng cùng cú pháp trong các câu lệnh scanf(). Những lệnh định dạng, bao gồm bổ từ và danh sách tham số được bàn luận cho printf() thì cũng hợp lệ cho scanf(), chúng tuân theo một số điểm khác biệt sau: Sự khác nhau trong danh sách tham số giữa printf() và scanf() Hàm printf() dùng các tên biến, hằng số, hằng chuỗi và các biểu thức, nhưng scanf() sử dụng những con trỏ tới các biến. Một con trỏ tới một biến là một mục dữ liệu chứa đựng địa chỉ của nơi mà biến được cất giữ trong bộ nhớ. Những con trỏ sẽ được bàn luận chi tiết ở chương sau. Khi sử dụng scanf() cần tuân theo những quy tắc cho danh sách tham số: Nếu ta muốn nhập giá trị cho một biến có kiểu dữ liệu cơ bản, gõ vào tên biến cùng với ký hiệu & trước nó. Khi nhập giá trị cho một biến thuộc kiểu dữ liệu dẫn xuất (không phải thuộc bốn kiểu cơ bản char , , float , double ), không sử dụng & trước tên biến. Ø Sự khác nhau trong lệnh định dạng giữa printf() và scanf() 1. Không có tùy chọn %g . 2. Mã định dạng %f và %e có cùng hiệu quả tác động. Cả hai nhận một ký hiệu tùy chọn, một chuỗi các con số có hay không có dấu chấm thập phân và một trường số mũ tùy chọn. Cách thức hoạt động của scanf() scanf() sử dụng những ký tự không được in như ký tự khoảng trắng, ký tự phân cách (tab), ký tự xuống dòng để quyết định khi nào một trường nhập kết thúc và bắt đầu. Có sự tương ứng giữa lệnh định dạng với những trường trong danh sách tham số theo một thứ tự xác định, bỏ qua những ký tự khoảng trắng bên trong. Do đó, đầu vào có thể được trải ra hơn một dòng, miễn là chúng ta có ít nhất một ký tự phân cách, khoảng trắng hay hàng mới giữa các trường nhập vào. Nó bỏ qua những khoảng trắng và ranh giới hàng để thu được dữ liệu. Ví dụ 6.6: /*Chương trình sau mô tả việc dùng hàm scanf()*/ #include <stdio.h> #include <conio.h> main() { int a; float d; char ch, name[40]; printf("Please enter the data "); scanf("%d %f %c %s", &a, &d, &ch, name); printf(" The values accepted are: %d, %f, %c, %s", a, d, ch, name); getch(); Ví dụ dưới đây mô tả việc sử dụng hàm scanf() để nhập vào một chuỗi gồm có những ký tự viết HOA và khoảng trắng. Chuỗi sẽ có chiều dài không xác định nhưng nó bị giới hạn trong 79 ký tự (thật ra, 80 ký tự bao gồm ký tự trống (null) được thêm vào nơi cuối chuỗi). Ví dụ 6.8: #include <stdio.h> #include <conio.h> main() { char line[80]={}; /* line[80] là một mảng lưu 80 ký tự */ scanf("%[ ABCDEFGHIJKLMNOPQRSTUVWXYZ]", line); printf("%s",line); getch(); } Mã khuôn dạng %[] có nghĩa những ký tự được định nghĩa bên trong [] có thể được chấp nhận như những ký tự chuỗi hợp lệ. Nếu chuỗi DIEN TU MAY TINH được nhập vào từ thiết bị nhập chuẩn, khi chương trình được thực thi, toàn bộ chuỗi sẽ được gán cho mảng một khi chuỗi chỉ toàn là ký tự viết hoa và khoảng trắng: 6.3 Bộ nhớ đệm Nhập và Xuất (Buffered I/O) Ngôn ngữ C bản thân nó không định nghĩa các thao tác nhập và xuất. Tất cả thao tác nhập và xuất được thực hiện bởi các hàm có sẵn trong thư viện hàm của C. Thư viện hàm C chứa một hệ thống hàm riêng mà nó điều khiển các thao tác này. Ðó là: Bộ nhớ đệm Nhập và Xuất – được dùng để đọc và viết các ký tự ASCII Một vùng đệm là nơi lưu trữ tạm thời, nằm trên bộ nhớ máy tính hoặc trên thẻ nhớ của bộ điều khiển thiết bị (controller card). Các ký tự nhập vào từ bàn phím được đưa vào bộ nhớ và đợi đến khi người dùng nhấn phím return hay enter thì chúng sẽ được thu nhận như một khối và cung cấp cho chương trình. Bộ nhớ đệm nhập và xuất có thể được phân thành: § Thiết bị nhập/xuất chuẩn (Console I/O) § Tập tin đệm nhập/xuất (Buffered File I/O) Thiết bị nhập/xuất chuẩn liên quan đến những hoạt động của bàn phím và màn hình của máy tính. Tập tin đệm nhập/xuất liên quan đến những hoạt động thực hiện đọc và viết dữ liệu vào tập tin. Chúng ta sẽ nói về Thiết bị nhập/xuất. Trong C, Thiết bị nhập/xuất chuẩn là một thiết bị luồng. Các hàm trong Thiết bị nhập/xuất chuẩn hướng các thao tác đến thiết bị nhập và xuất chuẩn của hệ thống. Các hàm đơn giản nhất của Thiết bị nhập/xuất chuẩn là: § getchar() – Ðọc một và chỉ một ký tự từ bàn phím. § putchar() – Xuất một ký tự đơn ra màn hình. 6.3.1 getchar() Hàm getchar() được dùng để đọc dữ liệu nhập vào, chỉ một ký tự tại một thời điểm từ bàn phím.Trong hầu hết việc thực thi của C, khi dùng getchar() , các ký tự nằm trong vùng đệm cho đến khi người dùng nhấn phím xuống dòng. Vì vậy nó sẽ đợi cho đến khi phím Enter được gõ. Hàm getchar() không có tham số, nhưng vẫn phải có cặp dấu ngoặc đơn. Nó đơn giản lấy về ký tự tiếp theo và sẵn sàng đưa ra cho chương trình. Chúng ta nói rằng hàm này trả về một giá trị có kiểu ký tự. Chương trình sau trình bày cách dùng hàm getchar() . Ví dụ 6.10: /* Chương trình trình bày cách dùng getchar() */ #include <stdio.h> #include <conio.h> main() { char letter; printf(" Please enter any character: "); letter = getchar(); printf(" The character entered by you is %c. ", letter); getch(); } rong chương trình trên ‘letter’ là một biến được khai báo là kiểu char do vậy nó sẽ nhận vào ký tự. Một thông báo: Please enter any character: sẽ xuất hiện trên màn hình. Ta nhập vào một ký tự, trong ví dụ là S, qua bàn phím và nhấn Enter. Hàm getchar() nhận ký tự đó và gán cho biến có tên là letter. Sau đó nó được hiển thị trên màn hình và ta có được thông báo. The character entered by you is S. 6.3.2 putchar() putchar() là hàm xuất ký tự trong C, nó sẽ xuất một ký tự lên màn hình tại vị trí con trỏ màn hình. Hàm này yêu cầu một tham số. Tham số của hàm putchar() có thể thuộc các loại sau: · Hằng ký tự đơn · Ðịnh dạng (Escape sequence) · Một biến ký tự. Nếu tham số là một hằng nó phải được bao đóng trong dấu nháy đơn. Bảng 6.5 trình bày vài tùy chọn cho putchar() và tác động của chúng. Biến ký tự putchar(c) Hiện thị nội dung của biến ký tự c Hằng biến ký tự putchar(‘A’) Hiển thị ký tự A Hằng số putchar(‘5’) hien thi 5 Ðịnh dạng (escape sequence) putchar(‘\t’) Chèn một ký tự khoảng cách (tab) tại vị trí con trỏ màn hình Ðịnh dạng (escape sequence) putchar(‘ ’) Chèn một mã xuống dòng tại vị trí con trỏ màn hình Chương trình sau trình bày về hàm putchar() : Ví dụ 6.11: /* Chương trình này trình bày việc sử dụng hằng và định dạng trong hàm putchar() */ #include <stdio.h> #include <conio.h> main() { putchar('H'); putchar(' '); putchar('\t'); putchar('E'); putchar(' '); putchar('\t'); putchar('\t'); putchar('L'); putchar(' '); putchar('\t'); putchar('\t'); putchar('\t'); putchar('L'); putchar(' '); putchar('\t'); putchar('\t'); putchar('\t'); putchar('\t'); putchar('O'); getch(); } Các câu lệnh điều kiện cho phép chúng ta thay đổI luồng chương trình. Dựa trên một điều kiện nào đó, một câu lệnh hay một chuỗI các câu lệnh có thể được thực hiện hoặc không. Hầu hết các ngôn ngữ lập trình đều sử dụng lệnh if để đưa ra điều kiện. Nguyên tắc thực hiện như sau nếu điều kiện đưa ra là đúng (true), chương trình sẽ thực hiện một công việc nào đó, nếu điều kiện đưa ra là sai (false), chương trình sẽ thực hiện một công việc khác. Ví dụ 7.1: Để xác định một số là số chẳn hay số lẻ, ta thực hiện như sau: 1. Nhập vào một số. 2. Chia số đó cho 2 để xác định số dư. 3. Nếu số dư của phép chia là 0, đó là số “Chẵn”. HOẶC Nếu số dư của phép chia khác 0, đó là số “Lẻ”. Bước 2 trong giải thuật trên kiểm tra phần dư của số đó khi chia cho 2 có bằng 0 không? Nếu đúng, ta thực hiện việc hiển thị thông báo đó là số chẵn. Nếu số dư đó khác 0, ta thực hiện việc hiển thị thông báo đó là số lẻ. Trong C một điều kiện được coi là đúng (true) khi nó có giá trị khác 0, là sai (false) khi nó có giá trị bằng 0. 7.2. Các câu lệnh lựa chọn: C cung cấp hai dạng câu lệnh lựa chọn: Ø Câu lệnh if Ø Câu lệnh switch Chúng ta hãy tìm hiểu hai câu lệnh lựa chọn này. 7.2.1 Câu lệnh ‘if’: Câu lệnh if cho phép ta đưa ra các quyết định dựa trên việc kiểm tra một điều kiện nào đó là đúng (true) hay sai (false). Các điều kiện gồm các toán tử so sánh và logic mà chúng ta đã thảo luận ở bài 4. Dạng tổng quát của câu lệnh Biểu thức phải luôn được đặt trong cặp dấu ngoặc . Mệnh đề theo sau từ khoá if là một điều kiện (hoặc một biểu thức điều kiện) cần được kiểm tra. Tiếp đến là một lệnh hay một tập các lệnh sẽ được thực thi khi điều kiện (hoặc biểu thức điều kiện) có kết quả true
Ví dụ 7.2:
#include <stdio.h>
#include <conio.h>
main()
{
int x, y;
char a = 'y';
x = y = 0;
if (a == 'y')
{
x += 5;
printf("The numbers are %d and \t%d", x, y);
}
getch();
}
Có kết quả này là do biến a đã được gán giá trị 'y'.
Chú ý rằng, khối lệnh sau lệnh if
{}
. Khi có nhiều lệnh cần được thực hiện, các câu lệnh đó được coi như một block (khốI lệnh) và phảI được đặt trong cặp dấu
{}
. Nếu trong ví dụ trên ta không đưa vào dấu ngoặc nhọn ở câu lệnh if, chỉ có câu lệnh đầu tiên (x += 5) được thực hiện khi điều kiện trong câu lệnh if là đúng.
Ví dụ dưới đây sẽ kiểm tra một năm có phải là năm nhuận hay không. Năm nhuận là năm chia hết cho 4 hoặc 400 nhưng không chia hết cho 100. Chúng ta sử dụng lệnh if để kiểm tra điều kiện.
Ví dụ 7.3:
/* To test for a leap year */
#include <stdio.h>
#include <conio.h>
main()
{
int year;
printf("
Please enter a year:");
scanf("%d", &year);
if(year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
printf("
%d is a leap year!", year);
getch();
Điều kiện year % 4 == 0 && year % 100 != 0 || year % 400 == 0
7.2.2 Câu lệnh ‘if … else’:
Ở trên chúng ta đã biết dạng đơn giản nhất của câu lệnh if, cho phép ta lựa chọn để thực hiện hay không một câu lệnh hoặc một chuỗI các lệnh. C cũng cho phép ta lựa chọn trong hai khốI lệnh để thực hiện bằng cách dùng cấu trúc if – else. Cú pháp như sau:
Nếu biểu thức điều kiện trên là đúng (khác 0), câu lệnh 1 được thực hiện. Nếu nó sai (khác 0) câu lệnh 2 được thực hiện. Câu lệnh sau if và else có thể là lệnh đơn hoặc lệnh phức. Các câu lệnh đó nên được lùi vào trong dòng mặc dù không bắt buộc. Cách viết đó giúp ta nhìn thấy ngay những lệnh nào sẽ được thực hiện tùy theo kết quả của biểu thức điều kiện.
Bây giờ chúng ta viết một chương trình kiểm tra một số là số chẵn hay số lẻ. Nếu đem chia số đó cho 2 được dư là 0 chương trình sẽ hiển thị dòng chữ “The number is Even”, ngược lại sẽ hiển thị dòng chữ “The number is Odd”.
Ví dụ 7.4:
#include <stdio.h>
#include <conio.h>
main()
{
int num, res;
printf("Enter a number: ");
scanf("%d", &num);
res = num % 2;
if (res == 0)
printf("The number is Even");
else
printf("The number is Odd");
getch();
}
Xem một ví dụ khác, đổi một ký tự hoa thành ký tự thường. Nếu ký tự không phải là một ký tự hoa, nó sẽ được in ra mà không cần thay đổi. Chương trình sử dụng cấu trúc if-else để kiểm tra xem một ký tự có phải là ký tự hoa không, rồI thực hiện các thao tác tương ứng.
Ví dụ 7.5:
/* Doi mot ky tu hoa thanh ky tu thuong */
#include <stdio.h>
#include <conio.h>
main()
{
char c;
printf("Please enter a character: ");
scanf("%c", &c);
if (c >= 'A' && c <= 'Z')
printf("Lowercase character = %c", c + 'a' - 'A');
else
printf("Character Entered is = %c", c);
getch();
}
Biểu thức
c >= ‘A’ && c <= ‘Z’
kiểm tra ký tự nhập vào có là ký tự hoa không. Nếu biểu thức trả về true, ký tự đó sẽ được đổi thành ký tự thường bằng cách sử dụng biểu thức
c + ‘a’ – ‘A’
, và được in ra màn hình qua hàm
printf()
. Nếu giá trị của biểu thức là false, câu lệnh sau else được chạy và chương trình hiển thị kí tự đó ra màn hình mà không cần thực hiện bất cứ sự thay đổi nào:
7.2.3 Nhiều lựa chọn – Các câu lệnh ‘if … else’:
Câu lệnh if cho phép ta lựa chọn thực hiện một hành động nào đó hay không. Câu lệnh if – else cho phép ta lựa chọn thực hiện giữa hai hành động. C cho phép ta có thể đưa ra nhiều lựa chọn hơn. Chúng ta mở rộng cấu trúc if – else bằng cách thêm vào cấu trúc else – if để thực hiện điều đó. Nghĩa là mệnh đề else trong một câu lệnh if – else lạI chứa một câu lệnh if – else khác. Do đó nhiều điều kiện hơn được kiểm tra và tạo ra nhiều lựa chọn hơn.
Cú pháp tổng quát trong trường hợp này như sau:
if (biểu thức) câu_lệnh;
else
if (biểu thức) câu_lệnh;
……
else câu_lệnh;
Cấu trúc này gọI là if–else–if ladder hay if-else-if staircase.
Cách canh lề (lùi vào trong) như trên giúp ta nhìn chương trình một cách dễ dàng khi có một hoặc hai lệnh if. Tuy nhiên khi có nhiều lệnh if hơn cách viết đó dễ gây ra nhầm lẫn vì nhiều câu lệnh sẽ phải lùi vào quá sâu. Vì vậy, lệnh if-else-if thường được canh lề theo dạng:
if (biểu thức)
câu_lệnh;
else if (biểu thức)
câu_lệnh;
else if (biểu thức)
câu_lệnh;
……….
else
câu_lệnh;
Các điều kiện được kiểm tra từ trên xuống dưới. Khi có một điều kiện nào đó là true, các câu lệnh gắn với nó sẽ được thực hiện và các lệnh còn lại sẽ được bỏ qua. Nếu không có điều kiện nào là true, các câu lệnh gắn với else cuối cùng sẽ được thực hiện. Nếu mệnh đề else đó không tồn tại, sẽ không có lệnh nào được thực hiện do tất cả các điều kiện đều false.
Ví dụ dưới đây nhận một số từ người dùng. Nếu số đó có giá trị từ 1 đến 3, chương trình sẽ in ra số đó, ngược lại chương trình in ra thông báo “Invalid choice”.
Ví dụ 7.6:
#include <stdio.h>
#include <conio.h>
main()
{
int x;
x = 0;
printf("Enter Choice (1 - 3): ");
scanf("%d", &x);
if (x == 1)
printf("
Choice is 1");
else if ( x == 2)
printf("
Choice is 2");
else if ( x == 3)
printf("
Choice is 3");
else
printf("
Invalid Choice: Invalid Choice");
getch();
}
Trong chương trình trên:
Nếu x = 1, hiển thị dòng chữ “Choice is 1”.
Nếu x = 2, hiển thị dòng chữ “Choice is 2”.
Nếu x = 3, hiển thị dòng chữ “Choice is 3” được hiển thị.
Nếu x là bất kỳ một số nào khác 1, 2, hoặc 3, “Invalid Choice” được hiển thị:
Nếu chúng ta muốn thực hiện nhiều hơn một lệnh sau mỗi câu lệnh if hay else, ta phải đặt các câu lệnh đó vào trong cặp dấu ngoặc nhọn {}. Các câu lệnh đó tạo thành một nhóm gọi là lệnh phức hay một khối lệnh:
if (result >= 45)
{
printf("Passed
");
printf("Congratulations
");
}
else
{
printf("Failed
");
printf("Good luck next time
");
}
7.2.4 Các cấu trúc if lồng nhau:
Một cấu trúc if lồng nhau là một lệnh if được đặt bên trong một lệnh if hoặc else khác. Trong C, lệnh else luôn gắn với lệnh if không có else gần nó nhất, và nằm trong cùng một khối lệnh với nó. Ví dụ:
if (biểu thức–1)
{
if (biểu thức–2)
câu_lệnh1;
if (biểu thức–3)
câu_lệnh2;
else
câu_lệnh3; /* với if (biểu thức–3) */
}
else
câu_lệnh4; /* với if (biểu thức–1) */
Trong đoạn lệnh minh họa ở trên, nếu giá trị của biểu thức-1 là true thì lệnh if thứ hai sẽ được kiểm tra. Nếu biểu thức-2 là true thì lệnh câu_lệnh1 sẽ được thực hiện. Nếu biểu thứu-3 là true, câu_lệnh2 sẽ được thực hiện nếu không câu_lệnh3 được thực hiện. Nếu biểu thức-1 là false thì câu_lệnh4 được thực hiện.
Vì lệnh else trong cấu trúc else-if là không bắt buộc, nên có thể có một cấu trúc khác như dạng dưới đây:
if (điều kiện-1)
if (điều kiện-2)
câu_lệnh1;
else
câu_lệnh2;
câu lệnh kế tiếp;
Trong đoạn mã trên, nếu điều kiện-1 là true, chương trình sẽ chuyển đến thực hiện lệnh if thứ hai và điều kiện-2 được kiểm tra. Nếu điều kiện đó là true, câu_lệnh1 được thực hiện, nếu không câu_lệnh2 được thực hiện, sau đó chương trình thực hiện những lệnh trong câu lệnh kế tiếp. Nếu điều kiện-1 là false, chương trình sẽ chuyển đến thực hiện những lệnh trong câu lệnh kế tiếp.
Ví dụ, marks1 và marks2 là điểm hai môn học của một sinh viên. Điểm marks2 sẽ được cộng thêm 5 điểm nếu nó nhỏ hơn 50 và marks1 lớn hơn 50. Nếu marks2 lớn hơn hoặc bằng 50 thì sinh viên đạt loại ‘A’. Điều này có thể được biểu diễn bởi đoạn if có cấu trúc như sau:
if (marks1 > 50 && marks2 < 50)
marks2 = marks2 + 5;
if (marks2 >= 50)
grade = ‘A’;
Một số người đưa ra đoạn code như sau:
if (marks1 > 50)
if (marks2 < 50)
marks2 = marks2 + 5;
else
grade = ‘A’;
Trong đoạn lệnh này, ‘A’ được gán cho biến grace chỉ khi marks1 lớn hơn 50 và marks2 lớn hơn hoặc bằng 50. Nhưng theo như yêu cầu của bài toán, bíến grace được gán giá trị ‘A’ sau khi thưc hiện việc kiểm tra để cộng điểm và kiểm tra giá trị của marks2. Hơn nữa, giá trị của biến grace không phụ thuộc vào marks1.
Vì lệnh else trong cấu trúc if-else là không bắt buộc, nên khi có lệnh else nào đó không được đưa vào trong chuỗi cấu trúc if lồng nhau chương trình sẽ trông không rõ ràng. Một lệnh else luôn được gắn với lệnh if gần nó nhất mà lệnh if này chưa được kết hợp với một lệnh else nào.
Ví dụ :
if (n >0)
if ( a > b)
z = a;
else
z = b;
Lệnh else đi với lệnh if bên trong. Việc viết lùi vào trong dòng là một cách thể hiện mối quan hệ đó. Tuy nhiên canh lề không có chức năng gắn else với lệnh if. Cặp dấu ngoặc nhọn {} giúp chúng ta thực hiện chức năng đó một cách chính xác.
if (n > 0)
{ if ( a > b)
z = a;
}
else
z = b;
Hình bên dưới biểu diễn sự kết hợp giữa if và else trong một chuỗi các lệnh if lồng nhau.
Theo chuẩn ANSI, có thể lồng nhau đến 15 mức. Tuy nhiên, hầu hết trình biên dịch cho phép nhiều hơn thế.
Một ví dụ về if lồng nhau được cho bên dưới:
Ví dụ 7.7:
#include <stdio.h>
#include <conio.h>
main()
{
int x, y;
x = y = 0;
printf("Enter Choice (1 - 3): ");
scanf("%d", &x);
if(x == 1)
{
printf("
Enter value for y (1 - 5): ");
scanf ("%d", &y);
if (y <= 5)
printf("
The value for y is: %d", y);
else
printf("
The value of y exceeds 5");
}
else
printf ("
Choice entered was not 1");
getch();
}
Trong chương trình trên, nếu giá trị của x được nhập là 1, người dùng được yêu cầu nhập tiếp giá trị của y. Ngược lại, dòng chữ “Choice entered was not 1” được hiển thị. Lệnh if đầu tiên có lồng một if trong đó để hiển thị giá trị của y nếu người dùng nhập vào một giá trị nhỏ hơn 5 cho y, hoặc ngược lại sẽ hiển thị dòng chữ “The value of y exceeds 5”:
7.2.5 Câu lệnh ‘switch’:
Câu lệnh switch cho phép ta đưa ra quyết định có nhiều cách lựa chọn, nó kiểm tra giá trị của một biểu thức trên một danh sách các hằng số nguyên hoặc kí tự. Khi nó tìm thấy một giá trị trong danh sách trùng với giá trị của biểu thức điều kiện, các câu lệnh gắn với giá trị đó sẽ được thực hiện. Cú pháp tổng quát của lệnh switch như sau:
switch (biểu_thức)
{ case hằng_1:
chuỗi_câu_lệnh;
break;
case hằng_2:
chuỗi_câu_lệnh;
break;
case hằng_3:
chuỗi_câu_lệnh;
break;
default:
chuỗi_câu_lệnh;
}
Ở đó, switch, case và default là các từ khoá, chuỗi_câu_lệnh có thể là lệnh đơn hoặc lệnh ghép và không cần đặt trong cặp dấu ngoặc. Biểu_thức theo sau từ khóa switch phải được đặt trong dấu ngoặc ( ), và toàn bộ phần thân của lệnh switch phải được đặt trong cặp ngoặc nhọn { }. Kiểu dữ liệu kết quả của biểu_thức và kiểu dữ liệu của các hằng theo sau từ khoá case phải đồng nhất. Chú ý, hằng số sau case chỉ có thể là một hằng số nguyên hoặc hằng ký tự. Nó cũng có thể là các hằng biểu thức – những biểu thức không chứa bất kỳ một biến nào. Tất cả các giá trị của case phải khác nhau.
Trong câu lệnh switch, biểu thức được xác định giá trị, giá trị của nó được so sánh với từng giá trị gắn với từng case theo thứ tự đã chỉ ra. Nếu một giá trị trong một case trùng với giá trị của biểu thức, các lệnh gắn với case đó sẽ được thực hiện. Lệnh break (sẽ nói ở phần sau) cho phép thoát ra khỏi switch. Nếu không dùng lệnh break, các câu lệnh gắn với case bên dưới sẽ được thực hiện không kể giá trị của nó có trùng với giá trị của biểu thức điều kiện hay không. Chương trình cứ tiếp tục thực hiện như vậy cho đến khi gặp một lệnh break. Chính vì thế, lệnh break được coi là lệnh quan trọng nhất khi dùng switch.
Các câu lệnh gắn với default sẽ được thực hiện nếu không có case nào thỏa mãn. Lệnh default là tùy chọn. Nếu không có lệnh default và không có case nào thỏa mãn, không có hành động nào được thực hiện. Có thể thay đổi thứ tự của case và default.
Xét một ví dụ.
Ví dụ 7.9:
#include <stdio.h>
#include <conio.h>
main ()
{
char ch;
printf("
Enter a lower cased alphabet (a - z): ");
scanf("%c", &ch);
if (ch < 'a' || ch > 'z')
printf("
Character not a lower cased alphabet");
else
switch (ch)
{
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
printf("
Character is a vowel");
break;
case 'z':
printf ("
Last Alphabet (z) was entered");
break;
default:
printf("
Character is a consonant");
break;
}
getch();
}
Chương trình trên nhận vào một kí tự ở dạng chữ thường và hiển thị thông báo kí tự đó là nguyên âm, là chữ z hay là một phụ âm. Nếu nó không phải ba loại ở trên, chương trình hiển thị thông báo “Character not a lower cased alphabet”:
Nên sử dụng lệnh break trong cả case cuối cùng hoặc default mặc dù về mặt logic là không cần thiết. Nhưng điều đó rất có ích nếu sau này chúng ta đưa thêm case vào cuối.
Dưới đây là một ví dụ, ở đó biểu thức của switch là một biến kiểu số nguyên và giá trị của mỗi case là một số nguyên.
Ví dụ 7.10:
/* Integer constants as case labels */
#include <stdio.h>
#include <conio.h>
main()
{
int basic;
printf("
Please enter your basic: ");
scanf("%d", &basic);
switch (basic)
{
case 200:
printf("
Bonus is dollar %d
", 50);
break;
case 300:
printf("
Bonus is dollar %d
", 125);
break;
case 400:
printf("
Bonus is dollar %d
", 140);
break;
case 500:
printf("
Bonus is dollar %d
", 175);
break;
default:
printf("
Invalid entry");
break;
}
getch();
}
Từ ví dụ trên, lệnh switch rất thuận lợi khi chúng ta muốn kiểm tra một biểu thức dựa trên một danh sách giá trị riêng biệt. Nhưng nó không thể dùng để kiểm tra một giá trị có nằm trong một miền nào đó hay không. Ví dụ, không thể dùng switch để kiểm tra xem basic có nằm trong khoảng từ 200 đến 300 hay không, để từ đó xác định mức tiền thưởng. Trong những trường hợp như vậy, ta phải sử dụng if-else.
Vòng lặp:
Vòng lặp là một đoạn mã lệnh trong chương trình được thực hiện lặp đi lặp lại cho đến khi thỏa mãn một điều kiện nào đó. Vòng lặp là một khái niệm cơ bản trong lập trình cấu trúc.
Trong C có các loại vòng lặp sau:
Vòng lặp
for
Vòng lặp
while
Vòng lặp
do…while
Ta sử dụng các
toán tử quan hệ
và
toán tử logic
trong các biểu thức điều kiện để điều khiển sự thực hiện của vòng lặp.
9.2 Vòng lặp ‘for’:
Cú pháp tổng quát của vòng lặp
for
như sau:
for( khởi tạo giá trị cho biến điều khiển;
biểu thức điều kiện;
biểu thức thay đổi giá trị của biến điều khiển)
{
Câu lệnh (các câu lệnh);
}
Khởi tạo giá trị cho biến điều khiển
là một câu lệnh gán giá trị ban đầu cho biến điều khiển trước khi thực hiện vòng lặp. Lệnh này chỉ được thực hiện duy nhất một lần.
Biểu thức điều kiện
là một biểu thức quan hệ, xác định điều kiện thoát cho vòng lặp.
Biểu thức thay đổi giá trị của biến điều khiển
xác định biến điều khiển sẽ bị thay đổi như thế nào sau mỗi lần vòng lặp được lặp lại (thường là tăng hoặc giảm giá trị của biến điều khiển). Ba phần trên được phân cách bởi dấu chấm phẩy. Câu lệnh trong thân vòng lặp có thể là một lệnh duy nhất (lệnh đơn) hoặc lệnh phức (nhiều lệnh).
Vòng lặp
for
sẽ tiếp tục được thực hiện chừng nào mà biểu thức điều kiện còn
đúng (true). Khi biểu thức điều kiện là
sai (false), chương trình sẽ thoát ra khỏi vòng lặp
for
.
Xem ví dụ sau:
Ví dụ 9.1:
/* Đây là chương trình minh họa vòng lặp for trong chương trình C*/
#include <stdio.h>
#include <conio.h>
main()
{
int count;
printf("\t This is a
");
for (count = 1; count <= 6; count++)
printf("
\t \t nice");
printf("
\t\t world.
");
getch();
}
Chúng ta sẽ xem xét kĩ đoạn vòng lặp
for
trong chương trình trên:
1.
Khởi tạo giá trị cho biến điều khiển:
c
ount = 1
.
Lệnh này được thực hiện duy nhất một lần khi vòng lặp bắt đầu được thực hiện, và biến count được đặt giá trị là 1.
2.
Biểu thức điều kiện:
count < = 6
.
Chương trình kiểm tra xem giá trị hiện tại của biến count có nhỏ hơn hay bằng 6 hay không. Nếu đúng, các câu lệnh trong thân vòng lặp sẽ được thực hiện.
3.
Thân của vòng lặp có duy nhất một lệnh
printf(“
\t \t nice”);
Câu lệnh này có thể đặt trong cặp dấu ngoặc nhọn
{}
cho dễ nhìn.
4.
Biểu thức thay đổi giá trị của biến điều khiển
count++
, tăng giá trị của biến
count
lên 1 cho lần lặp kế tiếp.
Các bước 2, 3, 4 được lặp lại cho đến khi biểu thức điều kiện là
sai. Vòng lặp trên sẽ được thực hiện 6 lần với giá trị của
count
thay đổi từ 1 đến 6. Vì vậy, từ
nice
xuất hiện 6 lần trên màn hình. Sau đó,
count
tăng lên 7. Do giá trị này lớn hơn 6, vòng lặp kết thúc và câu lệnh sau vòng lặp được thực hiện.
Chương trình sau in ra các số chẵn từ 1 đến 25.
Ví dụ 9.2:
#include <stdio.h>
#include <conio.h>
main()
{
int num;
printf("The even numbers from 1 to 25 are:
");
for (num=2; num <= 25; num+=2)
printf("%d
", num);
getch();
}
Vòng lặp
for
ở trên khởi tạo giá trị của biến nguyên
num
là 2 (để lấy một số chẵn) và tăng giá trị của nó lên 2 mỗi lẫn vòng lặp được lặp lại.
Trong các vòng lặp
for, biểu thức điều kiện luôn được kiểm tra ngay khi bắt đầu vòng lặp. Do đó các lệnh trong thân vòng lập sẽ không được thực hiện nếu ngay từ ban đầu điều kiện đó là
sai.
Ø
Toán tử
‘phẩy (comma)’:
Phần biểu thức trong toán tử
for
có thể được mở rộng để thêm vào các lệnh khởi tạo hay các lệnh thay đổi giá trị của biến. Cú pháp như sau:
biểu_thức1 , biểu_thức2
Các biểu thức trên được phân cách bởi toán tử
‘phẩy’
,
), và được thực hiện từ trái sang phải. Thứ tự của các biểu thức là quan trọng trong trường hợp giá trị của biểu thức thứ hai phụ thuộc vào giá trị của
biểu thức thứ nhất. Toán tử này có độ ưu tiên thấp nhất trong các toán tử của C.
Ví dụ dưới đây in ra một bảng các phép cộng với kết quả không đổi để minh họa khái niệm về toán tử phẩy rõ ràng hơn.
Ví dụ 9.3:
#include <stdio.h>
#include <conio.h>
main()
{
int i, j, max;
printf("Please enter the maxinum value
");
printf("for which a table can be printed: ");
scanf("%d", &max);
for (i = 0, j = max; i <= max; i++, j--)
printf("
%d + %d = %d", i, j, i + j);
getch();
Chú ý trong vòng lặp for, phần khởi tạo giá trị là:
i = 0, j = max
Khi vòng lặp bắt đầu chạy,
được gán giá trị 0 và
j
được gán giá trị của
max.
Phần thay đổi giá trị của biến điều khiển gồm hai biểu thức:
i++, j—
sau mỗi lần thực hiện thân vòng lặp,
được tăng lên 1 và
j
giảm đi 1. Tổng của hai biến đó luôn bằng
max
và được in ra màn hình:
Vòng lặp ‘for lồng nhau’:
Một vòng lặp
for
được gọi là lồng nhau khi nó nằm bên trong một vòng lặp
for
khác. Nó sẽ có dạng tương tự như sau:
for (i = 1; i < max1; i++)
{ ….
….
for (j = 0; j < max2 ; j++)
{
…..
}
….
}
Xem ví dụ sau:
Ví dụ 9.4:
#include <stdio.h>
#include <conio.h>
main()
{
int i, j, k;
i = 0;
printf("Enter no. of row: ");
scanf("%d", &i);
printf("
");
for (j = 0; j < i; j++)
{
printf("
");
for (k = 0; k <= j; k++) /*vòng lặp for bên trong*/
printf("*");
}
getch();
}
Chương trình trên sẽ hiển thị ký tự ‘*’ trên mỗi dòng và số ký tự ‘*’ trên mỗi dòng sẽ tăng thêm 1. Chương trình sẽ nhận vào số dòng, từ đó ký tự ‘*’ sẽ được in ra.
Vòng lặp
for
có thể được sử dụng mà không cần phải có đầy đủ các thành phần của nó.
Ví dụ:
…
for (num = 0; num != 255;)
{ printf(“Enter no. “);
scanf(“%d”,&num);
…
}
Đoạn mã trên sẽ yêu cầu nhập giá trị cho biến num cho đến khi nhập vào 255. Vòng lặp không có phần thay đổi giá trị của biến điều khiển. Vòng lặp sẽ kết thúc khi biến num có giá trị 255.
Tương tự, xét ví dụ sau:
.
.
printf("Enter value for checking :");
scanf("%d", &num);
for(; num < 100; )
Vòng lặp trên không có phần khởi tạo tham số và phần thay đổi giá trị của tham số.
Vòng lặp
for
khi không có bất kỳ thành phần nào sẽ là một vòng lặp vô tận
for ( ; ; )
printf(“This loop will go on and on and on…
”);
Tuy nhiên, lệnh
break
bên trong vòng lặp sẽ cho phép thoát khỏi vòng lặp.
…
for ( ; ; )
{ printf(“This will go on and on”);
i = getchar();
if (i == ‘X’ || i == ‘x’);
break;
}
…
Vòng lặp trên sẽ được thực hiện cho đến khi người dùng nhập vào
x
hoặc
X.
Vòng lặp
for
(hay vòng lặp bất kì) có thể không có bất kì lệnh nào trong phần thân của nó. Kĩ thuật này giúp tăng tính hiệu quả trong một vài giải thuật và để tạo ra độ trễ về mặt thời gian.
for (i = 0; i < xyz_value; i++);
là một ví dụ để tạo ra độ trễ về thời gian.
9.1.2 Vòng lặp
‘while’:
Cấu trúc lặp thứ hai trong C là vòng lặp
while. Cú pháp tổng quát như sau:
while (điều_kiện là đúng)
câu_lệnh;
Ở đó, câu_lệnh có thể là rỗng, hay một lệnh đơn, hay một khối lệnh. Nếu vòng lặp
while
chứa một tập các lệnh thì chúng phải được đặt trong cặp ngoặc xoắn
{}.
điều_kiện
có thể là biểu thức bất kỳ.
Vòng lặp sẽ được thực hiện lặp đi lặp lại khi điều kiện trên là
đúng (true). Chương trình sẽ chuyển đến thực hiện lệnh tiếp sau vòng lặp khi điều kiện trên là
sai (false).
Vòng lặp
for
có thể được sử dụng khi số lần thực hiện vòng lặp đã được xác định trước. Khi số lần lặp không biết trước, vòng lặp
while
có thể được sử dụng.
Ví dụ 9.5:
/* A simple program using the while loop*/
#include <stdio.h>
#include <conio.h>
main()
{
int count = 1;
while (count <= 10)
{ printf("
This is iteration %d
", count);
count++;
}
printf("
The loop is completed.
");
getch();
}
Đầu tiên chương trình gán giá trị của
count
là 1 ngay trong câu lệnh khai báo nó. Sau đó chương trình chuyển đến thực hiện lệnh
while. Phần biểu thức điều kiện được kiểm tra. Giá trị hiện tại của
count
là 1, nhỏ hơn 10. Kết quả kiểm tra điều kiện là
đúng (true)
nên các lệnh trong thân vòng lặp
while
được thực hiện. Các lệnh này được đặt trong cặp dấu ngoặc nhọn {}. Giá trị của biến
count
là 2 sau lần lặp đàu tiên. Sau đó biểu thức điều kiện lại được kiểm tra lần nữa. Quá trình này cứ lặp đi lặp lại cho đến khi giá trị của
count
lớn hơn 10. Khi vòng lặp kết thúc, lệnh
printf()
thứ hai được thực hiện.
Giống như vòng lặp for, vòng lặp while kiểm tra điều kiện ngay khi bắt đầu thực hiện vòng lặp. Do đó các lệnh trong thân vòng lặp sẽ không được thực hiện nếu ngay từ ban đầu điều kiện đó là sai
Biểu thức điều kiện trong vòng lặp có thể phức tạp tùy theo yêu cầu của bài toán. Các biến trong biểu thức điều kiện có thể bị thay đổi giá trị trong thân vòng lặp, nhưng cuối cùng đièu kiện đó phải
sai (false)
nếu không vòng lặp sẽ không bao giờ kết thúc.
Sau đây là ví dụ về một vòng lặp
while
vô hạn.
Ví dụ 9.6:
#include <stdio.h>
#include <conio.h>
main()
{
int count = 0;
while (count < 100)
{
printf("This goes on forever, HELP!!!
");
count += 10;
printf("\t%d", count);
count -= 10;
printf("\t%d", count);
printf("\Ctrl - C will help");
}
getch();
}
Ở trên,
count
luôn luôn bằng 0, nghĩa là luôn nhỏ hơn 100 và vì vậy biểu thức luôn luôn trả về giá trị
true. Nên vòng lặp không bao giờ kết thúc:
Nếu có hơn một điều kiện được kiểm tra để kết thúc vòng lặp, vòng lặp sẽ kết thúc khi có ít nhất một điều kiện trong các điều kiện đó là
false. Ví dụ sau sẽ minh họa điều này.
#include <stdio.h>
main()
{ int i, j;
i = 0;
j = 10;
while (i < 100 && j > 5)
{ ...
i++;
j -= 2;
}
...
}
Vòng lặp này sẽ thực hiện 3 lần, lần lặp thứ nhất
j
sẽ là 10, lần lặp kế tiếp
j
bằng 8 và lần lặp thứ ba
j
sẽ bằng 6. Khi đó
vẫn nhỏ hơn 100 (i bằng 3),
j
nhận giá trị 4 và điều kiện j > 5 trở thành
false, vì vậy vòng lặp kết thúc.
Chúng ta hãy viết một chương trình nhận dữ liệu từ bàn phím và in ra màn hình. Chương trình kết thúc khi bạn nhấn phím
^Z (Ctrl + Z).
Vòng lặp
‘do ... while’:
Vòng lặp
do ... while
còn được gọi là vòng lặp
do
trong C. Không giống như vòng lặp
for
và
while, vòng lặp này kiểm tra điều kiện tại cuối vòng lặp. Điều này có nghĩa là vòng lặp
do ... while
sẽ được thực hiện ít nhất một lần, ngay cả khi điều kiện là sai (false)
ở lần chạy đầu tiên.
Cú pháp tổng quát của vòng lặp
do ... while
như sau:
do{
câu_lệnh;
} while (điều_kiện);
Cặp dấu ngoặc
{}
là không cần thiết khi chỉ có một câu lệnh hiện diện trong vòng lặp, nhưng việc sử dụng dấu ngoặc {} là một thói quen tốt. Vòng lặp
do ... while
lặp đến khi
điều_kiện
mang giá trị
false. Trong vòng lặp
do ... while,
câu_lệnh
(khối các câu lệnh) sẽ được thực thi trước, và sau đó
điều_kiện
được kiểm tra. Nếu điều kiện là true, chương trình sẽ quay lại thực hiện lệnh do. Nếu điều kiện là false, chương trình chuyển đến thực hiện lệnh nằm sau vòng lặp.
Xét chương trình sau:
Ví dụ 9.8:
/* accept only int value */
#include <stdio.h>
#include <conio.h>
main()
{
int num1, num2;
num2 = 0;
do{
printf("
Enter a number: ");
scanf("%d",&num1);
printf("No. is %d", num1);
num2++;
}while (num1 != 0);
printf("
The total numbers entered were %d",--num2);
/* num2 is decremented before printing because count for last integer (0) is not to be considered */
getch();
Đoạn chương trình trên sẽ nhận các số nguyên và hiển thị chúng cho đến khi một số 0 được nhập vào. Và sau đó chương trình sẽ thoát khỏi vòng lặp
do ... while
và số lượng các số nguyên đã được nhập vào.
Cũng giống như vòng lặp
for, các vòng lặp
while
và
do ... while
cũng có thể được lồng vào nhau. Hãy xem một ví dụ được đưa ra dưới đây.
Ví dụ 9.9:
#include <stdio.h>
#include <conio.h>
main()
{
int x;
char i, ans;
i = ' ';
do{
x = 0;
ans = 'y';
printf("
Enter sequence of character: ");
do{
i = getchar();
x++;
}while (i != '
');
i = ' ';
printf("
Number of characters entered is:%d", --x);
printf("
More sequences (Y/N)?");
ans = getch();
}while (ans == 'Y' || ans == 'y');
Ch
ươ
ng trình trên yêu cầu ng
ư
ời dùng nhập vào một chuỗi kí tự cho đến khi nhấn phím enter (vòng lặp while bên trong). Khi đó, ch
ươ
ng trình thoát khỏi vòng lặp
do…while
bên trong. Sau đó ch
ươ
ng trình hỏi ng
ư
ời dùng có muốn nhập tiếp nữa hay thôi. Nếu người dùng nhấn phím ‘y’ hoặc ‘Y’, điều kiện cho vòng while bên ngoài là
true
và chương trình nhắc người dùng nhập vào chuỗi ký tự khác. Chương trình cứ tiếp tục cho đến khi người dùng nhấn bất kỳ một phím nào khác với phím ‘y’ hoặc ‘Y’. Và chương trình kết thúc.
Các lệnh nhẩy:
C có bốn câu lệnh thực hiện sự rẽ nhánh không điều kiện:
return
,
,
break
,
và
continue
. Sự rẽ nhánh không điều kiện nghĩa là sự chuyển điều khiển từ một điểm đến một lệnh xác định. Trong các lệnh chuyển điều khiển trên,
return
và
có thể dùng bất kỳ vị trí nào trong chương trình, trong khi lệnh
break
và
continue
được sử dụng kết hợp với các câu lệnh vòng lặp.
ệnh
‘return’:
Lệnh
return
dùng để quay lại vị trí gọi hàm sau khi các lệnh trong hàm đó được thực thi xong. Trong lệnh
return
có thể có một giá trị gắn với nó, giá trị đó sẽ được trả về cho chương trình. Cú pháp tổng quát của câu lệnh
return
như sau:
return biểu_thức;
Biểu_thức là một tùy chọn (không bắt buộc). Có thể có hơn một lệnh
return
được sử dụng trong một hàm. Tuy nhiên, hàm sẽ quay trở về vị trí gọi hàm khi gặp lệnh
return
đầu tiên. Lệnh
return
sẽ được làm rõ hơn sau khi học về hàm.
9.2.2 Lệnh
‘goto’:
C là một ngôn ngữ lập trình có cấu trúc, tuy vậy nó vẫn chứa một số câu lệnh làm phá vớ cấu trúc của chương trình:
Lệnh
cho phép chuyển quyền điều khiển tới một lệnh bất kì nằm trong cùng khối lệnh hay khác khối lệnh bên trong hàm đó. Vì vậy nó vi phạm các qui tắc của một ngôn ngữ lập trình có cấu trúc.
Cú pháp tổng quát của một câu lệnh
là:
goto label
Trong đó
label
là một định danh phải xuất hiện như là tiền tố (prefix) của một câu lệnh khác trong cùng một hàm.
Dấu chấm phẩy (
) sau
label
đánh dấu sự kết thúc của lệnh
goto. Các lệnh
làm cho chương trình khó đọc. Chúng làm giảm độ tin cậy và làm cho chương trình khó bảo trì. Tuy nhiên, chúng vẫn được dùng vì chúng cung cấp các cách thức hữu dụng để thoát ra khỏi những vòng lặp lồng nhau quá nhiều mức. Xét đoạn mã sau:
for (...) {
for(...) {
for(...) {
while(...) {
if (...) goto error1;
...
}
}
}
}
error1: printf(“Error !!!”);
Như đã thấy,
label
xuất hiện như là một tiền tố của một câu lệnh khác trong chương trình:
label: câu_lệnh
hoặc
label:
{
Chuỗi các câu lệnh;
}
Ví dụ 9.10:
#include <stdio.h>
#include <conio.h>
main()
{
int num;
label1:
printf("
Enter a number (1): ");
scanf("%d", &num);
if (num == 1)
goto Test;
else
goto label1;
Test:
printf("All done...");
getch();
Câu lệnh
break
có hai cách dùng. Nó có thể được sử dụng để kết thúc một
case
trong câu lệnh
switch
hoặc để kết thúc ngay một vòng lặp, mà không cần kiểm tra điều kiện vòng lặp.
Khi chương trình gặp lệnh
break
trong một vòng lặp, ngay lập tức vòng lặp được kết thúc và quyền điều khiển chương trình được chuyển đến câu lệnh theo sau vòng lặp.
#include <stdio.h>
#include <conio.h>
main()
{
int count1, count2;
for (count1= 1,count2 = 0; count1 <= 100; count1++)
{ printf("Enter %d Count2: ",count1);
scanf("%d",&count2);
if (count2==100) break;
}
getch();
}
Trong đoạn mã lệnh trên, người dùng có thể nhập giá trị 100 cho
count2. Tuy nhiên, nếu 100 được nhập vào, vòng lặp kết thúc và điều khiển được chuyển đến câu lệnh kế tiếp.
Một điểm khác cần lưu ý là việc sử dụng câu lênh
break
trong các lệnh lặp lồng nhau. Khi chương trình thực thi đến một lệnh
break
nằm trong một vòng lặp
for
lồng bên trong một vòng lặp
for
khác, quyền điều khiển được chuyển trở về vòng lặp
for
bên ngoài.
9.2.4 Lệnh
‘continue’:
Lệnh
continue
kết thúc lần lặp hiện hành và bắt đầu lần lặp kế tiếp. Khi gặp lệnh này trong chương trình, các câu lệnh còn lại trong thân của vòng lặp được bỏ qua và quyền điều khiển được chuyển đến bước đầu của vòng lặp trong lần lặp kế tiếp.
Trong trường hợp vòng lặp
for,
continue
thực hiện biểu thức thay đổi giá trị của biến điều khiển và sau đó kiểm tra biểu thức điều kiện. Trong trường hợp của lệnh
while
và
do…while, quyền điều khiển chương trình được chuyển đến biểu thức kiểm tra điều kiện. Ví dụ:
Ví dụ 9.12:
#include <stdio.h>
#include <
con
io.h>
main()
{
int num;
for (num = 1; num <= 100; num++)
{
if (num % 9 == 0) continue;
printf("%d\t", num);
}
getch();
}
Chương trình trên in ra tất cả các số từ 1 đến 100 không chia hết cho 9.
Hàm
exit()
là một hàm trong thư viện chuẩn của C. Nó làm việc tương tự như một lệnh chuyển quyền điều khiển, điểm khác nhau chính là các lệnh chuyển quyền điều khiển thường được sử dụng để thoát khỏi một vòng lặp, trong khi
exit()
được sử dụng để thoát khỏi chương trình. Hàm này sẽ ngay lập tức kết thúc chương trình và quyền điều khiển được trả về cho hệ điều hành. Hàm
exit()
thường được dùng để kiểm tra một điều kiện bắt buộc cho việc thực thi của một chương trình có được thoả mãn hay không. Cú pháp tổng quát của hàm
exit()
như sau:
exit (int mã_trả_về);
ở đó
mã_trả_về
là một tùy chọn. Số 0 thường được dùng như một
mã_trả_về
để xác định sự kết thúc chương trình một cách bình thường. Những giá trị khác xác định một vài loại lỗi.
Một mảng được “đối xử” khác với một biến trong C. Thậm chí hai mảng có cùng kiểu và kích thước cũng không thể tương đương nhau. Hơn nữa, không thể gán một mảng trực tiếp cho một mảng khác. Thay vì thế, mỗi phần tử mảng phải được gán riêng lẻ tương ứng với từng phần tử của mảng khác. Các giá trị không thể được gán cho toàn bộ một mảng, ngoại trừ tại thời điểm khởi tạo. Tuy nhiên, từng phần tử không chỉ có thể được gán trị mà còn có thể được so sánh.
int player1[11], player2[11];
for (i = 0; i < 11; i++)
player1[i] = player2[i];
Tương tự, cũng có thể có kết quả như vậy bằng việc sử dụng các lệnh gán riêng lẻ như sau:
player1[0] = player2[0];
player1[1] = player2[1];
...
player1[10] = player2[10];
Cấu trúc for là cách lý tưởng để thao tác các mảng.
Ví dụ 11.1:
/* Program demonstrates a single dimensional array */
#include <stdio.h>
#include <conio.h>
main()
{
int num[5];
num[0] = 10;
num[1] = 70;
num[2] = 60;
num[3] = 40;
num[4] = 50;
for (i = 0; i < 5; i++)
printf("
Number at [%d] is %d", i, num[i]);
getch();
}
Một con trỏ là một biến, nó chứa địa chỉ vùng nhớ của một biến khác, chứ không lưu trữ giá trị của biến đó. Nếu một biến chứa địa chỉ của một biến khác, thì biến này được gọi là con trỏ đến biến thứ hai kia. Một con trỏ cung cấp phương thức gián tiếp để truy xuất giá trị của các phần tử dữ liệu. Xét hai biến var1 và var2, var1 có giá trị 500 và được lưu tại địa chỉ 1000 trong bộ nhớ. Nếu var2 được khai báo như là một con trỏ tới biến var1, sự biểu diễn sẽ như sau:
Các con trỏ có thể trỏ đến các biến của các kiểu dữ liệu cơ sở như int, char, hay double hoặc dữ liệu có cấu trúc như mảng.
13.1.2 Tại sao con trỏ được dùng?
Con trỏ có thể được sử dụng trong một số trường hợp sau:
Ø
Để trả về nhiều hơn một giá trị từ một hàm
Ø
Thuận tiện hơn trong việc truyền các mảng và chuỗi từ một hàm đến một hàm khác
Ø
Sử dụng con trỏ để làm việc với các phần tử của mảng thay vì truy xuất trực tiếp vào các phần tử này
Ø
Để cấp phát bộ nhớ động và truy xuất vào vùng nhớ được cấp phát này (dynamic memory allocation)
Nếu một biến được sử dụng như một con trỏ, nó phải được khai báo trước. Câu lệnh khai báo con trỏ bao gồm một
kiểu dữ liệu cơ bản
, một dấu
*
, và một
tên biến
. Cú pháp tổng quát để khai báo một biến con trỏ như sau:
type *name;
Ở đó type là một kiểu dữ liệu hợp lệ bất kỳ, và name là tên của biến con trỏ. Câu lệnh khai báo trên nói với trình biên dịch là name được sử dụng để lưu địa chỉ của một biến có kiểu dữ liệu type. Trong câu lệnh khai báo, * xác định rằng một biến con trỏ đang được khai báo.
Trong ví dụ của var1 và var2 ỏ trên, vì var2 là một con trỏ giữ địa chỉ của biến var1 có kiểu int, nó sẽ được khai báo như sau:
int *var2;
Bây giờ, var2 có thể được sử dụng trong một chương trình để trực tiếp truy xuất giá trị của var1. Nhớ rằng, var2 không phải có kiểu dữ liệu int nhưng nó là một con trỏ trỏ đến một biến có kiểu dữ liệu int.
Kiểu dữ liệu cơ sở của con trỏ xác định kiểu của biến mà con trỏ trỏ đến. Về mặt kỹ thuật, một con trỏ có kiểu bất kỳ có thể trỏ đến bất kỳ vị trí nào trong bộ nhớ. Tuy nhiên, tất cả các phép toán số học trên con trỏ đều có liên quan đến kiểu cơ sở của nó, vì vậy khai báo kiểu dữ liệu của con trỏ một cách rõ ràng là điều rất quan trọng.
13.3 Các toán tử con trỏ
Có hai toán tử đặc biệt được dùng với con trỏ:
*
và
&
. Toán tử
&
là một toán tử một ngôi và nó trả về địa chỉ của toán hạng. Ví dụ:
var2 = &var1;
lấy địa chỉ vùng nhớ của biến var1 gán cho var2. Địa chỉ này là vị trí ô nhớ bên trong máy tính của biến var1 và nó không làm gì với giá trị của var1. Toán tử & có thể hiểu là trả về “
địa chỉ của
”. Vì vậy, phép gán trên có nghĩa là “var2 nhận địa chỉ của var1”. Trở lại, giá trị của var1 là 500 và nó dùng vùng nhớ 1000 để lưu giá trị này. Sau phép gán trên, var2 sẽ có giá trị 1000.
Toán tử thứ hai, toán tử
*
, là phần bổ sung của toán tử
&
. Nó là một toán tử một ngôi và trả về giá trị chứa trong vùng nhớ được trỏ bởi giá trị của biến con trỏ.
Xem ví dụ trước, ở đó var1 có giá trị 500 và được lưu trong vùng nhớ 1000, sau câu lệnh
var2 = &var1;
var2
chứa giá trị 1000, và sau lệnh gán:
temp = *var2;
sẽ chứa 500, là giá trị của biến mà var2 trỏ đến. Toán tử
*
có thể được hiểu là: “giá trị của”.
Cả hai toán tử
*
và
&
có độ ưu tiên cao hơn tất cả các toán tử toán học ngoại trừ toán tử lấy giá trị âm. Chúng có cùng độ ưu tiên với toán tử lấy giá trị âm (
-
).
Chương trình dưới đây in ra giá trị của một biến kiểu số nguyên, địa chỉ của nó được lưu trong một biến con trỏ, và chương trình cũng in ra địa chỉ của biến con trỏ.
#include <stdio.h>
#include <conio.h>
main()
{
int var = 500, *ptr_var;
//var is declared as an integer and ptr_var as a pointer
pointing to an integer
ptr_var = &var; //stores address of var in ptr_var
//Prints value of variable (var) and address where var is
stored
printf("The value %d is stored at address: %u", var, &var);
//Prints value stored in ptr variable (ptr_var) and address
where ptr_var is stored
printf("
The value %u is stored at address: %u",
ptr_var, &ptr_var);
//Prints value of variable (var) and address where
var is stored, using pointer to variable
printf("
The value %d is stored at address: %u",
*ptr_var,ptr_var);
getch();
Trong ví dụ trên, ptr_var chứa địa chỉ 2686788, là địa chỉ vùng nhớ lưu trữ giá trị của var. Nội dung ô nhớ 2686788 này có thể lấy được bằng cách sử dụng toán tử *, như *ptr_var. Lúc này *ptr_var tương ứng với giá trị 500, là giá trị của var. Bởi vì ptr_var cũng là một biến, nên địa chỉ của nó có thể được in ra bằng toán tử &. Trong ví dụ trên, ptr_var được lưu tại địa chỉ 2686784. Mã quy cách %u chỉ định cách in giá trị các tham số theo kiểu số nguyên không dấu (unsigned int).
Chú ý rằng hai câu lệnh sau cho ra cùng một kết quả.
printf(“The value is %d”, var);
printf(“The value is %d”, *(&var));
Gán giá trị cho con trỏ
Các giá trị có thể được gán cho biến con trỏ thông qua toán tử &. Câu lệnh gán sẽ là:
ptr_var = &var;
Lúc này địa chỉ của var được lưu trong biến ptr_var. Cũng có thể gán giá trị cho con trỏ thông qua một biến con trỏ khác trỏ đến một phần tử dữ liệu có cùng kiểu.
ptr_var = &var;
ptr_var2 = ptr_var;
Giá trị NULL cũng có thể được gán đến một con trỏ bằng số 0 như sau:
ptr_var = 0;
Các biến cũng có thể được gán giá trị thông qua con trỏ của chúng.
*ptr_var = 10;
sẽ gán 10 cho biến var nếu ptr_var trỏ đến var.
Nói chung, các biểu thức có chứa con trỏ cũng theo cùng qui luật như các biểu thức khác trong C. Điều quan trọng cần chú ý phải gán giá trị cho biến con trỏ trước khi sử dụng chúng; nếu không chúng có thể trỏ đến một giá trị không xác định nào đó.
Phép toán số học con trỏ
Chỉ phép cộng và trừ là các toán tử có thể thực hiện trên các con trỏ. Ví dụ sau minh họa điều này:
int var, *ptr_var;
ptr_var = &var;
var = 500;
Trong ví dụ trên, chúng ta giả sử rằng var được lưu tại địa chỉ 1000. Sau đó, giá trị 1000 sẽ được lưu vào ptr_var. Vì kiểu số nguyên chiếm 2 bytes, nên sau biểu thức:
ptr_var++ ;
ptr_var sẽ chứa 1002 mà KHÔNG phải là 1001. Điều này có nghĩa là ptr_var bây giờ trỏ đến một số nguyên được lưu tại địa chỉ 1002. Mỗi khi ptr_var được tăng lên, nó sẽ trỏ đến số nguyên kế tiếp và bởi vì các số nguyên là 2 bytes, ptr_var sẽ được tăng trị là 2. Điều này cũng tương tự với phép toán giảm trị.
Đây là một vài ví dụ:
++ptr_var or ptr_var++
Trỏ đến số nguyên kế tiếp đứng sau var
--ptr_var or ptr_var--
ptr_var + i
Trỏ đến số nguyên thứ i sau var
ptr_var - i
++*ptr_var or (*ptr_var)++
Sẽ tăng trị var bởi 1
*ptr_var++
Sẽ tác động đến giá trị của số nguyên kế tiếp sau var
Mỗi khi một con trỏ được tăng giá trị, nó sẽ trỏ đến ô nhớ của phần tử kế tiếp. Mỗi khi nó được giảm giá trị, nó sẽ trỏ đến vị trí của phần tử đứng trước nó. Với những con trỏ trỏ tới các ký tự, nó xuất hiện bình thường, bởi vì mỗi ký tự chiếm 1 byte. Tuy nhiên, tất cả những con trỏ khác sẽ tăng hoặc giảm trị tuỳ thuộc vào độ dài kiểu dữ liệu mà chúng trỏ tới.
Như đã thấy trong các ví dụ trên, ngoài các toán tử tăng trị và giảm trị, các số nguyên cũng có thể được cộng vào và trừ ra với con trỏ. Ngoài phép cộng và trừ một con trỏ với một số nguyên, không có một phép toán nào khác có thể thực hiện được trên các con trỏ. Nói rõ hơn, các con trỏ không thể được nhân hoặc chia. Cũng như kiểu float và double không thể được cộng hoặc trừ với con trỏ.
So sánh con trỏ.
Hai con trỏ có thể được so sánh trong một biểu thức quan hệ. Tuy nhiên, điều này chỉ có thể nếu cả hai biến này đều trỏ đến các biến có cùng kiểu dữ liệu. ptr_a và ptr_b là hai biến con trỏ trỏ đến các phần tử dữ liệu a và b. Trong trường hợp này, các phép so sánh sau đây là có thể thực hiện:
ptr_a < ptr_b
Trả về giá trị true nếu a được lưu trữ ở vị trí trước b
ptr_a > ptr_b
Trả về giá trị true nếu a được lưu trữ ở vị trí sau b
ptr_a <= ptr_b
Trả về giá trị true nếu a được lưu trữ ở vị trí trước b hoặc ptr_a và ptr_b trỏ đến cùng một vị trí
ptr_a == ptr_b
Trả về giá trị true nếu cả hai con trỏ ptr_a và ptr_b trỏ đến cùng một phần tử dữ liệu.
ptr_a != ptr_b
Trả về giá trị true nếu cả hai con trỏ ptr_a và ptr_b trỏ đến các phần tử dữ liệu khác nhau nhưng có cùng kiểu dữ liệu.
ptr_a == NULL
Trả về giá trị true nếu ptr_a được gán giá trị NULL (0)
Tương tự, nếu ptr_begin và ptr_end trỏ đến các phần tử của cùng một mảng thì:
ptr_end - ptr_begin
sẽ trả về số bytes cách biệt giữ hai vị trí mà chúng trỏ đến.
Tên của một mảng thật ra là một con trỏ trỏ đến phần tử đầu tiên của mảng đó. Vì vậy, nếu ary là một mảng một chiều, thì địa chỉ của phần tử đầu tiên trong mảng có thể được biểu diễn là &ary[0] hoặc đơn giản chỉ là ary. Tương tự, địa chỉ của phần tử mảng thứ hai có thể được viết như &ary[1] hoặc ary+1,... Tổng quát, địa chỉ của phần tử mảng thứ (i + 1) có thể được biểu diễn là &ary[i] hay (ary+i). Như vậy, địa chỉ của một phần tử mảng bất kỳ có thể được biểu diễn theo hai cách:
Ø
Sử dụng ký hiệu & trước một phần tử mảng
Ø
Sử dụng một biểu thức trong đó chỉ số được cộng vào tên của mảng.
Ghi nhớ rằng trong biểu thức (ary + i), ary tượng trưng cho một địa chỉ, trong khi i biểu diễn số nguyên. Hơn thế nữa, ary là tên của một mảng mà các phần tử có thể là cả kiểu số nguyên, ký tự, số thập phân,… (dĩ nhiên, tất cả các phần tử của mảng phải có cùng kiểu dữ liệu). Vì vậy, biểu thức ở trên không chỉ là một phép cộng; nó thật ra là xác định một địa chỉ, một số xác định của các ô nhớ . Biểu thức (ary + i) là một sự trình bày cho một địa chỉ chứ không phải là một biểu thức toán học.
Như đã nói ở trước, số lượng ô nhớ được kết hợp với một mảng sẽ tùy thuộc vào kiểu dữ liệu của mảng cũng như là kiến trúc của máy tính. Tuy nhiên, người lập trình chỉ có thể xác định địa chỉ của phần tử mảng đầu tiên, đó là tên của mảng (trong trường hơp này là ary) và số các phần tử tiếp sau phần tử đầu tiên, đó là, một giá trị chỉ số. Giá trị của i đôi khi được xem như là một độ dời khi được dùng theo cách này.
Các biểu thức &ary[i] và (ary+i) biểu diễn địa chỉ phần tử thứ i của ary, và như vậy một cách logic là cả ary[i] và *(ary + i) đều biểu diễn nội dung của địa chỉ đó, nghĩa là, giá trị của phần tử thứ i trong mảng ary. Cả hai cách có thể thay thế cho nhau và được sử dụng trong bất kỳ ứng dụng nào khi người lập trình mong muốn.
Chương trình sau đây biểu diễn mối quan hệ giữa các phần tử mảng và địa chỉ của chúng.
#include<stdio.h>
#include<conio.h>
main()
{
static int ary[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (i = 0; i < 10; i ++)
{
printf("
i=%d, ary[i]=%d, *(ary+i)= %d", i,
ary[i], *(ary + i));
printf("&ary[i]= %X, ary+i=%X", &ary[i], ary + i);
//%X gives unsigned hexadecimal
}
getch();
}
Chương trình trên định nghĩa mảng một chiều ary, có 10 phần tử kiểu số nguyên, các phần tử mảng được gán giá trị tương ứng là 1, 2, ..10. Vòng lặp for được dùng để hiển thị giá trị và địa chỉ tương ứng của mỗi phần tử mảng. Chú ý rằng, giá trị của mỗi phần tử được xác định theo hai cách khác nhau, ary[i] và *(ary + i), nhằm minh họa sự tương đương của chúng. Tương tự, địa chỉ của mỗi phần tử mảng cũng được hiển thị theo hai cách.
Kết quả này trình bày rõ ràng sự khác nhau giữa ary[i] - biểu diễn giá trị của phần tử thứ i trong mảng, và &ary[i] - biểu diễn địa chỉ của nó.
Khi gán một giá trị cho một phần tử mảng như ary[i], vế trái của lệnh gán có thể được viết là ary[i] hoặc *(ary + i). Vì vậy, một giá trị có thể được gán trực tiếp đến một phần tử mảng hoặc nó có thể được gán đến vùng nhớ mà địa chỉ của nó là phần tử mảng. Đôi khi cần thiết phải gán một địa chỉ đến một định danh. Trong những trường hợp như vậy, một con trỏ phải xuất hiện trong vế trái của câu lệnh gán. Không thể gán một địa chỉ tùy ý cho một tên mảng hoặc một phần tử của mảng. Vì vậy, các biểu thức như ary, (ary + i) và &ary[i] không thể xuất hiện trong vế trái của một câu lệnh gán. Hơn thế nữa, địa chỉ của một mảng không thể thay đổi một cách tùy ý, vì thế các biểu thức như ary++ là không được phép. Lý do là vì: ary là địa chỉ của mảng ary. Khi mảng được khai báo, bộ liên kết đã quyết định mảng được bắt đầu ở đâu, ví dụ, bắt đầu ở địa chỉ 1002. Một khi địa chỉ này được đưa ra, mảng sẽ ở đó. Việc cố gắng tăng địa chỉ này lên là điều vô nghĩa, giống như khi nói
x = 5++;
Bởi vì hằng không thể được tăng giá trị, trình biên dịch sẽ đưa ra thông báo lỗi.
Trong trường hợp mảng ary, ary cũng được xem như là một hằng con trỏ. Nhớ rằng, (ary + 1) không di chuyển mảng ary đến vị trí (ary + 1), nó chỉ trỏ đến vị trí đó, trong khi ary++ cố găng dời ary sang 1 vị trí.
Địa chỉ của một phần tử không thể được gán cho một phần tử mảng khác, mặc dù giá trị của một phần tử mảng có thể được gán cho một phần tử khác thông qua con trỏ.
&ary[2] = &ary[3]; /* không cho phép*/
ary[2] = ary[3]; /* cho phép*/
Nhớ lại rằng trong hàm scanf(), tên các tham biến kiểu dữ liệu cơ bản phải đặt sau dấu (&), trong khi tên tham biến mảng là ngoại lệ. Điều này cũng dễ hiểu. Vì scanf() đòi hỏi địa chỉ bộ nhớ của từng biến dữ liệu trong danh sách tham số, trong khi toán tử & trả về địa chỉ bộ nhớ của biến, do đó trước tên biến phải có dấu &. Tuy nhiên dấu & không được yêu cầu đối với tên mảng, bởi vì tên mảng tự biểu diễn địa chỉ của nó.Tuy nhiên, nếu một phần tử trong mảng được đọc, dấu & cần phải sử dụng.
scanf(“%d”, *ary) /* đối với phần tử đầu tiên */
scanf(“%d”, &ary[2]) /* đối với phần tử bất kỳ */
13.4.1 Con trỏ và mảng nhiều chiều
Một mảng nhiều chiều cũng có thể được biểu diễn dưới dạng con trỏ của mảng một chiều (tên của mảng) và một độ dời (chỉ số). Thực hiện được điều này là bởi vì một mảng nhiều chiều là một tập hợp của các mảng một chiều.Ví dụ, một mảng hai chiều có thể được định nghĩa như là một con trỏ đến một nhóm các mảng một chiều kế tiếp nhau. Cú pháp báo mảng hai chiều có thể viết như sau:
data_type (*ptr_var)[expr 2];
thay vì
data_type array[expr 1][expr 2];
Khái niệm này có thể được tổng quát hóa cho các mảng nhiều chiều, đó là,
data_type (*ptr_var)[exp 2] .... [exp N];
thay vì
data_type array[exp 1][exp 2] ... [exp N];
Trong các khai báo trên, data_type là kiểu dữ liệu của mảng, ptr_var là tên của biến con trỏ, array là tên mảng, và exp 1, exp 2, exp 3, ... exp N là các giá trị nguyên dương xác định số lượng tối đa các phần tử mảng được kết hợp với mỗi chỉ số.
Chú ý dấu ngoặc () bao quanh tên mảng và dấu * phía trước tên mảng trong cách khai báo theo dạng con trỏ. Cặp dấu ngoặc () là không thể thiếu, ngược lại cú pháp khai báo sẽ khai báo một mảng của các con trỏ chứ không phải một con trỏ của một nhóm các mảng.
Ví dụ, nếu ary là một mảng hai chiều có 10 dòng và 20 cột, nó có thể được khai báo như sau:
int (*ary)[20];
thay vì
int ary[10][20];
Trong sự khai báo thứ nhất, ary được định nghĩa là một con trỏ trỏ tới một nhóm các mảng một chiều liên tiếp nhau, mỗi mảng có 20 phần tử kiểu số nguyên. Vì vậy, ary trỏ đến phần tử đầu tiên của mảng, đó là dòng đầu tiên (dòng 0) của mảng hai chiều. Tương tự, (ary + 1) trỏ đến dòng thứ hai của mảng hai chiều, ...
Một mảng thập phân ba chiều fl_ary có thể được khai báo như:
float (*fl_ary)[20][30];
thay vì
float fl_ary[10][20][30];
Trong khai báo đầu, fl_ary được định nghĩa như là một nhóm các mảng thập phân hai chiều có kích thước 20 x 30 liên tiếp nhau. Vì vậy, fl_ary trỏ đến mảng 20 x 30 đầu tiên, (fl_ary + 1) trỏ đến mảng 20 x 30 thứ hai,...
Trong mảng hai chiều ary, phần tử tại dòng 4 và cột 9 có thể được truy xuất sử dụng câu lệnh:
ary[3][8];
hoặc
*(*(ary + 3) + 8);
Cách thứ nhất là cách thường được dùng. Trong cách thứ hai, (ary + 3) là một con trỏ trỏ đến dòng thứ 4. Vì vậy, đối tượng của con trỏ này, *(ary + 3), tham chiếu đến toàn bộ dòng. Vì dòng 3 là một mảng một chiều, *(ary + 3) là một con trỏ trỏ đến phần tử đầu tiên trong dòng 3, sau đó 8 được cộng vào con trỏ. Vì vậy, *(*(ary + 3) + 8) là một con trỏ trỏ đến phần tử 8 (phần tử thứ 9) trong dòng thứ 4. Vì vậy đối tượng của con trỏ này, *(*(ary + 3) + 8), tham chiếu đến tham chiếu đến phần tử trong cột thứ 9 của dòng thứ 4, đó là ary [3][8].
Có nhiều cách thức để định nghĩa mảng, và có nhiều cách để xử lý các phần tử mảng. Lựa chọn cách thức nào tùy thuộc vào người dùng. Tuy nhiên, trong các ứng dụng có các mảng dạng số, định nghĩa mảng theo cách thông thường sẽ dễ dàng hơn.
Con trỏ và chuỗi
Chuỗi đơn giản chỉ là một mảng một chiều có kiểu ký tự. Mảng và con trỏ có mối liên hệ mật thiết, và như vậy, một cách tự nhiên chuỗi cũng sẽ có mối liên hệ mật thiết với con trỏ. Xem trường hợp hàm strchr(). Hàm này nhận các tham số là một chuỗi và một ký tự để tìm kiếm ký tự đó trong mảng, nghĩa là,
ptr_str = strchr(strl, 'a');
biến con trỏ ptr_str sẽ được gán địa chỉ của ký tự ‘a’ đầu tiên xuất hiện trong chuỗi str. Đây không phải là vị trí trong chuỗi, từ 0 đến cuối chuỗi, mà là địa chỉ, từ địa chỉ bắt đầu chuỗi đến địa chỉ kết thúc của chuỗi.
Chương trình sau sử dụng hàm strchr(), đây là chương trình cho phép người dùng nhập vào một chuỗi và một ký tự để tìm kiếm. Chương trình in ra địa chỉ bắt đầu của chuỗi, địa chỉ của ký tự, và vị trí tương đối của ký tự trong chuỗi (0 là vị trí của ký tự đầu tiên, 1 là vị trí của ký tự thứ hai,...). Vị trí tương đối này là hiệu số giữa hai địa chỉ, địa chỉ bắt đầu của chuỗi và địa chỉ nơi mà ký tự cần tìm đầu tiên xuất hiện.
#include <stdio.h>
#include <conio.h>
#include <string.h>
main ()
{
char a, str[81], *ptr;
printf("
Enter a sentence:");
printf("
Enter character to search for:");
a = getchar();
ptr = strchr(str, a);
/* return pointer to char*/
printf("
String starts at address: %u", str);
printf("
First occurrence of the character is at address: %u", ptr);
printf("
Position of first occurrence (starting from 0)is: %d", ptr-str);
getch();
}
Trong câu lệnh khai báo, biến con trỏ ptr được thiết đặt để chứa địa chỉ trả về từ hàm strchr(), vì vậy đây là một địa chỉ của một ký tự (ptr có kiểu char).
Để sử dụng hàm strchr(), thư viện string.h phải được khai báo.
13.5 Cấp phát bộ nhớ
Cho đến thời điểm này thì chúng ta đã biết là tên của một mảng thật ra là một con trỏ trỏ tới phần tử đầu tiên của mảng. Hơn nữa, ngoài cách định nghĩa một mảng thông thường có thể định nghĩa một mảng như là một biến con trỏ. Tuy nhiên, nếu một mảng được khai báo một cách bình thường, kết quả là một khối bộ nhớ cố định được dành sẵn tại thời điểm bắt đầu thực thi chương trình, trong khi điều này không xảy ra nếu mảng được khai báo như là một biến con trỏ. Sử dụng một biến con trỏ để biểu diễn một mảng đòi hỏi việc gán một vài ô nhớ khởi tạo trước khi các phần tử mảng được xử lý. Sự cấp phát bộ nhớ như vậy thông thường được thực hiện bằng cách sử dụng hàm thư viện malloc().
Xem ví dụ sau. Một mảng số nguyên một chiều ary có 20 phần tử có thể được khai báo như sau:
int *ary;
thay vì
int ary[20];
Tuy nhiên, ary sẽ không được tự động gán một khối bộ nhớ khi nó được khai báo như là một biến con trỏ, trong khi một khối ô nhớ đủ để chứa 10 số nguyên sẽ được dành sẵn nếu ary được khai báo như là một mảng. Nếu ary được khai báo như là một con trỏ, số lượng bộ nhớ có thể được gán như sau:
ary = malloc(20 *sizeof(int));
Sẽ dành một khối bộ nhớ có kích thước (tính theo bytes) tương đương với kích thước của một số nguyên. Ở đây, một khối bộ nhớ cho 20 số nguyên được cấp phát. 20 con số gán với 20 bytes (một byte cho một số nguyên) và được nhân với sizeof(int), sizeof(int) sẽ trả về kết quả 2, nếu máy tính dùng 2 bytes để lưu trữ một số nguyên. Nếu một máy tính sử dụng 1 byte để lưu một số nguyên, hàm sizeof() không đòi hỏi ở đây. Tuy nhiên, sử dụng nó sẽ tạo khả năng uyển chuyển cho mã lệnh. Hàm malloc() trả về một con trỏ chứa địa chỉ vị trí bắt đầu của vùng nhớ được cấp phát. Nếu không gian bộ nhớ yêu cầu không có, malloc() trả về giá trị NULL. Sự cấp phát bộ nhớ theo cách này, nghĩa là khi được yêu cầu trong một chương trình được gọi là Cấp phát bộ nhớ động.
Trước khi tiếp tục xa hơn, chúng ta hãy thảo luận về khái niệm Cấp phát bộ nhớ động. Một chương trình C có thể lưu trữ các thông tin trong bộ nhớ của máy tính theo hai cách chính. Phương pháp thứ nhất bao gồm các biến toàn cục và cục bộ – bao gồm các mảng. Trong trường hợp các biến toàn cục và biến tĩnh, sự lưu trữ là cố định suốt thời gian thực thi chương trình. Các biến này đòi hỏi người lập trình phải biết trước tổng số dung lượng bộ nhớ cần thiết cho mỗi trường hợp. Phương pháp thứ hai, thông tin có thể được lưu trữ thông qua Hệ thống cấp phát động của C. Trong phương pháp này, sự lưu trữ thông tin được cấp phát từ vùng nhớ còn tự do và khi cần thiết.
Hàm malloc() là một trong các hàm thường được dùng nhất, nó cho phép thực hiện việc cấp phát bộ nhớ từ vùng nhớ còn tự do. Tham số cho malloc() là một số nguyên xác định số bytes cần thiết.
Một ví dụ khác: xét mảng ký tự hai chiều ch_ary có 10 dòng và 20 cột. Sự khai báo và cấp phát bộ nhớ trong trường hợp này phải như sau:
char (*ch_ary)[20];
ch_ary = (char*)malloc(10*20*sizeof(char));
Như đã nói ở trên, malloc() trả về một con trỏ trỏ đến kiểu rỗng (void). Tuy nhiên, vì ch_ary là một con trỏ kiểu char, sự chuyển đổi kiểu là cần thiết. Trong câu lệnh trên, (char*) đổi kiểu trả về của malloc() thành một con trỏ trỏ đến kiểu char.
Tuy nhiên, nếu sự khai báo của mảng phải chứa phép gán các giá trị khởi tạo thì mảng phải được khai báo theo cách bình thường, không thể dùng một biến con trỏ:
int ary[10] = {1,2,3,4,5,6,7,8,9,10};
hoặc
int ary[] = {1,2,3,4,5,6,7,8,9,10};
Ví dụ sau đây tạo một mảng một chiều và sắp xếp mảng theo thứ tự tăng dần. Chương trình sử dụng con trỏ và hàm malloc() để gán bộ nhớ.
#include<stdio.h>
#include <conio.h>
#include<malloc.h>
main()
{
int *p, n, i, j, temp;
printf("
Enter number of elements in the array: ");
scanf("%d", &n);
p = (int*) malloc(n * sizeof(int));
for(i = 0; i < n; ++i)
{
printf("
Enter element no. %d:", i + 1);
scanf("%d", p + i);
}
for(i = 0; i < n - 1; ++i)
for(j = i + 1; j < n; ++j)
if(*(p + i) > *(p + j))
{
temp = *(p + i);
*(p + i) = *(p + j);
*(p + j) = temp;
}
for(i = 0; i < n; ++i)
printf("
%d", *(p + i));
getch();
}
Chú ý lệnh malloc():
p = (int*)malloc(n*sizeof(int));
Ở đây, p được khai báo như một con trỏ trỏ đến một mảng và được gán bộ nhớ sử dụng malloc().
Dữ liệu được đọc vào sử dụng lệnh scanf().
scanf("%d",p+i);
Trong scanf(), biến con trỏ được sử dụng để lưu dữ liệu vào trong mảng.
Các phần tử mảng đã lưu trữ được hiển thị bằng printf():
printf("%d
", *(p + i));
Chú ý dấu * trong trường hợp này, vì giá trị lưu trong vị trí đó phải được hiển thị. Không có dấu *, printf() sẽ hiển thị địa chỉ.
Ø
Hàm free()
Hàm này có thể được sử dụng để giải phóng bộ nhớ khi nó không còn cần thiết.
Dạng tổng quát của hàm
free()
:
void free( void *ptr );
Hàm
free()
giải phóng không gian được trỏ bởi ptr, không gian được giải phóng này có thể sử dụng trong tương lai. ptr đã sử dụng trước đó bằng cách gọi đến
malloc()
,
calloc()
, hoặc
realloc()
,
calloc()
và
realloc()
(sẽ được thảo luận sau).
Ví dụ bên dưới sẽ hỏi bạn có bao nhiêu số nguyên sẽ được bạn lưu vào trong một mảng. Sau đó sẽ cấp phát bộ nhớ động bằng cách sử dụng malloc và lưu số lượng số nguyên, in chúng ra, và sau đó xóa bộ nhớ cấp phát bằng cách sử dụng free.
#include <stdio.h>
#include <conio.h>
#include <stdlib.h> /* required for the malloc and free functions */
main()
{
int number;
int *ptr;
printf("How many ints would you like store? ");
scanf("%d", &number);
ptr = (int *) malloc (number * sizeof(int)); /*allocate memory*/
if(ptr != NULL)
{
for(i = 0 ; i < number ; i++)
{
*(ptr+i) = i;
}
for(i=number ; i>0 ; i--)
{
printf("
%d", *(ptr+(i-1))); /*print out in reverse order*/
}
free(ptr); /* free allocated memory */
}
else
{
printf("
Memory allocation failed - not enough memory.
");
}
getch();
Bạn đang đọc truyện trên: AzTruyen.Top