質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.78%

制作した数値入力関数をより安全高速綺麗にしたい

受付中

回答 9

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 1,050

Kazumori102

score 40

前提・実現したいこと

前々から、「半角の「数字、正負記号、小数点」以外を受け付けず、かつ、桁数制限のある関数」を作成したいと思っており、ついに完成しました。これをさらに安全高速綺麗にしたいです。ご助言願います。

仕様

文字コードにおいて数字0~9は、0から始まり連続して存在しているとする。(そうでない環境なんてレアらしいし。)
与えた因数で桁数を制限する
返り値は成功した場合は入力した文字列をdouble型に変換したもの。
失敗は今のところ定義されていない。入力にミスがあった場合は入力処理を繰り返すようになっている。
char型配列dateの大きさを越える入力があるとオーバーフローする。

ソースコード

//http://stroll.hatenablog.com/entry/2015/07/20/221125 を基に開発
#ifndef imputtestfunc//二重でincludeされることを防ぐ
#define imputtestfunc

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

double dnkscan(int lim) {
    char data[256];
    int i,j, intp = 0,decp=0, nsign = 1, flag = 0, isdecimal=0;
    double dnam;
    //printf("[db]入力文字列が「半角数値及び正負記号,小数点」の時はその値を返します。それ以外ははエラーとなります。\n");
    //printf("[db]%d桁以内の数値を入力してください。>>\n",lim);
    do{
        intp=0;decp=0;nsign=1;flag=0;isdecimal=0;

        //fgets(data,lim,stdin);//new これだと入力許容桁数で自動的に切り詰められてOK判定が出てしまう。
        scanf("%s", data) ;//old これだと、オーバーフローの可能性が捨てきれない。
        if (strlen(data) > lim) {
            flag = 2;
        }
        i = 0;j = 0;
        if (flag == 0 && data[0] == '+') {
            i++;
        }
        if (flag == 0 && data[0] == '-') {
            nsign = -1;
            i++;
        }

        //printf("[db]flag is %d data[i] is %c isdecimal is %d j is %d\n",flag,data[i],isdecimal,j);
        for (; i < strlen(data); i++) {
            //printf("[db]flag is %d data[i] is %c isdecimal is %d j is %d\n",flag,data[i],isdecimal,j);

            if (flag == 0 && data[i] == '.'&&isdecimal==0) {
                isdecimal = 1;
                //i++;//いるか?
            }

            else if (flag == 0 && (data[i] >= '0' && data[i] <= '9')&&isdecimal==0) {
                intp = intp * 10 + data[i] - '0';
                //printf("[db]整数部シーケンス intp=%d\n",intp);
            }
            else if(flag == 0 && (data[i] >= '0' && data[i] <= '9')&&isdecimal==1) {
                decp = decp * 10 + data[i] - '0';
                //printf("[db]小数部シーケンス decp=%d\n",decp);
                j++;
            }
            else{
                if(flag == 2) {
                    printf("桁あふれエラー>>\n");
                    break;
                }else{
                    printf("エラー>>\n");
                    flag = 1;
                    break;
                }
            }
        }
        //printf("[db]flag is %d data[i] is %c isdecimal is %d j is %d\n",flag,data[i],isdecimal,j);
        if (flag == 0) {
            dnam = nsign*( intp+ decp*pow(0.1,j) );
            //printf("成功:%lf\n", dnam);
        }
    }while (flag!=0);
    ///memset(data, '\0', sizeof(data) );
    //printf("[db]date=[%s]",data);//ちゃんとリセットかかっているかの点検用
    return dnam;
}
#endif

やってみたこと

fgets使ってみましたが、あふれた桁を読み飛ばして、成功としてしまうので、これはどう回避したものか。

余談

関数名は double型のnamberをscanするkazumori製関数という意味で付けました。もっといい感じの名前も募集中。

19/02/13追記

変換部分の試行錯誤として、C言語関数辞典のstrtodを参考に作ってみようと、サンプルコードを試してみましたが求める結果が出ませんでした。関数の仕様でしょうか?

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

/* macros */
#define N 256

/* main */
int main(void) {
    char s[N] = {'\0'}, *endptr;
    double x;

    /* 入力 */
    fgets(s, N, stdin);
    printf("変換前: %s", s);

    /* 変換 */
    x = strtod(s, &endptr);
    printf("変換後: %.2f\n", x);
    printf("endptr: %s\n", endptr);

    return EXIT_SUCCESS;
}
変換前: 123456789012345678901234567890
変換後: 123456789012345680000000000000.00
求結果: 123456789012345678901234567890.00
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • cateye

    2018/12/26 16:48

    上記プログラムは、"+123.45 67890"という入力はどうなるのでしょう?

    キャンセル

  • Kazumori102

    2018/12/26 17:21 編集

    #include <stdio.h>
    #include "imputtestfunc1.h"
    int main(void) {
    printf("%lf\n",dnkscan(9));
    }
    で実行すると、「123.450000」 ですね。scanfの挙動故にspaceで区切られちゃってますね。

    キャンセル

回答 9

+4

内部実装ではなく外部仕様に対するコメントですが、ご参考にどうぞ:

char型配列dateの大きさを越える入力があるとオーバーフローする。 

安全性の観点で、この外部仕様は致命的な欠陥を抱えています。C言語において「オーバーフローが生じる可能性がある」は、「学習用サンプルコード以外では使い物にならない」と同義です。

実際、バッファ・オーバーフローの可能性をゼロにできないC標準ライブラリ関数gets()は、既にC標準ライブラリからも削除されました。JPCERTの MSC24-C. 非推奨関数や時代遅れの関数を使用しない も参照ください。

与えた因数で桁数を制限する

「桁数を制限」とありますが、実装を見る限り「文字数を制限」でしょうか?この外部仕様で望ましいかは一考の余地があると思います。例:引数に3を与えたとき、入力+3.1を受け入れるべきですか?入力3.1はどうでしょう?

(PS. s/因数/引数/ですね)

失敗は今のところ定義されていない。入力にミスがあった場合は入力処理を繰り返すようになっている。

キーボードからの入力のみを想定しているのでしょうか?該当プログラムの標準入力をパイプ接続する(他プログラムの出力を本プログラムの入力とする)場合は、使い勝手が悪いかもしれません。対象外とするのでであれば問題ないと思います。

文字コードにおいて数字0~9は、0から始まり連続して存在しているとする。(そうでない環境なんてレアらしいし。)

数字0~9に対する文字コードの連続性は、C言語仕様上100%保証されます。数字から数値への変換: ch - '0' もご参考に。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/12/26 17:31 編集

    >使い物にならない
    そのとおりですね。

    >制限~
    文字数より桁数を制限したいですね。2.4なら二桁。

    >入力の想定
    パイプもつなげられれば便利とは思っていますが、今のところはキーボードのみの予定です。もしよろしければパイプの際はどういうふうにした方がいいかのアドバイスをいただければ幸いです。

    >Cの仕様で保障されてましたか。
    文字コードについて調べた際に連続していないものもあるといった感じの記述を見た覚えがあったので。

    キャンセル

  • 2018/12/26 19:09

    > パイプの際はどういうふうにした方がいいか

    現在の外部仕様は「外部入力の解釈」と「失敗時の再試行」という2つの機能がセットになっており、キーボード入力のようなユーザとのインタラクティブ利用に限定されています。

    パイプ接続のような非インタラクティブ入力を考慮すると、提供機能を前者「外部入力の解釈」のみに限定し、解釈失敗時のエラーハンドリングは上位に任せる構造がベターでしょうね。(=scanfやstrtodに近い仕様)

    キャンセル

  • 2018/12/26 19:28

    えーつまりは、失敗したら失敗をreturnするって感じですかね?
    若葉なのでまだまだやんわりとしかわからないですね。

    キャンセル

  • 2018/12/27 15:55

    このあたりは「単一責任原則」という考え方があります。
    https://プログラマが知るべき97のこと.com/%E3%82%A8%E3%83%83%E3%82%BB%E3%82%A4/%E5%8D%98%E4%B8%80%E8%B2%AC%E4%BB%BB%E5%8E%9F%E5%89%87/

    キャンセル

+2

dnam = nsign*( intp+ decp*pow(0.1,j) );

0.1って2進数的に循環小数だよなーと思ってテスト
0.25(2進数で0.01)を正しく扱えるか:

int main(void){
    double x = 0.25;
    double y;
    sscanf("0.25","%lf", &y);
    double z = dnkscan(4); // 0.25と入力

    printf("%.20lf\n",x); //0.25000000000000000000
    printf("%.20lf\n",y); //0.25000000000000000000
    printf("%.20lf\n",z); //0.25000000000000005551
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/12/26 17:34

    見事に誤差が出ちゃってますね。循環小数とかなんとかは意識したことがなかったです。どのようにすれば誤差が減りますかね?

    キャンセル

  • 2018/12/26 18:13

    実際どうやるのか思いつかなかったので調べました
    http://krashan.ppa.pl/articles/stringtofloat/

    4節からです

    キャンセル

  • 2018/12/26 19:08

    ohメリケン語だ。
    わざわざ情報ありがとうございます.

    キャンセル

+2

関数名は double型のnamberをscanするkazumori製関数という意味で付けました。もっといい感じの名前も募集中。

拡張したstrtofという意味を込めてstrtof_exと名付けてはいかがでしょうか?
あるいはstrtof_k(azumori)など。

以下余談。

私なら標準入力からのユーザー入力処理と、数値変換処理は別関数に分けます。
そうすることで、変換関数はファイル入力からなどでも利用できるようになります。
また、標準入力からの入力での注意点については過去質問c言語のif文についてにて議論されていますのでコメント欄含め一読されることをお勧めします。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/12/26 17:36

    貴重な情報ありがとうございます。

    キャンセル

+2

綺麗さの面で。

  • 動作確認時のコードは消しましょう。
  • 記号前後の空白の有無を統一しましょう。
    → &&や||の前後は空けるとか、四則演算の記号前後は空けるとか
  • ひと目でわからない変数、処理は適切なコメントを入れましょう。
    → flagの値は何を表すのかなど
  • 綴りミスは誤読を招きます。
    → dnam → dnum(たぶんnumberの意味だと思う)
  • 個人的に変数宣言以外でのステートメントの横並びは避けた方が見やすい
    → intp=0;decp=0;nsign=1;...など

一言できれいと言っても個人差があるので、せめて見やすいコードにはすべきかと思います。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/12/26 16:52

    確認時のは皆さまの助言を聞いて完成してから消させていただきます
    空白の有無の統一は、流用ゆえに統一するのを忘れておりました。

    ありがとうございます。

    キャンセル

+1

scanf使ってる時点で失格
なぜfgetsではダメなの?

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/12/26 16:01

    んで、もう一つ言うと、切り詰められてOK出すというのはコードの不備によるものです
    なぜNG出すようにできないんでしょう

    キャンセル

  • 2018/12/26 16:10

    その方法をご協力願えると幸いです。

    キャンセル

  • 2018/12/26 16:33

    そこらへんの仕様がはっきりしないけど、
    単純にある程度以上の長さの文字列ならNG出すとかなんとか。

    #別回答にあるように改行があるかどうかでも

    キャンセル

+1

安全高速綺麗

綺麗については個人的趣向が多々あるので割愛しますが・・・
まず、xxxf関数(scanf,printfなど)は恐ろしく遅い関数です。(固定長で送られてくる数値をsscanf()で変換していたのをそれ専用の関数にしたら8〜10倍ほど早くなった経験があります)
また、fgets()でバッファに文字が残ったかどうかは、読み込んだ文字列に改行が含まれているかどうかで判断できるのでfgets()を推奨します。

追記:

scanfの挙動故にspaceで区切られちゃってますね。 

scanf()は言われたとおりにしか動きません。で、fgets()で1行読み込んで問題がないことを確認の上数値に変換する必要があります。あと、空白も読み込むといううことは数字の前後も確認しなくてはいけません。・・・安全第一ならば、入力された文字が該当なものかすべて確認が必要ですd^^

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/12/26 17:38

    遅いと聞いたことは記憶にありましたがそこまでとは...
    情報ありがとうございます。

    キャンセル

  • 2018/12/26 17:55 編集

    例えば、'+'や'-'が有った(ない場合もある)ら次にくるのは数字(この時点で小数点を許すかどうかは決めごと)が来なくてはいけない。数字が1個以上続いたら終端かピリオド(.)以外は来ない。ピリオドが来たら数字がなくてはならない。数字が来たら終端がなくてはならない・・・等、状態を管理する必要があります。
    あと、数字の判断はisdigit()で行えますが?・・・is〜〜〜()系の関数で文字の種別は判断できます。

    キャンセル

  • 2018/12/26 19:11

    「+.01」といった入力は、許容する予定です。

    なお、spaceは文字認識プロセスではじかれるはず…。
    scanf使用時ではそのプロセスに行く前にscanf内部で処理されてしまっていますからダメでしたが。

    キャンセル

+1

fgets使った方が良いです。

fgets(data, sizeof data, stdin);
if(data[strlen(data)-1]=='\n'){
    dataの中に収まった;
}else{
    dataの長さが足りなかった;
    残りが入力バッファに残っているので、改行まで読み飛ばし要;
}


あと、powとか使ってると、精度が悪くなりそうなので、使わない方が良いと思います。
小数点無視して値を作り上げ、あとで小数点以下の桁数分だけ10で割る。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/12/26 17:42

    わざわざサンプルありがとうございます。
    powの件もありがとうございます。

    読み飛ばしはいろいろ調べてみましたが、一番汎用性が高いのは何なんでしょうかね?気軽なfflushは環境依存?ですし。

    キャンセル

  • 2018/12/27 00:52

    while(getchar()!='\n') ;

    キャンセル

+1

  • flag=2の場合、未初期化のdnamが返されてしまう。そもそも直ぐにreturnすればよいのでは?
  • for文の中でflag==0の判定は無駄。
  • if文内の同一条件が複数あるのは無駄。以下のように書いた方がわかりやすいですね。
else if (data[i] >= '0' && data[i] <= '9') {
    if (isdecimal == 0) {
        ~
    } else {
        ~
    }
}

※コンパイラの最適化でうまくやってくれるかもしれませんけどねw

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/12/26 19:07

    flag==2だとwhileは抜けないので、未初期化のままで返るのはないはずですが、
    確かに、
    if (flag == 0) {
    dnam = nsign*( intp+ decp*pow(0.1,j) );
    //printf("成功:%lf\n", dnam);
    }
    は無駄だし、return直ならdnam自体が無駄ですね。改善します。

    キャンセル

  • 2018/12/26 19:24

    dnamに値を設定しているのは、flag==0のときだけですよね?

    キャンセル

  • 2018/12/26 19:29

    そうですね
    flag==0の時だけです。
    そこに何か問題がありますでしょうか?特にエラーの処理等々で。

    キャンセル

  • 2018/12/27 09:12

    学習用に意味のない関数を作成しているのなら良いですが、実際にこの関数を使用すると呼び出し元では正しい値が返ってきたのか、エラーで無効な値が返ってきているのか判断できませんよね?

    呼び出し元がエラーを検知できるようにすべきですね。

    キャンセル

0

関数の仕様でしょうか? 

IEEE754の仕様でしょう。

お示しのコードと
https://wandbox.org/permlink/CEbTN74y8nZdRNBj
C++で書いたコードで
https://wandbox.org/permlink/StShUU4W5cTrUwpF
同じ結果になります。

それはそうとstrtodを扱うときはerrnoの値もきちんとみて上げる必要があると思います。
C言語で安全に標準入力から数値を取得 - Qiita


わかった、
Visual C++ change history 2003 - 2015 | Microsoft Docs

Refactored binaries

The CRT Library has been refactored into a two different binaries, a Universal CRT (ucrtbase), which contains most of the standard functionality, and a VC Runtime Library (vcruntime), which contains the compiler-related functionality, such as exception handling, and intrinsics.
  Floating point formatting and parsing  
New floating point formatting and parsing algorithms have been introduced to improve correctness. This change affects the printf and scanf families of functions, as well as functions like strtod.
  The old formatting algorithms would generate only a limited number of digits, then would fill the remaining decimal places with zero. This is usually good enough to generate strings that will round-trip back to the original floating point value, but it's not great if you want the exact value (or the closest decimal representation thereof). The new formatting algorithms generate as many digits as are required to represent the value (or to fill the specified precision). As an example of the improvement; consider the results when printing a large power of two:

printf("%.0f\n", pow(2.0, 80))

Old output:

1208925819614629200000000

New output:

1208925819614629174706176

The old parsing algorithms would consider only up to 17 significant digits from the input string and would discard the rest of the digits. This is sufficient to generate a very close approximation of the value represented by the string, and the result is usually very close to the correctly rounded result. The new implementation considers all present digits and produces the correctly rounded result for all inputs (up to 768 digits in length). In addition, these functions now respect the rounding mode (controllable via fesetround). This is a potentially breaking behavior change because these functions might output different results. The new results are always more correct than the old results.

Visual Studio 2015の段階でC標準ライブラリの実装をmsvcrtからucrtbase+vcruntimeに切り替えたんだ、一方mingw gccのC実装はmsvcrtを参照し続けている。

で、msvcrtの実装では入力文字列の最大 17 桁の有効桁数のみが考慮され、残りの桁は破棄されていた、ということらしい。

ucrtbase+vcruntimeはWindows 10 SDKに含まれていて、これを使うようにコンパイルするとWindows10でしか動かないから、mingwはおそらくまだmsvcrtのほうに依存している、ということだろう。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/02/18 08:30

    mingwでもなんかucrtbaseのほう使える様になったみたいな話をどっかで見た気がするんだけど検索してもうまく見つからないorz

    キャンセル

  • 2019/02/18 08:31

    msys2 mingwのissueに投げて適当なワークアラウンドがないか聞いてみます。

    キャンセル

  • 2019/02/18 09:17

    https://github.com/Alexpux/MINGW-packages/issues/4987
    Issue投げてきました。スルーされそうですが。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.78%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る