Linux編程之C++1:入門基礎

0 前言

  • 以下環境均採用VMWare虛擬機安裝CentOS6.6環境下編程。
  • 想要在Linux上進行編輯,必須學會Linux基本編輯命令和其他基本命令
    學習路徑:Linux系統基礎一
  • 有編程基礎。

1 安裝編譯器,並開始第一程序

1.1 安裝編譯器gcc &g++

yum install gcc //安裝gcc 
yum install gcc-c++  //安裝g++

1.2 編寫第一個程序

[root@bogon 2018-11-08]# pwd
/studySourceCode/2018-11-08
[root@bogon 2018-11-08]# vi main.cpp 
#include <iostream>
using namespace std;
int main(){
  cout << "Hello World" << endl;
  return 0;
}
[root@bogon 2018-11-08]# g++ main.cpp 
[root@bogon 2018-11-08]# ls -a
.  ..  a.out  main.cpp
[root@bogon 2018-11-08]# ./a.out 
Hello World

編寫程序的方式跟在平時使用windows上的qt之類的一樣。
編譯程序,使用命令 g++ 文件名
會在當前目錄自動生成一個a.out的可執行文件,
執行程序,通過命令 ./a.out
假如我們不想使用系統自動生成a.out的可執行文件,
可以通過使用-o選項,如下:

[root@bogon 2018-11-08]# g++ main.cpp -o HelloWorld
[root@bogon 2018-11-08]# ls -la
總用量 28
drwxr-xr-x. 2 root root 4096 11月  9 05:15 .
drwxr-xr-x. 4 root root 4096 11月  9 05:07 ..
-rwxr-xr-x. 1 root root 6270 11月  9 05:10 a.out
-rwxr-xr-x. 1 root root 6270 11月  9 05:15 HelloWorld
-rw-r--r--. 1 root root  102 11月  9 05:10 main.cpp

可以看到使用命令 g++ 要編譯的文件名 -o 生成的可執行程序文件名來指定自己想要生成的可執行程序的別名。

2 多文件混合編程

首先我們在第二小節的基礎上再次增加一個c文件程序
假如是a.h:

[root@bogon 2018-11-08]# vi a.h
#ifndef _A_H_
#define _A_H_
extern "C" {
        void testC();
}
#endif

編寫實現,a.c

[root@bogon 2018-11-08]# vi a.c
#include "a.h"
#include <stdio.h>

void testC(){
  printf("I'm C language program\n");
}

在main.cpp中調用testC()函數

#include <iostream>
#include "a.h"

using namespace std;
int main(){
  testC();
  cout << "Hello World" << endl;
  return 0;
}

最後再次使用g++編譯程序

[root@bogon 2018-11-08]# g++ main.cpp
/tmp/cczY1jqD.o: In function `main':
main.cpp:(.text+0xa): undefined reference to `testC'
collect2: ld 返回 1

可以看到再次編譯顯示錯誤testC沒有定義,那是爲什麼?
因爲使用了a.c中的函數,但是a.c沒有編譯,那麼只要使用g++命令將a.c編譯一次,如下:

[root@bogon 2018-11-08]# g++ a.c main.cpp -o HelloWorld2
[root@bogon 2018-11-08]# ls -la
總用量 36
drwxr-xr-x. 2 root root 4096 11月  9 05:32 .
drwxr-xr-x. 4 root root 4096 11月  9 05:07 ..
-rw-r--r--. 1 root root   90 11月  9 05:24 a.c
-rw-r--r--. 1 root root   65 11月  9 05:20 a.h
-rwxr-xr-x. 1 root root 6270 11月  9 05:15 HelloWorld
-rwxr-xr-x. 1 root root 6476 11月  9 05:32 HelloWorld2
-rw-r--r--. 1 root root  127 11月  9 05:23 main.cpp
[root@bogon 2018-11-08]# ./HelloWorld2
I'm C language program
Hello World

3 預處理(預編譯),編譯,彙編,鏈接

學過編程的都知道,程序的執行順序是:預處理->編譯->彙編->鏈接->運行。下面就簡單介紹一下如何通過gcc/g++進行這些處理。

3.1 預處理

命令:

gcc -E hello.c -o hello.i

主要作用: 處理關於 “#” 的指令

  • 刪除#define,展開所有宏定義。例#define portnumber 3333
  • 處理條件預編譯 #if, #ifdef, #if, #elif,#endif
  • 處理“#include”預編譯指令,將包含的“.h”文件插入對應位置。這可是遞歸進行的,文件內可能包含其他“.h”文件。
  • 刪除所有註釋。/**/,//。
  • 添加行號和文件標識符。用於顯示調試信息:錯誤或警告的位置。
  • 保留#pragma編譯器指令。(1)設定編譯器狀態,(2)指示編譯器完成一些特定的動作。

3.2 編譯

命令:

gcc -s hello.c -o hello.s

主要作用: 1.掃描(詞法分析),2.語法分析,3.語義分析,4.源代碼優化(中間語言生成),5.代碼生成,目標代碼優化。

  • 將源代碼程序輸入掃描器,將源代碼的字符序列分割成一系列記號。例array[index] = (index + 4) * (2 + 6);
  • 基於詞法分析得到的一系列記號,生成語法樹。
  • 由語義分析器完成,指示判斷是否合法,並不判斷對錯。又分靜態語義:隱含浮點型到整形的轉換,會報warning,動態語義:在運行時才能確定:例1除以3
  • 中間代碼(語言)使得編譯器分爲前端和後端,前端產生與機器(或環境)無關的中間代碼,編譯器的後端將中間代碼轉換爲目標機器代碼,目的:一個前端對多個後端,適應不同平臺。
  • 編譯器後端主要包括:代碼生成器:依賴於目標機器,依賴目標機器的不同字長,寄存器,數據類型等。目標代碼優化器:選擇合適的尋址方式,左移右移代替乘除,刪除多餘指令。

3.3 彙編

命令:

gcc -c hello.c -o hello.o

主要作用: 彙編器是將彙編代碼轉變成可以執行的指令,生成 目標文件。

3.4 鏈接

命令:

gcc hello.o -o hello

主要作用: 通過編譯器的5個步驟後,我們獲得目標代碼,但是裏面的各個地址還沒有確定,空間還沒有分配。

  • 鏈接過程主要包括:地址和空間的分配,符號決議和重定位。
  • 地址和空間:略
  • 符號決議:也可以說地址綁定,分動態鏈接和靜態鏈接,
  • 重定位:假設此時又兩個文件:A,B。A需要B中的某個函數mov的地址,未鏈接前將地址置爲0,當A與B鏈接後修改目標地址,完成重定位。

總結:

預處理, 展開頭文件/宏替換/去掉註釋/條件編譯 (hello.i main .i)
編譯, 檢查語法,生成彙編 ( hello.s main .s)
彙編, 彙編代碼轉換機器碼 (hello.o main.o)
鏈接 鏈接到一起生成可執行程序 a.out

4 編譯

這裏的說的編譯是不準確的,其實包含預處理,編譯和彙編過程。生成的爲機器代碼。我們姑且稱爲編譯。
上面已經介紹過編譯使用的命令爲:

gcc/g++ -c 文件名

示例:

[root@bogon 2018-11-08]# ls -la
總用量 36
drwxr-xr-x. 2 root root 4096 11月  9 05:44 .
drwxr-xr-x. 4 root root 4096 11月  9 05:07 ..
-rw-r--r--. 1 root root   90 11月  9 05:24 a.c
-rw-r--r--. 1 root root   46 11月  9 05:42 a.h
-rwxr-xr-x. 1 root root 6270 11月  9 05:15 HelloWorld
-rwxr-xr-x. 1 root root 6476 11月  9 05:32 HelloWorld2
-rw-r--r--. 1 root root  127 11月  9 05:23 main.cpp
[root@bogon 2018-11-08]# g++ -c a.c -o a.o
[root@bogon 2018-11-08]# g++ -c main.cpp -i main.o
[root@bogon 2018-11-08]# ls -la
總用量 44
drwxr-xr-x. 2 root root 4096 11月  9 05:45 .
drwxr-xr-x. 4 root root 4096 11月  9 05:07 ..
-rw-r--r--. 1 root root   90 11月  9 05:24 a.c
-rw-r--r--. 1 root root   46 11月  9 05:42 a.h
-rw-r--r--. 1 root root 1088 11月  9 05:45 a.o
-rwxr-xr-x. 1 root root 6270 11月  9 05:15 HelloWorld
-rwxr-xr-x. 1 root root 6476 11月  9 05:32 HelloWorld2
-rw-r--r--. 1 root root  127 11月  9 05:23 main.cpp
-rw-r--r--. 1 root root 1964 11月  9 05:45 main.o
[root@bogon 2018-11-08]# g++ main.o a.o -o HelloWorld3
[root@bogon 2018-11-08]# ldd HelloWorld3
        linux-gate.so.1 =>  (0x00cf9000)
        libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x0070c000)
        libm.so.6 => /lib/libm.so.6 (0x004a9000)
        libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x006ec000)
        libc.so.6 => /lib/libc.so.6 (0x0
[root@bogon 2018-11-08]# ./HelloWorld3
I'm C language program
Hello World

通過g++命令的-c選項接文件名的形式將程序進行編譯,
接着通過g++ 接編譯後的*.o文件進行鏈接,生成可執行程序
使用ldd 可執行程序可以查看到該可執行程序所鏈接的庫。
最後通過./程序名進行運行程序

5. make工具

如果有很多源代碼文件,不可能一個一個進行編譯。於是便有了批量編譯的工具。make就是其中一種。make是一個控制計算機程序從代碼源文件到可執行文件或其他非源文件生成過程的工具。
控制命令通過稱爲makefile的文件傳遞給make工具。makefile記錄瞭如何生成可執行文件等命令。
下面我們就通過一個簡單的示例,看看怎樣來編寫Makefile文件,來控制源代碼的編譯,以及可執行文件的生成。

從前面可以知道通過命令
g++ -c *.o即可編譯程序
g++ *.o既可鏈接文件,生成可執行程序
那麼當我們的程序非常龐大的時候,務必.cpp、.c文件時如此之多,那麼我們還是通過一個一個g++ 去敲文件名的形式去編譯和鏈接嗎?這想必是非常麻煩的,也不符合我們程序員的作風,那麼這個時候便引入了make工具,通過makefile腳本,我們便可以輕鬆的管理我們的程序文件,那麼就讓我們看看怎麼編寫makefile吧!

我們接着前面的程序,首先去編寫一個基本的makefile吧:

[root@bogon 2018-11-08]# rm -rf *.o
[root@bogon 2018-11-08]# ls -la
總用量 56
drwxr-xr-x. 2 root root 4096 11月  9 06:02 .
drwxr-xr-x. 4 root root 4096 11月  9 05:07 ..
-rw-r--r--. 1 root root   90 11月  9 05:24 a.c
-rw-r--r--. 1 root root   46 11月  9 05:42 a.h
-rwxr-xr-x. 1 root root 6270 11月  9 05:15 HelloWorld
-rwxr-xr-x. 1 root root 6476 11月  9 05:32 HelloWorld2
-rwxr-xr-x. 1 root root 6480 11月  9 05:46 HelloWorld3
-rw-r--r--. 1 root root  127 11月  9 05:23 main.cpp
[root@bogon 2018-11-08]# vi makefile
start:
        g++ -o main.o -c main.cpp
        g++ -o a.o -c a.c
        g++ -o HelloWorld4 a.o main.o

可以看到上面我們已經編寫了一個makefile,其中

  1. makefile爲腳本的名字
  2. start可以隨便命名, 這裏寫爲start表示是程序的開始部分
  3. start後面接的:,表示start爲可以執行的部分,可通過命令make start執行
  4. g++命令前面的空白部分,注意不是空格,必須是鍵盤左上角部分的tab鍵

最後運行makefile腳本,使用make命令,如下:

[root@bogon 2018-11-08]# make
g++ -o main.o -c main.cpp
g++ -o a.o -c a.c
g++ -o HelloWorld4 a.o main.o
[root@bogon 2018-11-08]# make start
g++ -o main.o -c main.cpp
g++ -o a.o -c a.c
g++ -o HelloWorld4 a.o main.o

因爲start:爲該makefile腳本的第一個可執行部分腳本代碼,所以直接通過make也可執行start:所示部分,與使用make start命令是相同的。
可以看到通過makefile腳本編譯鏈接後的*.o文件是沒有作用的了,那麼我們是否有辦法刪除呢?
如下:

[root@bogon 2018-11-08]# vi makefile 
start:
        g++ -o main.o -c main.cpp
        g++ -o a.o -c a.c
        g++ -o HelloWorld4 a.o main.o
clean:
        rm -rf a.o main.o

可以看到makefile中是完全兼容linux命令的(可以在makefile文件中執行Linux命令),所以只要再添一個clean:部分,刪除相應的*.o文件,即可通過make clean執行,如下:

[root@bogon 2018-11-08]# ls -la
總用量 64
drwxr-xr-x. 2 root root 4096 11月  9 06:18 .
drwxr-xr-x. 4 root root 4096 11月  9 05:07 ..
-rw-r--r--. 1 root root   90 11月  9 05:24 a.c
-rw-r--r--. 1 root root   46 11月  9 05:42 a.h
-rw-r--r--. 1 root root 1088 11月  9 06:09 a.o
-rwxr-xr-x. 1 root root 6270 11月  9 05:15 HelloWorld
-rwxr-xr-x. 1 root root 6476 11月  9 05:32 HelloWorld2
-rwxr-xr-x. 1 root root 6480 11月  9 05:46 HelloWorld3
-rwxr-xr-x. 1 root root 6480 11月  9 06:09 HelloWorld4
-rw-r--r--. 1 root root  127 11月  9 05:23 main.cpp
-rw-r--r--. 1 root root 1964 11月  9 06:09 main.o
-rw-r--r--. 1 root root  110 11月  9 06:16 makefile
[root@bogon 2018-11-08]# make clean
rm -rf a.o main.o
[root@bogon 2018-11-08]# ls -la
總用量 56
drwxr-xr-x. 2 root root 4096 11月  9 06:18 .
drwxr-xr-x. 4 root root 4096 11月  9 05:07 ..
-rw-r--r--. 1 root root   90 11月  9 05:24 a.c
-rw-r--r--. 1 root root   46 11月  9 05:42 a.h
-rwxr-xr-x. 1 root root 6270 11月  9 05:15 HelloWorld
-rwxr-xr-x. 1 root root 6476 11月  9 05:32 HelloWorld2
-rwxr-xr-x. 1 root root 6480 11月  9 05:46 HelloWorld3
-rwxr-xr-x. 1 root root 6480 11月  9 06:09 HelloWorld4
-rw-r--r--. 1 root root  127 11月  9 05:23 main.cpp
-rw-r--r--. 1 root root  110 11月  9 06:16 makefile

如果我們僅僅知道如上makefile的編寫的話,那麼別人一看肯定知道我們是菜鳥,那麼怎麼對上面的makefile進行優化呢?
如下:

[root@bogon 2018-11-08]# vi makefile 
XX=g++

start:
        $(XX) -o main.o -c main.cpp
        $(XX) -o a.o -c a.c
        $(XX) -o HelloWorld4 a.o main.o
clean:
        rm -rf a.o main.o
[root@bogon 2018-11-08]# make
g++ -o main.o -c main.cpp
g++ -o a.o -c a.c
g++ -o HelloWorld4 a.o main.o  

上面Makefile文件的優化,用到變量,定義方式:變量別名=變量值,使用方式爲:$(變量別名)類似於一個簡單的賦值操作,也非常類似c和c++中的宏定義去定義一個全局的變量。
這樣做的好處是,當有相同的變量值(文件名,命令等)重複時,可能我們需要批量去管理該變量,那麼這時候只需要定義一個全局的變量,以助於我們方便管理該變量,
如我們上述所示的g++我們以後可能會替換成gcc等,那麼使用之前的形式,便需要一個個需修改,通過如上述所示變可以直接修改XX=右邊的值即可。是不是方便很多。

寫到這裏,懂的人看了其實還是認爲你是個菜鳥,那麼是否可以繼續優化呢?接着看到:

[root@bogon 2018-11-08]# vi makefile
X=g++

start:main.o a.o
        $(XX) -o HelloWorld4 a.o main.o

a.o:
        $(XX) -o a.o -c a.c

main.o:
        $(XX) -o main.o -c main.cpp

clean:
        rm -rf a.o main.o   

我們修改的部分涉及到,start:main.o a.o,這個表示在start:部分執行之前,先去查找main.o與a.o是否存在,不存在則去執行下面的部分,使其先生成a.o &main.o,如果存在則直接進行鏈接。

a.o:
        $(XX) -o a.o -c a.c

main.o:
        $(XX) -o main.o -c main.cpp

然後使用make進行編譯:

[root@bogon 2018-11-08]# make
g++ -o HelloWorld4 a.o main.o
[root@bogon 2018-11-08]# make clean
rm -rf a.o main.o
[root@bogon 2018-11-08]# make
g++ -o main.o -c main.cpp
g++ -o a.o -c a.c
g++ -o HelloWorld4 a.o main.o

可以看到可以成功進行編譯,那麼這樣帶來的好處又是什麼呢?
我們接着修改下,a.c吧,如下:

[root@bogon 2018-11-08]# vi a.c
#include "a.h"
#include <stdio.h>

void testC(){
  printf("I'm C language program\n");  
  printf("change!!!!\n");
}
[root@bogon 2018-11-08]# rm a.o
rm:是否刪除普通文件 "a.o"?y
[root@bogon 2018-11-08]# make
g++ -o a.o -c a.c
g++ -o HelloWorld4 a.o main.o

通過上面分析,可以清晰的看到好處就是:

  1. 當已經編譯生成了.o文件,則不會再進行編譯,會直接進行鏈接。
  2. 當某個文件進行了修改,只會再次編譯該缺少的文件。

這樣當一個項目非常大的時候,這是非常節省編譯時間的,不需要去編譯重複的文件。
當寫到這裏,其實這個makefile還只是很普通的,那麼我們繼續優化吧:

[root@bogon 2018-11-08]# mv a.c a.cpp
XX=g++

start:main.o a.o
        $(XX) -o HelloWorld5 main.o a.o

.cpp.o:
        $(XX) -o $@ -c $<

clean:
        rm -rf a.o main.o


可以看到上面我們將之前的兩部分編譯部分合成了一句,通過:

  1. $<表示編譯的以.cpp結尾的源文件,所以我們上面首先通過mv命令將a.c重命名爲a.cpp方便演示,
  2. $@表示將編譯的結果重命名爲.o文件

接着我們看下make效果:

[root@bogon 2018-11-08]# make
g++ -o HelloWorld5 main.o a.o
[root@bogon 2018-11-08]# make clean
rm -rf a.o main.o
[root@bogon 2018-11-08]# make
g++ -o main.o -c main.cpp
g++ -o a.o -c a.cpp
g++ -o HelloWorld5 main.o a.o
[root@bogon 2018-11-08]# vi a.cpp
#include "a.h"
#include <stdio.h>

void testC(){
  printf("I'm C language program\n");
  printf("change!!!!\n");
  printf("Change!!!again\n");
}
[root@bogon 2018-11-08]# make
g++ -o a.o -c a.cpp
g++ -o HelloWorld5 main.o a.o
[root@bogon 2018-11-08]# ./HelloWorld5 
I'm C language program
change!!!!
Change!!!again
Hello World

通過上面分析,可以清晰的看到好處就是:

  1. 當已經編譯生成了.o文件,則不會再進行編譯,會直接進行鏈接。
  2. 語句更加簡便,方便管理
  3. 當某個文件進行了修改,只會再次編譯修改的文件

這樣當一個項目非常大的時候,這是非常節省編譯時間的,不需要去編譯重複的文件。
到了這裏makefile已經是一個比較合格的了,但是這裏還是不夠的,我們接着優化:

[root@bogon 2018-11-08]# vi makefile 
XX=g++
SRCS=main.cpp\
        a.cpp

OBJS=$(SRCS:.cpp=.o)

EXEC=HellWorld6

start:$(OBJS)
        $(XX) -o$(EXEC) $(OBJS)

.cpp.o:
        $(XX) -o $@ -c $<

clean:
        rm -rf $(OBJS)

最後可以看到我們將所有我們可能需要批量修改的變量通過定義變量別名的方式定義在腳本中,再次聲明怎麼定義:通過:變量別名=變量(文件名,命令等),使用的時候,只需要使用$(變量別名)OBJS=$(SRCS:.cpp=.o)表示將所有.cpp文件前綴文件名直接複製到文件名.o,這樣當我們增加了.cpp文件後就不用手動去增加.o文件。

[root@bogon 2018-11-08]# make clean
rm -rf main.o a.o
[root@bogon 2018-11-08]# make
g++ -o main.o -c main.cpp
g++ -o a.o -c a.cpp
g++ -o HellWorld6 main.o a.o
[root@bogon 2018-11-08]# vi main.cpp 
#include <iostream>
#include "a.h"

using namespace std;
int main(){
  testC();
  cout << "Hello World!!!!!!!!!!" << endl;
  return 0;
}
[root@bogon 2018-11-08]# make
g++ -o main.o -c main.cpp
g++ -o HellWorld6 main.o a.o

可以看到上面我們最終版的makefile的好處是:

  1. 當已經編譯生成了.o文件,則不會再進行編譯,會直接進行鏈接。
  2. 語句更加簡便,方便管理
  3. 當某個文件進行了修改,只會再次編譯修改的文件。那麼make是怎麼知道你修改了那些文件呢?其實它是根據.cpp與.o的最後修改時間去判斷是否需要編譯,當.o文件都不存在時,則判斷失去意義。

6 windows上的代碼移植到linux

在學習C++的文章中,我們在第十九小節學寫了windows上socket通信,那麼現在我們就將這個例子直接移植到linux上。

首先通過之前用過的工具WinSCP將qt寫過的例子文件拖到Linux對應目錄:

[root@bogon 2018-11-16]# ls -la
總用量 20
drwxr-xr-x. 2 root root 4096 11月 17 07:34 .
drwxr-xr-x. 8 root root 4096 11月 17 07:33 ..
-rw-r--r--. 1 root root  229 9月  24 2017 main.cpp
-rw-r--r--. 1 root root 3265 11月 17 07:28 udp.cpp
-rw-r--r--. 1 root root  238 12月 17 2017 udp.h

接着跟着之前學習過的,編寫一個makefile

[root@bogon 2018-11-16]# vi makefile 
XX=g++
SRCS=main.cpp\
        udp.cpp

OBJS=$(SRCS:.cpp=.o)

EXEC=MyUdp

start:$(OBJS)
        $(XX) -o$(EXEC) $(OBJS)

.cpp.o:
        $(XX) -o $@ -c $<
clean:
        rm -rf $(OBJS)

比較簡單,直接將之前學習過的makefile改下源文件SRCS,和可執行程序名字EXEC

需要注意的是之前學習中我們編寫的文件名爲:udp.c,現在我們臨時把它更名爲udp.cpp,因爲目前學習的makefile中我們僅支持一種形式的文件,也就是都是統一的.cpp或者都是統一的.c

最後make編譯一下

[root@bogon 2018-11-16]# make
g++ -o main.o -c main.cpp 
g++ -o udp.o -c udp.cpp 
udp.cpp:3:22: 錯誤:WinSock2.h:沒有那個文件或目錄
udp.cpp: In function ‘int socket_send(const char*)’:
udp.cpp:10: 錯誤:‘DWORD’在此作用域中尚未聲明
udp.cpp:10: 錯誤:expected ‘;’ before ‘ver’
udp.cpp:11: 錯誤:‘WSADATA’在此作用域中尚未聲明
udp.cpp:11: 錯誤:expected ‘;’ before ‘wsaData’
udp.cpp:12: 錯誤:‘ver’在此作用域中尚未聲明
udp.cpp:12: 錯誤:‘MAKEWORD’在此作用域中尚未聲明
udp.cpp:13: 錯誤:‘wsaData’在此作用域中尚未聲明
udp.cpp:13: 錯誤:‘WSAStartup’在此作用域中尚未聲明
udp.cpp:19: 錯誤:‘SOCKET’在此作用域中尚未聲明
udp.cpp:19: 錯誤:expected ‘;’ before ‘st’
udp.cpp:20: 錯誤:聚合‘sockaddr_in addr’類型不完全,無法被定義
udp.cpp:22: 錯誤:‘AF_INET’在此作用域中尚未聲明
udp.cpp:23: 錯誤:‘htons’在此作用域中尚未聲明
udp.cpp:24: 錯誤:‘inet_addr’在此作用域中尚未聲明
udp.cpp:39: 錯誤:‘st’在此作用域中尚未聲明
udp.cpp:39: 錯誤:‘sendto’在此作用域中尚未聲明
udp.cpp:41: 錯誤:‘st’在此作用域中尚未聲明
udp.cpp:41: 錯誤:‘closesocket’在此作用域中尚未聲明
udp.cpp:42: 錯誤:‘WSACleanup’在此作用域中尚未聲明
udp.cpp: In function ‘int socket_receive()’:
udp.cpp:49: 錯誤:‘DWORD’在此作用域中尚未聲明
udp.cpp:49: 錯誤:expected ‘;’ before ‘ver’
udp.cpp:50: 錯誤:‘WSADATA’在此作用域中尚未聲明
udp.cpp:50: 錯誤:expected ‘;’ before ‘wsaData’
udp.cpp:51: 錯誤:‘ver’在此作用域中尚未聲明
udp.cpp:51: 錯誤:‘MAKEWORD’在此作用域中尚未聲明
udp.cpp:52: 錯誤:‘wsaData’在此作用域中尚未聲明
udp.cpp:52: 錯誤:‘WSAStartup’在此作用域中尚未聲明
udp.cpp:58: 錯誤:‘SOCKET’在此作用域中尚未聲明
udp.cpp:58: 錯誤:expected ‘;’ before ‘st’
udp.cpp:59: 錯誤:聚合‘sockaddr_in addr’類型不完全,無法被定義
udp.cpp:61: 錯誤:‘AF_INET’在此作用域中尚未聲明
udp.cpp:62: 錯誤:‘htons’在此作用域中尚未聲明
udp.cpp:63: 錯誤:‘INADDR_ANY’在此作用域中尚未聲明
udp.cpp:63: 錯誤:‘htonl’在此作用域中尚未聲明
udp.cpp:66: 錯誤:‘st’在此作用域中尚未聲明
udp.cpp:66: 錯誤:‘bind’在此作用域中尚未聲明
udp.cpp:69: 錯誤:聚合‘sockaddr_in sendaddr’類型不完全,無法被定義
udp.cpp:77: 錯誤:‘recvfrom’在此作用域中尚未聲明
udp.cpp:78: 錯誤:‘inet_ntoa’在此作用域中尚未聲明
udp.cpp:83: 錯誤:‘st’在此作用域中尚未聲明
udp.cpp:83: 錯誤:‘closesocket’在此作用域中尚未聲明
udp.cpp:84: 錯誤:‘WSACleanup’在此作用域中尚未聲明
make: *** [udp.o] 錯誤 1

會發現有一大堆錯誤,但是不要緊,我們先看到之前原始的udp.cpp

#include <string.h>
#include <stdio.h>
#include <WinSock2.h>


#pragma comment(lib,"ws2_32.lib")
int socket_send(const char *IP)
{

    //初始化socket
    DWORD ver;
    WSADATA wsaData;
    ver = MAKEWORD(1,1);//在調用WSAStartup()要告訴window,我用什麼版本的socket
    WSAStartup(ver,&wsaData);//windows要求,只要用socket,第一步,必須調用這個函數
    //初始化Socket完成
    
    //建立一個socket,第一個參數是指定socket要用哪個協議,AF_INET代表我要用TCP/IP協議
    //第二個參數SOCK_DGRAM意思是要用UDP協議
    //第三個參數一般默認填0
    SOCKET st = socket(AF_INET,SOCK_DGRAM,0);
    struct sockaddr_in addr;
    memset(&addr,0,sizeof(addr));//初始化結構addr
    addr.sin_family = AF_INET;//代表要使用TCP/IP的地址
    addr.sin_port = htons(8080);//host to net short
    addr.sin_addr.s_addr = inet_addr(IP);//"127.0.0.1" 這個IP代表自己
//    unsigned long laddr = inet_addr("192.168.6.200");
//    printf("%x\n",laddr);
//    unsigned char *p = &laddr;
//    printf("%u,%u,%u,%u\n",*(p),*(p+1),*(p+2),*(p+3));

    char buf[1024] = {0};
    size_t rc;
    while(1)
    {
        memset(buf, 0, sizeof(buf));
        gets(buf);
        if(buf[0] == '0')
        break;
        //發送udp數據
        rc = sendto(st,buf,strlen(buf),0,(struct sockaddr *)&addr,sizeof(addr));
    }
    closesocket(st);//使用完socket要將其關閉
    WSACleanup();//釋放win socket內部相關資源
    return rc;

}

int socket_receive()
{
    //初始化socket
    DWORD ver;
    WSADATA wsaData;
    ver = MAKEWORD(1,1);//在調用WSAStartup()要告訴window,我用什麼版本的socket
    WSAStartup(ver,&wsaData);//windows要求,只要用socket,第一步,必須調用這個函數
    //初始化Socket完成

    //建立一個socket,第一個參數是指定socket要用哪個協議,AF_INET代表我要用TCP/IP協議
    //第二個參數SOCK_DGRAM意思是要用UDP協議
    //第三個參數一般默認填0
    SOCKET st = socket(AF_INET,SOCK_DGRAM,0);
    struct sockaddr_in addr;
    memset(&addr,0,sizeof(addr));//初始化結構addr
    addr.sin_family = AF_INET;//代表要使用TCP/IP的地址
    addr.sin_port = htons(8080);//host to net short
    addr.sin_addr.s_addr = htonl(INADDR_ANY);//作爲接收方,不需要指定具體的IP地址,接受的主機是

    int rc=0;
    if (bind(st,(struct sockaddr *)&addr,sizeof(addr)) > -1 )//將端口號和程序綁定
    {
        char buf[1024] = {0};
        struct sockaddr_in sendaddr;
        memset(&sendaddr, 0 , sizeof(sendaddr));
         int len = sizeof(sendaddr);
         
        while(1)
        {
            memset(buf, 0, sizeof(buf));
            //接受udp數據
            rc = recvfrom(st,buf,sizeof(buf),0, (struct sockaddr *)&sendaddr,&len);
            inet_ntoa(sendaddr.sin_addr);//這個函數是不可重入函數
            printf("sendIp:%s,message:%s\n",inet_ntoa(sendaddr.sin_addr),buf);
        }
    }
    closesocket(st);//使用完socket要將其關閉
    WSACleanup();//釋放win socket內部相關資源
    
    return rc;
}


unsigned long getIp(const char* ip){
    unsigned long tempIp=*ip;
}

首先linux中不需要加載lib庫,刪除:

#pragma comment(lib,"ws2_32.lib")

其次linux不用初始化socket和釋放socket,這部分代碼是windows特有的,刪除:

 //初始化socket
    DWORD ver;
    WSADATA wsaData;
    ver = MAKEWORD(1,1);//在調用WSAStartup()要告訴window,我用什麼版本的socket
    WSAStartup(ver,&wsaData);//windows要求,只要用socket,第一步,必須調用這個函數
    //初始化Socket完成\
    ....
 WSACleanup();//釋放win socket內部相關資源
 ....

WinSock2.h 找不到就直接刪除包含的頭文件:

#include <WinSock2.h>

以上修改完畢後,在make一下:

[root@bogon 2018-11-16]# make
g++ -o udp.o -c udp.cpp 
udp.cpp: In function ‘int socket_send(const char*)’:
udp.cpp:11: 錯誤:‘SOCKET’在此作用域中尚未聲明
udp.cpp:11: 錯誤:expected ‘;’ before ‘st’
udp.cpp:12: 錯誤:聚合‘sockaddr_in addr’類型不完全,無法被定義
udp.cpp:14: 錯誤:‘AF_INET’在此作用域中尚未聲明
udp.cpp:15: 錯誤:‘htons’在此作用域中尚未聲明
udp.cpp:16: 錯誤:‘inet_addr’在此作用域中尚未聲明
udp.cpp:31: 錯誤:‘st’在此作用域中尚未聲明
udp.cpp:31: 錯誤:‘sendto’在此作用域中尚未聲明
udp.cpp:33: 錯誤:‘st’在此作用域中尚未聲明
udp.cpp:33: 錯誤:‘closesocket’在此作用域中尚未聲明
udp.cpp: In function ‘int socket_receive()’:
udp.cpp:44: 錯誤:‘SOCKET’在此作用域中尚未聲明
udp.cpp:44: 錯誤:expected ‘;’ before ‘st’
udp.cpp:45: 錯誤:聚合‘sockaddr_in addr’類型不完全,無法被定義
udp.cpp:47: 錯誤:‘AF_INET’在此作用域中尚未聲明
udp.cpp:48: 錯誤:‘htons’在此作用域中尚未聲明
udp.cpp:49: 錯誤:‘INADDR_ANY’在此作用域中尚未聲明
udp.cpp:49: 錯誤:‘htonl’在此作用域中尚未聲明
udp.cpp:52: 錯誤:‘st’在此作用域中尚未聲明
udp.cpp:52: 錯誤:‘bind’在此作用域中尚未聲明
udp.cpp:55: 錯誤:聚合‘sockaddr_in sendaddr’類型不完全,無法被定義
udp.cpp:63: 錯誤:‘recvfrom’在此作用域中尚未聲明
udp.cpp:64: 錯誤:‘inet_ntoa’在此作用域中尚未聲明
udp.cpp:69: 錯誤:‘st’在此作用域中尚未聲明
udp.cpp:69: 錯誤:‘closesocket’在此作用域中尚未聲明

會發現錯誤少了一些,目前提示很多**在此作用域中尚未聲明,說明需要加入對應的頭文件,那麼如何知道該類型在Linux中對應的頭文件呢,以socket爲例,使用如下命令:

    [root@bogon 2018-11-16]# man socket
    SOCKET(2)                  Linux Programmer’s Manual                 SOCKET(2)
    
    NAME
           socket - create an endpoint for communication
    
    SYNOPSIS
           #include <sys/types.h>          /* See NOTES */
           #include <sys/socket.h>
    
           int socket(int domain, int type, int protocol);
    
    DESCRIPTION
           socket() creates an endpoint for communication and returns a descriptor.
    
           The domain argument specifies a communication domain; this  selects  the
           protocol  family  which  will be used for communication.  These families
           are  defined  in  <sys/socket.h>.   The  currently  understood   formats
           include:
    
           Name                Purpose                          Man page
           AF_UNIX, AF_LOCAL   Local communication              unix(7)
           AF_INET             IPv4 Internet protocols          ip(7)

可以找到該類型需要的頭文件,其他的以此類推,加完之後在make一下:

[root@bogon 2018-11-16]# make
g++ -o udp.o -c udp.cpp 
udp.cpp: In function ‘int socket_send(const char*)’:
udp.cpp:14: 錯誤:‘SOCKET’在此作用域中尚未聲明
udp.cpp:14: 錯誤:expected ‘;’ before ‘st’
udp.cpp:34: 錯誤:‘st’在此作用域中尚未聲明
udp.cpp:36: 錯誤:‘st’在此作用域中尚未聲明
udp.cpp:36: 錯誤:‘closesocket’在此作用域中尚未聲明
udp.cpp: In function ‘int socket_receive()’:
udp.cpp:47: 錯誤:‘SOCKET’在此作用域中尚未聲明
udp.cpp:47: 錯誤:expected ‘;’ before ‘st’
udp.cpp:55: 錯誤:‘st’在此作用域中尚未聲明
udp.cpp:72: 錯誤:‘st’在此作用域中尚未聲明
udp.cpp:72: 錯誤:‘closesocket’在此作用域中尚未聲明
make: *** [udp.o] 錯誤 1

首先SOCKET在Linux中是int類型的,closetsocket函數在linux中是close函數,並且和上述一樣推出close函數的頭文件,加入close函數相應的頭文件
再make一下:

[root@bogon 2018-11-16]# make
g++ -o udp.o -c udp.cpp 
udp.cpp: In function ‘int socket_receive()’:
udp.cpp:69: 錯誤:從類型‘int*’到類型‘socklen_t*’的轉換無效
udp.cpp:69: 錯誤:  初始化‘ssize_t recvfrom(int, void*, size_t, int, sockaddr*, socklen_t*)’的實參 6
make: *** [udp.o] 錯誤 1

會發現在linux中recvfrom函數中的第6個參數是socklen_t*類型而不是int *類型,更改過來,在make:

[root@bogon 2018-11-16]# make
g++ -o udp.o -c udp.cpp 
g++ -oMyUdp main.o udp.o
udp.o: In function `socket_send(char const*)':
udp.cpp:(.text+0xa7): warning: the `gets' function is dangerous and should not be used.
main.o: In function `main':
main.cpp:(.text+0x1b): undefined reference to `socket_send'
main.cpp:(.text+0x22): undefined reference to `socket_receive'
collect2: ld 返回 1
make: *** [start] 錯誤 1

會發現函數socket_send與socket_receive找不到,之前學習這部分的時候,我們已經講過這個原理了,但是我認爲這個比較重要,所以再講解一遍。

記得之前說過.cpp編寫的程序和.c編寫的程序,編譯器針對這兩種文件編譯形式是不一樣的,如果函數前標明"extern C"則表示該函數需要按照.c的編譯形式去尋找編譯結果,但是現在我們爲了makefile編譯方便,已經將udp.c更名爲了udp.cpp,那麼編譯器會按照.cpp的編譯形式去編譯socket_send與socket_receive函數,但是在main.cpp中調用兩個函數,main.cpp發現這兩個函數使用了"extern C"標明,於是按照.c的編譯形式形成的編譯結果去尋找函數,於是造成了矛盾,出現了函數找不到的情況。

那麼如何解決這個問題呢?
我們只需要將兩個函數標明的"extern C"幹掉,這樣main.cpp自然會按照.cpp的編譯形式形成的編譯結果去尋找兩個函數,結果就是正確的。

最後再make一下,發現已經是成功了:

[root@bogon 2018-11-16]# make
g++ -o main.o -c main.cpp 
g++ -o udp.o -c udp.cpp 
g++ -oMyUdp main.o udp.o
udp.o: In function `socket_send(char const*)':
udp.cpp:(.text+0xa7): warning: the `gets' function is dangerous and should not be used.

最後貼出修改後的udp.h與udp.cpp:

#ifndef UPD_H
#define UPD_H
int socket_send(const char *IP);
int socket_receive();
#endif // UPD_H
#include <string.h>
#include <stdio.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define SOCKET int

int socket_send(const char *IP)
{

    //建立一個socket,第一個參數是指定socket要用哪個協議,AF_INET代表我要用TCP/IP協議
    //第二個參數SOCK_DGRAM意思是要用UDP協議
    //第三個參數一般默認填0
    SOCKET st = socket(AF_INET,SOCK_DGRAM,0);
    struct sockaddr_in addr;
    memset(&addr,0,sizeof(addr));//初始化結構addr
    addr.sin_family = AF_INET;//代表要使用TCP/IP的地址
    addr.sin_port = htons(8080);//host to net short
    addr.sin_addr.s_addr = inet_addr(IP);//"127.0.0.1" 這個IP代表自己
//    unsigned long laddr = inet_addr("192.168.6.200");
//    printf("%x\n",laddr);
//    unsigned char *p = &laddr;
//    printf("%u,%u,%u,%u\n",*(p),*(p+1),*(p+2),*(p+3));

    char buf[1024] = {0};
    size_t rc;
    while(1)
    {
        memset(buf, 0, sizeof(buf));
        gets(buf);
        if(buf[0] == '0')
        break;
        //發送udp數據
        rc = sendto(st,buf,strlen(buf),0,(struct sockaddr *)&addr,sizeof(addr));
    }
    close(st);//使用完socket要將其關閉
    return rc;

}

int socket_receive()
{

    //建立一個socket,第一個參數是指定socket要用哪個協議,AF_INET代表我要用TCP/IP協議
    //第二個參數SOCK_DGRAM意思是要用UDP協議
    //第三個參數一般默認填0
    SOCKET st = socket(AF_INET,SOCK_DGRAM,0);
    struct sockaddr_in addr;
    memset(&addr,0,sizeof(addr));//初始化結構addr
    addr.sin_family = AF_INET;//代表要使用TCP/IP的地址
    addr.sin_port = htons(8080);//host to net short
    addr.sin_addr.s_addr = htonl(INADDR_ANY);//作爲接收方,不需要指定具體的IP地址,接受的主機是

    int rc=0;
    if (bind(st,(struct sockaddr *)&addr,sizeof(addr)) > -1 )//將端口號和程序綁定
    {
        char buf[1024] = {0};
        struct sockaddr_in sendaddr;
        memset(&sendaddr, 0 , sizeof(sendaddr));
        socklen_t len = sizeof(sendaddr);

        while(1)
        {
            memset(buf, 0, sizeof(buf));
            //接受udp數據
            rc = recvfrom(st,buf,sizeof(buf),0, (struct sockaddr *)&sendaddr,&len);
            inet_ntoa(sendaddr.sin_addr);//這個函數是不可重入函數
            printf("sendIp:%s,message:%s\n",inet_ntoa(sendaddr.sin_addr),buf);
        }
    }

    close(st);//使用完socket要將其關閉
    return rc;
}


unsigned long getIp(const char* ip){
    unsigned long tempIp=*ip;
}

那麼現在我們在linux運行該程序:

[root@bogon 2018-11-16]# ls -la
總用量 40
drwxr-xr-x. 2 root root 4096 11月 17 08:43 .
drwxr-xr-x. 8 root root 4096 11月 17 07:33 ..
-rw-r--r--. 1 root root  229 11月 17 08:19 main.cpp
-rw-r--r--. 1 root root 1680 11月 17 08:43 main.o
-rw-r--r--. 1 root root  157 11月 17 08:20 makefile
-rwxr-xr-x. 1 root root 7762 11月 17 08:43 MyUdp
-rw-r--r--. 1 root root 2680 11月 17 08:40 udp.cpp
-rw-r--r--. 1 root root  104 11月 17 08:43 udp.h
-rw-r--r--. 1 root root 2344 11月 17 08:43 udp.o
[root@bogon 2018-11-16]# ./MyUdp 

在這裏插入圖片描述
可以看到會發現不起作用,那麼是爲什麼呢?是由於linux防火牆的原因,我們只需要將linux防火牆關閉即可,
關閉Linux系統的防火牆,否則Windows不能夠連接Linux
(1) 重啓後永久性生效: 開啓:chkconfig iptables on 關閉:chkconfig iptables off
(2) 即時生效,重啓後失效: 開啓:service iptables start 關閉:service iptables stop

[root@bogon 2018-11-16]# service iptables stop
iptables:將鏈設置爲政策 ACCEPT:filter                    [確定]
iptables:清除防火牆規則:                                 [確定]
iptables:正在卸載模塊:                                   [確定]
[root@bogon 2018-11-16]# ./MyUdp 

然後在嘗試發送,會發現已經成功
在這裏插入圖片描述
但是發送中文卻還是有點問題,可以看到我發了兩個中文的你好,但是卻只顯示了一個,因爲我的SecurtCRT是設置的UTF-8格式的字符,而windows終端默認是gb2312,所有後面,我將SecurtCRT設置爲gb2312之後,成功顯示了windows發過來的你好
在這裏插入圖片描述
上面我們已經成功將windows上的程序移植到了linux,並運行起來了,但是假如我們又要從linux中移植到windows呢?那豈不是又要更改一遍?這樣豈不是很麻煩,那該如何做呢?

一般我們會通過宏定義去區分,當需要時,打開該宏,那麼如何改呢?我們拿到原始windows上的程序,通過宏直接加入linux增加的代碼,最終如下:

#include <string.h>
#include <stdio.h>
#include "udp.h"
#ifdef MYLINUX
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define SOCKET int
#else
#include <WinSock2.h>
#endif


#pragma comment(lib,"ws2_32.lib")
int socket_send(const char *IP)
{
#ifndef MYLINUX
    //初始化socket
    DWORD ver;
    WSADATA wsaData;
    ver = MAKEWORD(1,1);//在調用WSAStartup()要告訴window,我用什麼版本的socket
    WSAStartup(ver,&wsaData);//windows要求,只要用socket,第一步,必須調用這個函數
    //初始化Socket完成
#endif
    //建立一個socket,第一個參數是指定socket要用哪個協議,AF_INET代表我要用TCP/IP協議
    //第二個參數SOCK_DGRAM意思是要用UDP協議
    //第三個參數一般默認填0
    SOCKET st = socket(AF_INET,SOCK_DGRAM,0);
    struct sockaddr_in addr;
    memset(&addr,0,sizeof(addr));//初始化結構addr
    addr.sin_family = AF_INET;//代表要使用TCP/IP的地址
    addr.sin_port = htons(8080);//host to net short
    addr.sin_addr.s_addr = inet_addr(IP);//"127.0.0.1" 這個IP代表自己
//    unsigned long laddr = inet_addr("192.168.6.200");
//    printf("%x\n",laddr);
//    unsigned char *p = &laddr;
//    printf("%u,%u,%u,%u\n",*(p),*(p+1),*(p+2),*(p+3));

    char buf[1024] = {0};
    size_t rc;
    while(1)
    {
        memset(buf, 0, sizeof(buf));
        gets(buf);
        if(buf[0] == '0')
        break;
        //發送udp數據
        rc = sendto(st,buf,strlen(buf),0,(struct sockaddr *)&addr,sizeof(addr));
    }
#ifdef MYLINUX
    close(st);
#else
    closesocket(st);//使用完socket要將其關閉
    WSACleanup();//釋放win socket內部相關資源
#endif
    return rc;

}

int socket_receive()
{
#ifndef MYLINUX
    //初始化socket
    DWORD ver;
    WSADATA wsaData;
    ver = MAKEWORD(1,1);//在調用WSAStartup()要告訴window,我用什麼版本的socket
    WSAStartup(ver,&wsaData);//windows要求,只要用socket,第一步,必須調用這個函數
    //初始化Socket完成
#endif

    //建立一個socket,第一個參數是指定socket要用哪個協議,AF_INET代表我要用TCP/IP協議
    //第二個參數SOCK_DGRAM意思是要用UDP協議
    //第三個參數一般默認填0
    SOCKET st = socket(AF_INET,SOCK_DGRAM,0);
    struct sockaddr_in addr;
    memset(&addr,0,sizeof(addr));//初始化結構addr
    addr.sin_family = AF_INET;//代表要使用TCP/IP的地址
    addr.sin_port = htons(8080);//host to net short
    addr.sin_addr.s_addr = htonl(INADDR_ANY);//作爲接收方,不需要指定具體的IP地址,接受的主機是

    int rc=0;
    if (bind(st,(struct sockaddr *)&addr,sizeof(addr)) > -1 )//將端口號和程序綁定
    {
        char buf[1024] = {0};
        struct sockaddr_in sendaddr;
        memset(&sendaddr, 0 , sizeof(sendaddr));
#ifdef MYLINUX
         socklen_t len = sizeof(sendaddr);
#else
         int len = sizeof(sendaddr);
#endif

        while(1)
        {
            memset(buf, 0, sizeof(buf));
            //接受udp數據
            rc = recvfrom(st,buf,sizeof(buf),0, (struct sockaddr *)&sendaddr,&len);
            inet_ntoa(sendaddr.sin_addr);//這個函數是不可重入函數
            printf("sendIp:%s,message:%s\n",inet_ntoa(sendaddr.sin_addr),buf);
        }
    }

#ifdef MYLINUX
    close(st);
#else
    closesocket(st);//使用完socket要將其關閉
    WSACleanup();//釋放win socket內部相關資源
#endif
    return rc;
}


unsigned long getIp(const char* ip){
    unsigned long tempIp=*ip;
}

可以看到我們通過MYLINXUX這樣的宏定義與否來判斷啓動Linux還是windows模塊的相關代碼,

那麼MYLINUX如何定義呢?我們需要在makefile中加入如下:

XX=g++
SRCS=main.cpp\
        udp.cpp

OBJS=$(SRCS:.cpp=.o)

EXEC=MyUdp

start:$(OBJS)
        $(XX) -o$(EXEC) $(OBJS)

.cpp.o:
        $(XX) -o $@ -c $< -DMYLINUX
clean:
        rm -rf $(OBJS)

通過-D接着宏定義的形式,因爲宏定義是需要預編譯,所以跟.cpp.o一起

-DMYLINUX

最後接着編譯,還是沒有問題:

[root@bogon 2018-11-16]# make clean
rm -rf main.o udp.o
[root@bogon 2018-11-16]# clear
[root@bogon 2018-11-16]# ls -la
總用量 32
drwxr-xr-x. 2 root root 4096 11月 17 09:11 .
drwxr-xr-x. 8 root root 4096 11月 17 07:33 ..
-rw-r--r--. 1 root root  229 11月 17 08:19 main.cpp
-rw-r--r--. 1 root root  166 11月 17 09:10 makefile
-rwxr-xr-x. 1 root root 7762 11月 17 09:11 MyUdp
-rw-r--r--. 1 root root 3693 11月 17 09:09 udp.cpp
-rw-r--r--. 1 root root  104 11月 17 08:43 udp.h
[root@bogon 2018-11-16]# make
g++ -o main.o -c main.cpp -DMYLINUX
g++ -o udp.o -c udp.cpp -DMYLINUX
g++ -oMyUdp main.o udp.o
udp.o: In function `socket_send(char const*)':
udp.cpp:(.text+0xa7): warning: the `gets' function is dangerous and should not be used.
[root@bogon 2018-11-16]# 

7 linux上編譯so庫,併兼容.c與.cpp的調用

在windows上我們通常想封裝一些接口給其他人調用,但是卻不想別人看到我們的實現,我們通常會編寫一個dll,通過dll的形式提供給其他人調用,這樣別人只用關心接口使用,不用關心實現,也不能修改具體實現。保證代碼的安全可靠。

那麼在Linux中,通常我們採用的是共享庫的形式,也就是so庫的形式提供給調用者,那麼具體如何實現呢?

首先我們簡單的申明一個比較大小的函數,通過傳入兩個參數,比較兩個參數的大小,誰大就返回誰的形式實現它,代碼如下:

[root@bogon 2018-11-18]# vim mylib.h
#ifndef MY_LIB_H
#define MY_LIB_H

int max(int a,int b);

#endif
[root@bogon 2018-11-18]# vim mylib.c
#include "mylib.h"

int max(int a,int b){
  return a>b?a:b;
}

編寫好max接口後,那麼該如何將它編譯成一個so庫的形式呢?通過我們的學習,我們都知道linux程序都是makefile來進行編譯的,那麼so庫的makefile該如何編寫呢?

[root@bogon 2018-11-18]# vim makefile
XX=gcc
SRCS=mylib.c

OBJS=$(SRCS:.c=.o)

EXEC=libmylib.so

start:$(OBJS)
        $(XX) -o$(EXEC) $(OBJS) -shared

.c.o:
        $(XX) -o $@ -c $< -fPIC
clean:
        rm -rf $(OBJS)

在linux編譯so,

  1. 首先so名稱必須以lib開頭,以so結尾
  2. 鏈接時必須加入-fPIC,表示這是編譯so庫,沒有偏移位置
  3. 在 編譯時,需要指定 -shared 選項,標明是一個共享庫

最後編譯一把,

[root@bogon 2018-11-18]# ls -la
總用量 20
drwxr-xr-x. 2 root root 4096 11月 19 05:09 .
drwxr-xr-x. 9 root root 4096 11月 19 04:55 ..
-rw-r--r--. 1 root root  161 11月 19 05:08 makefile
-rw-r--r--. 1 root root   62 11月 19 05:05 mylib.c
-rw-r--r--. 1 root root   65 11月 19 05:00 mylib.h
[root@bogon 2018-11-18]# make
gcc -o mylib.o -c mylib.c -fPIC
gcc -olibmylib.so mylib.o -shared
[root@bogon 2018-11-18]# ls -la
總用量 28
drwxr-xr-x. 2 root root 4096 11月 19 05:13 .
drwxr-xr-x. 9 root root 4096 11月 19 04:55 ..
-rwxr-xr-x. 1 root root 3876 11月 19 05:13 libmylib.so
-rw-r--r--. 1 root root  161 11月 19 05:08 makefile
-rw-r--r--. 1 root root   62 11月 19 05:05 mylib.c
-rw-r--r--. 1 root root   65 11月 19 05:00 mylib.h
-rw-r--r--. 1 root root  685 11月 19 05:13 mylib.o
[root@bogon 2018-11-18]# 

共享庫製作完成,那麼現在我們通過模擬一個main.c程序去調用我們編寫的共享庫是否可用吧:

[root@bogon 2018-11-18]# vim main.c
#include "mylib.h"
#include <stdio.h>

int main(){
   printf("max=%d",max(5,6));
   return 0;
}

然後在編寫makefile,看看效果吧:

[root@bogon 2018-11-18]# mv makefile makefile.lib
[root@bogon 2018-11-18]# vim makefile
XX=gcc
SRCS=main.c

OBJS=$(SRCS:.c=.o)

EXEC=mylibTest

start:$(OBJS)
        $(XX) -o $(EXEC) $(OBJS)

.c.o:
        $(XX) -o $@ -c $<
clean:
        rm -rf $(OBJS)

好的,最後再make一下吧:

[root@bogon 2018-11-18]# make
g++ -o main.o -c main.c
g++ -o mylibTest main.o
main.o: In function `main':
main.c:(.text+0x19): undefined reference to `max(int, int)'
collect2: ld 返回 1
make: *** [start] 錯誤 1
[root@bogon 2018-11-18]# 

會發現max是沒有定義的,因爲我們想使用共享庫,但是卻沒有在makefile中指定該共享庫,因此裏面的函數肯定是無法找到的,那麼如何在makefile中指定共享庫呢?

[root@bogon 2018-11-18]# vim makefile
XX=gcc
SRCS=main.c

OBJS=$(SRCS:.c=.o)

EXEC=mylibTest

start:$(OBJS)
        $(XX) -o $(EXEC) $(OBJS) -L. -lmylib

.c.o:
        $(XX) -o $@ -c $<
clean:
        rm -rf $(OBJS)
 [root@bogon 2018-11-18]# make
gcc -o main.o -c main.c
gcc -o mylibTest main.o -L. -lmylib 

-L 表示指定共享路徑 .代表當前目錄 -l指定共享庫名稱,因爲linux默認認爲共享庫以lib開頭,.so結尾,所以直接指定mylib名字即可。
最後make顯然已經生成了mylibTest程序

那麼現在如果我們需要將庫運行在main.cpp裏面呢?會有問題嗎?我們試驗下吧:

[root@bogon 2018-11-18]# cp main.c main.cpp
[root@bogon 2018-11-18]# vim makefile 
XX=g++
SRCS=main.cpp

OBJS=$(SRCS:.cpp=.o)

EXEC=mylibTest

start:$(OBJS)
        $(XX) -o $(EXEC) $(OBJS) -L. -lmylib 

.cpp.o:
        $(XX) -o $@ -c $<
clean:
        rm -rf $(OBJS)                          

因爲.cpp需要通過g++編譯,所以需要將gcc改爲g++進行編譯

[root@bogon 2018-11-18]# make clean
rm -rf main.o
[root@bogon 2018-11-18]# make
g++ -o main.o -c main.cpp
g++ -o mylibTest main.o -L. -lmylib 
main.o: In function `main':
main.cpp:(.text+0x19): undefined reference to `max(int, int)'
collect2: ld 返回 1
make: *** [start] 錯誤 1
[root@bogon 2018-11-18]# 

會發現max函數找不到,經過第6小節的學習我們知道,是因爲.cpp和.c的編譯形式,導致編譯結果不一樣,main.cpp認爲max函數是.cpp編譯出來的,所以找不到,那麼按照之前的寫法我們作出更改mylib.h:

[root@bogon 2018-11-18]# vim mylib.h
#ifndef MY_LIB_H
#define MY_LIB_H

extern "C"{
int max(int a,int b);
}
#endif
[root@bogon 2018-11-18]# make clean
rm -rf main.o
[root@bogon 2018-11-18]# make
g++ -o main.o -c main.cpp
g++ -o mylibTest main.o -L. -lmylib 
[root@bogon 2018-11-18]# 

再次編譯即可成功,那麼現在問題來了,我們假如又想在main.c中調用呢?我們試一試呢?

[root@bogon 2018-11-18]# vim makefile
XX=gcc
SRCS=main.c

OBJS=$(SRCS:.c=.o)

EXEC=mylibTest

start:$(OBJS)
        $(XX) -o $(EXEC) $(OBJS) -L. -lmylib 

.c.o:
        $(XX) -o $@ -c $<
clean:
        rm -rf $(OBJS)
[root@bogon 2018-11-18]# make
gcc -o main.o -c main.c
In file included from main.c:2:
mylib.h:4: 錯誤:expected identifier or ‘(’ before string constant
make: *** [main.o] 錯誤 1

會發現mylib.h是有錯誤的,那是因爲extern "C"只在c++中才有,
那麼豈不是又要刪除剛剛的修改呢?那麼是否有一個兼蓉的辦法呢?答案是肯定的。

通過使用宏定義"__cplucplus" ,當使用.cpp作爲調用者時,編譯器會定義該宏定義,那麼這樣就簡單了,可以根據該宏定義是否定義:

[root@bogon 2018-11-18]# vim mylib.h
#ifndef MY_LIB_H
#define MY_LIB_H
#ifdef __cplusplus
extern "C"{
#endif
int max(int a,int b);

#ifdef __cplusplus
}

#endif
#endif
-bash: majke: command not found
[root@bogon 2018-11-18]# make
gcc -o main.o -c main.c
gcc -o mylibTest main.o -L. -lmylib 

最後發現便可以成功了,那麼再改成.cpp試試呢?

[root@bogon 2018-11-18]# vim makefile
XX=g++

SRCS=main.cpp

OBJS=$(SRCS:.cpp=.o)

EXEC=mylibTest

start:$(OBJS)
        $(XX) -o $(EXEC) $(OBJS) -L. -lmylib 

.cpp.o:
        $(XX) -o $@ -c $<
clean:
        rm -rf $(OBJS)

最後發現也是可以成功的。

主要內容摘自Linux下C++編程基礎
同時部分內容參考一下博客:

  1. gcc——預處理(預編譯),編譯,彙編,鏈接
  2. c語言編譯過程詳解,預處理,編譯,彙編,鏈接(乾貨滿滿)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章