アプリ版:「スタンプのみでお礼する」機能のリリースについて

csvやファイルパスをカンマや'\'で分割する関数をユニコードでも
対応できるように改修していのですがやり方が分かりません。

関数のロジックはおおよそ以下の通りです。
(仕事先のセキュリティの関係でソースは持ち出せませんので概要ですが・・・)

split(str, item, len, words[item][len], delimiter)
char* str: 分割対象の文字列
int item:項目数(上限あり)
int len:項目の文字数(上限あり)
words[item][len]:文字列を分割、項目として格納する配列
char delimiter:区切り文字

int i=0, j=0;
while(*str != NULL){

// 文字列を分割する処理
if(str != delimiter){
words[i][j++] = *str
}
else if{
words[i][j] = NULL
i++; j=0;
}
・・・・・・・・・

str++;
}
// 後処理があります

VCの設定がマルチバイト対応だったので問題なかったのですが
ユニコードに設定を切り替えるとパスの分割でカタカナの「ソ」
を含む文字列が正しく分割されないという事象が発生しました。

関数内の改修だけで解決する方法があるでしょうか?
ある程度プロジェクトが動いているのであまり時間をかけないで
対応する必要があります。

OS: Windows7 SP1
環境: VC++ 2008 MFC

A 回答 (10件)

...別に問題なさそうですが。



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

int split(const TCHAR* str, TCHAR* words[], TCHAR delimiter) {
TCHAR* p;
int row = 0;
TCHAR* buf = (TCHAR*)malloc(sizeof(TCHAR)*(_tcslen(str)+1));
_tcscpy(buf, str);
words[row] = buf;
for ( p = buf; *p != _T('\0'); ++p ) {
if ( *p == delimiter) {
*p = _T('\0');
words[++row] = p+1;
}
}
return ++row;
}

/* おためし */
int main() {
const TCHAR* input = _T("ソーシャル,ネットワーク,,ソリューション,");
TCHAR* words[5];
int n;
int i;
setlocale(LC_ALL, "japanese");
n = split(input, words, L',');
for ( i = 0; i < n; ++i ) {
_tprintf(_T("[%s]\n"), words[i]);
}
free(words[0]);
}

この回答への補足

回答ありがとうございます。

いくつか確認させてください。
1.split()の引数が全てTCHAR型です。
  問題の関数は全て現在はchar型で処理しています。
  内部的にTCHAR型にキャストして処理すればよいでしょうか?
  あるいはsplit()の呼び出し部分も合わせて改修が必要ですか?
  プログラム内部では文字列は原則std::string型で保持していて
 split()コール時にchar型にキャストしています。

2.setlocale()がコールされていますが、必須でしょうか?
  現状はファイルI/Oかコンソール出力時のみ、該当の箇所で直前に
 設定して処理後に設定を解除しています。

3.main()関数の最後にfree()がコールされていますが、TCHAR型は
 メモリ領域を確保いているのでしょうか?

よろしくお願いいたします。

補足日時:2012/02/19 10:10
    • good
    • 0

そのファイルがShift_JISで書かれていているのでは?


それをchar*として読み込めば、「全角」文字は「2つの文字」になります。
「ソ」を2つに分けたとき、2番目が「\」になっています。
この2番目の\を「ソの一部」と判断できずに「\」だと判定してしまっているのが今の現象です。
※ 「シフトJISのだめ文字」として有名な現象です。検索すれば他にもあることがわかります。

プロジェクト全体を変えるなら、TCHARを使って全角文字を「1文字」として処理するようにするのがいいのでしょう
(#1さんにある方法です)

ここだけを変えるなら、「2バイト目でないdelimiter」だけを処理するようにすることでしょう。
例えば。
bool is2byte = false ; // 2バイト目かどうかのフラグ
while(*str != NULL){
 // 文字列を分割する処理
 if( is2byte || (str != delimiter) ){ //2バイト目ならdelimiterとの比較はしない
  words[i][j++] = *str;
  // 次の処理が2バイト目かどうか
  if(is2byte){
   is2byte=false;
  }else{
   is2byte=(*strが2バイト文字の1バイト目):
  }
 }
 else {
  words[i][j] = NULL
  i++; j=0;
  is2byte=false;
 }
とか。



あと、細かいことかもしれませんが
> while(*str != NULL){
> words[i][j] = NULL

「何も指していないポインタ」であるNULLと、ヌル文字'\0'は別です。
VC++の実装でNULL='\0'=0にはなっていますが。
    • good
    • 0
この回答へのお礼

回答ありがとうございます。

>「ソ」を2つに分けたとき、2番目が「\」になっています。
>この2番目の\を「ソの一部」と判断できずに「\」だと判定してしまっているのが今の現象です。

知りませんでした。
いままで文字コードを意識してコーディングをしたことがなかったもので・・・

>あと、細かいことかもしれませんが
>> while(*str != NULL){
>> words[i][j] = NULL

>「何も指していないポインタ」であるNULLと、ヌル文字'\0'は別です。
>VC++の実装でNULL='\0'=0にはなっていますが。

こちらのほうは改修に合わせて修正します。

お礼日時:2012/02/19 22:07

>  内部的にTCHAR型にキャストして処理すればよいでしょうか?


あなたのいうUNICODE対応とは何ですか?
呈示したコードは文字セットがchar, wchar_tのどちらでも動くことを
目的としたものです。

> setlocale()がコールされていますが、必須でしょうか?
それは僕がきめられることではありません。

> main()関数の最後にfree()がコールされていますが、TCHAR型は
> メモリ領域を確保いているのでしょうか?
コードを読んでください。spィt内で確保した領域を解放しています。
    • good
    • 0
この回答へのお礼

何度も回答いただきありがとうございます。

1.2.については不具合を指摘した人の環境を確認してみます。

3.についてはコードをよく読んでいませんでした。

#1で示していただいた内容と、他の方のアドバイスも含めて
プロジェクトのリーダに相談いたします。

お手数をおかけしました。

お礼日時:2012/02/19 22:33

どうも


epistemeさんへの補足について

1.
そこなんですよね。

「ふつ~」に使いたいという前提では少なくとも

char*の内部表現とwchar_t*の内部表現は違いますので
こればっかりは全部マルチバイト専用のコードになってる場合
必要箇所全てにわたって書きなおす必要があります。

(現実的には「編集→検索と置換→クイック置換」などの機能を駆使して必要箇所をまとめて書き変えていく、といったことになるかと)

コストがかかっても変換する必要がある場合はWideCharToMultiByteやMultiByteToWideCharなどで変換する必要があります。

(MFCではCStringA、CStringW、CStringなどの便利な使い方があったと思いますが
http://www.usefullcode.net/2006/11/widechartomul …

ただ
いずれにせよ、よっぽどでかいプロジェクトの場合、最初からこれが配慮されてないコードで書かれたものを正確に直し続けていく事は

結構大変な作業になることは覚悟しなければいけません。

ただし、特定の個所でのみ両対応にする、という場合はそれほど大変ではない場合もあります。

例えば

const char* text = "aaa";
MessageBox(NULL, text, "", 0 );

はデフォがマルチバイトの設定でしか通用しない書き方ですが

LPCTSTR text = _T("aaa");
MessageBox(NULL, text, _T(""), 0 );

とかに直していくのが面倒で、この付近に関してはマルチバイト文字しか使わなくてOKと分かってる場合は

const char* text = "aaa";
MessageBoxA(NULL, text, "", 0 );

こういう風に、呼び出し側の関数をマルチバイト専用に明示的に指定してやることで対処することも可能です。

逆に、ユニコード版しか用意されてない関数とかでは

LPCWSTR text = L"WINDOW";
HTHEME theme = OpenThemeData( hw, text );

などと、明示的にユニコード指定にしてやる必要があります。


2.
ユニコードを使う「可能性がある箇所」では必須です。
なのでTCHARを使う場合はやっておいてください。
とはいえ、アプリケーション中で一つのロケールしか必要ないのであれば
アプリケーションの最初に1回だけ呼んでおいて、そのまま変更しなければOKです。


3.
これは
epistemeさんが、baysidehotelさんの「関数のロジック」をもとに
splitを書いたときに

内部でmallocを使用したことに起因するものです。

もし私が一から作る場合は

確保と解放は近くに書かれてた方がぱっと見分かりやすいので、呼び出し側で確保するともいます。

コードを一部お借りすると
MFCが使えるという事でC++の規則込みで書いてしまいますが


#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <locale.h>
#include <tchar.h>

typedef LPCTSTR const TcsLiteral;

int split( TCHAR* p, LPCTSTR words[], const TCHAR delimiter ){

int row = 0;
words[row] = p;

for ( ; *p; ++p ) {
if ( *p != delimiter ) continue;
*p = _T('\0');
words[++row] = p+1;
}

return ++row;

}

/* おためし */

int main() {

TcsLiteral input = _T("ソーシャル,ネットワーク,,ソリューション,");

LPCTSTR words[5]; //おためしなので決め打ち
TCHAR* const temp = _tcsdup( input ); //確保に当たる

setlocale( LC_ALL, "japanese" );

const int n = split( temp, words, _T(',') );
for ( int i = 0; i < n; ++i ) _tprintf( _T("[%s]\n"), words[i] );

free(temp); //対で解放

}


私ならこんな感じにするかもしれません。

実行結果

[ソーシャル]
[ネットワーク]
[]
[ソリューション]
[]
    • good
    • 0
この回答へのお礼

回答ありがとうございます。

1.2.について
→やはり根本的には相当数のコストをかけて該当箇所を修正するのがベストですね
 ただ実施できるかは。。。

3.について
→epistemeさんのコードをよく見ていませんでした。
 メモリの確保と開放がmain()内にあるので保守性を考慮するとこちらのほうが
 いいのかもしれません。

お礼日時:2012/02/19 22:17

もひとつ



>プログラム内部では文字列は原則std::string型で保持していて

依然大変な作業になる可能性はあるかもしれませんが、状況次第では
こちらのページの内容が役に立つかもしれません。

・UNICODEで分けないtstring型を宣言する
http://marupeke296.com/TIPS_No14_tstring.html
    • good
    • 0

>char* str: 分割対象の文字列



「分割対象の文字列」の文字コードがUTF-8であるなら問題ないですが
(問題なといってもASCIIコードの文字で分割する場合においてですが)
Shift_JISだと処理内部でシングルバイト文字と
マルチバイト文字の1バイト目と2バイト目を区別する必要があります。
なぜ区別しないといけないかはShift_JISの文字コード表とにらめっこするなり
googleなどで検索するなりしてみてください。

また他の人もいってますが
>while(*str != NULL){
>words[i][j] = NULL
コンパイラはエラーや警告を出さないかもしれませんが
内容的にはバグと変わりありません。
NULLはポインタなので例え定義が0であろうと
charとして扱ってはいけません。
    • good
    • 0
この回答へのお礼

回答ありがとうございます。

いただいた指摘と他の方のアドバイスも含めてプロジェクトのリーダさんを相談します。

お礼日時:2012/02/19 22:27

あ、さらにもうひとつw


C++用に変更、なのでやってもなんら問題のですが(むしろやった方が良いはず)
下のコードで修正忘れてました。

int main(){
の直後に
setlocale( LC_ALL, "japanese" );
を持ってくるように直してください。
そうすれば、内部で何が行われてても確実に大丈夫なはずです。
    • good
    • 0
この回答へのお礼

何度も回答いただきましてありがとうございます。

いただいたアドバイスともとにプロジェクトのリーダを相談したいと思います。

お礼日時:2012/02/19 22:21

おお!全員に対してしっかりご返答なさっていらっしゃいますし


ご担当なさっているプロジェクトのこれからについて大事なことでしょうから

こちらとしてももうちょいしっかり情報を補足しておきます。


・ユニコードを使う「可能性がある箇所」では必須です。

と書きましたが、これの意図するところは

実際には
現状ではファイルI/Oやコンソール出力時などのみで、確かに十分、というのが実状かもしれません。

ただし「将来的なことまで見据えて考えると」
Microsoftの気まぐれ一つで状況が変わったりするかもしれませんし

「特定の、この関数」内部では未来永劫絶対setlocaleの必要はない!と断言するのは
さすがにMicrosoft幹部でも難しいと思うので

場合分けしてやって、将来変更がないか常におびえる状況になるよりも
先に全体にやっておいた方が遥かにリスクが少ない

ので「必須と思った方が遥かに楽であろう」という意味です。


・usertype.datについて

既にご存知でしたら良いですが
もしまだ未着手でしたらだいぶ可読性が上がるかもしれませんので

usertype.datを使うと、intやifなどの、元々色がつくようになっている
予約語など以外にも
VC++上で任意のキーワードに色がついた状態で表示出来ます。

こちらの環境では
Program Files\Microsoft Visual Studio 9.0\Common7\IDEの中に
usertype.datを置くと効果がありました。

既にある場合は、ここに色を変えたいキーワードを一行にひとつずつ書き加えます。
ない場合は、例としてはメモ帳などで以下のように書いたテキストファイルを作り

LPCTSTR
TcsLiteral
TCHAR

ファイル名と拡張子をusertype.datにして上記などの、適切なディレクトリに置き
VC++を再起動すれば効果が確認できるはずです。



・実際の実装法について

実際では決め打ちとかできないので
標準ではない便利なライブラリとかになるべく頼らず書くなら(C++03の仕様では)
これくらい書かないといけないかもしれません。

(なお、載せるのに分割状況書くのは面倒なので全部インライン指定になってますが、コンストラクタやデストラクタなどは一見短いように見えて、生成される機械語は長くなったりする可能性があるのでインライン化はこの場合お勧めできません。また、細かい仕様とかその辺は調整してください。)


typedef LPCTSTR const TcsLiteral;

class TcsPtrArray {

typedef struct LINEARDATA {
LINEARDATA* next;
TcsLiteral p;
LINEARDATA( TcsLiteral p_ ) : next(NULL), p(p_){}
} LD;


TCHAR* temp;
LD* words;

TcsPtrArray() : temp(NULL), words(NULL) {}
~TcsPtrArray(){
free( temp );
ReleaseAllWords();
}

void ReleaseAllWords(){
for ( LD* a; a = words; ){ words = words->next; delete a; }
words = NULL;
}

public:

static void CheckFunc_S( TcsLiteral s ){ _tprintf( _T("[%s]\n"), s ); }

void Split( const TCHAR delimiter ){

if (words) ReleaseAllWords();

TCHAR* c = temp;
words = new LD( c );
LD** pp = &words->next;

for ( ; *c; ++c ) {
if ( *c != delimiter ) continue;
*c = _T('\0');
*pp = new LD( c+1 );
pp = &(*pp)->next;
}

}


template < class F >
void ForEach( F& f ) const { for ( const LD* a = words; a ; a = a->next ) f(a->p); }

static bool Create( TcsPtrArray** pp, TcsLiteral s ){ //生成

try { *pp = new TcsPtrArray; }
catch ( std::bad_alloc ){ return false; }

(*pp)->temp = _tcsdup( s );
if ( !(*pp)->temp ) return false;

return true;

}

static void Release( TcsPtrArray** pp ){ //解放
delete *pp; *pp = NULL;
}

};


少々長くなりましたが、必要ヘッダとかをインクルードすればコピペでサクッと出来ると思います。
機能拡張等は用途に応じてご自由に行ってください。

ここまで準備しておけば使い方は簡単です。


int main() {

setlocale( LC_ALL, "japanese" );

TcsLiteral input = _T("ソーシャル,ネットワーク,,ソリューション,");

TcsPtrArray* tpa;
if ( !TcsPtrArray::Create( &tpa, input ) ) return 0;

tpa->Split( _T(',') );
tpa->ForEach( TcsPtrArray::CheckFunc_S );

TcsPtrArray::Release( &tpa ); //Createに対して対で解放

}



分からない箇所がありましたら聞いてください。
    • good
    • 0

おっと、ReleaseAllWords関数のとこの


words = NULL;
は不要でしたw
美しくないので削除しておいてくださいw
    • good
    • 0

それとCreate関数内で



if ( !(*pp)->temp ) return false;
となってる部分がありますが


if ( !(*pp)->temp ){
Release( pp );
return false;
}

にしとかないと(ほとんどないとは思いますが)メモリリークの危険が一応残っていますね。

ついでにstd::bad_allocに対しても全部例外安全にしようとすると
Splitのnewのとこもこだわらないといけませんが

std::bad_allocが飛ぶ状況ってほとんどまず考慮しないでいいかもしれないので
アプリケーション次第ではtry-catch使わない方向で割り切ってしまっても全然いいかもしれません。

(後で細切れで気づくと何度も投稿しないといけないのが不便ですw 編集出来たらいいのに >教えて!goo )
    • good
    • 0
この回答へのお礼

回答ありがとうございます。
本日、リーダさんに確認をとりました。

プロジェクトのどこかで工数を確保して改修することになりそうです。

いただいた回答全てを盛り込めるほどの工数は無理と思いますが・・・

お礼日時:2012/02/21 00:29

お探しのQ&Aが見つからない時は、教えて!gooで質問しましょう!