在C语言中采用3中语法来实现循环,它们分别是while、for、do while,本文将分别说明这三种循环的实现,并对它们的运行效率进行比较。
do while
首先来看do while的实现:下面是简单的代码:
int nCount = 0;
int nMax = 10;
do
{
nCount++;
} while (nCount < nMax);
return 0;
下面对应的是它的汇编代码:
9: int nCount = 0;
00401268 mov dword ptr [ebp-4],0
10: int nMax = 10;
0040126F mov dword ptr [ebp-8],0Ah
11: do
12: {
13: nCount++;
00401276 mov eax,dword ptr [ebp-4]
00401279 add eax,1
0040127C mov dword ptr [ebp-4],eax
14: } while (nCount < nMax);
0040127F mov ecx,dword ptr [ebp-4];exc = nCount
00401282 cmp ecx,dword ptr [ebp-8];比较nCount 和 nMax的值
00401285 jl main+26h (00401276);跳转到循环体中
15: return 0;
00401287 xor eax,eax
在汇编代码中首先执行了一次循环体中的操作,然后判断,当条件满足时会跳转回循环体,然后再次执行,当条件不满足时会接着执行后面的语句。
这个过程可以用goto来模拟:
int nCount = 0;
int nMax = 10;
__WHILE:
nCount++;
if(nCount < nMax)
goto __WHILE;
while循环
不同于do while的先执行再比较,while采取的是先比较再循环的方式,下面是一个while的例子:
int nCount = 0;
int nMax = 10;
while (nCount < nMax)
{
nCount++;
}
00401268 mov dword ptr [ebp-4],0
10: int nMax = 10;
0040126F mov dword ptr [ebp-8],0Ah
11: while (nCount < nMax)
00401276 mov eax,dword ptr [ebp-4]
00401279 cmp eax,dword ptr [ebp-8]
0040127C jge main+39h (00401289)
12: {
13: nCount++;
0040127E mov ecx,dword ptr [ebp-4]
00401281 add ecx,1
00401284 mov dword ptr [ebp-4],ecx
14: }
00401287 jmp main+26h (00401276)
15: return 0;
00401289 xor eax,eax
从汇编代码上可以看出,执行while循环时会有两次跳转,当条件不满足时会执行一次跳转,跳转到循环体外,而条件满足,执行完一次循环后,会再次跳转到循环体中,再次进行比较。相比于do while来说,while执行了两次跳转,效率相对较低。
for 循环
for循环是首先进行初始化操作然后进行比较,条件满足时执行循环,再将循环变量递增,最后再次比较,执行循环或者跳出。下面是for的简单例子:
int nMax = 10;
for (int i = 0; i < nMax; i++)
{
printf("%d\n", i);
}
下面是它对应的汇编代码:
9: int nMax = 10;
00401268 mov dword ptr [ebp-4],0Ah
10: for (int i = 0; i < nMax; i++)
0040126F mov dword ptr [ebp-8],0 ;初始化循环变量
00401276 jmp main+31h (00401281);跳转到比较操作处
00401278 mov eax,dword ptr [ebp-8]
0040127B add eax,1
0040127E mov dword ptr [ebp-8],eax;这三句话实现的是循环变量自增操作
00401281 mov ecx,dword ptr [ebp-8];ecx = i
00401284 cmp ecx,dword ptr [ebp-4];比较ecx与i
00401287 jge main+4Ch (0040129c);跳转到循环体外
11: {
12: printf("%d\n", i);
00401289 mov edx,dword ptr [ebp-8]
0040128C push edx
0040128D push offset string "%d\n" (0042e01c)
00401292 call printf (00401540)
00401297 add esp,8
13: }
0040129A jmp main+28h (00401278);跳转到i++位置
14: return 0;
0040129C xor eax,eax
从上面的汇编代码可以看出for循环的效率最低,它经过了3次跳转,生成对应的汇编代码上,初始化操作后面紧接着是循环变量自增操作,所以首先在完成初始化后会进行一次跳转,跳转到判断,然后根据判断条件再次跳转或者接着执行循环体,最后当循环完成后会再次跳转到循环变量自增的位置,同样采用goto语句来模拟这个操作:
int nMax = 10;
int i = 0;
goto __CMP;
__ADD:
i++;
__CMP:
if (i >= nMax)
{
goto __RETURN;
}
__LOOP:
printf("%d\n", i);
goto __ADD;
__RETURN:
return 0;
continue语句
continue用于结束这次循环进入下一次循环,下面采用最复杂的for循环来说明continue语句:
int nMax = 10;
int i = 0;
for(;i < nMax; i++)
{
if (i == 6)
{
continue;
}
}
下面是它对应的汇编代码:
00401268 mov dword ptr [ebp-4],0Ah
10: int i = 0;
0040126F mov dword ptr [ebp-8],0
11: for(;i < nMax; i++)
00401276 jmp main+31h (00401281)
00401278 mov eax,dword ptr [ebp-8]
0040127B add eax,1
0040127E mov dword ptr [ebp-8],eax
00401281 mov ecx,dword ptr [ebp-8]
00401284 cmp ecx,dword ptr [ebp-4]
00401287 jge main+43h (00401293)
12: {
13: if (i == 6)
00401289 cmp dword ptr [ebp-8],6;
0040128D jne main+41h (00401291);条件不满足组跳转到循环结束处
14: {
15: continue;
0040128F jmp main+28h (00401278)
16: }
17: }
00401291 jmp main+28h (00401278)
18: return 0;
00401293 xor eax,eax
从上面的汇编代码可以看到,continue语句也是一个跳转语句,它会直接跳转到循环体的开始位置。对于for来说相对特殊一些(我觉得循环变量自增并不属于循环体),由于第一次进入循环时并没有执行循环变量自增,所以它会跳转到循环变量自增的位置,其他则直接到循环开始处。
慎用goto
goto 语句就像汇编中的 jmp 一样,是直接跳转到对应的标识位置,从上面我们使用goto来模拟各种循环来看,goto语句的可读性不强,而且有可能跳过变量的初始化等过程造成一些难以察觉的问题,但有些时候goto确实好用,例如在写socket或者其他需要清理资源的代码时,goto可以显著的增加程序的可读性并且也会减少相关代码的编写,例如一个典型的服务端socket例子
#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#pragma comment(lib, "ws2_32.lib") // Winsock Library
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
WSADATA wsaData;
SOCKET serverSocket, clientSocket;
struct sockaddr_in serverAddr, clientAddr;
int addrLen = sizeof(clientAddr);
char buffer[BUFFER_SIZE];
// 初始化 Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("Failed to initialize Winsock. Error Code: %d\n", WSAGetLastError());
return EXIT_FAILURE;
}
// 创建 socket
serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket == INVALID_SOCKET) {
printf("Could not create socket. Error Code: %d\n", WSAGetLastError());
WSACleanup();
return EXIT_FAILURE;
}
// 设置服务器地址结构
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的接口
serverAddr.sin_port = htons(PORT); // 转换为网络字节序
// 绑定 socket
if (bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("Bind failed. Error Code: %d\n", WSAGetLastError());
closesocket(serverSocket);
WSACleanup();
return EXIT_FAILURE;
}
// 开始监听
if (listen(serverSocket, 3) == SOCKET_ERROR) {
printf("Listen failed. Error Code: %d\n", WSAGetLastError());
closesocket(serverSocket);
WSACleanup();
return EXIT_FAILURE;
}
printf("Server is listening on port %d...\n", PORT);
// 接受客户端连接
clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &addrLen);
if (clientSocket == INVALID_SOCKET) {
printf("Accept failed. Error Code: %d\n", WSAGetLastError());
closesocket(serverSocket);
WSACleanup();
return EXIT_FAILURE;
}
printf("Client connected.\n");
// 发送消息给客户端
const char *message = "Hello from server!";
send(clientSocket, message, strlen(message), 0);
// 关闭 sockets
closesocket(clientSocket);
closesocket(serverSocket);
WSACleanup();
return EXIT_SUCCESS;
}
中间有好几次执行了closesocket、以及最后的WSACleanup操作、前面每一步出错都要写一次这些清理资源的操作。如果使用goto将会简单的多
#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#pragma comment(lib, "ws2_32.lib") // Winsock Library
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
WSADATA wsaData;
SOCKET serverSocket, clientSocket;
struct sockaddr_in serverAddr, clientAddr;
int addrLen = sizeof(clientAddr);
char buffer[BUFFER_SIZE];
int err = EXIT_SUCCESS;
// 初始化 Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("Failed to initialize Winsock. Error Code: %d\n", WSAGetLastError());
err = EXIT_FAILURE;
goto __CLEANUP;
}
// 创建 socket
serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket == INVALID_SOCKET) {
printf("Could not create socket. Error Code: %d\n", WSAGetLastError());
err = EXIT_FAILURE;
goto __CLEANUP;
}
// 设置服务器地址结构
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的接口
serverAddr.sin_port = htons(PORT); // 转换为网络字节序
// 绑定 socket
if (bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("Bind failed. Error Code: %d\n", WSAGetLastError());
err = EXIT_FAILURE;
goto __CLEANUP;
}
// 开始监听
if (listen(serverSocket, 3) == SOCKET_ERROR) {
printf("Listen failed. Error Code: %d\n", WSAGetLastError());
err = EXIT_FAILURE;
goto __CLEANUP;
}
printf("Server is listening on port %d...\n", PORT);
// 接受客户端连接
clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &addrLen);
if (clientSocket == INVALID_SOCKET) {
printf("Accept failed. Error Code: %d\n", WSAGetLastError());
err = EXIT_FAILURE;
goto __CLEANUP;
}
printf("Client connected.\n");
// 发送消息给客户端
const char *message = "Hello from server!";
send(clientSocket, message, strlen(message), 0);
// 关闭 sockets
__CLEANUP:
if(clientSocket != INVALID_SOCKET)
{
closesocket(clientSocket)
}
if(serverSocket != INVALID_SOCKET)
{
closesocket(serverSocket);
}
WSACleanup();
return err;
}
如果在不允许使用goto的情况下,可以考虑使用 do while 来模拟这种情况,上面的代码可以修改为
#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#pragma comment(lib, "ws2_32.lib") // Winsock Library
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
WSADATA wsaData;
SOCKET serverSocket, clientSocket;
struct sockaddr_in serverAddr, clientAddr;
int addrLen = sizeof(clientAddr);
char buffer[BUFFER_SIZE];
int err = EXIT_SUCCESS;
do{
// 初始化 Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("Failed to initialize Winsock. Error Code: %d\n", WSAGetLastError());
err = EXIT_FAILURE;
break;
}
// 创建 socket
serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket == INVALID_SOCKET) {
printf("Could not create socket. Error Code: %d\n", WSAGetLastError());
err = EXIT_FAILURE;
break;
}
// 设置服务器地址结构
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的接口
serverAddr.sin_port = htons(PORT); // 转换为网络字节序
// 绑定 socket
if (bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("Bind failed. Error Code: %d\n", WSAGetLastError());
err = EXIT_FAILURE;
break;
}
// 开始监听
if (listen(serverSocket, 3) == SOCKET_ERROR) {
printf("Listen failed. Error Code: %d\n", WSAGetLastError());
err = EXIT_FAILURE;
break;
}
printf("Server is listening on port %d...\n", PORT);
// 接受客户端连接
clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &addrLen);
if (clientSocket == INVALID_SOCKET) {
printf("Accept failed. Error Code: %d\n", WSAGetLastError());
err = EXIT_FAILURE;
break;
}
printf("Client connected.\n");
// 发送消息给客户端
const char *message = "Hello from server!";
send(clientSocket, message, strlen(message), 0);
}while (FALSE);
// 关闭 sockets
if(clientSocket != INVALID_SOCKET)
{
closesocket(clientSocket)
}
if(serverSocket != INVALID_SOCKET)
{
closesocket(serverSocket);
}
WSACleanup();
return err;
}
这里的while不是为了循环,而是利用了do while 无论如何都会先执行循环体中代码的特性,只执行一次上述主体代码,利用break来跳转到最后的清理模块,实现与goto 类似的效果。
使用goto 的方案比do while的方案要显得简洁易懂,goto使用的好,也能使得程序简单易懂。具体使用哪种方案是个见仁见智的事情,看个人喜好。如果遇上公司要求不能使用 goto,那么就可以采用do while的实现方案
评论 (0)