1. 程式人生 > 實用技巧 >Linux下C++程式編譯連結的學習例子

Linux下C++程式編譯連結的學習例子

Linux下C++程式編譯連結的學習例子

一、專案例子的整體結構

  1. 本專案旨在簡單學習C++編譯連線的過程以及Makefile和Cmake的簡單語法,專案結構如下:

    .
    ├── appC
    │ ├── include
    │ │ ├── ADll.h
    │ │ ├── A.h
    │ │ ├── BDll.h
    │ │ └── B.h
    │ ├── lib
    │ │ └── linux
    │ └── src
    │     ├── main
    │     ├── main.cpp
    │     └── Makefile
    ├── cleanAll.sh
    ├── libA
    │ ├── output
    │ │ ├── include
    │ │ │ ├── ADll.h
    │ │ │ └── A.h
    │ │ └── lib
    │ │     └── linux
    │ └── src
    │     ├── A.cpp
    │     ├── ADll.h
    │     ├── A.h
    │     └── Makefile
    ├── libB
    │ ├── output
    │ │ ├── include
    │ │ │ ├── ADll.h
    │ │ │ ├── A.h
    │ │ │ ├── BDll.h
    │ │ │ └── B.h
    │ │ └── lib
    │ │     └── linux
    │ └── src
    │     ├── B.cpp
    │     ├── BDll.h
    │     ├── B.h
    │     └── Makefile
    └── readme.md
    
    
  2. 依賴關係為:main->B->A,cleanAll.sh用來刪除生成的*.o *.a *.so檔案,以便學習不同的連結方式。

二、程式碼結構

  1. main程式碼如下:

    #include "B.h"
    #include <iostream>
    
    using namespace std;
    
    int main (int argc, char *argv[])
    {
        B objB;
        int result = objB.func(1, 2);
        
        cout << "result = " << result << endl;
    
    }
    
  2. libA程式碼如下:

    /**
    *********A.cpp************
    */
    #include "A.h"
    #include <iostream>
    
    using namespace std;
    
    A::A()
    {
        cout << "A ctor" << endl;
    }
    
    A::~A()
    {
        cout << "A dtor" << endl;
    }
    
    int A::func(int x, int y)
    {
        return (x + y) * 2;
    }
     /**
     *********A.h************
     */
     #ifndef _A_H_
    #define _A_H_
    
    #include "ADll.h"
    
    class LIBA_API A {
    public:
        A();
        ~A();
        int func(int x, int y);
    
    };
    
    #endif
    
  3. libB程式碼如下:

     /**
    *******************B.cpp********************
     */
    #include "B.h"
    #include "A.h"
    #include <iostream>
    using namespace std;
    
    class BImpl {
    public:
        BImpl() {};
        ~BImpl() {};
    
        int func(int x, int y){
            return objA.func(x, y);
        }
    
    private:
        A objA;
    };
    
    B::B() : _impl(new BImpl)
    {
        cout << "B ctor" << endl;
    }
    
    B::~B()
    {
      cout << "B dtor" << endl;
      delete _impl;  
    }
    
    int B::func(int x, int y){
        return _impl->func(x, y);
    }
    
    
     /**
    ************B.h*******************
     */
    #ifndef _B_H_
    #define _B_H_
    
    #include "BDll.h"
    
    class BImpl;
    
    class LIBB_API B {
    public:
        B();
        ~B();
        int func(int x, int y);
    
    private:
        BImpl *_impl;
    };
    
    #endif
    
  4. 以libA下的makefile為例學習makefile:

    CXX			= g++ #編譯器
    AR			= ar   # 對.o檔案進行歸檔,生成庫
    
    CXXFLAGS = -shared -g -Wall -fPIC -std=c++11 -I./      #g++引數:-shared 呼叫動態庫  -g 可執行程式包含除錯資訊 -Wall 編譯後顯示所有警告     																											-fPIC 真正的共享.so   -std=c++11 支援c++11標準; -I 後跟標頭檔案地址
    LDFLAGS  = -L./  # 庫檔案地址      ### -Wl,-rpath=your_lib_dir 只有-L選項時有可能編譯通過但是執行時not found 庫,因為-L只有編																							譯時有效,可以通過加上此命令記住連結庫的位置;
    
    SRCS = $(wildcard *.cpp)  # wildcard 函式,展開所有的.cpp檔案 
    OBJS = $(patsubst %.cpp, %.o, $(SRCS))  patsubst函式  將.cpp全部替換成.o並返回,等於objs等於.cpp編譯的.o檔案
    $(info SRCS = $(SRCS))
    $(info OBJS = $(OBJS))  # info make的列印函式
    
    TARGET	= libA.so libA.a   # 編譯目標
    OUTPUT	= ../output #輸出位置
    
    all: clean $(TARGET) install copy2B     # 讓所有的操作順次執行,
    .PHONY: clean install copy2B   #因為make的規則是 目標 :依賴 ,當 main : main.c 時,為了從依賴獲取目標,就會執行之後的規則;
                                           #但是當clean:後沒有依賴時,make會認為依舊獲取到目標,因此不再執行clean後的規則,
                                                 #用.PHONY關鍵字,意為欺騙make,clean目標還未達成,即會執行clean之後的規則;
    							#  目標  : 依賴
    							      #  規則
    
    
    .cpp.o:  #自動將.cpp識別為原始檔字尾,.o識別為目標檔案字尾;   g++ -c 將.cpp編譯成.o ,而不是可執行
    	@echo "\ncompile..."
    	$(CXX) -c $< -o $@ -DLIBA_API_EXPORTS $(CXXFLAGS)     # $@目標檔案   $^所有的依賴檔案  $<第一個依賴檔案  ¥% 僅當目標時函式庫文																																		件,表示規則中的目標成員名;
    
    $(TARGET): $(OBJS)   # .o到.so .a
    	$(CXX) $^ -o $@ $(CXXFLAGS) $(LDFLAGS)  #g++生成動態庫 gcc -fPIC -shard -o 目標  原始檔   gcc -c -fPIC  *.c     gcc  -shared -o *.so *.o 
    	$(AR) -crsv libA.a $^     # ar  建立靜態庫 -c 禁止再建立庫的檔案時產生的正常資訊 -r 如果已存在,則替換 -s 無論是否修改了庫的內容,強制生																成庫符號表 其餘命令可查詢學習
    
    install:
    	@echo "\ninstall to output..."
    	cp A.h ADll.h $(OUTPUT)/include   # cp 移動
    	cp -a $(TARGET) ${OUTPUT}/lib/linux
    
    clean:
    	@echo "\nclean..."
    	rm -rf $(TARGET) *.o  #清除中間檔案
    
    copy2B:
    	@echo "\ncopy to libB..."
    	cp ../output/include/*	../../libB/include
    	cp ../output/lib/linux/*	../../libB/lib/linux
    
    
  5. 現在已經學會了簡單的makefile檔案編寫,接下來就可以進行實踐嘗試了;

三、動態庫的連結

  1. 將A編譯成動態庫,B依賴A編譯成動態庫,main依賴B生成可執行檔案,依賴A,B的動態庫,

    可以通過修改makefile來達到目的。

    問題1:

    ​ A,B的編譯都順利通過,編譯main時依舊順利通過,但當執行./main時報錯:

    ./main: error while loading shared libraries: libA.so: cannot open shared object file: No such file or directory
    
    
    # 檢查makefile裡的配置
    LDFLAGS  = -lA -lB -L../lib/linux -Wl,-rpath=../lib/linux
    

    通過ldd ./main可以檢視動態庫連結資訊,可以發現成功連結到libB.so,但是libA.so not found

    $ ldd ./main
    	linux-vdso.so.1 (0x00007ffe1d7ff000)
    	libB.so => ../lib/linux/libB.so (0x00007f173646a000)
    	libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f17360e1000)
    	libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f1735ec9000)
    	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1735ad8000)
    	libA.so => not found
    	libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f173573a000)
    	/lib64/ld-linux-x86-64.so.2 (0x00007f1736870000)
    
    

    解決方法:由於在Linux上,可執行程式會去系統預設下的路徑去尋找共享庫(*.so),因此動態庫放在自己的目錄下時,編譯時會通過,但是執行時卻會提示找不到。

    1. linux系統預設的動態庫路徑有 /lib/ /usr/lib 因此在編譯動態庫時可以直接在makefile裡將動態庫cp到系統的預設路徑下,即可解決問題。

    2. 如果想自己建立一個用來學習測試的庫目錄,可以將自己的目錄新增到系統的配置檔案中,也可以達到目的,方法如下:

      sudo vim /etc/ld.so.conf
      

      通過vim或gedit開啟配置檔案,新增自己的目錄。

      sudo  /sbin/ldconfig
      

      新增完成之後,儲存並通過以上命令載入配置即可。

      至於為什麼可以找到B卻不能找到A,筆者猜測是由於可執行檔案先載入B,此時搜尋完成指定目錄,而B又依賴於A,此時去搜索A的時候,之前的搜尋已經完成,因此直接跳過指定目錄,轉而去搜索系統預設的其他路徑。以上只是隨意猜測,實際具體的原因筆者並未深入瞭解;

  2. 如果我們自己生成的B庫依賴於第三方的A庫,但是不希望主程式知道A庫,該怎麼辦呢?

    即將A編譯成靜態庫,在編譯B時直接將A完成連結,此時只需要提供B的動態庫給主程式即可。我們可以通過修改makefile檔案來測試。

  3. 如果A,B都編譯為靜態庫,同樣需要將A,B都交給main。