C语言循环的实现

Masimaro
2016-04-11 / 0 评论 / 16 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2024年12月20日,已超过146天没有更新,若内容或图片失效,请留言反馈。

在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

评论 (0)

取消