Thông báo

Icon
Error

Kiểu Và Khai Báo Biến Trong C.
thanhan3892 Offline
#1 Đã gửi : 04/10/2011 lúc 11:20:33(UTC)
Danh hiệu: Ma mới

Nhóm: Registered
Gia nhập: 14-09-2011(UTC)
Bài viết: 5
Đến từ: dhtonducthang

Ngôn ngữ lập trình C là một ngôn ngữ mệnh lệnh được phát triển từ đầu thập niên 1970 bởi Ken ThompsonDennis Ritchie để dùng trong hệ điều hành UNIX. Từ dó, ngôn ngữ này đã lan rộng ra nhiều hệ điều hành khác và trở thành một những ngôn ngữ phổ dụng nhất. C là ngôn ngữ rất có hiệu quả và được ưa chuộng nhất để viết các phần mềm hệ thống, mặc dù nó cũng được dùng cho việc viết các ứng dụng. Ngoài ra, C cũng thường được dùng làm phương tiện giảng dạy trong khoa học máy tính mặc dù ngôn ngữ này không dược thiết kế dành cho người nhập môn.

Những phát triển ban đầu

Phát triển khởi đầu của C xảy ra ở AT&T Bell Labs giữa 19691973; theo Ritchie thì thời gian sáng tạo nhất là vào năm 1972. Nó được đặt tên là C vì nhiều đặc tính của nó rút ra từ một ngôn ngữ trước đó là B.

Thêm vào đó, các điểm khác với ngôn ngữ nguyên thủy "B": Ken Thompson kể tới ngôn ngữ lập trình BCPL, nhưng ông ta cũng đã tạo ra ngôn ngữ là Bon để vinh danh vợ mình.

Có nhiều truyền thuyết về nguồn gốc của C và hệ điều hành liên quan tới nó là Unix bao gồm:

  • Sự phát triển của C là kết quả của các lập trình viên đã muốn chơi Space Travel. Họ đã chơi nó trên mainframe của hãng làm việc, nhưng bị thiếu khả năng (chạy) và phải hỗ trợ khoảng 100 người dùng, Thompson và Ritchie tìm thấy rằng họ đã không có đủ sự kiểm soát tàu vũ trụ (của trò chơi) để tránh được các va chạm khỏi sự chuyển dịch của các thiên thạch. Do đó, họ quyết định để xuất trò chơi này sang một máy PDP-7 để không trong văn phòng. Nhưng nó lại không có hệ điều hành; do đó, họ viết một hệ điều hành. Tiếp tục, họ quyết định để xuất hệ điều hành này sang PDP-11 của văn phòng nhưng việc này thật khó vì tất cả mã đều là ngôn ngữ Assembly. Họ quyết dịnh dùng một ngôn ngữ dễ xuất cấp cao để hệ điều hành có thể xuất được dễ dàng từ máy tính này sang máy khác. Họ đã tìm đến ngôn ngữ B, nhưng nó lại thiếu các chức năng để khai thác một số khả năng của PDP-11. Vậy nên họ đã sáng tạo ra một ngôn ngữ mới là C.
  • Unix nguyên đã được phát triển để tạo ra một hệ thống tự động lập hồ sơ cho các bằng phát minh. Phiên bản đầu tiên của Unix đã phát triển từ ngôn ngữ Assembly. Sau đó, ngôn ngữ C đã được phát triển để từ đó thay thế hệ điều hành mới.

Cho đến 1973, C đã trở nên đủ mạnh để dùng viết nhân cho Unix, thay vì trước nó chúng được viết bằng Assembly trong các máy PDP-11/20. Đây là lần đầu tiên mà nhân của một hệ điều hành được lắp thành bằng một ngôn ngữ khác hơn Assembly.

Ngôn ngữ lập trình C có một hệ thống mở rộng cho việc khai báo các biến của các kiểu khác nhau. Những qui tắc dành cho các kiểu phức tạp có thể gây nhầm lẫn tùy theo các kiểu thiết kế của chúng. Bài này nói về các khai báo biến, bắt đầu từ các kiểu đơn giản, và dẫn tới các kiểu phức tạp hơn.

K&R C

Năm 1978, Ritchie và Brian Kernighan xuất bản lần đầu cuốn The C Programming Language. Sách này được những người lập trình biết tới như là "K&R", được dùng trong nghiều năm như là một đặc tả không chính thức của C. Phiên bản C mà cuốn sách đó đề cập thường được gọi là "K&R C". (Lần xuất bản thứ hai của cuốn này cũng bao gồm chuẩn ANSI C).

K&R giới thiệu các chức năng sau đây:

  • Kiểu dữ liệu struct
  • Kiểu dữ liệu long int
  • Kiểu dữ liệu unsigned int
  • Toán tử =+ đã được đổi thành +=, và tương tự cho các toán tử khác để tránh gây hiểu nhầm cho bộ phân tích từ vựng của trình dịch C. (Thí dụ: sự giống nhau dể lầm lẫn của hai câu lệnh i =+ 10 và i = +10).

K&R C thường được xem là phần cơ bản nhất của ngôn ngữ mà nó cần phải có cho một trình dịch C. Trong nhiều năm, ngay cả sau khi ANSI C được giới thiệu, nó đã được xem như là "mẫu số chung nhỏ nhất" mà người lập trình C phải bám lấy nếu muốn có được khả năng dịch chuyển (tái dụng trên nhiều máy) bởi vì không phải mọi trình dịch đều hỗ trợ toàn bộ ANSI C, và một cách hợp lý là mã viết trong K&R C cũng là mã hợp lệ trong ANSI C.

Trong các phiên bản trước đây của C, chỉ có những hàm nào trả về một số khác số nguyên mới cần được khai báo trước khi dùng. Một hàm dùng mà không có bất kì sự khai báo nào trước đó được giả thiết là sẽ trả về một số nguyên.

Thí dụ việc gọi với yêu cầu của sự khai báo trước:

  1.  long int SomeFunction();
    
  2.  
    
  3.  int CallingFunction()
    
  4.  {
    
  5.      long int ret;
    
  6.      ret = SomeFunction();
    
  7.  }
    

Thí dụ việc gọi mà không cần phải khai báo trước:

  1.  int CallingFunction()
    
  2.  {
    
  3.      int ret;
    
  4.      ret = SomeOtherFunction();
    
  5.  }
    
  6.  
    
  7.  int SomeOtherFunction()
    
  8.  {
    
  9.      return 0;
    
  10.  }
    

Bởi vì nguyên mẫu của K&R đã không bao gồm bất kì thông tin nào về các tham số của hàm, chức năng kiểm tra kiểu của các đối số đã không được tiến hành, mặc dù một số trình dịch sẽ cho ra thông báo cảnh cáo nếu một hàm đã được gọi với số lượng tham số không đúng.

Trong nhiều năm tiếp theo của sự tái bản K&R C, nhiều chức năng "không chính thức" đã được thêm vào cho ngôn ngữ, được hỗ trợ bởi các trình dịch của AT&T và một số nơi khác. Trong đó bao gồm:

  • Các hàm có kiểu void và dữ liệu có kiểu void *.
  • Các hàm trả về các kiểu struct hay union.
  • Tên của các miền trong một không gian tên cho mỗi kiểu struct.
  • Phép gán cho kiểu dữ liệu struct.
  • Hằng const được xem là đối tượng chỉ cho phép đọc.
  • Một thư viện chuẩn được sự hợp tác để xây dựng bởi nhiều nhà sản xuất.
  • Các kiểu enumeration.
  • Kiểu chính xác đơn float.

Quan hệ với C++

C++ nguyên là sự kết thừa từ C. Mặc dù vậy, không phải mọi chương trình trong C đều hợp lệ trong C++. Vì là hai ngôn ngữ độc lập, số lượng không tương thích giữa hai ngôn ngữ này đã tăng lên. [2]. Phiên bản cuối cùng C99 đã tạo ra thêm nhiều tính năng xung đột (giữa C và C++). Các sự khác nhau này tạo ra khó khăn để viết các chương trình và thư viện đẻ có thể được dịch và hoạt động chính xác trong cả hai loại mã C hay C++, đồng thời gây nhầm lẫn cho những người lập trình dùng cả hai ngôn ngữ này. Sự chênh lệch này cũng gây khó khăn cho ngôn ngữ này có thể tiếp thu các tính năng của ngôn ngữ kia.

Bjarne Stroustrup, cha đẻ của C++ đã lập đi lập lại rằng [3]: Các tính chất không tương thích giữa C và C++ nên được hạ thấp càng nhiều càng tốt để mở rộng tối đa khả năng hoạt động thông suốt của hai ngôn ngữ này. Một số người tranh biện rằng vì C và C++ là hai ngôn ngữ khác nhau, sự tương thích giữ chúng thì hữu ích nhưng không phải có tính sống còn, theo lập trường này, nỗ lực để giảm sự không tương thích không được phá hủy cố gắng để nâng cao mỗi ngôn ngữ đứng riêng.

Ngày nay, những khác nhau căn bản, không kể các mở rộng thêm vào của C++ như là các lớp, các tiêu bản, các không gian tên, và quá tải, giữa hai ngôn ngữ là:

  • inline — các hàm inline có giá trị toàn cục trong C++ và chỉ có giá trị trong phạm vi tập tin trong C.
  • Từ khóa bool trong C99 thì có riêng tập tin tiêu dề của nó là <stdbool.h>. Các chuẩn C trước đây đã không định nghĩa kiểu boolean và nhiều phương pháp không tương thích đã được dùng để mô phỏng kiểu boolean.
  • Các hằng kí tự (được đặt trong dấu ') có độ lớn của một int trong C và có độ lớn của một char trong C++. Mặc dù vậy, ngay cả trong C các hàng này sẽ không bao giờ vượt quá giá trị của một char, cho nên việc chuyển đổi kiểu (char)'a' thì hoàn toàn an toàn.
  • Nhừng từ khóa mới thêm vào trong C++ sẽ không thể dược dùng làm các tên trong C như trước đây nữa. (Thí dụ: try, catch, template, new, delete, ...).
  • Trong C++, trình dịch tự dộng tạo một "thẻ" cho mỗi struct, union hay enum, do vậy, struct S {}; trong C++ tương đương với typedef struct S {} S; trong C.

C99 tiếp thu một số tính năng mà xuất hiện đầu tiên trong C++. Trong số đó là:

  • Bắt cuộc khai báo nguyên mẫu của hàm.
  • Thêm từ khóa inline.
  • Hủy bỏ "hiểu ngầm" của sự trả về sẽ có kiểu int.

 

Ngôn ngữ trung gian

C được dùng như là một ngôn ngữ trung gian vì nó có thể xuất thành dạng tập tin object hay ngôn ngữ máy. Việc này giúp C trở nên dễ vận chuyển hay dễ tối ưu hóa. Các trình dịch C thường có sẵn cho nhiều loại CPU và các hệ điều hành và hầu hết những trình dịch đó cho ra được tập tin *.obj cũng như ngôn ngữ máy có tối ưu hóa. Do đó, các đầu ra của mã nguồn C đột nhiên trở nên rất là dễ vận chuyển, và có khả năng dùng trong dạng *.obj hay mã máy được tối ưu hóa. Dầu sao thì C được thiết kế như là một ngôn ngữ lập trình, nó không phải là lý tưởng cho việc dùng như là một ngôn ngữ trung gian. Điều này dẫn tới việc phát triển các ngôn ngữ trung gian lấy C làm cơ sở, như là một C--.

Các trình dịch quan trọng

Những trình dịch về C ngày nay thương được cung cấp kèm chung với C++ và ngay cả trình dịch cho ngôn ngữ Assmebly. Những sản phẩm trình dịch được bán phổ biến trên thị trường cũng thường cung cấp thêm nhiều công cụ trợ giúp cho người lập trình như là IDE, debuger, ...

Sau đây là danh sách một số trình dịch phổ biến:

  • GCC trình dịch hoàn toàn miễn phí của theo giấy phép GNU toàn bộ gói sẽ bao gồm trình dịch của nhiều ngôn ngữ điển hình là C/C++ và Fortran. Đây là trình dịch chính dùng cho các hệ diều hành Linux. Nó hỗ trợ hầu hết các tiêu chuẩn C/C++. Tuy nhiên vì là miễn phí nên nó không cung cấp các phương tiện đồ họa hỗ trợ cho việc sửa lỗi và viết mã mặc dù nó cũng có các công cụ để giúp phát hiện lỗi rất mạnh như gdb.
  • Borland C/C++, trình dịch này ngày nay đã đổi tên thành Borland Builder và bị giảm sút thị phần rất nhiều nhưng đây là trình dịch có hỗ trợ chuẩn C98.
  • Microsoft C/C++, đây là trình dịch chỉ được dùng chủ yếu để phát triển tcác phần mềm trên các hệ Windows. Trình dịch này rất mạnh về các hỗ trợ đồ họa cũng như các công cụ để phát triển và sản xuất phần mềm. Rất tiếc, trình dịch này không hoàn toàn tương thích với các chuẩn. Để có mã nguồn theo chuẩn thì người lập trình phải cài đặt lại một số thông số mặc định. Một điểm yếu của trình dịch này là nó không hỗ trợ cho các hệ điều hành nào không do Microsoft sản xuất.
  • Ngoài ra, còn rất nhiều trình dịch khác ở mức độ ít phổ biến hơn như là trình dịch C/C++ của Intel, Bell Labs,...

Kiểu cơ bản

Có 4 kiểu cơ bản của các biến trong C; đó là: char, int, double và float.

Tên kiểu Ý nghĩa
char Đơn vị cơ bản nhất có thể địa chỉ hóa được; nó là một byte. Đây là một kiểu nguyên.
int Loại số nguyên theo kích cỡ tự nhiên nhất của các máy tính. Thông thường nó có thể lấy trọn một khoảng có thể địa chỉ hoá được của một word với độ lớn biến thiên từ 16, 32, hay 64 bit tùy theo kiến trúc của CPUhệ điều hành.
float Một giá trị dấu chấm động có độ chính xác đơn.
double Một giá trị dấu chấm động có độ chính xác kép.

 

Dấu

Một kiểu được gọi là có dấu nếu kiểu nguyên đó có thể chứa các số âm. Ngược lại các kiểu cơ bản nào không chấp nhận các số âm là kiểu không dấu.

Có hai kiểu nguyên là char và int có thể có dấu âm hay không. Theo mặc định thì mọi kiểu int là có dấu (nghĩa là chúng chấp nhận các số âm). Để dùng dưới dạng không có dấu (tức là kiểu nguyên chỉ chấp nhận các sô không âm) thì từ khoá unsigned phải được dùng. Ngoài ra, thay vì khai báo đầy đủ trong dạng unsigned int, người ta có thể lược bỏ bớt từ khóa int (và nó được xem như hiểu ngầm -- điều này chỉ dùng được cho kiểu int mà thôi). Như vậy hai khai báo sau đây hoàn toàn tương đương:

Đặc tả của C không xác định rõ ràng là kiểu char sẽ là loại có dấu hay không dấu; khi đó, dấu của kiểu này tùy thuộc vào quy định của nhà phát hành trình dịch. Như vậy, một cách để giảm sai sót khi làm việc trên nhiều loại trình dịch C khác nhau là khai báo rõ ràng bằng các định tính signed hay unsigned nếu dùng kiểu char để tính toán trên các con số. (Dầu sao, nó thực sự sẽ không quá quan trọng nếu dùng kiểu char như là kiểu "ký tự".)

unsigned char grey;
signed char white;

Tiêu chuẩn chung yêu cầu char, signed char, unsigned char là các kiểu khác nhau. Ngoài ra, các hàm chuẩn về dãy các kí tự sử dụng các con trỏ chỉ tới kiểu char (không có định tính), nhiều trình dịch C sẽ bắt lỗi (hay cảnh cáo) nếu các kiểu kí tự khác được dùng như là dãy kí tự được chuyển vào các hàm này.

Kích cỡ

Trong phần này hai cụm từ "chiếm" và "có độ dài" đều có nghĩa là "phần bộ nhớ cần thiết để dành cho một biến"; biến này có kiểu được miêu tả tùy theo chi tiết của bài viết.

Kiểu int cũng có các định tính về kích cỡ để đặc biệt hóa tầm rộng của giá trị mà kiểu này cho phép (tương ứng với đó là việc thay đổi phần bộ nhớ dùng để chứa các số có kiểu này).

short int yellow;
long int orange;

Tương tự như đã đề cập trong phần trước, người ta có thể bỏ không viết từ khóa int trong các kiểu mà đầy đủ phải viết là short int or long int. Thí dụ hai khai báo sau đây là tương đương.

long int brown;
long brown;

Có một số nhầm lẫn trong giới hiểu biết về C như là các kiểu nguyên có độ lớn bao nhiêu. Trong tiêu chuẩn thì không chỉ một cách rõ ràng việc này:

  • Kiểu short int không thể lớn hơn kiểu int.
  • Kiểu int không thể lớn hơn long int.
  • Kiểu short int phải dài ít nhất 16 bit.
  • Kiểu long int phải dài ít nhất 32 bit.

Trong tiêu chuẩn đã không đòi hỏi gì về các kích cỡ nêu trên và những khác nhau cần thiết. (Nghĩa là hoàn toàn hợp lệ nếu cả ba kiểu đều dài 64 bit!) Để có được một miêu tả chính xác và đơn giản của các kiểu, một máy tính người ta áp dụng vào trong mỗi kiểu (cũng như là kích cỡ của một kiểu con trỏ; xem phần đưới đây) một loại lược đồ đã được tạo ra; (xem :64-Bit Programming Models). Hai lược đồ được biết nhiều nhất là

  • ILP32, trong đó int, long int và các kiểu con trỏ chiếm 32 bit.
  • LP64, trong đó, long int và con trỏ mỗi loại chiếm 64 bit, còn int có độ dài 32 bit.

Hầu hết các trình dịch dùng các lược đồ trên dùng 16 bit cho kiểu short int.

Một biến double có thể là một long double, mà trình dịch có thể sử dụng thay cho một kiểu double thuần túy. Tương tự tình huống trước, chuẩn C không hề nêu rõ các kích cỡ tương đối giữa các giá trị dấu chấm động, mà chỉ đòi hỏi float không được lớn hơn long double về kích cỡ.

Từ khóa định tính const cho các kiểu

Để giúp tăng cường độ an toàn trong các chương trình, các giá trị có thể được đánh dấu là các hằng bằng từ khóa định tính const. Với từ khóa này thì một biến khai báo trở thành một hằng. Mọi thao tác do vô ý hay cố ý để điều chỉnh giá trị của nó sẽ bị báo lỗi bởi hầu hết các trình dịch. Bởi vì sau khi đã dùng từ khóa định tính const thì các giá trị của biến không thể thay đổi nữa nên người lập trình phải gán giá trị ban đầu ngay lúc khai báo.

Chuẩn C cho phép hoán đổi vị trí của các hiệu chính. Thí dụ cả hai khai báo hằng sau đây là tương đương

int const black = 12;
const int black = 12;

Cách khai báo đầu thường phản ánh cách dùng const trong cách dùng kiểu con trỏ trong khi cách thứ nhì lại tự nhiên hơn và phổ dụng hơn.

Con trỏ

Một biến có thể được khai báo như là một con trỏ chỉ đến các giá trị có kiểu nào đó, với ý nghĩa của dùng từ khóa định tính *. Để khai báo chỉ việc viết thêm ngay trước tên biến một dấu sao:

char *square;
long *circle;

Lưu ý: Nếu dùng nhiều hơn một dấu sao thì sẽ tạo nên dạng các con trỏ đứng trước chỉ vào con trỏ đứng sau và con trỏ cuối cùng mới chỉ đến địa chỉ của giá trị biến.

Trong cuốn "The C Programming Language" (Ngôn ngữ lập trình C) có cho một giải thích tường tận về việc "hơi kì cục" khi dùng dấu sao trước tên của biến, trong khi dường như việc dùng dấu sao này đứng trước tên của kiểu thì có vẻ "hợp lý" hơn. Đó chính là việc tham chiếu ngược con trỏ, nó có kiểu của đối tượng mà nó chỉ tới. Trong thí dụ trên, *circle là một giá trị của kiểu long. Trong khi điều này khó thấy rõ trong thí dụ trên, thì nó lại cho thấy ưu điểm nếu dùng trong các kiểu phức hợp. Đây là lí do tại sao C "hơi kì cục" trong cách khai báo các kiểu phức hợp, lúc đó, tên của biến sẽ không còn rõ ràng trong khi khai báo kiểu như các thí dụ sẽ nêu trong phần tiếp sau đây.

Có một kiểu đặc biệt của giá trị mà không thể dùng được trực tiếp như là biến có kiểu, nhưng lại có thể chỉ đến nó nếu khai báo con trỏ.

void *triangle;

Giá trị được chỉ tới ở đây không thể dùng trực tiếp được; mọi cố gắng để tham chiếu ngược con trỏ này sẽ dẫn tới một lỗi. Sự tiện lợi ở đây là vì nó là một con trỏ "tổng quát"; nó hữu dụng khi làm việc trên dữ liệu mà kiểu được chỉ tới là không giữ vai trò gì quan trọng. Đơn giản chỉ cần cái địa chỉ con trỏ. Nó thường được ứng dụng để chứa các con trỏ trong các kiểu để làm tiện ích như là danh sách liên kết, bảng băm (hash). Khi nào cần thì tiện ích sẽ đổi kiểu (typecast) thành con trỏ có kiểu cần dùng.

Sau đây là thí dụ về các khai báo con trỏ hợp lệ:

long int *rectangle;
unsigned short int *rhombus;
const char *kite;

Lưu ý đặc biệt về việc dùng const trong trường hợp cuối cùng: ở đây kite là một con trỏ không phải là hằng chỉ tới một const char (tức là nó chỉ tới là một hằng có kiểu kí tự). Giá trị của kite tự nó không phải là hằng, chỉ có giá trị của char mà nó chỉ tới là một hằng.

Vị trí của từ khoá const đặt sau kiểu sẽ cho một cách thức để khai báo hằng con trỏ. Và như là một hằng, nó phải được gán giá trị khởi động khi khai báo:

char * const pentagon = &some_char;

Ở đây, pentagon là một hằng con trỏ, mà nó chỉ tới một char. Giá trị mà nó chỉ tới lại không là một hằng; và sẽ không gây lỗi khi thay đổi kí tự được nó chỉ tới. Chỉ khi nào thay đổi chính con trỏ này thì sẽ gây lỗi (vì đã khai báo nó là hằng). Cũng có thể khai báo cả hai: con trỏ và giá trị mà nó chỉ tới đều là hằng. Có hai cách tương đương nếu muốn khai báo như vậy là:

char const * const hexagon = &some_char; const char * const hexagon = &some_char;

Con trỏ chỉ tới con trỏ

Vì lý do một khai báo chẳng hạn như char * tự nó là một kiểu, nên một biến con trỏ có thể được khai báo để nó chỉ vào các giá trị có kiểu như vây. Nói gọn hơn, chúng là con trỏ chỉ tới các con trỏ. Thí dụ:

char **septagon;

Như đã đề cập phần trên các từ khóa định tính const có thể áp dụng vào chẳng hạn:

unsigned long const int * const *octogon;

Dòng trên khai báo octogon là một con trỏ chỉ tới một hằng con trỏ, và hằng con trỏ này trở lại chỉ tới một hằng số nguyên dạng unsigned long. Các kiểu con trỏ có thể lồng nhau, nhưng chúng càng trở nên khó khăn để nghĩ tới việc sử dụng khi mà càng nhiều cấp độ của sự gián tiếp tham gia vào. Mọi mã dùng nhiều hơn hai cấp độ của con trỏ có thể sẽ cần tới một sự thiết kế, dạng struct các con trỏ.

 


Mảng

Đối với nhiều người lập trình, trong hầu hết các ngôn ngữ tương tự C, kiểu của một mảng nằm trong số phần tử mà nó chứa. Do vậy, khai báo sau đây có thể dùng trong các ngôn ngữ như Java hay C# để khai báo một mảng 10 giá trị số nguyên.

int[10] cat; /* THIS IS NOT VALID C CODE */

Mặc dù vậy, như đã nhắc tới trước đây, nguyên lý trong cú pháp khai báo của C làm cho việc khai báo tương tự như việc sử dụng của biến. Thí dụ: một truy cập tới mảng này chẳng hạn như là cat[i], lúc khai khai báo lại cũng có cú pháp dạng:

int cat[10];

Mảng của các mảng

Tương tự như con trỏ, kiểu mảng có thể được lồng nhau. Vì trong cách viết mảng sử dụng các ngoặc vuông ([]), là một cách viết hậu tố, nên kích cỡ của mảng bên trong thì được ghi ở bên ngoài (hay đằng sau):

double dog[5][12];

Câu lệnh trên khai báo rằng dog là một mảng có năm phần tử. Mỗi phần tử là một mảng của 12 giá trị double.

Mảng của các con trỏ

Vì kiểu của phần trong một mảng tự nó lại là một kiểu của C, mảng của các con trỏ đương nhiên cũng có cấu trúc:

char *mice[10];

Câu lệnh này khai báo biến mice là mảng của 10 phần tử, trong đó, mỗi phần tử là một con trỏ chỉ tới char.

Con trỏ chỉ tới các mảng

Để khai báo một biến là một con trỏ chỉ tới một mảng, nhất thiết phải dùng tới dấu ngoặc đơn. Nó tương tự như cách dùng ngoặc để đổi thứ tự ưu tiên cho phép toán (phép toán trong ngoặc sẽ được tính trước) chẳng hạn:

2 + 3 * 4
(2 + 3) * 4

Hoàn toàn tương tự cho con trỏ chỉ tới các mảng. Lưu ý rằng dấu ngoặc vuông ([]) có độ ưu tiên cao hơn dấu sao (*), do đó khai báo sẽ có dạng:

double (*elephant)[20];

Câu lệnh này khai báo biến elephant là một con trỏ, và nó chỉ tới một mảng có 20 giá trị kiểu double.

Để khai báo một con trỏ chỉ tới dãy các con trỏ, chỉ cần kết hợp các cách viết:

int *(*crocodile)[15];

Hàm

Một sự khai báo hàm là một thí dụ điển hình của một kiểu dẫn xuất. Bởi vì hàm có thể nhận vào các tham số, kiểu của mỗi tham số phải được ghi rõ ra. Tên của mỗi tham số không nhất thiết phải được cho trước khi khai báo một hàm. Hai cách khai báo sau đây là tương đương:

long bat(char); long bat(char c);

Tham số

Trong khi cả hai dạng trên đều đúng cú pháp, thì cách viết bỏ qua tên của các tham số thường được xét như là một dạng tồi khi viết các khai báo hàm trong các tập tin tiêu đề. Các tên này có thể cung ứng các thông tin có giá trị cho những người đọc các tập tin đó chẳng hạn như là ý nghĩa và phép toán của chúng.

Các hàm có thể nhận và trả về các kiểu con trỏ dùng cách viết thông thường cho một con trỏ:

int const *ball(long int l, int i, unsigned char *s);

Kiểu đặc biệt void hữu dụng cho việc khai báo các hàm mà chúng không có tham số nào cả:

char *wicket(void);

Điều này khác với một bộ tham số trống rỗng, được dùng trong ANSI C, để khai báo một hàm, nhưng không cho bất cứ thông tin nào về các kiểu tham số của nó.

double umpire();

Câu lệnh trên khai báo một hàm tên là umpire, nó trả về một giá trị double, nhưng không đề cập gì về các tham số mà hàm đó dùng tới.

Hàm nhận hàm khác làm tham số

Trong C, các hàm không thể trực tiếp lấy các hàm khác như là tham số của nó, hay không thể trả về một hàm số như là kết quả. Mặc dù vậy, chúng có thể lấy vào hay trả về các con trỏ. Để khai báo rằng một hàm lấy một con trỏ hàm như là một tham số, thì dùng cách viết chuẩn như đã ghi ở trên.

int crowd(char p1, int (*p2)(void));

Khai báo bên trên có một hàm mà có hai tham số. Đối số đầu tiên, p1, là một kí tự kiểu char thông thường. Đối số còn lại, p2 là một con trỏ chỉ tới một hàm. Hàm được chỉ tới này không (nên) có các tham số, và sẽ trả về một số nguyên int.

Hàm trả về một hàm khác

Để khai báo một hàm mà nó trả về một hàm khác phải dùng tới dấu ngoặc đơn, để thay thứ tự ưu tiên của các phép toán (về hàm)

long (*boundary(int height, int width))(int x, int y);

Như trên, có hai bộ danh sách tham số, sự khai báo này nên được đọc thật kĩ, vì nó không được rõ ràng. Ở đây, hàm boundary được định nghĩa. Nó có hai tham số nguyên height và width, và trả về một con trỏ hàm. Con trỏ trả về này chỉ tới một hàm mà tự hàm đó có hai tham số nguyên là x và y, và trả về một số nguyên long.

Cách này có thể được mở rộng tùy ý để làm cho các hàm trả về con trỏ chỉ tới hàm mà hàm đó lại trả về các con trỏ, mà các con trỏ này chỉ tới các hàm khác, và vân vân, nhưng việc này sẽ biến mã nguồn trở nên khó hiểu một cách nhanh chóng, và rất dễ phát sinh lỗi. Nếu thấy cần thiết làm chuyện đó, thì người lập trình nên cứu xét việc thiết kế lại hay dùng một cách định nghĩa kiểu typedef.

Cấu trúc

Cấu trúc (từ khóa tương ứng struct) thực sự là một "kiểu mở rộng" của mảng. So với mảng thì cấu trúc mạnh hơn ở chỗ nó cho phép các phần tử của nó có các kiểu khác nhau và mỗi phần tử này được gọi là thành phần của một cấu trúc:

 struct person 
 {
     char name[60];
     int age;
 }; //lưu ý dấu ";" cần dùng để kết thúc câu lệnh

Câu lệnh struct nêu trên là một khai báo chuẩn để tạo ra một kiểu cấu trúc trong C.

Định nghĩa biến kiểu struct

Việc định nghĩa một biến có kiểu struct cũng đơn giản như khi định nghĩa các biến bình thường:

 struct person Bluesman;
 
 struct person Bio =
 {
   "Hieu",
   30
 };

Trong cách đầu thì biến Bluesman chưa có giá trị khởi động (nó vẫn có thể được truy cập và thay đổi giá trị sau này) trong khi biến Bio đã được gán các giá trị ban đầu. Hãy lưu ý dùng dấu phẩy "," để phân biệt các giá trị được gán lên những thành phần của cấu trúc -- và dĩ nhiên chúng phải có đúng kiểu cũng như không thể gán thiếu các giá trị cho các thành phần này.

Để truy cập đến các giá trị của biến có kiểu struct thì có thể dùng toán tử "." như câu lệnh sau:

  printf("Name : %s\n", Bio.name);

Mảng của các struct

Để kiến tạo một mảng của các struct thì dùng cú pháp sau:

struct person list[10];

Con trỏ chỉ tới struct

Cũng vậy, việc tiến hành khai báo một biến con trỏ có kiểu là struct tương tự cách thông thường. Chỉ cần thêm vào đó dấu sao đằng trước tên biến:

  struct person *Huong;

Cấu trúc lồng nhau

Kiểu cấu trúc cũng có thể định nghĩa lồng vào nhau. Thí dụ dưới đây cho thấy việc khai báo cấu trúc worker có chứa cấu trúc person như là một thành phần. Việc truy cập dữ liệu thành phần của cấu trúc bên trong cũng được tiến hành theo cách dùng toán tử "." nối tiếp nhau.

struct person { char name[60]; int age; }; struct worker { struct person peronal_ID; char job[30]; float income; };

Kiểu hợp nhất

Kiểu hợp nhất có tên từ khóa là union kiểu đặc biệt này cho phép nó chứa dữ liệu mà có thể có kiểu khác nhau trong cùng một phần bộ nhớ (mà nó có thể được cấp phát khi khai báo biến):

 union folder
 {
   int number;
   double real;
   char letter;
 }; //lưu ý dấu ";" cần dùng để kết thúc câu lệnh

Để khai báo biến, có thể dùng cách thông thường, tạo mảng các union hay cách tham chiếu:

 union folder matter;
 union folder listtype[100];
 union folder *matterptr;

Để gán hay truy cập giá trị cho một biến union, có thể dùng toán tử "." Theo hàng khai báo đầu tiên của thí dụ trên ta có thể viết một trong các phép gán:

 matter.real=3.1416;

hay là:

 matter.letter = 't';

hay là:

 matter.number = 1;

Lưu ý:

  • Việc gán giá trị cho một biến kiểu union đòi hỏi kiểu của dữ liệu đó phải có mặt trong khai báo ban đầu của nó. Theo thí dụ trên thì kiểu folder chỉ chấp nhận chứa một đơn vị dữ liệu của một trong ba kiểu int, double, và char.
  • Một khi giá trị có kiểu đúng nào đó được gán cho một biến kiểu union thì nó sẽ xóa bỏ hẳn giá trị cũ (nếu có) mà biến này đã chứa trước đó.
  • Việc truy cập một giá trị từ một biến kiểu union cần lưu ý đến kiểu hiện tại của dữ liệu đang được chứa của biến này nếu không, có thể gây ra lỗi dùng sai kiểu.
  • Điểm khác nhau quan trọng giữa union và struct là union chỉ có được một thành phần (nhưng thành phần này phải có kiểu tùy theo khai báo của người lập trình) trong khi struct bao gồm nhiều thành phần (và mỗi thành phần có thể có kiểu khác nhau).
  • Tương tự như strruct, union cho phép khai báo nhiều union lồng nhau.

Dùng #define để định nghĩa hằng và kiểu

Một cách tổng quát thì từ khóa tiền xử lý #define đùng để định nghĩa tên của một kiểu (đối tượng) nào đó. Thực ra, câu lệnh #define chỉ là một loại câu lệnh macro. Có hai ứng dụng chính như sau:

Định nghĩa tên hằng

Có thể dùng câu lệnh tiền xử lý #define để định nghĩa một hằng:

 #define PI 3.14159           //định nghĩa tên một hằng số PI
 #define STANDARD "ANSI C"    //định nghĩa tên một hằng dãy kí tự 
 #define ESC '\033'           //định nghĩa tên một hằng kí tự mã ASCII của phiếm Esc.

Lưu ý: so với cách định nghĩa dùng từ khóa const thì cách dùng này không được uyển chuyển bằng nhưng nó thường cho hiệu quả thực thi nhanh hơn vì đây chỉ là các macro.

Định nghĩa tên của kiểu dữ liệu

Có thể dùng #define để định nghĩa tên của một kiểu dữ liệu:

 #define real float  //định nghĩa tên kiẻu real cho dữ liệu có kiểu float

Việc khai báo các biến không có gì khác lạ ngoại trừ tên mới được dùng:

 real x, y[3], *z;

Lưu ý: Việc sử dụng #define có thể có các hiệu ứng phụ không ngờ nếu dùng nó kết hợp với nhiều định tính và có thể dẫn đến những lỗi khó tìm khi viết mã:

 #define STRING char *

Trong lúc định nghĩa biến người lập có thể muốn định nghĩa hai con trỏ char như sau:

 STRING name, job;

Tuy nhiên, điều ước muốn sẽ không xảy ra vì #define là macro nên trình dịch sẽ diễn giải thành (nó chỉ thay thế tên STRING bằng char *):

 char * name, job;

Và như vậy, người lập trình sẽ không nhận được hai biến con trỏ như dự tính mà chỉ có một biến name là con trỏ mà thôi.

Dùng typedef để định nghĩa kiểu

Một cách khác để đặt tên riêng cho kiểu dữ liệu là dùng câu lệnh với từ khóa typedef:

 typedef float real;

Nếu so sánh cách viết trong thí dụ trên với việc dùng từ khoá #define để định nghĩa thì chúng hoàn toàn tương đương (chỉ khác nhau về thứ tự các chữ float và real). Tuy nhiên, cách viết này là một sự thay thế thế tên "đúng nghĩa" chứ không phải là một macro đơn thuần. Trở lại thí dụ:

  typedef char* string;

Câu lệnh trên cho phép đặt tên string như là một kiểu mới (mà nội dung của nó là kiểu con trỏ char). Bây giờ hãy xét đến câu lệnh khai báo biến:

  string name, job;

Trường hợp này sẽ được trình dịch diễn dịch đúng theo mong muốn thành:

  char *name, *job;

Dùng cho struct

Một thí dụ khác liên quan đến việc đặt tên cho struct là việc kết hợp cả hai khai báo và đặt tên lại trong cùng một câu lệnh:

typedef struct { char * name; int * age; } person;

Như vậy khi khai báo biến chỉ cần viết là:

person Trung;

Ứng dụng

Một ứng dụng đáng lưu ý của typedef là việc làm cho mã C trở nên linh hoạt hơn trong nhiều môi trường khác nhau. Thí dụ: khi muốn xác định dùng đúng 4 byte cho một kiểu nguyên nhưng trên một số hệ máy thì nó ứng với kiểu int, trong khi trên một số hệ máy khác nó lại ứng với kiểu long int. Để giải quyết việc dùng chính xác 4 byte cho kiểu nguyên mà người lập trình muốn, thì có thể dùng giải pháp là: thêm vào một tập tin bao gồm trong đó có chứa định nghĩa:

  typedef int FOURBYTE;  //dùng cho các máy lấy int là 4 byte

hay định nghĩa:

  typedef long int FOURBYTE;  //dùng cho các máy lấy long int là 4 byte

và chỉ cần thêm vào trong mã nguồn câu lệnh #include <Tên_tập_tin_bao_gồm> như vậy chỉ cần thay nội dung của tập tin bao gồm thì toàn bộ mã vẫn hoạt động đún

Kiểu enum

Kiểu enum là một kiểu dữ liệu đặc biệt được dùng để định nghĩa một quan hệ thứ tự cho một tập họp hữu hạn các tên. (Trong thực tế thì enum có kiểu là int -- Theo trang 553 trong cuốn "New C Primer Plus" -- xem thêm phần tham khảo):

 enum Wiki {Arisa, Bluesman, VietBio, Trung, Quang, Minh};

Để khai báo biến member có kiểu enum dùng câu lệnh:

 enum member;

Các giá trị (hiểu ngầm) của các kí hiệu Arisa, Bluesman, VietBio, Trung, Quang, Minh theo mặc định sẽ tương ứng với 0, 1, 2, 3, 4, 5. Các câu lệnh cách viết sau đây là có hiệu lực:

  member = Minh;
  if (member == VietBio) { do_some_commands }
  for (member=Arisa; member <= Trung; member++) { do_some_commands }

Như vậy, theo mặc định, các tên của một enum được xem là các hằng số từ 0 tăng dần cho đến tên cuối cùng.

Tuy nhiên, C không loại trừ khả năng xếp lại giá trị của một kiểu enum theo cách riêng:

  enum reordert={duck,cat=10, mouse =50, elephant =1000, lion, virus};

Trong thí dụ trên thì duck có giá trị tương ứng là 0, cat là 10, ..., elephant là 1000, còn lion tương ứng với giá trị 1001 và virus tương ứng với 1002.

Một trong những ứng dụng chính của kiểu này là để tăng cường khả năng đọc mã được dễ hiểu hay phù hợp hơn.

Kiểu FILE là kiểu dữ liệu dùng để xử lí các tập tin. Theo ANSI thì có hai phương thức để truy cập là nhị phân (binary) và văn bản (text). Người ta dùng một biến con trỏ để khai báo:

 FILE *fp;

Thủ tục quan trọng cần làm tiếp theo là việc mở tập tin. Hàm thường được dùng để mở tập tin là fopen

  fp = fopen ("Dung.txt", "r");

Trong dòng lệnh trên thì tập tin có tên Dung.txt sẽ được mở trong chế độ đọc r. Các chế độ tuy cập cơ bản bao gồm:

  • r (đọc)
  • w (viết)
  • a (viết tiếp vào cuối tập tin và tạo tập tin mới nếu chưa có tập tin này)
  • r+ (đọc và viết)
  • w+ (đọc và viết nhưng cắt bỏ nội dung cũ của tập tin nếu có, tạo tập tin nếu nó chưa tồn tại)
  • a+
  • Các chế độ rb, wb, ab, rb+, r+b, wb+, w+b, ab+, a+b giống như các trường hợp trên nhưng chỉ dùng cho tập tin nhị phân.

Để tiếp tục việc xử lí thì có thể dùng tới các hàm trong thư viện chuẩn như: getc(), putc(), fprintf(), fscanf(), fgets(), fputs(), fseek(), ftell() và hàm fclose().

Lưu ý về biến được khai báo static

Các biến có được xác định bởi định tính static đặt trước tên kiểu biến khi khai báo sẽ cho biến đó một tính năng đặc biệt, đó là, giá trị của nó sẽ được lưu giữ không bị mất đi mặc dù khối mã chứa nó đã được xử lí xong. Trường hợp. Đặc biệt nếu một biến được khai báo có định tính static trong một hàm và được cài đặt giá trị nào đó thì sau khi hàm đó được gọi, giá trị của biến static đó vẫn còn giữ nguyên giữa mỗi lần gọi (cho tới khi nó được gán giá trị khác trong lần gọi tới của hàm). Thi dụ sau đây khai báo biến my_static có định tính static trong một hàm:

 #include<stdio.h>
 int static_func(int init)
 {
    static int my_static_var;
    my_static_var += init;
    return my_static_var;
 }
 int main(void)
 {
   printf("call the 1st time (init=0), my_static_var = %d\n", static_func(0));
   printf("call the 2nd time (init=1), my_static_var = %d\n", static_func(1));
   printf("call the 3rd time (init=2), my_static_var = %d\n", static_func(2));
 }

Sau khi dịch và chạy mã này sẽ cho kết quả:

   call the 1st time (init=0), my_static_var = 0   //0 +0 =0
   call the 2nd time (init=1), my_static_var = 1   //0 +1 =1
   call the 3rd time (init=2), my_static_var = 3   //1 +2 =3

Lưu ý: Mọi biến toàn cục đều có định tính static một cách tự động.

 






 

Quảng cáo
Liên hệ quảng cáo vdhong2007@yahoo.com
Ai đang xem chủ đề này?
Guest
Di chuyển  
Bạn không thể tạo chủ đề mới trong diễn đàn này.
Bạn không thể trả lời chủ đề trong diễn đàn này.
Bạn không thể xóa bài của bạn trong diễn đàn này.
Bạn không thể sửa bài của bạn trong diễn đàn này.
Bạn không thể tạo bình chọn trong diễn đàn này.
Bạn không thể bỏ phiếu bình chọn trong diễn đàn này.

Phát triển bởi VDH | Vũ Hồng © 2011, diễn đàn Thấy Hông?
Thời gian xử lý trang này hết 0.715 giây.