1. 程式人生 > 其它 >呼叫另一個cpp的變數_如何組織一個C++專案:一點淺見(1)

呼叫另一個cpp的變數_如何組織一個C++專案:一點淺見(1)

技術標籤:呼叫另一個cpp的變數執行程式碼後總是會出現很多的的debug [main請問如何解決

由於課題需要,最近在利用《C++ Primer》這本書補習C++知識。當前我遇到了這樣一個問題:該如何正確的編譯一個別人寫的C++專案(即Lammps裡所謂的"UserPackage")。

其實這屬於一類問題,我們可以自然而然地將其表述為:一箇中(甚至大)型的實用C++專案,到底是如何被開發者組織起來的

對類似我這種非科班同學來說,相信大家都曾有過這種疑問。因為非科班生在使用程式語言時,往往特別關心於語法的正確與否,或者某個演算法該如何實現——這些小問題,很多用一個原始檔的程式碼量就能解決(比如C++的一個.cpp或者Python的一個.py)。然而身邊那些實用中、大型軟體,開啟資料夾一看,就知道肯定不是一個檔案就能搞定的。下面我就結合自己的理解,以一個“殺雞用牛刀”的例子,探討一下該如何組織一個真正的專案


這個例子要實現的需求非常簡單:交換兩個數。平臺就選擇科學計算人員都熟悉的Linux系統(Ubuntu-20.04),編譯器選最常用的g++。

"這還不簡單!"你啪的一下新建了一個叫Main.cpp的檔案,很快啊!然後上來就是,一個main函式,一個指標式交換:

# include <iostream>

全部編,編譯通過了啊,執行以後,自然是以點到為止——

3a3bc4b03ec1a22ed43dbe39fe27f3d0.png

慢著,這樣的話本文還有什麼意義......小夥子你要講碼德,程式碼怎麼能搞窩裡鬥呢

沒錯,組織一個專案與僅僅寫一個演算法,最大的區別之一就是分離式編譯。專案越大越是如此,程式碼一旦搞起窩裡鬥,也就是相互耦合

起來,將會給後期的debug和維護工作帶來難以想象的困難。為此,我們不妨靈活的使用#include命令,專門定義一個叫transfer的函式(其實應該叫swap,懶得改了....),單獨放進另一個.cpp檔案裡。這樣,後期一旦發現函式本身有bug,就不用回到主函式所在的Main.cpp裡,去一行一行找了。

新建一個叫way1的資料夾,作為我們的專案資料夾(注意這裡的措辭)。然後在裡面新建兩個.cpp檔案:Main.cpp和transfer.cpp:

Main.cpp:

//方式1 : 通過include一個cpp檔案來實現
# include <iostream>
# include "transfer.cpp"

int main()
{
    int para1 = 1;
    int para2 = 2;
    transfer(&para1,&para2); //呼叫transfer函式
    std::cout << para1 <<","<< para2 <<std::endl;
}

transfer.cpp:

//transfer.cpp
void transfer(int *a,int *b) //形參a,b都是指標
{
    int tmp;
    tmp = *a;
    *a = *b;
    *b = tmp;
}

然後編譯(注意必須進入到專案資料夾去)、執行,依然是順利通過:

56a692f6ab053b8022e119a61edabd5f.png

你若有所思的點點頭,突然大叫到:"誒誒誒,丁老師你也不講碼德,怎麼把.cpp檔案給include進去了?!"

哈哈,這個反應很正常。因為我們平時見到的include,包含的一般是標頭檔案,如Main裡面的標準庫<iostream>。其實,include命令做的是一種文字上的巨集替換。你可以簡單(但不嚴謹)的理解為複製貼上:編譯器在預編譯階段將transfer裡面的內容粘進Main,替你把Main的程式碼補全。所以include一個.cpp還能編譯通過,其實不足為奇。(而且注意,那行g++命令裡並沒有出現transfer.cpp,但編譯器也沒有報錯,這就更加印證了本觀點)

不過你的想法也沒錯,因為一般沒有哪個開發者會這麼寫。實踐上,人們通常利用標頭檔案,把函式和類等的宣告和實現分開寫:宣告寫在.h裡,具體實現寫在.cpp裡。這種做法有巨大優勢:標頭檔案實際上扮演了“介面”的角色。一個大型專案的程式碼之間可能出現相互呼叫的行為,譬如你寫了transfer函式,但有另一個人想呼叫它,那他無需知道你.cpp裡的程式碼,而是直接#include你的transfer函式的標頭檔案就行了。

所以,我們新建一個專案資料夾way2。這個新專案裡,應該包含3個檔案

Main.cpp

//方式2:使用標頭檔案組織專案
# include <iostream>
# include "transfer.h" //引號用於include非標準庫,比如你寫的transfer.h

int main()
{
    int para1 = 1;
    int para2 = 2;
    transfer(&para1,&para2); //呼叫transfer函式
    std::cout << para1 <<","<< para2 <<std::endl;
}

transfer.cpp

//transfer.cpp:transfer函式的具體實現
void transfer(int *a,int *b) //形參a,b都是指標
{
    int tmp;
    tmp = *a;
    *a = *b;
    *b = tmp;
}

transfer.h,也就是標頭檔案。注意,標頭檔案名雖無強制約束,但應該與函式名或類名一致,這也是一種“碼德”哦。

# pragma once //標頭檔案保護符,防止重複編譯。一般都要加上。

void transfer(int *a,int *b); //標頭檔案裡,宣告有一個叫transfer的函式

編譯就有講究了:由於Main裡用到了transfer函式,我們得先把transfer和Main編譯成兩個.o檔案,才能連結(link)起來,形成最終輸出檔案:

05ec1ff2b02575131b3cb91697fe6917.png

至此,我們似乎就獲得了通過標頭檔案,組織起一個專案的思路。問題看似解決了——

且慢,如果一個大專案裡包含很多這種標頭檔案,它們要求的編譯順序又不盡相同,那我們該怎麼管理這個“編譯流程”呢?總不能一個一個用命令列編譯吧。。

實際上,有專門的軟體替我們完成這個工作,那就是大名鼎鼎的makefile。在Linux下,該軟體叫make,你可以通過以下命令檢視有沒有安裝它:

87235c93a47dc290e26a400e23aa2a7a.png

如果命令列提示找不到該程式,可以用以下命令安裝:

sudo apt-get install make

關於make怎麼用,文末放上了一個很詳細的教程供參考。現在只需記住:我們要在資料夾下建立一個叫Makefile的文字檔案,這檔案裡面定義了編譯順序:

Main.exe : Main.o transfer.o
	g++ -o Main.exe Main.o transfer.o

Main.o : Main.cpp transfer.h
	g++ -c Main.cpp

transfer.o : transfer.cpp
	g++ -c transfer.cpp

然後在命令列下敲擊make,一鍵搞定!

203c9ee935745f030b9fbd1349b85c37.png

makefile教程

概述 - 跟我一起寫Makefile 1.0 文件​seisman.github.io