[Học lập trình C++] Chương 1: 1.10 – làm quen với tiền xử lý và header guards
1.10 – làm quen với tiền xử lý và header
guards
Tiền xử lý là một ý
tưởng có lẽ là tốt nhất giống như là một chương trình riêng biệt chạy trước quá
trình biên dịch. Mục đích của nó để xử lý các chỉ thị.
Chỉ thị là một câu lệnh
đặc biệt mà bắt đầu bằng ký tự # và kết thúc với một dòng mới không phải một dấu
chấm phẩy. Có nhiều kiểu của chị thị, chúng ta sẽ khảo sát bên dưới. Tiền xử lý
thì không thông minh. Nó không hiểu cú pháp C++; đúng hơn, nó thao tác văn bản
trước khi trình biên dịch chạy.
Include
Bạn đã nhìn thấy chỉ
thị #include hoạt động. Khi bạn #include một file, tiền xử lý sao chép nội dung
của file được include vào trong file include nó tại vị trí chỉ thị #include. Điều
này hữu ích khi bạn có thông tin cần được include trong nhiều nơi.
Câu lệnh #include có
hai dạng:
#include
<filename> nói với tiền xử lý tìm kiếm file trong một nơi đặc biệt được định
nghĩa bởi hệ thống mà nơi đó giữ các header file trong thư viện chuẩn runtime
C++.
#include “filename”
nói với tiền xử lý tìm kiếm file trong đường dẫn chứa file nguồn đang sử dụng
#include.
Nếu nó không tìm thấy
file header ở đây, nó sẽ kiểm tra bất kì đường dẫn khác mà bạn mô tả trong phần
cài đặt trình biên dịch hoặc IDE của bạn. Nếu thất bại, nó thực hiện giống hệt như trường hợp dấu ngoặc nhọn.
Định nghĩa Macro
Chỉ thị #define có thể
được sử dụng để tạo ra macro. Một macro là một nguyên tắc mà định nghĩa cách một
chuỗi input (ví dụ định danh) được chuyển sang một chuỗi ouput thay thế (ví dụ
chuỗi văn bản).
Có hai loại macro cơ
bản: Object-like macros, và function-like macros.
Function-like macros
hoạt động giống hàm, và phục vụ mục đích tương tự. Chúng ta không thảo luận
chúng, bởi vì việc sử dụng chúng thông thường khá nguy hiểm, và hầu như những
gì chúng làm được thì điều làm được bởi inline funtion.
Object-like macros có
thể được định nghĩa trong một trong hai cách sau:
#define identifier
#define identifier substitution_text
Định nghĩa trên cùng
không có chuỗi thay thế, còn cái bên dưới thì có. Bởi vì có khai báo tiền xử
lý, chú ý rằng cả hai dạng này đều không kết thúc bởi dấu chấm phẩy.
Object-like macros với chuỗi thay thế
Cứ khi nào tiền xử lý
gặp chỉ thị này, bất kì định danh nào được phát hiện được thay thế bằng chuỗi
thay thế. Định danh được nhập một cách truyền thống là chuỗi in hoa, sử dụng dấu
gạch dưới thay cho khoảng trắng.
Quan sát đoạn chương
trình sau:
#define MY_FAVORITE_NUMBER 9
std::cout << "My favorite number is:
" << MY_FAVORITE_NUMBER << std::endl;
tiền xử lý sẽ chuyển
chúng thành:
std::cout << "My favorite number is: " << 9 << std::endl;
khi chạy chương trình
đầu ra là:
My favorite number is: 9
.
Chúng ta sẽ thảo luận
trường hợp này (và tại sao bạn không nên dùng nó) trong chương 2.8
Object – like macros không có chuỗi thay thế
Object-like macro
cũng có thể được định nghĩa mà không cần chuỗi thay thế:
Ví dụ:
#define USE_YEN
Macros của dạng này
làm việc như bạn mong đợi: Khi tiền xử lý gặp phải định danh này, nó được xóa
đi phải thay thế bởi không gì cả!
Trong khi điều này
trong có vẻ vô dụng, thì nó lại được sử dụng nhiều hơn cả dạng thay thế chuỗi.
Chúng ta sẽ thảo luận tại sao trong phần tiếp theo, trong phần biên dịch có điều
kiện.
Không giống như
object-like macros với chuỗi thay thế, macros của dạng này thường được chấp nhận
sử dụng.
Biên dịch có điều kiện
Chỉ thị tiền xử lý
biên dịch có điều kiện cho phép bạn chỉ rõ điều kiện gì thì một thứ gì đó sẽ hoặc
không biên dịch. Chúng ta chỉ khảo sát những chỉ thị biên dịch có điều kiện sau
là #ifdef, #ifndef và #endif.
Chỉ thị tiền xử lý
#ifdef cho phép tiền xử lý kiểm tra xem giá trị có được định nghĩa trước đó hay
chưa. Nếu có thì đoạn code giữa #ifdef và #endif được biên dịch. Nếu không,
code sẽ bị bỏ qua.
Xem xét đoạn code
sau:
#define PRINT_JOE
#ifdef PRINT_JOE
cout << "Joe" << endl;
#endif
#ifdef PRINT_BOB
cout << "Bob" << endl;
#endif
Bởi vì PRINT_JOE đã
được định nghĩa, dòng cout<< “Joe”<<endl; sẽ được biên dịch. Bởi vì
PRINT_BOB chưa được định nghĩa, dòng lệnh cout<<”Bob”<<endl; sẽ
không được biên dịch.
#ifndef thì ngược lại
với #ifdef, nó cho phép bạn kiểm tra xem tên đó chưa được định nghĩa hay không.
#ifndef PRINT_BOB
cout << "Bob" << endl;
#endif
Chương trình này sẽ
in ra “Bob”, bởi vì PRINT_BOB chưa được định nghĩa.
Header guards
Bởi vì header file có
thể được include trong header file khác, nó có thể kết thúc trong tình huống mà
một header file được include nhiều lần trong cùng một file nguồn. Ví dụ, quan
sát chương trình sau:
mymath.h:
1
2
3
4
5
|
// note: This function really should be defined in mymath.cpp,
but we're doing it here for the sake of example
int cardsInDeck()
{
return 52;
}
|
add.h:
1
2
|
#include "mymath.h"
int add(int x, int y); // defined in add.cpp
|
subtract.h:
1
2
|
#include "mymath.h"
int subtract(int x, int y); // defined in subtract.cpp
|
main.cpp:
1
2
3
4
|
#include "add.h"
#include "subtract.h"
// rest of main.cpp, including main() here
|
Khi chúng ta include
add.h, nó mang cả mymath.h và nguyên mẫu của add(). Khi include subtract.h, nó
mang cả mymath.h và nguyên mâu subtract().
Do đó, nội dung của
mymath.h sẽ được trình biên dịch báo lỗi rằng cardInDeck() định nghĩa 2 lần
trong cùng một chỗ.
Để ngăn ngừa chuyện
này xảy ra, chúng ta sử dụng header guards, cài mà chỉ thị biên dịch có điều kiện
thực hiện theo dạng
#ifndef SOME_UNIQUE_NAME_HERE
#define SOME_UNIQUE_NAME_HERE
// your declarations here
#endif
Khi header này được
include, thứ đầu tiên được kiểm tra đó là SOME_UNIQUE_NAME_HERE đã được định
nghĩa trước đó hay chưa. Nếu đây là lần đầu chúng ta include header, thì SOME_UNIQUE_NAME_HERE chưa được định nghĩa. Do đó, nó định nghĩa và include nội
dung của file. Nếu chúng ta đã include header trước đó rồi thì SOME_UNIQUE_NAME_HERE đã được định nghĩa từ lần đầu tiên. Do đó, toàn bộ file
header sẽ bị bỏ qua.
Tất
cả file header của bạn nên có header guard trên nó. SOME_UNIQUE_NAME_HERE có thể là bất kì tên nào bạn muốn, nhưng thông thường là tên
của file header với phần mở rộng _H. Ví dụ:
2
3
4
5
6
|
#ifndef ADD_H
#define ADD_H
// your declarations here
#endif
|
Mặc dù thư viện chuẩn
include sử dụng header guards. Nếu bạn quan sát iostream header file từ Visual
Studio 2005 Express, bạn sẽ thấy:
#ifndef _IOSTREAM_
#define _IOSTREAM_
// content here
#endif
Cập nhật ví dụ mymath.h với header guards
Hãy quay trở lại với
ví dụ mymath.h, như chúng ta đã thấy ở trên.
Để ngăn ngừa việc
cardsInDeck() bị include hai lần, chúng ta có thể thêm header guard vào
mymath.h
mymath.h with header
guards:
1
2
3
4
5
6
7
8
9
10
|
#ifndef MYMATH_H
#define MYMATH_H
// note: This function really should be defined in mymath.cpp,
but we're doing it here for the sake of example
int cardsInDeck()
{
return 52;
}
#endif
|
Bây giờ, main.cpp
#include add.h, cái mà #include mymath.h. do MYMATH_H chưa được định nghĩa, nội
dung của mymath.h được đưa vào và MYMATH_H. Do MYMATH_H đã được định nghĩa nên
nội dung của mymath.h bị bỏ qua.
Bởi việc thêm header
guard, chúng ta chắc chắn rằng nội dung của mymath.h được included chỉ một lần
trong main.cpp
#pragma once
Nhiều trình biên dịch
hỗ trợ đơn giản hơn, thay thế cho dang header guard sử dụng chỉ thị #pragma
#pragma once
// your code here
#pragma once phục vụ
mục đích tương tự như header guards, và có thể có thêm lợi ích của việc ngắn
hơn và ít lỗi xuất hiện hơn.
Tuy nhiên, #pragma
once thì không phải là một phần chính thông của ngôn ngữ C++, và không phải tất
cả trình biên dịch đều hỗ trợ (mặc dù hầu hết các trình biên dịch hiện đại điều
có).
Cho mục đích tương
thích, chúng tôi đề nghị sử dụng header guards.
Tóm tắt
Header guard được thiết
kế để chắc chắn rằng nội dung của một header file không được sao chép quá một lần
trong bất kì một file đơn nào (ngăn ngừa sự định nghĩa lại). Tuy nhiên, header
guard không ngăn ngừa nội dung của một header file khỏi vấn đề sao chép sang
nhiều project file. Đây là một điều tốt, bởi vì chúng ta thường cần tham khảo nội
dung của một header file từ một file project khác.
Nguồn: learncpp.com
Nhận xét
Đăng nhận xét