title of your story
Một chương trình ứng dụng có thể quản lý nhiều loại dữ liệu. Trong trường hợp này, chương trình phải chỉ định bộ nhớ cho mỗi đơn vị dữ liệu. Khi chỉ định bộ nhớ, có hai điểm cần lưu ý như sau :
1. Bao nhiêu bộ nhớ sẽ được gán
2. Mỗi đơn vị dữ liệu được lưu trữ ở đâu trong bộ nhớ.
Trước đây, các lập trình viên phải viết chương trình theo ngôn ngữ máy gồm các mã 1 và 0. Nếu muốn lưu trữ một giá trị tạm thời, vị trí chính xác nơi mà dữ liệu được lưu trữ trong bộ nhớ máy tính phải được chỉ định. Vị trí này là một con số cụ thể, gọi là địa chỉ bộ nhớ.
Các ngôn ngữ lập trình hiện đại cho phép chúng ta sử dụng các tên tượng trưng gọi là biến (variable), chỉ đến một vùng bộ nhớ nơi mà các giá trị cụ thể được lưu trữ.
Kiểu dữ liệu quyết định tổng số bộ nhớ được chỉ định. Những tên được gán cho biến giúp chúng ta sử dụng lại dữ liệu khi cần đến.
Chúng ta đã quen với cách sử dụng các ký tự đại diện trong một công thức. Ví dụ, diện tích hình chữ nhật được tính bởi :
Diện tích = A = chiều dài x chiều rộng = L x B
Cách tính lãi suất đơn giản được cho như sau:
Tiền lãi = I = Số tiền ban đầu x Thời gian x Tỷ lệ/100 = P x T x R /100
Các ký tự A, L, B, I, P, T, R là các biến và là các ký tự viết tắt đại diện cho các giá trị khác nhau.
Xem ví dụ sau đây :
Tính tổng điểm cho 5 sinh viên và hiển thị kết quả. Việc tính tổng được thực hiện theo hướng dẫn sau:
Hiển thị giá trị tổng của 24, 56, 72, 36 và 82
Khi giá trị tổng được hiển thị, giá trị này không còn được lưu trong bộ nhớ máy tính. Giả sử, nếu chúng ta muốn tính điểm trung bình, thì giá trị tổng đó phải được tính một lần nữa.
Tốt hơn là chúng ta sẽ lưu kết quả vào bộ nhớ máy tính, và sẽ lấy lại nó khi cần đến.
sum = 24 + 56 + 72 + 36 + 82
Ở đây, sum là biến được dùng để chứa tổng của 5 số. Khi cần tính điểm trung bình, có thể thực hiện như sau:
Avg = sum / 5
Trong C, tất cả biến cần phải được khai báo trước khi dùng chúng.
Chúng ta hãy xét ví dụ nhập hai số và hiển thị tổng của chúng trong ví dụ 1.
2.2 Hằng (constant)
Trong trường hợp ta dùng biến, giá trị được lưu sẽ thay đổi. Một biến tồn tại từ lúc khai báo đến khi thoát khỏi phạm vi dùng nó. Những câu lệnh trong phạm vi khối mã này có thể truy cập giá trị của biến, và thậm chí có thể thay đổi giá trị của biến. Trong thực tế, đôi khi cần sử dụng một vài khoản mục mà giá trị của chúng không bao giờ bị thay đổi.
Một hằng là một giá trị không bao giờ bị thay đổi. Ví dụ, 5 là một hằng, mà giá trị toán học luôn là 5 và không thể bị thay đổi bởi bất cứ ai. Tương tự, ‘Black’ là một hằng, nó biểu thị cho màu đen. Khi đó, 5 được gọi là hằng số (numeric constant), ‘Black’ được gọi là hằng chuỗi (string constant).
2.3 Định danh (Identifier)
Tên của các biến (variables), các hàm (functions), các nhãn (labels) và các đối tượng khác nhau do người dùng định nghĩa gọi là định danh. Những định danh này có thể chứa một hay nhiều ký tự. Ký tự đầu tiên của định danh phải là một chữ cái hay một dấu gạch dưới ( _ ). Các ký tự tiếp theo có thể là các chữ cái, các con số hay dấu gạch dưới.
Arena, s_count, marks40, và class_one là những định danh đúng. Các ví dụ về các định danh sai là 1sttest, oh!god, và start... end.
Các định danh có thể có chiều dài tuỳ ý, nhưng số ký tự trong một biến được nhận diện bởi trình biên dịch thì thay đổi theo trình biên dịch. Ví dụ, nếu một trình biên dịch nhận diện 31 con số có ý nghĩa đầu tiên cho một tên định danh thì các câu sau sẽ hiển thị cùng một kết quả:
Đây là biến testing.... testing
Đây là biến testing.... testing ... testing
Các định danh trong C có phân biệt chữ hoa và chữ thường, cụ thể, arena thì khác ARENA.
2.3.1 Các nguyên tắc cho việc chỉ đặt tên
Các quy tắc đặt tên biến khác nhau tuỳ ngôn ngữ lập trình. Tuy nhiên, vài quy ước chuẩn được tuân theo như :
Ø Tên biến phải bắt đầu bằng một ký tự chữ cái.
Ø Các ký tự theo sau ký tự đầu bằng một chuỗi các chữ cái hoặc con số và cũng có thể bao gồm ký tự đặc biệt như dấu gạch dưới.
Ø Tránh dùng ký tự O tại những vị trí mà có thể gây lầm lẫn với số không (0) và tương tự chữ cái l (chữ thường của chữ hoa L) có thể lầm lẫn với số 1.
Ø Tên riêng nên tránh đặt tên cho biến.
Ø Theo tiêu chuẩn C các chữ cái thường và hoa thì xem như khác nhau ví dụ. biến ADD, add và Add là khác nhau.
Ø Việc phân biệt chữ hoa và chữ thường khác nhau tuỳ theo ngôn ngữ lập trình. Do đó, tốt nhất nên đặt tên cho biến theo cách thức chuẩn.
Ø Tên một biến nên có ý nghĩa, gợi tả và mô tả rõ kiểu dữ liệu của nó. Ví dụ, nếu tìm tổng của hai số thì tên biến lưu trữ tổng nên đặt là sum (tổng). Nếu đặt tên là s hay ab12 thì không hay lắm.
2.3.2 Từ khóa (Keywords)
Tất cả các ngôn ngữ dành một số từ nhất định cho mục đích riêng. Những từ này có một ý nghĩa đặc biệt trong ngữ cảnh của từng ngôn ngữ, và được xem là “từ khóa”. Khi đặt tên cho các biến, chúng ta cần bảo đảm rằng không dùng bất cứ từ khóa nào làm tên biến.
Tên kiểu dữ liệu tất cả được coi là từ khóa.
Do vậy, đặt tên cho một biến là int sẽ phát sinh một lỗi, nhưng đặt tên cho biến là integer thì không.
Vài ngôn ngữ lập trình yêu cầu lập trình viên chỉ ra tên của các biến cũng như kiểu dữ liệu của nó trước khi dùng biến đó thật sự. Bước này được gọi là khai báo biến. Ta sẽ nói rõ bước này trong phần tiếp theo khi thảo luận về các kiểu dữ liệu. Ðiều quan trọng cần nhớ bây giờ là bước này giúp hệ điều hành thật sự cấp phát một khoảng không gian vùng nhớ cho biến trước khi bắt đầu sử dụng nó.
2.4 Các kiểu dữ liệu (Data types)
Các loại dữ liệu khác nhau được lưu trữ trong biến là
Các số nguyên.
Ví dụ : 10 hay 178993455.
· Các số thực.
Các số thực.
Ví dụ : 15.22 hay 15463452.25.
· Các số dương.
· Các số âm.
Tên.
Ví dụ : John
Giá trị luận lý.
Ví dụ : Y hay N.
Khi dữ liệu được lưu trữ trong các biến có kiểu dữ liệu khác nhau, nó yêu cầu dung lượng bộ nhớ sẽ khác nhau.
Dung lượng bộ nhớ được chỉ định cho một biến tùy thuộc vào kiểu dữ liệu của nó.
Ðể chỉ định bộ nhớ cho một đơn vị dữ liệu, chúng ta phải khai báo một biến với một kiểu dữ liệu cụ thể.
Khai báo một biến có nghĩa là một vùng nhớ nào đó đã được gán cho biến. Vùng bộ nhớ đó sau này sẽ được tham chiếu thông qua tên của biến. Dung lượng bộ nhớ được cấp cho biến bởi hệ điều hành phụ thuộc vào kiểu dữ liệu được lưu trữ trong biến. Vì vậy, một kiểu dữ liệu sẽ mô tả loại dữ liệu phù hợp với biến.
Dạng thức chung cho việc khai báo một biến:
Dạng thức chung cho việc khai báo một biến:
Kiểu dữ liệu (Tên biến)
Kiểu dữ liệu thường được dùng trong các công cụ lập trình có thể được phân chia thành:
1 Kiểu dữ liệu số - lưu trữ giá trị số.
2 Kiểu dữ liệu ký tự – lưu trữ thông tin mô tả
Những kiểu dữ liệu này có thể có tên khác nhau trong các ngôn ngữ lập trình khác nhau. Ví dụ, một kiểu dữ liệu số được gọi trong C là int trong khi đó tại Visual Basic được gọi là integer. Tương tự, một kiểu dữ liệu ký tự được đặt tên là char trong C trong khi đó trong Visual Basic nó được đặt tên là string. Trong bất cứ trường hợp nào, các dữ liệu được lưu trữ luôn giống nhau. Ðiểm khác duy nhất là các biến được dùng trong một công cụ phải được khai báo theo tên của kiểu dữ liệu được hỗ trợ bởi chính công cụ đó.
C có 5 kiểu dữ liệu cơ bản. Tất cả những kiểu dữ liệu khác dựa vào một trong số những kiểu này. 5 kiểu dữ liệu đó là:
int là một số nguyên, về cơ bản nó biểu thị kích cỡ tự nhiên của các số nguyên
float và double được dùng cho các số có dấu chấm động. Kiểu float (số thực) chiếm 4 byte và có thể có tới 6 con số phần sau dấu thập phân, trong khi double chiếm 8 bytes và có thể có tới 10 con số phần thập phân
char chiếm 1 byte và có khả năng lưu một ký tự đơn (character)
void được dùng điển hình để khai báo một hàm không trả về giá trị. Ðiều này sẽ được nói rõ hơn trong phần hàm.
Dung lượng nhớ và phạm vi giá trị của những kiểu này thay đổi theo mỗi loại bộ xử lý và việc cài đặt các trình biên dịch C khác nhau.
Kiểu dữ liệu int
Là kiểu dữ liệu lưu trữ dữ liệu số và là một trong những kiểu dữ liệu cơ bản trong bất cứ ngôn ngữ lập trình nào. Nó bao gồm một chuỗi của một hay nhiều con số.
Thí dụ trong C, để lưu trữ một giá trị số nguyên trong một biến tên là ‘num’, ta khai báo như sau:
int num;
Biến num không thể lưu trữ bất cứ kiểu dữ liệu nào như “Alan” hay “abc”. Kiểu dữ liệu số này cho phép các số nguyên trong phạm vi -32768 tới 32767 được lưu trữ. Hệ điều hành cấp phát 16 bit (2 byte) cho một biến đã được khai báo kiếu int. Ví dụ: 12322, 0, -232.
Nếu chúng ta gán giá trị 12322 cho num thì biến này là biến kiểu số nguyên và 12322 là hằng số nguyên.
Kiểu dữ liệu số thực (float)
Một biến có kiểu dữ liệu số thực được dùng để lưu trữ các giá trị chứa phần thập phân. Trình biên dịch phân biệt các kiểu dữ liệu float và int.
Ðiểm khác nhau chính của chúng là kiểu dữ liệu int chỉ bao gồm các số nguyên, trong khi kiểu dữ liệu float có thể lưu giữ thêm cả các phân số.
Ví dụ, trong C, để lưu trữ một giá trị float trong một biến tên gọi là ‘num’, việc khai báo sẽ như sau :
float num;
Biến đã khai báo là kiểu dữ liệu float có thể lưu giá trị thập phân có độ chính xác tới 6 con số. Biến này được cấp phát 32 bit (4 byte) của bộ nhớ. Ví dụ: 23.05, 56.5, 32.
Nếu chúng ta gán giá trị 23.5 cho num, thì biến num là biến số thực và 23.5 là một hằng số thực.
Kiểu dữ liệu double
Kiểu dữ liệu double được dùng khi giá trị được lưu trữ vượt quá giới hạn về dung lượng của kiểu dữ liệu float. Biến có kiểu dữ liệu là double có thể lưu trữ nhiều hơn khoảng hai lần số các chữ số của kiểu float.
Số các chữ số chính xác mà kiểu dữ liệu float hoặc double có thể lưu trữ tùy thuộc vào hệ điều hành cụ thể của máy tính.
Các con số được lưu trữ trong kiểu dữ liệu float hay double được xem như nhau trong hệ thống tính toán. Tuy nhiên, sử dụng kiểu dữ liệu float tiết kiệm bộ nhớ một nửa so với kiểu dữ liệu double.
Kiểu dữ liệu double cho phép độ chính xác cao hơn (tới 10 con số). Một biến khai báo kiểu dữ liệu double chiếm 64 bit (8 byte) trong bộ nhớ.
Thí dụ trong C, để lưu trữ một giá trị double cho một biến tên ‘num’, khai báo sẽ như sau:
double num;
Nếu chúng ta gán giá trị 23.34232324 cho num, thì biến num là biến kiểu double và 23.34232324 là một hằng kiểu double.
Kiểu dữ liệu char
Kiểu dữ liệu char được dùng để lưu trữ một ký tự đơn.
Một kiểu dữ liệu char có thể lưu một ký tự đơn được bao đóng trong hai dấu nháy đơn (‘’). Thí dụ kiểu dữ liệu char như: ‘a’, ‘m’, ‘$’ ‘%’.
Ta có thể lưu trữ những chữ số như những ký tự bằng cách bao chúng bên trong cặp dấu nháy đơn. Không nên nhầm lẫn chúng với những giá trị số. Ví dụ, ‘1’, ‘5’ và ‘9’ sẽ không được nhầm lẫn với những số 1, 5 và 9.
Xem xét những câu lệnh của mã C dưới đây:
char gender;
gender='M';
Hàng đầu tiên khai báo biến gender của kiểu dữ liệu char. Hàng thứ hai lưu giữ một giá trị khởi tạo cho nó là ‘M’. Biến gender là một biến ký tự và ‘M’ là một hằng ký tự. Biến này được cấp phát 8 bit (1 byte) trong bộ nhớ.
Kiểu dữ liệu void
C có một kiểu dữ liệu đặc biệt gọi là void. Kiểu dữ liệu này chỉ cho trình biên dịch C biết rằng không có dữ liệu của bất cứ kiểu nào. Trong C, các hàm số thường trả về dữ liệu thuộc một kiểu nào đó. Tuy nhiên, khi một hàm không có gì để trả về, kiểu dữ liệu void được sử dụng để chỉ ra điều này.
2.4.1 Những kiểu dữ liệu cơ bản và dẫn xuất
Bốn kiểu dữ liệu (char, int, float và double) mà chúng ta đã thảo luận ở trên được sử dụng cho việc trình bày dữ liệu thực sự trong bộ nhớ của máy tính. Những kiểu dữ liệu này có thể được sửa đổi sao cho phù hợp với những tình huống khác nhau một cách chính xác. Kết quả, chúng ta có được các kiểu dữ liệu dẫn xuất từ những kiểu cơ bản này.
Một bổ từ (modifier) được sử dụng để thay đổi kiểu dữ liệu cơ bản nhằm phù hợp với các tình huống đa dạng. Ngoại trừ kiểu void, tất cả các kiểu dữ liệu khác có thể cho phép những bổ từ đứng trước chúng. Bổ từ được sử dụng với C là signed, unsigned, long và short. Tất cả chúng có thể được áp dụng cho dữ liệu kiểu ký tự và kiểu số nguyên. Bổ từ long cũng có thể được áp dụng cho double.
Một vài bổ từ như :
1. unsigned
2. long
3. short
Ðể khai báo một biến kiểu dẫn xuất, chúng ta cần đặt trước khai báo biến thông thường một trong những từ khóa của bổ từ. Một giải thích chi tiết về các bổ từ này và cách thức sử dụng chúng được trình bày bên dưới.
Ø Các kiểu có dấu (signed) và không dấu(unsigned)
Khi khai báo một số nguyên, mặc định đó là một số nguyên có dấu. Tính quan trọng nhất của việc dùng signed là để bổ sung cho kiểu dữ liệu char, vì char là kiểu không dấu theo mặc định.
Kiểu unsigned chỉ rõ rằng một biến chỉ có thể có giá trị dương. Bổ từ này có thể được sử dụng với kiểu dữ liệu int và kiểu dữ liệu float. Kiểu unsigned có thể áp dụng cho kiểu dữ liệu float trong vài trường hợp nhưng điều này giảm bớt tính khả chuyển (portability) của mã lệnh.
Với việc thêm từ unsigned vào trước kiểu dữ liệu int, miền giá trị cho những số dương có thể được tăng lên gấp đôi.
Ta xem những câu lệnh của mã C cung cấp ở bên dưới, nó khai báo một biến theo kiểu unsigned int và khởi tạo biến này có giá trị 23123.
unsigned int varNum;
varNum = 23123;
Chú ý rằng không gian cấp phát cho kiểu biến này vẫn giữ nguyên. Nghĩa là, biến varNum được cấp phát 2 byte như khi nó dùng kiểu int. Tuy nhiên, những giá trị mà một kiểu unsgned int hỗ trợ sẽ nằm trong khoảng từ 0 đến 65535, thay vì là -32768 tới 32767 mà kiểu int hỗ trợ. Theo mặc định, int là một kiểu dữ liệu có dấu.
Ø Các kiểu long và short
Chúng được sử dụng khi một số nguyên có chiều dài ngắn hơn hoặc dài hơn chiều dài bình thường. Một bổ từ short được áp dụng cho kiểu dữ liệu khi chiều dài yêu cầu ngắn hơn chiều dài số nguyên bình thường và một bổ từ long được dùng khi chiều dài yêu cầu dài hơn chiều dài số nguyên bình thường.
Bổ từ short được sử dụng với kiểu dữ liệu int. Nó sửa đổi kiểu dữ liệu int theo hướng chiếm ít vị trí bộ nhớ hơn. Bởi vậy, trong khi một biến kiểu int chiếm giữ 16 bit (2 byte) thì một biến kiểu short int (hoặc chỉ là short), chiếm giữ 8 bit (1 byte) và cho phép những số có trong phạm vi từ -128 tới 127.
Bổ từ long được sử dụng tương ứng một miền giá trị rộng hơn. Nó có thể được sử dụng với int cũng như với kiểu dữ liệu double. Khi được sử dụng với kiểu dữ liệu int, biến chấp nhận những giá trị số trong khoảng từ -2,147,483,648 đến 2,147,483,647 và chiếm giữ 32 bit ( 4 byte). Tương tự, kiểu long double của một biến chiếm giữ 128 bit (16 byte).
Một biến long int được khai báo như sau:
long int varNum;
Nó cũng có thể được khai báo đơn giản như long varNum. Một số long integer có thể được khai báo như long int hay chỉ là long. Tương tự, ta có short int hay short.
Bảng dưới đây trình bày phạm vi giá trị cho các kiểu dữ liệu khác nhau và số bit nó chiếm giữ dựa theo tiêu chuẩn ANSI.
Kiểu
Dung lượng xấp xỉ (đơn vị là bit)
Phạm vi
char
8
-128 tới 127
unsigned
8
0 tới 255
signed char
8
-128 tới 127
int
16
-32,768 tới 32,767
unsigned int
16
0 tới 65,535
signed int
16
Giống như kiểu int
short int
16
-128 tới 127
unsigned short int
16
0 tới 65, 535
signed short int
16
Giống như kiểu short int
long int
32
-2,147,483,648 tới 2,147,483,647
signed long int
32
Giống như kiểu long int
unsigned long int
32
0 tới 4,294,967,295
float
32
6 con số thập phân
double
64
10 con số thập phân
long double
128
10 con số thập phân
Thí dụ sau trình bày cách khai báo những kiểu dữ liệu trên.
Ví dụ 2:
main()
{
char abc; /*abc of type character */
int xyz; /*xyz of type integer */
float length; /*length of type float */
double area; /* area of type double */
long liteyrs; /*liteyrs of type long int */
short arm; /*arm of type short integer*/
}
Chúng ta xem lại ví dụ cộng hai số và hiển thị tổng ở chương trước. Mã giả như sau :
Ví dụ 3:
BEGIN
INPUT A, B
C = A + B
DISPLAY C
END
Trong ví dụ này, các giá trị cho hai biến A và B được nhập. Các giá trị được cộng và tổng được lưu cho biến C bằng cách dùng câu lệnh C = A + B. Trong câu lệnh này, A và B là những biến và ký hiệu + gọi là toán tử. Chúng ta sẽ nói về toán tử số học của C ở phần sau đây. Tuy nhiên, có những loại toán tử khác trong C sẽ được bàn tới ở phần kế tiếp.
2.5 Các toán tử số học (Arithmetic Operators)
Những toán tử số học được sử dụng để thực hiện những thao tác mang tính số học. Chúng được chia thành hai lớp : Toán tử số học một ngôi (unary) và toán tử số học hai ngôi (binary).
Bảng 2.2 liệt kê những toán tử số học và chức năng của chúng.
Các toán tử một ngôi
Chức năng
Các toán tử hai ngôi
Chức năng
-
Lấy đối số
+
Cộng
++
Tăng một giá trị
-
Trừ
--
Giảm một giá trị
*
Nhân
%
Lấy phần dư
/
Chia
^
Lấy số mũ
Bảng 2.2: Các toán tử số học và chức năng
Ø Các toán tử hai ngôi
Trong C, các toán tử hai ngôi có chức năng giống như trong các ngôn ngữ khác. Những toán tử như +, -, * và / có thể được áp dụng cho hầu hết kiểu dữ liệu có sẵn trong C. Khi toán tử / được áp dụng cho một số nguyên hoặc ký tự, bất kỳ phần dư nào sẽ được cắt bỏ. Ví dụ, 5/2 sẽ bằng 2 trong phép chia số nguyên. Toán tử % sẽ cho ra kết quả là số dư của phép chia số nguyên. Ví dụ: 5%2 sẽ có kết quả là 1. Tuy nhiên, % không thể được sử dụng với những kiểu có dấu chấm động.
Chúng ta hãy xem xét một ví dụ của toán tử số mũ.
9^2
Ở đây 9 là cơ số và 2 là số mũ.
Số bên trái của ‘^’ là cơ số và số bên phải ‘^’ là số mũ.
Kết quả của 9^2 là 9*9 = 81.
Thêm ví dụ khác:
5 ^ 3
Có nghĩa là:
5 * 5 * 5
Do đó: 5 ^ 3 = 5 * 5 * 5 = 125.
Ghi chú: Những ngôn ngữ lập trình như Basic, hỗ trợ toán tử mũ. Tuy nhiên, ANSI C không hỗ trợ ký hiệu ^ cho phép tính lũy thừa. Ta có thể dùng cách khác tính lũy thừa trong C là dùng hàm pow() đã được định nghĩa trong math.h. Cú pháp của nó thể hiện qua ví dụ sau:
...
#include<math.h>
void main(void)
{
….
/* the following function will calculate x to the power y. */
z = pow(x, y);
….
}
Ví dụ sau trình bày tất cả toán tử hai ngôi được dùng trong C. Chú ý rằng ta chưa nói về hàm printf() và getchar(). Chúng ta sẽ bàn trong những phần sau.
Ví dụ 4:
#include<stdio.h>
main()
{
int x,y;
x = 5;
y = 2;
printf("The integers are : %d & %d
", x, y);
printf("The addition gives : %d
", x + y);
printf("The subtraction gives : %d
", x - y);
printf("The multiplication gives : %d
", x * y);
printf("The division gives : %d
", x / y);
printf("The modulus gives : %d
", x % y);
getchar();
}
Các toán tử một ngôi (unary)
Các toán tử một ngôi là toán tử trừ một ngôi ‘-’, toán tử tăng ‘++’ và toán tử giảm ‘--’
Toán tử trừ một ngôi
Ký hiệu giống như phép trừ hai ngôi. Lấy đối số để chỉ ra hay thay đổi dấu đại số của một giá trị. Ví dụ:
a = -75;
b = -a;
Kết quả của việc gán trên là a được gán giá trị -75 và b được gán cho giá trị 75 (-(- 75)). Dấu trừ được sử dụng như thế gọi là toán tử một ngôi vì nó chỉ có một toán hạng.
Nói một cách chính xác, không có toán tử một ngôi + trong C. Vì vậy, một lệnh gán như.
invld_pls = +50;
khi mà invld_pls là một biến số nguyên là không hợp lệ trong chuẩn của C. Tuy nhiên, nhiều trình biên dịch không phản đối cách dùng như vậy.
Các toán tử Tăng và Giảm
C bao chứa hai toán tử hữu ích mà ta không tìm thấy được trong những ngôn ngữ máy tính khác. Chúng là ++ và --. Toán tử ++ thêm vào toán hạng của nó một đơn vị, trong khi toán tử -- giảm đi toán hạng của nó một đơn vị.
Cụ thể:
x = x + 1;
có thể được viết là:
x++;
và:
x = x - 1;
có thể được viết là:
x--;
Cả hai toán tử này có thể đứng trước hoặc sau toán hạng, chẳng hạn:
x = x + 1;
có thể được viết lại là
x++ hay ++x;
Và cũng tương tự cho toán tử --.
Sự khác nhau giữa việc xử lý trước hay sau trong toán tử một ngôi thật sự có ích khi nó được dùng trong một biểu thức. Khi toán tử đứng trước toán hạng, C thực hiện việc tăng hoặc giảm giá trị trước khi sử dụng giá trị của toán hạng. Ðây là tiền xử lý (pre-fixing). Nếu toán tử đi sau toán hạng, thì giá trị của toán hạng được sử dụng trước khi tăng hoặc giảm giá trị của nó. Ðây là hậu xử lý (post-fixing). Xem xét ví dụ sau :
a = 10;
b = 5;
c = a * b++;
Trong biểu thức trên, giá trị hiện thời của b được sử dụng cho tính toán và sau đó giá trị của b sẽ tăng sau. Tức là, c được gán 50 và sau đó giá trị của b được tăng lên thành 6.
Tuy nhiên, nếu biểu thức trên là:
c = a * ++b;
thì giá trị của c sẽ là 60, và b sẽ là 6 bởi vì b được tăng 1 trước khi thực hiện phép nhân với a, sau đó giá trị được gán vào c.
Trong trường hợp mà tác động của việc tăng hay giảm là riêng lẻ thì toán tử có thể đứng trước hoặc sau toán hạng đều được.
Hầu hết trình biên dịch C sinh mã rất nhanh và hiệu quả đối với việc tăng và giảm giá trị. Mã này sẽ tốt hơn so với khi ta dùng toán tử gán. Vì vậy, các toán tử tăng và giảm nên được dùng bất cứ khi nào có thể.
Tóm tắt bài học
Ø Thông thường, khi chương trình ứng dụng cần xử lý dữ liệu, nó cần có nơi nào đó để lưu trữ tạm thời dữ liệu này. Nơi mà dữ liệu được lưu trữ gọi là bộ nhớ.
Ø Các ngôn ngữ lập trình hiện đại ngày nay cho phép chúng ta sử dụng các tên tượng trưng gọi là biến (variable), dùng để chỉ đến một vùng trong bộ nhớ nơi mà các giá trị cụ thể được lưu trữ.
Ø Không có giới hạn về số vị trí bộ nhớ mà một chương trình có thể dùng.
Ø Một hằng (constant) là một giá trị không bao giờ bị thay đổi.
Ø Tên của các biến (variable), các hàm (function), các nhãn (label) và các đối tượng khác nhau do người dùng định nghĩa gọi là định danh.
Ø Tất cả ngôn ngữ dành một số từ nhất định cho mục đích riêng. Những từ này được gọi là là “từ khóa” (keywords).
Ø Các kiểu dữ liệu chính của C là character, integer, float, double và void.
Ø Một bổ từ được sử dụng để thay đổi kiểu dữ liệu cơ bản sao cho phù hợp với nhiều tình huống đa dạng. Các bổ từ được sử dụng trong C là signed, unsigned, long và short.
Ø C hỗ trợ hai loại toán tử số học: một ngôi và hai ngôi.
Ø Toán tử tăng ‘++’ và toán tử giảm ‘--’ là những toán tử một ngôi. Nó chỉ hoạt động trên biến kiểu số.
Ø Toán tử hai ngôi số học là +, -, *, /, %, nó chỉ tác động lên những hằng số, biến hay biểu thức.
Ø Toán tử phần dư ‘%’ chỉ áp dụng trên các số nguyên và cho kết quả là phần dư của phép chia số nguyên.
C có một tập các toán tử phong phú. Toán tử là công cụ dùng để thao tác dữ liệu. Một toán tử là một ký hiệu dùng để đại diện cho một thao tác cụ thể nào đó được thực hiện trên dữ liệu. C định nghĩa bốn loại toán tử: toán tử số học (arithmetic), quan hệ (relational), luận lý (logical), và toán tử luận lý nhị phân (bitwise). Bên cạnh đó, C còn có một số toán tử đặc biệt.
Toán tử thao tác trên hằng hoặc biến. Hằng hoặc biến này được gọi là toán hạng (operands). Biến đã được đề cập ở các chương trước. Hằng là những giá trị cố định mà chương trình không thể thay đổi. Hằng trong C có thể là bất cứ kiểu dữ liệu cơ bản nào. Toán tử được phân loại: toán tử một ngôi, hai ngôi hoặc ba ngôi. Toán tử một ngôi chỉ thao tác trên một phần tử dữ liệu, toán tử hai ngôi trên hai phần tử dữ liệu và ba ngôi trên ba phần tử dữ liệu.
Ví dụ 4.1:
c = a + b;
Ở đây a, b, c là những toán hạng, dấu ‘=’ và dấu ‘+’ là những toán tử.
4.1 Biểu thức (Expressions)
Một biểu thức là tổ hợp các toán tử và toán hạng. Toán tử thực hiện các thao tác như cộng, trừ, so sánh v.v... Toán hạng là những biến hay những giá trị mà các phép toán được thực hiện trên nó. Trong ví dụ a + b, “a” và “b” là toán hạng và “+” là toán tử. Tất cả kết hợp lại là một biểu thức.
Trong quá trình thực thi chương trình, giá trị thực sự của biến (nếu có) sẽ được sử dụng cùng với các hằng có mặt trong biểu thức. Việc đánh giá biểu thức được thực hiện nhờ các toán tử. Vì vậy, mọi biểu thức trong C đều có một giá trị.
Các ví dụ về biểu thức là:
2
x
3 + 7
2 × y + 5
2 + 6 × (4 - 2)
z + 3 × (8 - z)
Ví dụ 4.2:
Roland nặng 70 kilograms, và Mark nặng k kilograms. Viết một biểu thức cho tổng cân nặng của họ. Tổng cân nặng của hai người tính bằng kilograms là 70 + k.
Ví dụ 4.3:
Tính giá trị biểu thức 4 × z + 12 với z = 15.
Chúng ta thay thế mọi z với giá trị 15, và đơn giản hóa biểu thức theo quy tắc: thi hành phép toán trong dấu ngoặc trước tiên, kế đến lũy thừa, phép nhân và chia rồi phép cộng và trừ.
4 × z + 12 trở thành
4 × 15 + 12 = (phép nhân thực hiện trước phép cộng)
60 + 12 =
72
Toán tử gán (Assignment Operator)
Trước khi nghiên cứu các toán tử khác, ta hãy xét toán tử gán (=). Ðây là toán tử thông dụng nhất cho mọi ngôn ngữ và mọi người đều biết. Trong C, toán tử gán có thể được dùng cho bất kỳ biểu thức C hợp lệ. Dạng thức chung cho toán tử gán là:
Tên biến = biểu thức;
Gán liên tiếp
Nhiều biến có thể được gán cùng một giá trị trong một câu lệnh đơn. Việc này thực hiện qua cú pháp gán liên tiếp. Ví dụ:
a = b = c =10;
Dòng mã trên gán giá trị 10 cho a, b,và c. Tuy nhiên, việc này không thể thực hiện lúc khai báo biến. Ví dụ:
int a = int b = int c= 0;
Câu lệnh trên phát sinh lỗi vì sai cú pháp.
Biểu thức số học (Arithmetic Expressions)
Các phép toán thường được thực hiện theo một thứ tự cụ thể (hoặc riêng biệt) để cho ra giá trị cuối cùng. Thứ tự này gọi là độ ưu tiên (sẽ nói đến sau).
Các biểu thức toán học trong C được biểu diễn bằng cách sử dụng toán tử số học cùng với các toán hạng dạng số và ký tự. Những biểu thức này gọi là biểu thức số học (Arithmetic Expressions). Ví dụ về biểu thức số học là :
a * (b+c/d)/22;
++i % 7;
5 + (c = 3+8);
Như chúng ta thấy ở trên, toán hạng có thể là hằng, biến hay kết hợp cả hai. Hơn nữa, một biểu thức có thể là sự kết hợp của nhiều biểu thức con. Chẳng hạn, trong biểu thức đầu, c/d là một biểu thức con, và trong biểu thức thứ ba c = 3+8 cũng là một biểu thức con.
4.2 Toán tử quan hệ (Relational Operators)
Toán tử quan hệ được dùng để kiểm tra mối quan hệ giữa hai biến, hay giữa một biến và một hằng. Ví dụ, việc xét số lớn hơn của hai số, a và b, được thực hiện thông qua dấu lớn hơn (>) giữa hai toán hạng a và b (a > b).
Trong C, true (đúng) là bất cứ giá trị nào khác không (0), và false (sai) là bất cứ giá trị nào bằng không (0). Biểu thức dùng toán tử quan hệ trả về 0 cho false và 1 cho true. Ví dụ biểu thức sau :
a == 14 ;
Biểu thức này kiểm tra xem giá trị của a có bằng 14 hay không. Giá trị của biểu thức sẽ là 0 (false) nếu a có giá trị khác 14 và 1 (true) nếu nó là 14.
> lớn hơn >= lon hon bang != khong bang
4.3 Toán tử luận lý (Logical Operators) và biểu thức
Toán tử luận lý là các ký hiệu dùng để kết hợp hay phủ định biểu thức có chứa các toán tử quan hệ.
Những biểu thức dùng toán tử luận lý trả về 0 cho false và 1 cho true.
Bảng sau mô tả ý nghĩa của các toán tử luận lý.
&& AND: trả về kết quả là true khi cả 2 toán hạng đều true || OR : trả về kết quả là true khi chỉ một trong hai toán hạng đều true ! NOT: Chuyển đổi giá trị của toán hạng duy nhất từ true thành false và ngược lại
Bất cứ toán tử luận lý nào có ký hiệu là hai ký tự thì không được có khoảng trắng giữa hai ký tự đó, ví dụ : == sẽ không đúng nếu viết là = =.
Giả sử một chương trình phải thực thi những bước nhất định nếu điều kiện a < 10 và b == 7 được thoả mãn. Ðiều kiện này được viết ra bằng cách dùng toán tử quan hệ kết hợp với toán tử luận lý AND. Toán tử AND được viết là &&. Ta sẽ có điều kiện để kiểm tra như sau :
(a < 10) && (b == 7);
Tương tự, toán tử OR dùng để kiểm tra xem có một trong số các điều kiện kiểm tra là đúng hay không. Nó có dạng là dấu (||). Cùng ví dụ trên nhưng điều kiện cần kiểm tra là: chỉ cần một trong hai câu lệnh là đúng thì ta có mã sau :
(a < 10) || (b == 7);
Toán tử luận lý thứ ba là NOT được biểu diễn bằng ký hiệu dấu chấm than ‘!’. Toán tử này đảo ngược giá trị luận lý của biểu thức. Ví dụ, để kiểm tra xem biến s có bé hơn 10 hay không, ta viết đều kiện kiểm tra như sau:
(s < 10);
hay là
(! (s >= 10)) /* s không lớn hơn hay bằng 10 */
Cả toán tử quan hệ và luận lý có quyền ưu tiên thấp hơn toán tử số học. Ví dụ, 5 > 4 + 3 được tính tương đương với 5 > (4 + 3), nghĩa là 4+3 sẽ được tính trước và sau đó toán tử quan hệ sẽ được thực hiện. Kết quả sẽ là false, tức là trả về 0.
Câu lệnh sau:
printf("%d", 5> 4 + 3);
sẽ cho ra:
0
vì 5 bé hơn (4 + 3) .
4.4 Toán tử luận lý nhị phân (Bitwise Logical Operators) và biểu thứ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;
int i;
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ý.
Thứ tự
Toán tử
1
NOT
2
AND
3
OR
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
Khi một biểu thức có nhiều hơn một kiểu toán tử thì thứ tự ưu tiên phải được thiết lập giữa các kiểu toán tử với nhau.
Bảng dưới đây cho biết thứ tự ưu tiên giữa các kiểu toán tử khác nhau.
Thứ tự
Kiểu toán tử
1
Số học
2
So sánh (Quan hệ)
3
Luận lý
Bảng 4.6. 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.
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():
Ðịnh dạng
printf()
scanf()
Ký tự đơn (Single Character)
%c
%c
Chuỗi (String)
%s
%s
Số nguyên có dấu (Signed decimal integer)
%d
%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
%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
%u
Số thập lục phân không dấu (Dùng “ABCDEF”)
(Unsigned hexadecimal integer)
%x
%x
Số bát phân không dấu (Unsigned octal integer)
%o
%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:
Mã định dạng
Quy ước in ấn
%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ự %
Bảng 6.3: Các ký tự đặc biệt trong chuỗi điều khiển
Bảng dưới đây đưa ra vài ví dụ sử dụng chuỗi điều khiển và mã định dạng khác nhau.
Số
Câu lệnh
Chuỗi điều khiển
Nội dung mà chuỗi điều khiển chứa đựng
Danh sách tham số
Giải thích danh sách tham số
Hiển thị trên màn hình
1.
printf("%d", 300);
%d
Chỉ chứa lệnh định dạng
300
Hằng số
300
2.
printf("%d", 10+5);
%d
Chỉ chứa lệnh định dạng
10 + 5
Biểu thức
15
3.
printf("Good Morning Mr. Lee.");
Good Morning Mr. Lee.
Chỉ là các ký tự văn bản
Không có (Nil)
Không có
Good Morning Mr. Lee.
4.
int count = 100;
printf("%d", count);
%d
Chỉ chứa lệnh định dạng
Count
Biến
100
5.
printf("
hello");
hello
Chỉ là các ký tự văn bản và ký tự không in được.
Không có
Không có
Hello
(Trên dòng mới)
6.
#define str "Good Apple"
……..
printf("%s", str);
%s
Chỉ chứa lệnh định dạng
Str
Hằng chuỗi
Good Apple
7.
……..
int count,stud_num;
count = 0;
stud_num = 100;
printf("%d %d
", count, stud_num);
%d %d
Chỉ chứa lệnh định dạng và trình tự thoát ra
count, stud_num
Hai biến
0, 100
Bảng 6.4 : Chuỗi điều khiển và mã định dạng
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();
}
Kết quả chương trình như sau:
Bổ từ (Modifier) cho các lệnh định dạng trong printf()
Các lệnh định dạng có thể có bổ từ (modifier), để thay đổi các đặc tả chuyển đổi gốc. Sau đây là các bổ từ được chấp nhận trong câu lệnh printf(). Nếu có nhiều bổ từ được dùng thì chúng tuân theo trình tự sau :
Bổ từ ‘-‘
Dữ liệu sẽ được canh trái bên trong không gian dành cho nó, chúng sẽ được in bắt đầu từ vị trí ngoài cùng bên trái.
Bổ từ xác định độ rộng
Chúng có thể được dùng với kiểu: float, double hay char array (chuỗi-string). Bổ từ xác định độ rộng là một số nguyên xác định độ rộng nhỏ nhất của trường dữ liệu. Các dữ liệu có độ rộng nhỏ hơn sẽ cho kết quả canh phải trong trường dữ liệu. Các dữ liệu có kích thước lớn hơn sẽ được in bằng cách dùng thêm những vị trí cho đủ yêu cầu.Ví dụ, %10f là lệnh định dạng cho các mục dữ liệu kiểu số thực với độ rộng trường dữ liệu thấp nhất là 10.
Bổ từ xác định độ chính xác
Chúng có thể được dùng với kiểu float, double hay mảng ký tự (char array, string). Bổ từ xác định độ rộng chính xác được viết dưới dạng .m với m là một số nguyên. Nếu sử dụng với kiểu float và double, chuỗi số chỉ ra số con số tối đa có thể được in ra phía bên phải dấu chấm thập phân.
Nếu phần phân số của các mục dữ liệu kiểu float hay double vượt quá độ rộng con số chỉ trong bổ từ, thì số đó sẽ được làm tròn. Nếu chiều dài chuỗi vượt quá chiều dài chỉ định thì chuỗi sẽ được cắt bỏ phần dư ra ở phía cuối. Một vài số không (0) sẽ được thêm vào nếu số con số thực sự trong một mục dữ liệu ít hơn được chỉ định trong bổ từ. Tương tự, các khoảng trắng sẽ được thêm vào cho chuỗi ký tự. Ví dụ, %10.3f là lệnh định dạng cho mục dữ liệu kiểu float, với độ rộng tối thiểu cho trường dữ liệu là 10 và 3 vị trí sau phần thập phân.
Bổ từ ‘0’
Theo mặc định, việc thêm vào một trường được thực hiện với các khoảng trắng. Nếu người dùng muốn thêm vào trường với số không (0), bổ từ này phải được dùng.
Bổ từ ‘l’
Bổ từ này có thể được dùng để hiển thị số nguyên như: long int hay một tham số kiểu double. Mã định dạng tương ứng cho nó là %ld.
Bổ từ ‘h’
Bổ từ này được dùng để hiện thị kiểu short integer. Mã định dạng tương ứng cho nó là %hd.
Bổ từ ‘*’
Bổ từ này được dùng khi người dùng không muốn chỉ trước độ rộng của trường mà muốn chương trình xác định nó. Nhưng khi đi với bổ từ này, một tham số được yêu cầu phải chỉ ra độ rộng trường cụ thể.
Chúng ta hãy xem những bổ từ này hoạt động thế nào. Ðầu tiên, chúng ta xem xét tác động của nó đối với những dữ liệu kiểu số nguyên.
Ví dụ 6.2:
/* Chương trình này trình bày cách dùng bổ từ trong printf() */
#include <stdio.h>
#include <conio.h>
main()
{
printf("The number 555 in various forms:
");
printf("Without any modifier:
");
printf("[%d]
", 555);
printf("With - modifier:
");
printf("[%-d]
", 555);
printf("With digit string 10 as modifier:
");
printf("[%10d]
", 555);
printf("With 0 as modifier:
");
printf("[%0d]
", 555);
printf("With 0 and digit string 10 as modifiers:
");
printf("[%010d]
", 555);
printf("With -, 0 and digit string 10 as modifiers:
");
printf("[%-010d]
", 555);
getch();
}
Kết quả như dưới đâ
Chúng ta đã dùng ký hiệu ‘[‘ và ‘]’ để chỉ ra nơi trường bắt đầu và nơi kết thúc. Khi chúng ta dùng %d mà không có bổ từ, chúng ta thấy rằng nó dùng cho một trường có cùng độ rộng với số nguyên. Khi dùng %10d chúng ta thấy rằng nó dùng 10 khoảng trắng cho trường và số được canh lề phải theo mặc định. Nếu ta dùng bổ từ –, số sẽ được canh trái trong trường đó. Nếu dùng bổ từ 0, chúng ta thấy rằng số sẽ thêm vào 0 thay vì là khoảng trắng.
Bây giờ chúng ta hãy xem bổ từ dùng với số thực.
Ví dụ 6.3:
/* Chương trình này trình bày cách dùng bổ từ trong printf() */
#include <stdio.h>
#include <conio.h>
main()
{
printf("The number 555.55 in various forms:
");
printf("In float form without modifiers:
");
printf("[%f]
", 555.55);
printf("In exponential form without any modifier:
");
printf("[%e]
", 555.55);
printf("In float form with - modifier:
");
printf("[%-f]
", 555.55);
printf("In float form with digit string 10.3 as modifier
");
printf("[%10.3f]
", 555.55);
printf("In float form with 0 as modifier:
");
printf("[%0f]
", 555.55);
printf("In float form with 0 and digit string 10.3");
printf("as modifiers:
");
printf("[%010.3f]
", 555.55);
printf("In float form with -, 0 ");
printf("and digit string 10.3 as modifiers:
");
printf("[%-010.3f]
", 555.55);
printf("In exponential form with 0");
printf("and digit string 10.3 as modifiers:
");
printf("[%010.3e]
", 555.55);
printf("In exponential form with -, 0");
printf("and digit string 10.3 as modifiers:
");
printf("[%-010.3e]
", 555.55);
getch();
}
Theo mặc định cho %f, chúng ta có thể thấy rằng có 6 con số cho phần thập phân và mặc định cho %e là một con số tại phần nguyên và 6 con số phần bên phải dấu chấm thập phân. Chú ý cách thể hiện 2 số cuối cùng trong ví dụ trên, số các con số bên phải dấu chấm thập phân là 3, dẫn đến kết quả không được làm tròn.
Bây giờ, chúng ta hãy xem bổ từ dùng với chuỗi số. Chú ý cách mở rộng trường để chứa toàn bộ chuỗi. Hơn nữa, chú ý cách đặc tả độ chính xác .4 trong việc giới hạn số ký tự được in.
Ví dụ 6.4:
/* Chương trình trình bày cách dùng bổ từ với chuỗi*/
#include <stdio.h>
#include <conio.h>
main()
{
printf("A string in various forms:
");
printf("Without any format command:
");
printf("Good day Mr. Lee.
");
printf("With format command but without any modifier:
");
printf("[%s]
", "Good day Mr. Lee.");
printf("With digit string 4 as modifier:
");
printf("[%4s]
", "Good day Mr. Lee.");
printf("With digit string 19 as modifier:
");
printf("[%19s]
", "Good day Mr. Lee.");
printf("With digit string 23 as modifier:
");
printf("[%23s]
", "Good day Mr. Lee.");
printf("With digit string 25.4 as modifier:
");
printf("[%25.4s]
", "Good day Mr.Lee.");
printf("With - and digit string 25.4 as modifiers:
");
printf("[%-25.4s]
", "Good day Mr.shroff.");
getch();
}
Kết quả như sau:
Những ký tự ta nhập tại bàn phím không được lưu ở dạng các ký tự. Thật sự chúng lưu theo dạng các số dưới dạng mã ASCII (Bộ mã chuẩn Mỹ cho việc trao đổi thông tin - American Standard Code for Information Interchange). Các giá trị của một biến được thông dịch dưới dạng ký tự hay một số tùy vào kiểu của biến đó. Ví dụ sau mô tả điều này:
Ví dụ 6.5:
#include <stdio.h>
#include <conio.h>
main()
{
int a = 80;
char b= 'C';
printf("
This is the number stored in 'a' %d",a);
printf("
This is a character interpreted from 'a' %c",a);
printf("
This is also a character stored in 'b' %c",b);
printf("
Hey! The character of 'b' is printed as a number! %d", b);
getch();
}
Kết quả này mô tả việc dùng các đặc tả định dạng và việc thông dịch của mã ASCII. Mặc dù các biến a và b đã được khai báo là các biến kiểu int và char, nhưng chúng đã được in như là ký tự và số nhờ vào việc dùng các bộ định dạng khác nhau. Ðặc điểm này của C giúp việc xử lý dữ liệu được linh hoạt.
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 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, int, 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();
}
Kết quả như sau:
Nhập vào:
12 67.9 F MARK
Dữ liệu đầu vào có thể là:
12 67.9 F MARK
hoặc như:
12 67.9 F MARK
cũng được nhận vào các biến a, d, ch, và name.
Xem ví dụ khác:
Ví dụ 6.7:
#include <stdio.h>
#include <conio.h>
main()
{
int i;
float x;
char c;
scanf("%3d %5f %c", &i, &x, &c);
printf("%d, %f, %c",i,x,c);
getch();
}
Kết quả như sau:
Nếu dữ liệu nhập vào là:
21 10.345 F
Khi chương trình được thực thi, thì 21 sẽ gán tới i, 10.34 sẽ gán tới x và ký tự 5 sẽ được gán cho c. Còn lại là đặc tính F sẽ bị bỏ qua.
Khi ta chỉ rõ một chiều rộng trường bên trong scanf(), thí dụ %10s, rồi sau đó scanf() chỉ thu nhận tối đa 10 ký tự hoặc tới ký tự khoảng trắng đầu tiên (bất cứ ký tự nào đầu tiên). Ðiều này cũng áp dụng cho các kiểu int, float và double.
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:
Nếu chuỗi được viết là DIEN Tu may tinh, chỉ có chuỗi ký tự DIEN T được gán cho mảng, khi đó thì ký tự viết thường đầu tiên (trong trường hợp này là ‘u’) được thông dịch như ký tự đầu tiên bên ngoài chuỗi:
Ðể chấp nhận bất kỳ ký tự nào đến khi gặp ký tự xuống dòng, chúng ta sử dụng mã định dạng %[^
], điều này ngụ ý rằng chuỗi đó sẽ chấp nhận bất kỳ ký tự nào trừ “
” (ký tự xuống dòng).
Dấu mũ (^) ngụ ý rằng tất cả các ký tự trừ những ký tự nằm sau dấu mũ đó sẽ được chấp nhận như ký tự hợp lệ.
Ví dụ 6.9:
#include <stdio.h>
#include <conio.h>
main()
{
char line[80];
scanf("%[^.]", line);
printf("%s",line);
getch();
}
Kết quả như sau:
Nếu nhập vào chuỗi: dientumaytinh.com
Khi hàm scanf() được thực thi, chỉ có chuỗi dientumaytinh được gán vào mảng, ký tự '.' đầu tiên sẽ được coi là kết thúc một chuỗi.
Bổ từ * cho kết quả khác nhau trong scanf(). Dấu * được dùng để chỉ rằng một trường sẽ được bỏ qua luôn hay tạm bỏ qua.
Ví dụ xét chương trình:
#include <stdio.h>
#include <conio.h>
main()
{
char item[20];
int partno;
float cost;
.........
scanf("%s %*d %f", item, &partno, &cost);
.........
}
Nếu các mục dữ liệu tương ứng là:
battery 12345 0.05
thì battery sẽ được gán cho item và 0.05 sẽ được gán cho cost nhưng 12345 sẽ không được gán cho partno bởi vì dấu * ngăn chặn việc gán.
Bất cứ ký tự khác trong scanf() mà không là mã định dạng trong chuỗi điều khiển phải được nhập vào chính xác nếu không sẽ phát sinh lỗi. Ðặc điểm này được dùng để chấp nhận dấu phân cách phẩy (,).
Ví dụ chuỗi dữ liệu
10, 15, 17
và lệnh nhập vào
scanf(“%d, %f, %c”, &intgr, &flt, &ch);
Chú ý rằng dấu phẩy trong chuỗi chuyển đổi tương ứng dấu phẩy trong chuỗi nhập và vì vậy nó sẽ có chức năng như dấu phân cách.
Ký tự khoảng trắng trong chuỗi điều khiển thường được bỏ qua mặc dù nó sẽ phát sinh trở ngại khi dùng với mã định dạng %c. Nếu chúng ta dùng bộ định dạng %c thì một khoảng trắng được xem như là một ký tự hợp lệ.
Xét đoạn mã sau:
int x, y;
char ch;
scanf(“%2d %c %d”,&x, &ch, &y);
printf(“%d %d %d
”,x, ch, y);
ta nhập vào:
14 c 5
14 sẽ được gán cho x, ký tự ch nhận ký tự khoảng trắng (số 32 trong hệ thập phân), do vậy y được gán giá trị của ký tự ‘c’ tức là số 99 trong hệ thập phân.
Xét đoạn mã sau:
#include <stdio.h>
#include <conio.h>
main()
{
char c1, c2, c3;
…………..
scanf(“%c%c%c”,&c1, &c2, &c3);
………………..
}
Nếu dữ liệu nhập vào là:
a b c
(với khoảng trắng giữa các ký tự), thì kết quả của phép gán:
c1 = a, c2 = <Khoảng trắng>, c3 = b
Ở đây chúng ta có thể thấy c2 chứa một khoảng trắng vì chuỗi nhập có chứa ký tự khoảng trắng. Ðể bỏ qua các ký tự khoảng trắng này và đọc ký tự tiếp theo không phải là ký tự khoảng trắng, ta nên dùng tập chuyển đổi %1s.
scanf(“%c%1s%1s”,&c1, &c2, &c3);
Khi đó kết quả sẽ khác đi với cùng dữ liệu nhập vào như trước và kết quả đúng như ý định của ta:
c1 = a, c2 = b, c3 = c
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à:
o 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();
}
Kết quả như sau:
Trong 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.
Tham số
Hàm
Tác dụ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’)
Hiển thị con số 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
Bảng 6.5: Những tùy chọn cho putchar() và tác dụng của chúng
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();
}
Kết quả như sau:
Khác nhau giữa getchar() và putchar() là putchar() yêu cầu một tham số trong khi getchar() thì không.
Ví dụ 6.12:
/* Chương trình trình bày getchar() và putchar() */
#include <stdio.h>
#include <conio.h>
main()
{
char letter;
printf("You can enter a character now: ");
letter = getchar();
putchar(letter);
getch();
}
Kết quả như sau:
7.1. Câu lệnh điều kiện là gì ?
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 if:
if (biểu thức)
Các 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();
}
Kết quả của chương trình như sau:
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 được đặt trong cặp ngoặc nhọn {}. 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 trả về giá trị 1 nếu năm đó là năm nhuận. Khi đó, chương trình hiển thị thông báo gồm biến year và dòng chữ “is a leap year”. Nếu điều kiện trên không thỏa mãn, chương trình không hiển thị thông báo nào.
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:
if (biểu thức)
câu_lệnh – 1;
else
câu_lệnh – 2;
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.
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”:
Chương trình dưới đây đưa ra cách sử dụng của if lồng nhau.
Ví dụ 7.8:
Một công ty sản xuất 3 loại sản phẩm có tên gọi: văn phòng phẩm cho máy tính (computer stationery), đĩa cứng (fixed disks) và máy tính (computer).
Sản phẩm Mã
Computer Stationery 1
Fixed Disks 2
Computers 3
Công ty có chính sách giảm giá như sau:
Sản phẩm Giá trị đặt hàng Tỷ lệ giảm giá
Computer Stationery $500/- hoặc hơn 12%
Computer Stationery $300/- hoặc hơn 8%
Computer Stationery dưới $300/- 2%
Fixed Disks $2000/- hoặc hơn 10%
Fixed Disks $1500/- hoặc hơn 5%
Computers $5000/- hoặc hơn 10%
Computer $2500/- hoặc hơn 5%
Dưới đây là chương trình tính giảm giá:
#include <stdio.h>
#include <conio.h>
main()
{
int productcode;
float orderamount, rate = 0.0;
printf("
Please enter the product code: ");
scanf("%d", &productcode);
printf("Please enter the order amount: ");
scanf("%f", &orderamount);
if (productcode == 1)
{
if (orderamount >= 500)
rate = 0.12;
else if (orderamount >= 300)
rate = 0.08;
else
rate = 0.02;
}
else if (productcode == 2)
{
if (orderamount >= 2000)
rate = 0.10;
else if (orderamount >= 1500)
rate = 0.05;
}
else if (productcode == 3)
{
if (orderamount >= 5000)
rate = 0.10;
else if (orderamount >= 2500)
rate = 0.05;
}
orderamount -= orderamount * rate;
printf("The net order amount is % .2f
", orderamount);
getch();
}
Ở trên, else sau cùng trong chuỗi các else-if không cần kiểm tra bất kỳ điều kiện nào. Ví dụ, nếu mã sản phẩm được nhập vào là 1 và giá trị đặt hàng nhỏ hơn $300, thì không cần phải kiểm tra điều kiện, vì tất cả các khả năng đã được kiểm soát.
Kết quả thực thi chương trình với mã sản phẩm là 3 và giá trị đặt hàng là $6000 được trình bày ở trên.
Sửa đổi chương trình trên để chú ý đến trường hợp dữ liệu nhập là một mã sản phẩm không hợp lệ. Điều này có thể dễ dàng đạt được bằng cách thêm một lệnh else vào chuỗi lệnh if dùng kiểm tra mã sản phẩm. Nếu gặp một mã sản phẩm không hợp lệ, chương trình phải kết thúc mà không cần tính giá trị thực của đơn đặt hàng.
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();
}
9.1 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: count = 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, i đượ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, i đượ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.
Các trường hợp khác của vòng lặp ‘for’:
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 đó i 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í dụ 9.7:
/* ECHO PROGRAM */
/* A program to accept input data from the console and print it on the screen */
/* End of input data is indicated by pressing ‘^Z’*/
#include <stdio.h>
main()
{
char ch;
while ((ch = getchar()) != EOF)
{
putchar(ch);
}
Dữ liệu người dùng nhập vào được in đậm. Chương trình làm việc như thế nào ? Sau khi nhập vào một tập hợp các ký tự, nội dung của nó sẽ được in hai lần lên màn hình khi bạn nhấn <Enter>. Điều này là do các ký tự bạn nhập vào từ bàn phím được lưu trữ trong bộ đệm bàn phím. Và lệnh putchar() sẽ lấy nó từ bộ đệm sau khi bạn nhấn phím <Enter>. Chú ý cách thức kết thúc quá trình nhập dữ liệu bằng tổ hợp phím ^Z, đây là kí tự kết thúc file trong DOS.
9.1.3 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ác vòng lặp ‘while lồng nhau’ và ‘do ... while’
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.
9.2 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, goto, 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à goto 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.
9.2.1 Lệ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:
Ø goto
Ø label
Lệnh goto 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 goto 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 goto 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();
}
9.2.3 Lệnh ‘break’:
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. Ví dụ,
Ví dụ 9.11:
#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 <conio.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.
9.3 Hàm ‘exit()’:
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.
Các phần tử mảng và các chỉ mục:
Mỗi phần tử của mảng được định danh bằng một chỉ mục hoặc chỉ số gán cho nó. Chiều của mảng được xác định bằng số chỉ số cần thiết để định danh duy nhất mỗi phần tử. Một chỉ số là một số nguyên dương được bao bằng dấu ngoặc vuông [ ] đặt ngay sau tên mảng, không có khoảng trắng ở giữa. Một chỉ số chứa các giá trị nguyên bắt đầu bằng 0. Vì vậy, một mảng player với 11 phần tử được biểu diễn như sau:
player[0], player[1], player[2], ... , player[10].
Như đã thấy, phần tử mảng bắt đầu với player[0], và vì vậy phần tử cuối cùng là player[10] không phải là player[11]. Điều này là do bởi trong C, chỉ số mảng bắt đầu từ 0; do đó trong mảng N phần tử, phần tử cuối cùng có chỉ số là N-1. Phạm vi cho phép của các giá trị chỉ số được gọi là miền giới hạn của chỉ số mảng, giới hạn dưới và giới hạn trên. Một chỉ số mảng hợp lệ phải có một giá trị nguyên nằm trong niềm giới hạn. Thuật ngữ hợp lệ được sử dụng cho một nguyên nhân rất đặc trưng. Trong C, nếu người dùng cố gắng truy xuất một phần tử nằm ngoài dãy chỉ số hợp lệ (như player[11] trong ví dụ trên của mảng), trình biên dịch C sẽ không phát sinh ra lỗi. Tuy nhiên, có thể nó truy xuất một giá trị nào đó dẫn đến kết quả không đoán được. Cũng có nguy cơ viết chồng lên dữ liệu hoặc mã lệnh chương trình. Vì vậy, người lập trình phải đảm bảo rằng tất cả các chỉ số là nằm trong miền giới hạn hợp lệ.
Ø Khai báo một mảng:
Một mảng có một vài đặc tính riêng biệt và phải được khai báo khi sử dụng chúng. Những đặc tính này bao gồm:
· Lớp lưu trữ
· Kiểu dữ liệu của các phần tử mảng.
· Tên mảng – xác định vị trí phần tử đầu tiên của mảng.
· Kích thước mảng - một hằng số có giá trị nguyên dương.
Một mảng được khai báo giống như cách khai báo một biến, ngoại trừ tên mảng được theo sau bởi một hoặc nhiều biểu thức, được đặt trong dấu ngoặc vuông [] xác định chiều dài của mảng. Cú pháp tổng quát khai báo một mảng như sau:
lớp_lưu_trữ kiểu_dữ_liệu tên_mảng[biểu_thức_kích_thước]
Ở đây, biểu_thức_kích_thước là một biểu thức xác định số phần tử trong mảng và phải định ra một trị nguyên dương. Lớp_lưu_trữ là một tùy chọn. Mặc định lớp automatic được dùng cho mảng khai báo bên trong một hàm hoặc một khối lệnh, và lớp external được dùng cho mảng khai báo bên ngoài một hàm. Vì vậy mảng player được khai báo như sau:
int player[11];
Nên nhớ rằng, trong khi khai báo mảng, kích thước của mảng sẽ là 11, tuy nhiên các chỉ số của từng phần tử bên trong mảng sẽ là từ 0 đến 10.
Các qui tắc đặt tên mảng là giống với qui tắc đặt tên biến. Một tên mảng và một tên biến không được giống nhau, nó dẫn đến sự nhập nhằng. Nếu một sự khai báo như vậy xuất hiện trong chương trình, trình biên dịch sẽ hiển thị thông báo lỗi.
Ø Một vài qui tắc với mảng:
· Tất cả các phần tử của một mảng có cùng kiểu. Điều này có nghĩa là, nếu một mảng được khai báo kiểu int, nó không thể chứa các phần tử có kiểu khác.
· Mỗi phần tử của mảng có thể được sử dụng bất cứ nơi nào mà một biến được cho phép hay được yêu cầu.
· Một phần tử của mảng có thể được tham chiếu đến bằng cách sử dụng một biến hoặc một biểu thức nguyên. Sau đây là các tham chiếu hợp lệ:
player[i]; /*Ở đó i là một biến, tuy nhiên cần phải chú ý rằng i nằm trong miền giới hạn của chỉ số đã được khai báo cho mảng player*/
player[3] = player[2] + 5;
player[0] += 2;
player[i / 2 + 1];
· Kiểu dữ liệu của mảng có thể là int, char, float, hoặc double.
11.2 Việc quản lý mảng trong C:
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];
int i;
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();
/*Input values are accepted from the user into the array ary[10]*/
#include <stdio.h>
#include <conio.h>
main()
{
int ary[10];
int i, total, high;
for (i = 0; i < 10; i++)
{
printf("Enter value: %d: ", i + 1);
scanf("%d", &ary[i]);
}
/* Displays highest of the entered values */
high = ary[0];
for (i = 1; i < 10; i++)
{
if (ary[i] > high)
high = ary[i];
}
printf("
Highest value entered was %d", high);
/* Prints average of value entered for ary[10] */
for (i = 0, total = 0; i < 10; i++)
total = total + ary[i];
printf("
The average of the element of ary is %d", total/i);
getch();
}
Các mảng không được khởi tạo tự động, trừ khi mỗi phần tử mảng được gán một giá trị riêng lẻ. Không nên dùng các mảng trước khi có sự khởi tạo thích hợp. Điều này là bởi vì không gian lưu trữ của mảng không được khởi tạo tự động, do đó dễ gây ra kết quả không lường trước. Mỗi khi các phần tử của một mảng chưa khởi tạo được sử dụng trong các biểu thức toán học, các giá trị đã tồn tại sẵn trong ô nhớ sẽ được sử dụng, các giá trị này không đảm bảo rằng có cùng kiểu như khai báo của mảng, trừ khi các phần tử của mảng được khởi tạo một cách rõ ràng. Điều này đúng không chỉ cho các mảng mà còn cho các biến thông thường.
Trong đoạn mã lệnh sau, các phần tử của mảng được gán giá trị bằng các dùng vòng lặp for.
int ary[20], i;
for(i=0; i<20; i++)
ary[i] = 0;
Khởi tạo một mảng sử dụng vòng lặp for có thể được thực hiện với một hằng giá trị, hoặc các giá trị được sinh ra từ một cấp số cộng.
Một vòng lặp for cũng có thể được sử dụng để khởi tạo một mảng các ký tự như sau:
Ví dụ 11.3:
#include <stdio.h>
#include <conio.h>
main()
{
char alpha[26];
int i, j;
for(i = 65, j = 0; i < 91; i++, j++)
{
alpha[j] = i;
printf("The character now assigned is %c
", alpha[j]);
}
getch ();
}
Chương trình trên gán các mã ký tự ASCII cho các phần tử của mảng alpha. Kết quả là khi in với định dạng %c, một chuỗi các ký tự được xuất ra màn hình. Các mảng cũng có thể được khởi tạo khi khai báo. Điều này được thực hiện bằng việc gán tên mảng với một danh sách các giá trị phân cách nhau bằng dấu phẩy (,) đặt trong cặp dấu ngoặc nhọn {}. Các giá trị trong cặp dấu ngoặc nhọn {} được gán cho các phần tử trong mảng theo đúng thứ tự xuất hiện.
Ví dụ:
int deci[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
static float rates[4] = {0.0, -2.5, 13.75, 18.0};
char company[5] = {‘A’, ‘P’, ‘P’, ‘L’, ‘E’};
int marks[100] = {15, 13, 11, 9};
Các giá trị khởi tạo của mảng phải là các hằng, không thể là biến hoặc các biểu thức. Một vài phần tử đầu tiên của mảng sẽ được khởi tạo nếu số lượng giá trị khởi tạo là ít hơn số phần tử mảng được khai báo. Các phần tử còn lại sẽ được khởi tạo giá trị 0. Ví dụ, trong mảng marks sau khi có sự khởi tạo như trên, bốn phần tử đầu tiên (từ 0 đến 3) tương ứng được khởi tạo là 15, 13, 11 và 9. Các phần tử còn lại có giá trị 0. Không thể chỉ khởi tạo các phần tử từ 1 đến 4, hoặc từ 2 đến 4, hay từ 2 đến 5 khi sự khởi tạo được thực hiện tại thời điểm khai báo. Trong C không có khả năng lặp lại sự khởi tạo giá trị.
Trong trường hợp sự khởi tạo là tường minh, lớp extern hoặc static, các phần tử của mảng được đảm bảo khởi tạo là 0 (không giống lớp auto).
Không cần thiết khai báo kích thước của mảng đang được khởi tạo. Nếu kích thước của mảng được bỏ qua khi khai báo, trình biên dịch sẽ xác định kích thước của mảng bằng cách đếm các giá trị đang được khởi tạo. Ví dụ, sự khai báo mảng external sau đây sẽ chỉ định kích thước của mảng ary là 5 vì có 5 giá trị khởi tạo.
int ary[] = {1, 2, 3, 4, 5};
Ø Các mảng chuỗi/ký tự:
Một chuỗi có thể được khai báo như là một mảng ký tự, và được kết thúc bởi một ký tự NULL. Mỗi ký tự của chuỗi chiếm 1 byte, và ký tự cuối cùng của chuỗi luôn luôn là ký tự ‘\0’. Ký tư ‘\0’ được gọi là ký tự null. Nó là một mã thoát (escape sequence) tương tự như ‘
’, thay thế cho ký tự có giá trị 0. Vì ‘\0’ luôn là ký tự cuối cùng của một chuỗi, nên các mảng ký tự phải có nhiều hơn một ký tự so với chiều dài tối đa mà chúng quản lý. Ví dụ, một mảng ary quản lý một chuỗi 10 ký tự phải được khai báo như sau:
char ary[11];
Vị trí thêm vào được sử dụng để lưu trữ ký tự null. Nên nhớ rằng ký tự kết thúc (ký tự null) là rất quan trọng.
Các giá trị chuỗi có thể được nhập vào bằng cách sử dụng hàm scanf(). Với chuỗi ary được khai báo ở trên, mã lệnh nhập sẽ như sau:
scanf(“%s”, ary);
Trong lệnh trên, ary xác định vị trí nơi mà lần lượt các ký tự của mảng sẽ được lưu trữ.
Ví dụ 11.4:
#include <stdio.h>
#include <conio.h>
main()
{
char ary[5];
int i;
printf("
Enter string: ");
scanf("%s", ary);
printf("
The string is %s
", ary);
for (i = 0; i < 5; i++)
printf("\t%d", ary[i]);
getch();
}
Kết quả như trên là của 4 ký tự (appl) và ký tự thứ 5 là ký tự null. Điều này được thấy rõ với mã ASCII cho các ký tự được in ra ở dòng thứ hai. Ký tự thứ năm được in là 0, là giá trị của ký tự null.
Kết quả ở trên của là một dữ liệu đầu vào có 5 ký tự a, p, p, l và e. Nó không được xem là một chuỗi bởi vì ký tự thứ 5 của mảng không phải là \0. Một lần nữa, điều này được thấy rõ bằng dòng in ra mã ASCII của các ký tự a, p, p, l, e.
Trong ví dụ trên, khi chỉ có hai ký tự được nhập, ký tự thứ ba sẽ là ký tự null. Điều này cho biết là chuỗi đã được kết thúc. Những ký tự còn lại là những ký tự không dự đoán được.
Trong trường hợp trên, tính quan trọng của ký tự null trở nên rõ ràng. Ký tự null xác định sự kết thúc của chuỗi và là cách duy nhất để các hàm làm việc với chuỗi sẽ biết đâu là điểm kết thúc của chuỗi.
Mặc dù C không có kiểu dữ liệu chuỗi, nhưng nó cho phép các hằng chuỗi. Một hằng chuỗi là một dãy các ký tự được đặt trong dấu nháy đôi (“”). Không giống như các hằng khác, nó không thể được sửa đổi trong chương trình. Ví dụ như:
“Hi Dientumaytinh!”
Trình biên dịch C sẽ tự động thêm vào ký tự null cuối chuỗi.
C hỗ trợ nhiều hàm cho chuỗi, các hàm này nằm trong thư viện chuẩn string.h. Một vài hàm được đưa ra trong bảng 11.1. Cách làm việc của các hàm này sẽ được thảo luận trong bài 17.
Tên hàm
Chức năng
strcpy(s1, s2)
Sao chép chuỗi s2 vào s1
strcat(s1, s2)
Nối chuỗi s2 vào cuối chuỗi của s1
strlen(s1)
Trả về chiều dài của chuỗi s1
strcmp(s1, s2)
Trả về 0 nếu s1 và s2 là 2 chuỗi giống nhau; nhỏ hơn 0 nếu s1<s2; lớn hơn 0 nếu s1> s2 (theo thứ tự mã ASCII)
strchr(s1, ch)
Trả về một con trỏ trỏ đến vị trí xuất hiện đầu tiên của ch trong s1
strstr(s1, s2)
Trả về một con trỏ trỏ đến vị trí xuất hiện đầu tiên của chuỗi s2 trong chuỗi s1
11.3 Mảng hai chiều:
Chúng ta đã biết thế nào là mảng một chiều. Điều này có nghĩa là các mảng chỉ có một chỉ số. Các mảng có thể có nhiều hơn một chiều. Các mảng đa chiều giúp dễ dàng trình bày các đối tượng đa chiều, chẳng hạn một đồ thị với các dòng và cột hay tọa độ màn hình của máy tính. Các mảng đa chiều được khai báo giống như các mảng một chiều, ngoại trừ có thêm một cặp dấu ngoặc vuông [] trong trường hợp mảng hai chiều. Một mảng ba chiều sẽ cần ba cặp dấu ngoặc vuông,... Một cách tổng quát, một mảng đa chiều có thể được biểu diễn như sau:
storage_class data_type ary[exp1][exp2]....[expN];
Ở đó, ary là một mảng có lớp là storage_class, kiểu dữ liệu là data_type, và exp1, exp2,..... , expN là các biểu thức nguyên dương xác định số phần tử của mảng được kết hợp với mỗi chiều.
Dạng đơn giản nhất và thường được sử dụng nhất của các mảng đa chiều là mảng hai chiều. Một mảng hai chiều có thể xem như là một mảng của hai ‘mảng một chiều’. Một mảng hai chiều đặc trưng như bảng lịch trình của máy bay, xe lửa. Để xác định thông tin, ta sẽ chỉ định dòng và cột cần thiết, và thông tin được đọc ra từ vị trí (dòng và cột) được tìm thấy. Tương tự như vậy, một mảng hai chiều là một khung lưới chứa các dòng và cột trong đó mỗi phần tử được xác định duy nhất bằng toạ độ dòng và cột của nó. Một mảng hai chiều tmp có kiểu int với 2 dòng và 3 cột có thể được khai báo như sau:
int tmp[2][3];
Mảng này sẽ chứa 2 x 3 (6) phần tử, và chúng có thể được biểu diễn như sau:
Ở đó e1 – e6 biểu diễn cho các phần tử của mảng. Cả dòng và cột được đánh số từ 0. Phần tử e6 được xác định bằng dòng 1 và cột 2. Truy xuất đến phần tử này như sau:
tmp[1][2];
Ø Khởi tạo mảng đa chiều:
Khai báo mảng đa chiều có thể kết hợp với việc gán các giá trị khởi tạo. Cần phải cẩn thận lưu ý đến thứ tự các giá trị khởi tạo được gán cho các phần tử của mảng (chỉ có mảng external và static có thể được khởi tạo). Các phần tử trong dòng đầu tiên của mảng hai chiều sẽ được gán giá trị trước, sau đó đến các phần tử của dòng thứ hai, … Hãy xem sự khai báo mảng sau:
int ary[3][4] ={1,2,3,4,5,6,7,8,9,10,11,12};
Kết quả của phép khai báo trên sẽ như sau:
ary[0][0] = 1 ary[0][1] = 2 ary[0][2] = 3 ary[0][3]= 4
ary[1][0] = 5 ary[1][1] = 6 ary[1][2] = 7 ary[1][3] = 8
ary[2][0] = 9 ary[2][1] = 10 ary[2][2] = 11 ary[2][3] = 12
Chú ý rằng chỉ số thứ 1 chạy từ 0 đến 2 và chỉ số thứ hai chạy tử 0 đến 3. Một điểm cần nhớ là các phần tử của mảng sẽ được lưu trữ ở những vị trí kế tiếp nhau trong bộ nhớ. Mảng ary ở trên có thể xem như là một mảng của 3 phần tử, mỗi phần tử là một mảng của 4 số nguyên, và sẽ xuất hiện như sau:
Dòng 0
Dòng 1
Dòng 2
1 2 3 4
5 6 7 8
9 10 11 12
Thứ tự tự nhiên mà các giá trị khởi tạo được gán có thể thay đổi bằng hình thức nhóm các giá trị khởi tạo lại trong các dấu ngoặc nhọn {}. Quan sát sự khởi tạo sau:
int ary [3][4] ={
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
Mảng sẽ được khởi tạo như sau:
ary[0][0]=1 ary[0][1]=2 ary[0][2]=3 ary[0][3]=0
ary[1][0]=4 ary[1][1]=5 ary[1][2]=6 ary[1][3]=0
ary[2][0]=7 ary[2][1]=8 ary[2][2]=9 ary[2][3]=0
Một phần tử của mảng đa chiều có thể được sử dụng như một biến trong C bằng cách dùng các chỉ số để xác định phần tử của mảng.
Ví dụ 11.5:
/* Chương trình nhập các số vào một mảng hai chiều. */
#include <stdio.h>
#include <conio.h>
main()
{
int arr[2][3];
int row, col;
for(row = 0; row < 2; row++)
{
for(col = 0; col < 3; col++)
{
printf("Enter a Number at [%d][%d]: ", row, col);
scanf("%d", &arr[row][col]);
}
}
for(row = 0; row < 2; row++)
{
for(col = 0; col < 3; col++)
{
printf("
The Number at [%d][%d] is %d",
row, col, arr[row][col]);
}
}
getch();
}
Mảng hai chiều và chuỗi:
Như chúng ta đã biết ở phần trước, một chuỗi có thể được biểu diễn bằng mảng một chiều, kiểu ký tự. Mỗi ký tự trong chuỗi được lưu trữ trong một phần tử của mảng. Mảng của chuỗi có thể được tạo bằng cách sử dụng mảng ký tự hai chiều. Chỉ số bên trái xác định số lượng chuỗi, và chỉ số bên phải xác định chiều dài tối đa của mỗi chuỗi. Ví dụ bên dưới khai báo một mảng chứa 25 chuỗi và mỗi chuỗi có độ dài tối đa 80 ký tự kể cả ký tự null.
char str_ary[25][80];
Ø Ví dụ minh hoạ cách sử dụng của một mảng hai chiều:
Ví dụ bên dưới minh hoạ cách dùng của mảng hai chiều như các chuỗi.
Xét bài toán tổ chức một danh sách tên theo thứ tự bảng chữ cái. Ví dụ sau đây nhập một danh sách các tên và sau đó sắp xếp chúng theo thứ tự bảng chữ cái.
Ví dụ 11.6
#include <stdio.h>
#include <string.h>
#include <conio.h>
main()
{
int i, n = 0;
int item;
char x[10][12];
char temp[12];
printf("Enter each string on a separate line
");
printf("Type 'END' when over
");
/* Read in the list of strings */
do
{
printf("String %d: ", n + 1);
scanf("%s", x[n]);
} while (strcmp(x[n++], "END"));
/*Reorder the list of strings */
n = n-1;
for(item = 0; item < n - 1; ++item)
{
/* Find lowest of remaining strings */
for(i = item + 1; i < n; ++i)
{
if(strcmp(x[item], x[i]) > 0)
{
/*Interchange two strings*/
strcpy(temp, x[item]);
strcpy(x[item], x[i]);
strcpy(x[i], temp);
}
}
}
/* Display the arranged list of strings */
printf("Recorded list of strings:
");
for(i = 0; i < n; ++i)
{
printf("
String %d is %s", i + 1, x[i]);
}
getch();
}
Chương trình trên nhập vào các chuỗi đến khi người dùng nhập vào từ “END”. Khi END được nhập vào, chương trình sẽ sắp xếp danh sách các chuỗi và in ra theo thứ tự đã sắp xếp. Chương trình kiểm tra hai phần tử kế tiếp nhau. Nếu thứ tự của chúng không thích hợp, thì hai phần tử sẽ được đổi chỗ. Sự so sánh hai chuỗi được thực hiện với sự trợ giúp của hàm strcmp() và sự đổi chỗ được thực hiện với hàmg strcpy().
13.1 Con trỏ là gì?
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:
Vị trí Giá trị Tên
Bộ nhớ lưu trữ biến
1000 500 var1
1001
1002
.
.
1108 1000 var2
Ở đây, var2 chứa giá trị 1000, đó là địa chỉ của biến var1.
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)
13.2 Các biến con trỏ
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;
temp 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--
Trỏ đến số nguyên đứng trước var
ptr_var + i
Trỏ đến số nguyên thứ i sau var
ptr_var - i
Trỏ đến số nguyên thứ i trước var
++*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 a được lưu trữ ở vị trí sau 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.
13.4 Con trỏ và mảng một chiều
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};
int i;
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:");
gets(str);
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;
int i;
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();
Hàm calloc()
calloc tương tự như malloc, nhưng khác biệt chính là mặc nhiên các giá trị được lưu trong không gian bộ nhớ đã cấp phát là 0. Với malloc, cấp phát bộ nhớ có thể có giá trị bất kỳ.
calloc đòi hỏi hai đối số. Đối số thứ nhất là số các biến mà bạn muốn cấp phát bộ nhớ cho. Đối số thứ hai là kích thước của mỗi biến.
void *calloc( size_t num, size_t size );
Giống như malloc, calloc sẽ trả về một con trỏ rỗng (void) nếu sự cấp phát bộ nhớ là thành công, ngược lại nó sẽ trả về một con trỏ NULL.
Ví dụ bên dưới chỉ ra cho bạn gọi hàm calloc như thế nào và tham chiếu đến ô nhớ đã cấp phát sử dụng một chỉ số mảng. Giá trị khởi tạo của vùng nhớ đã cấp phát được in ra trong vòng lặp for.
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
int main()
{
float *calloc1, *calloc2;
int i;
calloc1 = (float *) calloc(3, sizeof(float));
calloc2 = (float *) calloc(3, sizeof(float));
if(calloc1 != NULL && calloc2 != NULL)
{
for(i = 0; i < 3; i++)
{
printf("
calloc1[%d] holds %05.5f ", i, calloc1[i]);
printf("
calloc2[%d] holds %05.5f", i, *(calloc2 + i));
}
free(calloc1);
free(calloc2);
}
else
{
printf("Not enough memory
");
}
getch();
}
Trong tất cả các máy, các mảng calloc1 và calloc2 phải chứa các giá trị 0. calloc đặc biệt hữu dụng khi bạn đang sử dụng mảng đa chiều. Đây là một ví dụ khác minh họa cách dùng của hàm calloc().
/* This program gets the number of elements, allocates
spaces for the elements, gets a value for each
element, sum the values of the elements, and print
the number of the elements and the sum.
*/
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
main()
{
int *a, i, n, sum = 0;
printf("
%s%s","An array will be created dynamically.
","Input an array size n followed by integers: ");
scanf("%d", &n); //get the number of elements
a = (int *) calloc (n, sizeof(int)); //allocate space
//get a value for each element
for( i = 0; i < n; i++ )
{
printf("Enter %d values: ", n);
scanf("%d", a + i);
}
//sum the values
for(i = 0; i < n; i++ )
sum += a[i];
free(a); //free the space
//print the number and the sum
printf("
%s%7d
%s%7d
", "Number of elements: ", n,"Sum of the elements: ", sum);
getch();
Giả sử chúng ta đã cấp phát một số bytes cho một mảng nhưng sau đó nhận ra là bạn muốn thêm các giá trị. Bạn có thể sao chép mọi thứ vào một mảng lớn hơn, cách này không hiệu quả. Hoặc bạn có thể cấp phát thêm các bytes sử dụng bằng cách gọi hàm realloc, mà dữ liệu của bạn không bị mất đi.
realloc() nhận hai đối số. Đối số thứ nhất là một con trỏ tham chiếu đến bộ nhớ. Đối số thứ hai là tổng số bytes bạn muốn cấp phát thêm:
void *realloc( void *ptr, size_t size );
Truyền 0 như là đối số thứ hai thì tương đương với việc gọi hàm free.
Một lần, realloc trả về một con trỏ rỗng (void) nếu thành công, ngược lại một con trỏ NULL được trả về.
Ví dụ này sử dụng calloc để cấp phát đủ bộ nhớ cho một mảng int có năm phần tử. Sau đó realloc được gọi để mở rộng mảng để có thể chứa bảy phần tử.
#include<stdio.h>
#include <conio.h>
#include <stdlib.h>
main()
{
int *ptr;
int i;
ptr = (int *)calloc(5, sizeof(int *));
if(ptr!=NULL)
{
*ptr = 1;
*(ptr + 1) = 2;
ptr[2] = 4;
ptr[3] = 8;
ptr[4] = 16;
/* ptr[5] = 32; wouldn't assign anything */
ptr = (int *)realloc(ptr, 7 * sizeof(int));
if(ptr!=NULL)
{
printf("Now allocating more memory...
");
ptr[5] = 32; /* now it's legal! */
ptr[6] = 64;
for(i = 0;i < 7; i++)
{
printf("ptr[%d] holds %d
", i, ptr[i]);
}
realloc(ptr, 0); /* same as free(ptr); - just fancier! */
}
else
printf("Not enough memory - realloc failed.
");
}
else
printf("Not enough memory - calloc failed.
");
getch();
Chú ý hai cách khác nhau được sử dụng khi khởi tạo mảng: ptr[2] = 4 là tương đương với *(ptr + 2) = 4 (chỉ dễ đọc hơn!).
Trước khi sử dụng realloc, việc gán một giá trị đến phần tử ptr[5] không gây ra lỗi cho trình biên dịch. Chương trình vẫn thực thi, nhưng ptr[5] không chứa giá trị mà bạn đã gán.
ử dụng các hàm
Nói chung, các hàm được sử dụng trong C để thực thi một chuỗi các lệnh liên tiếp. Tuy nhiên, cách sử dụng các hàm thì không giống với các vòng lặp. Các vòng lặp có thể lặp lại một chuỗi các chỉ thị với các lần lặp liên tiếp nhau. Nhưng việc gọi một hàm sẽ sinh ra một chuỗi các chỉ thị được thực thi tại vị trí bất kỳ trong chương trình. Các hàm có thể được gọi nhiều lần khi có yêu cầu. Giả sử một phần của mã lệnh trong một chương trình dùng để tính tỉ lệ phần trăm cho một vài con số. Nếu sau đó, trong cùng chương trình, việc tính toán như vậy cần phải thực hiện trên những con số khác, thay vì phải viết lại các chỉ thị giống như trên, một hàm có thể được viết ra để tính tỉ lệ phần trăm của bất kỳ các con số. Sau đó chương trình có thể nhảy đến hàm đó, để thực hiện việc tính toán (trong hàm) và trở về nơi nó đã được gọi. Điều này sẽ được giải thích rõ ràng hơn khi thảo luận về cách hoạt động của các hàm.
Một điểm quan trọng khác là các hàm thì dễ viết và dễ hiểu. Các hàm đơn giản có thể được viết để thực hiện các tác vụ xác định. Việc gỡ rối chương trình cũng dễ dàng hơn khi cấu trúc chương trình dễ đọc, nhờ vào sự đơn giản hóa hình thức của nó. Mỗi hàm có thể được kiểm tra một cách độc lập với các dữ liệu đầu vào, với dữ liệu hợp lệ cũng như không hợp lệ. Các chương trình chứa các hàm cũng dễ bảo trì hơn, bởi vì những sửa đổi, nếu yêu cầu, có thể được giới hạn trong các hàm của chương trình. Một hàm không chỉ được gọi từ các vị trí bên trong chương trình, mà các hàm còn có thể đặt vào một thư viện và được sử dụng bởi nhiều chương trình khác, vì vậy tiết kiệm được thời gian viết chương trình.
15.2 Cấu trúc hàm
Cú pháp tổng quát của một hàm trong C là:
type_specifier function_name (arguments)
{
//body of the function
//return statement
}
type_specifier xác định kiểu dữ liệu của giá trị sẽ được trả về bởi hàm. Nếu không có kiểu được đưa ra, hàm cho rằng trả về một kết quả số nguyên. Các đối số được phân cách bởi dấu phẩy. Một cặp dấu ngoặc rỗng () vẫn phải xuất hiện sau tên hàm ngay cả khi nếu hàm không chứa bất kỳ đối số nào. Các tham số xuất hiện trong cặp dấu ngoặc () được gọi là tham số hình thức hoặc đối số hình thức. Phần thân của hàm có thể chứa một hoặc nhiều câu lệnh. Một hàm nên trả về một giá trị và vì vậy ít nhất một lệnh return phải có trong hàm.
15.2.1 Các đối số của một hàm
Trước khi thảo luận chi tiết về các đối số, xem ví dụ sau:
#include <stdio.h>
main()
{
int i;
for(i =1; i <=10; i++)
printf(“
Square of %d is %d “, i,squarer (i));
}
squarer(int x)
/* int x; */
{
int j;
j = x * x;
return(j);
}
Chương trình trên tính tính bình phương các số từ 1 đến 10. Điều này được thực hiện bằng việc gọi hàm squarer. Dữ liệu được truyền từ thủ tục gọi (trong trường hợp trên là hàm main()) đến hàm được gọi squarer thông qua các đối số. Trong thủ tục gọi, các đối số được biết như là các đối số thực và trong định nghĩa của hàm được gọi (squarer()) các đối số được gọi là các đối số hình thức.
Kiểu dữ liệu của các đối số thực phải cùng kiểu với các đối số hình thức. Hơn nữa, số lượng và thứ tự của các tham số thực phải giống như của các tham số hình thức.
Khi một hàm được gọi, quyền điều khiển sẽ được chuyển đến cho nó, ở đó các đối số hình thức được thay thế bởi các đối số thực. Sau đó hàm được thực thi và khi bắt gặp câu lệnh return, nó sẽ chuyển quyền điều khiển cho chương trình gọi nó.
Hàm squarer() được gọi bằng cách truyền số cần được tính bình phương. Đối số x có thể được khai báo theo một trong các cách sau khi định nghĩa hàm:
Phương pháp 1:
squarer(int x)
//x được định nghĩa cùng với kiểu dữ liệu trong cặp dấu ngoặc ().
Phương pháp 2:
squarer(x)
int x;
//x được đặt trong cặp dấu ngoặc (), và kiểu của nó được khai báo ngay sau tên hàm.
Chú ý:
· Trong trường hợp 1: Khi các đối số được khai báo trong cặp dấu ngoặc (), mỗi đối số phải được định nghĩa riêng lẻ, cho dù chúng có cùng kiểu dữ liệu. Ví dụ, nếu x và y là hai đối số của một hàm abc(), thì abc(char x, char y) là một khai báo đúng và abc(char x, y) là sai.
· Trong trường hợp 2: x phải được định nghĩa ngay sau tên hàm, trước khối lệnh. Điều này thật tiện lợi khi có nhiều tham số có cùng kiểu dữ liệu được truyền. Trong trường hợp như vậy, chỉ phải chỉ rõ kiểu đề một lần duy nhất tại điểm bắt đầu.
15.2.2 Sự trả về từ hàm
Lệnh return có hai mục đích:
Ø Ngay lập tức trả điều khiển từ hàm về chương trình gọi.
Ø Bất kỳ cái gì bên trong cặp dấu ngoặc () theo sau return được trả về như là một giá trị cho chương trình gọi.
Trong hàm squarer(), một biến j kiểu int được định nghĩa để lưu giá trị bình phương của đối số truyền vào. Giá trị của biến này được trả về cho hàm gọi thông qua lệnh return. Một hàm có thể thực hiện một tác vụ xác định và trả quyền điều khiển về cho thủ tục gọi nó mà không cần trả về bất kỳ giá trị nào. Trong trường hợp như vậy, lệnh return có thể được viết dạng return(0) hoặc return. Chú ý rằng, nếu một hàm cung cấp một giá trị trả về và nó không làm điều đó thì nó sẽ trả về giá trị không thích hợp.
Trong chương trình tính bình phương của các số, chương trình truyền dữ liệu tới hàm squarer thông qua các đối số. Có thể có các hàm được gọi mà không cần bất kỳ đối số nào. Ở đây, hàm thực hiện một chuỗi các lệnh và trả về giá trị, nếu được yêu cầu.
Chú ý rằng, hàm squarer() cũng có thể được viết như sau
squarer(int x)
{
return(x*x);
}
Ở đây một biểu thức hợp lệ được xem như một đối số trong câu lệnh return. Trong thực tế, lệnh return có thể được sử dụng theo một trong các cách sau đây:
return;
return(hằng);
return(biến);
return(biểu thức);
return(câu lệnh đánh giá); ví dụ: return(a>b?a:b);
Tuy nhiên, giới hạn của lệnh return là nó chỉ có thể trả về một giá trị duy nhất.
15.2.3 Kiểu của một hàm
type-specifier được sử dụng để xác định kiểu dữ liệu trả về của một hàm. Trong ví dụ trên, type-specifier không được viết bên cạnh hàm squarer(), vì squarer() trả về một giá trị kiểu int. type-specifier là không bắt buộc nếu một giá trị kiểu số nguyên được trả về hoặc nếu không có giá trị nào được trả về. . Tuy nhiên, tốt hơn nên chỉ ra kiểu dữ liệu trả về là int nếu một giá trị số nguyên được trả về và tương tự dùng void nếu hàm không trả về giá trị nào.
15.3 Gọi hàm
Có thể gọi một hàm từ chương trình chính bằng cách sử dụng tên của hàm, theo sau là cặp dấu ngoặc (). Cặp dấu ngoặc là cần thiết để nói với trình biên dịch là đây là một lời gọi hàm. Khi một tên hàm được sử dụng trong chương trình gọi, tên hàm có thể là một phần của một một lệnh hoặc chính nó là một câu lệnh. Mà ta đã biết một câu lệnh luôn kết thúc với một dấu chấm phẩy (;). Tuy nhiên, khi định nghĩa hàm, không được dùng dấu chấm phầy ở cuối phần định nghĩa. Sự vắng mặt của dấu chấm phẩy nói với trình biên dịch đây là phần định nghĩa của hàm và không được gọi hàm.
Một số điểm cần nhớ:
Ø Một dấu chấm phẩy được dùng ở cuối câu lệnh khi một hàm được gọi, nhưng nó không được dùng sau một sự định nghĩa hàm.
Ø Cặp dấu ngoặc () là bắt buộc theo sau tên hàm, cho dù hàm có đối số hay không.
Ø Hàm gọi đến một hàm khác được gọi là hàm gọi hay thủ tục gọi. Và hàm được gọi đến còn được gọi là hàm được gọi hay thủ tục được gọi.
Ø Các hàm không trả về một giá trị số nguyên cần phải xác định kiểu của giá trị được trả về.
Ø Chỉ một giá trị có thể được trả về bởi một hàm.
Ø Một chương trình có thể có một hoặc nhiều hàm.
15.4 Khai báo hàm
Một hàm nên được khai báo trong hàm main() trước khi nó được định nghĩa hoặc sử dụng. Điều này phải được thực hiện trong trường hợp hàm được gọi trước khi nó được định nghĩa.
Xem ví dụ:
#include <stdio.h>
main()
{
…
address(…);
…
}
address(…)
{
…
}
Hàm main() gọi hàm address() và hàm address() được gọi trước khi nó được định nghĩa. Mặc dù, nó không được khai báo trong hàm main() thì điều này có thể thực hiện được trong một số trình biên dịch C, hàm address() được gọi mà không cần khai báo gì thêm cả. Đây là sự khai báo không tường minh của một hàm.
Trong trình biên dịch Dev-C++, ta cần phải khai báo nguyên mẫu hàm trước hàm main() nếu như muốn định nghĩa hàm đó sau hàm main(), ví dụ:
#include <stdio.h>
address(…);
main()
{
…
address();
…
}
address(…)
{
…
}
15.5 Các nguyên mẫu hàm
Một nguyên mẫu hàm là một khai báo hàm trong đó xác định rõ kiểu dữ liệu của các đối số và giá trị trả về. Thông thường, các hàm được khai báo bằng cách xác định kiểu của giá trị được trả về bởi hàm, và tên hàm. Tuy nhiên, chuẩn ANSI C cho phép số lượng và kiểu dữ liệu của các đối số hàm được khai báo. Một hàm abc() có hai đối số kiểu int là x và y, và trả về một giá trị kiểu char, có thể được khai báo như sau:
char abc();
hoặc
char abc(int x, nt y);
Cách định nghĩa sau được gọi là nguyên mẫu hàm. Khi các nguyên mẫu được sử dụng, C có thể tìm và thông báo bất kỳ kiểu dữ liệu không hợp lệ khi chuyển đổi giữa các đối số được dùng để gọi một hàm với sự định nghĩa kiểu của các tham số. Một lỗi sẽ được thông báo ngay khi có sự khác nhau giữa số lượng các đối số được sử dụng để gọi hàm và số lượng các tham số khi định nghĩa hàm.
Cú pháp tổng quát của một nguyên mẫu hàm:
type function_name(type parm_namel,type parm_name2,..type
parm_nameN);
Khi hàm được khai báo không có các thông tin nguyên mẫu, trình biên dịch cho rằng không có thông tin về các tham số được đưa ra. Một hàm không có đối số có thể gây ra lỗi khi khai báo không có thông tin nguyên mẫu. Để tránh điều này, khi một hàm không có tham số, nguyên mẫu của nó sử dụng void trong cặp dấu ngoặc (). Như đã nói ở trên, void cũng được sử dụng để khai báo tường minh một hàm không có giá trị trả về.
Ví dụ, nếu một hàm noparam() trả về kiểu dữ liệu char và không có các tham số được gọi, có thể được khai báo như sau:
char noparam(void);
Khai báo trên chỉ ra rằng hàm không có tham số, và bất kỳ lời gọi có truyền tham số đến hàm đó là không đúng.
Khi một hàm không nguyên mẫu được gọi, tất cả các kiểu char được đổi thành kiểu int và tất cả kiểu float được đổi thành kiểu double. Tuy nhiên, nếu một hàm là nguyên mẫu, thì các kiểu đã đưa ra trong nguyên mẫu được giữ nguyên và không có sự tăng cấp kiểu xảy ra.
15.6 Các biến
Như đã thảo luận, các biến là những vị trí được đặt tên trong bộ nhớ, được sử dụng để chứa giá trị có thể hoặc không thể được sửa đổi bởi một chương trình hoặc một hàm. Có ba loại biến cơ bản: biến cục bộ, tham số hình thức, và biến toàn cục.
1-Biến cục bộ: là những biến được khai báo bên trong một hàm.
2-Tham số hình thức: được khai báo trong một định nghĩa hàm như là các tham số.
3-Biến toàn cục: được khai báo bên ngoài các hàm.
15.6.1 Biến cục bộ
Biến cục bộ còn được gọi là biến động, từ khoá auto được sử dụng để khai báo chúng. Chúng chỉ được tham chiếu đến bởi các lệnh bên trong của khối lệnh mà biến được khai báo. Để rõ hơn, một biến cục bộ được tạo ra trong lúc vào một khối và bị huỷ trong lúc đi ra khỏi khối đó. Khối lệnh thông thường nhất mà trong đó một biến cục bộ được khai báo chính là hàm.
Xem đoạn mã lệnh sau:
void blkl(void) /* void denotes no value returned*/
{
char ch;
ch = ‘a’;
…
}
void blk2(void)
{
char ch;
ch = ‘b’;
…
}
Biến ch được khai báo hai lần, trong blk1() và blk2(). ch trong blk1() không có liên quan đến ch trong blk2() bởi vì mỗi ch chỉ được biết đến trong khối lệnh mà nó được khai báo.
Vì các biến cục bộ được tạo ra và huỷ đi trong một khối mà chúng được khai báo, nên nội dung của chúng bị mất bên ngoài phạm vi của khối. Điều này có nghĩa là chúng không thể duy trì giá trị của chúng giữa các lần gọi hàm.
Từ khóa auto có thể được dùng để khai báo các biến cục bộ, nhưng thường nó không được dùng vì mặc nhiên các biến không toàn cục được xem như là biến cục bộ.
Các biến cục bộ được sử dụng bởi các hàm thường được khai báo ngay sau dấu ngoặc mở ‘{‘ của hàm và trước tất cả các câu lệnh. Tuy nhiên, các khai báo có thể ở bên trong một khối của một hàm. Ví dụ:
void blk1(void)
{
int t;
t = 1;
if(t > 5)
{
char ch;
…
}
}
Trong ví dụ trên biến ch được tạo ra và chỉ hợp lệ bên trong khối mã lệnh if. Nó không thể được tham chiếu đến trong một phần khác của hàm blk1().
Một trong những thuận lợi của sự khai báo một biến theo cách này đó là bộ nhớ sẽ chỉ được cấp phát cho nó khi nếu điều kiện để đi vào khối lệnh if được thoả. Điều này là bởi vì các biến cục bộ chỉ được khai báo khi đi vào khối lệnh mà các biến được định nghĩa trong đó.
Chú ý: Điều quan trọng cần nhớ là tất cả các biến cục bộ phải được khai báo tại điểm bắt đầu của khối mà trong đó chúng được định nghĩa, và trước tất cả các câu lệnh thực thi.
15.6.2 Tham số hình thức
Một hàm sử dụng các đối số phải khai báo các biến để nhận các giá trị của các đối số. Các biến này được gọi là tham số hình thức của hàm và hoạt động giống như bất kỳ một biến cục bộ bên trong hàm.
Các biến này được khai báo bên trong cặp dấu ngoặc () theo sau tên hàm. Xem ví dụ sau:
blk1(char ch, int i)
{
if(i > 5)
ch = ‘a’;
else
i = i +1;
return;
}
Hàm blk1() có hai tham số: ch và i.
Các tham số hình thức phải được khai báo cùng với kiểu của chúng. Như trong ví dụ trên, ch có kiều char và i có kiểu int. Các biến này có thể được sử dụng bên trong hàm như các biến cục bộ bình thường. Chúng bị huỷ đi khi ra khỏi hàm. Cần chú ý là các tham số hình thức đã khai báo có cùng kiểu dữ liệu với các đối số được sử dụng khi gọi hàm. Trong trường hợp có sai, C có thể không hiển thị lỗi nhưng có thể đưa ra một kết quả không mong muốn. Điều này là vì, C vẫn đưa ra một vài kết quả trong các tình huống khác thường. Người lập trình phải đảm bảo rằng không có các lỗi về sai kiểu.
Cũng giống như với các biến cục bộ, các phép gán cũng có thể được thực hiện với tham số hình thức của hàm và chúng cũng có thể được sử dụng bất kỳ biểu thức nào mà C cho phép.
15.6.3 Biến toàn cục
Các biến toàn cục là biến được thấy bởi toàn bộ chương trình, và có thể được sử dụng bởi một mã lệnh bất kỳ. Chúng được khai báo bên ngoài các hàm của chương trình và lưu giá trị của chúng trong suốt sự thực thi của chương trình. Các biến này có thể được khai báo bên ngoài main() hoặc khai báo bất kỳ nơi đâu trước lần sử dụng đầu tiên. Tuy nhiên, nơi tốt nhất để khai báo các biến toàn cục là tại đầu chương trình, nghĩa là trước hàm main().
int ctr; /* ctr is global */
void blk1(void);
void blk2(void);
void main(void)
{
ctr = 10;
blk1 ();
…
}
void blk1(void)
{
int rtc;
if (ctr > 8)
{
rtc = rtc + 1;
blk2();
}
}
void blk2(void)
{
int ctr;
ctr = 0;
}
Trong đoạn mã lệnh trên, ctr là một biến toàn cục và được khai báo bên ngoài hàm main() và blk1(), nó có thể được tham chiếu đến trong các hàm. Biến ctr trong blk2(), là một biến cục bộ và không có liên quan với biến toàn cục ctr. Nếu một biến toàn cục và cục bộ có cùng tên: tất cả các tham chiếu đến tên đó bên trong khối chứa định nghĩa biến cục bộ sẽ được kết hợp với biến cục bộ mà không phải là biến toàn cục.
Các biến toàn cục được lưu trữ trong các vùng cố định của bộ nhớ. Các biến toàn cục hữu dụng khi nhiều hàm trong chương trình sử dụng cùng dữ liệu. Tuy nhiên, nên tránh sử dụng biến toàn cục nếu không cần thiết, vì chúng giữ bộ nhớ trong suốt thời gian thực hiện chương trình. Vì vậy việc sử dụng một biến toàn cục ở nơi mà một biến cục bộ có khả năng đáp ứng cho hàm sử dụng là không hiệu quả. Ví dụ sau sẽ giúp làm rõ hơn điều này:
void addgen(int i, int j)
{
return(i + j);
}
int i, j;
void addspe(void)
{
return(i + j);
}
Cả hai hàm addgen() và addspe() đều trả về tổng của các biến i và j. Tuy nhiên, hàm addgen() được sử dụng để trả về tổng của hai số bất kỳ; trong khi hàm addspe() chỉ trả về tổng của các biến toàn cục i và j.
15.7 Lớp lưu trữ (Storage Class)
Mỗi biến trong C có một đặc trưng được gọi là lớp lưu trữ. Lớp lưu trữ xác định hai khía cạnh của biến: thời gian sống của biến và phạm vi của biến. Thời gian sống của một biến là thời gian mà giá trị của biến tồn tại. Phạm vi của một biến xác định các phần của một chương trình sẽ có thể nhận ra biến. Một biến có thể xuất hiện trong một khối, một hàm, một tập tin, một nhóm các tập tin, hoặc toàn bộ chương trình
Theo cách nhìn của trình biên dịch C, một tên biến xác định một vài vị trí vật lý bên trong máy tính, ở đó một chuỗi các bit biểu diễn giá trị được lưu trữ của biến. Có hai loại vị trí trong máy tính mà ở đó giá trị của biến có thể được lưu trữ: bộ nhớ hoặc thanh ghi CPU. Lớp lưu trữ của biến xác định vị trí biến được lưu trữ là trong bộ nhớ hay trong một thanh ghi. C có bốn lớp lưu trữ. Đó là:
Ø auto
Ø external
Ø static
Ø register
Đó là các từ khoá. Cú pháp tổng quát cho khai báo biến như sau:
storage_specifier type var_name;
15.7.1 Biến tự động (auto)
Biến tự động thật ra là biến cục bộ mà chúng ta đã nói ở trên. Phạm vi của một biến tự động có thể nhỏ hơn hàm, nếu nó được khai báo bên trong một câu lệnh ghép: phạm vi của nó bị giới hạn trong câu lệnh ghép đó. Chúng có thể được khai báo bằng từ khóa auto, nhưng sự khai báo này là không cần thiết. Bất kỳ một biến được khai báo bên trong một hàm hoặc một khối lệnh thì mặc nhiên là thuộc lớp auto và hệ thống cung cấp vùng bộ nhớ được yêu cầu cho biến đó.
15.7.2 Biến ngoại (extern)
Trong C, một chương trình lớn có thể được chia thành các module nhỏ hơn, các module này có thể được biên dịch riêng lẻ và được liên kết lại với nhau. Điều này được thực hiện nhằm tăng tốc độ quá trình biên dịch các chương trình lớn.
Tuy nhiên, khi các module được liên kết, các tập tin phải được chương trình thông báo cho biết về các biến toàn cục được yêu cầu. Một biến toàn cục chỉ có thể được khai báo một lần. Nếu hai biến toàn cục có cùng tên được khai báo trong cùng một tập tin, một thông điệp lỗi ‘duplicate variable name’ (tên biến trùng) có thể được hiển thị hoặc đơn giản trình biên dịch C chọn một biến khác. Một lỗi tương tự xảy ra nếu tất cả các biến toàn cục được yêu cầu bởi chương trình chứa trong mỗi tập tin.
Mặc dù trình biên dịch không đưa ra bất kỳ một thông báo lỗi nào trong khi biên dịch, nhưng sự thật các bản sao của cùng một biến đang được tạo ra. Tại thời điểm liên kết các tập tin, bộ liên kết sẽ hiển thị một thông báo lỗi như sau ‘duplicate label’ (nhãn trùng nhau) vì nó không biết sử dụng biến nào.
Lớp extern được dùng trong trường hợp này. Tất cả các biến toàn cục được khai báo trong một tập tin và các biến giống nhau được khai báo là biến ngoại, trong tất cả các tập tin.
Xem đoạn mã lệnh sau:
File1 File2
int i,j; extern int i,j;
char a; extern char a;
main() xyz()
{ {
… i = j * 5
… …
} }
abc() pqr()
{ {
i = 123; j = 50;
… …
} }
File2 có các biến toàn cục giống như File1, ngoại trừ một điểm là các biến này có từ khóa extern được thêm vào sự khai báo của chúng. Từ khóa này nói với trình biên dịch là tên và kiểu của biến toàn cục được sử dụng mà không cần phải tạo lại sự lưu trữ cho chúng. Khi hai module được liên kết, các tham chiếu đến các biến ngoại được giải quyết.
Nếu một biến không được khai báo trong một hàm, trình biên dịch sẽ kiểm tra nó có so khớp với bất kỳ biến toàn cục nào không. Nếu khớp với một biến toàn cục, thì trình biên dịch sẽ xem như một biến toàn cục đang được tham chiếu đến.
15.7.3 Biến tĩnh (static)
Các biến tĩnh là các biến cố định bên trong các hàm và các tập tin. Không giống như các biến toàn cục, chúng không được biết đến bên ngoài hàm hoặc tập tin của chúng, nhưng chúng giữ được giá trị của chúng giữa các lần gọi. Điều này có nghĩa là, nếu một hàm kết thúc và sau đó được gọi lại, các biến tĩnh đã định nghĩa trong hàm đó vẫn giữ được giá trị của chúng. Sự khai báo biến tĩnh được bắt đầu với từ khóa static.
Có thể định nghĩa các biến tĩnh có cùng tên như hướng dẫn với các biến ngoại. Các biến cục bộ (biến tĩnh cũng như biến động) có độ ưu tiên cao hơn các biến ngoại và giá trị của các biến ngoại sẽ không ảnh hưởng bởi bất kỳ sự thay đổi nào các biến cục bộ. Các biến ngoại có cùng tên với các biến nội trong một hàm không thể được truy xuất trực tiếp bên trong hàm đó.
Các giá trị khởi tạo có thể được gán cho các biến trong sự khai báo các biến tĩnh, nhưng các giá trị này phải là các hằng hoặc các biểu thức. Trình biên dịch tự động gán một giá trị mặc nhiên 0 đến các biến tĩnh không được khởi tạo. Sự khởi tạo thực hiện ở đầu chương trình.
Xem hai chương trình sau. Sự khác nhau giữa 2 biến cục bộ: tự động và tĩnh sẽ được làm rõ.
Ví dụ về biến tự động:
#include <stdio.h>
#include <conio.h>
void incre();
main()
{
incre();
incre();
incre();
getch();
}
void incre()
{
char var = 65; /* var is automatic variable*/
printf("
The character stored in var is %c", var++);
}
Ví dụ về biến tĩnh:
#include<stdio.h>
#include <conio.h>
void incre();
main()
{
incre();
incre();
incre();
getch();
}
void incre()
{
static char var = 65; /* var is static variable */
printf("
The character stored in var is %c", var++);
}
Cả hai chương trình gọi incre() ba lần. Trong chương trình thứ nhất, mỗi lần incre() được gọi, biến var với lớp lưu trữ auto (lớp lưu trữ mặc định) được khởi tạo lại là 65 (là mã ASCII tương ứng của ký tự A). Vì vậy khi kết thúc hàm, giá trị mới của var (66) bị mất đi (ASCII ứng với ký tự B).
Trong chương trình thứ hai, var là của lớp lưu trữ static. Ở đây var được khởi tạo là 65 chỉ một lần duy nhất khi biên dịch chương trình. Cuối lần gọi hàm đầu tiên, var có giá trị 66 (ASCII B) và tương tự ở lần gọi kế tiếp var có giá trị 67 (ASCII C). Sau lần gọi hàm cuối cùng, var được tăng giá trị theo sự thi hành của lệnh printf(). Giá trị này bị mất khi chương trình kết thúc.
15.7.4 Biến thanh ghi (register)
Các máy tính có các thanh ghi trong bộ số học logic - Arithmetic Logic Unit (ALU), các thanh ghi này được sử dụng để tạm thời lưu trữ dữ liệu được truy xuất thường xuyên. Kết quả tức thời của phép tính toán cũng được lưu vào các thanh ghi. Các thao tác thực hiện trên dữ liệu lưu trữ trong các thanh ghi thì nhanh hơn dữ liệu trong bộ nhớ. Trong ngôn ngữ assembly (hợp ngữ), người lập trình phải truy xuất đến các thanh ghi này và sử dụng chúng để giúp chương trình chạy nhanh hơn. Các ngôn ngữ lập trình bậc cao thường không truy xuất đến các thanh ghi của máy tính. Trong C, việc lựa chọn vị trí lưu trữ cho một giá trị tùy thuộc vào người lập trình.
Nếu một giá trị đặc biệt được dùng thường xuyên (ví dụ giá trị điều khiển của một vòng lặp), lớp lưu trữ của nó có thể khai báo là register.
Sau đó nếu trình biên dịch tìm thấy một thanh ghi còn trống, và các thanh ghi của máy tính đủ lớn để chứa biến, biến sẽ được đặt vào thanh ghi đó. Ngược lại, trình biên dịch sẽ xem các biến thanh ghi như các biến động khác, nghĩa là lưu trữ chúng trong bộ nhớ. Từ khóa register được dùng khi định nghĩa các biến thanh ghi.
Phạm vi và sự khởi tạo của các biến thanh ghi giống như các biến động, ngoại trừ vị trí lưu trữ. Các biến thanh ghi là cục bộ trong một hàm. Nghĩa là, chúng tồn tại khi hàm được gọi và giá trị bị mất đi một khi thoát khỏi hàm. Sự khởi tạo các biến này được thực hiện bởi người lập trình.
Vì số lượng các thanh ghi là có hạn, lập trình viên cần xác định các biến nào trong chương trình được sử dụng thường xuyên để khai báo chúng là các biến thanh ghi.
Sự hữu dụng của các biến thanh ghi thay đổi từ máy này đến một máy khác và từ một trình biên dịch C này đến một trình biên dịch khác. Đôi khi các biến thanh ghi không được hỗ trợ bởi tất cả – từ khóa register vẫn được chấp nhận nhưng được xem giống như là từ khóa auto. Trong các trường hợp khác, nếu biến thanh ghi được hỗ trợ và nếu lập trình viên sử dụng chúng một cách hợp lý, chương trình sẽ được thực thi nhanh hơn gấp đôi.
Các biến thanh ghi được khai báo như bên dưới:
register int x;
register char c;
Sự khai báo thanh ghi chỉ có thể gắn vào các biến động và tham số hình thức. Trong trường hợp sau, sự khai báo sẽ giống như sau:
f(c,n)
register int c, n;
{
register int i;
...
}
Xét một ví dụ sau, ở đó chương trình hiển thị các số có tổng lập phương các số thành phần bằng chính nó. Ví dụ 370 là một số như vậy, vì:
3^3 + 7^3 + 0^3 = 27 + 343 + 0 = 370
Chương trình sau in ra các con số như vậy trong khoảng 1 đến 999:
#include <stdio.h>
#include <conio.h>
main()
{
register int i;
int no, digit, sum;
printf("
The numbers whose Sum of Cubes of Digits
is Equal to the number itself are:
");
for(i = 1; i < 999; i++)
{
sum = 0;
no = i;
while(no)
{
digit = no%10;
no = no/10;
sum = sum + digit * digit * digit;
}
if (sum == i)
printf("\t%d
", i);
}
getch();
Trong chương trình trên, giá trị của i , thay đổi từ 1 đến 999. Với mỗi giá trị này, lập phương của từng con số riêng lẻ được cộng và kết quả tổng được so sánh với i. Nếu hai giá trị này là bằng nhau, i được hiển thị. Vì i được sử dụng để điều khiển sự lặp, (phần chính của chương trình), nó được khai báo là của lớp lưu trữ thanh ghi. Sự khai báo này làm tăng hiệu quả của chương trình.
15.8 Các qui luật về phạm vi của một hàm
Qui luật về phạm vi là những qui luật quyết định một đoạn mã lệnh có thể truy xuất đến một đoạn mã lệnh khác hoặc dữ liệu hay không. Trong C, mỗi hàm của chương trình là các khối lệnh riêng lẻ. Mã lệnh bên trong một hàm là cục bộ với hàm đó và không thể được truy xuất bởi bất kỳ lệnh nào ở ngoài hàm, ngoại trừ lời gọi hàm. Mã lệnh bên trong một hàm là ẩn đối với phần còn lại của chương trình, và trừ khi nó sử dụng biến hoặc dữ liệu toàn cục, nó có thể tác động hoặc bị tác động bởi các phần khác của chương trình. Để rõ hơn, mã lệnh và dữ liệu được định nghĩa bên trong một hàm không thể tương tác với mã lệnh hay dữ liệu được định nghĩa trong hàm khác bởi vì hai hàm có phạm vi khác nhau.
Trong C, tất cả các hàm có cùng mức phạm vi. Nghĩa là, một hàm không thể được định nghĩa bên trong một hàm khác. Chính vì lý do này mà C không phải là một ngôn ngữ cấu trúc khối về mặt kỹ thuật.
15.9 Gọi hàm
Một cách tổng quát, các hàm giao tiếp với nhau bằng cách truyền tham số. Các tham số được truyền theo một trong hai cách sau:
Ø Truyền bằng giá trị.
Ø Truyền bằng tham chiếu.
15.9.1 Truyền bằng giá trị
Mặc nhiên trong C, tất cả các đối số của hàm được truyền bằng giá trị. Điều này có nghĩa là, khi các đối số được truyền đến hàm được gọi, các giá trị được truyền thông qua các biến tạm. Mọi sự thao tác chỉ được thực hiện trên các biến tạm này. Hàm được gọi không thể thay đổi giá trị của chúng. Xem ví dụ sau:
#include <stdio.h>
#include <conio.h>
int adder(int a, int b);
main()
{
int a, b, c;
a = b = c = 0;
printf("
Enter 1st integer: ");
scanf("%d", &a);
printf("
Enter 2nd integer: ");
scanf("%d", &b);
c = adder(a,b);
printf("
a & b in main() are: %d, % d", a, b);
printf("
c in main() is: %d", c);
/* c gives the addition of a and b */
getch();
}
int adder(int a, int b)
{
int c;
c = a + b;
a *= a;
b += 5;
printf("
a & b within adder function are: %d, %d ", a, b);
printf("
c within adder function is : %d",c);
return(c);
}
Chương trình trên nhận hai số nguyên, hai số này được truyền đến hàm adder(). Hàm adder() thực hiện như sau: nó nhận hai số nguyên như là các đối số của nó, cộng chúng lại, tính bình phương cho số nguyên thứ nhất, và cộng 5 vào số nguyên thứ hai, in kết quả và trả về tổng của các đối số thực. Các biến được sử dụng trong hàm main() và adder() có cùng tên. Tuy nhiên, không có gì là chung giữa chúng. Chúng được lưu trữ trong các vị trí bộ nhớ khác nhau. Điều này được thấy rõ từ kết quả của chương trình trên. Các biến a và b trong hàm adder() được thay đổi từ 2 và 4 thành 4 và 9. Tuy nhiên, sự thay đổi này không ảnh hưởng đến các giá trị của a và b trong hàm main(). Các biến được lưu ở những vị trí bộ nhớ khác nhau. Biến c trong main() thì khác với biến c trong adder().
Vì vậy, các đối số được gọi là truyền bằng giá trị khi giá trị của các biến được truyền đến hàm được gọi và bất kỳ sự thay đổi nào trên giá trị này cũng không ảnh hưởng đến giá trị gốc của biến đã truyền.
15.9.2 Truyền bằng tham chiếu
Khi các đối số được truyền bằng giá trị, các giá trị của đối số của hàm đang gọi không bị thay đổi. Tuy nhiên, có thể có trường hợp, ở đó giá trị của các đối số phải được thay đổi. Trong những trường hợp như vậy, truyền bằng tham chiếu được dùng. Truyền bằng tham chiếu, hàm được phép truy xuất đến vùng bộ nhớ thực của các đối số và vì vậy có thể thay đổi giá trị của các đối số của hàm gọi.
Ví dụ, xét một hàm, hàm này nhận hai đối số, hoán vị giá trị của chúng và trả về các giá trị của chúng. Nếu một chương trình giống như chương trình dưới đây được viết để giải quyết mục đích này, thì sẽ không bao giờ thực hiện được.
#include <stdio.h>
#include <conio.h>
void swap(int u, int v);
main()
{
int x, y;
x = 15; y = 20;
printf("
x = %d, y = %d
", x, y);
swap(x, y);
printf("
After interchanging x = %d, y = %d
", x, y);
getch();
}
void swap(int u, int v)
{
int temp;
temp = u;
u = v;
v = temp;
return;
}
Hàm swap() hoán vị các giá trị của u và v, nhưng các giá trị này không được truyền trở về hàm main(). Điều này là bởi vì các biến u và v trong swap() là khác với các biến u và v được dùng trong main(). Truyền bằng tham chiếu có thể được sử dụng trong trường hợp này để đạt được kết quả mong muốn, bởi vì nó sẽ thay đổi các giá trị của các đối số thực. Các con trỏ được dùng khi thực hiện truyền bằng tham chiếu.
Các con trỏ được truyền đến một hàm như là các đối số để cho phép hàm được gọi của chương trình truy xuất các biến mà phạm vi của nó không vượt ra khỏi hàm gọi. Khi một con trỏ được truyền đến một hàm, địa chỉ của dữ liệu được truyền đến hàm nên hàm có thể tự do truy xuất nội dung của địa chỉ đó. Các hàm gọi nhận ra bất kỳ thay đổi trong nội dung của địa chỉ. Theo cách này, đối số hàm cho phép dữ liệu được thay đổi trong hàm gọi, cho phép truyền dữ liệu hai chiều giữa hàm gọi và hàm được gọi. Khi các đối số của hàm là các con trỏ hoặc mảng, truyền bằng tham chiếu được tạo ra đối nghịch với cách truyền bằng giá trị.
Các đối số hình thức của một hàm là các con trỏ thì phải có một dấu ‘*‘ phía trước, giống như sự khai báo biến con trỏ, để xác định chúng là các con trỏ. Các đối số thực kiểu con trỏ trong lời gọi hàm có thể được khai báo là một biến con trỏ hoặc một biến được tham chiếu đến (&var).
Ví dụ, định nghĩa hàm:
getstr(char *ptr_str, int *ptr_int)
đối số ptr_str trỏ đến kiểu char và ptr_int trỏ đến kiểu int. Hàm có thể được gọi bằng câu lệnh:
getstr(pstr, &var)
ở đó pstr được khai báo là một con trỏ và địa chỉ của biến var được truyền. Gán giá trị thông qua:
*ptr_int = var;
Hàm bây giờ có thể gán các giá trị đến biến var trong hàm gọi, cho phép truyền theo hai chiều đến và từ hàm.
char *pstr;
Quan sát ví dụ sau của hàm swap(). Bài toán này sẽ giải quyết được khi con trỏ được truyền thay vì dùng biến.
Mã lệnh tương tự như sau:
#include <stdio.h>
#include <conio.h>
void swap(int *u, int *v);
main()
{
int x, y, *px, *py;
//Storing address of x in px
px = &x;
//Storing address of y in py
py = &y;
x = 15; y = 20;
printf("
x = %d, y = %d
", x, y);
swap (px, py);
//Passing addresses of x and y
printf("
After interchanging x = %d, y = %d
", x, y);
getch();
}
void swap(int *u, int *v)
//Accept the values of px and py into u and v
{
int temp;
temp = *u;
*u = *v;
*v = temp;
return;
}
Hai biến kiểu con trỏ px và py được khai báo, và địa chỉ của biến x và y được gán đến chúng. Sau đó các biến con trỏ được truyền đến hàm swap(), hàm này hoán vị các giá trị lưu trong x và y thông qua các con trỏ.
15.10 Sự lồng nhau của lời gọi hàm
Lời gọi một hàm từ một hàm khác được gọi là sự lồng nhau của lời gọi hàm. Một chương trình kiểm tra một chuỗi có phải là chuỗi đọc xuôi - đọc ngược như nhau hay không, là một ví dụ cho các lời gọi hàm lồng nhau. Từ đọc xuôi - ngược giống nhau là một chuỗi các ký tự đối xứng. Xem đoạn mã lệnh theo sau đây:
main()
{
…
palindrome();
…
}
palindrome()
{
…
getstr();
reverse();
cmp();
…
}
Trong chương trình trên, hàm main() gọi hàm palindrome(). Hàm palindrome() gọi đến ba hàm khác getstr(), reverse() và cmp(). Hàm getstr() để nhận một chuỗi ký tự từ người dùng, hàm reverse() đảo ngược chuỗi và hàm cmp() so sánh chuỗi được nhập vào và chuỗi đã được đảo.
Vì main() gọi palindrome(), hàm palindrome() lần lượt gọi các hàm getstr(), reverse() và cmp(), các lời gọi hàm này được gọi là được lồng bên trong palindrome().
Sự lồng nhau của các lời gọi hàm như trên là được phép, trong khi định nghĩa một hàm bên trong một hàm khác là không được chấp nhận trong C.
15.11 Hàm trong chương trình nhiều tập tin
Các chương trình có thể được tạo bởi nhiều tập tin. Những chương trình như vậy được tạo bởi các hàm lớn, ở đó mỗi hàm có thể chiếm một tập tin. Cũng như các biến trong các chương trình nhiều tập tin, các hàm cũng có thể được định nghĩa là static hoặc extern. Phạm vi của hàm extern có thể được sử dụng trong tất cả các tập tin của chương trình, và đó là cách lưu trữ mặc định cho các tập tin. Các hàm static chỉ được nhận biết bên trong tập tin chương trình và phạm vi của nó không vượt khỏi tập tin chương trình. Phần tiêu đề (header) của hàm như sau:
static fn _type fn_name (argument list)
hoặc
extern fn_type fn_name (argument list)
Từ khóa extern là một tuỳ chọn (không bắt buộc) vì nó là lớp lưu trữ mặc định.
15.12 Con trỏ đến hàm
Một đặc tính mạnh mẽ của C vẫn chưa được đề cập, chính là con trỏ hàm. Dù rằng một hàm không phải là một biến, nhưng nó có địa chỉ vật lý trong bộ nhớ nơi có thể gán cho một con trỏ. Một địa chỉ hàm là điểm đi vào của hàm và con trỏ hàm có thể được sử dụng để gọi hàm.
Để hiểu các con trỏ hàm làm việc như thế nào, thật sự cần phải hiểu thật rõ một hàm được biên dịch và được gọi như thế nào trong C. Khi mỗi hàm được biên dịch, mã nguồn được chuyển thành mã đối tượng và một điểm đi vào của hàm được thiết lập. Khi một lời gọi được thực hiện đến một hàm, một lời gọi ngôn ngữ máy được thực hiện để chuyển điều khiển đến điểm đi vào của hàm. Vì vậy, nếu một con trỏ chứa địa chỉ của điểm đi vào của hàm, nó có thể được dùng để gọi hàm đó.
Địa chỉ của một hàm có thể lấy được bằng cách sử dụng tên hàm không có dấu ngoặc () hay bất kỳ đối số nào.
Một chương trình ứng dụng có thể quản lý nhiều loại dữ liệu. Trong trường hợp này, chương trình phải chỉ định bộ nhớ cho mỗi đơn vị dữ liệu. Khi chỉ định bộ nhớ, có hai điểm cần lưu ý như sau :
1. Bao nhiêu bộ nhớ sẽ được gán
2. Mỗi đơn vị dữ liệu được lưu trữ ở đâu trong bộ nhớ.
Trước đây, các lập trình viên phải viết chương trình theo ngôn ngữ máy gồm các mã 1 và 0. Nếu muốn lưu trữ một giá trị tạm thời, vị trí chính xác nơi mà dữ liệu được lưu trữ trong bộ nhớ máy tính phải được chỉ định. Vị trí này là một con số cụ thể, gọi là địa chỉ bộ nhớ.
Các ngôn ngữ lập trình hiện đại cho phép chúng ta sử dụng các tên tượng trưng gọi là biến (variable), chỉ đến một vùng bộ nhớ nơi mà các giá trị cụ thể được lưu trữ.
Kiểu dữ liệu quyết định tổng số bộ nhớ được chỉ định. Những tên được gán cho biến giúp chúng ta sử dụng lại dữ liệu khi cần đến.
Chúng ta đã quen với cách sử dụng các ký tự đại diện trong một công thức. Ví dụ, diện tích hình chữ nhật được tính bởi :
Diện tích = A = chiều dài x chiều rộng = L x B
Cách tính lãi suất đơn giản được cho như sau:
Tiền lãi = I = Số tiền ban đầu x Thời gian x Tỷ lệ/100 = P x T x R /100
Các ký tự A, L, B, I, P, T, R là các biến và là các ký tự viết tắt đại diện cho các giá trị khác nhau.
Xem ví dụ sau đây :
Tính tổng điểm cho 5 sinh viên và hiển thị kết quả. Việc tính tổng được thực hiện theo hướng dẫn sau:
Hiển thị giá trị tổng của 24, 56, 72, 36 và 82
Khi giá trị tổng được hiển thị, giá trị này không còn được lưu trong bộ nhớ máy tính. Giả sử, nếu chúng ta muốn tính điểm trung bình, thì giá trị tổng đó phải được tính một lần nữa.
Tốt hơn là chúng ta sẽ lưu kết quả vào bộ nhớ máy tính, và sẽ lấy lại nó khi cần đến.
sum = 24 + 56 + 72 + 36 + 82
Ở đây, sum là biến được dùng để chứa tổng của 5 số. Khi cần tính điểm trung bình, có thể thực hiện như sau:
Avg = sum / 5
Trong C, tất cả biến cần phải được khai báo trước khi dùng chúng.
Chúng ta hãy xét ví dụ nhập hai số và hiển thị tổng của chúng trong ví dụ 1.
2.2 Hằng (constant)
Trong trường hợp ta dùng biến, giá trị được lưu sẽ thay đổi. Một biến tồn tại từ lúc khai báo đến khi thoát khỏi phạm vi dùng nó. Những câu lệnh trong phạm vi khối mã này có thể truy cập giá trị của biến, và thậm chí có thể thay đổi giá trị của biến. Trong thực tế, đôi khi cần sử dụng một vài khoản mục mà giá trị của chúng không bao giờ bị thay đổi.
Một hằng là một giá trị không bao giờ bị thay đổi. Ví dụ, 5 là một hằng, mà giá trị toán học luôn là 5 và không thể bị thay đổi bởi bất cứ ai. Tương tự, ‘Black’ là một hằng, nó biểu thị cho màu đen. Khi đó, 5 được gọi là hằng số (numeric constant), ‘Black’ được gọi là hằng chuỗi (string constant).
2.3 Định danh (Identifier)
Tên của các biến (variables), các hàm (functions), các nhãn (labels) và các đối tượng khác nhau do người dùng định nghĩa gọi là định danh. Những định danh này có thể chứa một hay nhiều ký tự. Ký tự đầu tiên của định danh phải là một chữ cái hay một dấu gạch dưới ( _ ). Các ký tự tiếp theo có thể là các chữ cái, các con số hay dấu gạch dưới.
Arena, s_count, marks40, và class_one là những định danh đúng. Các ví dụ về các định danh sai là 1sttest, oh!god, và start... end.
Các định danh có thể có chiều dài tuỳ ý, nhưng số ký tự trong một biến được nhận diện bởi trình biên dịch thì thay đổi theo trình biên dịch. Ví dụ, nếu một trình biên dịch nhận diện 31 con số có ý nghĩa đầu tiên cho một tên định danh thì các câu sau sẽ hiển thị cùng một kết quả:
Đây là biến testing.... testing
Đây là biến testing.... testing ... testing
Các định danh trong C có phân biệt chữ hoa và chữ thường, cụ thể, arena thì khác ARENA.
2.3.1 Các nguyên tắc cho việc chỉ đặt tên
Các quy tắc đặt tên biến khác nhau tuỳ ngôn ngữ lập trình. Tuy nhiên, vài quy ước chuẩn được tuân theo như :
Ø Tên biến phải bắt đầu bằng một ký tự chữ cái.
Ø Các ký tự theo sau ký tự đầu bằng một chuỗi các chữ cái hoặc con số và cũng có thể bao gồm ký tự đặc biệt như dấu gạch dưới.
Ø Tránh dùng ký tự O tại những vị trí mà có thể gây lầm lẫn với số không (0) và tương tự chữ cái l (chữ thường của chữ hoa L) có thể lầm lẫn với số 1.
Ø Tên riêng nên tránh đặt tên cho biến.
Ø Theo tiêu chuẩn C các chữ cái thường và hoa thì xem như khác nhau ví dụ. biến ADD, add và Add là khác nhau.
Ø Việc phân biệt chữ hoa và chữ thường khác nhau tuỳ theo ngôn ngữ lập trình. Do đó, tốt nhất nên đặt tên cho biến theo cách thức chuẩn.
Ø Tên một biến nên có ý nghĩa, gợi tả và mô tả rõ kiểu dữ liệu của nó. Ví dụ, nếu tìm tổng của hai số thì tên biến lưu trữ tổng nên đặt là sum (tổng). Nếu đặt tên là s hay ab12 thì không hay lắm.
2.3.2 Từ khóa (Keywords)
Tất cả các ngôn ngữ dành một số từ nhất định cho mục đích riêng. Những từ này có một ý nghĩa đặc biệt trong ngữ cảnh của từng ngôn ngữ, và được xem là “từ khóa”. Khi đặt tên cho các biến, chúng ta cần bảo đảm rằng không dùng bất cứ từ khóa nào làm tên biến.
Tên kiểu dữ liệu tất cả được coi là từ khóa.
Do vậy, đặt tên cho một biến là int sẽ phát sinh một lỗi, nhưng đặt tên cho biến là integer thì không.
Vài ngôn ngữ lập trình yêu cầu lập trình viên chỉ ra tên của các biến cũng như kiểu dữ liệu của nó trước khi dùng biến đó thật sự. Bước này được gọi là khai báo biến. Ta sẽ nói rõ bước này trong phần tiếp theo khi thảo luận về các kiểu dữ liệu. Ðiều quan trọng cần nhớ bây giờ là bước này giúp hệ điều hành thật sự cấp phát một khoảng không gian vùng nhớ cho biến trước khi bắt đầu sử dụng nó.
2.4 Các kiểu dữ liệu (Data types)
Các loại dữ liệu khác nhau được lưu trữ trong biến là
Các số nguyên.
Ví dụ : 10 hay 178993455.
· Các số thực.
Các số thực.
Ví dụ : 15.22 hay 15463452.25.
· Các số dương.
· Các số âm.
Tên.
Ví dụ : John
Giá trị luận lý.
Ví dụ : Y hay N.
Khi dữ liệu được lưu trữ trong các biến có kiểu dữ liệu khác nhau, nó yêu cầu dung lượng bộ nhớ sẽ khác nhau.
Dung lượng bộ nhớ được chỉ định cho một biến tùy thuộc vào kiểu dữ liệu của nó.
Ðể chỉ định bộ nhớ cho một đơn vị dữ liệu, chúng ta phải khai báo một biến với một kiểu dữ liệu cụ thể.
Khai báo một biến có nghĩa là một vùng nhớ nào đó đã được gán cho biến. Vùng bộ nhớ đó sau này sẽ được tham chiếu thông qua tên của biến. Dung lượng bộ nhớ được cấp cho biến bởi hệ điều hành phụ thuộc vào kiểu dữ liệu được lưu trữ trong biến. Vì vậy, một kiểu dữ liệu sẽ mô tả loại dữ liệu phù hợp với biến.
Dạng thức chung cho việc khai báo một biến:
Dạng thức chung cho việc khai báo một biến:
Kiểu dữ liệu (Tên biến)
Kiểu dữ liệu thường được dùng trong các công cụ lập trình có thể được phân chia thành:
1 Kiểu dữ liệu số - lưu trữ giá trị số.
2 Kiểu dữ liệu ký tự – lưu trữ thông tin mô tả
Những kiểu dữ liệu này có thể có tên khác nhau trong các ngôn ngữ lập trình khác nhau. Ví dụ, một kiểu dữ liệu số được gọi trong C là int trong khi đó tại Visual Basic được gọi là integer. Tương tự, một kiểu dữ liệu ký tự được đặt tên là char trong C trong khi đó trong Visual Basic nó được đặt tên là string. Trong bất cứ trường hợp nào, các dữ liệu được lưu trữ luôn giống nhau. Ðiểm khác duy nhất là các biến được dùng trong một công cụ phải được khai báo theo tên của kiểu dữ liệu được hỗ trợ bởi chính công cụ đó.
C có 5 kiểu dữ liệu cơ bản. Tất cả những kiểu dữ liệu khác dựa vào một trong số những kiểu này. 5 kiểu dữ liệu đó là:
int là một số nguyên, về cơ bản nó biểu thị kích cỡ tự nhiên của các số nguyên
float và double được dùng cho các số có dấu chấm động. Kiểu float (số thực) chiếm 4 byte và có thể có tới 6 con số phần sau dấu thập phân, trong khi double chiếm 8 bytes và có thể có tới 10 con số phần thập phân
char chiếm 1 byte và có khả năng lưu một ký tự đơn (character)
void được dùng điển hình để khai báo một hàm không trả về giá trị. Ðiều này sẽ được nói rõ hơn trong phần hàm.
Dung lượng nhớ và phạm vi giá trị của những kiểu này thay đổi theo mỗi loại bộ xử lý và việc cài đặt các trình biên dịch C khác nhau.
Kiểu dữ liệu int
Là kiểu dữ liệu lưu trữ dữ liệu số và là một trong những kiểu dữ liệu cơ bản trong bất cứ ngôn ngữ lập trình nào. Nó bao gồm một chuỗi của một hay nhiều con số.
Thí dụ trong C, để lưu trữ một giá trị số nguyên trong một biến tên là ‘num’, ta khai báo như sau:
int num;
Biến num không thể lưu trữ bất cứ kiểu dữ liệu nào như “Alan” hay “abc”. Kiểu dữ liệu số này cho phép các số nguyên trong phạm vi -32768 tới 32767 được lưu trữ. Hệ điều hành cấp phát 16 bit (2 byte) cho một biến đã được khai báo kiếu int. Ví dụ: 12322, 0, -232.
Nếu chúng ta gán giá trị 12322 cho num thì biến này là biến kiểu số nguyên và 12322 là hằng số nguyên.
Kiểu dữ liệu số thực (float)
Một biến có kiểu dữ liệu số thực được dùng để lưu trữ các giá trị chứa phần thập phân. Trình biên dịch phân biệt các kiểu dữ liệu float và int.
Ðiểm khác nhau chính của chúng là kiểu dữ liệu int chỉ bao gồm các số nguyên, trong khi kiểu dữ liệu float có thể lưu giữ thêm cả các phân số.
Ví dụ, trong C, để lưu trữ một giá trị float trong một biến tên gọi là ‘num’, việc khai báo sẽ như sau :
float num;
Biến đã khai báo là kiểu dữ liệu float có thể lưu giá trị thập phân có độ chính xác tới 6 con số. Biến này được cấp phát 32 bit (4 byte) của bộ nhớ. Ví dụ: 23.05, 56.5, 32.
Nếu chúng ta gán giá trị 23.5 cho num, thì biến num là biến số thực và 23.5 là một hằng số thực.
Kiểu dữ liệu double
Kiểu dữ liệu double được dùng khi giá trị được lưu trữ vượt quá giới hạn về dung lượng của kiểu dữ liệu float. Biến có kiểu dữ liệu là double có thể lưu trữ nhiều hơn khoảng hai lần số các chữ số của kiểu float.
Số các chữ số chính xác mà kiểu dữ liệu float hoặc double có thể lưu trữ tùy thuộc vào hệ điều hành cụ thể của máy tính.
Các con số được lưu trữ trong kiểu dữ liệu float hay double được xem như nhau trong hệ thống tính toán. Tuy nhiên, sử dụng kiểu dữ liệu float tiết kiệm bộ nhớ một nửa so với kiểu dữ liệu double.
Kiểu dữ liệu double cho phép độ chính xác cao hơn (tới 10 con số). Một biến khai báo kiểu dữ liệu double chiếm 64 bit (8 byte) trong bộ nhớ.
Thí dụ trong C, để lưu trữ một giá trị double cho một biến tên ‘num’, khai báo sẽ như sau:
double num;
Nếu chúng ta gán giá trị 23.34232324 cho num, thì biến num là biến kiểu double và 23.34232324 là một hằng kiểu double.
Kiểu dữ liệu char
Kiểu dữ liệu char được dùng để lưu trữ một ký tự đơn.
Một kiểu dữ liệu char có thể lưu một ký tự đơn được bao đóng trong hai dấu nháy đơn (‘’). Thí dụ kiểu dữ liệu char như: ‘a’, ‘m’, ‘$’ ‘%’.
Ta có thể lưu trữ những chữ số như những ký tự bằng cách bao chúng bên trong cặp dấu nháy đơn. Không nên nhầm lẫn chúng với những giá trị số. Ví dụ, ‘1’, ‘5’ và ‘9’ sẽ không được nhầm lẫn với những số 1, 5 và 9.
Xem xét những câu lệnh của mã C dưới đây:
char gender;
gender='M';
Hàng đầu tiên khai báo biến gender của kiểu dữ liệu char. Hàng thứ hai lưu giữ một giá trị khởi tạo cho nó là ‘M’. Biến gender là một biến ký tự và ‘M’ là một hằng ký tự. Biến này được cấp phát 8 bit (1 byte) trong bộ nhớ.
Kiểu dữ liệu void
C có một kiểu dữ liệu đặc biệt gọi là void. Kiểu dữ liệu này chỉ cho trình biên dịch C biết rằng không có dữ liệu của bất cứ kiểu nào. Trong C, các hàm số thường trả về dữ liệu thuộc một kiểu nào đó. Tuy nhiên, khi một hàm không có gì để trả về, kiểu dữ liệu void được sử dụng để chỉ ra điều này.
2.4.1 Những kiểu dữ liệu cơ bản và dẫn xuất
Bốn kiểu dữ liệu (char, int, float và double) mà chúng ta đã thảo luận ở trên được sử dụng cho việc trình bày dữ liệu thực sự trong bộ nhớ của máy tính. Những kiểu dữ liệu này có thể được sửa đổi sao cho phù hợp với những tình huống khác nhau một cách chính xác. Kết quả, chúng ta có được các kiểu dữ liệu dẫn xuất từ những kiểu cơ bản này.
Một bổ từ (modifier) được sử dụng để thay đổi kiểu dữ liệu cơ bản nhằm phù hợp với các tình huống đa dạng. Ngoại trừ kiểu void, tất cả các kiểu dữ liệu khác có thể cho phép những bổ từ đứng trước chúng. Bổ từ được sử dụng với C là signed, unsigned, long và short. Tất cả chúng có thể được áp dụng cho dữ liệu kiểu ký tự và kiểu số nguyên. Bổ từ long cũng có thể được áp dụng cho double.
Một vài bổ từ như :
1. unsigned
2. long
3. short
Ðể khai báo một biến kiểu dẫn xuất, chúng ta cần đặt trước khai báo biến thông thường một trong những từ khóa của bổ từ. Một giải thích chi tiết về các bổ từ này và cách thức sử dụng chúng được trình bày bên dưới.
Ø Các kiểu có dấu (signed) và không dấu(unsigned)
Khi khai báo một số nguyên, mặc định đó là một số nguyên có dấu. Tính quan trọng nhất của việc dùng signed là để bổ sung cho kiểu dữ liệu char, vì char là kiểu không dấu theo mặc định.
Kiểu unsigned chỉ rõ rằng một biến chỉ có thể có giá trị dương. Bổ từ này có thể được sử dụng với kiểu dữ liệu int và kiểu dữ liệu float. Kiểu unsigned có thể áp dụng cho kiểu dữ liệu float trong vài trường hợp nhưng điều này giảm bớt tính khả chuyển (portability) của mã lệnh.
Với việc thêm từ unsigned vào trước kiểu dữ liệu int, miền giá trị cho những số dương có thể được tăng lên gấp đôi.
Ta xem những câu lệnh của mã C cung cấp ở bên dưới, nó khai báo một biến theo kiểu unsigned int và khởi tạo biến này có giá trị 23123.
unsigned int varNum;
varNum = 23123;
Chú ý rằng không gian cấp phát cho kiểu biến này vẫn giữ nguyên. Nghĩa là, biến varNum được cấp phát 2 byte như khi nó dùng kiểu int. Tuy nhiên, những giá trị mà một kiểu unsgned int hỗ trợ sẽ nằm trong khoảng từ 0 đến 65535, thay vì là -32768 tới 32767 mà kiểu int hỗ trợ. Theo mặc định, int là một kiểu dữ liệu có dấu.
Ø Các kiểu long và short
Chúng được sử dụng khi một số nguyên có chiều dài ngắn hơn hoặc dài hơn chiều dài bình thường. Một bổ từ short được áp dụng cho kiểu dữ liệu khi chiều dài yêu cầu ngắn hơn chiều dài số nguyên bình thường và một bổ từ long được dùng khi chiều dài yêu cầu dài hơn chiều dài số nguyên bình thường.
Bổ từ short được sử dụng với kiểu dữ liệu int. Nó sửa đổi kiểu dữ liệu int theo hướng chiếm ít vị trí bộ nhớ hơn. Bởi vậy, trong khi một biến kiểu int chiếm giữ 16 bit (2 byte) thì một biến kiểu short int (hoặc chỉ là short), chiếm giữ 8 bit (1 byte) và cho phép những số có trong phạm vi từ -128 tới 127.
Bổ từ long được sử dụng tương ứng một miền giá trị rộng hơn. Nó có thể được sử dụng với int cũng như với kiểu dữ liệu double. Khi được sử dụng với kiểu dữ liệu int, biến chấp nhận những giá trị số trong khoảng từ -2,147,483,648 đến 2,147,483,647 và chiếm giữ 32 bit ( 4 byte). Tương tự, kiểu long double của một biến chiếm giữ 128 bit (16 byte).
Một biến long int được khai báo như sau:
long int varNum;
Nó cũng có thể được khai báo đơn giản như long varNum. Một số long integer có thể được khai báo như long int hay chỉ là long. Tương tự, ta có short int hay short.
Bảng dưới đây trình bày phạm vi giá trị cho các kiểu dữ liệu khác nhau và số bit nó chiếm giữ dựa theo tiêu chuẩn ANSI.
Kiểu
Dung lượng xấp xỉ (đơn vị là bit)
Phạm vi
char
8
-128 tới 127
unsigned
8
0 tới 255
signed char
8
-128 tới 127
int
16
-32,768 tới 32,767
unsigned int
16
0 tới 65,535
signed int
16
Giống như kiểu int
short int
16
-128 tới 127
unsigned short int
16
0 tới 65, 535
signed short int
16
Giống như kiểu short int
long int
32
-2,147,483,648 tới 2,147,483,647
signed long int
32
Giống như kiểu long int
unsigned long int
32
0 tới 4,294,967,295
float
32
6 con số thập phân
double
64
10 con số thập phân
long double
128
10 con số thập phân
Thí dụ sau trình bày cách khai báo những kiểu dữ liệu trên.
Ví dụ 2:
main()
{
char abc; /*abc of type character */
int xyz; /*xyz of type integer */
float length; /*length of type float */
double area; /* area of type double */
long liteyrs; /*liteyrs of type long int */
short arm; /*arm of type short integer*/
}
Chúng ta xem lại ví dụ cộng hai số và hiển thị tổng ở chương trước. Mã giả như sau :
Ví dụ 3:
BEGIN
INPUT A, B
C = A + B
DISPLAY C
END
Trong ví dụ này, các giá trị cho hai biến A và B được nhập. Các giá trị được cộng và tổng được lưu cho biến C bằng cách dùng câu lệnh C = A + B. Trong câu lệnh này, A và B là những biến và ký hiệu + gọi là toán tử. Chúng ta sẽ nói về toán tử số học của C ở phần sau đây. Tuy nhiên, có những loại toán tử khác trong C sẽ được bàn tới ở phần kế tiếp.
2.5 Các toán tử số học (Arithmetic Operators)
Những toán tử số học được sử dụng để thực hiện những thao tác mang tính số học. Chúng được chia thành hai lớp : Toán tử số học một ngôi (unary) và toán tử số học hai ngôi (binary).
Bảng 2.2 liệt kê những toán tử số học và chức năng của chúng.
Các toán tử một ngôi
Chức năng
Các toán tử hai ngôi
Chức năng
-
Lấy đối số
+
Cộng
++
Tăng một giá trị
-
Trừ
--
Giảm một giá trị
*
Nhân
%
Lấy phần dư
/
Chia
^
Lấy số mũ
Bảng 2.2: Các toán tử số học và chức năng
Ø Các toán tử hai ngôi
Trong C, các toán tử hai ngôi có chức năng giống như trong các ngôn ngữ khác. Những toán tử như +, -, * và / có thể được áp dụng cho hầu hết kiểu dữ liệu có sẵn trong C. Khi toán tử / được áp dụng cho một số nguyên hoặc ký tự, bất kỳ phần dư nào sẽ được cắt bỏ. Ví dụ, 5/2 sẽ bằng 2 trong phép chia số nguyên. Toán tử % sẽ cho ra kết quả là số dư của phép chia số nguyên. Ví dụ: 5%2 sẽ có kết quả là 1. Tuy nhiên, % không thể được sử dụng với những kiểu có dấu chấm động.
Chúng ta hãy xem xét một ví dụ của toán tử số mũ.
9^2
Ở đây 9 là cơ số và 2 là số mũ.
Số bên trái của ‘^’ là cơ số và số bên phải ‘^’ là số mũ.
Kết quả của 9^2 là 9*9 = 81.
Thêm ví dụ khác:
5 ^ 3
Có nghĩa là:
5 * 5 * 5
Do đó: 5 ^ 3 = 5 * 5 * 5 = 125.
Ghi chú: Những ngôn ngữ lập trình như Basic, hỗ trợ toán tử mũ. Tuy nhiên, ANSI C không hỗ trợ ký hiệu ^ cho phép tính lũy thừa. Ta có thể dùng cách khác tính lũy thừa trong C là dùng hàm pow() đã được định nghĩa trong math.h. Cú pháp của nó thể hiện qua ví dụ sau:
...
#include<math.h>
void main(void)
{
….
/* the following function will calculate x to the power y. */
z = pow(x, y);
….
}
Ví dụ sau trình bày tất cả toán tử hai ngôi được dùng trong C. Chú ý rằng ta chưa nói về hàm printf() và getchar(). Chúng ta sẽ bàn trong những phần sau.
Ví dụ 4:
#include<stdio.h>
main()
{
int x,y;
x = 5;
y = 2;
printf("The integers are : %d & %d
", x, y);
printf("The addition gives : %d
", x + y);
printf("The subtraction gives : %d
", x - y);
printf("The multiplication gives : %d
", x * y);
printf("The division gives : %d
", x / y);
printf("The modulus gives : %d
", x % y);
getchar();
}
Các toán tử một ngôi (unary)
Các toán tử một ngôi là toán tử trừ một ngôi ‘-’, toán tử tăng ‘++’ và toán tử giảm ‘--’
Toán tử trừ một ngôi
Ký hiệu giống như phép trừ hai ngôi. Lấy đối số để chỉ ra hay thay đổi dấu đại số của một giá trị. Ví dụ:
a = -75;
b = -a;
Kết quả của việc gán trên là a được gán giá trị -75 và b được gán cho giá trị 75 (-(- 75)). Dấu trừ được sử dụng như thế gọi là toán tử một ngôi vì nó chỉ có một toán hạng.
Nói một cách chính xác, không có toán tử một ngôi + trong C. Vì vậy, một lệnh gán như.
invld_pls = +50;
khi mà invld_pls là một biến số nguyên là không hợp lệ trong chuẩn của C. Tuy nhiên, nhiều trình biên dịch không phản đối cách dùng như vậy.
Các toán tử Tăng và Giảm
C bao chứa hai toán tử hữu ích mà ta không tìm thấy được trong những ngôn ngữ máy tính khác. Chúng là ++ và --. Toán tử ++ thêm vào toán hạng của nó một đơn vị, trong khi toán tử -- giảm đi toán hạng của nó một đơn vị.
Cụ thể:
x = x + 1;
có thể được viết là:
x++;
và:
x = x - 1;
có thể được viết là:
x--;
Cả hai toán tử này có thể đứng trước hoặc sau toán hạng, chẳng hạn:
x = x + 1;
có thể được viết lại là
x++ hay ++x;
Và cũng tương tự cho toán tử --.
Sự khác nhau giữa việc xử lý trước hay sau trong toán tử một ngôi thật sự có ích khi nó được dùng trong một biểu thức. Khi toán tử đứng trước toán hạng, C thực hiện việc tăng hoặc giảm giá trị trước khi sử dụng giá trị của toán hạng. Ðây là tiền xử lý (pre-fixing). Nếu toán tử đi sau toán hạng, thì giá trị của toán hạng được sử dụng trước khi tăng hoặc giảm giá trị của nó. Ðây là hậu xử lý (post-fixing). Xem xét ví dụ sau :
a = 10;
b = 5;
c = a * b++;
Trong biểu thức trên, giá trị hiện thời của b được sử dụng cho tính toán và sau đó giá trị của b sẽ tăng sau. Tức là, c được gán 50 và sau đó giá trị của b được tăng lên thành 6.
Tuy nhiên, nếu biểu thức trên là:
c = a * ++b;
thì giá trị của c sẽ là 60, và b sẽ là 6 bởi vì b được tăng 1 trước khi thực hiện phép nhân với a, sau đó giá trị được gán vào c.
Trong trường hợp mà tác động của việc tăng hay giảm là riêng lẻ thì toán tử có thể đứng trước hoặc sau toán hạng đều được.
Hầu hết trình biên dịch C sinh mã rất nhanh và hiệu quả đối với việc tăng và giảm giá trị. Mã này sẽ tốt hơn so với khi ta dùng toán tử gán. Vì vậy, các toán tử tăng và giảm nên được dùng bất cứ khi nào có thể.
Tóm tắt bài học
Ø Thông thường, khi chương trình ứng dụng cần xử lý dữ liệu, nó cần có nơi nào đó để lưu trữ tạm thời dữ liệu này. Nơi mà dữ liệu được lưu trữ gọi là bộ nhớ.
Ø Các ngôn ngữ lập trình hiện đại ngày nay cho phép chúng ta sử dụng các tên tượng trưng gọi là biến (variable), dùng để chỉ đến một vùng trong bộ nhớ nơi mà các giá trị cụ thể được lưu trữ.
Ø Không có giới hạn về số vị trí bộ nhớ mà một chương trình có thể dùng.
Ø Một hằng (constant) là một giá trị không bao giờ bị thay đổi.
Ø Tên của các biến (variable), các hàm (function), các nhãn (label) và các đối tượng khác nhau do người dùng định nghĩa gọi là định danh.
Ø Tất cả ngôn ngữ dành một số từ nhất định cho mục đích riêng. Những từ này được gọi là là “từ khóa” (keywords).
Ø Các kiểu dữ liệu chính của C là character, integer, float, double và void.
Ø Một bổ từ được sử dụng để thay đổi kiểu dữ liệu cơ bản sao cho phù hợp với nhiều tình huống đa dạng. Các bổ từ được sử dụng trong C là signed, unsigned, long và short.
Ø C hỗ trợ hai loại toán tử số học: một ngôi và hai ngôi.
Ø Toán tử tăng ‘++’ và toán tử giảm ‘--’ là những toán tử một ngôi. Nó chỉ hoạt động trên biến kiểu số.
Ø Toán tử hai ngôi số học là +, -, *, /, %, nó chỉ tác động lên những hằng số, biến hay biểu thức.
Ø Toán tử phần dư ‘%’ chỉ áp dụng trên các số nguyên và cho kết quả là phần dư của phép chia số nguyên.
C có một tập các toán tử phong phú. Toán tử là công cụ dùng để thao tác dữ liệu. Một toán tử là một ký hiệu dùng để đại diện cho một thao tác cụ thể nào đó được thực hiện trên dữ liệu. C định nghĩa bốn loại toán tử: toán tử số học (arithmetic), quan hệ (relational), luận lý (logical), và toán tử luận lý nhị phân (bitwise). Bên cạnh đó, C còn có một số toán tử đặc biệt.
Toán tử thao tác trên hằng hoặc biến. Hằng hoặc biến này được gọi là toán hạng (operands). Biến đã được đề cập ở các chương trước. Hằng là những giá trị cố định mà chương trình không thể thay đổi. Hằng trong C có thể là bất cứ kiểu dữ liệu cơ bản nào. Toán tử được phân loại: toán tử một ngôi, hai ngôi hoặc ba ngôi. Toán tử một ngôi chỉ thao tác trên một phần tử dữ liệu, toán tử hai ngôi trên hai phần tử dữ liệu và ba ngôi trên ba phần tử dữ liệu.
Ví dụ 4.1:
c = a + b;
Ở đây a, b, c là những toán hạng, dấu ‘=’ và dấu ‘+’ là những toán tử.
4.1 Biểu thức (Expressions)
Một biểu thức là tổ hợp các toán tử và toán hạng. Toán tử thực hiện các thao tác như cộng, trừ, so sánh v.v... Toán hạng là những biến hay những giá trị mà các phép toán được thực hiện trên nó. Trong ví dụ a + b, “a” và “b” là toán hạng và “+” là toán tử. Tất cả kết hợp lại là một biểu thức.
Trong quá trình thực thi chương trình, giá trị thực sự của biến (nếu có) sẽ được sử dụng cùng với các hằng có mặt trong biểu thức. Việc đánh giá biểu thức được thực hiện nhờ các toán tử. Vì vậy, mọi biểu thức trong C đều có một giá trị.
Các ví dụ về biểu thức là:
2
x
3 + 7
2 × y + 5
2 + 6 × (4 - 2)
z + 3 × (8 - z)
Ví dụ 4.2:
Roland nặng 70 kilograms, và Mark nặng k kilograms. Viết một biểu thức cho tổng cân nặng của họ. Tổng cân nặng của hai người tính bằng kilograms là 70 + k.
Ví dụ 4.3:
Tính giá trị biểu thức 4 × z + 12 với z = 15.
Chúng ta thay thế mọi z với giá trị 15, và đơn giản hóa biểu thức theo quy tắc: thi hành phép toán trong dấu ngoặc trước tiên, kế đến lũy thừa, phép nhân và chia rồi phép cộng và trừ.
4 × z + 12 trở thành
4 × 15 + 12 = (phép nhân thực hiện trước phép cộng)
60 + 12 =
72
Toán tử gán (Assignment Operator)
Trước khi nghiên cứu các toán tử khác, ta hãy xét toán tử gán (=). Ðây là toán tử thông dụng nhất cho mọi ngôn ngữ và mọi người đều biết. Trong C, toán tử gán có thể được dùng cho bất kỳ biểu thức C hợp lệ. Dạng thức chung cho toán tử gán là:
Tên biến = biểu thức;
Gán liên tiếp
Nhiều biến có thể được gán cùng một giá trị trong một câu lệnh đơn. Việc này thực hiện qua cú pháp gán liên tiếp. Ví dụ:
a = b = c =10;
Dòng mã trên gán giá trị 10 cho a, b,và c. Tuy nhiên, việc này không thể thực hiện lúc khai báo biến. Ví dụ:
int a = int b = int c= 0;
Câu lệnh trên phát sinh lỗi vì sai cú pháp.
Biểu thức số học (Arithmetic Expressions)
Các phép toán thường được thực hiện theo một thứ tự cụ thể (hoặc riêng biệt) để cho ra giá trị cuối cùng. Thứ tự này gọi là độ ưu tiên (sẽ nói đến sau).
Các biểu thức toán học trong C được biểu diễn bằng cách sử dụng toán tử số học cùng với các toán hạng dạng số và ký tự. Những biểu thức này gọi là biểu thức số học (Arithmetic Expressions). Ví dụ về biểu thức số học là :
a * (b+c/d)/22;
++i % 7;
5 + (c = 3+8);
Như chúng ta thấy ở trên, toán hạng có thể là hằng, biến hay kết hợp cả hai. Hơn nữa, một biểu thức có thể là sự kết hợp của nhiều biểu thức con. Chẳng hạn, trong biểu thức đầu, c/d là một biểu thức con, và trong biểu thức thứ ba c = 3+8 cũng là một biểu thức con.
4.2 Toán tử quan hệ (Relational Operators)
Toán tử quan hệ được dùng để kiểm tra mối quan hệ giữa hai biến, hay giữa một biến và một hằng. Ví dụ, việc xét số lớn hơn của hai số, a và b, được thực hiện thông qua dấu lớn hơn (>) giữa hai toán hạng a và b (a > b).
Trong C, true (đúng) là bất cứ giá trị nào khác không (0), và false (sai) là bất cứ giá trị nào bằng không (0). Biểu thức dùng toán tử quan hệ trả về 0 cho false và 1 cho true. Ví dụ biểu thức sau :
a == 14 ;
Biểu thức này kiểm tra xem giá trị của a có bằng 14 hay không. Giá trị của biểu thức sẽ là 0 (false) nếu a có giá trị khác 14 và 1 (true) nếu nó là 14.
> lớn hơn >= lon hon bang != khong bang
4.3 Toán tử luận lý (Logical Operators) và biểu thức
Toán tử luận lý là các ký hiệu dùng để kết hợp hay phủ định biểu thức có chứa các toán tử quan hệ.
Những biểu thức dùng toán tử luận lý trả về 0 cho false và 1 cho true.
Bảng sau mô tả ý nghĩa của các toán tử luận lý.
&& AND: trả về kết quả là true khi cả 2 toán hạng đều true || OR : trả về kết quả là true khi chỉ một trong hai toán hạng đều true ! NOT: Chuyển đổi giá trị của toán hạng duy nhất từ true thành false và ngược lại
Bất cứ toán tử luận lý nào có ký hiệu là hai ký tự thì không được có khoảng trắng giữa hai ký tự đó, ví dụ : == sẽ không đúng nếu viết là = =.
Giả sử một chương trình phải thực thi những bước nhất định nếu điều kiện a < 10 và b == 7 được thoả mãn. Ðiều kiện này được viết ra bằng cách dùng toán tử quan hệ kết hợp với toán tử luận lý AND. Toán tử AND được viết là &&. Ta sẽ có điều kiện để kiểm tra như sau :
(a < 10) && (b == 7);
Tương tự, toán tử OR dùng để kiểm tra xem có một trong số các điều kiện kiểm tra là đúng hay không. Nó có dạng là dấu (||). Cùng ví dụ trên nhưng điều kiện cần kiểm tra là: chỉ cần một trong hai câu lệnh là đúng thì ta có mã sau :
(a < 10) || (b == 7);
Toán tử luận lý thứ ba là NOT được biểu diễn bằng ký hiệu dấu chấm than ‘!’. Toán tử này đảo ngược giá trị luận lý của biểu thức. Ví dụ, để kiểm tra xem biến s có bé hơn 10 hay không, ta viết đều kiện kiểm tra như sau:
(s < 10);
hay là
(! (s >= 10)) /* s không lớn hơn hay bằng 10 */
Cả toán tử quan hệ và luận lý có quyền ưu tiên thấp hơn toán tử số học. Ví dụ, 5 > 4 + 3 được tính tương đương với 5 > (4 + 3), nghĩa là 4+3 sẽ được tính trước và sau đó toán tử quan hệ sẽ được thực hiện. Kết quả sẽ là false, tức là trả về 0.
Câu lệnh sau:
printf("%d", 5> 4 + 3);
sẽ cho ra:
0
vì 5 bé hơn (4 + 3) .
4.4 Toán tử luận lý nhị phân (Bitwise Logical Operators) và biểu thứ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;
int i;
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ý.
Thứ tự
Toán tử
1
NOT
2
AND
3
OR
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
Khi một biểu thức có nhiều hơn một kiểu toán tử thì thứ tự ưu tiên phải được thiết lập giữa các kiểu toán tử với nhau.
Bảng dưới đây cho biết thứ tự ưu tiên giữa các kiểu toán tử khác nhau.
Thứ tự
Kiểu toán tử
1
Số học
2
So sánh (Quan hệ)
3
Luận lý
Bảng 4.6. 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.
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():
Ðịnh dạng
printf()
scanf()
Ký tự đơn (Single Character)
%c
%c
Chuỗi (String)
%s
%s
Số nguyên có dấu (Signed decimal integer)
%d
%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
%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
%u
Số thập lục phân không dấu (Dùng “ABCDEF”)
(Unsigned hexadecimal integer)
%x
%x
Số bát phân không dấu (Unsigned octal integer)
%o
%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:
Mã định dạng
Quy ước in ấn
%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ự %
Bảng 6.3: Các ký tự đặc biệt trong chuỗi điều khiển
Bảng dưới đây đưa ra vài ví dụ sử dụng chuỗi điều khiển và mã định dạng khác nhau.
Số
Câu lệnh
Chuỗi điều khiển
Nội dung mà chuỗi điều khiển chứa đựng
Danh sách tham số
Giải thích danh sách tham số
Hiển thị trên màn hình
1.
printf("%d", 300);
%d
Chỉ chứa lệnh định dạng
300
Hằng số
300
2.
printf("%d", 10+5);
%d
Chỉ chứa lệnh định dạng
10 + 5
Biểu thức
15
3.
printf("Good Morning Mr. Lee.");
Good Morning Mr. Lee.
Chỉ là các ký tự văn bản
Không có (Nil)
Không có
Good Morning Mr. Lee.
4.
int count = 100;
printf("%d", count);
%d
Chỉ chứa lệnh định dạng
Count
Biến
100
5.
printf("
hello");
hello
Chỉ là các ký tự văn bản và ký tự không in được.
Không có
Không có
Hello
(Trên dòng mới)
6.
#define str "Good Apple"
……..
printf("%s", str);
%s
Chỉ chứa lệnh định dạng
Str
Hằng chuỗi
Good Apple
7.
……..
int count,stud_num;
count = 0;
stud_num = 100;
printf("%d %d
", count, stud_num);
%d %d
Chỉ chứa lệnh định dạng và trình tự thoát ra
count, stud_num
Hai biến
0, 100
Bảng 6.4 : Chuỗi điều khiển và mã định dạng
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();
}
Kết quả chương trình như sau:
Bổ từ (Modifier) cho các lệnh định dạng trong printf()
Các lệnh định dạng có thể có bổ từ (modifier), để thay đổi các đặc tả chuyển đổi gốc. Sau đây là các bổ từ được chấp nhận trong câu lệnh printf(). Nếu có nhiều bổ từ được dùng thì chúng tuân theo trình tự sau :
Bổ từ ‘-‘
Dữ liệu sẽ được canh trái bên trong không gian dành cho nó, chúng sẽ được in bắt đầu từ vị trí ngoài cùng bên trái.
Bổ từ xác định độ rộng
Chúng có thể được dùng với kiểu: float, double hay char array (chuỗi-string). Bổ từ xác định độ rộng là một số nguyên xác định độ rộng nhỏ nhất của trường dữ liệu. Các dữ liệu có độ rộng nhỏ hơn sẽ cho kết quả canh phải trong trường dữ liệu. Các dữ liệu có kích thước lớn hơn sẽ được in bằng cách dùng thêm những vị trí cho đủ yêu cầu.Ví dụ, %10f là lệnh định dạng cho các mục dữ liệu kiểu số thực với độ rộng trường dữ liệu thấp nhất là 10.
Bổ từ xác định độ chính xác
Chúng có thể được dùng với kiểu float, double hay mảng ký tự (char array, string). Bổ từ xác định độ rộng chính xác được viết dưới dạng .m với m là một số nguyên. Nếu sử dụng với kiểu float và double, chuỗi số chỉ ra số con số tối đa có thể được in ra phía bên phải dấu chấm thập phân.
Nếu phần phân số của các mục dữ liệu kiểu float hay double vượt quá độ rộng con số chỉ trong bổ từ, thì số đó sẽ được làm tròn. Nếu chiều dài chuỗi vượt quá chiều dài chỉ định thì chuỗi sẽ được cắt bỏ phần dư ra ở phía cuối. Một vài số không (0) sẽ được thêm vào nếu số con số thực sự trong một mục dữ liệu ít hơn được chỉ định trong bổ từ. Tương tự, các khoảng trắng sẽ được thêm vào cho chuỗi ký tự. Ví dụ, %10.3f là lệnh định dạng cho mục dữ liệu kiểu float, với độ rộng tối thiểu cho trường dữ liệu là 10 và 3 vị trí sau phần thập phân.
Bổ từ ‘0’
Theo mặc định, việc thêm vào một trường được thực hiện với các khoảng trắng. Nếu người dùng muốn thêm vào trường với số không (0), bổ từ này phải được dùng.
Bổ từ ‘l’
Bổ từ này có thể được dùng để hiển thị số nguyên như: long int hay một tham số kiểu double. Mã định dạng tương ứng cho nó là %ld.
Bổ từ ‘h’
Bổ từ này được dùng để hiện thị kiểu short integer. Mã định dạng tương ứng cho nó là %hd.
Bổ từ ‘*’
Bổ từ này được dùng khi người dùng không muốn chỉ trước độ rộng của trường mà muốn chương trình xác định nó. Nhưng khi đi với bổ từ này, một tham số được yêu cầu phải chỉ ra độ rộng trường cụ thể.
Chúng ta hãy xem những bổ từ này hoạt động thế nào. Ðầu tiên, chúng ta xem xét tác động của nó đối với những dữ liệu kiểu số nguyên.
Ví dụ 6.2:
/* Chương trình này trình bày cách dùng bổ từ trong printf() */
#include <stdio.h>
#include <conio.h>
main()
{
printf("The number 555 in various forms:
");
printf("Without any modifier:
");
printf("[%d]
", 555);
printf("With - modifier:
");
printf("[%-d]
", 555);
printf("With digit string 10 as modifier:
");
printf("[%10d]
", 555);
printf("With 0 as modifier:
");
printf("[%0d]
", 555);
printf("With 0 and digit string 10 as modifiers:
");
printf("[%010d]
", 555);
printf("With -, 0 and digit string 10 as modifiers:
");
printf("[%-010d]
", 555);
getch();
}
Kết quả như dưới đâ
Chúng ta đã dùng ký hiệu ‘[‘ và ‘]’ để chỉ ra nơi trường bắt đầu và nơi kết thúc. Khi chúng ta dùng %d mà không có bổ từ, chúng ta thấy rằng nó dùng cho một trường có cùng độ rộng với số nguyên. Khi dùng %10d chúng ta thấy rằng nó dùng 10 khoảng trắng cho trường và số được canh lề phải theo mặc định. Nếu ta dùng bổ từ –, số sẽ được canh trái trong trường đó. Nếu dùng bổ từ 0, chúng ta thấy rằng số sẽ thêm vào 0 thay vì là khoảng trắng.
Bây giờ chúng ta hãy xem bổ từ dùng với số thực.
Ví dụ 6.3:
/* Chương trình này trình bày cách dùng bổ từ trong printf() */
#include <stdio.h>
#include <conio.h>
main()
{
printf("The number 555.55 in various forms:
");
printf("In float form without modifiers:
");
printf("[%f]
", 555.55);
printf("In exponential form without any modifier:
");
printf("[%e]
", 555.55);
printf("In float form with - modifier:
");
printf("[%-f]
", 555.55);
printf("In float form with digit string 10.3 as modifier
");
printf("[%10.3f]
", 555.55);
printf("In float form with 0 as modifier:
");
printf("[%0f]
", 555.55);
printf("In float form with 0 and digit string 10.3");
printf("as modifiers:
");
printf("[%010.3f]
", 555.55);
printf("In float form with -, 0 ");
printf("and digit string 10.3 as modifiers:
");
printf("[%-010.3f]
", 555.55);
printf("In exponential form with 0");
printf("and digit string 10.3 as modifiers:
");
printf("[%010.3e]
", 555.55);
printf("In exponential form with -, 0");
printf("and digit string 10.3 as modifiers:
");
printf("[%-010.3e]
", 555.55);
getch();
}
Theo mặc định cho %f, chúng ta có thể thấy rằng có 6 con số cho phần thập phân và mặc định cho %e là một con số tại phần nguyên và 6 con số phần bên phải dấu chấm thập phân. Chú ý cách thể hiện 2 số cuối cùng trong ví dụ trên, số các con số bên phải dấu chấm thập phân là 3, dẫn đến kết quả không được làm tròn.
Bây giờ, chúng ta hãy xem bổ từ dùng với chuỗi số. Chú ý cách mở rộng trường để chứa toàn bộ chuỗi. Hơn nữa, chú ý cách đặc tả độ chính xác .4 trong việc giới hạn số ký tự được in.
Ví dụ 6.4:
/* Chương trình trình bày cách dùng bổ từ với chuỗi*/
#include <stdio.h>
#include <conio.h>
main()
{
printf("A string in various forms:
");
printf("Without any format command:
");
printf("Good day Mr. Lee.
");
printf("With format command but without any modifier:
");
printf("[%s]
", "Good day Mr. Lee.");
printf("With digit string 4 as modifier:
");
printf("[%4s]
", "Good day Mr. Lee.");
printf("With digit string 19 as modifier:
");
printf("[%19s]
", "Good day Mr. Lee.");
printf("With digit string 23 as modifier:
");
printf("[%23s]
", "Good day Mr. Lee.");
printf("With digit string 25.4 as modifier:
");
printf("[%25.4s]
", "Good day Mr.Lee.");
printf("With - and digit string 25.4 as modifiers:
");
printf("[%-25.4s]
", "Good day Mr.shroff.");
getch();
}
Kết quả như sau:
Những ký tự ta nhập tại bàn phím không được lưu ở dạng các ký tự. Thật sự chúng lưu theo dạng các số dưới dạng mã ASCII (Bộ mã chuẩn Mỹ cho việc trao đổi thông tin - American Standard Code for Information Interchange). Các giá trị của một biến được thông dịch dưới dạng ký tự hay một số tùy vào kiểu của biến đó. Ví dụ sau mô tả điều này:
Ví dụ 6.5:
#include <stdio.h>
#include <conio.h>
main()
{
int a = 80;
char b= 'C';
printf("
This is the number stored in 'a' %d",a);
printf("
This is a character interpreted from 'a' %c",a);
printf("
This is also a character stored in 'b' %c",b);
printf("
Hey! The character of 'b' is printed as a number! %d", b);
getch();
}
Kết quả này mô tả việc dùng các đặc tả định dạng và việc thông dịch của mã ASCII. Mặc dù các biến a và b đã được khai báo là các biến kiểu int và char, nhưng chúng đã được in như là ký tự và số nhờ vào việc dùng các bộ định dạng khác nhau. Ðặc điểm này của C giúp việc xử lý dữ liệu được linh hoạt.
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 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, int, 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();
}
Kết quả như sau:
Nhập vào:
12 67.9 F MARK
Dữ liệu đầu vào có thể là:
12 67.9 F MARK
hoặc như:
12 67.9 F MARK
cũng được nhận vào các biến a, d, ch, và name.
Xem ví dụ khác:
Ví dụ 6.7:
#include <stdio.h>
#include <conio.h>
main()
{
int i;
float x;
char c;
scanf("%3d %5f %c", &i, &x, &c);
printf("%d, %f, %c",i,x,c);
getch();
}
Kết quả như sau:
Nếu dữ liệu nhập vào là:
21 10.345 F
Khi chương trình được thực thi, thì 21 sẽ gán tới i, 10.34 sẽ gán tới x và ký tự 5 sẽ được gán cho c. Còn lại là đặc tính F sẽ bị bỏ qua.
Khi ta chỉ rõ một chiều rộng trường bên trong scanf(), thí dụ %10s, rồi sau đó scanf() chỉ thu nhận tối đa 10 ký tự hoặc tới ký tự khoảng trắng đầu tiên (bất cứ ký tự nào đầu tiên). Ðiều này cũng áp dụng cho các kiểu int, float và double.
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:
Nếu chuỗi được viết là DIEN Tu may tinh, chỉ có chuỗi ký tự DIEN T được gán cho mảng, khi đó thì ký tự viết thường đầu tiên (trong trường hợp này là ‘u’) được thông dịch như ký tự đầu tiên bên ngoài chuỗi:
Ðể chấp nhận bất kỳ ký tự nào đến khi gặp ký tự xuống dòng, chúng ta sử dụng mã định dạng %[^
], điều này ngụ ý rằng chuỗi đó sẽ chấp nhận bất kỳ ký tự nào trừ “
” (ký tự xuống dòng).
Dấu mũ (^) ngụ ý rằng tất cả các ký tự trừ những ký tự nằm sau dấu mũ đó sẽ được chấp nhận như ký tự hợp lệ.
Ví dụ 6.9:
#include <stdio.h>
#include <conio.h>
main()
{
char line[80];
scanf("%[^.]", line);
printf("%s",line);
getch();
}
Kết quả như sau:
Nếu nhập vào chuỗi: dientumaytinh.com
Khi hàm scanf() được thực thi, chỉ có chuỗi dientumaytinh được gán vào mảng, ký tự '.' đầu tiên sẽ được coi là kết thúc một chuỗi.
Bổ từ * cho kết quả khác nhau trong scanf(). Dấu * được dùng để chỉ rằng một trường sẽ được bỏ qua luôn hay tạm bỏ qua.
Ví dụ xét chương trình:
#include <stdio.h>
#include <conio.h>
main()
{
char item[20];
int partno;
float cost;
.........
scanf("%s %*d %f", item, &partno, &cost);
.........
}
Nếu các mục dữ liệu tương ứng là:
battery 12345 0.05
thì battery sẽ được gán cho item và 0.05 sẽ được gán cho cost nhưng 12345 sẽ không được gán cho partno bởi vì dấu * ngăn chặn việc gán.
Bất cứ ký tự khác trong scanf() mà không là mã định dạng trong chuỗi điều khiển phải được nhập vào chính xác nếu không sẽ phát sinh lỗi. Ðặc điểm này được dùng để chấp nhận dấu phân cách phẩy (,).
Ví dụ chuỗi dữ liệu
10, 15, 17
và lệnh nhập vào
scanf(“%d, %f, %c”, &intgr, &flt, &ch);
Chú ý rằng dấu phẩy trong chuỗi chuyển đổi tương ứng dấu phẩy trong chuỗi nhập và vì vậy nó sẽ có chức năng như dấu phân cách.
Ký tự khoảng trắng trong chuỗi điều khiển thường được bỏ qua mặc dù nó sẽ phát sinh trở ngại khi dùng với mã định dạng %c. Nếu chúng ta dùng bộ định dạng %c thì một khoảng trắng được xem như là một ký tự hợp lệ.
Xét đoạn mã sau:
int x, y;
char ch;
scanf(“%2d %c %d”,&x, &ch, &y);
printf(“%d %d %d
”,x, ch, y);
ta nhập vào:
14 c 5
14 sẽ được gán cho x, ký tự ch nhận ký tự khoảng trắng (số 32 trong hệ thập phân), do vậy y được gán giá trị của ký tự ‘c’ tức là số 99 trong hệ thập phân.
Xét đoạn mã sau:
#include <stdio.h>
#include <conio.h>
main()
{
char c1, c2, c3;
…………..
scanf(“%c%c%c”,&c1, &c2, &c3);
………………..
}
Nếu dữ liệu nhập vào là:
a b c
(với khoảng trắng giữa các ký tự), thì kết quả của phép gán:
c1 = a, c2 = <Khoảng trắng>, c3 = b
Ở đây chúng ta có thể thấy c2 chứa một khoảng trắng vì chuỗi nhập có chứa ký tự khoảng trắng. Ðể bỏ qua các ký tự khoảng trắng này và đọc ký tự tiếp theo không phải là ký tự khoảng trắng, ta nên dùng tập chuyển đổi %1s.
scanf(“%c%1s%1s”,&c1, &c2, &c3);
Khi đó kết quả sẽ khác đi với cùng dữ liệu nhập vào như trước và kết quả đúng như ý định của ta:
c1 = a, c2 = b, c3 = c
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à:
o 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();
}
Kết quả như sau:
Trong 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.
Tham số
Hàm
Tác dụ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’)
Hiển thị con số 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
Bảng 6.5: Những tùy chọn cho putchar() và tác dụng của chúng
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();
}
Kết quả như sau:
Khác nhau giữa getchar() và putchar() là putchar() yêu cầu một tham số trong khi getchar() thì không.
Ví dụ 6.12:
/* Chương trình trình bày getchar() và putchar() */
#include <stdio.h>
#include <conio.h>
main()
{
char letter;
printf("You can enter a character now: ");
letter = getchar();
putchar(letter);
getch();
}
Kết quả như sau:
7.1. Câu lệnh điều kiện là gì ?
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 if:
if (biểu thức)
Các 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();
}
Kết quả của chương trình như sau:
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 được đặt trong cặp ngoặc nhọn {}. 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 trả về giá trị 1 nếu năm đó là năm nhuận. Khi đó, chương trình hiển thị thông báo gồm biến year và dòng chữ “is a leap year”. Nếu điều kiện trên không thỏa mãn, chương trình không hiển thị thông báo nào.
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:
if (biểu thức)
câu_lệnh – 1;
else
câu_lệnh – 2;
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.
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”:
Chương trình dưới đây đưa ra cách sử dụng của if lồng nhau.
Ví dụ 7.8:
Một công ty sản xuất 3 loại sản phẩm có tên gọi: văn phòng phẩm cho máy tính (computer stationery), đĩa cứng (fixed disks) và máy tính (computer).
Sản phẩm Mã
Computer Stationery 1
Fixed Disks 2
Computers 3
Công ty có chính sách giảm giá như sau:
Sản phẩm Giá trị đặt hàng Tỷ lệ giảm giá
Computer Stationery $500/- hoặc hơn 12%
Computer Stationery $300/- hoặc hơn 8%
Computer Stationery dưới $300/- 2%
Fixed Disks $2000/- hoặc hơn 10%
Fixed Disks $1500/- hoặc hơn 5%
Computers $5000/- hoặc hơn 10%
Computer $2500/- hoặc hơn 5%
Dưới đây là chương trình tính giảm giá:
#include <stdio.h>
#include <conio.h>
main()
{
int productcode;
float orderamount, rate = 0.0;
printf("
Please enter the product code: ");
scanf("%d", &productcode);
printf("Please enter the order amount: ");
scanf("%f", &orderamount);
if (productcode == 1)
{
if (orderamount >= 500)
rate = 0.12;
else if (orderamount >= 300)
rate = 0.08;
else
rate = 0.02;
}
else if (productcode == 2)
{
if (orderamount >= 2000)
rate = 0.10;
else if (orderamount >= 1500)
rate = 0.05;
}
else if (productcode == 3)
{
if (orderamount >= 5000)
rate = 0.10;
else if (orderamount >= 2500)
rate = 0.05;
}
orderamount -= orderamount * rate;
printf("The net order amount is % .2f
", orderamount);
getch();
}
Ở trên, else sau cùng trong chuỗi các else-if không cần kiểm tra bất kỳ điều kiện nào. Ví dụ, nếu mã sản phẩm được nhập vào là 1 và giá trị đặt hàng nhỏ hơn $300, thì không cần phải kiểm tra điều kiện, vì tất cả các khả năng đã được kiểm soát.
Kết quả thực thi chương trình với mã sản phẩm là 3 và giá trị đặt hàng là $6000 được trình bày ở trên.
Sửa đổi chương trình trên để chú ý đến trường hợp dữ liệu nhập là một mã sản phẩm không hợp lệ. Điều này có thể dễ dàng đạt được bằng cách thêm một lệnh else vào chuỗi lệnh if dùng kiểm tra mã sản phẩm. Nếu gặp một mã sản phẩm không hợp lệ, chương trình phải kết thúc mà không cần tính giá trị thực của đơn đặt hàng.
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();
}
9.1 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: count = 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, i đượ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, i đượ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.
Các trường hợp khác của vòng lặp ‘for’:
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 đó i 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í dụ 9.7:
/* ECHO PROGRAM */
/* A program to accept input data from the console and print it on the screen */
/* End of input data is indicated by pressing ‘^Z’*/
#include <stdio.h>
main()
{
char ch;
while ((ch = getchar()) != EOF)
{
putchar(ch);
}
Dữ liệu người dùng nhập vào được in đậm. Chương trình làm việc như thế nào ? Sau khi nhập vào một tập hợp các ký tự, nội dung của nó sẽ được in hai lần lên màn hình khi bạn nhấn <Enter>. Điều này là do các ký tự bạn nhập vào từ bàn phím được lưu trữ trong bộ đệm bàn phím. Và lệnh putchar() sẽ lấy nó từ bộ đệm sau khi bạn nhấn phím <Enter>. Chú ý cách thức kết thúc quá trình nhập dữ liệu bằng tổ hợp phím ^Z, đây là kí tự kết thúc file trong DOS.
9.1.3 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ác vòng lặp ‘while lồng nhau’ và ‘do ... while’
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.
9.2 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, goto, 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à goto 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.
9.2.1 Lệ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:
Ø goto
Ø label
Lệnh goto 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 goto 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 goto 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();
}
9.2.3 Lệnh ‘break’:
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. Ví dụ,
Ví dụ 9.11:
#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 <conio.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.
9.3 Hàm ‘exit()’:
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.
Các phần tử mảng và các chỉ mục:
Mỗi phần tử của mảng được định danh bằng một chỉ mục hoặc chỉ số gán cho nó. Chiều của mảng được xác định bằng số chỉ số cần thiết để định danh duy nhất mỗi phần tử. Một chỉ số là một số nguyên dương được bao bằng dấu ngoặc vuông [ ] đặt ngay sau tên mảng, không có khoảng trắng ở giữa. Một chỉ số chứa các giá trị nguyên bắt đầu bằng 0. Vì vậy, một mảng player với 11 phần tử được biểu diễn như sau:
player[0], player[1], player[2], ... , player[10].
Như đã thấy, phần tử mảng bắt đầu với player[0], và vì vậy phần tử cuối cùng là player[10] không phải là player[11]. Điều này là do bởi trong C, chỉ số mảng bắt đầu từ 0; do đó trong mảng N phần tử, phần tử cuối cùng có chỉ số là N-1. Phạm vi cho phép của các giá trị chỉ số được gọi là miền giới hạn của chỉ số mảng, giới hạn dưới và giới hạn trên. Một chỉ số mảng hợp lệ phải có một giá trị nguyên nằm trong niềm giới hạn. Thuật ngữ hợp lệ được sử dụng cho một nguyên nhân rất đặc trưng. Trong C, nếu người dùng cố gắng truy xuất một phần tử nằm ngoài dãy chỉ số hợp lệ (như player[11] trong ví dụ trên của mảng), trình biên dịch C sẽ không phát sinh ra lỗi. Tuy nhiên, có thể nó truy xuất một giá trị nào đó dẫn đến kết quả không đoán được. Cũng có nguy cơ viết chồng lên dữ liệu hoặc mã lệnh chương trình. Vì vậy, người lập trình phải đảm bảo rằng tất cả các chỉ số là nằm trong miền giới hạn hợp lệ.
Ø Khai báo một mảng:
Một mảng có một vài đặc tính riêng biệt và phải được khai báo khi sử dụng chúng. Những đặc tính này bao gồm:
· Lớp lưu trữ
· Kiểu dữ liệu của các phần tử mảng.
· Tên mảng – xác định vị trí phần tử đầu tiên của mảng.
· Kích thước mảng - một hằng số có giá trị nguyên dương.
Một mảng được khai báo giống như cách khai báo một biến, ngoại trừ tên mảng được theo sau bởi một hoặc nhiều biểu thức, được đặt trong dấu ngoặc vuông [] xác định chiều dài của mảng. Cú pháp tổng quát khai báo một mảng như sau:
lớp_lưu_trữ kiểu_dữ_liệu tên_mảng[biểu_thức_kích_thước]
Ở đây, biểu_thức_kích_thước là một biểu thức xác định số phần tử trong mảng và phải định ra một trị nguyên dương. Lớp_lưu_trữ là một tùy chọn. Mặc định lớp automatic được dùng cho mảng khai báo bên trong một hàm hoặc một khối lệnh, và lớp external được dùng cho mảng khai báo bên ngoài một hàm. Vì vậy mảng player được khai báo như sau:
int player[11];
Nên nhớ rằng, trong khi khai báo mảng, kích thước của mảng sẽ là 11, tuy nhiên các chỉ số của từng phần tử bên trong mảng sẽ là từ 0 đến 10.
Các qui tắc đặt tên mảng là giống với qui tắc đặt tên biến. Một tên mảng và một tên biến không được giống nhau, nó dẫn đến sự nhập nhằng. Nếu một sự khai báo như vậy xuất hiện trong chương trình, trình biên dịch sẽ hiển thị thông báo lỗi.
Ø Một vài qui tắc với mảng:
· Tất cả các phần tử của một mảng có cùng kiểu. Điều này có nghĩa là, nếu một mảng được khai báo kiểu int, nó không thể chứa các phần tử có kiểu khác.
· Mỗi phần tử của mảng có thể được sử dụng bất cứ nơi nào mà một biến được cho phép hay được yêu cầu.
· Một phần tử của mảng có thể được tham chiếu đến bằng cách sử dụng một biến hoặc một biểu thức nguyên. Sau đây là các tham chiếu hợp lệ:
player[i]; /*Ở đó i là một biến, tuy nhiên cần phải chú ý rằng i nằm trong miền giới hạn của chỉ số đã được khai báo cho mảng player*/
player[3] = player[2] + 5;
player[0] += 2;
player[i / 2 + 1];
· Kiểu dữ liệu của mảng có thể là int, char, float, hoặc double.
11.2 Việc quản lý mảng trong C:
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];
int i;
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();
/*Input values are accepted from the user into the array ary[10]*/
#include <stdio.h>
#include <conio.h>
main()
{
int ary[10];
int i, total, high;
for (i = 0; i < 10; i++)
{
printf("Enter value: %d: ", i + 1);
scanf("%d", &ary[i]);
}
/* Displays highest of the entered values */
high = ary[0];
for (i = 1; i < 10; i++)
{
if (ary[i] > high)
high = ary[i];
}
printf("
Highest value entered was %d", high);
/* Prints average of value entered for ary[10] */
for (i = 0, total = 0; i < 10; i++)
total = total + ary[i];
printf("
The average of the element of ary is %d", total/i);
getch();
}
Các mảng không được khởi tạo tự động, trừ khi mỗi phần tử mảng được gán một giá trị riêng lẻ. Không nên dùng các mảng trước khi có sự khởi tạo thích hợp. Điều này là bởi vì không gian lưu trữ của mảng không được khởi tạo tự động, do đó dễ gây ra kết quả không lường trước. Mỗi khi các phần tử của một mảng chưa khởi tạo được sử dụng trong các biểu thức toán học, các giá trị đã tồn tại sẵn trong ô nhớ sẽ được sử dụng, các giá trị này không đảm bảo rằng có cùng kiểu như khai báo của mảng, trừ khi các phần tử của mảng được khởi tạo một cách rõ ràng. Điều này đúng không chỉ cho các mảng mà còn cho các biến thông thường.
Trong đoạn mã lệnh sau, các phần tử của mảng được gán giá trị bằng các dùng vòng lặp for.
int ary[20], i;
for(i=0; i<20; i++)
ary[i] = 0;
Khởi tạo một mảng sử dụng vòng lặp for có thể được thực hiện với một hằng giá trị, hoặc các giá trị được sinh ra từ một cấp số cộng.
Một vòng lặp for cũng có thể được sử dụng để khởi tạo một mảng các ký tự như sau:
Ví dụ 11.3:
#include <stdio.h>
#include <conio.h>
main()
{
char alpha[26];
int i, j;
for(i = 65, j = 0; i < 91; i++, j++)
{
alpha[j] = i;
printf("The character now assigned is %c
", alpha[j]);
}
getch ();
}
Chương trình trên gán các mã ký tự ASCII cho các phần tử của mảng alpha. Kết quả là khi in với định dạng %c, một chuỗi các ký tự được xuất ra màn hình. Các mảng cũng có thể được khởi tạo khi khai báo. Điều này được thực hiện bằng việc gán tên mảng với một danh sách các giá trị phân cách nhau bằng dấu phẩy (,) đặt trong cặp dấu ngoặc nhọn {}. Các giá trị trong cặp dấu ngoặc nhọn {} được gán cho các phần tử trong mảng theo đúng thứ tự xuất hiện.
Ví dụ:
int deci[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
static float rates[4] = {0.0, -2.5, 13.75, 18.0};
char company[5] = {‘A’, ‘P’, ‘P’, ‘L’, ‘E’};
int marks[100] = {15, 13, 11, 9};
Các giá trị khởi tạo của mảng phải là các hằng, không thể là biến hoặc các biểu thức. Một vài phần tử đầu tiên của mảng sẽ được khởi tạo nếu số lượng giá trị khởi tạo là ít hơn số phần tử mảng được khai báo. Các phần tử còn lại sẽ được khởi tạo giá trị 0. Ví dụ, trong mảng marks sau khi có sự khởi tạo như trên, bốn phần tử đầu tiên (từ 0 đến 3) tương ứng được khởi tạo là 15, 13, 11 và 9. Các phần tử còn lại có giá trị 0. Không thể chỉ khởi tạo các phần tử từ 1 đến 4, hoặc từ 2 đến 4, hay từ 2 đến 5 khi sự khởi tạo được thực hiện tại thời điểm khai báo. Trong C không có khả năng lặp lại sự khởi tạo giá trị.
Trong trường hợp sự khởi tạo là tường minh, lớp extern hoặc static, các phần tử của mảng được đảm bảo khởi tạo là 0 (không giống lớp auto).
Không cần thiết khai báo kích thước của mảng đang được khởi tạo. Nếu kích thước của mảng được bỏ qua khi khai báo, trình biên dịch sẽ xác định kích thước của mảng bằng cách đếm các giá trị đang được khởi tạo. Ví dụ, sự khai báo mảng external sau đây sẽ chỉ định kích thước của mảng ary là 5 vì có 5 giá trị khởi tạo.
int ary[] = {1, 2, 3, 4, 5};
Ø Các mảng chuỗi/ký tự:
Một chuỗi có thể được khai báo như là một mảng ký tự, và được kết thúc bởi một ký tự NULL. Mỗi ký tự của chuỗi chiếm 1 byte, và ký tự cuối cùng của chuỗi luôn luôn là ký tự ‘\0’. Ký tư ‘\0’ được gọi là ký tự null. Nó là một mã thoát (escape sequence) tương tự như ‘
’, thay thế cho ký tự có giá trị 0. Vì ‘\0’ luôn là ký tự cuối cùng của một chuỗi, nên các mảng ký tự phải có nhiều hơn một ký tự so với chiều dài tối đa mà chúng quản lý. Ví dụ, một mảng ary quản lý một chuỗi 10 ký tự phải được khai báo như sau:
char ary[11];
Vị trí thêm vào được sử dụng để lưu trữ ký tự null. Nên nhớ rằng ký tự kết thúc (ký tự null) là rất quan trọng.
Các giá trị chuỗi có thể được nhập vào bằng cách sử dụng hàm scanf(). Với chuỗi ary được khai báo ở trên, mã lệnh nhập sẽ như sau:
scanf(“%s”, ary);
Trong lệnh trên, ary xác định vị trí nơi mà lần lượt các ký tự của mảng sẽ được lưu trữ.
Ví dụ 11.4:
#include <stdio.h>
#include <conio.h>
main()
{
char ary[5];
int i;
printf("
Enter string: ");
scanf("%s", ary);
printf("
The string is %s
", ary);
for (i = 0; i < 5; i++)
printf("\t%d", ary[i]);
getch();
}
Kết quả như trên là của 4 ký tự (appl) và ký tự thứ 5 là ký tự null. Điều này được thấy rõ với mã ASCII cho các ký tự được in ra ở dòng thứ hai. Ký tự thứ năm được in là 0, là giá trị của ký tự null.
Kết quả ở trên của là một dữ liệu đầu vào có 5 ký tự a, p, p, l và e. Nó không được xem là một chuỗi bởi vì ký tự thứ 5 của mảng không phải là \0. Một lần nữa, điều này được thấy rõ bằng dòng in ra mã ASCII của các ký tự a, p, p, l, e.
Trong ví dụ trên, khi chỉ có hai ký tự được nhập, ký tự thứ ba sẽ là ký tự null. Điều này cho biết là chuỗi đã được kết thúc. Những ký tự còn lại là những ký tự không dự đoán được.
Trong trường hợp trên, tính quan trọng của ký tự null trở nên rõ ràng. Ký tự null xác định sự kết thúc của chuỗi và là cách duy nhất để các hàm làm việc với chuỗi sẽ biết đâu là điểm kết thúc của chuỗi.
Mặc dù C không có kiểu dữ liệu chuỗi, nhưng nó cho phép các hằng chuỗi. Một hằng chuỗi là một dãy các ký tự được đặt trong dấu nháy đôi (“”). Không giống như các hằng khác, nó không thể được sửa đổi trong chương trình. Ví dụ như:
“Hi Dientumaytinh!”
Trình biên dịch C sẽ tự động thêm vào ký tự null cuối chuỗi.
C hỗ trợ nhiều hàm cho chuỗi, các hàm này nằm trong thư viện chuẩn string.h. Một vài hàm được đưa ra trong bảng 11.1. Cách làm việc của các hàm này sẽ được thảo luận trong bài 17.
Tên hàm
Chức năng
strcpy(s1, s2)
Sao chép chuỗi s2 vào s1
strcat(s1, s2)
Nối chuỗi s2 vào cuối chuỗi của s1
strlen(s1)
Trả về chiều dài của chuỗi s1
strcmp(s1, s2)
Trả về 0 nếu s1 và s2 là 2 chuỗi giống nhau; nhỏ hơn 0 nếu s1<s2; lớn hơn 0 nếu s1> s2 (theo thứ tự mã ASCII)
strchr(s1, ch)
Trả về một con trỏ trỏ đến vị trí xuất hiện đầu tiên của ch trong s1
strstr(s1, s2)
Trả về một con trỏ trỏ đến vị trí xuất hiện đầu tiên của chuỗi s2 trong chuỗi s1
11.3 Mảng hai chiều:
Chúng ta đã biết thế nào là mảng một chiều. Điều này có nghĩa là các mảng chỉ có một chỉ số. Các mảng có thể có nhiều hơn một chiều. Các mảng đa chiều giúp dễ dàng trình bày các đối tượng đa chiều, chẳng hạn một đồ thị với các dòng và cột hay tọa độ màn hình của máy tính. Các mảng đa chiều được khai báo giống như các mảng một chiều, ngoại trừ có thêm một cặp dấu ngoặc vuông [] trong trường hợp mảng hai chiều. Một mảng ba chiều sẽ cần ba cặp dấu ngoặc vuông,... Một cách tổng quát, một mảng đa chiều có thể được biểu diễn như sau:
storage_class data_type ary[exp1][exp2]....[expN];
Ở đó, ary là một mảng có lớp là storage_class, kiểu dữ liệu là data_type, và exp1, exp2,..... , expN là các biểu thức nguyên dương xác định số phần tử của mảng được kết hợp với mỗi chiều.
Dạng đơn giản nhất và thường được sử dụng nhất của các mảng đa chiều là mảng hai chiều. Một mảng hai chiều có thể xem như là một mảng của hai ‘mảng một chiều’. Một mảng hai chiều đặc trưng như bảng lịch trình của máy bay, xe lửa. Để xác định thông tin, ta sẽ chỉ định dòng và cột cần thiết, và thông tin được đọc ra từ vị trí (dòng và cột) được tìm thấy. Tương tự như vậy, một mảng hai chiều là một khung lưới chứa các dòng và cột trong đó mỗi phần tử được xác định duy nhất bằng toạ độ dòng và cột của nó. Một mảng hai chiều tmp có kiểu int với 2 dòng và 3 cột có thể được khai báo như sau:
int tmp[2][3];
Mảng này sẽ chứa 2 x 3 (6) phần tử, và chúng có thể được biểu diễn như sau:
Ở đó e1 – e6 biểu diễn cho các phần tử của mảng. Cả dòng và cột được đánh số từ 0. Phần tử e6 được xác định bằng dòng 1 và cột 2. Truy xuất đến phần tử này như sau:
tmp[1][2];
Ø Khởi tạo mảng đa chiều:
Khai báo mảng đa chiều có thể kết hợp với việc gán các giá trị khởi tạo. Cần phải cẩn thận lưu ý đến thứ tự các giá trị khởi tạo được gán cho các phần tử của mảng (chỉ có mảng external và static có thể được khởi tạo). Các phần tử trong dòng đầu tiên của mảng hai chiều sẽ được gán giá trị trước, sau đó đến các phần tử của dòng thứ hai, … Hãy xem sự khai báo mảng sau:
int ary[3][4] ={1,2,3,4,5,6,7,8,9,10,11,12};
Kết quả của phép khai báo trên sẽ như sau:
ary[0][0] = 1 ary[0][1] = 2 ary[0][2] = 3 ary[0][3]= 4
ary[1][0] = 5 ary[1][1] = 6 ary[1][2] = 7 ary[1][3] = 8
ary[2][0] = 9 ary[2][1] = 10 ary[2][2] = 11 ary[2][3] = 12
Chú ý rằng chỉ số thứ 1 chạy từ 0 đến 2 và chỉ số thứ hai chạy tử 0 đến 3. Một điểm cần nhớ là các phần tử của mảng sẽ được lưu trữ ở những vị trí kế tiếp nhau trong bộ nhớ. Mảng ary ở trên có thể xem như là một mảng của 3 phần tử, mỗi phần tử là một mảng của 4 số nguyên, và sẽ xuất hiện như sau:
Dòng 0
Dòng 1
Dòng 2
1 2 3 4
5 6 7 8
9 10 11 12
Thứ tự tự nhiên mà các giá trị khởi tạo được gán có thể thay đổi bằng hình thức nhóm các giá trị khởi tạo lại trong các dấu ngoặc nhọn {}. Quan sát sự khởi tạo sau:
int ary [3][4] ={
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
Mảng sẽ được khởi tạo như sau:
ary[0][0]=1 ary[0][1]=2 ary[0][2]=3 ary[0][3]=0
ary[1][0]=4 ary[1][1]=5 ary[1][2]=6 ary[1][3]=0
ary[2][0]=7 ary[2][1]=8 ary[2][2]=9 ary[2][3]=0
Một phần tử của mảng đa chiều có thể được sử dụng như một biến trong C bằng cách dùng các chỉ số để xác định phần tử của mảng.
Ví dụ 11.5:
/* Chương trình nhập các số vào một mảng hai chiều. */
#include <stdio.h>
#include <conio.h>
main()
{
int arr[2][3];
int row, col;
for(row = 0; row < 2; row++)
{
for(col = 0; col < 3; col++)
{
printf("Enter a Number at [%d][%d]: ", row, col);
scanf("%d", &arr[row][col]);
}
}
for(row = 0; row < 2; row++)
{
for(col = 0; col < 3; col++)
{
printf("
The Number at [%d][%d] is %d",
row, col, arr[row][col]);
}
}
getch();
}
Mảng hai chiều và chuỗi:
Như chúng ta đã biết ở phần trước, một chuỗi có thể được biểu diễn bằng mảng một chiều, kiểu ký tự. Mỗi ký tự trong chuỗi được lưu trữ trong một phần tử của mảng. Mảng của chuỗi có thể được tạo bằng cách sử dụng mảng ký tự hai chiều. Chỉ số bên trái xác định số lượng chuỗi, và chỉ số bên phải xác định chiều dài tối đa của mỗi chuỗi. Ví dụ bên dưới khai báo một mảng chứa 25 chuỗi và mỗi chuỗi có độ dài tối đa 80 ký tự kể cả ký tự null.
char str_ary[25][80];
Ø Ví dụ minh hoạ cách sử dụng của một mảng hai chiều:
Ví dụ bên dưới minh hoạ cách dùng của mảng hai chiều như các chuỗi.
Xét bài toán tổ chức một danh sách tên theo thứ tự bảng chữ cái. Ví dụ sau đây nhập một danh sách các tên và sau đó sắp xếp chúng theo thứ tự bảng chữ cái.
Ví dụ 11.6
#include <stdio.h>
#include <string.h>
#include <conio.h>
main()
{
int i, n = 0;
int item;
char x[10][12];
char temp[12];
printf("Enter each string on a separate line
");
printf("Type 'END' when over
");
/* Read in the list of strings */
do
{
printf("String %d: ", n + 1);
scanf("%s", x[n]);
} while (strcmp(x[n++], "END"));
/*Reorder the list of strings */
n = n-1;
for(item = 0; item < n - 1; ++item)
{
/* Find lowest of remaining strings */
for(i = item + 1; i < n; ++i)
{
if(strcmp(x[item], x[i]) > 0)
{
/*Interchange two strings*/
strcpy(temp, x[item]);
strcpy(x[item], x[i]);
strcpy(x[i], temp);
}
}
}
/* Display the arranged list of strings */
printf("Recorded list of strings:
");
for(i = 0; i < n; ++i)
{
printf("
String %d is %s", i + 1, x[i]);
}
getch();
}
Chương trình trên nhập vào các chuỗi đến khi người dùng nhập vào từ “END”. Khi END được nhập vào, chương trình sẽ sắp xếp danh sách các chuỗi và in ra theo thứ tự đã sắp xếp. Chương trình kiểm tra hai phần tử kế tiếp nhau. Nếu thứ tự của chúng không thích hợp, thì hai phần tử sẽ được đổi chỗ. Sự so sánh hai chuỗi được thực hiện với sự trợ giúp của hàm strcmp() và sự đổi chỗ được thực hiện với hàmg strcpy().
13.1 Con trỏ là gì?
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:
Vị trí Giá trị Tên
Bộ nhớ lưu trữ biến
1000 500 var1
1001
1002
.
.
1108 1000 var2
Ở đây, var2 chứa giá trị 1000, đó là địa chỉ của biến var1.
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)
13.2 Các biến con trỏ
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;
temp 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--
Trỏ đến số nguyên đứng trước var
ptr_var + i
Trỏ đến số nguyên thứ i sau var
ptr_var - i
Trỏ đến số nguyên thứ i trước var
++*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 a được lưu trữ ở vị trí sau 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.
13.4 Con trỏ và mảng một chiều
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};
int i;
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:");
gets(str);
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;
int i;
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();
Hàm calloc()
calloc tương tự như malloc, nhưng khác biệt chính là mặc nhiên các giá trị được lưu trong không gian bộ nhớ đã cấp phát là 0. Với malloc, cấp phát bộ nhớ có thể có giá trị bất kỳ.
calloc đòi hỏi hai đối số. Đối số thứ nhất là số các biến mà bạn muốn cấp phát bộ nhớ cho. Đối số thứ hai là kích thước của mỗi biến.
void *calloc( size_t num, size_t size );
Giống như malloc, calloc sẽ trả về một con trỏ rỗng (void) nếu sự cấp phát bộ nhớ là thành công, ngược lại nó sẽ trả về một con trỏ NULL.
Ví dụ bên dưới chỉ ra cho bạn gọi hàm calloc như thế nào và tham chiếu đến ô nhớ đã cấp phát sử dụng một chỉ số mảng. Giá trị khởi tạo của vùng nhớ đã cấp phát được in ra trong vòng lặp for.
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
int main()
{
float *calloc1, *calloc2;
int i;
calloc1 = (float *) calloc(3, sizeof(float));
calloc2 = (float *) calloc(3, sizeof(float));
if(calloc1 != NULL && calloc2 != NULL)
{
for(i = 0; i < 3; i++)
{
printf("
calloc1[%d] holds %05.5f ", i, calloc1[i]);
printf("
calloc2[%d] holds %05.5f", i, *(calloc2 + i));
}
free(calloc1);
free(calloc2);
}
else
{
printf("Not enough memory
");
}
getch();
}
Trong tất cả các máy, các mảng calloc1 và calloc2 phải chứa các giá trị 0. calloc đặc biệt hữu dụng khi bạn đang sử dụng mảng đa chiều. Đây là một ví dụ khác minh họa cách dùng của hàm calloc().
/* This program gets the number of elements, allocates
spaces for the elements, gets a value for each
element, sum the values of the elements, and print
the number of the elements and the sum.
*/
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
main()
{
int *a, i, n, sum = 0;
printf("
%s%s","An array will be created dynamically.
","Input an array size n followed by integers: ");
scanf("%d", &n); //get the number of elements
a = (int *) calloc (n, sizeof(int)); //allocate space
//get a value for each element
for( i = 0; i < n; i++ )
{
printf("Enter %d values: ", n);
scanf("%d", a + i);
}
//sum the values
for(i = 0; i < n; i++ )
sum += a[i];
free(a); //free the space
//print the number and the sum
printf("
%s%7d
%s%7d
", "Number of elements: ", n,"Sum of the elements: ", sum);
getch();
Giả sử chúng ta đã cấp phát một số bytes cho một mảng nhưng sau đó nhận ra là bạn muốn thêm các giá trị. Bạn có thể sao chép mọi thứ vào một mảng lớn hơn, cách này không hiệu quả. Hoặc bạn có thể cấp phát thêm các bytes sử dụng bằng cách gọi hàm realloc, mà dữ liệu của bạn không bị mất đi.
realloc() nhận hai đối số. Đối số thứ nhất là một con trỏ tham chiếu đến bộ nhớ. Đối số thứ hai là tổng số bytes bạn muốn cấp phát thêm:
void *realloc( void *ptr, size_t size );
Truyền 0 như là đối số thứ hai thì tương đương với việc gọi hàm free.
Một lần, realloc trả về một con trỏ rỗng (void) nếu thành công, ngược lại một con trỏ NULL được trả về.
Ví dụ này sử dụng calloc để cấp phát đủ bộ nhớ cho một mảng int có năm phần tử. Sau đó realloc được gọi để mở rộng mảng để có thể chứa bảy phần tử.
#include<stdio.h>
#include <conio.h>
#include <stdlib.h>
main()
{
int *ptr;
int i;
ptr = (int *)calloc(5, sizeof(int *));
if(ptr!=NULL)
{
*ptr = 1;
*(ptr + 1) = 2;
ptr[2] = 4;
ptr[3] = 8;
ptr[4] = 16;
/* ptr[5] = 32; wouldn't assign anything */
ptr = (int *)realloc(ptr, 7 * sizeof(int));
if(ptr!=NULL)
{
printf("Now allocating more memory...
");
ptr[5] = 32; /* now it's legal! */
ptr[6] = 64;
for(i = 0;i < 7; i++)
{
printf("ptr[%d] holds %d
", i, ptr[i]);
}
realloc(ptr, 0); /* same as free(ptr); - just fancier! */
}
else
printf("Not enough memory - realloc failed.
");
}
else
printf("Not enough memory - calloc failed.
");
getch();
Chú ý hai cách khác nhau được sử dụng khi khởi tạo mảng: ptr[2] = 4 là tương đương với *(ptr + 2) = 4 (chỉ dễ đọc hơn!).
Trước khi sử dụng realloc, việc gán một giá trị đến phần tử ptr[5] không gây ra lỗi cho trình biên dịch. Chương trình vẫn thực thi, nhưng ptr[5] không chứa giá trị mà bạn đã gán.
ử dụng các hàm
Nói chung, các hàm được sử dụng trong C để thực thi một chuỗi các lệnh liên tiếp. Tuy nhiên, cách sử dụng các hàm thì không giống với các vòng lặp. Các vòng lặp có thể lặp lại một chuỗi các chỉ thị với các lần lặp liên tiếp nhau. Nhưng việc gọi một hàm sẽ sinh ra một chuỗi các chỉ thị được thực thi tại vị trí bất kỳ trong chương trình. Các hàm có thể được gọi nhiều lần khi có yêu cầu. Giả sử một phần của mã lệnh trong một chương trình dùng để tính tỉ lệ phần trăm cho một vài con số. Nếu sau đó, trong cùng chương trình, việc tính toán như vậy cần phải thực hiện trên những con số khác, thay vì phải viết lại các chỉ thị giống như trên, một hàm có thể được viết ra để tính tỉ lệ phần trăm của bất kỳ các con số. Sau đó chương trình có thể nhảy đến hàm đó, để thực hiện việc tính toán (trong hàm) và trở về nơi nó đã được gọi. Điều này sẽ được giải thích rõ ràng hơn khi thảo luận về cách hoạt động của các hàm.
Một điểm quan trọng khác là các hàm thì dễ viết và dễ hiểu. Các hàm đơn giản có thể được viết để thực hiện các tác vụ xác định. Việc gỡ rối chương trình cũng dễ dàng hơn khi cấu trúc chương trình dễ đọc, nhờ vào sự đơn giản hóa hình thức của nó. Mỗi hàm có thể được kiểm tra một cách độc lập với các dữ liệu đầu vào, với dữ liệu hợp lệ cũng như không hợp lệ. Các chương trình chứa các hàm cũng dễ bảo trì hơn, bởi vì những sửa đổi, nếu yêu cầu, có thể được giới hạn trong các hàm của chương trình. Một hàm không chỉ được gọi từ các vị trí bên trong chương trình, mà các hàm còn có thể đặt vào một thư viện và được sử dụng bởi nhiều chương trình khác, vì vậy tiết kiệm được thời gian viết chương trình.
15.2 Cấu trúc hàm
Cú pháp tổng quát của một hàm trong C là:
type_specifier function_name (arguments)
{
//body of the function
//return statement
}
type_specifier xác định kiểu dữ liệu của giá trị sẽ được trả về bởi hàm. Nếu không có kiểu được đưa ra, hàm cho rằng trả về một kết quả số nguyên. Các đối số được phân cách bởi dấu phẩy. Một cặp dấu ngoặc rỗng () vẫn phải xuất hiện sau tên hàm ngay cả khi nếu hàm không chứa bất kỳ đối số nào. Các tham số xuất hiện trong cặp dấu ngoặc () được gọi là tham số hình thức hoặc đối số hình thức. Phần thân của hàm có thể chứa một hoặc nhiều câu lệnh. Một hàm nên trả về một giá trị và vì vậy ít nhất một lệnh return phải có trong hàm.
15.2.1 Các đối số của một hàm
Trước khi thảo luận chi tiết về các đối số, xem ví dụ sau:
#include <stdio.h>
main()
{
int i;
for(i =1; i <=10; i++)
printf(“
Square of %d is %d “, i,squarer (i));
}
squarer(int x)
/* int x; */
{
int j;
j = x * x;
return(j);
}
Chương trình trên tính tính bình phương các số từ 1 đến 10. Điều này được thực hiện bằng việc gọi hàm squarer. Dữ liệu được truyền từ thủ tục gọi (trong trường hợp trên là hàm main()) đến hàm được gọi squarer thông qua các đối số. Trong thủ tục gọi, các đối số được biết như là các đối số thực và trong định nghĩa của hàm được gọi (squarer()) các đối số được gọi là các đối số hình thức.
Kiểu dữ liệu của các đối số thực phải cùng kiểu với các đối số hình thức. Hơn nữa, số lượng và thứ tự của các tham số thực phải giống như của các tham số hình thức.
Khi một hàm được gọi, quyền điều khiển sẽ được chuyển đến cho nó, ở đó các đối số hình thức được thay thế bởi các đối số thực. Sau đó hàm được thực thi và khi bắt gặp câu lệnh return, nó sẽ chuyển quyền điều khiển cho chương trình gọi nó.
Hàm squarer() được gọi bằng cách truyền số cần được tính bình phương. Đối số x có thể được khai báo theo một trong các cách sau khi định nghĩa hàm:
Phương pháp 1:
squarer(int x)
//x được định nghĩa cùng với kiểu dữ liệu trong cặp dấu ngoặc ().
Phương pháp 2:
squarer(x)
int x;
//x được đặt trong cặp dấu ngoặc (), và kiểu của nó được khai báo ngay sau tên hàm.
Chú ý:
· Trong trường hợp 1: Khi các đối số được khai báo trong cặp dấu ngoặc (), mỗi đối số phải được định nghĩa riêng lẻ, cho dù chúng có cùng kiểu dữ liệu. Ví dụ, nếu x và y là hai đối số của một hàm abc(), thì abc(char x, char y) là một khai báo đúng và abc(char x, y) là sai.
· Trong trường hợp 2: x phải được định nghĩa ngay sau tên hàm, trước khối lệnh. Điều này thật tiện lợi khi có nhiều tham số có cùng kiểu dữ liệu được truyền. Trong trường hợp như vậy, chỉ phải chỉ rõ kiểu đề một lần duy nhất tại điểm bắt đầu.
15.2.2 Sự trả về từ hàm
Lệnh return có hai mục đích:
Ø Ngay lập tức trả điều khiển từ hàm về chương trình gọi.
Ø Bất kỳ cái gì bên trong cặp dấu ngoặc () theo sau return được trả về như là một giá trị cho chương trình gọi.
Trong hàm squarer(), một biến j kiểu int được định nghĩa để lưu giá trị bình phương của đối số truyền vào. Giá trị của biến này được trả về cho hàm gọi thông qua lệnh return. Một hàm có thể thực hiện một tác vụ xác định và trả quyền điều khiển về cho thủ tục gọi nó mà không cần trả về bất kỳ giá trị nào. Trong trường hợp như vậy, lệnh return có thể được viết dạng return(0) hoặc return. Chú ý rằng, nếu một hàm cung cấp một giá trị trả về và nó không làm điều đó thì nó sẽ trả về giá trị không thích hợp.
Trong chương trình tính bình phương của các số, chương trình truyền dữ liệu tới hàm squarer thông qua các đối số. Có thể có các hàm được gọi mà không cần bất kỳ đối số nào. Ở đây, hàm thực hiện một chuỗi các lệnh và trả về giá trị, nếu được yêu cầu.
Chú ý rằng, hàm squarer() cũng có thể được viết như sau
squarer(int x)
{
return(x*x);
}
Ở đây một biểu thức hợp lệ được xem như một đối số trong câu lệnh return. Trong thực tế, lệnh return có thể được sử dụng theo một trong các cách sau đây:
return;
return(hằng);
return(biến);
return(biểu thức);
return(câu lệnh đánh giá); ví dụ: return(a>b?a:b);
Tuy nhiên, giới hạn của lệnh return là nó chỉ có thể trả về một giá trị duy nhất.
15.2.3 Kiểu của một hàm
type-specifier được sử dụng để xác định kiểu dữ liệu trả về của một hàm. Trong ví dụ trên, type-specifier không được viết bên cạnh hàm squarer(), vì squarer() trả về một giá trị kiểu int. type-specifier là không bắt buộc nếu một giá trị kiểu số nguyên được trả về hoặc nếu không có giá trị nào được trả về. . Tuy nhiên, tốt hơn nên chỉ ra kiểu dữ liệu trả về là int nếu một giá trị số nguyên được trả về và tương tự dùng void nếu hàm không trả về giá trị nào.
15.3 Gọi hàm
Có thể gọi một hàm từ chương trình chính bằng cách sử dụng tên của hàm, theo sau là cặp dấu ngoặc (). Cặp dấu ngoặc là cần thiết để nói với trình biên dịch là đây là một lời gọi hàm. Khi một tên hàm được sử dụng trong chương trình gọi, tên hàm có thể là một phần của một một lệnh hoặc chính nó là một câu lệnh. Mà ta đã biết một câu lệnh luôn kết thúc với một dấu chấm phẩy (;). Tuy nhiên, khi định nghĩa hàm, không được dùng dấu chấm phầy ở cuối phần định nghĩa. Sự vắng mặt của dấu chấm phẩy nói với trình biên dịch đây là phần định nghĩa của hàm và không được gọi hàm.
Một số điểm cần nhớ:
Ø Một dấu chấm phẩy được dùng ở cuối câu lệnh khi một hàm được gọi, nhưng nó không được dùng sau một sự định nghĩa hàm.
Ø Cặp dấu ngoặc () là bắt buộc theo sau tên hàm, cho dù hàm có đối số hay không.
Ø Hàm gọi đến một hàm khác được gọi là hàm gọi hay thủ tục gọi. Và hàm được gọi đến còn được gọi là hàm được gọi hay thủ tục được gọi.
Ø Các hàm không trả về một giá trị số nguyên cần phải xác định kiểu của giá trị được trả về.
Ø Chỉ một giá trị có thể được trả về bởi một hàm.
Ø Một chương trình có thể có một hoặc nhiều hàm.
15.4 Khai báo hàm
Một hàm nên được khai báo trong hàm main() trước khi nó được định nghĩa hoặc sử dụng. Điều này phải được thực hiện trong trường hợp hàm được gọi trước khi nó được định nghĩa.
Xem ví dụ:
#include <stdio.h>
main()
{
…
address(…);
…
}
address(…)
{
…
}
Hàm main() gọi hàm address() và hàm address() được gọi trước khi nó được định nghĩa. Mặc dù, nó không được khai báo trong hàm main() thì điều này có thể thực hiện được trong một số trình biên dịch C, hàm address() được gọi mà không cần khai báo gì thêm cả. Đây là sự khai báo không tường minh của một hàm.
Trong trình biên dịch Dev-C++, ta cần phải khai báo nguyên mẫu hàm trước hàm main() nếu như muốn định nghĩa hàm đó sau hàm main(), ví dụ:
#include <stdio.h>
address(…);
main()
{
…
address();
…
}
address(…)
{
…
}
15.5 Các nguyên mẫu hàm
Một nguyên mẫu hàm là một khai báo hàm trong đó xác định rõ kiểu dữ liệu của các đối số và giá trị trả về. Thông thường, các hàm được khai báo bằng cách xác định kiểu của giá trị được trả về bởi hàm, và tên hàm. Tuy nhiên, chuẩn ANSI C cho phép số lượng và kiểu dữ liệu của các đối số hàm được khai báo. Một hàm abc() có hai đối số kiểu int là x và y, và trả về một giá trị kiểu char, có thể được khai báo như sau:
char abc();
hoặc
char abc(int x, nt y);
Cách định nghĩa sau được gọi là nguyên mẫu hàm. Khi các nguyên mẫu được sử dụng, C có thể tìm và thông báo bất kỳ kiểu dữ liệu không hợp lệ khi chuyển đổi giữa các đối số được dùng để gọi một hàm với sự định nghĩa kiểu của các tham số. Một lỗi sẽ được thông báo ngay khi có sự khác nhau giữa số lượng các đối số được sử dụng để gọi hàm và số lượng các tham số khi định nghĩa hàm.
Cú pháp tổng quát của một nguyên mẫu hàm:
type function_name(type parm_namel,type parm_name2,..type
parm_nameN);
Khi hàm được khai báo không có các thông tin nguyên mẫu, trình biên dịch cho rằng không có thông tin về các tham số được đưa ra. Một hàm không có đối số có thể gây ra lỗi khi khai báo không có thông tin nguyên mẫu. Để tránh điều này, khi một hàm không có tham số, nguyên mẫu của nó sử dụng void trong cặp dấu ngoặc (). Như đã nói ở trên, void cũng được sử dụng để khai báo tường minh một hàm không có giá trị trả về.
Ví dụ, nếu một hàm noparam() trả về kiểu dữ liệu char và không có các tham số được gọi, có thể được khai báo như sau:
char noparam(void);
Khai báo trên chỉ ra rằng hàm không có tham số, và bất kỳ lời gọi có truyền tham số đến hàm đó là không đúng.
Khi một hàm không nguyên mẫu được gọi, tất cả các kiểu char được đổi thành kiểu int và tất cả kiểu float được đổi thành kiểu double. Tuy nhiên, nếu một hàm là nguyên mẫu, thì các kiểu đã đưa ra trong nguyên mẫu được giữ nguyên và không có sự tăng cấp kiểu xảy ra.
15.6 Các biến
Như đã thảo luận, các biến là những vị trí được đặt tên trong bộ nhớ, được sử dụng để chứa giá trị có thể hoặc không thể được sửa đổi bởi một chương trình hoặc một hàm. Có ba loại biến cơ bản: biến cục bộ, tham số hình thức, và biến toàn cục.
1-Biến cục bộ: là những biến được khai báo bên trong một hàm.
2-Tham số hình thức: được khai báo trong một định nghĩa hàm như là các tham số.
3-Biến toàn cục: được khai báo bên ngoài các hàm.
15.6.1 Biến cục bộ
Biến cục bộ còn được gọi là biến động, từ khoá auto được sử dụng để khai báo chúng. Chúng chỉ được tham chiếu đến bởi các lệnh bên trong của khối lệnh mà biến được khai báo. Để rõ hơn, một biến cục bộ được tạo ra trong lúc vào một khối và bị huỷ trong lúc đi ra khỏi khối đó. Khối lệnh thông thường nhất mà trong đó một biến cục bộ được khai báo chính là hàm.
Xem đoạn mã lệnh sau:
void blkl(void) /* void denotes no value returned*/
{
char ch;
ch = ‘a’;
…
}
void blk2(void)
{
char ch;
ch = ‘b’;
…
}
Biến ch được khai báo hai lần, trong blk1() và blk2(). ch trong blk1() không có liên quan đến ch trong blk2() bởi vì mỗi ch chỉ được biết đến trong khối lệnh mà nó được khai báo.
Vì các biến cục bộ được tạo ra và huỷ đi trong một khối mà chúng được khai báo, nên nội dung của chúng bị mất bên ngoài phạm vi của khối. Điều này có nghĩa là chúng không thể duy trì giá trị của chúng giữa các lần gọi hàm.
Từ khóa auto có thể được dùng để khai báo các biến cục bộ, nhưng thường nó không được dùng vì mặc nhiên các biến không toàn cục được xem như là biến cục bộ.
Các biến cục bộ được sử dụng bởi các hàm thường được khai báo ngay sau dấu ngoặc mở ‘{‘ của hàm và trước tất cả các câu lệnh. Tuy nhiên, các khai báo có thể ở bên trong một khối của một hàm. Ví dụ:
void blk1(void)
{
int t;
t = 1;
if(t > 5)
{
char ch;
…
}
}
Trong ví dụ trên biến ch được tạo ra và chỉ hợp lệ bên trong khối mã lệnh if. Nó không thể được tham chiếu đến trong một phần khác của hàm blk1().
Một trong những thuận lợi của sự khai báo một biến theo cách này đó là bộ nhớ sẽ chỉ được cấp phát cho nó khi nếu điều kiện để đi vào khối lệnh if được thoả. Điều này là bởi vì các biến cục bộ chỉ được khai báo khi đi vào khối lệnh mà các biến được định nghĩa trong đó.
Chú ý: Điều quan trọng cần nhớ là tất cả các biến cục bộ phải được khai báo tại điểm bắt đầu của khối mà trong đó chúng được định nghĩa, và trước tất cả các câu lệnh thực thi.
15.6.2 Tham số hình thức
Một hàm sử dụng các đối số phải khai báo các biến để nhận các giá trị của các đối số. Các biến này được gọi là tham số hình thức của hàm và hoạt động giống như bất kỳ một biến cục bộ bên trong hàm.
Các biến này được khai báo bên trong cặp dấu ngoặc () theo sau tên hàm. Xem ví dụ sau:
blk1(char ch, int i)
{
if(i > 5)
ch = ‘a’;
else
i = i +1;
return;
}
Hàm blk1() có hai tham số: ch và i.
Các tham số hình thức phải được khai báo cùng với kiểu của chúng. Như trong ví dụ trên, ch có kiều char và i có kiểu int. Các biến này có thể được sử dụng bên trong hàm như các biến cục bộ bình thường. Chúng bị huỷ đi khi ra khỏi hàm. Cần chú ý là các tham số hình thức đã khai báo có cùng kiểu dữ liệu với các đối số được sử dụng khi gọi hàm. Trong trường hợp có sai, C có thể không hiển thị lỗi nhưng có thể đưa ra một kết quả không mong muốn. Điều này là vì, C vẫn đưa ra một vài kết quả trong các tình huống khác thường. Người lập trình phải đảm bảo rằng không có các lỗi về sai kiểu.
Cũng giống như với các biến cục bộ, các phép gán cũng có thể được thực hiện với tham số hình thức của hàm và chúng cũng có thể được sử dụng bất kỳ biểu thức nào mà C cho phép.
15.6.3 Biến toàn cục
Các biến toàn cục là biến được thấy bởi toàn bộ chương trình, và có thể được sử dụng bởi một mã lệnh bất kỳ. Chúng được khai báo bên ngoài các hàm của chương trình và lưu giá trị của chúng trong suốt sự thực thi của chương trình. Các biến này có thể được khai báo bên ngoài main() hoặc khai báo bất kỳ nơi đâu trước lần sử dụng đầu tiên. Tuy nhiên, nơi tốt nhất để khai báo các biến toàn cục là tại đầu chương trình, nghĩa là trước hàm main().
int ctr; /* ctr is global */
void blk1(void);
void blk2(void);
void main(void)
{
ctr = 10;
blk1 ();
…
}
void blk1(void)
{
int rtc;
if (ctr > 8)
{
rtc = rtc + 1;
blk2();
}
}
void blk2(void)
{
int ctr;
ctr = 0;
}
Trong đoạn mã lệnh trên, ctr là một biến toàn cục và được khai báo bên ngoài hàm main() và blk1(), nó có thể được tham chiếu đến trong các hàm. Biến ctr trong blk2(), là một biến cục bộ và không có liên quan với biến toàn cục ctr. Nếu một biến toàn cục và cục bộ có cùng tên: tất cả các tham chiếu đến tên đó bên trong khối chứa định nghĩa biến cục bộ sẽ được kết hợp với biến cục bộ mà không phải là biến toàn cục.
Các biến toàn cục được lưu trữ trong các vùng cố định của bộ nhớ. Các biến toàn cục hữu dụng khi nhiều hàm trong chương trình sử dụng cùng dữ liệu. Tuy nhiên, nên tránh sử dụng biến toàn cục nếu không cần thiết, vì chúng giữ bộ nhớ trong suốt thời gian thực hiện chương trình. Vì vậy việc sử dụng một biến toàn cục ở nơi mà một biến cục bộ có khả năng đáp ứng cho hàm sử dụng là không hiệu quả. Ví dụ sau sẽ giúp làm rõ hơn điều này:
void addgen(int i, int j)
{
return(i + j);
}
int i, j;
void addspe(void)
{
return(i + j);
}
Cả hai hàm addgen() và addspe() đều trả về tổng của các biến i và j. Tuy nhiên, hàm addgen() được sử dụng để trả về tổng của hai số bất kỳ; trong khi hàm addspe() chỉ trả về tổng của các biến toàn cục i và j.
15.7 Lớp lưu trữ (Storage Class)
Mỗi biến trong C có một đặc trưng được gọi là lớp lưu trữ. Lớp lưu trữ xác định hai khía cạnh của biến: thời gian sống của biến và phạm vi của biến. Thời gian sống của một biến là thời gian mà giá trị của biến tồn tại. Phạm vi của một biến xác định các phần của một chương trình sẽ có thể nhận ra biến. Một biến có thể xuất hiện trong một khối, một hàm, một tập tin, một nhóm các tập tin, hoặc toàn bộ chương trình
Theo cách nhìn của trình biên dịch C, một tên biến xác định một vài vị trí vật lý bên trong máy tính, ở đó một chuỗi các bit biểu diễn giá trị được lưu trữ của biến. Có hai loại vị trí trong máy tính mà ở đó giá trị của biến có thể được lưu trữ: bộ nhớ hoặc thanh ghi CPU. Lớp lưu trữ của biến xác định vị trí biến được lưu trữ là trong bộ nhớ hay trong một thanh ghi. C có bốn lớp lưu trữ. Đó là:
Ø auto
Ø external
Ø static
Ø register
Đó là các từ khoá. Cú pháp tổng quát cho khai báo biến như sau:
storage_specifier type var_name;
15.7.1 Biến tự động (auto)
Biến tự động thật ra là biến cục bộ mà chúng ta đã nói ở trên. Phạm vi của một biến tự động có thể nhỏ hơn hàm, nếu nó được khai báo bên trong một câu lệnh ghép: phạm vi của nó bị giới hạn trong câu lệnh ghép đó. Chúng có thể được khai báo bằng từ khóa auto, nhưng sự khai báo này là không cần thiết. Bất kỳ một biến được khai báo bên trong một hàm hoặc một khối lệnh thì mặc nhiên là thuộc lớp auto và hệ thống cung cấp vùng bộ nhớ được yêu cầu cho biến đó.
15.7.2 Biến ngoại (extern)
Trong C, một chương trình lớn có thể được chia thành các module nhỏ hơn, các module này có thể được biên dịch riêng lẻ và được liên kết lại với nhau. Điều này được thực hiện nhằm tăng tốc độ quá trình biên dịch các chương trình lớn.
Tuy nhiên, khi các module được liên kết, các tập tin phải được chương trình thông báo cho biết về các biến toàn cục được yêu cầu. Một biến toàn cục chỉ có thể được khai báo một lần. Nếu hai biến toàn cục có cùng tên được khai báo trong cùng một tập tin, một thông điệp lỗi ‘duplicate variable name’ (tên biến trùng) có thể được hiển thị hoặc đơn giản trình biên dịch C chọn một biến khác. Một lỗi tương tự xảy ra nếu tất cả các biến toàn cục được yêu cầu bởi chương trình chứa trong mỗi tập tin.
Mặc dù trình biên dịch không đưa ra bất kỳ một thông báo lỗi nào trong khi biên dịch, nhưng sự thật các bản sao của cùng một biến đang được tạo ra. Tại thời điểm liên kết các tập tin, bộ liên kết sẽ hiển thị một thông báo lỗi như sau ‘duplicate label’ (nhãn trùng nhau) vì nó không biết sử dụng biến nào.
Lớp extern được dùng trong trường hợp này. Tất cả các biến toàn cục được khai báo trong một tập tin và các biến giống nhau được khai báo là biến ngoại, trong tất cả các tập tin.
Xem đoạn mã lệnh sau:
File1 File2
int i,j; extern int i,j;
char a; extern char a;
main() xyz()
{ {
… i = j * 5
… …
} }
abc() pqr()
{ {
i = 123; j = 50;
… …
} }
File2 có các biến toàn cục giống như File1, ngoại trừ một điểm là các biến này có từ khóa extern được thêm vào sự khai báo của chúng. Từ khóa này nói với trình biên dịch là tên và kiểu của biến toàn cục được sử dụng mà không cần phải tạo lại sự lưu trữ cho chúng. Khi hai module được liên kết, các tham chiếu đến các biến ngoại được giải quyết.
Nếu một biến không được khai báo trong một hàm, trình biên dịch sẽ kiểm tra nó có so khớp với bất kỳ biến toàn cục nào không. Nếu khớp với một biến toàn cục, thì trình biên dịch sẽ xem như một biến toàn cục đang được tham chiếu đến.
15.7.3 Biến tĩnh (static)
Các biến tĩnh là các biến cố định bên trong các hàm và các tập tin. Không giống như các biến toàn cục, chúng không được biết đến bên ngoài hàm hoặc tập tin của chúng, nhưng chúng giữ được giá trị của chúng giữa các lần gọi. Điều này có nghĩa là, nếu một hàm kết thúc và sau đó được gọi lại, các biến tĩnh đã định nghĩa trong hàm đó vẫn giữ được giá trị của chúng. Sự khai báo biến tĩnh được bắt đầu với từ khóa static.
Có thể định nghĩa các biến tĩnh có cùng tên như hướng dẫn với các biến ngoại. Các biến cục bộ (biến tĩnh cũng như biến động) có độ ưu tiên cao hơn các biến ngoại và giá trị của các biến ngoại sẽ không ảnh hưởng bởi bất kỳ sự thay đổi nào các biến cục bộ. Các biến ngoại có cùng tên với các biến nội trong một hàm không thể được truy xuất trực tiếp bên trong hàm đó.
Các giá trị khởi tạo có thể được gán cho các biến trong sự khai báo các biến tĩnh, nhưng các giá trị này phải là các hằng hoặc các biểu thức. Trình biên dịch tự động gán một giá trị mặc nhiên 0 đến các biến tĩnh không được khởi tạo. Sự khởi tạo thực hiện ở đầu chương trình.
Xem hai chương trình sau. Sự khác nhau giữa 2 biến cục bộ: tự động và tĩnh sẽ được làm rõ.
Ví dụ về biến tự động:
#include <stdio.h>
#include <conio.h>
void incre();
main()
{
incre();
incre();
incre();
getch();
}
void incre()
{
char var = 65; /* var is automatic variable*/
printf("
The character stored in var is %c", var++);
}
Ví dụ về biến tĩnh:
#include<stdio.h>
#include <conio.h>
void incre();
main()
{
incre();
incre();
incre();
getch();
}
void incre()
{
static char var = 65; /* var is static variable */
printf("
The character stored in var is %c", var++);
}
Cả hai chương trình gọi incre() ba lần. Trong chương trình thứ nhất, mỗi lần incre() được gọi, biến var với lớp lưu trữ auto (lớp lưu trữ mặc định) được khởi tạo lại là 65 (là mã ASCII tương ứng của ký tự A). Vì vậy khi kết thúc hàm, giá trị mới của var (66) bị mất đi (ASCII ứng với ký tự B).
Trong chương trình thứ hai, var là của lớp lưu trữ static. Ở đây var được khởi tạo là 65 chỉ một lần duy nhất khi biên dịch chương trình. Cuối lần gọi hàm đầu tiên, var có giá trị 66 (ASCII B) và tương tự ở lần gọi kế tiếp var có giá trị 67 (ASCII C). Sau lần gọi hàm cuối cùng, var được tăng giá trị theo sự thi hành của lệnh printf(). Giá trị này bị mất khi chương trình kết thúc.
15.7.4 Biến thanh ghi (register)
Các máy tính có các thanh ghi trong bộ số học logic - Arithmetic Logic Unit (ALU), các thanh ghi này được sử dụng để tạm thời lưu trữ dữ liệu được truy xuất thường xuyên. Kết quả tức thời của phép tính toán cũng được lưu vào các thanh ghi. Các thao tác thực hiện trên dữ liệu lưu trữ trong các thanh ghi thì nhanh hơn dữ liệu trong bộ nhớ. Trong ngôn ngữ assembly (hợp ngữ), người lập trình phải truy xuất đến các thanh ghi này và sử dụng chúng để giúp chương trình chạy nhanh hơn. Các ngôn ngữ lập trình bậc cao thường không truy xuất đến các thanh ghi của máy tính. Trong C, việc lựa chọn vị trí lưu trữ cho một giá trị tùy thuộc vào người lập trình.
Nếu một giá trị đặc biệt được dùng thường xuyên (ví dụ giá trị điều khiển của một vòng lặp), lớp lưu trữ của nó có thể khai báo là register.
Sau đó nếu trình biên dịch tìm thấy một thanh ghi còn trống, và các thanh ghi của máy tính đủ lớn để chứa biến, biến sẽ được đặt vào thanh ghi đó. Ngược lại, trình biên dịch sẽ xem các biến thanh ghi như các biến động khác, nghĩa là lưu trữ chúng trong bộ nhớ. Từ khóa register được dùng khi định nghĩa các biến thanh ghi.
Phạm vi và sự khởi tạo của các biến thanh ghi giống như các biến động, ngoại trừ vị trí lưu trữ. Các biến thanh ghi là cục bộ trong một hàm. Nghĩa là, chúng tồn tại khi hàm được gọi và giá trị bị mất đi một khi thoát khỏi hàm. Sự khởi tạo các biến này được thực hiện bởi người lập trình.
Vì số lượng các thanh ghi là có hạn, lập trình viên cần xác định các biến nào trong chương trình được sử dụng thường xuyên để khai báo chúng là các biến thanh ghi.
Sự hữu dụng của các biến thanh ghi thay đổi từ máy này đến một máy khác và từ một trình biên dịch C này đến một trình biên dịch khác. Đôi khi các biến thanh ghi không được hỗ trợ bởi tất cả – từ khóa register vẫn được chấp nhận nhưng được xem giống như là từ khóa auto. Trong các trường hợp khác, nếu biến thanh ghi được hỗ trợ và nếu lập trình viên sử dụng chúng một cách hợp lý, chương trình sẽ được thực thi nhanh hơn gấp đôi.
Các biến thanh ghi được khai báo như bên dưới:
register int x;
register char c;
Sự khai báo thanh ghi chỉ có thể gắn vào các biến động và tham số hình thức. Trong trường hợp sau, sự khai báo sẽ giống như sau:
f(c,n)
register int c, n;
{
register int i;
...
}
Xét một ví dụ sau, ở đó chương trình hiển thị các số có tổng lập phương các số thành phần bằng chính nó. Ví dụ 370 là một số như vậy, vì:
3^3 + 7^3 + 0^3 = 27 + 343 + 0 = 370
Chương trình sau in ra các con số như vậy trong khoảng 1 đến 999:
#include <stdio.h>
#include <conio.h>
main()
{
register int i;
int no, digit, sum;
printf("
The numbers whose Sum of Cubes of Digits
is Equal to the number itself are:
");
for(i = 1; i < 999; i++)
{
sum = 0;
no = i;
while(no)
{
digit = no%10;
no = no/10;
sum = sum + digit * digit * digit;
}
if (sum == i)
printf("\t%d
", i);
}
getch();
Trong chương trình trên, giá trị của i , thay đổi từ 1 đến 999. Với mỗi giá trị này, lập phương của từng con số riêng lẻ được cộng và kết quả tổng được so sánh với i. Nếu hai giá trị này là bằng nhau, i được hiển thị. Vì i được sử dụng để điều khiển sự lặp, (phần chính của chương trình), nó được khai báo là của lớp lưu trữ thanh ghi. Sự khai báo này làm tăng hiệu quả của chương trình.
15.8 Các qui luật về phạm vi của một hàm
Qui luật về phạm vi là những qui luật quyết định một đoạn mã lệnh có thể truy xuất đến một đoạn mã lệnh khác hoặc dữ liệu hay không. Trong C, mỗi hàm của chương trình là các khối lệnh riêng lẻ. Mã lệnh bên trong một hàm là cục bộ với hàm đó và không thể được truy xuất bởi bất kỳ lệnh nào ở ngoài hàm, ngoại trừ lời gọi hàm. Mã lệnh bên trong một hàm là ẩn đối với phần còn lại của chương trình, và trừ khi nó sử dụng biến hoặc dữ liệu toàn cục, nó có thể tác động hoặc bị tác động bởi các phần khác của chương trình. Để rõ hơn, mã lệnh và dữ liệu được định nghĩa bên trong một hàm không thể tương tác với mã lệnh hay dữ liệu được định nghĩa trong hàm khác bởi vì hai hàm có phạm vi khác nhau.
Trong C, tất cả các hàm có cùng mức phạm vi. Nghĩa là, một hàm không thể được định nghĩa bên trong một hàm khác. Chính vì lý do này mà C không phải là một ngôn ngữ cấu trúc khối về mặt kỹ thuật.
15.9 Gọi hàm
Một cách tổng quát, các hàm giao tiếp với nhau bằng cách truyền tham số. Các tham số được truyền theo một trong hai cách sau:
Ø Truyền bằng giá trị.
Ø Truyền bằng tham chiếu.
15.9.1 Truyền bằng giá trị
Mặc nhiên trong C, tất cả các đối số của hàm được truyền bằng giá trị. Điều này có nghĩa là, khi các đối số được truyền đến hàm được gọi, các giá trị được truyền thông qua các biến tạm. Mọi sự thao tác chỉ được thực hiện trên các biến tạm này. Hàm được gọi không thể thay đổi giá trị của chúng. Xem ví dụ sau:
#include <stdio.h>
#include <conio.h>
int adder(int a, int b);
main()
{
int a, b, c;
a = b = c = 0;
printf("
Enter 1st integer: ");
scanf("%d", &a);
printf("
Enter 2nd integer: ");
scanf("%d", &b);
c = adder(a,b);
printf("
a & b in main() are: %d, % d", a, b);
printf("
c in main() is: %d", c);
/* c gives the addition of a and b */
getch();
}
int adder(int a, int b)
{
int c;
c = a + b;
a *= a;
b += 5;
printf("
a & b within adder function are: %d, %d ", a, b);
printf("
c within adder function is : %d",c);
return(c);
}
Chương trình trên nhận hai số nguyên, hai số này được truyền đến hàm adder(). Hàm adder() thực hiện như sau: nó nhận hai số nguyên như là các đối số của nó, cộng chúng lại, tính bình phương cho số nguyên thứ nhất, và cộng 5 vào số nguyên thứ hai, in kết quả và trả về tổng của các đối số thực. Các biến được sử dụng trong hàm main() và adder() có cùng tên. Tuy nhiên, không có gì là chung giữa chúng. Chúng được lưu trữ trong các vị trí bộ nhớ khác nhau. Điều này được thấy rõ từ kết quả của chương trình trên. Các biến a và b trong hàm adder() được thay đổi từ 2 và 4 thành 4 và 9. Tuy nhiên, sự thay đổi này không ảnh hưởng đến các giá trị của a và b trong hàm main(). Các biến được lưu ở những vị trí bộ nhớ khác nhau. Biến c trong main() thì khác với biến c trong adder().
Vì vậy, các đối số được gọi là truyền bằng giá trị khi giá trị của các biến được truyền đến hàm được gọi và bất kỳ sự thay đổi nào trên giá trị này cũng không ảnh hưởng đến giá trị gốc của biến đã truyền.
15.9.2 Truyền bằng tham chiếu
Khi các đối số được truyền bằng giá trị, các giá trị của đối số của hàm đang gọi không bị thay đổi. Tuy nhiên, có thể có trường hợp, ở đó giá trị của các đối số phải được thay đổi. Trong những trường hợp như vậy, truyền bằng tham chiếu được dùng. Truyền bằng tham chiếu, hàm được phép truy xuất đến vùng bộ nhớ thực của các đối số và vì vậy có thể thay đổi giá trị của các đối số của hàm gọi.
Ví dụ, xét một hàm, hàm này nhận hai đối số, hoán vị giá trị của chúng và trả về các giá trị của chúng. Nếu một chương trình giống như chương trình dưới đây được viết để giải quyết mục đích này, thì sẽ không bao giờ thực hiện được.
#include <stdio.h>
#include <conio.h>
void swap(int u, int v);
main()
{
int x, y;
x = 15; y = 20;
printf("
x = %d, y = %d
", x, y);
swap(x, y);
printf("
After interchanging x = %d, y = %d
", x, y);
getch();
}
void swap(int u, int v)
{
int temp;
temp = u;
u = v;
v = temp;
return;
}
Hàm swap() hoán vị các giá trị của u và v, nhưng các giá trị này không được truyền trở về hàm main(). Điều này là bởi vì các biến u và v trong swap() là khác với các biến u và v được dùng trong main(). Truyền bằng tham chiếu có thể được sử dụng trong trường hợp này để đạt được kết quả mong muốn, bởi vì nó sẽ thay đổi các giá trị của các đối số thực. Các con trỏ được dùng khi thực hiện truyền bằng tham chiếu.
Các con trỏ được truyền đến một hàm như là các đối số để cho phép hàm được gọi của chương trình truy xuất các biến mà phạm vi của nó không vượt ra khỏi hàm gọi. Khi một con trỏ được truyền đến một hàm, địa chỉ của dữ liệu được truyền đến hàm nên hàm có thể tự do truy xuất nội dung của địa chỉ đó. Các hàm gọi nhận ra bất kỳ thay đổi trong nội dung của địa chỉ. Theo cách này, đối số hàm cho phép dữ liệu được thay đổi trong hàm gọi, cho phép truyền dữ liệu hai chiều giữa hàm gọi và hàm được gọi. Khi các đối số của hàm là các con trỏ hoặc mảng, truyền bằng tham chiếu được tạo ra đối nghịch với cách truyền bằng giá trị.
Các đối số hình thức của một hàm là các con trỏ thì phải có một dấu ‘*‘ phía trước, giống như sự khai báo biến con trỏ, để xác định chúng là các con trỏ. Các đối số thực kiểu con trỏ trong lời gọi hàm có thể được khai báo là một biến con trỏ hoặc một biến được tham chiếu đến (&var).
Ví dụ, định nghĩa hàm:
getstr(char *ptr_str, int *ptr_int)
đối số ptr_str trỏ đến kiểu char và ptr_int trỏ đến kiểu int. Hàm có thể được gọi bằng câu lệnh:
getstr(pstr, &var)
ở đó pstr được khai báo là một con trỏ và địa chỉ của biến var được truyền. Gán giá trị thông qua:
*ptr_int = var;
Hàm bây giờ có thể gán các giá trị đến biến var trong hàm gọi, cho phép truyền theo hai chiều đến và từ hàm.
char *pstr;
Quan sát ví dụ sau của hàm swap(). Bài toán này sẽ giải quyết được khi con trỏ được truyền thay vì dùng biến.
Mã lệnh tương tự như sau:
#include <stdio.h>
#include <conio.h>
void swap(int *u, int *v);
main()
{
int x, y, *px, *py;
//Storing address of x in px
px = &x;
//Storing address of y in py
py = &y;
x = 15; y = 20;
printf("
x = %d, y = %d
", x, y);
swap (px, py);
//Passing addresses of x and y
printf("
After interchanging x = %d, y = %d
", x, y);
getch();
}
void swap(int *u, int *v)
//Accept the values of px and py into u and v
{
int temp;
temp = *u;
*u = *v;
*v = temp;
return;
}
Hai biến kiểu con trỏ px và py được khai báo, và địa chỉ của biến x và y được gán đến chúng. Sau đó các biến con trỏ được truyền đến hàm swap(), hàm này hoán vị các giá trị lưu trong x và y thông qua các con trỏ.
15.10 Sự lồng nhau của lời gọi hàm
Lời gọi một hàm từ một hàm khác được gọi là sự lồng nhau của lời gọi hàm. Một chương trình kiểm tra một chuỗi có phải là chuỗi đọc xuôi - đọc ngược như nhau hay không, là một ví dụ cho các lời gọi hàm lồng nhau. Từ đọc xuôi - ngược giống nhau là một chuỗi các ký tự đối xứng. Xem đoạn mã lệnh theo sau đây:
main()
{
…
palindrome();
…
}
palindrome()
{
…
getstr();
reverse();
cmp();
…
}
Trong chương trình trên, hàm main() gọi hàm palindrome(). Hàm palindrome() gọi đến ba hàm khác getstr(), reverse() và cmp(). Hàm getstr() để nhận một chuỗi ký tự từ người dùng, hàm reverse() đảo ngược chuỗi và hàm cmp() so sánh chuỗi được nhập vào và chuỗi đã được đảo.
Vì main() gọi palindrome(), hàm palindrome() lần lượt gọi các hàm getstr(), reverse() và cmp(), các lời gọi hàm này được gọi là được lồng bên trong palindrome().
Sự lồng nhau của các lời gọi hàm như trên là được phép, trong khi định nghĩa một hàm bên trong một hàm khác là không được chấp nhận trong C.
15.11 Hàm trong chương trình nhiều tập tin
Các chương trình có thể được tạo bởi nhiều tập tin. Những chương trình như vậy được tạo bởi các hàm lớn, ở đó mỗi hàm có thể chiếm một tập tin. Cũng như các biến trong các chương trình nhiều tập tin, các hàm cũng có thể được định nghĩa là static hoặc extern. Phạm vi của hàm extern có thể được sử dụng trong tất cả các tập tin của chương trình, và đó là cách lưu trữ mặc định cho các tập tin. Các hàm static chỉ được nhận biết bên trong tập tin chương trình và phạm vi của nó không vượt khỏi tập tin chương trình. Phần tiêu đề (header) của hàm như sau:
static fn _type fn_name (argument list)
hoặc
extern fn_type fn_name (argument list)
Từ khóa extern là một tuỳ chọn (không bắt buộc) vì nó là lớp lưu trữ mặc định.
15.12 Con trỏ đến hàm
Một đặc tính mạnh mẽ của C vẫn chưa được đề cập, chính là con trỏ hàm. Dù rằng một hàm không phải là một biến, nhưng nó có địa chỉ vật lý trong bộ nhớ nơi có thể gán cho một con trỏ. Một địa chỉ hàm là điểm đi vào của hàm và con trỏ hàm có thể được sử dụng để gọi hàm.
Để hiểu các con trỏ hàm làm việc như thế nào, thật sự cần phải hiểu thật rõ một hàm được biên dịch và được gọi như thế nào trong C. Khi mỗi hàm được biên dịch, mã nguồn được chuyển thành mã đối tượng và một điểm đi vào của hàm được thiết lập. Khi một lời gọi được thực hiện đến một hàm, một lời gọi ngôn ngữ máy được thực hiện để chuyển điều khiển đến điểm đi vào của hàm. Vì vậy, nếu một con trỏ chứa địa chỉ của điểm đi vào của hàm, nó có thể được dùng để gọi hàm đó.
Địa chỉ của một hàm có thể lấy được bằng cách sử dụng tên hàm không có dấu ngoặc () hay bất kỳ đối số nào.
Bạn đang đọc truyện trên: AzTruyen.Top