Chinaunix首页 | 论坛 | 博客
  • 博客访问: 255841
  • 博文数量: 108
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 314
  • 用 户 组: 普通用户
  • 注册时间: 2014-03-29 10:58
文章分类

全部博文(108)

文章存档

2015年(20)

2014年(88)

我的朋友

分类: C/C++

2014-06-27 09:56:26

這一篇最初的目的,是想來整理一下在 C++ 裡的字串切割的方法。不過寫到一半就發現,其實 Boost 提供的相關工具都可以延伸到其他形式的資料,並不僅限於字串,所以變成內容是著重在 STL containter 的切割,所以後來就決定變成是由字串切割,來帶到 split() 這個函式,以及Tokenizer 這個函式庫的介紹了。

這邊的「字串切割」,Heresy 個人是把它定義成為:「一個給定的字串裡,根據給定的字元來當作切割的條件,把這個字串分成好幾的部分」;比如說一個英文句子「Hello, the beautiful world!」,假設我們用空白、「,」、「!」這三個字元來做切割的話,他可以切割為「Hello」、「the」、「beautiful」、「world」這四個字串。

這類的動作,在要處理文字檔,或是要求使用者輸入數字的時候,都常有可能會用到;而要做到這樣把一個字串根據特定字元來切開的工作,除了自己下去掃整個字串外,其實還有不少現成的方法可以用,這邊就大概來提一下吧?

使用 C strtok()

要使用 C 語言來切割字串的話,基本上一般應該都是採用 strtok() 這個函式()。

strtok() 基本上是針對 C string(字元陣列)來做處理,每次呼叫會取出他的其中一項,所以可以透過迴圈的方法,來把整個字串切割完成;下面就是一個簡單的例子:

#include  #include  #include    using namespace std;  int main( int argc, char** argv )
{ char str[] = "Hello, the beautiful world!"; char spliter[] = " ,!"; char * pch; pch = strtok( str, spliter ); while( pch != NULL )
  {
    cout << pch << endl; pch = strtok( NULL, spliter ); } return 0;
}

在這個範例裡,str 這個字串是要被切割的字串,而 spliter 則是用一個字串來儲存要用來切割字串的字元,在這邊就是「 」、「,」和「!」。

而 strtok() 這個函式,它的形式是:

char* strtok( char* str, const char* delimiters );

使用 strtok() 時,要傳入兩個字元陣列,第一個是要被切割的字串(str)、第二個則是用來切割的字元(delimiters);而執行後他則會回傳一個字串,代表切割後的結果。比較特別的是,他只有在第一次呼叫的時候,要傳入要被切割的字串(str),這時候他會吧這個字串紀錄在內部,之後只要給他 NULL 就可以了。

而當 strtok() 有正確地切割出字串後,他就會把切出來的字傳傳回來,當沒有辦法切割的時候,則會回傳 NULL;所以要把整個字串都做處理的話,也就只要用迴圈不停地去執行 strtok( NULL, spliter),直到他的回傳值是 NULL 就可了?

像上面這樣的程式的結果,就會是:

Hello
the
beautiful
world

另外在使用 strtok() 時要注意的一點就是,傳入要被切割的字串(str)的內容,是會被改掉的?也就是在上面的範例裡,str 在執行過 strtok() 後,本身的內容就已經變了!所以如果這個字串還需要被重複使用的話,就得自己先複製一份了。

 

Boost String Algorithms 的 split

基本上,在一般要針對文字做處理狀況下,strtok() 的功能已經算是夠用了。不過說實話,Heresy 實在不喜歡它的使用邏輯(第二次以後要傳 NULL 進去的這種寫法…)…而且,他和 sprintf() 一樣,也算是個不安全的函式(參考《用 snprintf / asprintf 取代不安全的 sprintf》),所以個人不是很喜歡使用他。(註:在 gcc 下,應該還不是 thread-safe 的)

那如果不想用 strtok() 的話,有什麼替代方案嗎?其中一個,就是在 Boost C++ Libraries 裡,有一個專門為了處理字串的函示庫「String Algorithms Library」(),裡面的 split() 這個函式()雖然使用上的概念和 strtok() 不同,不過也可以很方便地做到同樣的事。他的用法是:

#include  #include  #include  #include  #include    using namespace std;  int main( int argc, char** argv )
{
  string s = "Hello, the beautiful world!";
  vector rs; boost::split( rs, s, boost::is_any_of( " ,!" ), boost::token_compress_on ); for( vector::iterator it = rs.begin(); it != rs.end(); ++ it )
    cout << *it << endl; return 0;
}

在上面的例子,和 strtok() 在處理時是一項一項地取出來相比,Boost 的 split() 是直接把結果放到一個 vector 裡(上面的) rs,完成後再讓使用者直接操作這個 vector;這點應該是兩者在操作邏輯上最大的差異了?(不過注意,上面的結果在最後會多一項是一個空字串)

而這邊可以看到,split() 有四個參數:

  • 第一個參數(rs)是用來儲存分割結果的容器。 
    他基本上是要是 STL 的容器(container),也就是說不一定是要 vector,換成用 set、list 等其他的 STL 的 containter 也是可以的。 
     
  • 第二個參數(s)是要切割的內容。 
    在這邊的型別就是 string;不過由於 split() 是一個 template 函式,所以只要其他的參數也都有改成對應的形式,那這邊也不限定一定要是 string 的。 
     
  • 第三個參數(boost::is_any_of" ,!" ))則是用來設定切割條件用的 function object。 
    這邊所使用的 is_any_of() 則是 Boost 在 /algorithm/string/classification.hpp 裡提供的預設函式,代表只要符合給定的字元都可以;而在這個標投檔裡除了 is_any_of() 外,也有提供不少其他現成的函式可以直接使用,有興趣的話可以參考 Boost 官。 
    而如果有需要的話,也可以自己撰寫符合自己需求的函式,拿來給 split() 用。 
     
  • 而最後一項參數,則是 boost 的 token_compress_mode_type。 
    他的值有 token_compress_on 和 token_compress_off 兩種(),是用來控制是否要「壓縮」找到的相連項目,預設是關閉的;在 Heresy 測試的結果看來,他主要是會把中間的空項給刪除。

而以這個例子來說,s 這個字串在經過 split() 的處理後,就會產生 rs 這個 vector,裡面的會有五項,內容分別是:「Hello」、「the」、「beautiful」、「world」、「」;雖然和strtok() 的結果相比,最後多了一個空字串,這點比較討厭,不過在 Heresy 來看,這樣的操作邏輯簡單多了?

 

將 split() 用於字串以外

前面也有提過,雖然 split() 這個函式是屬於 Boost 裡的 String Algorithms 的一部分,但是由於他本身是 template 的,所以也可以適用於其他型別的資料。下面就是一個簡單的例子:

#include  #include  #include  #include  #include  #include    using namespace std;  int main( int argc, char** argv )
{ // create test data  vector<int> v; for( int i = 0; i < 20; ++ i )
    v.push_back( i ); // create the set to split data  set<int> spliter;
  spliter.insert( 5 );
  spliter.insert( 6 );
  spliter.insert( 10 ); // split  list< vector<int> > rsv; boost::split( rsv, v, boost::is_any_of( spliter ) ); // output result  for( list< vector<int> >::iterator it = rsv.begin(); it != rsv.end(); ++ it )
  { for( vector<int>::iterator it2 = it->begin(); it2 != it->end(); ++ it2 )
      cout << *it2 << ",";
    cout << "\n";
  } return 0;
}

在這個例子裡,Heresy 是用 vector<int> 取代原來的 std::string,來做為要被切割的資料(v);它的內容則是 0 - 19,總共 20 的整數。

而用來切割的條件,還是使用 Boost 提供的 is_any_of(),不過相對的,條件 spliter 的型別則是變成 set<int>;數值的部分則是 5、6、10 三個數字,也就是遇到這接數字就會進行切割。另外,實際上 spliter 也可以用 std::vector 或是直接用陣列的形式,並不一定要是 std::set

在輸出的結果部分,Heresy 這邊則是用 list< vector<int> > 來儲存,有需要的話,也可以換成其他不同的 Container。而這樣的執行結果呢,則是:

0,1,2,3,4,

7,8,9,
11,12,13,14,15,16,17,18,19,

可以發現,中間有一整行是空的,這是因為在呼叫 split() 時沒有指定 token_compress_on 的關係;如果把它改成 boost::split( rsv, v, boost::is_any_of( spliter ),boost::token_compress_on ); 的話,那這個空的結果就會消失了。但是要注意的是,如果空白結果是出現在頭尾的話,那就算設定 token_compress_on 也是沒有用的。

最後,下面則是一個使用自訂條件的函式(TestFunc())來當作切割條件的例子:

#include  #include  #include  #include    using namespace std;  bool TestFunc( int x )
{ if( x % 5 == 0 ) return true; else return false;
}  int main( int argc, char** argv )
{ // create test data  vector<int> v; for( int i = 0; i < 20; ++ i )
    v.push_back( i ); // split  vector< vector<int> > rsv;
  boost::split( rsv, v, TestFunc ); // output result  for( vector< vector<int> >::iterator it = rsv.begin(); it != rsv.end(); ++ it )
  { for( vector<int>::iterator it2 = it->begin(); it2 != it->end(); ++ it2 )
      cout << *it2 << ",";
    cout << "\n";
  } return 0;
}

 转载:http://viml.nchc.org.tw/blog/paper_info.php?CLASS_ID=1&SUB_ID=1&PAPER_ID=209

阅读(1538) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~