C 语言进阶之面向接口编程和多态

数组指针

数组类型的语法

C 语言中的数组有自己特定的类型,可以通过 typedef 关键字定义数组类型,语法格式为:typedef type(name)[length];,例如:

  • typedef int(MyIntArray)[3];
  • typedef char(MyCharArray)[3];

数组指针类型的语法

  • 数组指针类型用于指向一个数组
  • 可以直接定义数组指针类型:typedef type(*name)[length];

数组类型与数组指针类型的使用

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
#include <stdio.h>

int main() {
// 1. 定义数组类型(会分配内存空间)
typedef int(MyArray)[3];

MyArray array;
array[0] = 1;
array[1] = 2;
array[2] = 3;

for (int i = 0; i < 3; i++) {
printf("array[%d] = %d\n", i, array[i]);
}

printf("\n");

// 2. 定义数组指针类型(不会分配内存空间)
typedef int(*MyPointArray)[3];

int a[3];
MyPointArray pointArray;
pointArray = &a; // 将数组指针类型指向一个数组
(*pointArray)[0] = 11;
(*pointArray)[1] = 22;
(*pointArray)[2] = 33;

for (int j = 0; j < 3; j++) {
printf("a[%d] = %d, ", j, a[j]);
printf("(*pointArray)[%d] = %d\n", j, (*pointArray)[j]);
}

printf("\n");

// 3. 定义一个数组指针变量(不会分配内存空间)
int(*pointArrayVar)[3];

int b[3];
pointArrayVar = &b; // 将数组指针变量指向一个数组
(*pointArrayVar)[0] = 111;
(*pointArrayVar)[1] = 222;
(*pointArrayVar)[2] = 333;

for (int n = 0; n < 3; n++) {
printf("b[%d] = %d, ", n, b[n]);
printf("(*pointArrayVar)[%d] = %d\n", n, (*pointArrayVar)[n]);
}

}

程序运行的输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
array[0] = 1
array[1] = 2
array[2] = 3

a[0] = 11, (*pointArray)[0] = 11
a[1] = 22, (*pointArray)[1] = 22
a[2] = 33, (*pointArray)[2] = 33

b[0] = 111, (*pointArrayVar)[0] = 111
b[1] = 222, (*pointArrayVar)[1] = 222
b[2] = 333, (*pointArrayVar)[2] = 333

函数指针

函数类型的语法

C 语言中的函数有自己特定的类型,可以通过 typedef 关键字定义函数类型,语法格式为:typedef type (name)(parameter list);,例如:

  • typedef int (f)(int, int);
  • typedef void (p)(int);

函数指针类型的语法

  • 函数指针类型用于指向一个函数
  • 函数有三大要素:名称、参数、返回值,函数名是函数体的入口地址
  • 可以通过函数类型定义函数指针类型: FuncType* pointer;
  • 也可以直接定义函数指针类型:typedef type (*pointer)(parameter list);
    • pointer:函数指针变量名
    • type:指向函数的返回值类型
    • parameter list:指向函数的参数类型列表

函数类型与函数指针类型的使用

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
#include <stdio.h>

int add(int a, int b) {
return a + b;
}

int main() {

// 1. 定义函数类型
typedef int (MyFuncType)(int a, int b);

// 通过函数类型定义函数指针类型
MyFuncType* myFuncType = add;
int result = myFuncType(1, 3);
printf("%d + %d = %d\n", 1, 3, result);

// 2. 定义一个函数指针类型(不会分配内存空间)
typedef int (*MyFuncPointType)(int a, int b);

// 加不加上"&"符号都是可以的,如果加上了"&"符号,可以解决C语言版本的兼容问题
// MyFuncPointType myFuncPointType = &add;

MyFuncPointType myFuncPointType = add;
int result2 = myFuncPointType(4, 5);
printf("%d + %d = %d\n", 4, 5, result2);

// 3. 定义函数指针变量(会分配内存空间)
int (*MyFuncPointVar)(int a, int b);
MyFuncPointVar = add;
int result3 = MyFuncPointVar(7, 9);
printf("%d + %d = %d\n", 7, 9, result3);

return 0;
}

程序运行的输出结果如下:

1
2
3
1 + 3 = 4
4 + 5 = 9
7 + 9 = 16

函数类型与函数指针作为函数参数

函数类型作为函数参数

当函数类型做为函数的参数传递给一个被调用函数,被调用函数就可以通过这个函数类型调用外部的函数,这就形成了回调函数。C 语言回调函数的本质是,提前做了一个协议的约定(把函数的参数、函数的返回值类型提前约定)。

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
#include <stdio.h>

// 定义函数类型
typedef int (MyFuncType)(int a, int b);

int add(int a, int b) {
return a + b;
}

int mult(int a, int b) {
return a * b;
}

// 函数类型作为函数参数
int callbackFunc(MyFuncType func) {
return func(3, 6);
}

int main() {
// 通过函数类型定义函数指针类型
MyFuncType* myFuncType = NULL;

myFuncType = add;
int result = callbackFunc(*myFuncType);
printf("result = %d\n", result);

myFuncType = mult;
int result2 = callbackFunc(*myFuncType);
printf("result = %d\n", result2);

return 0;
}

程序运行的输出结果如下:

1
2
result = 9
result = 18

函数指针作为函数参数

当函数指针做为函数的参数传递给一个被调用函数,被调用函数就可以通过这个指针调用外部的函数,这就形成了回调函数。C 语言回调函数的本质是,提前做了一个协议的约定(把函数的参数、函数的返回值类型提前约定)。

  • a) 将 “函数的调用” 和 “函数的实现” 解耦
  • b) 可以模拟 C++ 的多态机制(提前布局 VPTR 指针和虚函数表,找虚函数入口地址来实现函数调用)
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
#include <stdio.h>

int add(int a, int b) {
return a + b;
}

int mult(int a, int b) {
return a * b;
}

// 函数指针做函数参数
int callbackFunc(int (*MyFunc)(int a, int b)) {
return MyFunc(3, 4);
}

int main() {
// 定义函数指针变量
int (*myFuncVar)(int a, int b);

myFuncVar = add;
int result = callbackFunc(myFuncVar);
printf("result = %d\n", result);

myFuncVar = mult;
int result2 = callbackFunc(myFuncVar);
printf("result = %d\n", result2);

return 0;
}

程序运行的输出结果如下:

1
2
result = 7
result = 12

上述的 addmult 函数都是写在同一个源文件当中,假如 add 函数是一个库中的函数,此时就只有使用回调了,通过函数指针参数将外部函数地址传入来实现调用。日后如果库里面的 add 函数的代码作了修改,也不必改动函数调用方的代码,就可以正常实现调用,便于程序的维护和升级。

DLL 动态链接库的使用

下面将介绍 C/C++ 如何开发和调用一款 Socket 客户端的 DLL 动态链接库,该 DLL 主要实现了 Socket 客户端的初始化、报文发送、报文接收、资源释放等功能,第三方可以直接调用该 DLL 实现 Socket 通信。值得一提的是,本案例并没有真正完整地实现 Socket 客户端的底层代码,更多的是使用伪代码来模拟 Socket 客户端的通信。

运行环境说明

这里给出的案例代码和操作步骤,只适用于 Windows 系统的 C/C++ 开发,且依赖 Visual Studio 开发工具,不适用于 Linux 系统的 C/C++ 开发。

DLL 项目的创建

新建 DLL 项目

cplusplus-create-dll-1

cplusplus-create-dll-2

cplusplus-create-dll-3

值得一提的是,通过 Visual Studio 创建 DLL(动态链接库)项目,源文件默认的后缀是 .cpp,如果项目使用的是 C 语言,则需要将自动生成的 dllmain.cpppch.cpp 的文件名改为 dllmain.cpch.c

编写 DLL 代码

  • 创建 socketclient.c 源文件

cplusplus-create-dll-4

  • socketclient.c 源文件的代码如下,其中 __declspec(dllexport) 的作用是将函数导出给 DLL 的调用方
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#include "pch.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

// 定义函数指针类型,用于数据加密
typedef int (*EncodeData)(unsigned char* in, int inlen, unsigned char* out, int* outlen);

// 定义结构体
typedef struct _Sck_Handle {
char version[16];
char ip[16];
int port;
unsigned char* p;
int len;
char* p2;
EncodeData encodeCallback;
} Sck_Handle;

//客户端初始化
__declspec(dllexport)
int socketclient_init(void** handle) {
int ret = 0;
Sck_Handle* tmpHandle = NULL;

if (handle == NULL) {
ret = -1;
printf("function socketclient_init() err :%d check params == NULL err \n", ret);
return ret;
}

tmpHandle = (Sck_Handle*)malloc(sizeof(Sck_Handle));
if (tmpHandle == NULL) {
ret = -2;
printf("function socketclient_init() err :%d malloc err \n", ret);
return ret;
}

memset(tmpHandle, 0, sizeof(Sck_Handle));
strcpy(tmpHandle->version, "1.0.0.1");
strcpy(tmpHandle->ip, "192.168.12.12");
tmpHandle->port = 8081;

//间接赋值
*handle = tmpHandle;
return ret;
}

//客户端报文发送
__declspec(dllexport)
int socketclient_send(void* handle, unsigned char* buf, int buflen) {
int ret = 0;
Sck_Handle* tmpHandle = NULL;

if (handle == NULL || buf == NULL || buflen <= 0) {
ret = -2;
printf("function socketclient_send() err :%d (handle == NULL || buf == NULL || buflen <= 0 ) \n", ret);
return ret;
}
tmpHandle = (Sck_Handle*)handle;

if (tmpHandle->encodeCallback == NULL) {
//明文发送
tmpHandle->len = buflen;
tmpHandle->p = (unsigned char*)malloc(buflen);
if (tmpHandle->p == NULL) {
ret = -2;
printf("function socketclient_send() err :%d malloc len:%d \n", ret, buflen);
return ret;
}
memcpy(tmpHandle->p, buf, buflen);
}
else {
//加密发送
unsigned char crypdata[4096];
int cryptdatalen = 4096;
ret = tmpHandle->encodeCallback(buf, buflen, crypdata, &cryptdatalen);
if (ret != 0) {
printf("function encodeCallback() err :%d \n", ret);
return ret;
}

tmpHandle->len = cryptdatalen;
tmpHandle->p = (unsigned char*)malloc(cryptdatalen);
if (tmpHandle->p == NULL) {
ret = -1;
printf("function socketclient_send() err :%d malloc len:%d \n", ret, cryptdatalen);
return ret;
}
memcpy(tmpHandle->p, crypdata, cryptdatalen);
}
return ret;
}

//客户端报文加密发送
__declspec(dllexport)
int socketclient_send_encode(void* handle, unsigned char* buf, int buflen, EncodeData encodeCallback) {
int ret = 0;
unsigned char cryptbuf[4096];
int cryptbuflen = 4096;
Sck_Handle* tmpHandle = NULL;

if (handle == NULL || buf == NULL || encodeCallback == NULL) {
ret = -1;
printf("function socketclient_send_encode() err :%d (handle == NULL || buf == NULL || encodeCallback == NULL) \n",
ret);
return ret;
}

// 通过函数指针,执行数据的加密操作
ret = encodeCallback(buf, buflen, cryptbuf, &cryptbuflen);
if (ret != 0) {
ret = -2;
printf("function socketclient_send_encode() err :%d check encode_result == 0 err \n", ret);
return ret;
}

tmpHandle = (Sck_Handle*)handle;
tmpHandle->len = cryptbuflen;
tmpHandle->p = (unsigned char*)malloc(cryptbuflen);
if (tmpHandle->p == NULL) {
ret = -3;
printf("function socketclient_send_encode() err :%d malloc len:%d \n", ret, cryptbuflen);
return ret;
}

//把加密的明文缓存到内存中
memcpy(tmpHandle->p, cryptbuf, cryptbuflen);

return 0;
}

//客户端报文接收
__declspec(dllexport)
int socketclient_recv(void* handle, unsigned char* buf, int* buflen) {
int ret = 0;
Sck_Handle* tmpHandle = NULL;

if (handle == NULL || buf == NULL || buflen == NULL) {
ret = -2;
printf("function socketclient_recv() err :%d (handle == NULL || buf == NULL || buflen == NULL ) \n", ret);
return ret;
}
tmpHandle = (Sck_Handle*)handle;

memcpy(buf, tmpHandle->p, tmpHandle->len);
*buflen = tmpHandle->len;

return ret;
}

//客户端资源释放
__declspec(dllexport)
int socketclient_destory(void* handle) {
int ret = 0;
Sck_Handle* tmpHandle = NULL;

if (handle == NULL) {
return -1;
}

tmpHandle = (Sck_Handle*)handle;
if (tmpHandle->p != NULL) {
free(tmpHandle->p); //释放结构体成员域的指针所指向的内存空间
}
free(tmpHandle); //释放结构体内存

handle = NULL;
return 0;
}

//设置加密回调函数
__declspec(dllexport)
int socketclient_set_encode_callback(void* handle, EncodeData encodeCallback) {
int ret = 0;
Sck_Handle* tmpHandle = NULL;
if (handle == NULL || encodeCallback == NULL) {
ret = -1;
printf("function socketclient_set_encode_callback() err :%d check (handle == NULL || encodeCallback == NULL) err \n", ret);
return ret;
}
tmpHandle = (Sck_Handle*)handle;
tmpHandle->encodeCallback = encodeCallback;
return 0;
}

生成 DLL 文件

DLL 项目执行编译后,会自动在项目所在的文件夹内生成 .dll.lib 文件,例如 socket-client.dllsocket-client.lib。在 VS Studio 的 Developer Command Prompt 命令窗口中,使用 dumpbin /exports socket-client.dll 命令,查看得到 socket-client.dll 动态链接库的详细信息如下:

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
Microsoft (R) COFF/PE Dumper Version 14.29.30136.0
Copyright (C) Microsoft Corporation. All rights reserved.

Dump of file socket-client.dll

File Type: DLL

Section contains the following exports for socket-client.dll

00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
6 number of functions
6 number of names

ordinal hint RVA name

1 0 00011005 socketclient_destory = @ILT+0(socketclient_destory)
2 1 0001135C socketclient_init = @ILT+855(socketclient_init)
3 2 00011055 socketclient_recv = @ILT+80(socketclient_recv)
4 3 00011195 socketclient_send = @ILT+400(socketclient_send)
5 4 000112E4 socketclient_send_encode = @ILT+735(socketclient_send_encode)
6 5 00011244 socketclient_set_encode_callback = @ILT+575(socketclient_set_encode_callback)

Summary

1000 .00cfg
1000 .data
1000 .idata
1000 .msvcjmc
3000 .pdata
4000 .rdata
1000 .reloc
1000 .rsrc
9000 .text
10000 .textbss

案例代码下载

  • 点击下载完整的案例代码

DLL 的两种调用方式

DLL 动态调用

在本案例中,实现了通过函数指针类型,动态调用 DLL 里的函数,点击下载 使用到的 socket-client.dll 。值得一提的是,日后如果 DLL 里面的函数体代码作了修改,也不必改动函数调用方的代码(如下代码),就可以正常实现函数的调用,这样非常便于程序的维护和升级。特别注意,动态调用 DLL 里的函数时(动态加载 DLL),不需要 .h.lib 文件,只需要 .dll 文件,同时要知道所要调用的函数的参数类型以及返回值类型(用于定义函数指针类型)。

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
#include <stdio.h>
#include <windows.h>

// 定义函数指针类型
typedef int (*SocketInit)(void** handle);
typedef int (*SocketSend)(void* handle, unsigned char* buf, int buflen);
typedef int (*SocketRev)(void* handle, unsigned char* buf, int* buflen);
typedef int (*SocketDestory)(void* handle);

int main() {

HINSTANCE hInstance;

// 加载DLL动态链接库
hInstance = LoadLibrary("./socket-client.dll");
if (hInstance == NULL)
{
printf("LoadLibrary() 调用失败, ErrorCode: %d", GetLastError());
return -1;
}

// 调用DLL动态链接库
SocketInit socketInit = (SocketInit)GetProcAddress(hInstance, "socketclient_init");
SocketSend socketSend = (SocketSend)GetProcAddress(hInstance, "socketclient_send");
SocketRev socketRev = (SocketRev)GetProcAddress(hInstance, "socketclient_recv");
SocketDestory socketDestory = (SocketDestory)GetProcAddress(hInstance, "socketclient_destory");

if (socketInit == NULL)
{
return -1;
}

unsigned char inbuf[128];
int inbuflen = 128;
unsigned char outbuf[4096];
int outbuflen = 4096;

void* handle = NULL;
int initResult = socketInit(&handle);
int sendResult = socketSend(handle, inbuf, inbuflen);
int revResult = socketRev(handle, outbuf, &outbuflen);
int destoryResult = socketDestory(handle);

printf("initResult = %d\n", initResult);
printf("sendResult = %d\n", sendResult);
printf("revResult = %d\n", revResult);
printf("destoryResult = %d\n", destoryResult);

// 释放DLL动态链接库
if (hInstance != NULL) {
FreeLibrary(hInstance);
}

return 0;
}

程序运行的输出结果如下:

1
2
3
4
initResult = 0
sendResult = 0
revResult = 0
destoryResult = 0

DLL 静态调用

静态调用 DLL 里的函数(静态加载 DLL),需要同时使用 .h.lib 以及 .dll 文件,具体的操作步骤如下:

  • a) 将 .h.lib 以及 .dll 文件分别拷贝到项目所在的文件夹内,必须与 .c 源文件处于同一个文件夹
  • b) 在需要调用 DLL 的 .c 源文件中,通过 #pragma comment(lib "xxx.lib") 指令引入 .lib 文件
  • c) 在需要调用 DLL 的 .c 源文件中,通过 #include "xxx.h,引入 .h 头文件
  • d) 正常编写代码,并调用 DLL 里的函数

cplusplus-load-dll-2

若使用的开发工具是 Visual Studio,则可以不通过 #pragma comment(lib "xxx.lib") 指令引入 .lib 文件。右键项目,选择 属性,导航到 配置属性 -> 链接器 -> 输入 -> 附加依赖项,添加对应的 .lib 文件名即可,如下图所示:

cplusplus-load-dll-1


.h 头文件里一般定义了 DLL 动态链接库里的函数原型,例如 socket-client.h 头文件的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef _INC_MYSOCKETCLIENT_H__
#define _INC_MYSOCKETCLIENT_H__

#ifdef __cplusplus
extern "C" {
#endif

typedef int (*EncodeData)(unsigned char* in, int inlen, unsigned char* out, int* outlen);

int socketclient_init(void** handle);
int socketclient_send(void* handle, unsigned char* buf, int buflen);
int socketclient_recv(void* handle, unsigned char* buf, int* buflen);
int socketclient_destory(void* handle);
int socketclient_set_encode_callback(void* handle, EncodeData encodeCallback);
int socketclient_send_encode(void* handle, unsigned char* buf, int buflen, EncodeData encodeCallback);

#ifdef __cplusplus
}
#endif

#endif /* _INC_MYSOCKETCLIENT_H__ */

静态调用 DLL 里的函数(main.c)的代码如下:

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "socket-client.h"

int Hw_Encode(unsigned char* in, int inlen, unsigned char* out, int* outlen) {
printf("function Hw_Encode() begin ....\n");
strcpy((char*)out, "123456789");
*outlen = 9;
printf("function Hw_Encode() end ....\n");
return 0;
}

int Cisco_Encode(unsigned char* in, int inlen, unsigned char* out, int* outlen) {
printf("function Cisco_Encode() begin ....\n");
strcpy((char*)out, "123456789");
*outlen = 9;
printf("function Cisco_Encode() end ....\n");
return 0;
}

int main() {
unsigned char in[1024];
int inlen;

unsigned char out[1024];
int outlen;

void* handle = NULL;
int ret = 0;

strcpy((char*)in, "aaaaaaaa");
inlen = 9;

//客户端初始化
ret = socketclient_init(&handle);
if (ret != 0)
{
printf("function socketclient_init() err:%d \n", ret);
goto End;
}
printf("the result of socketclient_init() is %d \n", ret);

//设置加密回调函数
ret = socketclient_set_encode_callback(handle, Cisco_Encode);
if (ret != 0) {
printf("function socketclient_set_encode_callback() err:%d \n", ret);
}
printf("the result of socketclient_set_encode_callback() is %d \n", ret);

//客户端发送报文
ret = socketclient_send(handle, in, inlen);
if (ret != 0)
{
printf("function socketclient_send() err:%d \n", ret);
goto End;
}
printf("the result of socketclient_send() is %d \n", ret);

//客户端报文加密发送
ret = socketclient_send_encode(handle, in, inlen, Hw_Encode);
if (ret != 0)
{
printf("function socketclient_send_encode() err:%d \n", ret);
goto End;
}
printf("the result of socketclient_send_encode() is %d \n", ret);

//客户端接收报文
ret = socketclient_recv(handle, out, &outlen);
if (ret != 0)
{
printf("function socketclient_recv() err:%d \n", ret);
goto End;
}
printf("the result of socketclient_recv() is %d \n", ret);

End:
//客户端释放资源
ret = socketclient_destory(handle);
if (ret != 0)
{
printf("function socketclient_destory() err:%d \n", ret);
}
printf("the result of socketclient_destory() is %d \n", ret);

return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
6
7
8
9
10
the result of socketclient_init() is 0
the result of socketclient_set_encode_callback() is 0
function Cisco_Encode() begin ....
function Cisco_Encode() end ....
the result of socketclient_send() is 0
function Hw_Encode() begin ....
function Hw_Encode() end ....
the result of socketclient_send_encode() is 0
the result of socketclient_recv() is 0
the result of socketclient_destory() is 0

案例代码下载

  • 点击下载完整的案例代码(DLL 静态调用)

参考博客