strtok () แยกสตริงออกเป็นโทเค็นใน C ได้อย่างไร?


114

กรุณาอธิบายการทำงานของstrtok()ฟังก์ชัน คู่มือระบุว่าแบ่งสตริงออกเป็นโทเค็น ฉันไม่เข้าใจจากคู่มือว่ามันทำอะไรได้จริง

ฉันเพิ่มนาฬิกาstrและ*pchเพื่อตรวจสอบการทำงานเมื่อเกิดขึ้นในขณะที่ลูปครั้งแรกเนื้อหาstrมีเพียง "สิ่งนี้" ผลลัพธ์ที่แสดงด้านล่างถูกพิมพ์ออกมาบนหน้าจออย่างไร?

/* strtok example */
#include <stdio.h>
#include <string.h>

int main ()
{
  char str[] ="- This, a sample string.";
  char * pch;
  printf ("Splitting string \"%s\" into tokens:\n",str);
  pch = strtok (str," ,.-");
  while (pch != NULL)
  {
    printf ("%s\n",pch);
    pch = strtok (NULL, " ,.-");
  }
  return 0;
}

เอาท์พุท:

สตริงการแยก "- นี่คือสตริงตัวอย่าง" เป็นโทเค็น:
นี้

ตัวอย่าง
เชือก

5
strtok()แก้ไขสตริงอาร์กิวเมนต์โดยการยกเลิกโทเค็นด้วย NUL ก่อนส่งคืน ถ้าคุณพยายามที่จะตรวจสอบทั้งบัฟเฟอร์ (STR []) strtok()คุณจะเห็นว่ามันถูกแก้ไขระหว่างสายต่อเนื่องเพื่อ
Michael Foukarakis

แทนการดูstr, นาฬิกาstr[0], str[1], str[2]...
PMG

@pmg: ฉันดู str [0] และ str [1] .str [1] ควรเป็น "\ 0" แต่มันเป็นช่องว่างตรงนั้น
fuddin

3
ตามจริงแล้วฉันไม่เคยใส่ใจที่จะตรวจสอบ แต่ฉันคิดว่ามันเก็บตัวชี้สุดท้ายที่ส่งผ่านพร้อมกับตำแหน่งที่ค้างไว้ที่ จากนั้นก็สามารถดำเนินการต่อได้หากตัวชี้เป็นโมฆะหรือล้างตำแหน่งและเริ่มต้นใหม่ถ้าไม่
chris

7
@Firegun: ตัวแปรคงที่
DCoder

คำตอบ:


38

strtok()แบ่งสตริงออกเป็นโทเค็น กล่าวคือเริ่มต้นจากตัวคั่นอันใดอันหนึ่งไปยังอันถัดไปจะเป็นโทเค็นของคุณ ในกรณีของคุณโทเค็นเริ่มต้นจะมาจาก "-" และลงท้ายด้วยช่องว่างถัดไป "" จากนั้นโทเค็นถัดไปจะเริ่มจาก "" และลงท้ายด้วย "," ที่นี่คุณจะได้รับ "นี่" เป็นผลลัพธ์ ในทำนองเดียวกันสตริงที่เหลือจะถูกแยกออกเป็นโทเค็นจากช่องว่างไปยังช่องว่างและสิ้นสุดโทเค็นสุดท้ายใน "."


เงื่อนไขสิ้นสุดสำหรับโทเค็นหนึ่งจะกลายเป็นโทเค็นเริ่มต้นของโทเค็นถัดไปหรือไม่นอกจากนี้ยังมีอักขระ nul ที่วางไว้ในตำแหน่งของเงื่อนไขสิ้นสุดหรือไม่
fuddin

1
@ fahad- ใช่เส้นคั่นทั้งหมดที่คุณมีจะถูกแทนที่ด้วยอักขระ NUL ตามที่คนอื่นแนะนำ
Sachin Shanbhag

ถ้าตัวคั่นทั้งหมดถูกแทนที่ด้วย Nul ทำไมสตริงจึงมี "-this" ควรมี "\ 0"
fuddin

2
@fahad - แทนที่อักขระตัวคั่นด้วย NUL เท่านั้นไม่ใช่อักขระทั้งหมดที่อยู่ระหว่างตัวคั่น ประเภทของการแยกสตริงออกเป็นหลายโทเค็น คุณจะได้รับ "This" เนื่องจากอยู่ระหว่างตัวคั่นที่ระบุสองตัวไม่ใช่ "-this"
Sachin Shanbhag

1
@ ฟาฮัด - ใช่แน่นอน ช่องว่าง "," และ "-" ทั้งหมดจะถูกแทนที่ด้วย NUL เนื่องจากคุณได้ระบุว่าสิ่งเหล่านี้เป็นตัวคั่นเท่าที่ฉันเข้าใจ
Sachin Shanbhag

214

ฟังก์ชันรันไทม์ strtok ทำงานเช่นนี้

ครั้งแรกที่คุณเรียก strtok คุณระบุสตริงที่คุณต้องการโทเค็น

char s[] = "this is a string";

ในช่องว่างสตริงด้านบนดูเหมือนจะเป็นตัวคั่นที่ดีระหว่างคำดังนั้นให้ใช้:

char* p = strtok(s, " ");

สิ่งที่เกิดขึ้นตอนนี้คือ 's' ถูกค้นหาจนกระทั่งพบอักขระช่องว่างโทเค็นแรกจะถูกส่งคืน ('this') และ p ชี้ไปที่โทเค็นนั้น (สตริง)

เพื่อรับโทเค็นถัดไปและดำเนินการต่อด้วยสตริงเดียวกัน NULL จะถูกส่งผ่านเป็นอาร์กิวเมนต์แรกเนื่องจาก strtok รักษาตัวชี้แบบคงที่ไปยังสตริงที่ส่งผ่านก่อนหน้าของคุณ:

p = strtok(NULL," ");

p ชี้ไปที่ 'is'

และต่อไปจนกว่าจะไม่พบช่องว่างอีกต่อไปสตริงสุดท้ายจะถูกส่งกลับเป็น 'สตริง' โทเค็นสุดท้าย

สะดวกยิ่งขึ้นคุณสามารถเขียนแบบนี้แทนเพื่อพิมพ์โทเค็นทั้งหมด:

for (char *p = strtok(s," "); p != NULL; p = strtok(NULL, " "))
{
  puts(p);
}

แก้ไข:

หากคุณต้องการจัดเก็บค่าที่ส่งคืนจากstrtokคุณจำเป็นต้องคัดลอกโทเค็นไปยังบัฟเฟอร์อื่นเช่นstrdup(p);เนื่องจากสตริงเดิม (ชี้ไปที่ตัวชี้แบบคงที่ภายในstrtok) ถูกแก้ไขระหว่างการทำซ้ำเพื่อที่จะส่งคืนโทเค็น


ดังนั้นจึงไม่ได้วางอักขระ nul ระหว่างสตริงจริง ๆ เหตุใดนาฬิกาของฉันจึงแสดงว่าสตริงเหลือเพียง "THIS"
fuddin

4
มันแทนที่ '' ที่พบด้วย '\ 0' และจะไม่คืนค่า '' ในภายหลังดังนั้นสตริงของคุณจึงถูกทำลายไปโดยปริยาย

33
+1 สำหรับบัฟเฟอร์แบบคงที่นี่คือสิ่งที่ฉันไม่เข้าใจ
IEatBagels

1
รายละเอียดที่สำคัญมากขาดหายไปจากบรรทัด"โทเค็นแรกจะถูกส่งกลับและpชี้ไปที่โทเค็นนั้น"คือstrtokต้องเปลี่ยนสายอักขระเดิมโดยการวางอักขระว่างแทนตัวคั่น (มิฉะนั้นฟังก์ชันสตริงอื่นจะไม่ทราบว่า โทเค็นสิ้นสุด) และยังติดตามสถานะโดยใช้ตัวแปรคงที่
Groo

@Groo ฉันคิดว่าฉันได้เพิ่มสิ่งนั้นแล้วในการแก้ไขที่ฉันทำในปี 2560 แต่คุณพูดถูก
AndersK

25

strtokรักษาการอ้างอิงภายในแบบคงที่ซึ่งชี้ไปยังโทเค็นที่มีอยู่ถัดไปในสตริง ถ้าคุณส่งตัวชี้ NULL มันจะทำงานจากการอ้างอิงภายในนั้น

นี่คือเหตุผลที่strtokไม่ได้กลับเข้ามาใหม่ ทันทีที่คุณส่งตัวชี้ใหม่การอ้างอิงภายในเก่านั้นจะถูกรวมเข้าด้วยกัน


คุณหมายถึงอะไรจากการอ้างอิงภายในแบบเก่า 'getting clobbered' คุณหมายถึง 'เขียนทับ'?
ylun.ca

1
@ ylun.ca: ใช่นั่นคือสิ่งที่ฉันหมายถึง
John Bode

10

strtokไม่เปลี่ยนพารามิเตอร์เอง ( str) มันเก็บตัวชี้นั้น (ในตัวแปรคงที่ในเครื่อง) จากนั้นสามารถเปลี่ยนสิ่งที่พารามิเตอร์ชี้ไปในการโทรครั้งต่อ ๆ ไปโดยไม่ต้องส่งผ่านพารามิเตอร์กลับ (และสามารถเลื่อนตัวชี้ที่มันเก็บไว้ได้อย่างไรก็ตามจำเป็นต้องใช้งานได้)

จากstrtokหน้าPOSIX :

ฟังก์ชันนี้ใช้การจัดเก็บแบบคงที่เพื่อติดตามตำแหน่งสตริงปัจจุบันระหว่างการโทร

มีตัวแปรที่ปลอดภัยต่อเธรด ( strtok_r) ที่ไม่ใช้เวทมนตร์ประเภทนี้


2
ฟังก์ชั่นไลบรารี C เกิดขึ้นจากทางกลับเมื่อเธรดไม่ได้อยู่ในภาพเลย (ซึ่งเริ่มมีอยู่ในปี 2554 เท่าที่มาตรฐาน C เกี่ยวข้อง) ดังนั้นการเข้าใหม่จึงไม่สำคัญจริงๆ ( ฉันคิดว่า). โลคัลแบบคงที่ทำให้ฟังก์ชัน "ใช้งานง่าย" (สำหรับคำจำกัดความของ "ง่าย") เช่นเดียวกับการctimeส่งคืนสตริงคงที่ - ใช้งานได้จริง (ไม่มีใครต้องสงสัยว่าใครควรเป็นอิสระ) แต่อย่ากลับเข้ามาใหม่และพาคุณไปหากคุณไม่รู้
จ้า

สิ่งนี้ผิด: " strtokไม่ได้เปลี่ยนพารามิเตอร์เอง ( str)" puts(str);พิมพ์ "- นี้" ตั้งแต่มีการปรับเปลี่ยนstrtok str
MarredCheese

1
@MarredCheese: อ่านอีกครั้ง มันไม่ได้ปรับเปลี่ยนตัวชี้ มันแก้ไขข้อมูลที่ตัวชี้ชี้ไป (เช่นข้อมูลสตริง)
Mat

โอเคฉันไม่รู้ว่านั่นคือสิ่งที่คุณได้รับ ตกลง
MarredCheese

8

ในครั้งแรกที่คุณเรียกมันคุณต้องระบุสตริงที่จะโทเค็strtokน จากนั้นในการรับโทเค็นต่อไปนี้คุณเพียงแค่ให้NULLกับฟังก์ชันนั้นตราบเท่าที่ส่งกลับค่าไม่ใช่NULLตัวชี้

strtokฟังก์ชั่นบันทึกสตริงที่คุณให้ไว้เมื่อครั้งแรกที่คุณเรียกว่า (ซึ่งอันตรายจริงๆสำหรับการใช้งานแบบมัลติเธรด)


8

strtok จะโทเค็นสตริงเช่นแปลงเป็นสตริงย่อย

ทำได้โดยการค้นหาตัวคั่นที่แยกโทเค็นเหล่านี้ (หรือสตริงย่อย) และคุณระบุตัวคั่น ในกรณีของคุณคุณต้องการ '' หรือ ',' หรือ ' หรือ '-' เพื่อเป็นตัวคั่น

รูปแบบการเขียนโปรแกรมเพื่อแยกโทเค็นเหล่านี้คือคุณส่งสตริงหลักและชุดตัวคั่น จากนั้นคุณเรียกมันซ้ำ ๆ และทุกครั้งที่ strtok จะส่งคืนโทเค็นถัดไปที่พบ จนกระทั่งถึงจุดสิ้นสุดของสตริงหลักเมื่อส่งคืนค่าว่าง กฎอีกข้อหนึ่งคือคุณส่งสตริงในครั้งแรกเท่านั้นและเป็นค่า NULL สำหรับครั้งต่อ ๆ ไป นี่เป็นวิธีที่จะบอก strtok ว่าคุณกำลังเริ่มเซสชันใหม่ของโทเค็นด้วยสตริงใหม่หรือคุณกำลังดึงโทเค็นจากเซสชันโทเค็นก่อนหน้านี้ โปรดทราบว่า strtok จะจำสถานะสำหรับเซสชันโทเค็น และด้วยเหตุนี้จึงไม่ reentrant หรือเธรดปลอดภัย (คุณควรใช้ strtok_r แทน) สิ่งที่ต้องรู้อีกอย่างก็คือมันปรับเปลี่ยนสตริงเดิม มันเขียน '\ 0' สำหรับตัวคั่นที่พบ

วิธีหนึ่งในการเรียกใช้ strtok แบบรวบรัดมีดังนี้:

char str[] = "this, is the string - I want to parse";
char delim[] = " ,-";
char* token;

for (token = strtok(str, delim); token; token = strtok(NULL, delim))
{
    printf("token=%s\n", token);
}

ผลลัพธ์:

this
is
the
string
I
want
to
parse

5

strtok แก้ไขสตริงอินพุต มันวางอักขระ null ('\ 0') ไว้เพื่อที่จะส่งคืนบิตของสตริงเดิมเป็นโทเค็น ในความเป็นจริง strtok ไม่ได้จัดสรรหน่วยความจำ คุณอาจเข้าใจได้ดีขึ้นถ้าคุณวาดสตริงเป็นลำดับของกล่อง


3

เพื่อให้เข้าใจถึงวิธีการstrtok()ทำงานอันดับแรกต้องรู้ว่าตัวแปรคงคืออะไร ลิงค์นี้อธิบายได้ดีทีเดียว ....

กุญแจสำคัญในการดำเนินการstrtok()คือการรักษาตำแหน่งของตัวคั่นสุดท้ายระหว่างการโทรแบบ seccessive (นั่นคือเหตุผลที่strtok()ยังคงแยกวิเคราะห์สตริงดั้งเดิมที่ส่งผ่านไปยังเมื่อมีการเรียกใช้ด้วยการnull pointerโทรติดต่อกัน) ..

ลองดูstrtok()การใช้งานของฉันเองที่เรียกว่าzStrtok()ซึ่งมีฟังก์ชันการทำงานที่แตกต่างจากที่มีให้strtok()

char *zStrtok(char *str, const char *delim) {
    static char *static_str=0;      /* var to store last address */
    int index=0, strlength=0;           /* integers for indexes */
    int found = 0;                  /* check if delim is found */

    /* delimiter cannot be NULL
    * if no more char left, return NULL as well
    */
    if (delim==0 || (str == 0 && static_str == 0))
        return 0;

    if (str == 0)
        str = static_str;

    /* get length of string */
    while(str[strlength])
        strlength++;

    /* find the first occurance of delim */
    for (index=0;index<strlength;index++)
        if (str[index]==delim[0]) {
            found=1;
            break;
        }

    /* if delim is not contained in str, return str */
    if (!found) {
        static_str = 0;
        return str;
    }

    /* check for consecutive delimiters
    *if first char is delim, return delim
    */
    if (str[0]==delim[0]) {
        static_str = (str + 1);
        return (char *)delim;
    }

    /* terminate the string
    * this assignmetn requires char[], so str has to
    * be char[] rather than *char
    */
    str[index] = '\0';

    /* save the rest of the string */
    if ((str + index + 1)!=0)
        static_str = (str + index + 1);
    else
        static_str = 0;

        return str;
}

และนี่คือตัวอย่างการใช้งาน

  Example Usage
      char str[] = "A,B,,,C";
      printf("1 %s\n",zStrtok(s,","));
      printf("2 %s\n",zStrtok(NULL,","));
      printf("3 %s\n",zStrtok(NULL,","));
      printf("4 %s\n",zStrtok(NULL,","));
      printf("5 %s\n",zStrtok(NULL,","));
      printf("6 %s\n",zStrtok(NULL,","));

  Example Output
      1 A
      2 B
      3 ,
      4 ,
      5 C
      6 (null)

รหัสนี้มาจากไลบรารีการประมวลผลสตริงที่ฉันดูแลบน Githubเรียกว่า zString ดูรหัสหรือแม้แต่มีส่วนร่วม :) https://github.com/fnoyanisi/zString


3

นี่คือวิธีที่ฉันใช้งาน strtok ไม่ดีเท่าไหร่ แต่หลังจากทำงาน 2 ชั่วโมงในที่สุดมันก็ใช้งานได้ รองรับตัวคั่นหลายตัว

#include "stdafx.h"
#include <iostream>
using namespace std;

char* mystrtok(char str[],char filter[]) 
{
    if(filter == NULL) {
        return str;
    }
    static char *ptr = str;
    static int flag = 0;
    if(flag == 1) {
        return NULL;
    }
    char* ptrReturn = ptr;
    for(int j = 0; ptr != '\0'; j++) {
        for(int i=0 ; filter[i] != '\0' ; i++) {
            if(ptr[j] == '\0') {
                flag = 1;
                return ptrReturn;
            }
            if( ptr[j] == filter[i]) {
                ptr[j] = '\0';
                ptr+=j+1;
                return ptrReturn;
            }
        }
    }
    return NULL;
}

int _tmain(int argc, _TCHAR* argv[])
{
    char str[200] = "This,is my,string.test";
    char *ppt = mystrtok(str,", .");
    while(ppt != NULL ) {
        cout<< ppt << endl;
        ppt = mystrtok(NULL,", ."); 
    }
    return 0;
}


1

นี่คือการใช้งานของฉันซึ่งใช้ตารางแฮชสำหรับตัวคั่นซึ่งหมายความว่า O (n) แทน O (n ^ 2) (นี่คือลิงค์ไปยังรหัส) :

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

#define DICT_LEN 256

int *create_delim_dict(char *delim)
{
    int *d = (int*)malloc(sizeof(int)*DICT_LEN);
    memset((void*)d, 0, sizeof(int)*DICT_LEN);

    int i;
    for(i=0; i< strlen(delim); i++) {
        d[delim[i]] = 1;
    }
    return d;
}



char *my_strtok(char *str, char *delim)
{

    static char *last, *to_free;
    int *deli_dict = create_delim_dict(delim);

    if(!deli_dict) {
        /*this check if we allocate and fail the second time with entering this function */
        if(to_free) {
            free(to_free);
        }
        return NULL;
    }

    if(str) {
        last = (char*)malloc(strlen(str)+1);
        if(!last) {
            free(deli_dict);
            return NULL;
        }
        to_free = last;
        strcpy(last, str);
    }

    while(deli_dict[*last] && *last != '\0') {
        last++;
    }
    str = last;
    if(*last == '\0') {
        free(deli_dict);
        free(to_free);
        deli_dict = NULL;
        to_free = NULL;
        return NULL;
    }
    while (*last != '\0' && !deli_dict[*last]) {
        last++;
    }

    *last = '\0';
    last++;

    free(deli_dict);
    return str;
}

int main()
{
    char * str = "- This, a sample string.";
    char *del = " ,.-";
    char *s = my_strtok(str, del);
    while(s) {
        printf("%s\n", s);
        s = my_strtok(NULL, del);
    }
    return 0;
}

1

strtok () จัดเก็บตัวชี้ในตัวแปรคงที่เมื่อคุณหยุดทำงานครั้งสุดท้ายดังนั้นในการเรียกครั้งที่ 2 เมื่อเราส่งค่า null strtok () จะรับตัวชี้จากตัวแปรคงที่

หากคุณระบุชื่อสตริงเดียวกันอีกครั้งจะเริ่มตั้งแต่ต้น

ยิ่งไปกว่านั้น strtok () คือการทำลายล้างเช่นมันทำการเปลี่ยนแปลงกับสตริง orignal ดังนั้นตรวจสอบให้แน่ใจว่าคุณมีสำเนา orignal เสมอ

อีกปัญหาหนึ่งของการใช้ strtok () คือเนื่องจากมันเก็บที่อยู่ในตัวแปรคงที่ในการเขียนโปรแกรมแบบมัลติเธรดที่เรียก strtok () มากกว่าหนึ่งครั้งจะทำให้เกิดข้อผิดพลาด สำหรับสิ่งนี้ให้ใช้ strtok_r ()


0

สำหรับผู้ที่ยังคงมีปัญหาในการทำความเข้าใจstrtok()ฟังก์ชั่นนี้ลองดูตัวอย่าง pythontutorนี้ซึ่งเป็นเครื่องมือที่ยอดเยี่ยมในการแสดงภาพโค้ด C (หรือ C ++, Python ... ) ของคุณ

ในกรณีที่ลิงก์เสียให้วาง:

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

int main()
{
    char s[] = "Hello, my name is? Matthew! Hey.";
    char* p;
    for (char *p = strtok(s," ,?!."); p != NULL; p = strtok(NULL, " ,?!.")) {
      puts(p);
    }
    return 0;
}

เครดิตไปที่Anders K.


0

คุณสามารถสแกนอาร์เรย์ถ่านเพื่อค้นหาโทเค็นหากคุณพบว่าเพิ่งพิมพ์บรรทัดใหม่หรือพิมพ์อักขระ

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

int main()
{
    char *s;
    s = malloc(1024 * sizeof(char));
    scanf("%[^\n]", s);
    s = realloc(s, strlen(s) + 1);
    int len = strlen(s);
    char delim =' ';
    for(int i = 0; i < len; i++) {
        if(s[i] == delim) {
            printf("\n");
        }
        else {
            printf("%c", s[i]);
        }
    }
    free(s);
    return 0;
}

0

ดังนั้นนี่คือข้อมูลโค้ดเพื่อช่วยให้เข้าใจหัวข้อนี้ดีขึ้น

การพิมพ์โทเค็น

งาน: กำหนดประโยค s พิมพ์แต่ละคำของประโยคในบรรทัดใหม่

char *s;
s = malloc(1024 * sizeof(char));
scanf("%[^\n]", s);
s = realloc(s, strlen(s) + 1);
//logic to print the tokens of the sentence.
for (char *p = strtok(s," "); p != NULL; p = strtok(NULL, " "))
{
    printf("%s\n",p);
}

การป้อนข้อมูล: How is that

ผลลัพธ์:

How
is
that

คำอธิบาย:ดังนั้นที่นี่จึงใช้ฟังก์ชัน "strtok ()" และทำซ้ำโดยใช้สำหรับการวนซ้ำเพื่อพิมพ์โทเค็นในบรรทัดแยกกัน

ฟังก์ชันจะใช้พารามิเตอร์เป็น 'สตริง' และ 'จุดพัก' และทำลายสตริงที่จุดพักเหล่านั้นและสร้างโทเค็น ตอนนี้โทเค็นเหล่านั้นจะถูกเก็บไว้ใน 'p' และใช้สำหรับการพิมพ์ต่อไป


ฉันคิดว่าการอธิบายผ่านตัวอย่างนั้นดีกว่าการอ้างถึงเอกสารบางฉบับ
tr_abhishek
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.