#include ในภาษา C และ C++ (ครึ่งแรก)

ทุก ๆ คนที่เคยใช้ 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 และจะมาดูกันว่าเราจะแก้ปัญหานั้นได้อย่างไรครับ

One thought on “#include ในภาษา C และ C++ (ครึ่งแรก)

  1. ผมเห็นความสำคัญของการกำหนดให้มีเจ้า ” ” แยกกับ นะ เวลาเราตามอ่านเราจะรู้ทันทีเลยว่า ชื่อไฟล์ใน ” ” มันอยู่ใน path ที่ relative กับไฟล์ที่เราอ่านอยู่ ในขณะที่ มันจะไปขึ้นกับการเรียก compiler อีกทีหนึ่ง ซึ่งที่ใช้อยู่ก็ช่วยชี้ชัดเวลาเรามีการเปลี่ยนชื่อไฟล์แล้วตามแก้ได้ดีเหมือนกันฮะ

ส่งความเห็นที่ sleepyfurious ยกเลิกการตอบ

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *

This site uses Akismet to reduce spam. Learn how your comment data is processed.