【问题标题】:Issue with the strtok function in C: it only returns one tokenC 中 strtok 函数的问题:它只返回一个标记
【发布时间】:2021-06-30 20:43:30
【问题描述】:

谁能帮我弄清楚为什么我的代码没有将strtok 函数返回的下一个元素分配给我的索引?它附加了strtok 的第一个返回,但不包含以下任何一个。所以我的while循环只运行一次。我不确定发生了什么,非常感谢您的帮助。谢谢!

  #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(int ac, char **av, char **env)
    {
        char **token;
        const char *deli = ":";
        char *path = "PATH=";
        char *hold;
        int i, j, k = 0, inputSize = 100;
    
        int count;
    
        token = malloc(inputSize * sizeof(char));
            if(token == NULL)
            {
                exit;
            }
    
            for (count = 0; count < inputSize; count++)
            {
                token[count] = malloc(sizeof(char) * (inputSize));
    
                if (token[count] == NULL)
                {
                    for (count -= 1; count >= 0; count--)
                    {
                        free(token[count]);
                    }
                    free(token);
    
                    return (0);
                }
        }
    //this loop gets PATH
        for (i = 0; env[i] != NULL; i++)
        {
            for (j = 0; j < 5; j++)
            {
            if (path[j] != env[i][j])
                    break;
            }
            if (j == 5)
                break;
        }
    
        strtok(env[i], deli);
    
        hold = strtok(env[i], deli);
    
        while (hold != NULL)
        {
            token[k] = (char*)hold;
            printf("%s\n", token[k]);
            k++;
            hold = strtok(NULL, deli);
        }
        return (0);
    }

【问题讨论】:

  • 请注意,它是exit(); 而不是exit;。后者什么都不做。
  • 对于strtok,对于给定的缓冲区,您在第一次调用时传递缓冲区地址并为剩余的调用/令牌传递NULL
  • 你应该删除strtok()的第一条语句,这个函数会修改env_i的内容,下一次调用这个函数将无法正常工作。
  • 谢谢大家!删除第一个电话有帮助

标签: c shell loops environment-variables strtok


【解决方案1】:

您有几个导致问题的问题。当使用strtok() 时,第一次调用使用指针本身,例如strtok (p, delims); 而所有后续调用都使用 NULL 代替指针,例如strtok (NULL, delim);.

strtok() 修改它所操作的字符串。您可能希望在更改原始环境字符串之前复制包含 PATH 的环境字符串。

您还使定位 PATH 环境字符串变得复杂,然后删除 "PATH=" 部分,以便可以将其余部分标记为单独的路径组件。一个简单的方法是使用strncmp() 搜索前缀"PATH=",然后前进超过字符串中的前五个字符。

您可以简单地将PREFIX 定义为"PATH="prefixlen = strlen(PREFIX); 为:

    char path[PATH_MAX] = "",               /* storage for copy of environment string */
        *p = path,                          /* pointer - general use */
        **tokens = NULL;                    /* pointer to pointer for path components */
    ...
    for (int i = 0; env[i]; i++)            /* find PREFIX in environment */
        if (strncmp (env[i], PREFIX, prefixlen) == 0) {
            strcpy (p, env[i] + prefixlen); /* copy path from env to path */
            break;
        }

如果您将path 的存储初始化为空字符串或全为零,您可以通过测试您的副本是否包含字符串来确认找到PATH 环境变量,例如

    if (!*p) {  /* if PREFIX not found */
        fprintf (stderr, "error: '%s' not found in environment.\n", PREFIX);
        return 1;
    }

当您使用strtok() 分隔path 中的路径组件以通过pointer-to-pointer-to char 访问时,您必须为每个组件分配一个指针(并且如果您想提供一个标记 NULL 作为标记结束的最终指针,则需要一个附加指针)。虽然您通常希望分配一个指针块,但路径组件相对较少(少于 100 个),但您可以简单地 realloc 并为找到的每个标记添加一个指针。当您realloc 时,您始终使用临时指针来防止在realloc 失败时用NULL 覆盖您的原始指针。

您还需要为每个路径组件分配存储空间,以确保存储 nul 终止字符的字符串 (+1)。您可以使用简单的for() 循环和strtok() 来标记您的环境字符串副本,类似于:

    /* loop moving ep (end-pointer) to next DELIM in p or last token */
    for (p = strtok (p, DELIM); p; p = strtok (NULL, DELIM)) {
        size_t  len = strlen(p);            /* length of token */
        /* realloc pointers using temporary pointer
         * adding two more than index to set sentinel NULL as last pointer
         * (sentinel NULL is optional, just add 1 if not wanted)
         */
        void *tmp = realloc (tokens, (ndx + 2) * sizeof *tokens);
        if (!tmp) {                                 /* validate realloc */
            perror ("realloc-tokens");
            break;
        }
        tokens = tmp;                               /* assign new block to tokens */
        tokens[ndx + 1] = NULL;                     /* set sentinel NULL */
        if (!(tokens[ndx] = malloc (len + 1))) {    /* allocate/validate string storage */
            perror ("malloc-tokens[ndx]");
            tokens[ndx] = NULL;
            break;
        }
        memcpy (tokens[ndx++], p, len + 1);         /* copy path component */
    }

把它放在一个简短的例子中,你可以这样做:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>

#ifndef PATH_MAX
#define PATH_MAX 4096
#endif

#define PREFIX "PATH="  /* constants for PREFIX and DELIM */
#define DELIM ":\n"

int main (int argc, char **argv, char **env)
{
    char path[PATH_MAX] = "",               /* storage for copy of environment string */
        *p = path,                          /* pointer - general use */
        **tokens = NULL;                    /* pointer to pointer for path components */
        
    size_t  prefixlen = strlen (PREFIX),    /* prefix length */
            ndx = 0;                        /* path tokens index */
    
    for (int i = 0; env[i]; i++)            /* find PREFIX in environment */
        if (strncmp (env[i], PREFIX, prefixlen) == 0) {
            strcpy (p, env[i] + prefixlen); /* copy path from env to path */
            break;
        }
    
    if (!*p) {  /* if PREFIX not found */
        fprintf (stderr, "error: '%s' not found in environment.\n", PREFIX);
        return 1;
    }
    
    puts ("\nfull path:\n");    /* show full path to create tokens from */
    puts (p);
    
    /* loop moving ep (end-pointer) to next DELIM in p or last token */
    for (p = strtok (p, DELIM); p; p = strtok (NULL, DELIM)) {
        size_t  len = strlen(p);            /* length of token */
        /* realloc pointers using temporary pointer
         * adding two more than index to set sentinel NULL as last pointer
         * (sentinel NULL is optional, just add 1 if not wanted)
         */
        void *tmp = realloc (tokens, (ndx + 2) * sizeof *tokens);
        if (!tmp) {                                 /* validate realloc */
            perror ("realloc-tokens");
            break;
        }
        tokens = tmp;                               /* assign new block to tokens */
        tokens[ndx + 1] = NULL;                     /* set sentinel NULL */
        if (!(tokens[ndx] = malloc (len + 1))) {    /* allocate/validate string storage */
            perror ("malloc-tokens[ndx]");
            tokens[ndx] = NULL;
            break;
        }
        memcpy (tokens[ndx++], p, len + 1);         /* copy path component */
    }
    
    puts ("\npath components:\n");                  /* output separated tokens */
    for (size_t i = 0; tokens && tokens[i]; i++) {  /* loop until sentinel NULL */
        puts (tokens[i]);
        free (tokens[i]);                           /* free string storage */
    }
    free (tokens);                                  /* free pointers */
    
    (void)argc;     /* cast to prevent -Wunused warnings */
    (void)argv;
}

(注意:for() 循环条件 tokens &amp;&amp; tokens[i] 将处理带有 realloc() 的初始分配返回 NULL 避免取消引用 tokens[0] 的情况。您可以提供如果您愿意,可以在输出循环上方单独 if (!tokens) { perror ("initial allocation failed"); return 1; }

使用/输出示例

编译并运行程序会产生类似如下的输出:

$ ./bin/path_tokens

full path:

/opt/kde3/bin:/home/david/bin:/usr/local/bin:/usr/bin:/bin:/usr/lib/qt3/bin:/sbin:/usr/sbin:/usr/local/sbin:/opt/gcc-arm-none-eabi/bin

path components:

/opt/kde3/bin
/home/david/bin
/usr/local/bin
/usr/bin
/bin
/usr/lib/qt3/bin
/sbin
/usr/sbin
/usr/local/sbin
/opt/gcc-arm-none-eabi/bin

内存使用/错误检查

在您编写的任何动态分配内存的代码中,对于分配的任何内存块,您都有 2 个职责:(1)始终保留指向起始地址的指针内存块,因此,(2) 当不再需要它时可以释放

您必须使用内存错误检查程序来确保您不会尝试访问内存或写入超出/超出分配块的边界,尝试读取或基于未初始化的值进行条件跳转,最后, 以确认您已释放所有已分配的内存。

对于 Linux,valgrind 是正常的选择。每个平台都有类似的内存检查器。它们都易于使用,只需通过它运行您的程序即可。

$ valgrind ./bin/path_tokens
==22643== Memcheck, a memory error detector
==22643== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==22643== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==22643== Command: ./bin/path_tokens
==22643==

full path:

/opt/kde3/bin:/home/david/bin:/usr/local/bin:/usr/bin:/bin:/usr/lib/qt3/bin:/sbin:/usr/sbin:/usr/local/sbin:/opt/gcc-arm-none-eabi/bin

path components:

/opt/kde3/bin
/home/david/bin
/usr/local/bin
/usr/bin
/bin
/usr/lib/qt3/bin
/sbin
/usr/sbin
/usr/local/sbin
/opt/gcc-arm-none-eabi/bin
==22643==
==22643== HEAP SUMMARY:
==22643==     in use at exit: 0 bytes in 0 blocks
==22643==   total heap usage: 21 allocs, 21 frees, 1,679 bytes allocated
==22643==
==22643== All heap blocks were freed -- no leaks are possible
==22643==
==22643== For counts of detected and suppressed errors, rerun with: -v
==22643== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

始终确认您已释放已分配的所有内存并且没有内存错误。

适用于 Windows 和 Linux 的更改

由于windows使用';'(分号)作为路径分隔符,并以"Path="作为环境字符串的前缀,而Linux使用':'(冒号)作为分隔符,"PATH="作为前缀,一个简单的预处理器#if .. #else .. #endif,您可以修改程序以在 Widows 和 Linux 上运行。只需将 PREFIXDELIM 的定义替换为:

#if defined (_WIN64) || defined (_WIN32)
#define PREFIX "Path="  /* constants for PREFIX and DELIM on windows */
#define DELIM ";\r\n"
#else
#define PREFIX "PATH="  /* constants for PREFIX and DELIM on Linux */
#define DELIM ":\n"
#endif

Windows 上的使用/输出示例

完整路径已被省略,因为它会滚动一千多个字符:

>bin\path_tokens_win.exe

full path:

<snipped -- too long>

path components:

C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\bin\HostX86\x86
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\VC\VCPackages
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\bin\Roslyn
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Team Tools\Performance Tools
C:\Program Files (x86)\Microsoft Visual Studio\Shared\Common\VSPerfCollectionTools\
C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\
C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x86
C:\Program Files (x86)\Windows Kits\10\bin\x86
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\\MSBuild\15.0\bin
C:\Windows\Microsoft.NET\Framework\v4.0.30319
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\Tools\
C:\Program Files\ImageMagick-7.0.10-Q16
C:\Program Files (x86)\Common Files\Oracle\Java\javapath
C:\ProgramData\Oracle\Java\javapath
C:\Program Files\ImageMagick-7.0.3-Q16
C:\Windows\System32
C:\Windows
C:\Windows\System32\wbem
C:\Windows\System32\WindowsPowerShell\v1.0\
C:\Program Files (x86)\PDFtk\bin\
C:\Program Files (x86)\PDFtk Server\bin\
C:\Program Files (x86)\GNU\GnuPG\pub
C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\
C:\Windows\System32\OpenSSH\
C:\Program Files\Git\cmd
C:\Program Files\PuTTY\
C:\Program Files (x86)\Gpg4win\..\GnuPG\bin
C:\WINDOWS\system32
C:\WINDOWS
C:\WINDOWS\System32\Wbem
C:\WINDOWS\System32\WindowsPowerShell\v1.0\
C:\WINDOWS\System32\OpenSSH\
C:\Users\david\AppData\Local\Microsoft\WindowsApps
c:\MinGW\bin
c:\MinGW\msys\1.0\bin
c:\gtk2\bin
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja

(微软当然不会对路径大小感到害羞......)

除了strtok(),还有其他几个选项不会改变原始字符串。您可以使用strpbrk(),类似于使用strtok() 手动将开始和结束指针推进到括号并复制每个组件。您可以使用strchr() 以基本相同的方式定位每个分隔符。如果您喜欢使用索引而不是指针,您可以使用strcspn() 来获取每个分隔符之间的字符数。当然,您也可以只使用循环向下工作,然后手动定位每个分隔符。怎么做完全取决于你。

检查一下,如果您还有其他问题,请告诉我。

【讨论】:

  • 哇!!太棒了,非常感谢您的详细回复!!!我一定会再读几遍。非常详细的回复。谢谢。
  • 不客气。祝你的编码好运。如果您需要其他帮助,请告诉我。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-09
  • 1970-01-01
  • 1970-01-01
  • 2014-01-03
  • 1970-01-01
  • 2011-06-09
相关资源
最近更新 更多