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语言编译过程详解,预处理,编译,汇编,链接(干货满满)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章