ทุก ๆ คนที่เคยใช้ C/C++ ย่อมเคยที่จะใช้เจ้า preprocessor #include
อยู่แล้ว แต่หลายคนคงไม่เข้าใจว่าไอ้เจ้านี่มันทำอะไร วันนี้จะอธิบายเพิ่มเติมเรื่องของเจ้านี่นะครับ
Syntax
สำหรับวิธีการใช้ #include
นั้นมีอยู่สองแบบครับ นั่นก็คือ #include <ชื่อไฟล์>
และ #include "ชื่อไฟล์"
แต่ละแบบมีความหมายที่แตกต่างกัน ก็คือ แบบที่ใช้ <>
นั้นเป็นการอ้างอิงถึงไฟล์ที่อยู่ใน include path ที่เราตั้งค่าให้ compiler ไปหา (พูดกันง่าย ๆ ก็บรรดา library ทั้งหลายที่ไม่ได้เป็นส่วนหนึ่งของโปรเจคครับ ซึ่งรวมถึง standard libary ด้วย) กับแบบ ""
นั้นจะเป็นการอ้างถึงไฟล์ที่อยู่ในโปรเจคของเรา
ทั้งสองแบบต่างกันแค่ตำแหน่งของไฟล์เท่านั้นครับ แต่จริง ๆ มันทำงานเหมือนกัน ส่วนจะแยกเป็นสองแบบทำไมให้งงนี่ผมเองก็ไม่ทราบเหตุผลจริงๆ เหมือนกัน ถ้าให้เดาก็คงแค่อยากให้ใช้ไฟล์ที่มีชื่อเหมือนกันระหว่างในโปรเจคกับตัว library ที่เราเรียกใช้
การทำงานของ #include
เจ้า preprocessor #include
เป็นการสั่งให้ compiler นั้นไปเอาเนื้อหาของไฟล์ที่ระบุไว้มาแทนที่คำสั่งนี้ก่อนที่ไฟล์จะถูกคอมไพล์ครับ ยกตัวอย่างเช่น ถ้าผมมีไฟล์สองไฟล์ดังต่อไปนี้
abc.i
printf("ABC");
usage.cpp
int main(char** args, int argc) {
#include "include.i"
return 0;
}
ผมคิดว่าหลาย ๆ คนคงไม่เคยใช้โค้ดลักษณะนี้ แต่น่าจะพอเดาได้ ครับเวลาเราสั่งคอมไพล์ usage.cpp
นั้นคอมไพล์เลอร์จะเอาโค้ดจาก abc.i
ไปแทนประโยค #include "abc.i"
ก่อนที่จะคอมไพล์โค้ดนั้น เราก็จะได้โค้ดหน้าตาแบบนี้ครับ
int main(char** args, int argc) {
printf("ABC");
return 0;
}
คิดว่าน่าจะเห็นภาพนะครับว่าทำงานอย่างไร
ทั้งนี้การใช้งานลักษณะนี้นี่ไม่ค่อยได้รับความนิยมในระยะหลัง และผมเองก็ไม่แนะนำซะด้วย การใช้ #include
ลักษณะนี้ทำให้การไล่โปรแกรมเวลามีปัญหาทำได้ยากมากครับ ผมยืนยันได้ เพราะผมเคยทำงานกับโปรเจคที่ใช้ ไฟล์ include เยอะมากในลักษณะนี้มาก่อน บอกตามตรง มันน่าปวดหัวมากครับ
การใช้งาน #include
การใช้งาน #include
ที่ได้รับความนิยมและเป็นที่แนะนำกันก็คือการใช้ ไฟล์ include ในเชิงของการทำ forward declaration ยกตัวอย่างเช่น ถ้าเรามีไฟล์หลายไฟล์ที่ใช้งานฟังก์ชั่น int calculateAge(date birthdate, date currentdate)
ปัญหาของการใช้ Forward Declaration
ถ้าเราไม่ใช้ #include
เลยเราก็ต้องประกาศฟังก์ชั่นนี้บนทุกไฟล์ที่อ้างอิงถึง (และกำหนดฟังก์ชั่นนี้บนไฟล์ใดไฟล์หนึ่งเพียงไฟล์เดียว ไม่เช่นนั้นตอน link โปรแกรมจะพังอีก) ทุก ๆ ไฟล์ก็จะมีไอ้การประกาศนี้บนส่วนบนสุดของไฟล์ ก่อนหน้าที่ฟังก์ชั่นนี้จะถูกเรียกใช้งาน
int calculateAge(date birthdate, date currentdate);
นี่แค่ฟังก์ชั่นเดียวนะครับ ถ้ามีหลาย ๆ ฟังก์ชั่นนะ มันจะยาวสุด ๆ ไปเลยครับ อาจจะเป็นแบบนี้
int calculateAge(date birthdate, date currentdate);
int compareDate(date date1, date date2);
...
int hundredthFunctionAboutDate();
ถ้าสมมติว่าผมแก้ signature ของฟังก์ชั่นใดฟังก์ชั่นหนึ่ง ผมต้องไปตามแก้มันหมดทุกไฟล์ … ลาสุนัขบวชเลยล่ะครับ
แก้ปัญหาโดยการใช้ #include
ผมสามารถที่จะย้ายเอาการประกาศฟังก์ชั่นทั้งหมดมาใส่ในไฟล์เดียว แล้วใช้ #include
ในทุก ๆ ไฟล์ที่ใช้งานแทน สะดวกไหมครับ ? ผมอาจจะตั้งชื่อไฟล์นี้ว่า datefunction.h
แล้วใช้คำสั่ง #include "datefunction.h"
ในทุก ๆ ไฟล์ที่ผมจะใช้ฟังก์ชั่นที่ประกาศในไฟล์นี้ (ส่วนตัวฟังก์ชั่นจะกำหนดอยู่ที่ไหนนั่นอีกเรื่องนึงนะ)
ถ้าเกิดผมมีการแก้ไข signature ของฟังก์ชั่น เช่นเปลี่ยนจาก int calculateAge(date birthdate, date currentdate)
เป็น float calculate_age(date birthdate, date currentdate)
ก็แค่แก้ในไฟล์ datefunction.h
เท่านั้นเอง ยกเว้นถ้าเกิดว่ามีบางไฟล์ที่ใช้ float ไม่ได้จริง ๆ ก็ไปแก้ไฟล์นั้นเอาอีกที อะไรทำนองนี้
อ้อ ใน C/C++ เราจะเรียกไฟล์ include ในลักษณะที่เป็น forward declaration ว่า header file ครับ และมักจะใช้นามสกุล .h หรือ .hpp
Inclusion Guard
สำหรับ include ไฟล์นั้นก็เหมือนกับไฟล์ที่มีโค้ดของ C/C++ ตามปรกตินั่นล่ะครับ คือมีได้ทุกอย่างอย่างที่ .c หรือ .cpp มี แน่นอนว่านั่นรวมถึงการเรียกใช้คำสั่ง #include
ด้วย
ปัญหามันก็จะมาเมื่อเราใช้ include ไฟล์ที่มี #include
ไฟล์อื่น ๆ เข้ามา และไอ้ไฟล์ที่เรียกใช้ก็ดันไปเรียก #include
ไฟล์นั้นเข้ามา ก็คือ #include
ไฟล์เดียวกันทั้งบนไฟล์ที่เรียกใช้และไฟล์ที่ถูก include เข้ามา เช่น สมมติว่าผมมี
vector.h
struct {
float x;
float y;
float z;
float w;
} vector;
matrix.h
#include "vector.h"
struct {
float x0;
float y0;
float z0;
float w0;
/*....omitted*/
} matrix;
vector multiply(vector v, matrix m);
vector multiply(matrix m, vector v);
usage.c
#include "vector.h"
#include "matrix.h"
ผมละโค้ดที่เรียกใช้ใน usage.c ไว้เพราะว่ามันไม่สำคัญนะครับ แต่เรามาดูตอนคอมไพล์กันเลยดีกว่า ตัวคอมไพล์เลอร์นั้นจะทำการแทนที่โค้ดใน usage.c ให้มีลักษณะแบบนี้
struct {
float x;
float y;
float z;
float w;
} vector;
struct {
float x;
float y;
float z;
float w;
} vector;
struct {
float x0;
float y0;
float z0;
float w0;
/*....omitted*/
} matrix;
vector multiply(vector v, matrix m);
vector multiply(matrix m, vector v);
กลายเป็นว่าเรามี struct ที่ชื่อว่า vector สองตัว … กลายเป็นว่าคอมไพล์ไม่ผ่านครับ declaration สองตัวที่ชื่อเหมือนกันนั้นอยู่บนไฟล์เดียวกันไม่ได้ครับ กลายเป็นการประกาศซ้ำ
หลายคนคงคิดว่า ก็เอา #include "vector.h"
ใน usage.c
ออกไปสิ ทำอย่างนั้นก็ได้ครับ แต่สำหรับโปรแกรมที่ซับซ้อนมาก ๆ เราทำแบบนี้ไม่ได้ครับ คือถ้าให้ include สัก 5 ไฟล์ ใน 10 ไฟล์นี่ก็ไล่ไปเถอะครับ ขอให้โชคดี
วิธีแก้ก็คือ เราจะใช้ preprocessor สามตัวช่วยครับ ตามตัวอย่างโค้ดข้างล่างนี้
vector.h
#ifndef VECTOR_H
#define VECTOR_H
struct {
float x;
float y;
float z;
float w;
} vector;
#endif
matrix.h
#ifndef MATRIX_H
#define MATRIX_H
#include "vector.h"
struct {
float x0;
float y0;
float z0;
float w0;
/*....omitted*/
} matrix;
vector multiply(vector v, matrix m);
vector multiply(matrix m, vector v);
#endif
ผมขออธิบายเฉพาะ vector.h
นะครับ เริ่มจาก #define
ก่อน เจ้า #define
นี้เป็นตัวประกาศ macro ชื่อ VECTOR_H
อยู่ เป็นการบอกให้คอมไพล์เลอร์รู้ครับ คือในกรณีนี้เราไม่กำหนดว่า macro นี้ทำอะไร บอกแค่ว่าให้สร้างขึ้นมาณ.จุดนี้
ส่วนคำสั่ง #ifndef
ย่อมาจาก if not define จะตามท้ายด้วยชื่อ macro เดาจากชื่อคงพอบอกได้ ก็คือถ้ามี macro ชื่อ VECTOR_H
ประกาศเอาไว้ตัวคอมไพล์เลอร์จะเอาโค้ดตั้งแต่ประโยค #ifndef
จนถึง #endif
ออกจากโค้ดก่อนคอมไพล์นั่นเอง
พอเราสั่งคอมไพล์ usage.cpp
แล้วเนี่ย คอมไพล์เลอร์จะเริ่มจากประมวลผล preprocessor ที่บรรทัดแรก จะได้โค้ดหน้าตาแบบนี้
usage.cpp
#ifndef VECTOR_H
#define VECTOR_H
struct {
float x;
float y;
float z;
float w;
} vector;
#endif
#include "matrix.h"
จากนั้นมันจะทำ preprocessor บรรทัดบนสุดต่อ ก็คือ #ifndef VECTOR_H
จนถึง #endif
(มันใช้คู่กันก็เลยทำงานเป็นคู่ครับ) ซึ่งเนื่องจากณ.จุดนี้ยังไม่มีมาโครที่ชื่อ VECTOR_H
ประกาศเอาไว้เลย มันก็เลยคงโค้ดส่วนด้านในระหว่าง #ifndef
จนถึง #endif
เอาไว้ ผลลัพท์ที่ได้ก็จะเป็นแบบนี้
usage.cpp
#define VECTOR_H
struct {
float x;
float y;
float z;
float w;
} vector;
#include "matrix.h"
ตัว preprocessor ข้างบนสุดที่เหลือก็คือ #define VECTOR_H
ตัว compiler เห็นดังนั้น ก็ทำการสร้าง macro เก็บเอาไว้ในหน่วยความจำของตัวเอง แล้วก็เอาโค้ดบรรทัดนี้ออก ได้ผลลัพท์ออกมาเป็นแบบนี้
usage.cpp
struct {
float x;
float y;
float z;
float w;
} vector;
#include "matrix.h"
ต่อไปก็มี preprocessor ตัวที่อยู่บนสุดก็คือ #include "matrix.h"
คอมไพล์เลอร์ก็จะไปเอาเนื้อความของไฟล์matrix.h
มาใส่ตรงนี้
usage.c
struct {
float x;
float y;
float z;
float w;
} vector;
#ifndef MATRIX_H
#define MATRIX_H
#include "vector.h"
struct {
float x0;
float y0;
float z0;
float w0;
/*....omitted*/
} matrix;
vector multiply(vector v, matrix m);
vector multiply(matrix m, vector v);
#endif
พอถึงตรงนี้น่าจะเดาได้แล้ว ครับ มันทำงานคู่ #ifndef
จนถึง #endif
ต่อ เนื่องจาก MATRIX_H
ไม่ได้ถูกประกาศไว้ มันก็เลยคงโค้ดเอาไว้เหมือนเดิม กลายเป็นแบบนี้
usage.c
struct {
float x;
float y;
float z;
float w;
} vector;
#define MATRIX_H
#include "vector.h"
struct {
float x0;
float y0;
float z0;
float w0;
/*....omitted*/
} matrix;
vector multiply(vector v, matrix m);
vector multiply(matrix m, vector v);
แล้วก็ทำงานที่ประโยค #define MATRIX_H
ก็แค่สร้างมาโครขึ้นมาในหน่วยความจำของคอมไพล์เลอร์ แล้วก็ลบโค้ดประโยคนี้ทิ้ง
usage.c
struct {
float x;
float y;
float z;
float w;
} vector;
#include "vector.h"
struct {
float x0;
float y0;
float z0;
float w0;
/*....omitted*/
} matrix;
vector multiply(vector v, matrix m);
vector multiply(matrix m, vector v);
คำสั่งต่อไปก็คือ #include “vector.h” ก็แทนค่าเข้าไปเลย
usage.c
struct {
float x;
float y;
float z;
float w;
} vector;
#ifndef VECTOR_H
#define VECTOR_H
struct {
float x;
float y;
float z;
float w;
} vector;
#endif
struct {
float x0;
float y0;
float z0;
float w0;
/*....omitted*/
} matrix;
vector multiply(vector v, matrix m);
vector multiply(matrix m, vector v);
หลังจากนั้นมันก็ทำงานในส่วนของ #ifndef VECTOR_H
จนถึง #endif
ครับ แต่จำได้ไหมครับว่า เรามีการประกาศ VECTOR_H
เอาไว้แล้วข้างบน (ลองย้อนกลับขึ้นไปอ่านดูนะครับ) ดังนั้นโค้ดประโยคนี้จะไม่ทำอะไรเลย โค้ดที่ได้ก็จะกลายเป็นแบบนี้ไป
usage.c
struct {
float x;
float y;
float z;
float w;
} vector;
struct {
float x0;
float y0;
float z0;
float w0;
/*....omitted*/
} matrix;
vector multiply(vector v, matrix m);
vector multiply(matrix m, vector v);
และสุดท้ายพอไม่มี preprocessor เหลืออยู่แล้ว ตัวคอมไพล์เลอร์ถึงจะเริ่มคอมไพล์โค้ดครับ
นี่คือการทำงานของ inclusion guard ซึ่งเป็นส่วนสำคัญที่ header file ควรจะมี เรื่องหนึ่งที่ต้องระวังก็คือเราต้องใช้ชื่อ macro ที่ไม่ซ้ำกันในแต่ละไฟล์ ไม่เช่นนั้นอาจจะมีปัญหาได้ (ใน MSVC มีคำสั่ง #pragma once
ที่ทำงานเป็น inclusion guard ได้ ถ้าเราไม่สนใจเรื่อง cross-platform จะใช้ตัวนี้แทนก็ได้ครับ)
ครั้งนี้ผมจะขอจบแค่นี้ก่อน ครึ่งหลังเราจะมาทำความเข้าใจกับปัญหาของ include file และจะมาดูกันว่าเราจะแก้ปัญหานั้นได้อย่างไรครับ
โปรแกรมเมอร์ C++ และผู้นิยมดนตรี
ผมเห็นความสำคัญของการกำหนดให้มีเจ้า ” ” แยกกับ นะ เวลาเราตามอ่านเราจะรู้ทันทีเลยว่า ชื่อไฟล์ใน ” ” มันอยู่ใน path ที่ relative กับไฟล์ที่เราอ่านอยู่ ในขณะที่ มันจะไปขึ้นกับการเรียก compiler อีกทีหนึ่ง ซึ่งที่ใช้อยู่ก็ช่วยชี้ชัดเวลาเรามีการเปลี่ยนชื่อไฟล์แล้วตามแก้ได้ดีเหมือนกันฮะ