Mở đầu
Lịch sử của C
Phát triển bởi Ken Thompson và Dennis Ritchie của
AT&T Bell Labs vào năm 1972
Năm 1983, ANSI bắt đầu q trình chuẩn hóa
Năm 1999, chuẩn được bổ sung, kết quả là C99
Năm 1989, ISO tiếp tục quá trình chuẩn hóa
Năm 1990, q trình chuẩn hóa kết thúc, kết quả là C90
(Standard C)
Lịch sử của C
Ken Thompson
Dennis Ritchie &
Brian Kernighan
C là sản phẩm của hệ điều hành UNIX. Năm 1970, UNIX được viết bằng
hợp ngữ, khó dị lỗi và phát triển. Ken Thompson thiết kế một ngôn ngữ cấp
cao nhằm phát triển UNIX trong tương lai, đó là ngôn ngữ B, dựa trên BCPL
– một ngôn ngữ dùng viết hệ điều hành và trình biên dịch (Martin Richards,
1967). Năm 1972, Dennis Ritchie tham gia phát triển UNIX bằng ngơn ngữ B
trên DEC PDP-11. Do B khơng cịn phù hợp, Ritchie phát triển một phiên
bản mở rộng của B, gọi là NB (New B). Sau đó, Ritchie đặt tên cho ngôn ngữ
mới là C. Năm 1973, UNIX được viết lại hoàn toàn bằng C.
Năm 1978, Brian Kernighan và Dennis Ritchie xuất bản sách “The C
Programming Language”, cuốn sách này được xem như một chuẩn thực tế
của ngôn ngữ C, gọi là K&R C.
Chuẩn hóa
Cộng đồng lập trình viên dùng C nhanh chóng phát triển, nhưng chuẩn thực
tế K&R C cịn nhiều điểm mơ hồ, vì vậy cần chuẩn hóa C.
ANSI
ANSI (American National Standards Institute) bắt đầu quá trình chuẩn hóa C
từ năm 1983, kéo dài đến năm 1989. Kết quả là chuẩn ANSI X3.159-1989.
ISO
Năm 1989, chuẩn trên được ISO (International Standards Organization)
chấp thuận như chuẩn quốc tế ISO/IEC 9899:1990, mơ tả C90, cịn gọi là
ANSI C.
C99
C trải qua vài thay đổi và mở rộng vào năm 1999, mô tả trong Amendment1.
Phát hành chuẩn mới ISO/EIC 9899:1999; mô tả C99. Tuy nhiên C99 chưa
được áp dụng rộng rãi.
© Dương Thiên Tứ
www.trainingwithexperts.com
Đặc điểm của C
C là ngôn ngữ cấp thấp
C là ngơn ngữ lập trình hệ thống
C gọn nhẹ và khả chuyển
Chương trình C có thể khó hiểu và khó tùy biến
Đặc điểm của C
Ngơn ngữ cấp
thấp
Khác với các ngơn ngữ lập trình cấp cao (high-level), C cung cấp các tác vụ
gần với các chỉ thị của máy, các khái niệm truy xuất cấp độ máy, … Vì vậy
các chương trình C thường có tốc độ thực thi nhanh, ít dịng lệnh, ít hao tốn
vùng nhớ. C được xem là ngơn ngữ lập trình cấp thấp (low-level language).
Ngơn ngữ lập
trình hệ thống
C là ngơn ngữ được lựa chọn hàng đầu để phát triển các hệ điều hành:
UNIX, Linux, Windows, … C được tích hợp như cơng cụ trên UNIX và Linux.
Gọn nhẹ và khả
chuyển
C có ít đặc tả hơn các ngơn ngữ khác, thay vào đó C cung cấp một thư viện
lớn các hàm chuẩn: xuất nhập, xử lý chuỗi, cấp phát vùng nhớ, … và các tác
vụ tiện ích khác . Trình biên dịch C cũng nhỏ và dễ viết.
Sau khi chuẩn hóa, C cung cấp tính khả chuyển (portability), cho phép
chương trình biên dịch được trên nhiều hệ nền.
“Write Only”
C mang tiếng là ngôn ngữ “Write Only”, nghĩa là dễ dàng viết code nhưng
khó đọc được code. Tính linh động và gọn nhẹ của C có một tác động khơng
mong muốn, lập trình viên C viết các chương trình hiệu quả, nhưng khó đọc
nên khó tùy biến.
1
© Dương Thiên Tứ
www.trainingwithexperts.com
Một chương trình C
Báo cho trình biên dịch biết có sự tham gia
của thư viện các hàm nhập xuất chuẩn
(standard input and output functions)
#include <stdio.h> /* chú thích */
Hàm main
int main(void)
{
printf("Hello\n");
printf("Welcome to C!");
“Khởi đầu”
“Kết thúc”
return 0;
}
Hello
Welcome to C!
Cờ báo cho Hệ điều hành biết
chương trình thành cơng
Một chương trình C
#include
Chỉ thị biên dịch (directives) #include hướng dẫn cho bộ tiền xử lý của C
(C Preprocessor) tìm khai báo hàm trong tập tin “stdio.h”. “stdio” viết tắt
của “standard input and output” (nhập xuất chuẩn) và “.h” nghĩa là một
tập tin tiêu đề (header).
Chú thích
Các chú thích được đặt trong cặp dấu /* và */ theo thứ tự đó và có thể
trải dài trên nhiều dịng. C99 cho phép dùng // như C++, Java, …
main
Hàm main rất quan trọng. Nó định nghĩa điểm chương trình bắt đầu thực
hiện. Nếu bạn khơng viết hàm main chương trình sẽ khơng biên dịch.
Cặp ngoặc nhọn
{}
C dùng dấu ngoặc nhọn “{” với nghĩa “khởi đầu” và dấu “}” với nghĩa “kết
thúc”. Cặp ngoặc giúp phân chương trình thành các khối, nên dễ nhập
lệnh và dễ đọc. Cặp {} trong chương trình trên bao khối hàm main.
printf
Gọi hàm (function call) printf là một cách chuẩn để kết xuất dữ liệu từ
chương trình. Hàm này được định nghĩa trong thư viện chuẩn stdio.h.
Hai ký tự liên tiếp “\” (ký tự escape) và “n” (newline) là cách C xử lý tạo
dịng mới. Thay vì in “\n”, con trỏ sẽ được chuyển đến đầu dòng kế tiếp.
return
return trả về một trị cho Hệ điều hành. Hệ điều hành xử lý thơng tin từ
chương trình trả về như thế nào? MS-DOS lưu trữ nó trong biến
ERRORLEVEL, được dùng trong các tập tin lô (batch). Các shell Bourne
và Korn của UNIX lưu trữ nó trong một biến tạm $?, được dùng trong các
shell script. 0 nghĩa là thành công. Một trị 1, 2, 3, ... chỉ định mã lỗi khác
nhau. Tất cả các Hệ điều hành đều hỗ trợ các trị lên đến 255.
2
© Dương Thiên Tứ
www.trainingwithexperts.com
Dạng thức của C
Các phát biểu kết thúc với dấu ;
Các chuỗi đặt trong cặp ngoặc kép “”
Các cân chỉnh dịng được trình duyệt bỏ qua
C phân biệt chữ hoa-chữ thường. Tên các từ khóa và
hàm thư viện chuẩn đều là chữ thường.
Tạo dòng mới được xử lý với \n
Các chương trình có thể báo thành cơng hay lỗi, đừng
quên điều này
Dạng thức của C
Dấu ;
Dấu ; rất quan trọng trong C. Nó báo cho trình biên dịch biết rằng một phát
biểu (statements) kết thúc và một phát biểu khác bắt đầu. Nếu quên đặt nó
sau từng phát biểu, bạn sẽ nhận được lỗi biên dịch.
Dạng thức tự do
C là ngôn ngữ dạng thức tự do (free format). Nghĩa là bạn có thể trình bày,
cân chỉnh (indentation) hoàn toàn theo ý muốn, nhưng nên theo một nguyên
tắc để chương trình dễ đọc. Có thể viết một phát biểu trên nhiều dịng. Có
thể phân các phần trong khối bằng dòng trắng cho dễ đọc. Các dấu cách
(space) và các dấu tab trong chương trình sẽ được trình biên dịch bỏ qua.
Phân biệt chữ
hoa chữ thường
C là ngôn ngữ phân biệt chữ hoa với chữ thường (case sensitive). Chỉ có từ
khóa int được biên dịch; các từ “Int”, “INT” hoặc các biến thể khác đều
khơng là từ khóa. Tất cả từ khóa của C đều là chữ thường. Tất cả khoảng
hàng trăm hàm trong thư viện chuẩn của C đều là chữ thường.
Từ khóa của C:
auto break case char const continue default do double else
enum extern float for goto if int long register return short
signed sizeof static struct switch typedef union unsigned void
volatile while
C99: inline restrict _Bool _Complex _Imaginary
Hành vi
ngẫu nhiên
Chúng ta đã nói rằng hàm main sẽ trả về một số nguyên cho Hệ điều hành,
nếu bạn quên điều này (chỉ có return hoặc thiếu cả return) bạn sẽ nhận
được một cảnh báo lúc biên dịch. Nếu bỏ qua, chương trình sẽ có hành vi
ngẫu nhiên tùy theo số nguyên ngẫu nhiên nó trả cho Hệ điều hành.
3
© Dương Thiên Tứ
www.trainingwithexperts.com
Một ví dụ khác
#include <stdio.h>
Tạo hai biến
nguyên a và b
int main(void)
{
int a, b;
Đọc hai số
nguyên vào
“a” và “b”
printf("Nhap hai so: ");
scanf("%i %i", &a, &b);
printf("%i - %i = %i\n", a, b, a - b);
Viết “a”, “b” và
“a-b” theo dạng
thức chỉ định
return 0;
}
Nhap hai so: 21 17
21 - 17 = 4
Một ví dụ khác
int
Từ khóa int, được dùng để khai báo (declarations) các biến kiểu nguyên
(interger), Ở đây có hai biến được khai báo: “a” và “b”.
scanf
Hàm scanf ngược với printf. Trong khi printf kết xuất ra màn hình,
scanf lại đọc vào từ bàn phím. Chuỗi định dạng “%i” báo cho scanf đọc một
số nguyên từ bàn phím. Do đó “%i %i” nghĩa là hai số nguyên sẽ được đọc,
trị đầu tiên đặt vào biến “a” và trị thứ hai đặt vào biến “b”.
Dấu cách giữa hai “%i” trong “%i %i” rất quan trọng: nó báo cho scanf biết
rằng phải đợi một dấu cách trước khi nhận trị thứ hai. Nếu thay bằng
“%i,%i”, scanf sẽ đợi một dấu , trước khi nhận trị thứ hai.
printf
Ví dụ cho thấy printf và scanf là các hàm xuất nhập có định dạng. Với
chuỗi định dạng “%i”, hàm scanf sẽ nhập số nguyên từ bàn phím và hàm
printf sẽ xuất số nguyên ra màn hình.
Chú ý trong chuỗi định dạng của printf có ba “%i”, để nhận ba trị từ hai biến
(a, b) và một biểu thức (a-b).
Định dạng “%i” và “%d” giống nhau khi dùng printf. Tuy nhiên, với scanf,
“%i” có thể nhận số ở hệ octal hoặc hexadecimal, trong khi “%d” chỉ nhận số
thập phân.
Các biểu thức
Chú ý rằng C hiểu phải tính tốn biểu thức “a-b” và in kết quả ra như một trị
nguyên. Bạn có thể tạo một biến “c” khác, gán cho nó trị của “a-b” và in trị
của “c”, nhưng không cần thiết.
4
© Dương Thiên Tứ
www.trainingwithexperts.com
Các biến (variables)
Các biến phải khai báo trước khi dùng ngay sau dấu
“{”
Các ký tự hợp lệ là các chữ cái, các số, “_”
Vài trình biên dịch chỉ hiểu 6 ký tự đầu đối với các
biến toàn cục và các tên hàm
Phân biệt chữ hoa và chữ thường
Ký tự đầu tiên không thể là một số
C chỉ hiểu 31 ký tự đầu đối với các biến cục bộ, nếu
dùng nhiều hơn sẽ bị bỏ qua
Các biến
Khai báo biến
Trong C, tất cả các biến phải được khai báo trước khi dùng. Nghĩa là bạn
phải đặt cho biến một tên (gọi là định danh – identifier), gán cho biến một
kiểu, và cũng có thể khởi tạo biến với một trị) trước khi dùng.
Trong C, khai báo các biến phải nằm trước tất cả các phát biểu trong khối.
C99 không bắt buộc điều này.
Các tên hợp lệ
Không được dùng các từ khóa dành riêng cho C làm tên biến.
Chỉ có các chữ cái, các số và dấu “_” được dùng hợp lệ trong các tên biến.
Ký tự đầu của biến có thể là chữ cái hay dấu “_” (theo chuẩn, nên tránh dùng
ký tự “_” làm ký tự đầu). Như vậy các tên biến như “temp_in_celsius”,
“index32” và “sine_value” đều hợp lệ, trong khi các tên biến “32index”, “tempin-celsius” và “sine$value” là khơng hợp lệ.
Tên biến có thể dài, trình biên dịch sẽ sắp xếp với 31 ký tự đầu. Các ký tự
cịn lại trình biên dịch sẽ bỏ qua, nên tên biến có thể dài hơn nhưng phải khác
nhau trong 31 ký tự đầu. C99 cho phép đến 63 ký tự.
Vài trình biên dịch chỉ hiểu 6 ký tự đầu với các biến toàn cục và các tên hàm.
Các ký tự hoa
Các ký tự hoa có thể được dùng trong các tên biến nếu muốn. Người ta
thường dùng thay thế ký tự “_” dùng phổ biến trước đây, ví dụ thay cho tên
biến “temp_in_celcius” bằng tên biến “tempInCelcius”. Kiểu định danh này
được dùng phổ biến gần đây và dấu “_” đã bị bỏ quên.
5
© Dương Thiên Tứ
www.trainingwithexperts.com
printf và scanf
printf xuất các trị nguyên (integer) ra màn hình khi
dùng %i
scanf nhập các trị nguyên (integer) từ bàn phím khi
dùng %i
“&” RẤT quan trọng với scanf (cần để thay đổi tham
số được truyền, sẽ bàn sau) - nếu thiếu, chương trình
sẽ chạy sai
“&” khơng cần với printf vì các trị hiện tại của các
tham số sẽ được dùng
printf và scanf
printf
Hàm printf ghi kết xuất ra màn hình. Khi nó gặp chỉ định dạng thức %i, một
số nguyên sẽ được in ra. Muốn in dấu %, dùng %% trong chuỗi định dạng, ví
dụ:
printf("Net profit: %d%%\n", profit);
scanf
Hàm scanf nhập từ bàn phím. Khi nó gặp chỉ định dạng thức %i, nó sẽ chờ
người dùng nhập một số ngun.
&
Tốn tử (operator) “&” rất quan trọng với scanf. Nó cho phép scanf thay đổi
các biến được truyền tới nó. Một trong những lỗi phổ biến là quên “&” khi
dùng scanf.
Ví dụ:
scanf("%i", &j);
ký tự “&” sẽ cho phép biến “j” được thay đổi. Nếu quên ký tự này, j sẽ nhận
một ký tự ngẫu nhiên nào đó mà nó đã có trước (trừ khi bạn đã khởi tạo “j”).
Trong khi đó, printf khơng cần thay đổi trị của bất kỳ biến nào nó in ra, nó
khơng cần dấu “&” nào. Nếu “j” chứa trị 15, sau khi thực hiện phát biểu:
printf("%i", j);
bạn vẫn nhận được trị 15 từ biến “j” vì printf khơng có khả năng thay đổi
nó.
6
© Dương Thiên Tứ
www.trainingwithexperts.com
Kiểu nguyên trong C
C hỗ trợ nhiều kiểu số nguyên khác nhau
Cực đại và Cực tiểu định nghĩa trong “limits.h”
Kiểu
Dạng thức
bytes
Cực tiểu
Cực đại
char
%c
1
CHAR_MIN
CHAR_MAX
signed char
%c
1
SCHAR_MIN
SCHAR_MAX
unsigned char
%c
1
0
UCHAR_MAX
short [int]
%hi
2
SHRT_MIN
SHRT_MAX
unsigned short
%hu
2
0
USHRT_MAX
int
%i
2 hoặc 4
INT_MIN
INT_MAX
unsigned int
%u
2 hoặc 4
0
UINT_MAX
long [int]
%li
4
LONG_MIN
LONG_MAX
unsigned long
%lu
4
0
ULONG_MAX
Kiểu nguyên trong C
limits.h
Tập tin tiêu đề (header) chứa các định nghĩa của một vài hằng số, cho biết
kích thước cực đại và cực tiểu của vài kiểu nguyên.
Các kiểu nguyên
khác nhau
C hỗ trợ các kiểu ngun có kích thước khác nhau. Các từ short và long
phản ánh kích thước vùng nhớ dành cho nó. Một số ngun short cần ít
vùng nhớ hơn một số nguyên long. Các hằng số cho bạn biết giới hạn của
kiểu nguyên, dễ dàng cho việc lựa chọn kiểu nguyên thích hợp.
Chú ý rằng kích thước của kiểu int là 2 hoặc 4 byte tùy theo hệ nền
(platform) cụ thể. Vì vậy chương trình có tính khả chuyển thường khơng dùng
kiểu int mà dùng kiểu short hoặc long.
unsigned
Từ khóa unsigned cho thấy tất cả các bit được dùng để lưu trữ các số, với
số có dấu, bit cao nhất dùng để lưu trữ dấu. Nghĩa là kiểu unsigned có thể
lưu trữ trị lớn nhất hơn kiểu có dấu 2 lần. Khi unsigned được dùng, số âm
không được lưu trữ, chỉ có số 0 và số dương.
%hi
“h” cho thấy kiểu short bằng nửa (half) kiểu int (tùy trình biên dịch).
C99
C99 cung cấp thêm hai kiểu nguyên chuẩn: long long int và unsigned
long long int, ít nhất 64-bit, dùng cho các trị nguyên cực lớn. Với hằng
kiểu long long int, thêm LL (hoặc ll) phía sau trị khai báo. Với hằng kiểu
unsigned long long int, thêm U (hoặc u) phía trước hoặc sau LL (hoặc
ll). Chuỗi định dạng trong printf và scanf thêm ll trước d, o, u, x:
long long x;
scanf("%lld", &x);
7
© Dương Thiên Tứ
www.trainingwithexperts.com
Ví dụ về kiểu nguyên
#include <stdio.h>
#include <limits.h>
int main(void)
{
unsigned long big = ULONG_MAX;
printf("minimum
printf("maximum
printf("maximum
printf("maximum
printf("maximum
return 0;
}
int = %i, ", INT_MIN);
int = %i\n", INT_MAX);
unsigned = %u\n", UINT_MAX);
long int = %li\n", LONG_MAX);
unsigned long = %lu\n", big);
minimum
maximum
maximum
maximum
int = -32768, maximum int = 32767
unsigned = 65535
long int = 2147483647
unsigned long = 4294967295
Ví dụ về kiểu nguyên
INT_MIN,
INT_MAX
Kết quả của chương trình cho thấy kiểu int có kích thước 2 byte (hệ nền 16
bit). Vì vậy trị cực đại là 32767. Nó cũng cho thấy trị cực đại của kiểu
unsigned int gấp đôi là 65535. Tương tự, trị cực đại của unsigned long
int là gấp đôi trị cực đại của signed long int.
8
© Dương Thiên Tứ
www.trainingwithexperts.com
Ví dụ về kiểu char
#include <stdio.h>
#include <limits.h>
int main(void)
{
char lower_a = 'a';
char lower_m = 'm';
printf("minimum char = %i, ", CHAR_MIN);
printf("maximum char = %i\n", CHAR_MAX);
printf("Sau '%c' la '%c'\n", lower_a, lower_a + 1);
printf("Chu hoa la '%c'\n", lower_m - 'a' + 'A');
return 0;
}
minimum char = -128, maximum char = 127
Sau 'a' la 'b'
Chu hoa la 'M'
Ví dụ về kiểu char
char
C dành kiểu dữ liệu char cho các ký tự. Các ký tự được định trị bằng cách
đặt ký tự trong cặp dấu nháy đơn ‘ ’. Ví dụ:
char lower_a = 'a';
sẽ đặt mã ASCII của ký tự “a” thường, 97, vào trong biến “lower_a”. Khi trị 97
này được in với chuỗi định dạng %c, nó sẽ chuyển trở lại thành ký tự “a”
thường. Nếu chương trình này chạy trên máy EBCDIC, trị lưu sẽ có giá trị
khác, nhưng nó cũng chuyển lại thành “a” và xuất ra.
CHAR_MIN,
CHAR_MAX
Hai hằng này chỉ trị cực đại và cực tiểu của kiểu char. Vì kiểu char ln
chiếm 1 byte, bạn có thể nghĩ các trị này là 0 đến 255. Tuy nhiên, C không
định nghĩa char là unsigned hoặc signed, nên trị cực tiểu là -128 và trị cực
đại là 127.
Số học với char
Chương trình cho thấy trình biên dịch dễ dàng thực hiện các phép tính số
học với kiểu char. Ví dụ:
lower_a + 1
kết quả là 97+1=98. Đây là trị của ký tự “b” thường (nằm nay sau “a”). Phép
tính:
lower_m - 'a' + 'A'
in ra ký tự “M”, sẽ cho kết quả khác trên máy EBCDIC.
%c và %i
Mặc dù kiểu char có thể in với chuỗi định dạng %i, nhưng không phải kiểu
nào cũng làm được như vậy. Ví dụ khơng thể in kiểu int hoặc short với
%li. Chú ý mẫu tự “l” không phải số 1.
9
© Dương Thiên Tứ
www.trainingwithexperts.com
Kiểu nguyên với các cơ số khác
Có thể làm việc với hệ bát phân (cơ số 8) hoặc hệ thập
Số 0 với “x” đặt
lục phân (cơ số 16) Số 0 đặt trình
biên dịch vào
chế độ cơ số 8
trình biên dịch vào
chế độ cơ số 16
#include <stdio.h>
int main(void)
{
int dec = 20, oct = 020, hex = 0x20;
printf("dec=%d, oct=%d, hex=%d\n", dec, oct, hex);
printf("dec=%d, oct=%o, hex=%x\n", dec, oct, hex);
return 0;
dec=20, oct=16, hex=32
dec=20, oct=20, hex=20
}
Kiểu nguyên với cơ số khác
Hệ thập phân,
bát phân và thập
lục phân
C không nhất thiết lúc nào cũng làm việc trong hệ thập phân (cơ số 10 decimal). Nếu cần bạn có thể dùng hệ bát phân (cơ số 8 - octal) hoặc thập
lục phân (cơ số 16 - hex). Bạn cũng có thể tính tốn phối hợp giữa chúng với
nhau.
Chỉ định một hằng số ở hệ 8 được thực hiện bằng cách đặt một số 0 ngay
đầu số đó, Như vậy mặc dù 8 là số hợp lệ chỉ 8 cơ số 10, nhưng 08 là một số
khơng hợp lệ vì khơng có số 8 trong hệ đếm cơ số 8.
Chỉ định một hằng số ở hệ 16 được thực hiện bằng cách đặt một số 0 với “x”
theo sau. Các ký tự “a”, “b”, “c”, “d”, “e”, và “f” dùng thể hiện các số từ 10 đến
15. Chữ hoa hoặc chữ thường cũng được, ví dụ 0x15AE, 0x15aE hoặc
0x15ae đều giống nhau.
%d
Chuỗi định dạng in một số nguyên dưới hệ decimal (giống %i).
%o
Chuỗi định dạng in một số nguyên dưới hệ octal.
%x
Chuỗi định dạng in một số nguyên dưới hệ hexadecimal (dùng abcdef).
%X
Chuỗi định dạng in một số nguyên dưới hệ hexadecimal (dùng ABCDEF).
10
© Dương Thiên Tứ
www.trainingwithexperts.com
Các kiểu số thực trong C
C hỗ trợ nhiều kiểu số thực khác nhau
Cực đại và cực tiểu định nghĩa trong “float.h”
Kiểu
float
double
long double
Dạng thức
%f %e %g
%lf %le %lg
%Lf %Le %Lg
bytes
4
8
10
Cực tiểu
FLT_MIN
DBL_MIN
LDBL_MIN
Cực đại
FLT_MAX
DBL_MAX
LDBL_MAX
Kiểu số thực trong C
float.h
Tập tin tiêu đề (header) chứa các định nghĩa của một vài hằng số liên quan
các kiểu dấu chấm động.
float
Đây là kiểu dữ liệu dấu chấm động bé nhất và kém chính xác nhất của C.
Tính tốn dùng float nhanh nhưng kém chính xác. Trị cực tiểu thơng
-38
+38
thường của float là 10 và trị cực đại thông thường là 10 .
double
Kiểu dữ liệu dấu chấm động có kích thước trung bình của C. Tính tốn dùng
double chậm hơn dùng float nhưng chính xác hơn. Bởi vì có thêm khơng
gian lưu trữ (gấp đôi so với float) nên trị cực tiểu và cực đại của nó cũng
-308
+308
lớn hơn. Thơng thường là 10
đến 10
.
long double
Kiểu dữ liệu dấu chấm động có kích thước lớn nhất của C. Tính tốn dùng
long double rất chậm nhưng rất chính xác. long double cho phép lưu trữ
+4932
những trị rất lớn, có thể lên đến 10
.
C99
C99 chia kiểu số thực thành hai loại:
- real floating type: float, double và long double
- complex type:
float _Complex, double _Complex và long double _Complex
C99 cũng hỗ trợ in số thực dưới dạng hexadecimal, nhưng hiếm dùng.
11
© Dương Thiên Tứ
www.trainingwithexperts.com
Ví dụ về kiểu thực
#include <stdio.h>
#include <float.h>
int main(void)
{
double f = 3.1416, g = 1.2e-5, h = 5000000000.0;
printf("f=%lf\tg=%lf\th=%lf\n", f, g, h);
printf("f=%le\tg=%le\th=%le\n", f, g, h);
printf("f=%lg\tg=%lg\th=%lg\n", f, g, h);
printf("f=%7.2lf\tg=%.2le\th=%.4lg\n", f, g, h);
return 0;
}
f=3.141600
g=0.000012
h=5000000000.000000
f=3.141600e+00
g=1.200000e-05
h=5.000000e+09
f=3.1416
g=1.2e-05
h=5e+09
f=
3.14
g=1.20e-05
h=5e+09
Ví dụ về kiểu thực
%lf
Chuỗi định dạng này chỉ định printf hiển thị độ chính xác 6 số thập phân,
bất chấp kích thước của số. Chú ý mẫu tự “l” không phải số 1.
%le
Chuỗi định dạng này chỉ định printf hiển thị độ chính xác 6 số thập phân,
tuy nhiên số được hiển thị theo kiểu “số mũ” (exponential). Ví dụ 1.200000e-5
05 cho thấy 1.2 phải nhân với 10 .
%lg
Chuỗi định dạng này chỉ định printf hiển thị ở dạng dễ dùng nhất. Chỉ
những số có nghĩa được in ra, các số 0 không ý nghĩa sẽ bị loại bỏ. Số cũng
được in ra ở dạng ngắn nhất có thể. Vì vậy thay vì in 0.000012, chúng ta có
dạng gọn hơn 12e-05.
%7.2lf
Số 7 chỉ định tổng chiều rộng của số, số hai chỉ định số các số phần thập
phân được hiển thị. Vậy số “3.14” chỉ có 4 ký tự mà ta chọn độ rộng là 7, 3 ký
tự dấu cách sẽ được in vào. Việc làm tròn cũng được thực hiện, ví dụ 3.148
sẽ làm trịn thành 3.15. Mặc định trị float được in với 6 ký tự phần thập
phân, bạn có thể chỉ định kích thước phần thập phân được in ra, ví dụ: chỉ in
2 số phần thập phân, dùng “%.2f”.
%.2le
Chỉ định hiển thị 2 số thập phân với dạng “số mũ”.
%.4lg
Chỉ định hiển thị 4 số thập phân với dạng ngắn nhất có thể.
12
© Dương Thiên Tứ
www.trainingwithexperts.com
Các hằng số
Trong C có một số kiểu hằng số
Các hằng số kết thúc bởi “F” là kiểu float: 3.5F, 1e-7F
Các hằng số khơng có “.”, “e” hoặc “F” kiểu int,
10000, -35 (vài trình biên dịch chuyển kiểu long int
nếu hằng số vượt quá kiểu int)
Vài hằng số long int cũng kết thúc bằng “L”:
9000000L
Các hằng số chứa “.” hoặc “e” là kiểu double: 3.5, 1e7, -1.29e15
Các hằng số kết thúc bởi “L” là kiểu long double: 1.29e15L, 1e-7L
Các hằng số
Kiểu của các
hằng số
Khi một biến được khai báo sẽ được cấp một kiểu. Kiểu này định nghĩa kích
thước của biến và cách sử dụng nó. Tương tự khi một hằng số (constant)
được khai báo trình biên dịch cũng cấp cho nó một kiểu. Với biến, kiểu được
khai báo rõ ràng. Với hằng số, kiểu không được khai báo. Việc xác định kiểu
của các hằng số không trực tiếp.
Các luật khi biên dịch được trình bày ở trên. Ví dụ, hằng số “12” sẽ có kiểu
ngun vì không chứa “.”, “e’, hoặc “F” nào để tạo thành kiểu có dấu chấm
động. Nhưng hằng số “12.” lại có kiểu double. “12.L” có kiểu long double,
“12.F” lại có kiểu float. Mặc dù “12.L” có kiểu long double nhưng “12L” lại
có kiểu long int.
13
© Dương Thiên Tứ
www.trainingwithexperts.com
Chú ý!
#include <stdio.h>
Hằng có độ chính xác double
được tạo bởi dấu “.”
int main(void)
{
double f = 5000000000.0;
double g = 5000000000;
printf("f=%lf\n", f);
printf("g=%lf\n", g);
Hằng là integer hoặc long
integer với trị lớn nhất
nhỏ hơn trị khai báo
return 0;
}
f=5000000000.000000
g=705032704.000000
OVERFLOW
Chú ý
Chương trình trên cho thấy một trong những sự cố xảy ra do không hiểu bản
chất của hằng số trong C. Dù chỉ có khác biệt nhỏ ".0" ở cuối của
5000000000, nhưng nếu thiếu nó 5000000000 được xem như kiểu int
(trường hợp trị gán cho “g”). Sự hiện diện của “.” (trường hợp trị gán cho “f”)
làm cho trị trở thành double.
Vấn đề là trị gán lớn hơn trị lớn nhất của int. Trị int sẽ bị tràn và trị tràn này
được gán cho g.
14
© Dương Thiên Tứ
www.trainingwithexperts.com
Đặt tên cho hằng số
Có thể đặt tên cho hằng số bằng từ khoá const
Tạo một
hằng số
kiểu int
#include <stdio.h>
int main(void)
{
const long double pi = 3.141592653590L;
const int days_in_week = 7;
const sunday = 0;
days_in_week = 5;
Lỗi
return 0;
}
Đặt tên cho hằng số
const
Việc phân biệt kiểu bằng “e”, “F”, ... cho hằng số quá tùy tiện, vì vậy C hỗ trợ
từ khóa const được dùng để tạo hằng số với kiểu.
Dùng const, các hằng số sẽ có kiểu rõ ràng, ngoại trừ const sunday ở trên
sẽ có kiểu int theo mặc định. Để phù hợp với luật này, thay cho short int
là short, thay cho long int là long.
Lvalues và
Rvalues
Khi các hằng số được tạo ra, nó trở thành một rvalue, tức nó chỉ có thể xuất
hiện bên phải dấu “=”. Các biến thơng thường là lvalue, tức nó có thể xuất
hiện bên trái dấu “=”. Phát biểu:
days_in_week = 5;
sẽ nhận được lỗi biên dịch lvalue.
15
© Dương Thiên Tứ
www.trainingwithexperts.com
Các hằng số xử lý trước
Có thể đặt tên cho hằng số bằng bộ tiền xử lý
- Dùng chỉ thị #define để vào chế độ “tìm – thay thế”
- Các hằng số thường dùng toàn bằng chữ hoa
Tìm “PI”, thay thế bằng
3.141592653590L
Chú ý:
Khơng có dấu “=”
Khơng có dấu “;”
#include <stdio.h>
#define PI 3.141592653590L
#define DAYS_IN_WEEK 7
#define SUNDAY 0
int day = SUNDAY;
long flag = USE_API;
Khơng nên có “PI” ở đây
Các hằng số xử lý trước
Bộ tiền xử lý là một đặc điểm của C. Nó khơng phải là một bộ soạn thảo tương
tác mà là bộ xử lý mã nguồn trước khi biên dịch. Trình biên dịch khơng làm
việc trực tiếp với mã nguồn mà chỉ nhận kết xuất từ bộ tiền xử lý. Ví dụ như
chỉ thị #include sẽ được bộ tiền xử lý thay thế bằng cách chèn tên của tập tin
tiêu đề tương ứng vào giúp trình biên dịch nhìn thấy nó.
Bộ tiền xử lý có khả năng tìm và thay thế. Để thực hiện chế độ này chỉ thị
#define được dùng. Cú pháp đơn giản như sau:
#define text_tìm text_thay_thế
Chỉ có chính xác từ cần tìm mới được thay thế. Các từ cần tìm nhưng nằm
trong chuỗi (bao bởi cặp “”) sẽ được bỏ qua.
16
© Dương Thiên Tứ
www.trainingwithexperts.com
Cẩn thận với printf và scanf
#include <stdio.h>
“a” có kích thước 2 byte,
“%c” chỉ chứa 1 byte
của “a”
“%f” dành cho trị float 4
byte theo dạng thức IEEE,
trong khi b chỉ có 2 byte và
khơng theo dạng thức IEEE
int main(void)
{
short a = 256, b = 10;
printf("Nhap mot so: ");
scanf("%c", &a);
printf("a = %hi, b = %f\n", a, b);
return 0;
}
Nhap mot so: 1
a = 305, b = Floating support not loaded
Chú ý
Sai đặc tả định
dạng
Một trong những lỗi phổ biến là sử dụng sai đặc tả định dạng (format
specifier) khi dùng các hàm printf và scanf. Trình biên dịch C khơng kiểm
tra lỗi này. Đặc tả định dạng phải cùng kiểu với trị sẽ đặt tại nó.
Chương trình trên thử dùng đặc tả định dạng “%c” (char 1 byte) để nhận một
trị short 2 byte. Biến kiểu short 2 byte “a” khởi tạo với trị 256, dạng nhị phân
như sau:
0000 0001 0000 0000
Người dùng nhập 1 tại dấu nhắc, thực chất là nhập mã ASCII của ký tự 1 có
trị 49, dạng nhị phân như sau:
0011 0001
Do dùng đặc tả định dạng “%c” nên trị này được lưu vào byte thấp của a, kết
quả là:
0000 0001 0011 0001
đây là dạng nhị phân của 305.
17
© Dương Thiên Tứ
www.trainingwithexperts.com
Toán tử trong C
Các toán tử trong C
Các toán tử số học
Toán tử ép kiểu (cast)
Tăng và giảm
Các toán tử thao tác trên bit (bitwise)
Các toán tử so sánh
Các toán tử gán
Toán tử sizeof
Toán tử biểu thức điều kiện
Các toán tử trong C
Mục tiêu của chương này là giới thiệu lần lượt các toán tử khác nhau có trong
C. Các tốn tử liên quan đến con trỏ, mảng và cấu trúc sẽ được trình bày
trong các chương sau.
18
© Dương Thiên Tứ
www.trainingwithexperts.com
Các toán tử Số học
C hỗ trợ các toán tử số học sau:
+
phép cộng
phép trừ
*
phép nhân
/
phép chia
%
phép lấy modulo (phần dư)
“%” không thể dùng với số thực
Các toán tử Số học
+, -, *, /
C cung cấp các tốn tử tốn học thơng thường. Chú ý là các toán tử “+” và “-”
cũng được dùng như toán tử một ngơi như sau:
x = +y
hoặc
x = -y
Dịng đầu chỉ làm mất thời gian, nó chính là “x = y”. Dòng thứ hai trước hết
nhân trị của “y”cho -1 rồi gán cho “x”.
%
C cung cấp phép lấy modulo, hay toán tử “lấy phần dư của phép chia”. Ví dụ,
25/4 bằng 6, 25%4 bằng 1. Phép tính này chỉ có ý nghĩa với phép chia số
nguyên, cho ra phần dư.
Với phép chia số thực, không cho ra phần dư mà cho ra một phân số. Vì vậy
phép tính này khơng áp dụng cho số thực. Bạn dùng hàm fmod để thay thế.
19
© Dương Thiên Tứ
www.trainingwithexperts.com
Dùng các tốn tử số học
Trình biên dịch dùng kiểu của các toán hạng để xác
định cách tính tốn nào sẽ được thực hiện
“i” và “j” kiểu int,
thực hiện phép chia
int, 1 được gán cho k
int main(void) {
int i = 5, j = 4, k;
double f = 5.0, g = 4.0, h;
“f” và “g” kiểu double,
thực hiện phép chia
double, 1.25 được gán
cho k
thực hiện phép chia
int, bất chấp “h” kiểu
double. Trị được gán
là 1.000000
k = i / j;
h = f / g;
h = i / j;
return 0;
}
Dùng các toán tử số học
Khi các hằng số và các biến số của những kiểu dữ liệu khác nhau được trộn
lẫn trong một biểu thức, tất cả sẽ được chuyển đổi thành một kiểu dữ liệu
chung. Đó là kiểu dữ liệu của tốn hạng có kích thước lớn nhất. Điều này
được gọi là nâng kiểu. Chú ý là trình biên dịch chỉ quan tâm đến kiểu dữ liệu
của các toán hạng.
20
© Dương Thiên Tứ
www.trainingwithexperts.com
Toán tử ép kiểu
Toán tử ép kiểu (Cast) tạm thời thay đổi kiểu của một
biến
một toán hạng ép thành
kiểu double, toán hạng
kia sẽ tự chuyển thành
kiểu double
Phép chia nguyên được
thực hiện, kết quả 1, sẽ
được ép thành kiểu
double 1.00000
int main(void)
{
int i = 5, j = 4;
double f;
f = (double)i / j;
f = i / (double)j;
f = (double)i / (double)j;
f = (double)(i / j);
return 0;
}
Toán tử ép kiểu
Thường gặp vấn đề như sau:
f = i / j;
trình biên dịch thực hiện phép chia nguyên cho kết quả là một số nguyên rồi
gán trị nguyên đó cho một biến số thực.
Tuy nhiên trình biên dịch cho phép “thay đổi suy nghĩ” về kiểu của biến hay
biểu thức. Điều này được thực hiện nhờ toán tử ép kiểu (cast). Toán tử ép kiểu
tạm thời thay đổi kiểu của biến / biểu thức mà nó tác động.
( type_name ) expression
Như vậy:
f = i / j;
Phép chia int được thực hiện bình thường. Còn:
f = (double)i / j;
kiểu của “i” được thay đổi tạm thời thành kiểu double (nghĩa là 5 thành 5.0).
Bây giờ trình biên dịch hiểu là chia một số double cho một số nguyên. Nó tự
động nâng cấp biến nguyên “j” thành kiểu double (thành 4.0) và thực hiện
phép chia 2 số double, cho kết quả là 1.25 và gán cho f.
21
© Dương Thiên Tứ
www.trainingwithexperts.com
Tăng 1 và Giảm 1
C có hai toán tử để tăng 1 và giảm 1 trị của 1 biến
++
tăng 1 (increment)
-giảm 1 (decrement)
Có hai cách dùng các toán tử trên: tiền tố (đặt trước
biến) và hậu tố (đặt sau biến)
int i = 5, j = 4;
“i” trở thành 6
“j” trở thành 3
i++;
--j;
++i;
“i” trở thành 7
Tăng 1 và Giảm 1
C có hai tốn tử đặc biệt, chun mơn, cho phép tăng 1 (Increment) hoặc
giảm 1 (Decrement) trị của một biến.
Điều này ánh xạ trực tiếp hoạt động của hợp ngữ bên dưới. Mọi máy đều hỗ
trợ chỉ thị “inc” để tăng trị tại một vị trí trong bộ nhớ lên 1 đơn vị. Tương tự mọi
máy đều hỗ trợ chỉ thị “dec” để giảm trị tại một vị trí trong bộ nhớ xuống 1. C
đã ánh xạ trực tiếp các chỉ thị này.
Cần chú ý khi dùng các tốn tử tăng 1 và giảm 1, chúng có thể gây hiệu ứng
lề (side effect) không mong muốn nếu dùng khơng chính xác trong các biểu
thức phức tạp.
Ví dụ: (i > 0 && ++j > 0)
nếu i > 0 là false thì ++j > 0 khơng được định trị, nghĩa là j không tăng.
Viết đúng: (++j > 0 && i > 0) hoặc tăng j trong phát biểu riêng.
22
© Dương Thiên Tứ
www.trainingwithexperts.com
Tiền tố và Hậu tố
Tính tốn Tiền tố (Prefix) và Hậu tố (Postfix) khác nhau
#include <stdio.h>
int main(void)
{
int i, j = 5;
i = ++j;
printf("i=%d, j=%d\n", i, j);
j = 5;
i = j++;
printf("i=%d, j=%d\n", i, j);
return 0;
}
i=6, j=6
i=5, j=6
tương đương:
1. j = j + 1;
2. i = j;
tương đương:
1. reg = j;
2. j = j + 1;
3. i = reg;
Tiền tố và Hậu tố
Có hai phiên bản của các tốn tử ++ và --, phiên bản tiền tố (prefix) và hậu tố
(postfix). Cả hai đều cộng thêm 1 hoặc trừ đi 1 vào tốn hạng, tuy nhiên phép
gán có khác nhau.
Prefix ++, --
Khi một toán tử tiền tố được dùng, việc tăng hay giảm toán hạng được thực
hiện đầu tiên, sau đó mới tiến hành gán trị đã thay đổi. Như vậy:
i = ++j;
Trị hiện tại của “j” là 5 sẽ được tăng thành 6, rồi sau đó mới được gán nhờ
toán tử “=” vào biến “i”
Postfix ++, --
Khi một toán tử hậu tố được dùng, trước tiên trị của toán hạng được lưu vào
một thanh ghi (reg), toán hạng được tăng hay giảm, sau đó trị từ thanh ghi
được gán.
Điều này gây cảm giác là phép gán trị chưa thay đổi được thực hiện trước,
sau đó mới thực hiện tăng hay giảm toán hạng.
i = j++;
Ta cảm thấy, trị hiện tại của “j” là 5 sẽ được gán nhờ tốn tử “=” vào biến “i”.
Sau đó trị “j” mới tăng thành 6.
Các thanh ghi
Trong hai trường hợp trên C dùng hoặc không dùng thanh ghi tạm để lưu trị.
Trong trường hợp prefix, “i = ++j”, phép tăng được thực hiện và trị được
chuyển. Trong trường hợp postfix, “i = j++”, C nạp trị hiện tại (tức “5”) vào
thanh ghi, phép tăng sẽ được thực hiện (thành “6”), sau đó C lấy trị lưu (“5”)
chuyển cho “i”. Như vậy phép tăng được thực hiện trước phép gán.
23
© Dương Thiên Tứ
www.trainingwithexperts.com
Đúng-Sai trong C
Để hiểu các toán tử so sánh (>, <, ...), các toán tử logic
(and, or, not, ...) phải hiểu cách C thể hiện Đúng-Sai
C không có kiểu dữ liệu luận lý (boolean). C dùng
integer để thay thế
Trị khác không, 1, -1, 0.3, -20.8 là true
Trị bằng 0 (hay 0.0) là false
if (32)
printf("Dong nay luon duoc in\n");
if (0)
printf("Dong nay khong bao gio in\n");
Đúng-Sai trong C
C có cách tiếp cận dễ hiểu để biết thế nào là true hoặc false.
True
Trị bất kỳ khác không là true. Như vậy, 1 và -5 đều là true, bởi vì cả hai đều
khác khơng. Tương tự 0.01 cũng là true vì nó cũng khác khơng.
False
Trị bất kỳ bằng 0 là false. Như vậy, 0, +0, -0, 0.0, 0.00 đều là false.
Kiểm tra
đúng sai
Như vậy, bạn có thể hình dung việc kiểm tra đúng sai là một tác vụ dễ dàng
trong C. Nạp trị cần kiểm tra vào thanh ghi và xem xét các bit của nó. Nếu chỉ
một bit được đặt (bằng 1) thì trị được chỉ định true. Nếu khơng có bit nào được
đặt, trị được chỉ định là false.
Ví dụ trên giới thiệu cho bạn làm quen phát biểu if trước khi học kỹ về nó sau
này. Dạng đơn giản của if như sau:
if (điều_kiện)
phát_biểu_sẽ_thực_hiện_nếu_điều_kiện_là_true;
C99
C99 cung cấp những hỗ trợ mới cho Đúng-Sai:
- Kiểu _Bool, là trị unsigned int, chỉ gán được trị 0 hoặc 1.
- Tập tin tiêu đề mới <stdbool.h>, khai báo bool, true và false, tiện dụng
hơn kiểu _Bool.
24