Bộ tiền xử lý "Preprocessor - Lập trình C: Bài 20

Bộ tiền xử lý "Preprocessor - Lập trình C: Bài 20
access_time 10/22/2019 12:00:00 AM
person Nguyễn Mạnh Hùng


Bộ tiền xử lý – Lập trình C : Bài 20

1. Giới thiệu

Trong các bài đã trình bày ở các phần trước, chúng ta đã sử dụng các chỉ thị như #include, #define, và chúng ta cũng không cần quan tâm chi tiết về các chỉ thị này hoạt động như thế nào.

Thực chất các chỉ thị #include, define và một số các chỉ thị khác được xử lý bởi bộ tiền xử lý (prepeocessor). Bộ tiền xử lý theo định nghĩa nó là một phần phần nhỏ và được biên soạn bởi chương trình C. Nó thực thi trước khi thực hiện biên dịch chương trình.

Bộ tiền xử lý là một công cụ mạnh mẽ của lập trình C. Tuy nhiên với các lập trình viên khi xuất hiện lỗi với bộ tiền xử lý thì vấn đề tìm ra bugs là một thách thức. Ngoài ra bộ tiền xử lý rất dễ gây ra việc sử dụng sai và nó cũng là nguyên nhân gây ra sự khó hiểu của chương trình. Tuy nhiên với sức mạnh và tính tiện lợi của bộ tiền xử lý vẫn được sử dụng rộng dãi khi xây dựng các ứng dụng bằng việc sử dụng ngôn ngữ lập trình C.

1.1. Sử dụng bộ tiền xử lý:

Xét theo khía cạnh tổng quát trong phát triển và xây dựng ứng dụng thì bộ tiền xử lý được sử dụng để giải quyết các trường hợp:

Tính khả chuyển – Portability: Xây dựng ứng dụng C cần hỗ trợ chạy trên nhiều các nền tảng Hệ điều hành hoặc các môi trường khác nhau là một yêu cầu trên thực tế. Bộ tiền xử lý được coi là một cách thuận tiện để đảm bảo tính khả chuyển của hệ thống khi hệ thống chạy trên nhiều hệ điều hành, môi trường khác nhau. Ví dụ : Lập trình viên sử dụng các chỉ thị điều kiện (#if, #else, # if defined ...) để kiểm tra các thiết lập cho chương trình khi chạy trên các hệ điều hành, môi trường khác nhau. Dựa trên các thông tin đã được thiết lập này, lập trình viên sẽ sử dụng một số các macro, các thư viện.. tương ứng với từng hệ điều hành, môi trường cụ thể. Ví dụ sử dụng #include <windows.h> khi chương trình C chạy trên hệ điều hành Linux hoặc MacOS...

Tính biến đổi – Variability: Lập trình viên thường sủ dụng chỉ thị điều kiện (#if, #else, # if defined ...) để cung cấp các đặc tính tùy chọn hoặc để lựa chọn một trong các giải pháp thực thi. Ví dụ : Sử dụng chỉ thị điều kiện để loại bỏ một phần thư viện không cần thiết của ứng dụng nhằm tăng tính hiệu năng của hệ thống và giảm kích cỡ ứng dụng. Tính năng DEBUG là một ví dụ về vấn để này. Sử dụng DEBUG thường là để in ra dòng thông báo khi chương trình xảy ra lỗi, với người dùng cuối DEBUG không có ý nghĩa tuy nhiên với lập trình viên, sử dụng DEBUG để ngăn chặn các lỗi xẩy ra không đáng có và có thể là nguyên nhân gây ra chương trình bị lỗi.

Tối ưu mà - Code Optimization: Một số nhà lập trình viên cho rằng, ngoài việc loại trừ một số chức năng không cần thiết, họ cũng sử dụng các chỉ thị điều kiện nhằm để tối ưu các đoạn mã và giảm kích cỡ chương trình.

Tiến hóa mã - Code Evolution: Một số lập trình viên thường sử dụng các chỉ thị điều kiện để khi nâng cấp hoặc tối ưu Code gắn với một chức năng nào đó. (Sử dụng chỉ thị có điều kiện có thể thiết lập một hàm hay một nhóm hàm gắn với một chức năng cụ thể có thể chạy trên nhiều phiên bản khác nhau. Điều này cho phép các lập trình viên có thể kiểm định được các tính năng mới phát triển của hệ thống.

Giới hạn ngôn ngữ - Language Limitations: Một số nhà phát triển đề cập đến việc sử dụng các chỉ thị có điều kiện vì những hạn chế của ngôn ngữ C. Ví dụ : Họ sử dụng #ifdef để kiểm tra tránh việc sử dụng cùng lúc nhiều các tệp header.

1.2. Bộ tiền xử lý trong lập trình

Cụ thể trong lập trình bộ tiền xử lý được sử dụng để:

Bao hàm các tệp header : Khi xây dựng hệ thống các lập trình thông thường phải gọi đến các thư viện được cung cấp sẵn bởi ngôn ngữ lập trình C. Để thực hiện điều này thì các lập trình viên thường sử dụng chỉ thị #include để thực hiện.

Mở rộng và định nghĩa các macro: Các lập trình viên có thể định nghĩa các macro, đó là các chữ viết tắt cho các đoạn mã C tùy ý. Bộ tiền xử lý sẽ thay thế các macro bằng định nghĩa của chúng trong suốt chương trình. Một số macro được C định nghĩa sẵn.

Chỉ thị có điều kiện: Trong một số các trường hợp lập trình viên cần phải loại bỏ hoặc bao gồm (chứa) các đoạn mã của chương trình.

Chẩn đoán: Trong quá trình chạy chương trình, chương trình có thể bị lỗi, sử dụng bộ tiền xử lý để hạn chế các lỗi phát sinh không đáng có và thông báo cho các nhà phát triển và người dùng. Ví dụ như dùng DEBUG.

1.3 Bộ tiền xử lý làm việc như thế nào

Các hành động của bộ tiền xử lý được điều khiển bởi các chỉ thị của bộ tiền xử lý: Các chỉ thị được đánh dấu bắt đầu bằng kí tự #, chúng ta đã gặp 2 tiền xử lý #include#define ở các phần mà tôi đã trình bày.

Chỉ thị #define định nghĩa một macro bao gồm tên của macro và thực thể của nó (đôi khi nó có thể như một hằng hoặc là một biểu thức). Khi một tiền xử lý được sử dụng trong chương trình, chương trình gặp tên marcro thì nó sẽ thay thế tên macro đó bằng chính nội dung của macro.

Chỉ thị #include thông báo cho bộ tiền xử lý mở một file được xác định thông qua tên file với nội dung gắn với file này và coi nội dung file như là một phần đã được biên dịch bởi chương trình.

Ví dụ : #include <stdio.h>

Chỉ thị cho bộ tiền xử lý mở file stdio.h và mang nội dung của file này vào trong chương trình. Stdio.h chứa các nguyên mẫu của các hàm vào/ra chuẩn của ngôn ngữ C.

Lược đồ mô tả thứ vai trò của bộ tiền xử lý trong biên dịch chương trình.


Hình số 1 : Vai trò tiền xử lý trong xử lý chương trình C

Đầu vào của tiền xử lý là chương trình C. Trong chương trình C chứa các chỉ thị và bộ tiền xử lý sẽ thực thi các chỉ thị. Đầu ra của khi kết thúc giai đoạn tiền xử lý là một chương trình C khác, phiên bản mới sẽ được cập nhật và không còn chứa các chỉ thị của bộ tiền xử lý nữa (Vì các chỉ thị của bộ tiền xử lý đã được thay thế bằng nội dung của nó). Nội dung này sẽ làm đầu vào trực tiếp cho trình biên dịch. Trình biên dịch sẽ thực hiện kiểm tra cú pháp, ngữ nghĩa khi thực hiện biên dịch chương trình.

2. Chỉ thị tiền xử lý Include

Chỉ thị Tiền xử lý #include được sử dụng để chèn tệp tiêu đề vào chương trình thông qua cú pháp :

#include <filename>

Hoặc

#include “filename”

Các file được chỉ thị tiền xử lý thông qua #include được gọi là các header file(file tiêu đề). File tiêu đề là file chứa các khai báo hoặc các định nghĩa macro và có thể là cả dữ liệu mang tính toàn cục. Các file tiêu đề này có thể được chia sẻ ở nhiều các file chứa mã nguồn xử lý nghiệp vụ của chương trình.

Lưu ý : filename có thể là tên file đầy đủ của file header hay nói cách khác là có thể chứa cả đường dẫn tới file header.

Ví dụ :

#include </u/example_prog/mine.h>

Trong hệ điều hành Unix, thì với trình biên dịch C thuần, các file header sẽ nằm ở thư mục /usr/include.

Khi thực hiện chỉ thị #include thì chương trình sẽ thực hiện tìm kiếm các file tiêu đề tại thự mục mặc định hoặc lại thư mục chứa các file .c (mã nguồn của chương trình).

2.1. Các file tiêu đề phục vụ 2 mục đích

+ File tiêu đề chứa các khai báo và các macro được hỗ trợ sẵn bởi ngôn ngữ C. File tiêu đề có đuôi mở rộng là .h; Khi các lập trình viên cần sử dụng thì sẽ sử dụng #include để gọi đến thư viện chứa các file tiêu đề này.

+ Các file tiêu đề (header) do các lập trình viên tự định nghĩa. Khi đó nó sẽ đóng vai trò như là các interface thực hiện giao tiếp giữa các file nguồn trong chương trình. Khi xây dựng chương trình, bạn cần nhóm các vấn đề có liên quan với nhau và định nghĩa các marcro cho việc xử lý tập hợp vấn đề phát sinh thì bạn lên sử dụng file tiêu đề để chứa các thông tin này.

Thực chất quá trình sử dụng chỉ thị #include <filename> hoặc #include “filename” tại các file nguồn .c có kết quả giống như là thực hiện copy nội dung của file header vào trong file nguồn .c của chương trình. Tuy nhiên nếu thực hiện copy (Theo nghĩa thông thường) thì sẽ gây mất thời gian và rất dễ lỗi. Với file header, việc khai báo thông qua việc sử dụng #include chỉ xuất hiện ở một nơi trong file nguồn (file .c).

Nếu vì một lý do nào đó cần cập nhật các file header thì quá trình này sẽ được thực hiện tại nơi chứa các file header, và tại file nguồn không cần chỉnh sửa gì cả và nội dung thực thi của chương trình sẽ tự động cập nhật lại khi chương trình được biên dịch lại.

Ví dụ :

Khi chúng ta làm việc với dữ liệu file, chúng ta phải sử dụng các hàm như fopen khi đó chúng ta cần phải bổ sung chỉ thị bộ tiền xử lý sau :

#include <stdio.h>

Trong nội dung của file studio.h chứa nội dung là các nguyên mẫu các hàm như sau :

FILE *fopen( const char *, const char * );

size_t fread( void *, size_t, size_t, FILE * );

FILE *freopen( const char *, const char *, FILE * );

char *tempnam( char *, char * );

FILE *tmpfile( void );

char *tmpnam( char * );

extern long ftell( FILE * );

2.2. Sử dụng file tiêu đề để chứa dữ liệu toàn cục

Khi lập trình, các lập trình viên có thể khai báo biến

int global_counter;

char global_char;

main()

{

...

}

Khi đó các biến global_counter, global_char sẽ được sử dụng trên phạm vi toàn bộ chương trình. Ngoài cách này ra thì các lập trình viên có thể định nghĩa các biến này trong file header như sau:

Tạo file "global_decs.h", trong file global_decs.h chứa các khai báo

int global_counter;

char global_char;

Khi đó trong các đoạn mã muốn sử dụng 2 biến này chúng ta sẽ phải

#include "global_decs.h"

2.3. Khai báo biến toàn cục với từ khóa extern

Trong lập trình C, sử dụng từ khóa extern để khai báo một biến sử dụng chung mang tính toàn cục trong phạm vi của một module nào đó. Khi khai báo một biến có từ khóa extern thì biến đó sẽ không cần phải khởi tạo giá trị ban đầu tại file header thay vào đó lập trình viên sẽ khởi tạo giá trị của biến này tại các file nguồn .c; Lưu ý tại file header khi khai báo biến với từ khóa extern cần phải sử dụng comment để ghi chú thích đối với từng biến.

Ví dụ

Định nghĩa file globalvar.h có nội dung

/* Initialized to 1 in start.c */

extern int page_num;

Khi đó tại file start.c lập trình viên sẽ sử dụng

int page_num = 1;

Khi gặp từ khóa extern trình biên dịch sẽ hiểu là biến đó sẽ được khai báo ở phạm vi toàn cục, không cần thiết lập giá trị trước (Có nghĩa là không cần phải để giành vùng bộ nhớ trống cho biến này) và biến này sẽ được thiết lập giá trị tại các file nguồn .c khi cần.

3. Macro

3.1 Định nghĩa Macro như một hằng

Cú pháp :

#define name_macro body_macro

Trong đó :

name_macro : là tên macro, body_macro: Nội dung macro hay thân macro.

Một macro là một tên có chứa chuỗi ký tự liên quan. Chuỗi ký tự này được gọi là nội dung hay thân của macro (macro body).

Trong C, sử dụng các chữa cái Hoa để đặt tên cho các macro, điều này được sử dụng để phân biệt giữa tên các macro với tên các biến (thông thường tên các biến sử dụng cả chữ cái hoa và thường).

Ví dụ

#define PI 3.14159

#define BUFF_LEN 512

Tên macro : PI, BUFF_LEN

Nội dung macro tương ứng với PI là : 3.14159 và tương ứng với BUFF_LEN là 512

Khi lập trình thay vì phải khai báo : char buf [512] thì chúng ta sử dụng char buf [BUFF_LEN];

Với các lập trình viên còn ít kinh nghiệm, khi lập trình chúng ta thường hay viết như sau :

static char in_buf[256];

main(){

...

for (a = 0; a < 256 ; a++){

                    in_buf[al = getchar();

}

}

Khi chạy chương trình chúng ta thấy giá trị 256 không đủ để lưu thông tin, lập tức chúng ta phải chỉnh sửa giá trị 256 này. Lúc đó chúng ta phải sửa ở 2 vị trí chứa số 256. Nếu một chương trình có nhiều chỗ sử dụng giá trị 256 thì lập tức chúng ta gặp vấn đề. Như vậy thay vì sử dụng giá trị trực tiếp 256 chúng ta sẽ thay thế nó bằng :

#define MAX_INPUT_BUFFER_SIZE 256

static char in_buf[MAX_INPUT_BUFFER_SIZE];

main(){

...

for (a = 0; a < MAX_INPUT_BUFFER_SIZE; a++){

                    in_buf[al = getchar();

}

}

3.2. Macro với tham số

Tương tự như hàm, macro có thể có chứa một hoặc nhiều tham số đầu vào, các tham số này sẽ được sử dụng tại nội dung (macro body) hay tại thân của macro:

Hình sau mô tả định nghĩa macro với tham số:


Hình số 2 : Macro có tham số

Ví dụ : Macro có một tham số

#define CIRCLE_AREA(x) ((PI) * (x) * (x))

Thay vì viết hàm tính diện tích đường tròn với bán kính x; sử dụng macro để thực hiện

Tên macro là : CIRCLE_AREA

Tham số truyền vào : x;

Nội dụng của macro: ((PI) * (x) * (x))

Khi lập trình sử dụng macro CIRCLE_AREA như sau:

float area = CIRCLE_AREA(4);

thì sẽ được dịch ra bởi bộ tiền xử lý là

float area =((PI)*(4)*(4));

Ví dụ : Với macro có 2 tham số

#define RECTANGLE_AREA(x, y) ((x) * (y))

Khi đó trong code

int rectArea = RECTANGLE_AREA(5, 6);

thì sẽ được dịch ra bởi bộ tiền xử lý là :

int rectArea = ((5) * (6));

3.3 Cảnh báo lỗi

3.3.1 Sử dụng dấu ;

Một trong các lỗi xảy ra khi lập trình đó là sử dụng định nghĩa marcro có chứa dấu ; ở cuối dòng định nghĩa marco

Ví dụ

#define SIZE 10;

Khi sử dụng macro SIZE như sau

int i = SIZE;

Thì sẽ được dịch ra bởi bộ tiền xử lý như sau

int i = 10;;

và lúc này sẽ xuất hiện lỗi, với các lập trình viên ít kinh nghiệm thì sẽ rất khó tìm ra lỗi này.

Một ví dụ khác :

#define GOOD_CONDITION (var == 1);

Sau đó sử dụng macro GOOD_CONDITION áp dụng với while

while GOOD_CONDITION {

          ...

}

Bộ tiền xử lý sẽ xử lý như sau

while (var == 1);{

          ...

}

Và lúc đó sẽ gây lỗi vì kết quả là nội dung thực thi trong while sẽ không còn là nội dung của while nữa.

3.3.2. Thứ tự thực hiện các phép toán
Do đặc thù macro nên khi sử dụng macro cần rất lưu ý về thứ tự ưu tiên ví dụ

#define RECTANGLE_AREA(x, y) (x * y)

Có nghĩa ở nội dung macro RECTANGLE_AREA ta thay thế ((x)*(y)) bằng (x * y) khi đó ta sử dụng:

int i = RECTANGLE_AREA(a+2,b); Lúc này bộ tiền xử lý sẽ thực hiện

int i = a+2*b; và lúc này kết quả sẽ khác (a+2) * (b);

3.3.3. Sử dụng dấu = trong định nghĩa macro

Một lỗi thông dụng xảy ra khi định nghĩa Macro đó là sử dụng phép gán để khởi tạo giá trị cho macro.

Ví dụ:

Thay vì phải viết :

#define MAX 100

Thì các lập trình viên sẽ viết

#define MAX = 100

Loại sai lầm này sẽ gây ra các lỗi tối nghĩa. Ví dụ áp dụng gây lỗi

for (j=MAX, j > 0; j-- ){...

}
Khi đó bộ tiền xử lý sẽ thực hiện :

for (j== 100; j > 0, j--){ ...

}

Vậy khi đó biểu thức j=100 sẽ biến thành j==100; lúc đó ngữ nghĩa sẽ khác hẳn và là nguyên nhân gây ra lỗi.

3.4. Các vấn đề cần lưu ý

Không giống như hàm, các tham số hình thức truyền vào hàm có kiểu, còn tham số của macro không có kiểu, điều đó có nghĩa là bộ tiền xử lý sẽ không kiểm tra kiểu đối với tham số của marco.

Ngoài ra thì marco không bị xung đột khi lập trình viên sử dụng các tham số trùng tên nhau, với hàm C thì không thể sử dụng các tham số hình thức truyền vào của hàm là trùng tên được;

Ví dụ

#define RECTANGLE_AREA(x, y) ((x) * (y))

Khi gọi có thể sử dụng

int i = RECTANGLE_AREA(a-1);

Lúc đó

i = ((a-1)*(a-1));

Vậy với hàm trong C thì không như thế được.

3.5. Chỉ thị bộ tiền xử lý #undef

Sử dụng chỉ thị bộ tiền xử lý #define để định nghĩa macro. Macro này sẽ có hiệu lực kể từ khi nó được định nghĩa đến khi kết thúc file chứa mã nguồn hoặc file header hoặc khi gặp chỉ thị bộ tiền xử lý #undef. Sử dụng chỉ thị bộ tiền xử lý #undef để loại bỏ các định nghĩa của các macro, khi đó muốn sử dụng lại macro thì lập trình viên sẽ phải định nghĩa lại.

Ví dụ:

#define WIDTH 80

#define ADD( X, Y ) ((X) + (Y))

.

.

.

#undef WIDTH

#undef ADD

3.6. Sử dụng tên Macro trong chính nội dung định nghĩa của Macro

Với hầu hết các trình biên dịch cũ của C đều không cho phép tên Macro trong chính nội dung (body) của chính macro.

Ví dụ:

Định nghĩa macro sqrt, trong đó sqrt là tên macro và sqrt cũng nằm trong chính body của nó.

#define sqrt (x) ( (x < 0) ? sqrt ('x) : sqrt (x) )

Tuy nhiên Chuẩn ANSI hỗ trợ cú pháp này. Nhưng xuất hiện cảnh báo nếu tên macro có trong nội dung của chính macro thì nó sẽ không được mở rộng (if a macro name appears in its own definition, it will not be expanded). Điều này tránh được vấn đề mở rộng vô hạn hay là đệ quy vô hạn trong định nghĩa macro.

Ở ví dụ trên, khi sử dụng sqrt như sau:

int y = sqrt ( 5 );

Bộ tiền xử lý sẽ thay thế như sau

int y = ( (5 < 0 ? sqrt (- 5) : sqrt (5 ) );

Bộ tiền xử lý sẽ coi sqrt là một hàm với các tham số truyền vào là -5 để tính kết quả của y. Lưu ý sử dụng tên macro trong chính nội dung của macro là rất nhậy cảm, đặc biệt khi tên macro trùng với tên hàm.

3.7. Macro và Hàm

Xét về một khía cạnh nào đó thì Macro và hàm là tương tự như nhau ở chỗ cả hai đều cho sử dụng tên để định danh và đều chứa nội dung cho phép một tập hợp các xử lý gắn với một nghiệp vụ nào đó của bài toán. Do vậy đôi khi khó quyết định xem sử dụng macro hay hàm thì hiệu quả hơn.

Các danh sách sau đây tóm tắt những ưu điểm và nhược điểm của macro so với hàm trong C.

Ưu điểm

+ Tốc độ thực thi của Macro nhanh hơn hàm.

+ Số lượng các tham số truyền vào cho macro được kiểm tra để phù hợp với định nghĩa. Trình biên dịch C cũng thực hiện điều đó với hàm nếu hàm đó được khai báo các nguyên mẫu hàm.

+ Tham số truyền vào cho macro không có kiểu do vậy khi sử dụng có thể truyền vào nhiều kiểu dữ liệu khác nhau.

Nhược điểm

+ Hàm trong C cho phép gọi các hàm khác và gọi chính nó trong nội dung hàm. Còn macro không cho phép thực hiện điều này. Chính vì vậy với cùng một nghiệp vụ, nếu chương trình sử dụng macro thì mã phát triển chương trình sẽ lớn hơn so với khi sử dụng hàm.

+ Mặc dù macro có kiểm tra số lượng các tham số truyền vào nhưng không kiểm tra kiểu dữ liệu của tham số, điều này dẫn đến khi sử dụng nếu không cẩn thận sẽ gây lỗi.

+ Sử dụng macro sẽ khó debug hơn vì macro hoạt động ở gian đoạn tiền xử lý.

3.8. Macro được xây dựng sẵn

Chuẩn ANSI cung cấp 5 macro được định nghĩa sẵn của bộ tiền xử lý. Mỗi tên của macro được xây dựng sẵn sẽ bắt đầu bởi dấu “__”. Lập trình viên không được sử dụng chỉ thị bộ tiền xử lý #undif để loại bỏ các macro này.

__LINE__: Thông tin về số dòng trong mã chương trình khi dòng lệnh đó được gọi, __LINE__ Hữu dụng khi xuất hiện các lỗi thực thi tại dòng lệnh nào đó. Lập trình viên muốn in thông tin về số thứ tự dòng lệnh xuất hiện lỗi này.

__FILE__: Trả về tên file khi file đó được gọi

__TIME__: Trả về thời gian hiện tại tại thời điểm biên dịch chương trình theo mẫu hh::mm::ss trong 24h.

__DATE__: Trả về ngày hiện tại tại thời điểm biên dịch chương trình theo mẫu mmm dd yyyy trong 24h.

__STDC__: Trả về giá trị 1 nếu trình biên dịch C theo chuẩn ANSI C

Ví dụ

Sử dụng macro được xây dựng sẵn để in các thông tin về số thứ tự dòng, tên file, thời gian, ngày tháng và thông tin chuẩn ANSI của trình biên dịch

Code minh họa


Hình số 3 : Macro được xây dựng sẵn

Kết quả khi chạy chương trình sẽ in ra các dòng thông tin liên quan tới LINE, TIME, DATE ...

Các lập trình viên thường hay sử dụng các macro được hỗ trợ sẵn để định nghĩa các file header nhằm hỗ trợ quá trình DEBUG chương trình.

3.9. Toán tử tiền xử lý ##

Trong ANSI sử dụng toán tử tiền xử lý ## để thực hiện dán 2 token (ghép 2 token) lại

Ví dụ

#define FILENAME ( extension ) test_ ## extension

Khi đó nếu thực hiện

FILENAME( bak )
Thì kết quả trả về là test_bak

Lưu ý : Trong C, các lập trình viên khi sử dụng bộ tiền xử lý thì không thể tự động ghép 2 token (thông tin) lại với nhau một cách thủ công.

Ví dụ

#define FILENAME ( extension ) test_extension

Không hoạt động bởi test_extension được coi là một định danh duy nhất và phần mở rộng (extension) không xuất hiện ở trong định danh.

Một ví dụ khác sử dụng toán tử tiền xử lý ##

#define READ( type) (file_##type == NULL? \

open_##type##_file(), read __ ##type() : \

read_##type() )

Trong ví dụ trên khi viết các macro dài quá giới hạn của dòng thì sử dụng \ để nối với dòng phía dưới. Macro này được sử dụng để đọc các phần tử trực thuộc file. Nếu file không được mở (ví dụ : file_##type == NULL), macro sẽ mở và gọi đến hàm read_##type(), mặt khác sẽ gọi đến hàm read_##type mà không cần mở tệp.

s = READ( player );

sẽ được thay thế bằng

s = ( file_player == NULL ? open __ FILE__player(), read_player () : read_player() );

Sẽ tương đương với

if (file_p1ayer == NULL) {

open __ FILE__player();

s = read_p1ayer;

else

s = read_p1ayer;

4. Dịch có điều kiện

Dịch có điều kiện cho phép lập trình viên kiểm soát được quá trình thực thi các chỉ thị của bộ tiền xử lý và thực hiện dịch mã chương trình. Giống như khi lập trình, C sử dụng cấu trúc if, else ... để kiểm soát sự thực thi các đoạn mã tương ứng với các điều kiện logic, bộ tiền xử lý sử dụng các macro #if, #else, #elif, and #endif để thực hiện các vấn đề này.

Hình sau mô tả các chỉ thị bộ tiền xử lý #if, #else...


Hình số 4: Mô tả chỉ thị dịch có điều kiện

Ví dụ:

#if x == 1

#undef x

#define x 0

#elif x == 2

#under x

#define x 3

#else

#define y 4

#endif

Trong đoạn mã trên, x là một macro. Nếu nội dung của x là 1 thì x sẽ được định nghĩa lại, và giá trị nội dung x sẽ là 0. Nếu giá trị nội dung là 2 thì định nghĩa lại x và giá trị nội dung nó là 3. Trong các trường hợp còn lại thì định nghĩa y và giá trị nội dung y là 4.

Việc áp dụng các chỉ thị tiền xử lý có điều kiện vào nhiều các trường hợp khác nhau như hỗ trợ xử lý các BUG, Lựa chọn thực thi các hàm gắn với các trình biên dịch khác nhau.

Ví dụ : Sử dụng chỉ thị bộ tiền xử lý có điều kiện để lựa chọn các hàm xử lý :

#if (_STDC_)

          extern int foot char a, float b );

          extern char *goo( char *string );

#else

          extern int foo();

          extern char *qoo();

#endif

Đoạn mã trên sẽ kiểm tra, nếu trình biên dịch theo chuẩn ANSI thì sử dụng 2 hàm

          extern int foot char a, float b );

          extern char *goo( char *string );

Trong trường hợp ngược lại thì sử dụng 2 hàm:

          extern int foo();

          extern char *qoo();

4.1. DEBUG với dịch có điều kiện

Dịch có điều kiện thỉnh thoảng phục vụ mục đích xử lý các vấn đề liên quan tới DEBUG của chương trình. Các bộ công cụ lập trình hỗ trợ các tính năng rất mạnh mẽ để phục vụ cho việc DEBUG, tuy nhiên đôi khi DEBUG không thành công hoặc gặp khó khăn, khi đó đôi khi lập trình viên sẽ cần phải in ra thông tin của một biến cụ thể trong một vòng lặp while, for hay do-while chẳng hạn.

Sử dụng printf kèm theo các chỉ thị có điều kiện khi gặp DEBUG và in ra giá trị của một biến.

Ví dụ:

#ifdef DEBUG

  printf("Variable x = %d\n", x);

#endif

Tất nhiên là DEBUG sẽ phải được định nghĩa bởi #define trước khi sử dụng chỉ thị #indef. Khi các lỗi được xử lý xong, thì các bạn có thể comment lại các #define DEBUG và hàm printf có liên quan. Ngoài cách này ra thì lập trình viên có thể sử dụng #undef. Tuy nhiên các xử lý này chỉ áp dụng với các chương trình nhỏ. Với các chương trình lớn thì lập trình viên cần tạo ra các file header riêng biệt để xử lý các vấn đề liên quan tới DEBUG.

4.2. Kiểm tra tồn tại của Macro

Chỉ thị dịch có điều kiện #if, #eif... cho phép kiểm tra gắn với các điều kiện, thường các điều kiện được xác định bởi giá trị của các biểu thức toán học. Lập trình viên cũng có thể sử dụng các điều kiện dịch đặc biệt để kiểm tra sự tồn tại hay không tồn tại của một macro thông qua #ifdef, #ifndef, và #endif.

Ví dụ:

#ifdef TEST

printf( "This is a test.\n" ):

#else

printf ( "This is not a test. \n" );

#endif

Đoạn mã trên nhằm mục đích kiểm tra macro TEST đã được định nghĩa hay chưa.

Nếu TEST đã được định nghĩa thì sẽ in ra dòng thông báo "This is a test.\n" ngược lại thì in ra dòng thông báo "This is not a test. \n"

Trong đó #ifdef TEST là tương đương với :

#if defined TEST

Hoặc

#if defined (TEST)

Về bản chất thì :

#if defined macro_name

Sẽ tương đương với

#ifdef macro_name

Và đảo ngược với đã định nghĩa (defined) thì chúng ta có chưa định nghĩa (!defined)

#if !defined macro_name

Tương đương với

#ifndef macro_name

Ví dụ:

Sử dụng !defined để định nghĩa Macro FALSE có giá trị là 0.

#if !FALSE

# define FALSE 0

#endif

Để tránh việc sử dụng !FALSE thì chúng ta có thể sử dụng #ifndef khi đó đoạn mã trên sẽ được viết thành:

#ifndef FALSE

# define FALSE 0

#elif FALSE

# undef FALSE

# define FALSE 0

#endif

4.3 Chỉ thị #error
Chỉ thị #error cho phép đưa ra thông báo lỗi trong quá trình dịch chương trình. Dòng thông báo gắn với chỉ thị #error sẽ gửi ra thiết bị thông báo lỗi chuẩn. Thông thường nó được sử dụng gắn với các #if... để kiểm tra một điều kiện nào đó.

Ví dụ:

#if INTSIZE < 16

# error INTSIZE too small

#endif

Thực hiện dịch bằng câu lệnh

Nội dung được ghi vào file ví dụ test.c hoặc một file header, sau đó trong test.c sẽ tiến hành #include.

cc -D INTSIZE=8 test.c

Khi đó sẽ nhận được thông báo

INTSIZE too small

5. Điều khiển dòng

Theo chuẩn ANSI, định nghĩa chỉ thị tiền xử lý #line thiết lập số thứ tự dòng hiện thời của câu lệnh được thực thi hiện tại của tệp mã nguồn và tên tệp mã nguồn.


Hình số 5: Cú pháp chỉ thị #line

Cú pháp

#line number_row

Với number_row là số dòng

Giả sử ta có câu lệnh:

printf( "Current line: %d\n", __LlNE_);

Khi đó kết quả in ra là Current line: 5

Điều đó có nghĩa là dòng lệnh thực thi hiện tại sẽ có thứ tự 5

Sau đó ta bổ sung câu lệnh

printf( "Current line: %d\n", __LlNE_);

#line 100

printf( "Current line: %d\n", __LlNE_);

Khi đó nó sẽ in ra thông tin là:

Current line: 5

Current line: 100

Vì khi gặp câu lệnh #line 100, thì trình biên dịch sẽ chuyển câu lệnh thực thi dòng hiện tại về vị trí là 100. Do vậy câu lệnh sau sẽ in ra là 100 chứ không phải là 7.

Ví dụ code minh họa


Hình số 6: Minh họa chỉ thị #line

6. Chỉ thị #pragma

Chỉ thị tiền xử lý #pragma thực thi các nhiệm vụ cụ thể và để cung cấp các thông tin bổ sung cho trình biên dịch C.

Cú pháp

#prama token_name

Trong đó : token_name có chứa 6 giá trị như : startup, exit, warn...

Ví dụ:

#include<stdio.h>

int display();

 

#pragma startup display

#pragma exit display

 

int main() {

  printf("\nI am in main function");

  return 0;

}

int display() {

  printf("\nI am in display function");

  return 0;

}

Khi gặp #pragma startup display thì trước khi thực hiện hàm main nó sẽ thực hiện hàm display.

Khi gặp #pragma exit display thì trước khi kết thúc chương trình nó sẽ gọi đến hàm display.

 

 

 

 

 

 

 


vertical_align_top
share
Chat...