C语言补坑

之前大一学C语言的时候有点迷迷糊糊,加之时间久了,就遗忘的差不多了,最近项目要写一个Socket的程序,而且需要在windows下写,顺手记录一下之前我不太会的和我自己忘记掉的。

环境配置

环境可以选择IDE,著名的VS和后起之秀CLion都是很棒的选择,也可以自己配置。

编辑器

编辑器其实都OK,用记事本什么的都行,我选的是sublime text3

编译器

其实编译器应该是最主要的一块了,也是windows下面的比较恶心的一块,它不像Ubuntu一样自带gcc编译器,需要自行安装。

如果只是想快捷省事,其实可以选择VC++,但是我因为以前socket是一直在linux上写并且编译执行的,所以这次想在windows下装个gcc的移植版。这里比较推荐的是MinGW,一款windows下面的gcc的移植版。安装完成之后再把二进制文件写入系统的环境变量中,在命令行中输入gcc --version ,看到有输出就可以了。

C基础补充

头文件

头文件是什么?

头文件中包含了 C 函数声明和宏定义,像知名的<stdio.h>就大致上长这样:

1565854956050

可以理解为其实头文件就像java中的接口,就是一些函数的定义但是没有实现。

C语言中有两种头文件,一种是编译器带的(所以如果要寻找头文件,需要去编译器的安装目录下去找,在ubuntu上我记得是/usr/include里面,可以通过运行命令cpp -v /dev/null -o /dev/null查看),还有一种是用户编写的,系统的使用是#include<stdio.h> 而用户自定义的是#include "myheadfile.h"

头文件的作用

显然是为了人读起来方便,实际上在编译的时候,#include这条会被文件的内容代替。举个简单的例子:

创建了一个test.h ,里面的内容很简单,就是定义了一个函数int getMax(); 然后在另外一个函数中引用:

1
2
3
4
5
#include "test.h"

int main(){
return 0;
}

这其实等价于:

1
2
3
4
5
int getMax();

int main(){
return 0;
}

小tips:在有多个 .h 文件和多个 .c 文件的时候,往往我们会用一个 global.h 的头文件来包括所有的 .h 文件,然后在除 global.h 文件外的头文件中 包含 global.h 就可以实现所有头文件的包含,同时不会乱。方便在各个文件里面调用其他文件的函数或者变量。

头文件带来的问题

重复定义

最明显的就是重复引用两个头文件的问题了。显然如果上面的test.h 出现两次,就相当于你把getMax这个函数定义了两次。但是你肯定会说,我又不傻,我肯定不会写两遍#include "test.h"的呀….考虑这么一个情况,a的头文件中已经引入了b,然后你引用了a.h,然后因为你对a的不熟悉,又去引用了b.h,这样b不就相当于被引用了两次么,实际情况嵌套什么的还要复杂得多。这会导致什么问题呢?轻则加重编译器的负担,重则项目出错(比如两个全局变量这种问题)所以这个时候就需要宏定义(#define)条件编译(#ifdef)出马了。

接下来以引入<stdio.h>为例:

1
2
3
4
5
//推荐:首先在头文件前后都加入下划线,然后全部大写,并且将.转化成下划线,这是推荐,不一定非要这么做
#ifndef _STDIO_H_
#define _STDIO_H_
头文件的定义内容
#endif

但是实际上的<stdio.h>是这么做的:

1565856688333

这么做的话,可以保证这个头文件只会被引用一次,因为被引用了一次之后_SSP_STDIO_H这个值就会被赋予1了,不再是未定义了,也就不会进入头文件接下来的内容了。


宏还有一个作用,就是用来给不同的系统定义不同的量,让程序可以稍微灵活一点。

1
2
3
4
5
#ifdef WINDOWS
#define TYPE long
#else
#define TYPE float
#endif

显然只要在这段代码之前定义一下WINDOWS 就可以让TYPE是long类型,否则就是float类型。

更多的预处理命令可参考这篇博客

一次编译过程

假设现在我希望自己实现一个C语言的相加和相减的一个函数的头文件,这样以后我只需要引入这个头文件就可以使用我自己的封装好的函数了,具体见下:

1
2
3
4
5
6
// mymath.h 这里只是定义了,并没有实现
#ifndef MYMATH_H
#define MYMATH_H
int add(int a, int b);
int sub(int a, int b);
#endif
1
2
3
4
5
6
7
8
// mymath.c 这里进行了实现
int add(int a, int b){
//here is comment
return a+b;
}
int sub(int a, int b){
return a-b;
}

最后是一个程序调用自己写的头文件并使用。

1
2
3
4
5
6
7
8
9
10
11
// main.c
#include <stdio.h>
#include "mymath.h"
int main(){
int a = 2;
int b = 3;
int sum = add(a, b);
printf("a=%d, b=%d, a+b=%d\n", a, b, sum);
int res = sub(a, b);
printf("a=%d, b=%d, a-b=%d\n", a, b, res);
}

整个文件的结构就像这样:

1
2
3
4
.
├── main.c
├── mymath.c
└── mymath.h

OK文件准备好了,可以开始试验了。注:以下文件的-o outputfile 只是为了更加便于理解加入的选项,实际中可以不加。

预处理(Preprocessing)

通过执行gcc --help 可以发现下面有这么一条,-E Preprocess only; do not compile, assemble or link,所以只需要运行gcc -E main.c -o main.i即可。这样就会预处理main.c,然后生成main.i,这个main.i会把#include进行展开,但是如果你打开会发现,只有add和sub的定义,但是没有实现(因为我们从头到尾都没告诉系统去找mymath.c这个文件,而函数的实现放在了这个文件中,这一步是放到最后的链接部分进行实现的),所以也应该同时编译mymath.c文件gcc -E mymath.c -o mymath.imymath.c这个文件只有实现,所以展开后你会发现只有那一句额外的注释没了。这样这两个.i文件就构建好了。

编译(Compilation)

第二步就是编译了:gcc -S main.i -o main.s 以及 gcc -S mymath.i -o mymath.s 当然这里你也可以直接从.c文件直接到.s文件,而不用中间的.i文件。然后就生成了汇编语言写的了。

汇编(Assemble)

这步的任务是把汇编代码转成机器码。

as main.s -o main.oas mymath.s -o mymath.o 生成了机器码,这一步结束之后mymathmain这两个程序还没有交集。也就是如果你是电脑,你只知道现在在main中有add和sub这两个函数,但是实际上你并不知道这个实现放在哪里,所以需要链接器出场。

链接(Linking)

将程序需要的文件链接成可执行文件。gcc -o res.out main.o mymath.o 链接有两种,静态链接和动态链接,动态链接只是名字里带着“链接”,其实并不是在这个阶段完成的,其实是在运行的时候把库文件链接上去的。

一点小总结

  • gcc -E main.c 预处理main.c,并且自动生成main.i
  • gcc -S main.c 预处理+编译main.c,并且自动生成main.s
  • gcc -c main.c 预处理+编译+转成机器码,自动生成main.o
  • gcc -o main main.o 预处理+编译+转成机器码+生成可执行文件,自动生成main
  • gcc main.c 一步到位,但是前提是不需要别的依赖文件

大型软件的编译

所有人都用过从源码包编译安装软件的过程,大体上来说都是这样的:

1
2
3
./configure
make
make install

一般来说后面两条会被make && make install这一条代替。

所以这三条命令究竟是什么意思呢?

configure

如果我是那个被安装的软件,在我被下载到用户的文件夹里之后,我首先需要知道:我应该被安装到哪里?与此同时,一般的软件可能还分成了好几个功能模块,用户可以按需安装,不必一次性全部安装。除了这些,当然还需要知道系统的库和头文件在哪里,以及为了之后编译的效率,还需要对库的编译顺序等做出优化,所以在这一步还会生成makefile,为后续服务。所以configure就是这么一个程序,能够指定软件被安装在了哪里,哪些组件需要被安装,生成makefile。而这个其实是一个二进制的可执行程序,至于为什么它要叫configure,因为规定呀~

make

make的主要工作,其实就是上面的四个步骤,预处理+编译+汇编+链接。

make install

经过make处理过后的程序,其实仅仅是在原始的目录里面,并没有拷贝到你指定的目录中,所以最后这一步就是把内存的东西拷贝过去,然后再进行一些操作比如通知操作系统啦,修改操作系统的一些文件如/usr/bin啦之类的。

库的使用

还是一样,大学里学的时候根本就没讲过静态链接库和动态链接库,所以只好现在回来填坑了。

首先在这里我有一个module.c,这里为了简洁,并没有加上宏定义防止重复定义,也没有使用头文件。

1
2
3
4
5
6
7
8
9
10
11
12
// module.c
#include <stdio.h>

int my_add(int a,int b);
void my_print(void);

int my_add(int a,int b){
return a+b;
}
void my_print(void){
printf("hello world!\n");
}

然后有一个test.c,里面调用了这两个函数:

1
2
3
4
5
6
7
8
// test.c
#include <stdio.h>
int main(void)
{
my_add(5,6);
my_print();
return 0;
}

这个时候如果直接编译一下test.c,结果会是:

1
2
3
4
5
6
7
8
9
10
11
12
13
gcc test.c                      
test.c: In function ‘main’:
test.c:5:5: warning: implicit declaration of function ‘my_add’ [-Wimplicit-function-declaration]
my_add(5,6);
^~~~~~
test.c:6:5: warning: implicit declaration of function ‘my_print’; did you mean ‘dprintf’? [-Wimplicit-function-declaration]
my_print();
^~~~~~~~
dprintf
/tmp/cca0pWFl.o: In function `main':
test.c:(.text+0x14): undefined reference to `my_add'
test.c:(.text+0x1e): undefined reference to `my_print'
collect2: error: ld returned 1 exit status

显然test.c里面的这两个函数是没定义过的,当然会报错啦,需要解决的话,可以用静态链接库和动态链接库这两种解决方法,具体方法见下。

静态库

首先编译一下库文件module.c,生成机器码文件:gcc -c module.c,此时会在你的路径下生成module.o文件

然后是把生成的obj文件打包成一个静态库文件,这里只有一个module.o被打包进了libmodule.aar -r libmodule.a module.o,此时会在当前目录生成libmodule.a的静态库文件

最后一步就是把之前生成的obj文件和静态链接库一起生成可执行文件:gcc test.o libmodule.a -o test,最后执行一下./test即可看到结果。这个时候就算你把libmodule.a移走,也不会影响test执行。

动态库

使用的两个文件和静态库是一样的。

编译下一下那个module.c,生成动态链接库,然后再在编译test.c的时候指定一下动态链接库就好了。这里说明一下,不是说动态链接库是在程序运行的时候才去动态链接的么,为什么在编译的时候指定呢?其实是告诉程序,当程序执行的时候,应该去找哪个动态链接库。

首先编译动态链接库:gcc -shared -fPIC -o libmodule.so module.c这句执行之后应该会在当前目录下生成libmodule.so这个动态链接库文件。

然后我们编译test.c的时候把这个库文件链接进去即可:gcc -o test test.c -L./ -lmodule,这个时候会有警告,编译器以为你先使用了这两个函数后进行了声明,但是它其实是会处理的,所以忽略这个警告即可。

-L这个选项指定你的库文件在哪里,上例中因为在当前目录下就有libmodule.so文件,所以指定了当前目录,而-l则是指定库的名字,需要去掉前面的lib和后面的“后缀名”(.so或者.a)

最后就可以直接./test看到结果啦。如果这个时候你把当前目录下面的libmodule.so移走,就会出现如下的错误提示:

1
./test: error while loading shared libraries: libmodule.so: cannot open shared object file: No such file or directory

从这里也可以发现动态链接库动态的含义,程序在运行时候需要依赖它,一旦在运行的时候找不到,程序就无法执行了。

还有一点就是,使用静态库会比使用动态库生成的可执行文件大得多(但是我这个例子只大了0.4K,因为是toy demo嘛),毕竟人家静态库是把自己都打包进了程序,而动态库其实就是打个标记,执行的时候再去调用嘛,好坏之处也是一眼就看出来了:用静态库的文件可移植性好,但是文件大;用动态库的文件可移植性差(然而其实你可以把你的动态库和你的程序一起发布呀~),但是文件小。

指针数组和数组指针

指针数组

这个其实很好理解,本质上是一个数组,只不过数组的内容是指针而已。

1
2
3
4
5
6
7
8
int i = 1, j = 2, k = 3;
int *p1[3] = { &i, &j, &k};

// print &j which is the address of j
printf("%d\n", *(p1 + 1));

// print the value of j which is 2
printf("%d\n", **(p1 + 1));

数组指针

首先需要确定这是一个指针,但是指针本身就可以直接指向一个数组。

先来看一段程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
int main(int argc, char const *argv[])
{
int dp[][5] =
{
{1, 2, 3, 4, 5},
{6, 7, 8, 9, 10},
{11, 12, 13, 14, 15}
};

// 故意写成6
int (*array)[6] = dp;

// 这两个地址是一样的
printf("%d\n", dp );
printf("%d\n", array);

// print 11
printf("%d\n", array[1][4]);

// print 10
printf("%d\n", *( (*array) + 9));
return 0;
}

上面为什么会输出11呢?因为你定义的时候确定了array的每一段长度被指定成了6,所以可以间接说明二维数组下标的计算是通过你的定义来进行计算的。

所以问题来了,我明明完全可以通过int *array = dp来取代int (*array)[6] = dp 为什么还要耗费精力呢?这主要就是为了能够使用像array[1][4]这样的语法,所以如果你不想用二维数组两个下标的这种语法,确实可以不用数组指针。

socket编程

win下和linux下又有一些不同,由于这次任务是完成win下的,所以暂且先记录下win的,linux等有时间再来填坑吧。

win client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <WINSOCK2.H>
#include <STDIO.H>

#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 6666
#define SEND_BUFFER_SIZE 1024
#define RECV_BUFFER_SIZE 1024

#pragma comment(lib,"ws2_32.lib") //把ws2_32.lib这个库放入Lib中


int main(int argc, char* argv[])
{
char sendbuf[SEND_BUFFER_SIZE];
char recvbuf[RECV_BUFFER_SIZE];
WORD sockVersion = MAKEWORD(2,2);
WSADATA data;
if(WSAStartup(sockVersion, &data) != 0)
{
return 0;
}

SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sclient == INVALID_SOCKET)
{
printf("invalid socket !");
return 0;
}

sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(SERVER_PORT);
serAddr.sin_addr.S_un.S_addr = inet_addr(SERVER_ADDRESS);
if (connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
{
printf("connect error !");
closesocket(sclient);
return 0;
}

// first, say hello to server
char * sendData = "hello!";
send(sclient, sendData, strlen(sendData), 0);

// then get command from server and run the command
while(1){
int ret = recv(sclient, recvbuf, RECV_BUFFER_SIZE, 0);
if(ret > 0 )
{
recvbuf[ret] = 0x00;
printf(recvbuf);
printf("\n");
if (0 == strcmp(recvbuf, "QUIT")) {
break;
}
FILE *fp;
fp = popen(recvbuf, "r");
while ((fgets(sendbuf, sizeof(sendbuf), fp)) != NULL) {
send(sclient, sendbuf, strlen(sendbuf), 0);
}
send(sclient, END_MESSAGE, strlen(END_MESSAGE), 0);
}else{
break;
}
}

closesocket(sclient);
WSACleanup();
return 0;
}
//编译请使用 gcc client.cpp -o client -lwsock32

win server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/python
#coding=utf-8
import socket

IP_ADDRESS = '127.0.0.1'
PORT = 6666
RECV_BUFFER_SIZE = 1024
END_MESSAGE = b"FINISH"

address = (IP_ADDRESS,PORT)
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(address)
s.listen(5)
while True:
ss,addr = s.accept()
print('got connected from ',addr)

# ↓ get 'hello' from client
ra = ss.recv(RECV_BUFFER_SIZE)
print(ra)

# send message
while True:
command = input("please input command > ")
if 'QUIT' in command:
ss.send(b'QUIT')
break
ss.send(command.encode(encoding="utf-8"))
while True:
ra = ss.recv(RECV_BUFFER_SIZE)
print(ra.decode("gbk"))
if END_MESSAGE in ra:
break
s.close()

简单来说就是通信,然后客户端就能执行服务器端传送过来的命令了。客户端最主要的就是执行服务器发送过来的命令了,原本我用的system,但是这个函数默认的输出是标准输出,也就是屏幕,我希望是无声无息的,然后可以通过重定向system到文件中来解决屏幕输出这个问题,但是还是会残留文件,所以就用了popen来执行shell命令。