Chương 2: Lập trình khoa học bằng ngôn ngữ C

Trở về Mục lục cuốn sách

Giới thiệu chung

Như ta đã đề cập trước đây, C là một ngôn ngữ lập trình bậc cao linh động, và rất mạnh, được thiết kế với mục đích ban đầu là viết các hệ điều hành và các chương trình ứng dụng hệ thống. Thực ra, toàn bộ các hệ điều hành gốc UNIX, cũng như phần lớn các ứng dụng của UNIX như trình soạn thảo văn bản, trình quản lý cửa sổ, v.v. đều được viết bằng C. Tuy nhiên, C cũng là một phương tiện rất tốt để lập trình khoa học, vì theo yêu cầu tự nhiên, một ngôn ngữ lập trình khoa học tốt phải mạnh, có tính linh hoạt, và là ngôn ngữ bậc cao. Song cũng có nhiều đặc điểm của C vốn khiến cho nhà khoa học máy tính hứng thú lại không liên quan đến yêu cầu của nhà lập trình khoa học. Vì vậy, tiếp sau đây ta chỉ đề cập đến một phần của C mà thực sự cần thiết cho chương trình khoa học. Có thể tranh luận rằng phiên bản rút gọn này của C giống FORTRAN một cách đáng ngờ. Nhưng sự giống nhau này không đáng ngạc nhiên. Hơn hết, FORTRAN là một ngôn ngữ lập trình bậc cao được thiết kế dành riêng cho tính toán khoa học.

Cũng đã được đề cập, C++ là phần mở rộng của C, nhằm mục đích cho phép lập trình hướng đối tượng. Có quá nhiều đặc điểm của C++ so với nhu cầu của ta trong khóa học này. Tuy nhiên, C++ cũng bao gồm một số đặc điểm mới, không liên quan đến hướng đối tượng, vốn rất có ích cho người lập trình khoa học. Ta sẽ xem qua những đặc điểm đó ở cuối mục này. Cuối cùng, ta sẽ mô tả một số lớp của C++ cho phép tính số phức (vốn không có trong C), dùng mảng có kích cỡ thay đổi, và biểu diễn đồ họa trong chương trình.

Biến

Các tên biến trong C có thể chứa các chữ cái và số theo thứ tự bất kì, miễn là kí tự đầu tiên phải là chữ cái. Các tên cũng được phân biệt viết in, vì vậy tên viết thường và viết in là khác nhau. Dấu gạch dưới (_) cũng có thể được dùng trong tên biến, và được coi như một chữ cái. Không có giới hạn về độ dài của tên biến trong C. Dĩ nhiên, các tên biến không thể trùng với tên của các từ khóa, vốn đóng vai trò quan trọng trong ngôn ngữ C. Các từ khóa này bao gồm int, double, if, return, void, v.v. Sau đây là ví dụ những tên biến hợp lệ trong C:

x  c14  area  electron_mass  TEMPERATURE 

Ngôn ngữ C cũng hỗ trợ nhiều kiểu dữ liệu khác nhau. Tuy nhiên, có hai kiểu thường xuất hiện nhất trong các chương trình khoa học, đó là số nguyên, gọi là int, và số có phần thập phân (hay dấu phẩy động theo thuật ngữ máy tính), gọi là double. (Chú ý rằng những biến dùng kiểu dữ liệu số thập phân cơ bản, float thường không đủ khả năng lưu trữ đủ số chữ số có nghĩa cần thiết trong lập trình khoa học.) Kiểu dữ liệu (int hoặc double) của mỗi biến trong một chương trình C phải được khai báo trước khi biến đó xuất hiện trong một câu lệnh của chương trình.

Hằng số nguyên trong C được biểu thị, như thường lệ, bởi một dãy số, chẳng hạn,

0  57  4567  128933

Hằng số có phần thập phân được viết theo một trong hai cách: thông thường hoặc theo kí pháp khoa học, chẳng hạn,

0.01  70.456  3e+5  .5067e-16

Chuỗi kí tự được dùng trong chương trình khoa học chủ yếu để phục vụ công việc nhập xuất dữ liệu. Mỗi chuỗi là một dãy liên tiếp các kí tự liền nhau (tính cả dấu trống) và được đặt giữa một cặp dấu nháy kép, chẳng hạn,

"red"  "Austin TX, 78723"  "512-926-1477"

Kể cả dấu xuống dòng cũng có thể được đưa vào chuỗi bằng cách dùng chuỗi thoát nghĩa, \n chẳng hạn,

"Line 1\nLine 2\nLine 3"

Chuỗi trên đây sẽ xuất hiện ra màn hình của máy tính như sau

Line 1
Line 2
Line 3

Lời khai báo có tác dụng gắn nhóm một nhóm các biến với một kiểu dữ liệu cụ thể. Như đã đề cập từ trước, tất cả mọi biến đều phải được khai báo trước khi chúng xuất hiện trong câu lệnh trong chương trình. Một lời khai gồm có tên kiểu dữ liệu, tiếp theo là một hoặc nhiều tên biến, và kết thúc bằng một dấu chấm phẩy. Chẳng hạn,

int    a, b, c;
double    acc, epsilon, t;

trong đó a, b, và c đều được khai báo là các biến số nguyên, còn acc, epsilon, và t được khai báo là biến số có phần thập phân.

Một lời khai báo cũng có thể được dùng để gán giá trị ban đầu cho các biến. Một số ví dụ cụ thể như sau:

int    a = 3, b = 5;
double    factor = 1.2E-5;

Ở đây các biến số nguyên ab được gán các giá trị ban đầu lần lượt bằng 3 và 5, trong khi biến số có phần thập phân, factor được gán giá trị ban đầu bằng 1. 2 × 10 - 5.

Chú ý là không có hạn chế và độ dài của lời khai báo kiểu biến: mỗi lời khai báo có thể viết trên nhiều dòng, miễn là cuối cùng nó được kết thúc bởi một dấu chấm phẩy. Tuy nhiên, tất cả những lời khai báo trong chương trình (hay đoạn chương trình đều phải đứng trước câu lệnh thực thi đầu tiên.

Biểu thức và câu lệnh

Biểu thức đại diện cho một đơn vị dữ liệu—thường là một số. Biểu thức có thể chỉ bao gồm một đối tượng, như hằng số hay biến, hoặc tổ hợp nhiều đối tượng được nối với nhau bởi các toán tử. Biểu thức cũng có thể đại diện cho điều kiện logic vốn có thể nhận giá trị đúng hay sai. Tuy nhiên, trong C, các giá trị đúng và sai được thể hiện lần lượt bởi các giá trị nguyên 10. Một vài biểu thức đơn giản như sau:

a + b
x = y
t = u + v
x <= y
++j

Biểu thức thứ nhất, dùng toán tử cộng (+), biểu thị cho tổng của hai giá trị được gán cho các biến ab. Biểu thức thứ hai có một toán tử gán (=), và có tác dụng làm cho giá trị được biểu thị bởi y được gán cho x. Trong biểu thức thứ ba, giá trị của biểu thức (u + v) được gán cho t. Biểu thức thứ tư nhận giá trị 1 (đúng) nếu giá trị của x nhỏ hơn hoặc bằng giá trị của y. Còn không thì biểu thức này sẽ nhận giá trị 0 (sai). Ở đây, <= là một toán tử quan hệ để so sánh các giá trị của xy. Ví dụ cuối cùng có tác dụng làm cho giá trị của j được tăng thêm 1. Vì vậy, biểu thức tương đương với

j = j + 1

Toán tử tăng thêm (một đơn vị) ++ thuộc loại toán tử đơn, vì nó chỉ mang theo một hạng tử.

Câu lệnh khiến cho máy tính phải thực hiện một hành động cụ thể. Có ba loại câu lệnh trong C: câu lệnh biểu thức, câu lệnh phức hợp, và câu lệnh điều khiển.

Một câu lệnh biểu thức bao gồm một biểu thức theo sau là dấu chấm phẩy. Việc thực hiện một câu lệnh như vậy khiến cho biểu thức trong đó được định giá. Chẳng hạn:

a = 6;
c = a + b;
++j;

Hai câu lệnh biểu thức đầu tiên đều khiến cho giá trị của biểu thức ở bên phải dấu bằng được gán cho biến ở bên trái. Câu lệnh biểu thức thứ ba khiến cho giá trị của j được tăng thêm 1. Một lần nữa, không có sự hạn chế nào về chiều dài của câu lệnh biểu thức: một lệnh như vậy có thể được viết rải thành nhiều dòng, miễn là cuối cùng kết thúc với một dấu chấm phẩy.

Một câu lệnh phức hợp bao gồm một số câu lệnh đơn lẻ được nhóm lại trong một cặp ngoặc nhọn { }. Bản thân các câu lệnh đơn lẻ có thể là câu lệnh biểu thức, câu lệnh phức hợp, hoặc câu lệnh điều khiển. Không như nhưng câu lệnh biểu thức, câu lệnh phức hợp không kết thúc bằng dấu chấm phẩy. Một câu lệnh phức hợp tiêu biểu như sau:

{
  pi = 3.141593;
  circumference = 2. * pi * radius;
  area = pi * radius * radius;
}

Câu lệnh phức hợp nói trên chứa ba câu lệnh biểu thức, nhưng có tác dụng như một đối tượng duy nhất trong chương trình mà nó xuất hiện.

Hằng số tượng trưng là một tên gọi dùng để thay cho một chuỗi các kí tự. Những kí tự này có thể biếu diễn cho một số hoặc một chuỗi. Khi chương trình được biên dịch, mỗi sự xuất hiện của hằng số tượng trưng sẽ được thay bởi chuỗi các kí tự tương ứng với nó. Các hằng số tượng trưng thường được định nghĩa ở đầu chương trình, bằng cách viết

#define  NAME  text

trong đó NAME biểu thị một tên tượng trưng, và hay được viết toàn bằng chữ in, còn text biểu thị chuỗi các kí tự được gắn với tên đó. Chú ý rằng text không có dấu chấm phẩy phía sau, vì lời định nghĩa cho hằng số tượng trưng không phải là một câu lệnh thực sự trong C. Thật ra, trong quá trình biên dịch, việc giải quyết các tên tượng trưng được thực hiện (bởi bộ tiền xử lý C) trước khi bắt đầu biên dịch. Chẳng hạn, giả sử ta có một chương trình C chứa định nghĩa hằng số tượng trưng sau:

#define  PI  3.141593

Và giả sử rằng chương trình có chứa câu lệnh

area = PI * radius * radius;

Trong quá trình biên dịch, bộ tiền xử lý thay thế mỗi sự xuất hiện của hằng số tượng trưng PI với đoạn chữ tương ứng với nó. Vì vậy, câu lệnh trên sẽ trở thành

area = 3.141593 * radius * radius;

Các hằng số tượng trưng đặc biệt có ích trong những chương trình khoa học để biểu thị những hằng số trong tự nhiên, như khối lượng của một electron, tốc độ ánh sáng, v.v. Vì những đại lượng này đều không đổi nên việc gán nó cho một biến không có ý nghĩa gì.

Toán tử

Như ta đã thấy, các biểu thức nói chung đều được hình thành bằng việc ghép cac hằng số và biến bằng những toán tử khác nhau. Trong C, toán tử có 5 loại: toán tử số học, toán tử đơn, toán tử quan hệ và logic, toán tử gán, và toán tử điều kiện. Bây giờ ta hãy xét kĩ từng loại này:

4 toán tử số học chính trong C. Đó là:

cộng          +
trừ           -
nhân          *
chia          /

Thật khó tin là trong C không có sẵn một toán tử để tính lũy thừa (C được viết bởi các nhà khoa học máy tính)! Thay vào đó, có một hàm thư viện (pow) đảm nhiệm phép tính này (sau này ta sẽ đề cập tới).

Việc trộn lẫn các kiểu dữ liệu trong biểu thức toán học là một cách lập trình dở. Nói cách khác, hai hạng tử tham gia vào phép tính cộng, trừ, nhân, hoặc chia cần được thống nhất cùng kiểu int hoặc double. Giá trị của một biểu thức có thể được chuyển đổi sang một kiểu dữ liệu khác bằng cách điền vào phía trước tên của kiểu dữ liệu cần được chuyển đến, bao trong cặp dấu ngoặc đơn. Dạng cấu trúc này được gọi là hóa. Vì vậy, để chuyển từ một biến số nguyên j sang một biến số có phần thập phân, với cùng giá trị đó, ta có thể viết

(double) j

Cuối cùng, để tránh việc trộn lẫn kiểu dữ liệu khi chia một biến số có phần thập phân x, cho một biến số nguyên i, ta có thể viết

x / (double) i

Dĩ nhiên là kết quả của phép tính này sẽ có kiểu double.

Các toán tử trong C được nhóm theo từng cấp căn cứ vào độ ưu tiên, nghĩa là thứ tự được lượng giá. Trong số các toán tử số học, */ có độ ưu tiên cao hơn +-. Nói cách khác, khi lượng giá các biểu thức, C thực hiện nhân và chia trước cộng và trừ. Dĩ nhiên, quy tắc và độ ưu tiên luôn có thể bỏ qua được khi dùng cặp ngoặc đơn. Vì vậy, biểu thức

a - b / c + d

tương đương với biểu thức rõ ràng sau

a - (b / c) + d

vì phép chia có độ ưu tiên cao hơn phép cộng và trừ.

Đặc điểm riêng biệt của toán tử đơn là ở chỗ nó chỉ có tác dụng với đúng một hạng tử. Toán tử đơn thường gặp nhất là dấu âm, vốn xuất hiện trước một hằng số, một biến số, hoặc một biểu thức. Chú ý rằng dấu âm khác hẳn với toán tử số học (-) vốn để biểu thị phép trừ, vì toán tử trừ có tác dụng đối với hai toán hạng. Ngoài ra còn hai toán tử đơn thường gặp khác là toán tử tăng, ++, và toán tử giảm, --. Toán tử tăng có tác dụng làm cho hạng tử được tăng thêm 1, còn toán tử giảm làm cho toán hạng giảm đi 1. Chẳng hạn, --i tương đương với i = i - 1. Một phép hóa cũng có thể được coi là một toán tử đơn. Chú ý rằng toán tử đơn có thứ tự ưu tiên cao hơn các toán tử số học. Vì vậy, - x + y tương đương với biểu thức rõ ràng (-x) + y, vì toán tử dấu âm có độ ưu tiên cao hơn toán tử cộng.

Lưu ý rằng có một điểm khác biệt nhở giữa các biểu thức a++++a. Ở trường hợp thứ nhất, giá trị của biến a được trả lại trước khi nó được tăng lên. Ở trường hợp thứ hai, giá trị của a được trả lại sau khi được tăng. Vì vậy,

b = a++;

tương đương với

b = a;
a = a + 1;

trong khi

b = ++a;

tương đương với

a = a + 1;
b = a;

Cũng có sự khác biệt như vậy giữa các biểu thức a----a.

Có bốn toán tử quan hệ trong C. Chúng gồm có:

nhỏ hơn                   <
nhỏ hơn hoặc bằng         <=
lớn hơn                   >
lớn hơn hoặc bằng         >=

Độ ưu tiên của các toán tử này thấp hơn so với các toán tử số học.

Gắn liên với các toán tử quan hệ là hai toán tử bình đẳng:

bằng        ==
khác        !=

Độ ưu tiên của các toán tử bình đẳng thì thấp hơn các toán tử quan hệ.

Các toán tử quan hệ và bình đẳng được dùng để lập những biểu thức logic, nhằm biểu diễn những điều kiện hoặc đúng hoặc sai. Kết quả của những biểu thức này là kiểu int, vì giá trị đúng sẽ được biểu diễn bởi giá trị số nguyên 1 còn giá trị sai bởi giá trị nguyên 0. Chẳng hạn, biểu thức i < j là đúng (giá trị 1) nếu giá trị của i nhỏ hơn giá trị của j, hoặc sai (giá trị 0) trong trường hợp còn lại. Cũng như vậy, biểu thức j == 3 là đúng nếu giá trị j bằng 3, và sai trong trường hợp còn lại.

C cũng có hai toán tử logic khác. Chúng là:

&&        and (và)
||        or (hoặc)

Các biểu thức logic tác động lên các toán tử vốn là biểu thức logic. Hiệu ứng tổng cộng sẽ là sự kết hợp các biểu thức logic riêng rẽ thành một biểu thức phức tạp hơn với giá trị đúng hoặc sai. Kết quả của một phép toán and logic chỉ đúng khi cả hai hạng tử đều đúng, trong khi kết quả của một phép toán or logic chỉ sai khi cả hai hạng tử đều sai. Chẳng hạn, biểu thức (i >= 5) && (j == 3) là đúng nếu giá trị của i lớn hơn hoặc bằng 5 giá trị của j bằng 3, hoặc là sai trong trường hợp còn lại. Độ ưu tiên của toán tử and logic cao hơn so với toán tử or logic, nhưng thấp hơn so với các toán tử bình đẳng.

C cũng có toán tử đơn ! để phủ định giá trị của một biểu thức logic: nghĩa là nó làm cho một biểu thức ban đầu đúng trở thành sai, và ngược lại. Nó được gọi là toán tử phủ định logic hay not logic. Chẳng hạn, biểu thức !(k == 4) đúng nếu giá trị của k không bằng 4, và sai trong trường hợp còn lại.

Chú ý rằng việc dựa quá nhiều vào độ ưu tiên của toán tử sẽ là cách lập trình dở, vì sự dựa dẫm đó có xu hướng khiến chương trình C trở nên khó đọc hơn đối với người khác. Chẳng hạn, thay vì viết

i + j == 3 && i * l >= 5

và dựa vào thực tế là các toán tử số học có độ ưu tiên hơn so với các toán tử quan hệ và bình đẳng, mà bản thân chúng lại có độ ưu tiên cao hơn so với các toán tử logic, tốt hơn là ta nên viết

((i + j) == 3) && (i * l >= 5)

và ý nghĩa đã tương đối sáng tỏ, ngay cả đối với những người không nhớ được thứ tự ưu tiên giữa các toán tử khác nhau của C.

Toán tử gán thông dụng nhất trong C là =. Chẳng hạn, biểu thức

f = 3.4

khiến cho giá trị số thập phân 3.4 được gán cho biến f. Lưu ý rằng toán tử gán = và toán tử bình đẳng == có chức năng hoàn toàn khác nhau trong C, và nhất thiết không được nhầm lẫn. Phép gán nhiều lần cũng hợp lệ trong C. Chẳng hạn,

i = j = k = 4

khiến cho giá trị nguyên 4 được gán cho i, j, và k cùng một lúc. Một lần nữa, cần lưu ý rằng không nên trộn các kiểu dữ liệu khác nhau vào trong biểu thức gán. Vì vậy, các kiểu dữ liệu của những hằng hoặc biến số ở hai phía của dấu = phải luôn hợp nhau.

C có thêm bốn toán tử gán: +=, -=, *=, và /=. Biểu thức

i += 6

tương đương với i = i + 6. Tương tự, biểu thức

i -= 6

tương đương với i = i - 6. Biểu thức

i *= 6

tương đương với i = i * 6. Cuối cùng, biểu thức

i /= 6

tương đương với i = i / 6. Chú ý rằng độ ưu tiên của toán tử gán thì thấp hơn tất cả những toán tử đã đề cập trước đây.

Những phép tính điều kiện đơn giản có thể được thực hiện bằng toán tử điều kiện (? : ). Một biểu thức tận dụng toán tử điều kiện được gọi là biểu thức điều kiện. Biểu thức như vậy có dạng chung sau

biểu thức 1  ?  biểu thức 2  :  biểu thức 3

Nếu biểu thức 1 đúng (tức là giá trị của nó khác không) thì biểu thức 2 được định giá và trở thành giá trị của biểu thức điều kiện. Còn trong trường hợp còn lại, biểu thức 1 sai (tức là giá trị của nó bằng không) thì biểu thức 3 sẽ được lượng giá và trở thành giá trị của biểu thức điều kiện. Chẳng hạn, biểu thức

(j < 5) ? 12 : -6

nhận giá trị 12 nếu giá trị của j nhỏ hơn 5, và nhận giá trị -6 trong trường hợp còn lại. Câu lệnh gán

k = (i < 0) ? n : m

sẽ làm cho giá trị của n được gán cho biến k nếu giá trị của i nhỏ hơn không, và giá trị của m được gán cho k trong trường hợp còn lại. Độ ưu tiên của toán tử điều kiện chỉ lớn hơn các toán tử gán.

Cũng được đề cập đến trước đây, các chương trình khoa học thường có xu hướng tiêu tốn rất nhiều tài nguyên máy. Vì vậy người lập trình khoa học luôn cần chú ý tới những phương pháp để tăng tốc độ thực thi mã lệnh. Điều quan trọng phải nhận thấy là các phép nhân (*) và (/) tiêu tốn thời gian hoạt động của CPU nhiều hơn một cách đáng kể so với các phép cộng (+), trừ (-), so sánh, hoặc gán. Vì vậy, một quy tắc nằm lòng đơn giản để viết được mã lệnh hiệu quả là tránh các phép tính nhân và chia không cần thiết. Điều này đặc biệt quan trọng với những đoạn mã được thực hiện lặp đi lặp lại, chẳng hạn những đoạn mã nằm trong các vòng lặp. Một minh họa kinh điển cho điều này là cách lượng giá một đa thức. Phương pháp dễ thấy nhất để lượng giá, chẳng hạn một đa thức bậc bốn, sẽ là như sau:

p = c_0 + c_1 * x + c_2 * x * x + c_3 * x * x * x + c_4 * x * x * x * x

Chú ý rằng biểu thức trên dùng đến 10 phép tính nhân tốn kém. Tuy nhieen, con số này có thể giảm xuống còn 4 bằng cách biến đổi đại số đơn giản

p = c_0 + x * (c_1 + x * (c_2 + x * (c_3 + x * c_4)))

Rõ ràng là biểu thức sau hiệu quả hơn nhiều so với biểu thức đầu.

Các hàm trong thư viện

Ngôn ngữ C được đi kèm với một số các hàm trong thư viện chuẩn để thực hiện những nhiệm vụ hữu ích. Nói riêng, tất cả các thao tác nhập và xuất (tức là in ra màn hình) và các phép toán (ví dụ tính sin và cô-sin) đều được thực hiện được bởi các hàm trong thư viện.

Để dùng được một hàm thư viện, cần phải gọi header file phù hợp ở đầu chương trình. Header file sẽ báo cho chương trình biết về tên, kiểu, số lượng và kiểu của các đối số cho tất cả các hàm có trong thư viện cần quan tâm. Một header file sẽ được gọi qua lệnh tiền xử lý sau:

#include   <filename>

trong đó filename biểu thị tên của header file này.

Một hàm trong thư viện được truy cập đơn giản chỉ bằng cách viết tên hàm, tiếp theo là một danh sách các đối số, để biểu thị thông tin được chuyển vào trong hàm. Những đối số này phải được đặt trong cặp ngoặc đơn, và phân cách bởi các dấu phẩy; chúng có thể là các hằng số, biến, hoặc những biểu thức phức tạp hơn. Chú ý rằng cặp ngoặc kép phải có mặt ngay cả khi không có đối số nào.

Thư viện math (toán) của C có header file math.h, trong đó chứa những hàm có ích sau:

Hàm             Kiểu        Tác dụng     
--------        ----        -------

acos(d)         double      Trả lại arc cosin của d (trong khoảng từ 0 đến pi)
asin(d)         double      Trả lại arc sin của d (trong khoảng từ -pi/2 đến pi/2)
atan(d)         double      Trả lại arc tang của d (trong khoảng từ -pi/2 đến pi/2)
atan2(d1, d2)   double      Trả lại arc tang của d1/d2 (trong khoảng từ -pi đến pi)
cbrt(d)         double      Trả lại căn bậc ba của d
cos(d)          double      Trả lại cosin của d
cosh(d)         double      Trả lại cosin hyperbol của d
exp(d)          double      Trả lại e mũ d
fabs(d)         double      Trả lại giá trị tuyệt đối của d
hypot(d1, d2)   double      Trả lại sqrt(d1 * d1 + d2 * d2)
log(d)          double      Trả lại loga tự nhiên của d
log10(d)        double      Trả lại loga (cơ số 10) của d
pow(d1, d2)     double      Trả lại d1 nâng lên lũy thừa d2
sin(d)          double      Trả lại sin của d
sinh(d)         double      Trả lại sin hyperbol của d
sqrt(d)         double      Trả lại căn bậc hai của d
tan(d)          double      Trả lại tang của d
tanh(d)         double      Trả lại tang hyperbol của d

Ở đây, Kiểu dùng để chỉ kiểu dữ liệu của đại lượng được hàm số trả về. Ngoài ra, d, d1, v.v. để chỉ các đối số có kiểu double.

Một chương trình sử dụng thư viện math của C cần có câu lệnh

#include   <math.h>

ngay ở đầu. Trong phần thân của chương trình, một lệnh như

x = cos(y);

sẽ khiến cho biến x được gán cho một giá trị vốn là cosin của giá trị thuộc về biến y (cả xy cần phải có cùng kiểu double).

Lưu ý rằng các hàm thư viện toán có xu hướng rất tiêu tốn thời gian chạy CPU, và do đó chỉ nên được dùng đến khi thực sự cần thiết. Ví dụ minh họa điển hình cho điều này là việc dùng hàm pow(). Hàm này giả sử rằng, nói chung ta sẽ gọi nó với một số mũ là số có phần thập phân và do đó, thi hành việc khai triển chuỗi hoàn chỉnh (và cũng rất tiêu tốn). Rõ ràng nếu muốn tính bình phương hoặc lập phương thì việc dùng hàm này sẽ không có hiệu quả. Nói cách khác, nếu một đại lượng cần được nâng lên lũy thừa một số nguyên dương, nhỏ, thì có thể trực tiếp dùng phép nhân thay vì dùng hàm pow(). Nghĩa là ta nên viết x * x thay vì pow(x, 2), và x * x * x thay vì pow(x, 3), v.v. (Dĩ nhiên, một hàm lũy thừa được thiết kế cẩn thận sẽ phải tự nhận ra rằng sẽ hiệu quả hơn khi tính các lũy thừa số nguyên nhỏ bằng cách trực tiếp. Thật không may là hàm pow() lại được viết bởi các nhà khoa học máy tính!)

Thư viện math của C đi kèm với một tập hợp các hằng số toán học được định sẵn:

Tên gọi     Mô tả    
----        -----------    

M_PI        Pi, tỉ số giữa chu vi đường tròn và đường kính.
M_PI_2      Pi chia cho 2.
M_PI_4      Pi chia cho 4.    
M_1_PI      Nghịch đảo của pi (1/pi).
M_SQRT2     Căn bậc hai của 2.
M_SQRT1_2   Nghịch đảo của căn bậc hai của 2 
             (cũng là căn bậc hai của 1/2). 
M_E         Cơ sở của loga tự nhiên.

Các hàm thư viện khác thường dùng trong những chương trình C sẽ được giới thiệu vào lúc thích hợp trong quá trình học.

Nhập và xuất dữ liệu

Các thao tác nhập và xuất dữ liệu trong C được thực hiện bởi thư viện input/output chuẩn (header file: stdio.h) qua các hàm scanf, printf, fscanf, và fprintf, lần lượt để đọc dữ liệu từ và ghi dữ liệu ra thiết bị đầu cuối, đọc dữ liệu từ và ghi dữ liệu ra file. Các hàm phụ trợ fopenfclose lần lượt mở và đóng những kết nối giữa chương trình C với một file dữ liệu. Trong phần tiếp theo, các hàm này sẽ được mô tả kĩ hơn.

Hàm scanf đọc dữ liệu vào từ thiết bị vào chuẩn (thường là thiết bị đầu cuối hay bàn phím). Một lời gọi đến hàm này có dạng chung sau

scanf(control_string,  arg1,  arg2,  arg3,  ...)

trong đó #control_string#(chuỗi điều khiển) chỉ đến một chuỗi kí tự bao gồm những thông tin định dạng được yêu cầu nhất định, còn arg1, arg2, v.v. là các đối số biểu diễn những đơn vị dữ liệu vào riêng lẻ.

Chuỗi điều khiển bao gồm các nhóm kí tự riêng, với mỗi nhóm kí tự cho một đơn vị dữ liệu vào. Dạng đơn giản nhất của mỗi nhóm kí tự gồm có một dấu phần trăm (%), theo sau là một tập hợp các kí tự chuyển đổi để chỉ định kiểu của đơn vị dữ liệu tương ứng. Hai tập hợp kí tự chuyển đổi quan trọng nhất như sau:

Kí tự          Kiểu
---------      ----

d              int
lf             double

Còn các đối số là một tập hợp các biến mà kiểu của nó hợp với kiểu nhóm kí tự tương ứng trong chuỗi điều khiển. Với những lí do mà sẽ được giải thích sau này, mỗi tên biến phải viết sau một dấu kí hiệu và (&). Dưới đây là cách dùng tiêu biểu hàm scanf:

#include  <stdio.h>
. . . 
int  k;
double  x, y;
. . .
scanf("%d %lf %lf", &k, &x, &y);
. . .

Ở ví dụ này, hàm scanf đọc một giá trị nguyên và hai giá trị số thập phân, từ thiết bị đầu vào chuẩn, và đưa vào biến nguyên k và hai biến số có phần thập phân lần lượt là xy.

Hàm scanf trả về một số nguyên bằng số giá trị đã đọc được từ thiết bị đầu vào chuẩn, nó có thể nhỏ hơn con số dự kiến ban đầu, hoặc bằng không, trong trường hợp không có tương xứng giữa dữ liệu vào và biến. Giá trị đặc biệt EOF (mà trong nhiều hệ thống tương ứng với  - 1) được trả lại nếu việc đọc gặp phải kí tự kết thúc (end-of-file) trước khi công việc chuyển đổi được tiến hành. Đoạn mã lệnh sau là ví dụ về cách dùng hàm scanf để kiểm tra tính đúng đắn của dữ liệu đầu vào:

#include  <stdio.h>
. . . 
int  check_input;
double  x, y, z;
. . .
check_input = scanf("%lf %lf %lf", &x, &y, &z);
if (check_input < 3)
 {
  printf("Error during data input\n");
  . . .
 }
. . .

Sau này ta sẽ đề cập đến cấu trúc if().

Hàm printf ghi dữ liệu ra thiết bị đầu ra chuẩn (vốn thường là đầu cuối, hay màn hình). Một lời gọi đến hàm này có dạng chung sau

printf(control_string,  arg1,  arg2,  arg3,  ...)

trong đó control_string để chỉ một chuỗi kí tự bao gồm thông tin định dạng, và arg1, arg2, v.v., là các đối số biểu thị từng đối tượng dữ liệu đầu ra riêng lẻ.

Chuỗi kí tự điều khiển bao gồm các nhóm kí tự riêng, mỗi nhóm kí tự dành cho một đơn vị dữ liệu đầu ra. Dạng đơn giản nhất của một nhóm kí tự gồm có một dấu phần trăm (%), tiếp theo là một kí tự chuyển đổi để điều khiển định dạng của đơn vị dữ liệu tương ứng. Những kí tự chuyển đổi hữu ích nhất gồm có:

Kí tự        Ý nghĩa
---------    -------

d            Hiển thị đơn vị dữ liệu dưới dạng số nguyên có dấu
f            Hiển thị đơn vị dữ liệu dưới dạng số có phần thập phân, không lũy thừa
e            Hiển thị đơn vị dữ liệu dưới dạng số có phần thập phân, có lũy thừa    

Các đối số là một tập hợp các biến có kiểu tương ứng với các nhóm kí tự trong chuỗi điều khiển, nghĩa là kiểu int cho dạng d, và kiểu double cho dạng f hay e). Trái với hàm scanf, ở đây các đối số không đi sau dấu “và”. Dưới đây là một cách dùng điển hình của hàm scanf:

#include  <stdio.h>
. . . 
int  k = 3;
double  x = 5.4, y = -9.81;
. . .
printf("%d %f %f\n", k, x, y);
. . .

Ở ví dụ này, chương trình xuất ra màn hình các giá trị của biến nguyên k và các biến số có phần thập phân, xy. Nếu chạy chương trình ta sẽ được kết quả đầu ra kiểu như sau:

3 5.400000 -9.810000   
%

Lưu ý rằng mục đích của chuỗi thoát nghĩa \n trong chuỗi điều khiển là để xuống dòng sau khi ghi đủ 3 đơn vị dữ liệu ra màn hình.

Dĩ nhiên, hàm printf cũng có thể được dùng để viết một chuỗi kí tự đơn giản ra màn hình, chẳng hạn,

printf(text_string)

Đoạn chữ thường cũng có thể được đưa vào chuỗi điều khiển được đề cập tới ở trên.

Một ví dụ thể hiện cách dùng phức tạp hơn một chút của hàm printf là như sau:

#include  <stdio.h>
. . . 
int  k = 3;
double  x = 5.4, y = -9.81;
. . .
printf("k = %3d  x + y = %9.4f  x*y = %11.3e\n", k, x + y, x*y);
. . .

Chạy chương trình này, ta sẽ được đầu ra sau đây:

k =   3  x + y =   -4.4100  x*y =  -5.297e+01        
%

Lưu ý rằng hai đối số sau cùng của hàm printf đều là biểu thức số học. Cũng cần lưu ý việc đưa dòng chữ để giải thích vào trong chuỗi điều khiển.

Chuỗi kí tự %3d trong chuỗi điều khiển để chỉ định rằng đơn vị dữ liệu tương ứng cần được ghi ra dưới dạng số nguyên có dấu; trên màn hình nó sẽ chiếm một bề rộng ít nhất là 3 kí tự. Tổng quát hơn, chuỗi kí tự %nd chỉ định rằng số nguyên cần chiếm một chỗ có bề rộng ít nhấtn kí tự. Nếu số kí tự của bản thân dữ liệu không đến n, thì dữ liệu sẽ được chèn thêm các dấu trống vào phía đầu (bên trái) cho tới mức đủ chỗ định trước. Mặt khác, nếu dữ liệu vượt quá bề rộng định trước thì máy sẽ huy động đủ chỗ để hiển thị được toàn bộ dữ liệu.

Chuỗi kí tự %9.4f trong chuỗi điều khiển để chỉ định rằng đơn vị dữ liệu tương ứng cần được in ra dưới dạng số có phần thập phân, theo cách thông thường (không có lũy thừa kiểu khoa học), và chiếm chỗ ít nhất là 9 kí tự, với 4 chữ số trong phần thập phân. Tổng quát hơn, chuỗi kí tự %n.mf chỉ định rằng dữ liệu cần in ra dưới dạng số có phần thập phân, không theo lũy thừa khoa học, chiếm một chỗ có bề rộng ít nhấtn kí tự, với m chữ số trong phần thập phân.

Sau cùng, chuỗi kí tự %11.3e trong chuỗi điều khiển để chỉ định rằng đơn vị dữ liệu tương ứng cần được in ra dưới dạng số có phần thập phân, theo hình thức lũy thừa kiểu khoa học, và chiếm chỗ ít nhất là 11 kí tự, với 3 chữ số trong phần thập phân. Tổng quát hơn, chuỗi kí tự %n.me chỉ định rằng dữ liệu cần in ra dưới dạng số có phần thập phân, có lũy thừa khoa học, chiếm một chỗ có bề rộng ít nhấtn kí tự, với m chữ số trong phần thập phân.

Hàm printf trả lại một số nguyên bằng với số kí tự đã được in ra, hoặc một giá trị âm nếu có lỗi ở khâu đầu ra.

Khi làm việc với một file dữ liệu, bước đầu tiên là thiết lập một vùng đệm, tại đó thông tin được tạm thời lưu trữ trong khi được chuyển qua lại giữa chương trình và file. Vùng đệm này cho phép thông tin được đọc hoặc ghi ra file dữ liệu một cách nhanh chóng nhất. Một vùng đệm được lập ra bằng cách viết

FILE *stream;

trong đó FILE (cần viết toàn chữ in) là một kiểu cấu trúc đặc biệt nhằm thiết lập vùng đệm, còn stream là số chỉ định của vùng đệm được tạo ra. Lưu ý rằng vùng đệm thường được đề cập đến như một stream nhập/xuất. Ý nghĩa của dấu sao (*) đứng trước số chỉ định của stream sẽ được giải thích sau này. Dĩ nhiên là ta có thể thiết lập nhiều stream nhập/xuất, miễn là chúng có số chỉ định riêng biệt.

Một file dữ liệu phải được mở và gắn với một stream nhập/xuất cụ thể trước khi nó có thể được tạo ra hay xử lý. Thao tác này được đảm nhiệm bởi hàm fopen. Một lời gọi điển hình đến fopen có dạng

stream = fopen(file_name, file_type);

trong đó stream là thẻ nhận diện của stream input/output tại đó mà file được gắn vào, và file_name cùng file_type là các chuỗi kí tự để lần lượt chỉ tên file dữ liệu và kiểu thao tác với file dữ liệu đó. Chuỗi file_type phải là một trong các chuỗi sau:

file_type         Ý nghĩa
---------         -------

"r"        Mở file đã có sẵn chỉ để đọc
"w"        Mở một file mới chỉ để ghi (Nếu file đã tồn tại thì
            sẽ bị ghi đè)
"a"        Mở file đã có để ghi bổ sung vào. (Dữ liệu đầu ra 
            sẽ được ghi thêm vào file)

Hàm fopen trả lại giá trị nguyên NULL (mà trong phần lớn các hệ thống tương ứng với số 0) nếu có lỗi xảy ra.

Một file dữ liệu cũng phải được đóng lại ở cuối chương trình. Thao tác này được thực hiện bởi hàm fclose. Cú pháp để thực hiện lợi gọi fclose đơn giản chỉ là

fclose(stream);

trong đó stream là tên của stream nhập/xuất cần được gỡ khỏi file dữ liệu. Hàm fclose trả lại giá trị nguyên 0 nếu thực hiện thành công, còn và giá trị đặc biệt EOF trong trường hợp thất bại.

Dữ liệu có thể được đọc từ một file dữ liệu đã mở sẵn bằng hàm fscanf, với cú pháp như sau

fscanf(stream, control_string,  arg1,  arg2,  arg3,  ...)

Ở đây, stream là thẻ nhận diện của stream nhập/xuất mà file gắn vào, còn các đối số còn lại có định dạng và ý nghĩa giống hệt như các đối số tương ứng của hàm scanf. Các giá trị trả về từ fscanf cũng giống như của hàm scanf.

Tương tự như vậy, dữ liệu có thể được ghi vào file mở sẵn bằng hàm fprintf với cú pháp như sau

fprintf(stream, control_string,  arg1,  arg2,  arg3,  ...)

Ở đây, stream là thẻ nhận diện của stream nhập/xuất mà file gắn vào, còn các đối số còn lại có định dạng và ý nghĩa giống hệt như các đối số tương ứng của hàm fprintf. Các giá trị trả về từ fprintf cũng giống như của hàm printf.

Một ví dụ của chương trình C nhằm xuất dữ liệu ra file “data.out” như sau:

#include  <stdio.h>
. . .
int  k = 3;
double  x = 5.4, y = -9.81;
FILE *output;
. . .
output = fopen("data.out", "w");
if (output == NULL)
 {
  printf("Error opening file data.out\n");
  . . .
 }
. . .
fprintf(output, "k = %3d  x + y = %9.4f  x*y = %11.3e\n", k, x + y, x*y);
. . .
fclose(output);
. . .

Khi chạy, chương trình trên sẽ in ra dòng

k =   3  x + y =   -4.4100  x*y =  -5.297e+01        

vào file “data.out”.

Cấu trúc của một chương trình C

Cú pháp của một chương trình C hoàn chỉnh như sau:

. . .
int main() 
{
 . . .
 return 0;
}
. . .

Ý nghĩa của các câu lệnh int main()return sau này sẽ được làm rõ. Các lệnh tiền xử lý như #define#include theo thông lệ được đặt trước lệnh int main(). Tất cả các lệnh thực thi phải được đặt giữa các lệnh int main()return. Các định nghĩa hàm (được trình bày sau) theo thông lệ được đặt sau câu lệnh return.

Một chương trình C đơn giản (quadratic.c) nhằm tính các nghiệm thực của một phương trình bậc hai bằng công thức rất cơ bản được liệt kê dưới đây.

/* quadratic.c */
/*
  Program to evaluate real roots of quadratic equation

     2
  a x  + b x + c = 0

  using quadratic formula

                     2  
  x = ( -b +/- sqrt(b - 4 a c) ) / (2 a)
*/

#include <stdio.h>
#include <math.h>

int main() 
{
  double a, b, c, d, x1, x2;

  /* Read input data */
  printf("\na = ");
  scanf("%lf", &a);
  printf("b = ");
  scanf("%lf", &b);
  printf("c = ");
  scanf("%lf", &c);

  /* Perform calculation */
  d = sqrt(b * b - 4. * a * c);
  x1 = (-b + d) / (2. * a);
  x2 = (-b - d) / (2. * a);

  /* Display output */
  printf("\nx1 = %12.3e   x2 = %12.3e\n", x1, x2);

  return 0;
}

Chú ý cách dùng lời chú thích (được đặt giữa các dấu phân cách /**/), trước hết là để giải thích công dụng của chương trình và sau đó chỉ rõ những phần chính trong chương trình. Cũng cần lưu ý rằng cách viết thụt đầu dòng để làm rõ các câu lệnh thực thi. Khi chạy, chương trình sẽ cho kết quả sau:

a = 2
b = 4
c = 1

x1 =   -2.929e-01   x2 =   -1.707e+00
%

Dĩ nhiên là các số 2, 4, và 1 đều được người dùng nhập vào tại dấu nhắc của chương trình.

Điều quan trọng là cần nhận thấy để viết một chương trình máy tính hoàn chỉnh thì việc chỉ sắp xếp đúng thứ tự các lời khai báo và câu lệnh vẫn chưa đủ. Ta còn cần phải lưu ý làm cho chương trình cũng như kết quả đầu ra của nó càng dễ đọc càng tốt, nhờ đó mà công dụng của chương trình sẽ trở nên hiển nhiên đối với người khác. Có thể thực hiện điều đó bằng cách dùng hợp lý các khoảng thụt đầu dòng và dấu trắng, cũng như kèm theo các chú thích, và việc tạo ra kết quả đầu ra có tiêu đề rõ ràng. Hi vọng rằng phương pháp này sẽ được minh họa bởi các chương trình trong khóa học.

Câu lệnh điều khiển

Ngôn ngữ C bao gồm nhiều câu lệnh điều khiển mạnh và linh hoạt. Trong số đó, những câu lệnh có ích nhất sẽ được trình bày sau đây.

Câu lệnh if-else được dùng để thực hiện một phép kiểm tra logic và sau đó chọn một trong hai hành động khả dĩ, tùy theo kết quả của phép kiểm tra là đúng hay sai. Phần else của câu lệnh có thể được bỏ qua. Vì vậy, dạng đơn giản nhất của câu lệnh if-else là:

if (biểu thức) câu lệnh 

Biểu thức phải được đặt trong cặp ngoặc đơn, như ở trên. Với dạng này, câu lệnh sẽ chỉ được thực hiện nếu biểu thức có một giá trị khác không, nghĩa là nếu biểu thức là đúng. Còn nếu biểu thức có giá trị bằng không, nghĩa là nếu biểu thức là sai, thì câu lệnh sẽ bị bỏ qua. Câu lệnh này có thể có dạng đơn giản hoặc phức hợp.

Chương trình quadratic.c ở trên không có không có khả năng tính trong trường hợp các nghiệm là số phức, nghĩa là b2 < 4 a c, hoặc khi a = 0. Một cách lập trình tốt là cần kiểm tra các trường hợp rơi ra ngoài phạm vi đúng đắn của chương trình, và đưa ra một thông báo lỗi khi trường hợp đó xảy ra. Một phiên bản được sửa lại của quadratic.c trong đó dùng lệnh if-else để từ chối dữ liệu đầu vào không hợp lệ được liệt kê dưới đây.

/* quadratic1.c */
/*
  Program to evaluate real roots of quadratic equation

     2
  a x  + b x + c = 0

  using quadratic formula

                     2  
  x = ( -b +/- sqrt(b - 4 a c) ) / (2 a)

  Program rejects cases where roots are complex
  or where a = 0.
*/

#include <stdio.h>
#include <math.h>
#include <stdlib.h>

int main() 
{
  double a, b, c, d, e, x1, x2;

  /* Read input data */
  printf("\na = ");
  scanf("%lf", &a);
  printf("b = ");
  scanf("%lf", &b);
  printf("c = ");
  scanf("%lf", &c);

  /* Test for complex roots */
  e = b * b - 4. * a * c;

  if (e < 0.) 
   {
    printf("\nError: roots are complex\n");
    exit(1);
   }

  /* Test for a = 0. */
  if (a == 0.) 
   {
    printf("\nError: a = 0.\n");
    exit(1);
   }

  /* Perform calculation */
  d = sqrt(e);
  x1 = (-b + d) / (2. * a);
  x2 = (-b - d) / (2. * a);

  /* Display output */
  printf("\nx1 = %12.3e   x2 = %12.3e\n", x1, x2);

  return 0;
}

Lưu ý cách viết thụt đầu dòng để chỉ rõ những câu lệnh chỉ được thực hiện theo điều kiện cụ thể, tức là những câu lệnh bên trong lệnh if-else. Lời gọi đến hàm trong thư viện chuẩn, exit(1) (header file: stdlib.h) khiến chương trình dừng lại với trạng thái lỗi. Việc thực thi chương trình trên trong trường hợp nghiệm phức sẽ cho ra kết quả sau:

a = 4
b = 2
c = 6

Error: roots are complex
%

}

Dạng chung của lệnh if-else, trong đó có mặt cả vế else, là

if (biểu thức)  lệnh 1  else  lệnh 2

Nếu biểu thức có giá trị khác không (nghĩa là nếu biểu thức là đúng) thì lệnh 1 sẽ được thực thi. Còn nếu không, thì lệnh 2 sẽ được thực thi. Chương trình dưới đây là một phiên bản mở rộng của chương trình trước, quadratic.c, lần này giải quyết được trường hợp nghiệm phức.

/* quadratic2.c */
/*
  Program to evaluate all roots of quadratic equation

     2
  a x  + b x + c = 0

  using quadratic formula

                     2  
  x = ( -b +/- sqrt(b - 4 a c) ) / (2 a)

  Program rejects cases where a = 0.
*/

#include <stdio.h>
#include <math.h>
#include <stdlib.h>

int main() 
{
  double a, b, c, d, e, x1, x2;

  /* Read input data */
  printf("\na = ");
  scanf("%lf", &a);
  printf("b = ");
  scanf("%lf", &b);
  printf("c = ");
  scanf("%lf", &c);

  /* Test for a = 0. */
  if (a == 0.) 
   {
    printf("\nError: a = 0.\n");
    exit(1);
   }

  /* Perform calculation */
  e = b * b - 4. * a * c;

  if (e > 0.) // Test for real roots
   {  
    /* Case of real roots */
    d = sqrt(e);
    x1 = (-b + d) / (2. * a);
    x2 = (-b - d) / (2. * a);
    printf("\nx1 = %12.3e   x2 = %12.3e\n", x1, x2);
   } 
  else 
   {
    /* Case of complex roots */
    d = sqrt(-e);
    x1 = -b / (2. * a);
    x2 = d / (2. * a);
    printf("\nx1 = (%12.3e, %12.3e)   x2 = (%12.3e, %12.3e)\n", 
           x1, x2, x1, -x2);
   }
  return 0;
}

Chú ý cách dùng lệnh if-else để giải quyết hai trường hợp nghiệm thực và nghiệm phức. Cũng lưu ý rằng trình biên dịch C bỏ qua tất cả kí tự đứng sau dấu //.1 Do vậy, dấu này có thể được dùng để chú thích từng dòng lệnh trong chương trình. Kết quả đầu ra của chương trình trên với trường hợp nghiệm phức có dạng:

a = 9
b = 2
c = 2

x1 = (  -1.111e-01,    4.581e-01)   x2 = (  -1.111e-01,   -4.581e-01)
%

Câu lệnh while được dùng để thực hiện thao tác có tính lặp vòng quanh, trong đó một nhóm các câu lệnh được thực thi lặp lại nhiều lần đến khi một điều kiện nào đó được thỏa mãn. Dạng chung của câu lệnh while

while  (biểu thức)  câu lệnh

Câu lệnh được thực hiện lặp đi lặp lại, miễn là biểu thức vẫn còn khác không (tức là biểu thức còn đúng). Câu lệnh có thể ở dạng đơn giản hoặc phức hợp. Dĩ nhiên, câu lệnh phải bao gồm một đặc điểm nào đó sao cho cuối cùng nó sẽ thay đổi giá trị của biểu thức, một cơ chế thoát khỏi vòng lặp.

Chương trình được liệt kê dưới đây (iteration.c) dùng một câu lệnh while để giải một phương trình đại số bằng phương pháp lặp, theo cách được chỉ ra trong phần chú thích phía đầu.

/* iteration.c */
/* 
   Program to solve algebraic equation

    5      2 
   x  + a x  - b = 0

   by iteration. Easily shown that equation must have at least
   one real root. Coefficients a and b are supplied by user.

   Iteration scheme is as follows:

                    2  0.2
   x    =  ( b - a x  )
    n+1             n

   where x_n is nth iteration. User must supply initial guess for x.
   Iteration continues until relative change in x per iteration is 
   less than eps (user supplied) or until number of iterations exceeds 
   NITER. Program aborts if (b - a x*x) becomes negative. 
*/

#include <stdio.h>
#include <math.h>
#include <stdlib.h>

/* Set max. allowable no. of iterations */
#define NITER 30   

int main() 
{
  double a, b, eps, x, x0, dx = 1., d;
  int count = 0;

  /* Read input data */
  printf("\na = ");
  scanf("%lf", &a);
  printf("b = ");
  scanf("%lf", &b);
  printf("eps = ");
  scanf("%lf", &eps);

  /* Read initial guess for x */
  printf("\nInitial guess for x = ");  
  scanf("%lf", &x);
  x0 = x;

  while (dx > eps)  // Start iteration loop: test for convergence
   {
    /* Check for too many iterations */
    ++count;
    if (count > NITER) 
     {
      printf("\nError: no convergence\n");
      exit(1);
     }

    /* Reject complex roots */
    d = b - a * x * x;
    if (d < 0.)    
     {
      printf("Error: complex roots - try another initial guess\n");
      exit(1);
     }

    /* Perform iteration */
    x = pow(d, 0.2);
    dx = fabs( (x - x0) / x );
    x0 = x;

    /* Output data on iteration */
    printf("Iter = %3d   x = %8.4f   dx = %12.3e\n", count, x, dx);
   }
  return 0;
}

Một kết quả đầu ra của chương trình trên có thể như sau:

a = 3
b = 10
eps = 1.e-6

Initial guess for x = 1
Iter =   1   x =   1.4758   dx =    3.224e-01
Iter =   2   x =   1.2823   dx =    1.509e-01
Iter =   3   x =   1.3834   dx =    7.314e-02
Iter =   4   x =   1.3361   dx =    3.541e-02
Iter =   5   x =   1.3595   dx =    1.720e-02
Iter =   6   x =   1.3483   dx =    8.350e-03
Iter =   7   x =   1.3537   dx =    4.056e-03
Iter =   8   x =   1.3511   dx =    1.969e-03
Iter =   9   x =   1.3524   dx =    9.564e-04
Iter =  10   x =   1.3518   dx =    4.644e-04
Iter =  11   x =   1.3521   dx =    2.255e-04
Iter =  12   x =   1.3519   dx =    1.095e-04
Iter =  13   x =   1.3520   dx =    5.318e-05
Iter =  14   x =   1.3519   dx =    2.583e-05
Iter =  15   x =   1.3520   dx =    1.254e-05
Iter =  16   x =   1.3520   dx =    6.091e-06
Iter =  17   x =   1.3520   dx =    2.958e-06
Iter =  18   x =   1.3520   dx =    1.436e-06
Iter =  19   x =   1.3520   dx =    6.975e-07      
%    

Khi một vòng lặp được thiết lập bằng câu lệnh while, phép kiểm tra cho khả năng tiếp tục lặp được thực hiện ở đầu đoạn lệnh lặp. Tuy vậy, đôi khi ta muốn có một vòng lặp mà việc kiểm tra khả năng tiếp tục lặp được thực hiện ở cuối đoạn lệnh lặp. Có thể làm điều này bằng cách dùng câu lệnh do-while. Dạng chung của lệnh do-while

do  lệnh  while  (biểu thức);

Câu lệnh sẽ được thực hiện lặp đi lặp lại, miễn là biểu thức còn đúng. Song cũng cần nhớ rằng câu lệnh sẽ luôn được thực hiện, ít nhất là một lần, vì việc kiểm tra cho phép lặp tiếp chỉ được tiến hành bắt đầu từ cuối vòng lặp thứ nhất. Câu lệnh có thể ở dạng đơn giản hoặc phức hợp, và dĩ nhiên cần có đặc điểm nào đó để sau này có thể thay đổi giá trị của biểu thức.

Dưới đây là chương trình được cải tiến chút ít so với phiên bản chương trình trước (iteration.c) trong đó dùng một vòng lặp do-while để kiểm tra sự hội tụ ở cuối (thay vì ở đầu) mỗi vòng lặp.

/* iteration1.c */
/* 
   Program to solve algebraic equation

    5      2 
   x  + a x  - b = 0

   by iteration. Easily shown that equation must have at least
   one real root. Coefficients a and b are supplied by user.

   Iteration scheme is as follows:

                    2  0.2
   x    =  ( b - a x  )
    n+1             n

   where x_n is nth iteration. User must supply initial guess for x.
   Iteration continues until relative change in x per iteration is 
   less than eps (user supplied) or until number of iterations exceeds 
   NITER. Program aborts if (b - a x*x) becomes negative. 
*/

#include <stdio.h>
#include <math.h>
#include <stdlib.h>

/* Set max. allowable no. of iterations */
#define NITER 30  

int main() 
{
  double a, b, eps, x, x0, dx, d;
  int count = 0;

  /* Read input data */
  printf("\na = ");
  scanf("%lf", &a);
  printf("b = ");
  scanf("%lf", &b);
  printf("eps = ");
  scanf("%lf", &eps);

  /* Read initial guess for x */
  printf("\nInitial guess for x = ");  
  scanf("%lf", &x);
  x0 = x;

  do   // Start iteration loop
   {
    /* Check for too many iterations */
    ++count;
    if (count > NITER) 
     {
      printf("\nError: no convergence\n");
      exit(1);
     }

    /* Reject complex roots */
    d = b - a * x * x;
    if (d < 0.)     
     {
      printf("Error: complex roots - try another initial guess\n");
      exit(1);
     }

    /* Perform iteration */
    x = pow(d, 0.2);
    dx = fabs( (x - x0) / x );
    x0 = x;

    /* Output data on iteration */
    printf("Iter = %3d   x = %8.4f   dx = %12.3e\n", count, x, dx);

   } while (dx > eps);  // Test for convergence

  return 0;
}

Kết quả đầu ra của chương trình trên cũng giống hệt như kết quả của chương trình iteration.c.

Các lệnh whiledo-while đều đặc biệt phù hợp với các tình huống lặp trong đó số lần lặp không được biết trước. Trái lại, trong trường hợp số lần lặp đã được biết trước, tốt nhất là ta dùng câu lệnh for. Dạng chung của câu lệnh for

for  (biểu thức 1;  biểu thức 2;  biểu thức 3)  câu lệnh

trong đó biểu thức 1 được dùng để khởi tạo một tham số nào đó (được gọi là chỉ số) dùng để điều khiển hoạt động lặp, biểu thức 2 đại diện cho một điều kiện cần được thỏa mãn để vòng lặp có thể tiếp tục được thực hiện, và biểu thức 3 được dùng để thay đổi giá trị mà ban đầu được gán bởi biểu thức 1. Khi một câu lệnh for được thực hiện, biểu thức 2 sẽ được lượng giá và kiểm tra ở đầu mỗi lần lặp, còn biểu thức 3 được lượng giá ở cuối mỗi lần lặp.

Chương trình dưới đây dùng một câu lệnh for để tính giai thừa của một số nguyên không âm.

/* factorial.c */
/* 
   Program to evaluate factorial of non-negative
   integer n supplied by user.
*/

#include <stdio.h>
#include <stdlib.h>

int main()
{
  int n, count;
  double fact = 1.;

  /* Read in value of n */
  printf("\nn = ");
  scanf("%d", &n);

  /* Reject negative value of n */
  if (n < 0) 
   {
    printf("\nError: factorial of negative integer not defined\n");
    exit(1);
   }

  /* Calculate factorial */
  for (count = n; count > 0; --count) fact *= (double) count;

  /* Output result */
  printf("\nn = %5d    Factorial(n) = %12.3e\n", n, fact);

  return 0;
}

Một kết quả đầu ra của chương trình trên như sau:

n = 6

n =     6    Factorial(n) =    7.200e+02 
%

Bản thân các lệnh nằm trong if-else, while, do-while, hoặc for cũng có thể ở dạng phức hợp, để hình thành các câu lệnh if-else lồng ghép, những vòng lặp được thực hiện theo điều kiện, những vòng lặp lồng ghép, v.v. Khi làm việc với các lệnh điều khiển phức hợp, nhất thiết phải tuân theo những quy tắc cú pháp đã đề cập đến, để tránh nhầm lẫn.

Hàm

Ta đã thấy rằng C hỗ trợ việc dùng những hàm thư viện đã định nghĩa sẵn, vốn được thiết kế để thực hiện rất nhiều thao tác thường gặp. Tuy nhiên C cũng cho phép người dùng tạo riêng những hàm của họ. Với những hàm do người dùng tự định nghĩa, ta có thể chia nhỏ một chương trình lớn thành những bộ phận nhỏ nhưng đủ tính năng. Nói cách khác, một chương trình C có thể được mô-đun hóa bằng cách sử dụng hợp lý những hàm do người dùng định nghĩa. Nhìn chung, các chương trình có mô-đun thường dễ viết và gỡ lỗi hơn nhiều so với chương trình nguyên khối. Hơn nữa, việc mô-đun hóa hợp lý sẽ cho phép người khác nắm được cấu trúc logic của chương trình dễ dàng hơn.

Hàm là một đoạn chương trình có đủ tính năng để thực hiện một nhiệm vụ cụ thể nào đó. Mỗi chương trình C bao gồm một hoặc nhiều hàm. Một trong những hàm này phải được đặt tên là main. Việc thực thi chương trình luôn bắt đầu với việc thực hiện những lệnh có trong main. Lưu ý rằng nếu một chương trình chứa nhiều hàm thì những định nghĩa hàm này có thể đặt với thứ tự bất kì. Cùng một hàm có thể được truy cập đên từ một vài chỗ trong chương trình. Một khi hàm được thực hiện hoạt động như định trước của nó thì quyền điều khiển sẽ được trả về điểm mà từ đó đã truy cập hàm. Một cách tổng quát, hàm sở hữu những thông tin truyền đến nó từ đoạn chương trình gọi, và trả về một giá trị. Tuy vậy có những hàm nhận vào thông tin nhưng không trả về thứ gì.

Một lờii định nghĩa hàm có hai thành phần chính: dòng đầu (gồm cả khai báo đối số), và phần được gọi là thân của hàm.

Dòng đầu của hàm có dạng sau

kiểu_dữ_liệu  tên(kiểu 1  arg 1,  kiểu 2  arg 2,  ...,  kiểu n  arg n)

trong đó kiểu_dữ_liệu biểu thị cho kiểu dữ liệu của thứ mà hàm trả lại, name biểu thị tên hàm, và kiểu 1, kiểu 2, …, kiểu n biểu thị các kiểu dữ liệu cho những đối số arg 1, arg 2, …, arg n. Các kiểu dữ liệu cho phép dành cho hàm gồm có:

int       cho một hàm trả về giá trị nguyên
double    cho một hàm trả về giá trị số có phần thập phân
void      cho một hàm không trả về giá trị nào

Các kiểu dữ liệu dùng được cho các đối số của hàm gồm có intdouble. Lưu ý rằng các thẻ nhận diện dùng để tham chiếu đến các đối số của hàm đều có tính cục bộ, tức là ngoài phạm vi của hàm thì chúng không tồn tại. Do vậy, tên các đôi số trong lời định nghĩa của hàm không nhất thiết phải giống như ở trong các đoạn chương trình mà hàm được gọi đến. Tuy nhiên, các kiểu tương ứng của đối số phải luôn hợp nhau.

Phần thân của hàm là một câu lệnh phức hợp nhằm chỉ rõ công việc do hàm này đảm nhiệm. Cũng như một câu lệnh phức hợp, phần thân này có thể chứa các câu lệnh biểu thức, câu lệnh điều khiển, hay câu lệnh phức hợp khác, v.v. Trong phần thân, có thể truy cập đén các hàm khác. Thật ra, còn có thể truy cập bản thân hàm đó—quá trình này được gọi là đệ quy. Tuy nhiên phần thân phải bao gồm một hoặc nhiều câu lệnh return để có thể trả một giá trị về đoạn chương trình từ đó gọi hàm.

Một lệnh return khiến cho logic chương trình trở về điểm trong chương trình từ đó hàm được truy cập đến. Dạng chung của câu lệnh return là:

return  expression;

Câu lệnh này khiến cho giá trị của expression được trả về phần chương trình gọi nó. Dĩ nhiên, kiểu dữ liệu của expression phải khớp với kiểu dữ liệu đã được khai báo của hàm. Với một hàm void, vốn không trả lại giá trị, câu lệnh return đơn giản chỉ là:

return;

Chỉ có tối đa là một biểu thức được kèm trong câu lệnh return. Do đó, một hàm có thể trả tối đa là một giá trị về chỗ gọi nó trong chương trình. Tuy nhiên, một lời định nghĩa hàm có thể chứa nhiều câu lệnh return, mỗi lệnh chứa một biểu thức khác nhau, và được thực thi theo điều kiện, tùy theo logic chương trình.

Lưu ý rằng, theo thông lệ, hàm main có kiểu int và trả giá trị nguyên bằng 0 về hệ điều hành, để thông báo rằng chương trình đã kết thúc mà không có lỗi. Dạng đơn giản nhất của hàm main không có đối số. Lời gọi hàm thư viện exit(1), trong chương trình ví dụ trước, đã ngừng việc thực thi của chương trình, trả giá trị nguyên bằng 1 về hệ điều hành, để (theo thông lệ) báo rằng chương trình đã kết thúc với một trạng thái lỗi.

Đoạn chương trình dưới đây cho thấy cách chuyển chương trình trước, factorial.c, thành một hàm factorial(n) để trả lại giai thừa (dưới dạng số có phần thập phân) của một số nguyên không âm n:

double factorial(int n) 
{
  /* 
     Function to evaluate factorial (in floating-point form)
     of non-negative integer n.
  */

  int count;
  double fact = 1.;

  /* Abort if n is negative integer */
  if (n < 0) 
   {
    printf("\nError: factorial of negative integer not defined\n");
    exit(1);
   }

  /* Calculate factorial */
  for (count = n; count > 0; --count) fact *= (double) count;

  /* Return value of factorial */
  return fact;      
}

Hàm có thể được truy cập, hay được gọi, bằng cách chỉ định tên của nó, tiếp theo là một danh sách các đối số đặt trong cặp ngoặc đơn và ngăn cách bởi các dấu phẩy. Nếu lời gọi hàm không yêu cầu đối số nào thì vẫn cần có một cặp ngoặc đơn đi sau tên hàm. Lời gọi hàm có thể bộ phận của một biểu thức đơn giản, như một lệnh gán, hoặc có thể là một hạng tử trong biểu thức phức tạp hơn. Đối số xuất hiện trong lời gọi hàm có thể được biểu thị dưới dạng hằng số, biến đơn lẻ, hoặc biểu thức phức tạp hơn. Tuy nhiên, cả số lượng và kiểu của các đối số phải khớp như trong lời định nghĩa hàm.

Chương trình (printfact.c) dưới đây dùng hàm factorial() nêu trên để in ra giai thừa của tất cả các số tự nhiên từ 0 đến 20:

/* printfact.c */
/*
  Program to print factorials of all integers
  between 0 and 20
*/

#include <stdio.h>
#include <stdlib.h>

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

double factorial(int n) 
{
  /* 
     Function to evaluate factorial (in floating-point form)
     of non-negative integer n.
  */

  int count;
  double fact = 1.;

  /* Abort if n is negative integer */
  if (n < 0) 
   {
    printf("\nError: factorial of negative integer not defined\n");
    exit(1);
   }

  /* Calculate factorial */
  for (count = n; count > 0; --count) fact *= (double) count;

  /* Return value of factorial */
  return fact;      
}

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

int main()
{
  int j;

  /* Print factorials of all integers between 0 and 20 */
  for (j = 0; j <= 20; ++j) 
    printf("j = %3d    factorial(j) = %12.3e\n", j, factorial(j));

  return 0;
}

Lưu ý rằng lời gọi đến factorial() nằm trong một biểu thức phức hợp, nghĩa là một lời gọi hàm printf(). Cũng lưu ý rằng đối số của factorial() có tên gọi mới (nhưng vẫn cùng kiểu dữ liệu) trong hai phần khác nhau của chương trình: trong hàm main() và hàm factorial(). Kết quả đầu ra của chương trình trên như sau:

j =   0    factorial(j) =    1.000e+00
j =   1    factorial(j) =    1.000e+00
j =   2    factorial(j) =    2.000e+00
j =   3    factorial(j) =    6.000e+00
j =   4    factorial(j) =    2.400e+01
j =   5    factorial(j) =    1.200e+02
j =   6    factorial(j) =    7.200e+02
j =   7    factorial(j) =    5.040e+03
j =   8    factorial(j) =    4.032e+04
j =   9    factorial(j) =    3.629e+05
j =  10    factorial(j) =    3.629e+06
j =  11    factorial(j) =    3.992e+07
j =  12    factorial(j) =    4.790e+08
j =  13    factorial(j) =    6.227e+09
j =  14    factorial(j) =    8.718e+10
j =  15    factorial(j) =    1.308e+12
j =  16    factorial(j) =    2.092e+13
j =  17    factorial(j) =    3.557e+14
j =  18    factorial(j) =    6.402e+15
j =  19    factorial(j) =    1.216e+17
j =  20    factorial(j) =    2.433e+18      
%

Trong chương trình C, lý tưởng nhất là định nghĩa của hàm luôn đi trước lời gọi nó. Yêu cầu này có thể thỏa mãn được bằng cách sắp xếp một cách phù hợp các hàm trong chương trình; song gần như phải đặt hàm main() ở vị trí cuối chương trình. Vì vậy, nếu đổi vị trí hai hàm factorial()main() trong chương trình trên thì sẽ xuất hiện thông báo lỗi khi biên dịch, vì khi đó hàm factorial() được cố gắng gọi đến trước khi nó được định nghĩa. Không may là, phần đông lập trình viên dùng C muốn đặt hàm main() ngay tại điểm đầu của chương trình để làm sáng tỏ cấu trúc logic. Dù nói gì đi nữa, main() luôn là phần đầu tiên của chương trình được thực thi. Trong trường hợp đó, các lời gọi hàm bên trong main() đều đi trước các lời định nghĩa hàm tương ứng. Song thật may là ta có thể tránh được lỗi khi biên dịch bằng cách dùng một cấu trúc gọi là mẫu hàm.

Mẫu hàm theo thông lệ được đặt ở đầu chương trình, tức là trước hàm main() nhằm mục đích báo cho trình biên dịch biết tên, kiểu dữ liệu cũng như số lượng và kiểu dữ liệu của các đối số trong tất cả những hàm do người dùng định nghĩa trong chương trình. Dạng chung của một mẫu hàm là

data-type  name(type 1,  type 2,  ...,  type n);

trong đó data-type biểu thị cho kiểu dữ liệu của đối tượng trả lại bởi hàm được tham chiếu, name là tên hàm, còn type 1, type 2,..., type n là các kiểu dữ liệu của đối số trong hàm. Lưu ý rằng không cần thiết phải chỉ rõ tên của các đối số trong một mẫu hàm. Cần nói thêm là, các mẫu hàm cho các hàm trong thư viện có sẵn được chứa trong các header file tương ứng mà ta luôn phải ghi kèm nó ở mỗi chương trình mà dùng đến những hàm đó.

Chương trình ở dưới đây là một phiên bản được sửa từ printfact.c trong đó hàm main() là hàm đầu tiên được định nghĩa:

/* printfact1.c */
/*
  Program to print factorials of all integers
  between 0 and 20
*/

#include <stdio.h>
#include <stdlib.h>

/* Prototype for function factorial() */
double factorial(int);    

int main()
{
  int j;

  /* Print factorials of all integers between 0 and 20 */
  for (j = 0; j <= 20; ++j) 
    printf("j = %3d    factorial(j) = %12.3e\n", j, factorial(j));

  return 0;
}

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

double factorial(int n) 
{
  /* 
     Function to evaluate factorial (in floating-point form)
     of non-negative integer n.
  */

  int count;
  double fact = 1.;

  /* Abort if n is negative integer */
  if (n < 0) 
   {
    printf("\nError: factorial of negative integer not defined\n");
    exit(1);
   }

  /* Calculate factorial */
  for (count = n; count > 0; --count) fact *= (double) count;

  /* Return value of factorial */
  return fact;      
}

Lưu ý sự có mặt của mẫu hàm cho factorial() trước khi main() được định nghĩa. Điều này là cần thiết vì chương trình sẽ gọi factorial() trước khi hàm này được định nghĩa. Kết quả đầu ra của chương trình trên cũng giống như kết quả của printfact.c.

Một cách lập trình thường được đánh giá hay là việc đưa ra mẫu hàm của tất cả hàm do người dùng định nghĩa được truy cập đến trong chương trình, bất kể nó có hoặc không bị trình biên dịch yêu cầu đến.2 Lí do của điều này khá đơn giản. Nếu ta cung cấp mẫu cho một hàm cho trước thì trình biên dịch có thể so sánh cẩn thận từng lời gọi hàm trong chương trình với mẫu hàm này nhằm xác định xem chúng ta có gọi hàm một cách đúng đắn không. Nếu không có mẫu hàm, một lời gọi sai đến hàm, chẳng hạn như cung cấp thiếu hoặc thừa đối số hay sai kiểu dữ liệu của đối số, sẽ gây ra các lỗi thực thi rất khó tìm ra.

Khi một giá trị được truyền vào hàm như một đối số thì giá trị của đối số đó chỉ cần được sao chép đến hàm. Vì vậy, giá trị của đối số có thể được thay đổi sau này trong hàm mà không làm ảnh hưởng đến giá trị của nó trong đoạn chương trình gọi. Phương pháp này để chuyển giá trị của đối số đến hàm được gọi là chuyển theo giá trị.

Việc chuyển đối số theo giá trị có cả ưu điểm lẫn nhược điểm. Ưu điểm thứ nhất là nó cho phép một đối số một giá trị được viết dưới dạng một biểu thức, thay vì phải hạn chế như một biến đơn lẻ. Hơn nữa, trong những tình huống mà đối số là một biến, giá trị của biến này được bảo vệ khỏi những thay đổi diễn ra bên trong hàm. Nhược điểm lớn nhất là thông tin không thể được chuyển ngược về cho đoạn chương trình gọi qua các đối số. Nói cách khác, chuyển theo giá trị là một phương pháp chuyển thông tin một chiều. Chương trình ở dưới đây, vốn là một dạng sửa đổi khác của printfact.c, minh họa cho điều này:

/* printfact2.c */
/*
  Program to print factorials of all integers
  between 0 and 20
*/

#include <stdio.h>
#include <stdlib.h>

/* Prototype for function factorial() */
double factorial(int);    

int main() 
{
  int j;

  /* Print factorials of all integers between 0 and 20 */
  for (j = 0; j <= 20; ++j) 
    printf("j = %3d    factorial(j) = %12.3e\n", j, factorial(j));

  return 0;
}

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

double factorial(int n) 
{
  /* 
     Function to evaluate factorial (in floating-point form)
     of non-negative integer n.
  */

  double fact = 1.;

  /* Abort if n is negative integer */
  if (n < 0) 
   {
    printf("\nError: factorial of negative integer not defined\n");
    exit(1);
   }

  /* Calculate factorial */
  for (; n > 0; --n) fact *= (double) n;

  /* Return value of factorial */
  return fact;      
}

Lưu ý rằng hàm factorial() đã được sửa đổi theo đó giá trị nguyên của đối số n cũng được dùng làm chỉ số bên trong một câu lệnh for. Do vậy, giá trị của đối số này được thay đổi bên trong hàm. Tuy nhiên kết quả đầu ra của chương trình trên vẫn giống hệt như của printfact.c, bởi những sự thay đổi n không bị chuyển ngược lại phần chính của chương trình. Lưu ý rằng tình cờ, việc lược bỏ biểu thức khởi tạo trong câu lệnh for của factorial().

Con trỏ

Một trong những đặc điểm chính của chương trình khoa học là có rất nhiều thông tin (số liệu) được chuyển qua lại giữa các hàm cấu thành chương trình. Nói chung, cách làm tiện nhất là truyền những thông tin này qua danh sách đối số, thay vì tên, của các hàm này. Song dù truyền bằng cách nào đi nữa cũng chỉ có một con số chuyển được qua tên hàm, trong khi chương trình khoa học thông thường đòi hỏi rất nhiều số được chuyển trong mỗi lần gọi hàm. Vì vậy, các hàm dùng trong chương trình khoa học thường không trả lại giá trị nào qua tên hàm, nghĩa là chúng thường có kiểu dữ liệu void nhưng sở hữu một dãy nhiều đối số. Dễ thấy có một vấn đề trong cách làm này. Đó là một hàm void mà truyền tất cả đôi số của nó theo giá trị thì sẽ không thể trả lại thông tin về đoạn chương trình mà nó được gọi. Thật may là có cách giải quyết vấn đề này: ta có thể truyền các đôi số của hàm theo tham chiếu, thay vì theo giá trị, bằng cách dùng con trỏ. Cách này cho phép trao đổi thông tin hai chiều qua đối số trong khi gọi hàm. Sau đây ta sẽ tìm hiểu về con trỏ.

Giả sử rằng v là một biến trong chương trình C để biểu thị một đơn vị dữ liệu nào đó. Dĩ nhiên, chương trình lưu dữ liệu này tại một vị trí cụ thể trong bộ nhớ máy tính. Dữ liệu này có thể được truy cập nếu ta biết vị trí, hay địa chỉ của nó trong bộ nhớ máy tính. Địa chỉ vị trí của v trong bộ nhớ được xác định bằng biểu thức &v, với & là một toán tử đơn có tên toán tử địa chỉ.

Giả sử rằng ta gán địa chỉ của v cho một biến khác, pv

pv = &v

Biến mới này được gọi là con trỏ đến v, vì nó chỉ đến vị trí mà v được lưu trong bộ nhớ. Tuy nhiên cần nhớ rằng pv biểu thị địa chỉ của v, chứ không phải giá trị của nó.

Dữ liệu được biểu diễn bởi v, tức là dữ liệu được lưu ở vị trí của v trong bộ nhớ, có thể được truy cập bằng biểu thức *pv, trong đó * là một toán tử đơn, được gọi là toán tử gián tiếp, vốn chỉ được dùng cho các biến con trỏ. Vì vậy, cả *pvv đều biểu diễn cùng một dữ liệu. Hơn nữa, nếu ta viết pv = &vu = *pv thì cả uv đều biểu diễn cùng một giá trị.

Chương trình đơn giản dưới đây minh họa cho một số điều ở trên:

/* pointer.c */
/* 
   Simple illustration of the action of pointers
*/

#include <stdio.h>

main() 
{
  int u = 5;     
  int v;
  int *pu;      // Khai bao con tro den mot bien nguyen
  int *pv;      // Khai bao con tro den mot bien nguyen

  pu = &u;      // Gan dia chi cua u cho pu
  v = *pu;      // Gan gia tri cua u cho v
  pv = &v;      // Gan dia chi cua v cho pv

  printf("\nu = %d  &u = %X  pu = %X  *pu = %d", u, &u, pu, *pu);
  printf("\nv = %d  &v = %X  pv = %X  *pv = %d\n", v, &v, pv, *pv);

  return 0;
}

Lưu ý rằng pu là con trỏ đến u, còn pv là con trỏ đến v. Cần nói thêm là kí tự chuyển đổi X, xuất hiện ở chuỗi điều khiển của lời gọi hàm printf() bên trên, cho biết rằng dữ liệu tương ứng cần được xuất ra dưới dạng số thập lục phân—đây là quy ước biểu diễn địa chỉ trong bộ nhớ máy tính. Chạy chương trình trên, ta được kết quả đầu ra như sau:

u = 5  &u = BFFFFA24  pu = BFFFFA24  *pu = 5
v = 5  &v = BFFFFA20  pv = BFFFFA20  *pv = 5         
%

Ở dòng thứ nhất, ta thấy rằng u biểu diễn cho giá trị 5, như đã ghi ở câu lại khai báo. Địa chỉ của u được trình biên dịch tự động xác định là BFFFFA24 (thập lục phân). Con trỏ pu được gán bằng giá trị này. Sau cùng, giá trị mà pu trỏ đến bằng 5, như mong đợi. Tương tự, dòng thứ hai cho thấy v cũng biểu diễn cho giá trị 5. Điều này đúng như mong đợi, vì ta đã gán giá trị của *pu cho v. Địa chỉ của vBFFFFA20. Dĩ nhiên, uv có địa chỉ khác nhau.

Các toán tử đơn &* có cùng độ ưu tiên như những toán tử đơn khác như ++--). Toán tử địa chỉ (&) chỉ có tác dụng với toán hạng sở hữu một địa chỉ duy nhất, như các biến bình thường. Vì vậy, toán tử đại chỉ sẽ không có tác dụng với các biểu thức số học, như 2 * (u + v). Toán tử gián tiếp (*) chỉ có tác dụng đối với các hạng tử là con trỏ.

Biến con trỏ, cũng như các biến khác, phải được khai báo trước khi xuất hiện trong câu lệnh thực hiện. Một lời khai báo biến con trỏ có dạng chung sau

data-type  *ptvar;

trong đó ptvar là tên của biến con trỏ, và data-type là kiểu của dữ liệu mà con trỏ chỉ đến. Lưu ý rằng phải luôn có một dấu sao đi trước tên biến con tro trong lời khai báo con trỏ.

Trở về Mục [nhập-xuất], bây giờ ta có thể thấy vai trò của dấu sao bí ẩn xuất hiện trong lời khai báo của stream nhập/xuất, chẳng hạn

FILE  *stream;

bởi vì stream là một biến con trỏ (chỉ đến một đối tượng có kiểu đặc biệt FILE). Thực ra, stream chỉ đến điểm đầu của stream nhập/xuất tương ứng trong bộ nhớ.

Con trỏ thường được truyền đến hàm dưới dạng đối số. Cách này cho phép các dữ liệu trong phần gọi hàm của chương trình có thể được hàm truy cập đến, làm thay đổi bên trong hàm, và truyền ngược giá trị sau khi thay đổi trở lại phần của chương trình gọi. Cách dùng con trỏ này được gọi là truyền tham số theo tham chiếu, thay vì theo giá trị.

Khi một tham số được truyền theo giá trị, dữ liệu tương ứng chỉ được sao chép cho hàm. Vì vậy, bất kì sự thay đổi nào đến dữ liệu bên trong hàm đều không được truyền ngược trở lại đoạn chương trình gọi. Tuy nhiên, khi đối số được truyền theo tham chiếu, địa chỉ của dữ liệu tương ứng được truyền vào hàm. Nội dung của địa chỉ này có thể được truy cập tùy ý bởi cả hàm lẫn đoạn chương trình gọi. Hơn nữa, bất kì thay đổi nào đối với dữ liệu lưu tại địa chỉ này sẽ được nhận thấy bởi cả hàm lẫn chương trình gọi. Do đó, cách dùng của con trỏ làm đối số cho phép trao đổi thông tin hai chiều giữa hàm và đoạn chương trình gọi.

Chương trình dưới đây, vốn là một phiên bản sửa đổi khác của printfact.c, dùng một con trỏ để truyền ngược thông tin từ hàm tới đoạn chương trình gọi:

/* printfact3.c */
/*
  Program to print factorials of all integers
  between 0 and 20
*/

#include <stdio.h>
#include <stdlib.h>

/* Prototype for function factorial() */
void factorial(int, double *);    

int main() 
{
  int j;
  double fact;

  /* Print factorials of all integers between 0 and 20 */
  for (j = 0; j <= 20; ++j) 
   {
    factorial(j, &fact);
    printf("j = %3d    factorial(j) = %12.3e\n", j, fact);
   }
  return 0;
}

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

void factorial(int n, double *fact) 
{
  /* 
     Function to evaluate factorial *fact (in floating-point form)
     of non-negative integer n.
  */

  *fact = 1.;

  /* Abort if n is negative integer */
  if (n < 0) 
   {
    printf("\nError: factorial of negative integer not defined\n");
    exit(1);
   }

  /* Calculate factorial */
  for (; n > 0; --n) *fact *= (double) n;

  return;      
}

Kết quả đầu ra của chương trình này một lần nữa lại giống với printfact.c. Lưu ý rằng hàm factorial() được sửa đổi sao cho không có dữ liệu gắn với tên của nó, nghĩa là hàm có kiểu void. Tuy nhiên, danh sách đối số của hàm này đã được kéo dài, và bây giờ có hai đối số. Đối số thứ nhất vẫn như cũ, là giá trị của số nguyên dương n mà ta cần dùng hàm để tính giai thừa. Đối số thứ hai, fact, là một con trỏ để truyền giai thừa của n (dưới dạng số có phần thập phân) trở về phần chính của chương trình. Cần nói thêm là, trình biên dịch biết rằng fact là một con trỏ bởi vì tên của nó đứng sau một dấu sao trong lời khai báo đối số của factorial(). Dĩ nhiên, trong phần thân hàm, tham chiếu được lập cho *fact (giá trị của dữ liệu được lưu tại vị trí bộ nhớ mà fact chỉ đến) thay vì bản thân fact (địa chỉ của vị trí bộ nhớ mà fact chỉ đến). Lưu ỷ rằng hàm void, vốn không trả lại giá trị nào, chỉ có thể được gọi bằng một câu lệnh bao gồm ten của hàm theo sau là một danh sách các đối số của nó (trong cặp ngoặc đơn và ngăn cách bởi các dấu phẩy). Vì vậy, hàm factorial() được gọi trong phần chính của chương trình bằng câu lệnh

factorial(j, &fact);

Câu lệnh này truyền giá trị nguyên j đến factorial(), vốn đến lượt mình, lại truyền về giá trị của giai thừa của j qua đối số thứ hai. Lưu ý rằng vì đối số thứ hai được truyền theo tham chiếu thay vì theo giá trị, nó được viết là &fact (địa chỉ của vị trí bộ nhớ nơi mà giá trị số có phần thập phân fact được lưu trữ) thay vì fact (giá trị của biến số có phần thập phân fact). Sau cùng, hãy chú ý rằng mẫu hàm cho factorial() có dạng

void factorial(int, double *);

Ở đây dấu sao đi sau double chỉ ra rằng đối số thứ hai là một con trỏ chỉ đến dữ liệu kiểu số có phần thập phân.

Bây giờ ta có thể hiểu được dấu “và” bí ẩn cần đặt trước tên biến trong lời gọi scanf() như

scanf("%d %lf %lf", &k, &x, &y);

Thật ra, scanf() là một hàm trả lại dữ liệu về đoạn chương trình gọi nó qua các đối số (trừ đối số đầu tiên, vốn là chuỗi điều khiển). Vì vậy, các đối số này phải được truyền đến scanf() theo tham chiếu, thay vì theo giá trị, nếu không thì ta sẽ không thể truyền thông tin ngược về đoạn chương trình gọi. Hệ quả là ta phải truyền địa chỉ của các biến (như &k) đến scanf(), thay vì giá trị của các biến (như k). Lưu ý rằng vì hàm printf() không trả thông tin nào về đoạn chương trình gọi thông qua các đối số của nó, ta không cần truyền những đối số này theo tham chiếu—chỉ cần truyền theo giá trị là được. Điều này giải thích lid do tại sao không có dấu “và” trong danh sách đối số của hàm printf().

Một con trỏ tới hàm có thể được truyền như đối số đến một hàm khác. Cách này cho phép một hàm được truyền đến hàm khác, như thể hàm thứ nhất là một biến. Điều này rất có ích trong lập trình khoa học. Giả sử rằng ta có một đoạn chương trình để tính tích phân số trị của hàm một biến. Tốt nhất là ta có thể dùng đoạn chương trình đó để tính cho nhiều hàm khác nhau. Ta có thể làm điều này bằng cách truyền (đến đoạn chương trình) tên của hàm cần tính tích phân như một đối số. Vì vậy, ta có thể dùng cùng một đoạn chương trình để tính tích phân của một đa thức, một hàm lượng giác, một hàm loga chẳng hạn.

Hãy gọi hàm mà tên nó được truyền làm tham số là hàm khách. Tương tự, hàm mà tên được chuyển đến đó là hàm chủ. Một con trỏ đến hàm khách được chỉ định bằng trong lời định nghĩa của hàm chủ bằng một đoạn kiểu như sau

data-type  (*function-name)(type 1,  type 2, ...)

ở trong phần khai báo đối số của hàm chủ. 3 Ở đây, data-type là kiểu dữ liệu của hàm khách, function-name là tên của hàm khách trong lời định nghĩa của hàm chủ, còn type 1, type 2,... là các kiểu dữ liệu của đối số trong hàm khách. Con trỏ đến hàm khách cũng cần được chỉ định bởi đoạn kiểu như sau

data-type  (*)(type 1,  type 2,  ...)

trong phần khai báo đối số của mẫu hàm chủ. Hàm khách có thể được truy cập được bên trong lời định nghĩa hàm chủ bằng cách dùng toán tử gián tiếp. Để làm được điều này, toán tử gián tiếp phải đi trước tên hàm khách, và cả toán tử gián tiếp lẫn tên hàm khách phải được đưa vào trong cặp ngoặc đơn:

(*function-name)(arg 1,  arg 2,  ...)

Ở đây, arg 1, arg 2,... là các đối số được truyền đến hàm khách. Sau cùng, tên của một hàm khách được truyền đến hàm chủ, trong quá trình gọi hàm chủ, bằng một đoạn có dạng

function-name

trong danh sách đối số của hàm chủ.

Chương trình dưới đây là một ví dụ khá ngốc nghếch để minh họa cách truyền tên hàm như đối số đến một hàm khác:

/* passfunction.c */
/*
  Program to illustrate the passing of function names as
  arguments to other functions via pointers
*/

#include <stdio.h>

/* Function prototype for host fun. */
void cube(double (*)(double), double, double *); 

double fun1(double);    // Function prototype for first guest function
double fun2(double);    // Function prototype for second guest function

int main() 
{
  double x, res1, res2;

  /* Input value of x */
  printf("\nx = ");
  scanf("%lf", &x);

  /* Evaluate cube of value of first guest function at x */
  cube(fun1, x, &res1);

  /* Evaluate cube of value of second guest function at x */
  cube(fun2, x, &res2);

  /* Output results */
  printf("\nx = %8.4f   res1 = %8.4f   res2 = %8.4f\n", x, res1, res2);

  return 0;
}

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

void cube(double (*fun)(double), double x, double *result) 
{
  /*
    Host function: accepts name of floating-point guest function 
    with single floating-point argument as its first argument, 
    evaluates this function at x (the value of its second argument), 
    cubes the result, and returns final result via its third argument.
  */

  double y;

  y = (*fun)(x);        // Evaluate guest function at x
  *result = y * y * y;  // Cube value of guest function at x

  return;
}

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

double fun1(double z) 
{
  /*
    First guest function
  */

  return 3.0 * z * z - z;
}

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

double fun2(double z) 
{
  /*
    Second guest function
  */

  return 4.0 * z  - 5.0 * z * z * z;
}

Trong chương trình trên, hàm cube() chấp nhận tên của một hàm khách (với một đôi số) làm đối số thứ nhất, lượng giá hàm này tại x (giá trị của đối số thứ hai, vốn cuối cùng sẽ được người dùng chỉ định), tính lập phương của kết quả trên, và sau đó truyền kết quả cuối cùng trở lại phần chính của chương trình qua đối số thứ ba (vốn lại là một con trỏ). Hai hàm khách, fun1()fun2(), với các tên được truyền cho cube(), đều là các đa thức đơn giản. Kết quả đầu ra của chương trình trên có dạng sau:

x = 2  

x =   2.0000   res1 = 1000.0000   res2 = -32768.0000
%

Biến toàn cục

Ta đã thấy rằng một chương trình C tổng quát về cơ bản, đều chứa nhiều hàm. Hơn nữa, các biến dùng trong hàm này có tính cục bộ, nghĩa là một biến được định nghĩa trong hàm này thì không nhận thấy được ở các hàm khác. Phương pháp chính để chuyển dữ liệu từ hàm này sang hàm khác là qua danh sách đối số trong lời gọi hàm. Đối số có thể được truyền theo một trong hai cách. Khi một đối số được truyền theo giá trị thì giá trị của biến (hoặc biểu thức) cục bộ trong đoạn chương trình gọi được sao chép vào một biến cục bộ trong hàm được gọi. Khi một đối số được truyền theo tham chiếu thì một biến cục bộ trong đoạn chương trình gọi chia sẻ cùng vị trí trong bộ nhớ với biến cục bộ trong hàm được gọi. Vì vậy, mỗi thay đổi ở một biến đều được tự động phản ánh ở biến kia. Tuy nhiên, còn có một phương pháp thứ ba để chuyển thông tin từ một hàm này sang hàm khác. Có thể định nghĩa các biến có phạm vi toàn cục. Những biến như vậy có thể được nhận biết bởi tất cả các hàm cấu thành chương trình, và có cùng một giá trị bên trong các hàm này.

Trình biên dịch C có thể nhận biết một biến có phạm vi toàn cục chứ không phải cục bộ, khi lowifi khai báo của nó nằm bên ngoài bất kì hàm nào cấu thành chương trình. Dĩ nhiên, biến toàn cục chỉ có thể được sử dụng trong một câu lệnh thực thi sau khi được khai báo. Vì vậy, vị trí tự nhiên để khai báo biến cục bộ là ở trước tất cả lời khai báo hàm, tức là ngay tại điểm đầu chương trình. Lời khai báo biến toàn cục có thể dùng luôn để khởi tạo giá trị theo cách thông thường. Tuy nhiên, giá trị ban đầu phải được viết dưới dạng hằng số thay vì biểu thức. Hơn nữa, giá trị ban đầu chỉ được gán một lần, tại điểm đầu chương trình.

Chương trình dưới đây là một phiên bản khác nữa của printfact.c, trong đó sự liên lạc giữa hai phần của chương trình được thực hiện hoàn toàn bằng biến toàn cục:

/* printfact4.c */
/*
  Program to print factorials of all integers
  between 0 and 20
*/

#include <stdio.h>
#include <stdlib.h>

/* Prototype for function factorial() */
void factorial();

/* Global variable declarations */
int j;             
double fact;

int main() 
{
  /* Print factorials of all integers between 0 and 20 */
  for (j = 0; j <= 20; ++j) 
   {
    factorial();
    printf("j = %3d    factorial(j) = %12.3e\n", j, fact);
   }
  return 0;
}

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

void factorial() 
{
  /* 
     Function to evaluate factorial (in floating-point form)
     of non-negative integer j. Result stored in variable fact.
  */

  int count;

  /* Abort if j is negative integer */
  if (j < 0) 
   {
    printf("\nError: factorial of negative integer not defined\n");
    exit(1);
   }

  /* Calculate factorial */
  for (count = j, fact = 1.; count > 0; --count) fact *= (double) count;

  return;      
}

Kết quả đầu ra của chương trình trên hoàn toàn giống như của printfact.c. Cần thấy rằng biến toàn cục j được dùng để truyền giá trị nguyên mà từ đó phải tính giai thừa, từ phần chính của chương trình đến hàm factorial(), trong khi biến toàn cục fact được dùng để chuyển giai thừa tính được trở lại phần chính của chương trình. Cũng cần thấy cách dùng câu lệnh khởi tạo nhiều lần (ngăn cách bởi các dấu phẩy) bên trong lệnh for của factorial().

Các biến toàn cục cần được dùng tiết kiệm trong chương trình khoa học (cũng như chương trình nói chung), vì chúng tiềm ẩn nguy cơ trong khi dùng. Việc thay đổi một giá trị của biến toàn cục trong một hàm nào đó sẽ ảnh hưởng đến toàn bộ những phần khác của chương trình. Thật không may là, sự thay đổi đó đôi khi xuất hiện do vô ý, mà chỉ là hiệu ứng phụ của một hoạt động khác. Do đó, có khả năng giá trị của một biến toàn cục bị thay đổi không liệu trước, gây ra một lập trình nhỏ và có thể rất khó tìm, vì dòng lệnh gây lỗi có thể nằm ở chỗ bất kì trong chương trình. Những lỗi tương tự gắn với biến cục bộ thường dễ gỡ hơn nhiều, vì phạm vi của biến cục bộ nhỏ hơn rất nhiều so với biến toàn cục.

Mảng

Chương trình khoa học thường liên quan đến nhiều đơn vị dữ liệu có chung đặc tính. Trong trường hợp như vậy, để thuận tiện ta thường đặt các dữ liệu trên vào một mảng, để chúng có tên chung, chẳng hạn, x. Từng dữ liệu riêng lẻ có thể là số nguyên hay số có phần thập phân. Tuy nhiên, chúng phải có cùng kiểu dữ liệu.

Trong C, một phần tử của mảng, tức là một đơn vị dữ liệu, được chỉ đến bằng cách viết tên mảng theo sau là một hay nhiều chỉ số, với mỗi chỉ số được đặt trong cặp ngoặc vuông. Tất cả các chỉ số phải là số nguyên không âm. Vì vậy, trong một mảng n phần tử có tên x, các phần tử sẽ là x[0], x[1], …, x[n-1]. Lưu ý rằng phần tử thứ nhất là x[0] chứ không phải x[1], như ở các ngôn ngữ lập trình khác.

Số lượng các chỉ số quy định số chiều của mảng. Chẳng hạn, x[i] chỉ đến một phần tử của mảng một chiều, x. Tương tự, y[i][j] chỉ đến một phần tử của mảng hai chiều, y, v.v.

Các mảng được khai báo theo cách giống như biến thông thường, chỉ khác ở chỗ mỗi tên mảng phải được kèm theo một thông tin về kích thước (thường là số phần tử). Với mảng một chiều, kích thước được cho bởi một số nguyên dương, đặt trong cặp ngoặc vuông. Và ta dễ thấy quy tắc tổng quát cho mảng nhiều chiều. Một số khai báo mảng hợp lệ như sau:

int j[100];
double x[20];
double y[10][20];

Theo đó, j là một mảng số nguyên gồm 100 phần tử, x là một mảng số thập phân gồm 20 phần tử, và y là một mảng số thập phân kích thước 10x20. Lưu ý rằng những khai báo mảng có độ dài thay đổi như

double a[n];

trong đó n là một số nguyên, đều không hợp lệ trong C.

Đôi khi sẽ tiện hơn nếu định nghĩa kích thước của mảng theo hằng số tượng trưng thay vì một số nguyên cố định. Cách này sẽ giúp dễ dàng thay đổi chương trình có dùng đến mảng, vì tất cả những chỗ có viết đến kích thước tối đa của mảng đều có thể thay đổi được chỉ bằng cách thay đổi giá trị của hằng só tượng trưng. Cách này được dùng trong nhiều chương trình ví dụ suốt khóa học.

Cũng như biến thông thường, mảng có thể có phạm vi cục bộ hay toàn cục , tùy theo lời khai báo của mảng nằm trong hay ngoài phạm vi của một hàm bất kì cấu thành chương trình. Cả hai loại mảng đều có thể được khởi tạo bằng những câu lệnh khai báo của chúng.4 Chẳng hạn,

int j[5] = {1, 3, 5, 7, 9};

khai báo j là một mảng số nguyên gồm 5 phần tử, mà từng phần tử cụ thể nhận các giá trị ban đầu j[0]=1, j[1]=3, v.v.

C không cho phép áp dụng thao tác đơn lẻ đối với cả một mảng. Vì vậy, nếu xy là các mảng giống nhau (nghĩa là có cùng kiểu dữ liệu, số chiều, và kích thước) thì các thao tác gán, so sánh, v.v. liên quan đến hai mảng này phải được thực hiện với từng phần tử một của chúng. Điều đó thường được thực hiện bằng vòng lặp (hoặc các vòng lặp lồng nhau trong trường hợp mảng nhiều chiều).

Chương trình dưới đây là một minh họa đơn giản cho cách dùng mảng trong C. Chương trình đọc một danh sách các số, được người dùng nhập, vào trong mảng một chiều list, rồi tính giá trị trung bình của các số đó. Chương trình cũng tính và xuất ra độ lệch của từng số so với trị trung bình.

/* average.c */
/*
  Program to calculate the average of n numbers and then
  compute the deviation of each number from the average

  Code adapted from "Programming with C", 2nd Edition, Byron Gottfreid,
  Schaum's Outline Series, (McGraw-Hill, New York NY, 1996) 
*/

#include <stdio.h>
#include <stdlib.h>

#define NMAX 100

int main() 
{
  int n, count;
  double avg, d, sum = 0.;
  double list[NMAX];

  /* Read in value for n */
  printf("\nHow many numbers do you want to average? ");
  scanf("%d", &n);

  /* Check that n is not too large or too small */
  if ((n > NMAX) || (n <= 0)) 
   {
    printf("\nError: invalid value for n\n");
    exit(1);
   }

  /* Read in the numbers and calculate their sum */
  for (count = 0; count < n; ++count) 
   {
    printf("i = %d  x = ", count + 1);
    scanf("%lf", &list[count]);
    sum += list[count];
   }

  /* Calculate and display the average */
  avg = sum / (double) n;
  printf("\nThe average is %5.2f\n\n", avg);

  /* Calculate and display the deviations about the average */
  for (count = 0; count < n; ++count) 
   {
    d = list[count] - avg;
    printf("i = %d  x = %5.2f  d = %5.2f\n", count + 1, list[count], d);
   }
  return 0;
}

Chú ý cách dùng hằng số tượng trưng NMAX để biểu thị kích thước của list, và theo đó, số tối đa các giá trị có thể được tính trung bình. Một kết quả đầu ra của chương trình trên như sau:

How many numbers do you want to average? 5
i = 1  x = 4.6
i = 2  x = -2.3
i = 3  x = 8.7
i = 4  x = 0.12
i = 5  x = -2.7

The average is  1.68

i = 1  x =  4.60  d =  2.92
i = 2  x = -2.30  d = -3.98
i = 3  x =  8.70  d =  7.02
i = 4  x =  0.12  d = -1.56
i = 5  x = -2.70  d = -4.38
%

Điều quan trọng phải nhận thấy được là tên mảng trong C chính là một con trỏ chỉ đến phần tử đầu tiên trong mảng đó.5 Vì vậy, nếu x 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 viết là &x[0] hoặc đơn giản là x. Hơn nữa, địa chỉ của phần tử thứ hai trong mảng có thể được viết là &x[1] hoặc (x+1). Nói chung, địa chỉ của phần tử thứ (i+1) trong mảng có thể được viết là &x[i] hoặc (x+i). Cần nói thêm là, ta nên hiểu rằng (x+i) là một dạng biểu thức tương đối đặc biệt, vì x biểu diễn cho một địa chỉ, còn i là một số nguyên. Biểu thức (x+i) thực ra chỉ định địa chỉ của phần tử trong mảng mà nó đứng cách i vị trí bộ nhớ tính từ địa chỉ của phần tử đầu tiên. (Dĩ nhiên là C lưu tất cả phần tử trong một mảng theo đúng thứ tự vào một vùng nhớ liền mạch trong máy.) Vì vậy, (x+i) là một cách biểu diễn hình tượng cho một địa chỉ, chứ không phải môt biểu thức số học.

Vì cả &x[i] lẫn (x+i) đều biểu diễn địa chỉ của phần tử thứ (i+1) của mảng x, nên cả x[i]*(x+i) đều phải biểu diễn nội dung của địa chỉ đó, tức là giá trị của phần tử thứ (i+1). Trên thực tế, hai cách viết sau cùng hoàn toàn trao đổi được khi lập trình bằng C.

Lúc này, ta hãy tạm thời tập trung vào mảng một chiều. Toàn bộ mảng có thể được truyền vào hàm như một đối số. Để làm điều này, tên mảng phải đứng một mình, không có ngoặc vuông hay chỉ số, làm tham số trong lời gọi hàm. Đối số tương ứng trong lời định nghĩa hàm phải được khai báo như một mảng. Muốn vậy, tên mảng phải được viết kèm theo một cặp ngoặc vuông để trống. Kích thước của mảng không được chỉ rõ. Trong mẫu hàm, một đối số hàm được viết theo sau kiểu dữ liệu của đối số và một cặp ngoặc vuông.

Vì, như đã thấy là một tên hàm chính là một con trỏ, nên rõ ràng khi mảng được truyền vào hàm, nó được truyền theo tham chiếu chứ không theo giá trị. Vì vậy, nếu bất kì phần tử mảng nào thay đổi bên trong hàm thì những thay đổi đó được nhận thấy ở đoạn chương trình gọi. Tương tự, nếu một mảng (thay vì một phần tử riêng của mảng) xuất hiện trong danh sách đối số của hàm scanf() thì ta không nên viết kèm vào trước nó toán tử địa chỉ (&), vì tên mảng cũng chính là địa chỉ. Lí do tại sao các mảng trong C luôn được truyền theo tham chiếu đã rõ ràng. Để có thể truyền mảng theo giá trị, ta cần sao chép giá trị của từng phần tử. Mặt khác, để truyền mảng theo tham chiếu, ta chỉ cần truyền địa chỉ của phần tử đầu tiên. Rõ ràng là với những mảng lớn, việc truyền theo tham chiếu hiệu quả hơn nhiều so với truyền theo giá trị.

Chương trình dưới đây là một phiên bản khác nữa của printfact.c, dù hiệu quả hơn nhiều so với tất cả những phiên bản trước. Ở bản này, các giai thừa của những số nguyên dương đến 20 được tính một loạt, bằng hàm factorial(), có dùng đẳng thức
(n + 1)! = (n + 1) n!
Các giai thừa được lưu trữ vào phần tử của mảng fact[], vốn được truyền làm tham số từ factorial() vào phần chính của chương trình.

/* printfact5.c */
/*
  Program to print factorials of all integers
  between 0 and 20
*/

#include <stdio.h>

/* Function prototype for factorial() */
void factorial(double []);  

int main() 
{
  int j;
  double fact[21];           // Declaration of array fact[]

  /* Calculate factorials */
  factorial(fact);

  /* Output results */
  for (j = 0; j <= 20; ++j) 
    printf("j = %3d    factorial(j) = %12.3e\n", j, fact[j]);

  return 0;
}

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

void factorial(double fact[]) 
{
  /*
    Function to calculate factorials of all integers
    between 0 and 20 (in form of floating-point
    numbers) via recursion formula

    (n+1)! = (n+1) n!

    Factorials returned in array fact[0..20]
  */

  int count;

  fact[0] = 1.;               // Set 0! = 1

  /* Calculate 1! through 20! via recursion */
  for (count = 0; count < 20; ++count) 
    fact[count+1] = (double)(count+1) * fact[count];

  return;
}

Kết quả đầu ra của chương trình trên giống hệt với của printfact.c.

Điều quan trọng cần nhận thấy là không có kiểm tra phạm vi mảng trong C. Nếu mảng x được khai báo là chứa 100 phần tử thì trình biên dịch sẽ dành ra 100 ô nhớ liên tục với kích thước phù hợp trong bộ nhớ máy tính. Nội dung của những ô nhớ này có thể được truy cập bằng các biểu thức kiểu như x[i], trong đó số nguyên i phải nằm trong khoảng từ 0 đến 99. Như ta đã thấy, trình biên dịch hiểu x[i] như nội dung của ô nhớ nằm cách i ô tính từ điểm đầu mảng. Thật không may là những biểu thức kiểu như x[100] hay x[1000] cũng được hiểu theo cách tương tự, dẫn đến việc trình biên dịch chỉ định file chạy phải truy cập đến các ô nhớ ở ngoài vùng nhớ dành cho x. Hiển nhiên là việc truy cập những phần tử của mảng mà không tồn tại sẽ gây lỗi. Khó nói được lỗi đó thuộc dạng gì—chương trình có thể đổ vỡ, nó có thể đưa ra kết quả sai một cách ngớ ngẩn—tất cả đều phụ thuộc vào chính những thông tin đang được lưu ở những vị trí lân cận với vùng nhớ dành cho x. Dạng lỗi này có thể cực kì khó gỡ, vì nó không cho thấy rõ điều gì đã trục trặc khi chương trình được thực hiện. Vì vậy, người lập trình có trách nhiệm đảm bảo rằng tất cả tham chiếu đến phần tử trong mảng đều nằm trong giới hạn đã được khai báo của mảng tương ứng.

Bây giờ ta hãy bàn thêm về mảng nhiều chiều. Các phần tử trong mảng nhiều chiều được lưu ở một vùng nhớ liên tục trên máy. Khi quét qua vùng nhớ này từ đầu đến cuối, việc lưu trữ được thực hiện sao cho chỉ số cuối cùng của mảng thay đổi nhanh nhất trong khi chỉ số đầu thay đổi chậm nhất. Chẳng hạn, các phần tử của mảng hai chiều x[2][2] được lưu theo thứ tự sau: x[0][0], x[0][1], x[1][0], x[1][1]. Các phần tử của mảng nhiều chiều chỉ được gán địa chỉ khi chương trình nói rõ kích thước của mảng theo các chiều thứ hai, thứ ba, v.v. Vì vậy, không có gì là ngạc nhiên khi một mảng nhiều chiều được truyền làm tham số đến một hàm, sau đó lời khai báo đối số tương ứng trong phần định nghĩa hàm phải bao gồm khai báo kích thước rõ ràng với tất cả vị trí chỉ số trừ chỉ số đầu. Điều tương tự cũng đúng với đối số mảng nhiều chiều xuất hiện trong mẫu hàm.

Chuỗi kí tự

Kiểu dữ liệu kí tự cơ bản trong C có tên là char. Một chuỗi kí tự trong C có thể được biểu diễn tượng trưng bởi một mảng dữ liệu kiểu char. Chẳng hạn, lời khai báo

char word[20] = "four";

đã khởi tạo mảng word gồm 20 kí tự, rồi lưu trữ vào đó chuỗi kí tự “four”. Kết quả là các phần tử của word như sau:

word[0] = 'f'  word[1] = 'o'  word[2] = 'u'  word[3] =  'r'  word[4] = ''

với các phần tử còn lại không xác định. Ở đây, 'f' biểu thị cho kí tự “f”, v.v., và '' biểu thị cho kí tự rỗng (có mã ASCII bằng 0), vốn dược dùng trong C để báo điểm kết thúc của chuỗi kí tự. Kí tự rỗng được tự động thêm vào mọi chuỗi kí tự đặt giữa hai dấu náy kép. Lưu ý rằng, vì tất cả mọi chuỗi kí tự trong C phải được két thúc bởi kí tự rỗng (không nhìn thấy), nên phải cần mảng kí tự kích cỡ tối thiểu là n+1 mới lưu được chuỗi gồm n chữ cái.

Cũng như một mảng số, tên của mảng kí tự tương đương với con trỏ chỉ đến phần tử đầu của mảng. Vì vậy, word[i]*(word + i) đều chỉ dến cùng một kí tự trong mảng kí tự word. Tuy nhiên cần lưu ý là tên của mảng kí tự không phải là một con trỏ thực sự, vì địa chỉ mà nó chỉ đến không thể thay đổi được. Dĩ nhiên, ta luôn có thể biểu thị một mảng kí tự bằng một con trỏ thực sự. Xét lời khai báo sau

char *word = "four";

Ở đây, word được khai báo là một con trỏ chỉ đến một char, và nó chỉ đến phần tử thứ nhất trong chuỗi kí tự 'f' 'o' 'u' 'r' ''. Khác với tên của một mảng kí tự, con trỏ thực sự đến một char có thể được đổi hướng. Vì vậy,

char *word =  "four";
. . .
word = "five";

là hợp lệ, còn

char word[20] =  "four";
. . .
word = "five";

thì không. Lưu ý rằng, ở ví dụ thứ nhất, địa chỉ của các phần tử đầu trong chuỗi “four” và “five” có thể khác nhau. Tất nhiên, nội dung của một mảng kí tự luôn có thể thay đổi được, theo từng phần tử—chỉ cần địa chỉ của phần tử đầu tiên được giữ cố định. Do đó,

char word[20] =  "four";
. . .
word[0] = 'f';
word[1] = 'i';
word[2] = 'v';
word[3] = 'e';
word[4] = '';

là hoàn toàn hợp lệ.

Sau cùng, cần lưu ý rằng một chuỗi kí tự có thể được in ra bằng hàm printf() dùng với một dấu %s trong chuỗi điều khiển, chẳng hạn,

printf("word = %s\n", word);

Ở đây, đối số thứ hai, word, có thể là tên của mảng kí tự hoặc một con trỏ thực sự đến phần tử đầu của chuỗi kí tự.

Chương trình gồm nhiều file

Trong một chương trình có chứa nhiều hàm, để thuận tiện ta thường đặt mỗi hàm vào trong một file riêng, rồi dùng tiện ích make để biên dịch từng file, sau đó nối chúng lại để tạo ra một file chạy.

Có một số quy tắc dễ hiểu về chương trình gồm nhiều file. Vì một file nhất định ban đầu được biên dịch tách biệt khỏi phần còn lại của chương trình nên tất cả các hằng số tượng trưng xuất hiện trong file đó cần được định nghĩa từ đầu. Tương tự, tất cả các hàm trong thư viện được dùng đến phải được đi kèm với những tham chiếu đến các header file. Ngoài ra, bất kì hàm do người dùng định nghĩa nào được tham chiếu tới phải có mẫu hàm của chúng ngay ở đầu file. Sau cùng, tất cả biến toàn cục dùng trong file phải được khai báo từ đầu. Điều này thường có nghĩa là những định nghĩa cho hằng số tượng trưng dùng chung, những header file cho hàm thư viện dùng chung, mẫu hàm cho hàm chung do người dùng định nghĩa, và lời khai báo cho các biến toàn cục dùng chung, tất cả đều xuất hiện trong nhiều file. Lưu ý rằng một biến toàn cục cho trước chỉ có thể được khởi tạo ở một trong số các lời khai báo của nó; đây được coi là lời khai báo thực sự của biến đó [theo thông lệ, lời khai báo thực sự sẽ xuất hiện trong file chứa hàm main()]. Còn các khai báo khác, mà ta sẽ gọi là định nghĩa, phải được đứng sau một từ khóa extern nhằm phân biệt chúng với khai báo thực sự.

Để ví dụ, ta lấy chương trình printfact4.c từ trước, và chia nó ra nhiều file, mỗi file chứa một hàm. Ở đây có 2 file main.cfactorial.c. Mã lệnh của 2 file cấu thành chương trình như sau:

/* main.c */
/*
  Program to print factorials of all integers
  between 0 and 20
*/

#include <stdio.h>

/* Prototype for function factorial() */
void factorial();    

/* Global variable definitions */
int j;               
double fact;

int main() 
{
  /* Print factorials of all integers between 0 and 20 */
  for (j = 0; j <= 20; ++j)
    {
      factorial();
      printf("j = %3d    factorial(j) = %12.3e\n", j, fact);
    }
  return 0;
}

/* factorial.c */
/* 
   Function to evaluate factorial (in floating point form)
   of non-negative integer j. Result stored in variable fact.
*/

#include <stdio.h>
#include <stdlib.h>

/* Global variable declarations */
extern int j;               
extern double fact;

void factorial() 
{
  int count;

  /* Abort if j is negative integer */
  if (j < 0) 
    {
      printf("\nError: factorial of negative integer not defined\n");
      exit(1);
    }

  /* Calculate factorial */
  for (count = j, fact = 1.; count > 0; --count) fact *= (double) count;

  return;      
}

Lưu ý rằng tất cả các hàm thư viện và hàm do người dùng định nghĩa được tham chiếu đến trong từng file được khai báo (hoặc là qua header file hay một mẫu hàm) ở điểm đầu file đó. Cũng lưu ý rằng sự khác biệt giữa lời khai báo biến toàn cục trong file main.cđịnh nghĩa biến toàn cục trong file factorial.c.

Tham số dòng lệnh

Hàm main() có thể tùy ý có thêm những đối số đặc biệt cho phép các tham số được truyền vào hàm này từ hệ điều hành. Có hai tham số như vậy, với tên gọi quy định là argcargv. Tham số đầu, argc, là một số nguyên được đặt cho số lượng những tham số truyền đến main(), còn tham số thứ hai, argv, là một mảng các con trỏ đến chuỗi kí tự để chứa những tham số đó. Để truyền một hoặc nhiều tham số vào chương trình C khi chạy nó từ hệ điều hành, những tham số phải được đặt sau tên chương trình gõ vào từ dòng lệnh, chẳng hạn,

% program-name  parameter_1  parameter_2  parameter_3 ... parameter_n

Tên của chương trình sẽ được lưu vào phần tử thứ nhất trong argv, tiếp theo là mỗi tham số. Vì vậy, nếu tên chương trình được tiếp theo bởi n tham số thì sẽ có n + 1 mục trong argv, từ argv[0] đến argv[n]. Hơn nữa, argc sẽ được tự động đặt bằng n + 1.

Chương trình dưới đây là một minh họa đơn giản cho cách dùng tham số dòng lệnh, với mục đích đơn giản là in lại tất cả các tham số được truyền đến.

/* repeat.c */
/*
  Program to read and echo data from command line
*/

int main(int argc, char *argv[])
{
  int i;

  for (i = 1; i < argc; i++) printf("%s ", argv[i]);
  printf("\n");

  return 0;
}

Giả sử rằng chương trình chạy có tên là repeat, thì một kết quả đầu ra của chương trình có thể như sau:

% repeat The quick brown fox jumped over the lazy hounds
The quick brown fox jumped over the lazy hounds
%

Giả sử rằng một hay nhiều trong số các tham số được truyền vào chương trình có kiểu số. Như ta đã thấy, những số này thực ra được truyền vào dưới hình thức chuỗi kí tự. Vì vậy, trước khi được dùng để tính toán, chúng phải được chuyển sang kiểu int hoặc double. Có thể làm điều đó bằng cách dùng các hàm atoi() and atof() (header file phù hợp cho các hàm này là stdlib.h). Theo đó, int atoi(char *ptr) chuyển đổi chuỗi kí tự mà ptr chỉ đến sang kiểu int, còn double atof(char *ptr) chuyển đổi chuỗi mà ptr chỉ đến sang kiểu double. Chương trình dưới đây minh họa cho cách dùng hàm atof(); nó đọc vào một số được truyền dưới dạng tham biến dòng lệnh, được hiểu như số đo nhiệt độ Fahrenheit, đổi nó sang thang đo Celsius, rồi in ra kết quả.

/* ftoc.c */
/*
  Program to convert temperature in Fahrenheit input
  on command line to temperature in Celsius
*/

#include <stdlib.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
  double deg_f, deg_c;

  /* If no parameter passed to program print error
     message and exit */
  if (argc < 2)
    {
      printf("Usage: ftoc temperature\n");
      exit(1); 
    }

  /* Convert first command line parameter to double */
  deg_f = atof(argv[1]); 
  /* Convert from Fahrenheit to Celsius */
  deg_c = (5. / 9.) * (deg_f - 32.);

  printf("%f degrees Fahrenheit equals %f degrees Celsius\n", 
         deg_f, deg_c);

  return 0;
}

Giả sử rằng chương trình chạy có tên ftoc, một kết quả đầu ra của chương trình có thể như sau:

% ftoc 98
98.000000 degrees Fahrenheit equals 36.666667 degrees Celsius
%

Đo thời gian

File header time.h định nghĩa một loạt hàm thư viện có thể dùng được để đán giá thời gian chạy trên CPU của một chương trình C kéo dài bao lâu. Hàm đơn giản nhất là clock(). Một lời gọi đến hàm này, không kèm theo đối số, sẽ trả về khoảng thời gian CPU đã dùng kể từ khi bắt đầu chương trình gọi. Thời gian được trả về theo một kiểu dữ liệu đặc biệt, clock_t, được định nghĩa trong time.h. Thời gian này phải được chia cho CLOCKS_PER_SEC, vốn cũng được định nghĩa trong time.h, để ra số giây. Khả năng đo thời lượng CPU mà mã lệnh tiêu tốn rất có ích trong lập trình khoa học; chẳng hạn, nó cho phép xác định hiệu quả của việc dùng những cờ tối ưu khác nhau của trình biên dịch. Việc tối ưu hóa thường (nhưng không luôn luôn!) làm tăng tốc độ thực hiện chương trình. Tuy nhiên, tối ưu hóa quá đáng có thể làm giảm tốc độ của chương trình xuống.

Chương trình dưới đây minh họa cách dùng đơn giản của hàm clock(). Chương trình so sánh thời gian CPU cần để nâng một số double lên lũy thừa bậc bốn bằng cách tính trực tiếp và bằng cách gọi hàm pow(). Mỗi cách tính được thực hiện 1 triệu lượt và thời gian chạy CPU tìm ra sẽ được chia cho 1 triệu.

/* timing.c */
/* 
  Program to test operation of clock() function
*/

#include <time.h>
#include <math.h>
#define N_LOOP 1000000

int main()
{
  int i;
  double a = 11234567890123456.0, b;
  clock_t time_1, time_2;

  time_1 = clock();
  for (i = 0; i < N_LOOP; i++) b = a * a * a * a;
  time_2 = clock();
  printf ("CPU time needed to evaluate a*a*a*a:    %f microsecs\n",
          (double) (time_2 - time_1) / (double) CLOCKS_PER_SEC);

  time_1 = clock();
  for (i = 0; i < N_LOOP; i++) b = pow(a, 4.);
  time_2 = clock();
  printf ("CPU time needed to evaluate pow(a, 4.): %f microsecs\n",
          (double) (time_2 - time_1) / (double) CLOCKS_PER_SEC);

  return 0;
}

Một kết quả đầu ra điển hình của chương trình như sau:

CPU time needed to evaluate a*a*a*a:    0.190000 microsecs
CPU time needed to evaluate pow(a, 4.): 1.150000 microsecs
%

Rõ ràng, cách tính lũy thừa bậc bốn bằng hàm pow() tốn kém hơn rất nhiều so với cách tính trực tiếp. Vì vậy, như đã đề cập từ trước, hàm pow() không nên được dùng để tính lũy thừa một số có phần thập phân với số mũ là số nguyên nhỏ.

Số ngẫu nhiên

Một lớp lớn các bài toán khoa học, chẳng hạn phương pháp Monte Carlo, yêu cầu dùng biến số ngẫu nhiên. Một lời gọi đến hàm rand() (header file stdlib.h), không kèm theo tham số, sẽ trả lại một số nguyên ngẫu nhiên khá tốt trong khoảng từ 0 đến RAND_MAX (định nghĩa ở stdlib.h). Hàm srand() đặt đối số của nó, vốn có kiểu int, làm nhân cho một dãy số mới được trả lại từ rand(). Những dãy này có thể lặp lại được bằng cách gọi srand() với giá trị nhân như trước. Nếu không cấp nhân, hàm rand() tự lấy nhân bằng 1. Một cách lập trình C thông dụng là lấy nhân để phát sinh số ngẫu nhiên bằng với số giây trôi qua kể từ thời điểm 00:00:00 UTC [Giờ tọa độ toàn cầu], 1/1/1970. Con số này được trả lại dưới dạng số nguyên khi gọi hàm time (NULL) (header file: <time.h>). Cách chọn nhân như vậy đảm bảo rằng những bộ số ngẫu nhiên sẽ tự được sinh ra mỗi khi chạy chương trình.

Chương trình dưới đây minh họa cách dùng hàm rand() để thiết lập một biến giả ngẫu nhiên x phân bố đều trong khoảng từ 0 đến 1. Chương trình tính ra 107 giá trị của x, rồi tìm trị trung bình và phương sai của các giá trị này.

/* random.c */
/*
  Program to test operation of rand() function
*/

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#define N_MAX 10000000

int main()
{
  int i, seed;
  double sum_0, sum_1, mean, var, x;

  /* Seed random number generator */
  seed = time(NULL);
  srand(seed);

  /* Calculate mean and variance of x: random number uniformly
     distributed in range 0 to 1 */
  for (i = 1, sum_0 = 0., sum_1 = 0.; i <= N_MAX; i++)
    {
      x = (double) rand() / (double) RAND_MAX;

      sum_0 += x;
      sum_1 += (x - 0.5) * (x - 0.5);
    }
  mean = sum_0 / (double) N_MAX;
  var = sum_1 / (double) N_MAX;

  printf("mean(x) = %12.10f  var(x) = %12.10f\n", mean, var);

  return 0;
}

Kết quả đầu ra điển hình của chương trình này có dạng:

mean(x) = 0.5000335261  var(x) = 0.0833193874    
%

Dễ thấy rằng trị trung bình và phương sai lí thuyết của x lần lượt bằng 1 / 21 / 12. Có thể thấy được những giá trị mà chương trình tìm ra khớp với những giá trị lý thuyết tới 5 chữ số trong phần thập phân; đó là điều trông đợi chỉ với 107 lần gọi hàm.

Phần mở rộng C++ từ C

Trong mục này, ta sẽ xét qua một số phần mở rộng hữu ích của ngôn ngữ C++ so với C; các phần mở rộng này không liên quan đến hướng đối tượng. File chứa mã nguồn có dùng đến phần mở rộng C++ cần được phân biệt với mã nguồn C bằng đuôi file .cpp.

Trong C, mọi biến cục bộ của hàm phải được khai báo trước câu lệnh thực thi đầu tiên. Trong C++, điều này được nới lỏng: biến cục bộ có thể được khai báo (gần như) ở vị trí bất kì trong hàm, miễn là lời khai báo này xuất hiện trước khi biến đó được sử dụng lần đầu. Đoạn mã sau minh họa cho đặc điểm mới này:

. . .
for (int i = 0; i < MAX; i++)
 {
  . . .
 }
. . .

Cần nhận thấy rằng chỉ số i của vòng lặp for bây giờ lại được khai báo ở đầu vòng lặp, thay vì vở đầu hàm trong đó chứa vòng lặp. Điều này tiện hơn nhiều, và cũng khiến mã lệnh dễ đọc hơn (vì ta không cần phải quay trở lại phần khai báo ở đầu hàm để kiểm tra xem i là một biến int). Tuy nhiên cần lưu ý rằng biến i chỉ được định nghĩa trong phạm vi của vòng lặp, tức là giữa cặp ngoặc nhọn). Bất kì sự tham chiếu nào đến i từ ngoài vòng lặp sẽ gây lỗi biên dịch. Nói chung, khi một biến được khai báo trong C++, phạm vi (tầm hoạt động) của nó sẽ là từ lời khai báo đến dấu ngoặc nhọn đóng khối lệnh hiện thời trong chương trình. Các khối lệnh trong chương trình gồm có hàm, vòng lặp, các lệnh phức hợp được thực hiện theo điều kiện, v.v. và được bao quanh bởi cặp ngoặc nhọn. Có một số hạn chế trong cách khai báo biến mới này. Các biến không thể khai báo được trong câu lệnh điều kiện, trong biểu thức thứ hai hoặc thứ ba của vòng lặp for, hay trong lời gọi hàm.

Trong C, ta thường thấy rằng để truyền một đối số vào một hàm theo cách mà những thay đổi ở đối số này trong hàm sẽ được truyền ngược về đoạn chương trình gọi, thật sự ta cần phải truyền một con trỏ đến đối số. Cách làm này, vốn được gọi là truyền theo tham chiếu, được minh họa trong đoạn mã lệnh sau đây:

. . .
void square(double, double *);
. . . 
int main()
{
  . . .
  double arg, res;
  square(arg, &res);
  . . .
  return 0;
}
. . .
void square(double x, double *y)
{
  *y = x * x;
  return;
}

Ở đây, đối số thứ hai cho square() được trả về main() dưới dạng đã bị thay đổi. Vì vậy đối số này phải được truyền như con trỏ. Cho nên ta phải viết là &res thay vì res, khi gọi square(), và phải tham chiếu đến đối số dưới dạng *y, thay vì y, trong bản thân hàm đó. Một hồi sau, tất cả những dấu “và” lẫn dấu sao này sẽ trở nên nhàm chán! C++ giới thiệu một phương pháp mới có phần gọn hơn để truyền theo tham chiếu. Phương pháp mới này được minh họa như sau:

. . .
void square(double, double &);
. . . 
int main()
{
  . . .
  double arg, res;
  square(arg, res);
  . . .
  return 0;
}
. . .
void square(double x, double &y)
{
  y = x * x;
  return;
}

Ở đây, đối số thứ hai cho square() lại được truyền theo tham chiếu. Tuy nhiên, lần này nó được chỉ định bằng cách thêm vào phái trước tên biến một dấu “và” ở lời khai báo hàm. Một dấu “và” tương ứng xuất hiện ở mẫu hàm. Lưu ý rằng ta không cần thực hiện rõ việc truyền con trỏ vào đối số thứ hai khi gọi square() (điều này được máy tự động làm): nghĩa là ta viết res, thay vì &res, khi gọi square(). Tương tự, ta không cần làm rõ khâu gỡ bỏ tham chiếu các đối số trong bản thân hàm (điều này máy tự làm): nghĩa là ta tham chiếu đến đối số bằng tên cục bộ của nó, y, thay vì viết *y, bên trong hàm.

Các hàm được dùng trong chương trình C để tránh việc phải gõ lại toàn bộ khối lệnh ở nhiều chỗ trong mã nguồn. Việc dùng hàm cũng làm mã lệnh dễ đọc và bảo trì hơn. Tuy nhiên, phải trả giá để đổi lại sự tiện lợi khi dùng hàm. Khi một hàm được gọi trong một chương trình chạy, chương trình sẽ nhảy đến vùng nhớ mà mã lệnh của hàm được biên dịch lưu tại đó, và sau đó nhảy trở về vị trí ban đầu trong bộ nhớ nơi mà hàm trả về. Thật không may là những bước nhảy lớn trong không gian bộ nhớ đi đôi với một lời gọi hàm sẽ chiếm một thời gian chạy CPU nhất định. Thật sự là những gánh nặng kèm theo mỗi lời gọi hàm thường khiến cho người lập trình khoa học ngại viết những hàm nhỏ, ngay cả khi muốn. C++ cung cấp một giải pháp cho điều này, bằng cách dùng từ khóa mới inline. Một hàm inline trông giống như một hàm khi dùng, nhưng nó được biên dịch theo cách khác. Việc gọi hàm inline từ vài chỗ khác nhau trong mã lệnh sẽ không dẫn đến việc gọi nhiều lần vào cùng một hàm. Thay vì vậy, đoạn mã lệnh cho hàm inline được trình biên dịch chèn vào trong mã lệnh của chương trình vào mỗi chỗ mà hàm được dùng đến.

Hàm inline chỉ phát huy tác dụng khi hàm còn nhỏ. Nhược điểm của việc chèn mã lệnh của một hàm lớn nhiều lần vào mã lệnh của chương trình sẽ dễ dàng đè bẹp những ưu điểm nhỏ về hiệu năng giành được qua việc tránh gọi hàm theo cách thông thường. Mức cân bằng giữa ưu và nhược thường là khoảng ba dòng lệnh thực thi.

Để inline một hàm, người lập trình cần thêm vào từ khóa inline ở đầu dòng định nghĩa hàm. Chẳng hạn:

inline double square(double x)
{
  return x * x;
}

Vì phần thân của một hàm inline phải được biết trước khi trình biên dịch có thể chèn nó vào mã lệnh chương trình nên bất kì nơi nào hàm được dùng đến, ta phải định nghĩa một hàm như vậy trước khi dùng nó lần đầu—một khai báo mẫu hàm ở đây là không đủ. Thường thì hàm inline được định nghĩa trong file mã lệnh tại chính vị trí mà các mẫu hàm thông thường được đặt.

Lời khai báo mảng có kích cỡ thay đổi theo dạng

void function(n) 
{
  int n;
  double x[n];
  . . .
}

không hợp lệ trong C; điều này rất bất tiện. Trong C++, những lời khai báo kiểu như vậy có thể được thực hiện bằng những từ khóa newdelete. Do đó, đoạn mã lệnh trên viết trong C++ sẽ có dạng:

void function(n) 
{
  . . .
  int n;
  double *x = new double[n];
  . . .
  x[i] = . . .
  . . .
  . . .
  delete x[];
  . . .
}

Lưu ý rằng x thực chất được khai báo là một con trỏ, thay vì là mảng thông thường. Lời khai báo new double[n] dành ra một vùng nhớ vừa đủ lớn để chứa n số double, và sau đó trả lại địa chỉ của điểm đầu vùng nhớ này. Dòng delete x[] giải phóng vùng nhớ gắn với mảng x khi nó không còn được dùng đến (lưu ý rằng điều này không được tự động thực hiện). Thật không may là các từ khóa newdelete không dùng được để tạo các mảng nhiều chiều có kích cỡ thay đổi.

Số phức

Như ta đã đề cập từ trước, bản định nghĩa cho ngôn ngữ C không bao gồm phép tính số phức—có lẽ là vì căn bậc hai của âm 1 không phải là khái niệm nảy sinh trong lập trình hệ thống! Thật may là nhược điểm lớn này—dưới góc nhìn của lập trình khoa học—có thể khắc phục được bằng C++. Chương trình dưới đây minh họa cách dùng lớp số phức của C++ (header file complex.h) để tính toán số phức từ những số double:

/* complex.cpp */
/*
  Program to test out C++ complex class 
*/

#include <complex.h>
#include <stdio.h>

/* Define complex double data type */
typedef complex<double> dcomp; 

int main()
{
  dcomp i, a, b, c, d, e, p, q, r; // Declare complex double variables
  double x, y;

  /* Set complex double variable equal to complex double constant */
  i = dcomp (0., 1.); 
  printf("\ni = (%6.4f, %6.4f)\n", i);

  /* Test arithmetic operations with complex double variables */
  a = i * i;
  b = 1. / i;  
  printf("\ni*i = (%6.4f, %6.4f)\n", a);
  printf("1/i = (%6.4f, %6.4f)\n", b);

  /* Test mathematical functions using complex double variables */
  c = sqrt(i);
  d = sin(i);
  e = pow(i, 0.25); 
  printf("\nsqrt(i) = (%6.4f, %6.4f)\n", c);
  printf("sin(i) = (%6.4f, %6.4f)\n", d);
  printf("i^0.25 = (%6.4f, %6.4f)\n", e);

  /* Test complex operations */
  p = conj(i);
  q = real(i);
  r = imag(i);
  printf("\nconj(i) = (%6.4f, %6.4f)\n", p);
  printf("real(i) = %6.4f\n", q);
  printf("imag(i) = %6.4f\n", r);

  return 0;
}

Kết quả đầu ra của chương trình này như sau:

i = (0.0000, 1.0000)

i*i = (-1.0000, 0.0000)
1/i = (0.0000, -1.0000)

sqrt(i) = (0.7071, 0.7071)
sin(i) = (0.0000, 1.1752)
i^0.25 = (0.9239, 0.3827)

conj(i) = (0.0000, -1.0000)
real(i) = 0.0000
imag(i) = 1.0000
%                              

Chương trình trước hết định nghĩa kiểu số phức kép dcomp. Sau đó các biến thuộc kiểu này được khai báo, được gán bằng các hằng số phức, dùng trong biểu thức số học, hay dùng làm đối số của hàm toán học, v.v. giống như cách làm thông thường như với các biến kiểu double trong C. Lưu ý các hàm đặc biệt conj(), real(), và imag() để lần lượt lấy liên hợp phức, tìm phần thực và tìm phần ảo của một biến số phức.

Mảng nhiều chiều có kích cỡ thay đổi

Mảng nhiều chiều nảy sinh trong nhiều ứng dụng của lập trình khoa học. Hơn nữa, kích thước của các mảng dùng trong mã lệnh tính toán khoa học thường thay đổi giữa những lần chạy, tùy thuộc vào giá trị cụ thể của các tham số. Chẳng hạn, trong một mã lệnh mô phỏng dòng chảy, kích thước của mảng phụ thuộc vào số điểm nút lưới tính toán, vốn lại phụ thuộc vào độ chính xác cần đạt. Vì vậy, một phép thử quan trọng về độ phù hợp của một ngôn ngữ lập trình phục vụ mục đích tính toán khoa học là khả năng xử lý thuận tiện các mảng nhiều chiều có kích cỡ thay đổi được. Không may là C không đạt phép thử này, vì lời khai báo ma trận có kích thước thay đổi theo dạng

void function(a, m, n) 
{
  int m, n;
  double a[m][n];
  . . .

không hợp lệ (khác với FORTRAN 77, ở đó khai báo ma trận có kích cỡ thay đổi hoàn toàn chấp nhận được). Thật sự là khiếm khuyết [hoàn toàn không nên có] này trong bản định nghĩa ngôn ngữ C thường được đưa ra làm lý do tại sao C không nên được dùng vào mục đích khoa học. May mắn là ta có thể khắc phục sự thiếu hụt tính năng mảng nhiều chiều có kích cỡ thay đổi trong C bằng cách dùng một gói chương trình miễn phí trong C++ có tên là thư viện Blitz++ —xem http://www.oonumerics.org/blitz/.

Chương trình sau đây minh họa cách dùng thư viện Blitz++. Chương trình cộng hai ma trận có kích thước và các phần tử được người dùng nhập vào, rồi in ra kết quả.

/* addmatrix.c */
/*
  Program to add two variable dimension matrices input by user
*/

#include <stdio.h>
#include <stdlib.h>
#include <blitz/array.h>

using namespace blitz;

/* Function prototypes */
void readin(Array<double,2>);
void writeout(Array<double,2>);
void addmatrices(Array<double,2>, Array<double,2>, Array<double,2>);

int main() 
{

  int n, m;

  /* Input number of rows and columns */
  printf("\nPlease input number of rows, n, and number of columns, m: ");
  scanf("%d %d", &n, &m);

  /* Check that n, m are positive integers */
  if (n <= 0 || m <= 0) 
    {
      printf("\nError: invalid values for n and/or m\n");
      exit(1);
    }

  /* Array declarations */
  Array<double,2> A(n, m), B(n, m), C(n, m);

  /* Read in elements of A, row by row */
  printf("\nReading in elements of A:\n");
  readin(A);

  /* Read in elements of B, row by row */
  printf("\nReading in elements of B:\n");
  readin(B);

  /* Write out elements of A, row by row */
  printf("\nWriting out elements of A:\n");
  writeout(A);

  /* Write out elements of B, row by row */
  printf("\n\nWriting out elements of B:\n");
  writeout(B);

  /* Add matrices A and B */
  addmatrices(A, B, C);

  /* Write out matrix C = A + B, row by row */
  printf("\n\nWriting out elements of C = A + B:\n");
  writeout(C);
  printf("\n");

  return 0;
}

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

/*
  Read in elements of matrix M, row by row
*/  

void readin(Array<double,2> M) 
{
  int n = M.extent(0);
  int m = M.extent(1);

  for (int i = 0; i < n; i++) 
    {
      printf("\nRow %d: ", i + 1);
      for (int j = 0; j < m; j++) scanf("%lf", &M(i, j)); 
    }
  return;
}

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

/*
  Write out elements of matrix M, row by row
*/  

void writeout(Array<double,2> M) 
{ 
  int n = M.extent(0);
  int m = M.extent(1);

  for (int i = 0; i < n; i++)
    {
      printf("\nRow %d: ", i + 1);
      for (int j = 0; j < m; j++) printf("%7.2f ", M(i, j)); 
    }
  return;
}

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

/*
    Add matrices M and N and store result in matrix P
*/

void addmatrices(Array<double,2> M, Array<double,2> N, Array<double,2> P) 
{ 
  int n = M.extent(0);
  int m = M.extent(1);

  for (int i = 0; i < n; i++) 
    for (int j = 0; j < m; j++) 
      P(i, j) = M(i, j) + N(i, j); 

   return;
}

Một kết quả đầu ra của chương trình có dạng

Please input number of rows, n, and number of columns, m: 3 3

Reading in elements of A:

Row 1: 1 4 5

Row 2: 6 7 8

Row 3: 3 6 0

Reading in elements of B:

Row 1: 1 6 0

Row 2: 4 7 8

Row 3: 4 8 8

Writing out elements of A:

Row 1:    1.00    4.00    5.00 
Row 2:    6.00    7.00    8.00 
Row 3:    3.00    6.00    0.00 

Writing out elements of B:

Row 1:    1.00    6.00    0.00 
Row 2:    4.00    7.00    8.00 
Row 3:    4.00    8.00    8.00 

Writing out elements of C = A + B:

Row 1:    2.00   10.00    5.00 
Row 2:   10.00   14.00   16.00 
Row 3:    7.00   14.00    8.00 

Header file cho thư viện Blitz++ có tên là blitz/array.h. Hơn nữa, bất kì file chương trình nào dùng đến Blitz++ phải có thêm dòng lệnh bí ẩn using namespace blitz; trước lần gọi đầu tiên, nếu không sẽ có lỗi biên dịch. Dòng Array<double,2> A(n, m) khai báo mảng 2 chiều kích cỡ n nhân m số double. Có thể dễ dàng khái quát cách làm cho mảng số nguyên, hay mảng có nhiều chiều hơn. Lưu ý rằng phần tử i, j của ma trận A được viết đơn giản là A(i,j). Lời gọi hàm M.extent(0) trả lại kích cỡ của mảng M theo chiều thứ nhất. Tương tự, M.extent(1) trả lại kích cỡ của mảng M theo chiều thứ hai, v.v. Chi tiết về thao tác dùng Blitz++ có thể được tham khảo trong tài liệu đầy đủ đi kèm theo thự viện. Không may là thư viện Blitz++ làm chậm đáng kể quá trình biên dịch vì nó dùng đến một số đặc điểm rất hấp dẫn của ngôn ngữ C++.

CAM Graphics Class / Lớp đồ họa CAM

Có rất nhiều lớp C++ viết sẵn có ích mà bạn có thể lấy miễn phí từ mạng—chi tiết có thể xem ở http://www.trumphurst.com/cpplibsx.html. Trong mục này, ta chỉ đề cập đến một trong số đó—lớp đồ họa CAM,6 với mục đích là cho phép chương trình C++ tạo ra bản vẽ chứa những nét đơn giản. Lớp này sẵn có ở đường link sau: http://www.math.ucla.edu/~anderson/CAMclass/CAMClass.html

Lớp đồ họa CAM thực chất sẽ phát sinh một file PostScript.7 PostScript là một ngôn ngữ lập trình để miêu tả ngoại hình của một trang in. Nó được Adobe phát triển từ 1985, và trở thành một tiêu chuẩn công nghiệp cho ngành in ấn và đồ họa. Tất cả những nhà sản xuất máy in lớn đều chế tạo máy in có thể hiểu được PostScript. Một file PostScript theo quy ước có phần đuôi là .ps. Đoạn khung chương trình sau minh họa cho cách dùng lớp đồ họa CAM một cách cơ bản nhất:

. . .
#include <gprocess.h> // Header file cua CAM graphic class
. . .
CAMgraphicsProcess Gprocess;                 // khai bao mot qua trinh do hoa
CAMpostScriptDriver Pdriver("filename.ps");  // khai bao mot PostScript driver
Gprocess.attachDriver(Pdriver);              // gan driver vao qua trinh
. . .
Gprocess.frame();                            // "dong khung" ban ve thu nhat
. . .
Gprocess.frame();                            // "dong khung" ban ve thu hai
. . .
. . .  
Gprocess.frame();                            // "dong khung" ban ve cuoi
Gprocess.detachDriver();                     // go bo driver
. . .

File header cho lớp này có tên là gprocess.h. Thủ tục cho việc phát sinh bản vẽ bao gồm trước hết là khai báo một quá trình đồ họa, sau đó khai báo một PostScript driver và gắn nó vào một file PostScript—filename.ps ở ví dụ trên—và cuối cùng là gắn driver này vào quá trình. Một file PostScript có thể chứa nhiều hình, hay khung. Mỗi khung được hoàn tất bằng cách gọi đến Gprocess.frame(). Sau cùng, driver được gỡ bỏ, với hiệu ứng là đóng file PostScript.

Chương trình dưới đây dùng lớp đồ họa CAM để vạch đường cong y = sin2x với x nằm trong khoảng từ  - 2π đến .

/* camgraph1.cpp */
/*
  Illustration of use of CAM graphics class to create simple line plot

  Program plots y = sin^2 x versus x in range -2 PI to +2 PI

  Program adapted from gpsmp1.cpp by Chris Anderson, UCLA 1996
*/

#include <gprocess.h>
#include <math.h>
#include <stdlib.h>

double func(double);

int main()
{
  int N_points = 400;
  double x_start = -2. * M_PI;                
  double x_end = 2. * M_PI; 
  double delta_x = (x_end - x_start) / ((double) N_points - 1.);

  double *x = new double[N_points];  
  double *y = new double[N_points];

  for (int i = 0; i < N_points; i++)
    {
      x[i] = x_start + (double) i * delta_x;
      y[i] = func(x[i]);
      x[i] /= M_PI;
    }

  { // This brace used to limit scope of Gprocess
    CAMgraphicsProcess Gprocess;                // declare a graphics process
    CAMpostScriptDriver Pdriver("graph1.ps");   // declare a PostScript driver
    Gprocess.attachDriver(Pdriver);             // attach driver to process

    Gprocess.setAxisRange(-2., 2., -2., 2.);    // set plotting ranges
    Gprocess.title("y = sin(x*x)");             // label the plot
    Gprocess.labelX("x / PI");
    Gprocess.labelY("y");

    Gprocess.plot(x, y, N_points);              // do the plotting

    Gprocess.frame();                           // "frame" the plot

    Gprocess.detachDriver();                    // detach the driver
  } // This brace calls the destructor for Gprocess:
    // without it the system() call would hang up

  delete[] x;              
  delete[] y;

  system("gv graph1.ps");  // display plot on screen         

  return 0;
}

double func(double x)
{ 
  return sin(x*x);
}

Lệnh Gprocess.plot(x, y, n) vạch ra một nét liền trong mặt phẳng tọa độ, từ n giá trị của tung độ y theo n giá trị của hoành độ x. Lệnh Gprocess.setAxisRange(x_low, x_high, y_low, y_high) đặt phạm vi vẽ. Sau cùng, các lệnh Gprocess.title("title"), Gprocess.labelX("x_label"), và Gprocess.labelY("y_label") viết tiêu đề biểu đồ, tiêu đề các trục xy. Cũng cần nói thêm là lời gọi hàm UNIX:

system("gv graph1.ps") được dùng để truyền lệnh gv graph1.ps đến hệ điều hành. Khi chạy chương trình, lệnh này có tác dụng hiển thị nội dung file graph1.ps lên màn hình. Đồ thị được vẽ từ file graph1.ps như trên Hình 1.

(Biểu đồ) Hình 1. Đồ thị ví dụ được vẽ bởi lớp đồ họa CAM.

Chương trình dưới đây minh họa một số đặc điểm nâng cao của lớp đồ họa CAM:

/* camgraph2.cpp */
/*
  Illustration of use of CAMgraphics class to create more advanced line plots

  Program plots three trigonometric functions versus x in range 
   -2 PI to +2 PI using different plot styles and different
   line styles

  Program adapted from gpsmp2.cpp by Chris Anderson, UCLA 1996
*/

#include <gprocess.h>
#include <math.h>
#include <stdlib.h>

double fun1(double);
double fun2(double);
double fun3(double);

int main()
{
  int N_points = 100;
  double x_start = -2. * M_PI;                
  double x_end = 2. * M_PI; 
  double delta_x = (x_end - x_start) / ((double) N_points - 1.);

  double *x  = new double[N_points];
  double *y1 = new double[N_points];
  double *y2 = new double[N_points];
  double *y3 = new double[N_points];

  for (int i = 0; i < N_points; i++)
    {
      x[i] = x_start + (double) i * delta_x;
      y1[i] = fun1(x[i]);
      y2[i] = fun2(x[i]);
      y3[i] = fun3(x[i]);
      x[i] /= M_PI;
    }

  {
    CAMgraphicsProcess Gprocess;              // declare a graphics process
    CAMpostScriptDriver Pdriver("graph2.ps"); // declare a PostScript driver
    Gprocess.attachDriver(Pdriver);           // attach driver to process

    /* First frame;  using different plot "styles" */
    Gprocess.setAxisRange(-2., 2., -2., 2.);            // set plotting ranges
    Gprocess.title("Plots Using Different Plot Styles");// label the plot
    Gprocess.labelX("x / PI");
    Gprocess.labelY("y");

    Gprocess.plot(x, y1, N_points);              // solid line (default)
    Gprocess.plot(x, y2, N_points, '+');         // + markers
    Gprocess.plot(x, y3, N_points, '+', 2);      // + markers and solid line

    Gprocess.frame();                            // "frame" the plot

    /* Second frame; using different plot line "styles" */
    Gprocess.setAxisRange(-2., 2., -2., 2.);            // set plotting ranges
    Gprocess.title("Plots Using Different Line Styles");// label the plot
    Gprocess.labelX("x / PI");
    Gprocess.labelY("y");

    Gprocess.plot(x, y1, N_points);         // solid line (default)
    Gprocess.setPlotDashPattern(1);
    Gprocess.plot(x, y2, N_points);         // dashed line
    Gprocess.setPlotDashPattern(4);
    Gprocess.plot(x, y3, N_points);         // dashed-dot line

    Gprocess.frame();                       // "frame" the plot

    Gprocess.detachDriver();                // detach the driver
  }

  delete[] x;
  delete[] y1;
  delete[] y2;
  delete[] y3;

  system("gv graph2.ps"); // display plots on screen     

  return 0;
}

double fun1(double x)
{ 
  return sin(x);
}

double fun2(double x)
{ 
  return cos(x);
}

double fun3(double x)
{ 
  return cos(2.*x);
}

Lệnh Gprocess.plot(x, y, n, '+') vẽ các điểm trên mặt phẳng tọa độ từ n giá trị tung độ yn giá trị hoành độ x, mỗi điẻm được biểu thị bởi một kí tự '+'. Lệnh Gprocess.plot(x, y, n, '+', 2) cũng làm việc tương tự, nhưng nối các điểm bằng một đường liền nét. Đối số thứ tư của lệnh này là một mã số nguyên quyết định kiểu đường nét. Các lựa chọn gồm có: 0 – đường; 1 – điểm; 2 – cả đường lẫn điểm. Lệnh Gprocess.setPlotDashPattern(n) đặt kiểu nét vẽ. Đối số lần này cũng là mã số nguyên. Các lựa chọn gồm có: 0 – nét liền; 1 – nét gạch đứt; 2 – hai nét gạch đứt; 4 – gạch-chấm; 5 – gạch-chấm-chấm; 6 – nét chấm.

Các biều đồ vẽ ra trong hai khung của graph2.ps được chỉ ra trên các Hình 2 và 3.

(Biểu đồ)Hình 2. Đồ thị ví dụ được vẽ bởi lớp đồ họa CAM.

(Biểu đồ) Hình 3. Đồ thị ví dụ được vẽ bởi lớp đồ họa CAM.


  1. Chặt chẽ mà nói, đây là tính năng mở rộng của C++ so với C. Tuy nhiên, các trình biên dịch C ngày nay đều chấp nhận kiểu chú thích này.
  2. Mẫu hàm là yêu cầu bắt buộc cho tất cả mọi hàm do người dùng định nghĩa trong các chương trình C++.
  3. Cặp ngoặc tròn bao quanh *function-name rất quan trọng:
    data-type *function-name(type 1, type 2, ...) được hiểu bởi trình biên dịch như một tham chiếu đến hàm trong đó trả lại một con trỏ đến kiểu data-type, thay vì một con trỏ đến hàm mà trả lại kiểu data-type.
  4. Trước khi có tiêu chuẩn ANSI mà trình biên dịch GNU C tuân theo, mảng cục bộ không thể khởi tạo được qua câu lệnh khai báo.
  5. Một tên mảng không hoàn toàn tương đương với một con trỏ, vì con trỏ có thể chỉ đến bất kì địa chỉ nào trong bộ nhớ máy tính—địa chỉ này thậm chí có thể thay đổi được—trong khi tên mảng bị hạn chế vì luôn phải chỉ đến địa chỉ của đơn vị đầu tiên của dữ liệu tương ứng.
  6. Bản quyền của CAM graphics class thuộc về tác giả, GS Chris Anderson, Khoa Toán, UCLA, 1998.
  7. PostScript là thương hiệu đã đăng kí của Adobe Systems Incorporated.

2 phản hồi

Filed under Tin học, Vật lý tính toán

2 responses to “Chương 2: Lập trình khoa học bằng ngôn ngữ C

  1. Xin gửi đến các bạn đọc bản dịch phần bài giảng về lập trình C trong giáo trình môn học “Vật lý tính toán” của PGS Richard Fitzpatrick, University of Texas at Austin. Việc chuyển đổi định dạng còn gặp chút khó khăn và hình vẽ vẫn chưa chèn vào được; tuy vậy vẫn hi vọng tài liệu này sẽ có ích!

  2. Pingback: Chương 1: Giới thiệu chung | Blog của Chiến

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s