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

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

新規登録して質問してみよう
ただいま回答率
85.50%
C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

Q&A

9回答

2153閲覧

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

Kazumori102

総合スコア45

C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

0グッド

0クリップ

投稿2018/12/26 06:32

編集2022/01/12 10:55

前提・実現したいこと

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

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

ソースコード

C

1//http://stroll.hatenablog.com/entry/2015/07/20/221125 を基に開発 2#ifndef imputtestfunc//二重でincludeされることを防ぐ 3#define imputtestfunc 4 5#include <stdio.h> 6#include <string.h> 7#include <math.h> 8 9double dnkscan(int lim) { 10 char data[256]; 11 int i,j, intp = 0,decp=0, nsign = 1, flag = 0, isdecimal=0; 12 double dnam; 13 //printf("[db]入力文字列が「半角数値及び正負記号,小数点」の時はその値を返します。それ以外ははエラーとなります。\n"); 14 //printf("[db]%d桁以内の数値を入力してください。>>\n",lim); 15 do{ 16 intp=0;decp=0;nsign=1;flag=0;isdecimal=0; 17 18 //fgets(data,lim,stdin);//new これだと入力許容桁数で自動的に切り詰められてOK判定が出てしまう。 19 scanf("%s", data) ;//old これだと、オーバーフローの可能性が捨てきれない。 20 if (strlen(data) > lim) { 21 flag = 2; 22 } 23 i = 0;j = 0; 24 if (flag == 0 && data[0] == '+') { 25 i++; 26 } 27 if (flag == 0 && data[0] == '-') { 28 nsign = -1; 29 i++; 30 } 31 32 //printf("[db]flag is %d data[i] is %c isdecimal is %d j is %d\n",flag,data[i],isdecimal,j); 33 for (; i < strlen(data); i++) { 34 //printf("[db]flag is %d data[i] is %c isdecimal is %d j is %d\n",flag,data[i],isdecimal,j); 35 36 if (flag == 0 && data[i] == '.'&&isdecimal==0) { 37 isdecimal = 1; 38 //i++;//いるか? 39 } 40 41 else if (flag == 0 && (data[i] >= '0' && data[i] <= '9')&&isdecimal==0) { 42 intp = intp * 10 + data[i] - '0'; 43 //printf("[db]整数部シーケンス intp=%d\n",intp); 44 } 45 else if(flag == 0 && (data[i] >= '0' && data[i] <= '9')&&isdecimal==1) { 46 decp = decp * 10 + data[i] - '0'; 47 //printf("[db]小数部シーケンス decp=%d\n",decp); 48 j++; 49 } 50 else{ 51 if(flag == 2) { 52 printf("桁あふれエラー>>\n"); 53 break; 54 }else{ 55 printf("エラー>>\n"); 56 flag = 1; 57 break; 58 } 59 } 60 } 61 //printf("[db]flag is %d data[i] is %c isdecimal is %d j is %d\n",flag,data[i],isdecimal,j); 62 if (flag == 0) { 63 dnam = nsign*( intp+ decp*pow(0.1,j) ); 64 //printf("成功:%lf\n", dnam); 65 } 66 }while (flag!=0); 67 ///memset(data, '\0', sizeof(data) ); 68 //printf("[db]date=[%s]",data);//ちゃんとリセットかかっているかの点検用 69 return dnam; 70} 71#endif

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

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

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

c

1#include <stdio.h> 2#include <stdlib.h> 3 4/* macros */ 5#define N 256 6 7/* main */ 8int main(void) { 9 char s[N] = {'\0'}, *endptr; 10 double x; 11 12 /* 入力 */ 13 fgets(s, N, stdin); 14 printf("変換前: %s", s); 15 16 /* 変換 */ 17 x = strtod(s, &endptr); 18 printf("変換後: %.2f\n", x); 19 printf("endptr: %s\n", endptr); 20 21 return EXIT_SUCCESS; 22}
変換前: 123456789012345678901234567890 変換後: 123456789012345680000000000000.00 求結果: 123456789012345678901234567890.00

気になる質問をクリップする

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

cateye

2018/12/26 07:48

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

2018/12/26 08:21 編集

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

回答9

0

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

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 07:23

編集2018/12/26 07:25
yohhoy

総合スコア6189

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kazumori102

2018/12/26 08:32 編集

>使い物にならない そのとおりですね。 >制限~ 文字数より桁数を制限したいですね。2.4なら二桁。 >入力の想定 パイプもつなげられれば便利とは思っていますが、今のところはキーボードのみの予定です。もしよろしければパイプの際はどういうふうにした方がいいかのアドバイスをいただければ幸いです。 >Cの仕様で保障されてましたか。 文字コードについて調べた際に連続していないものもあるといった感じの記述を見た覚えがあったので。
yohhoy

2018/12/26 10:09

> パイプの際はどういうふうにした方がいいか 現在の外部仕様は「外部入力の解釈」と「失敗時の再試行」という2つの機能がセットになっており、キーボード入力のようなユーザとのインタラクティブ利用に限定されています。 パイプ接続のような非インタラクティブ入力を考慮すると、提供機能を前者「外部入力の解釈」のみに限定し、解釈失敗時のエラーハンドリングは上位に任せる構造がベターでしょうね。(=scanfやstrtodに近い仕様)
Kazumori102

2018/12/26 10:28

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

2018/12/27 06: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/
guest

0

綺麗さの面で。

  • 動作確認時のコードは消しましょう。
  • 記号前後の空白の有無を統一しましょう。

→ &&や||の前後は空けるとか、四則演算の記号前後は空けるとか

  • ひと目でわからない変数、処理は適切なコメントを入れましょう。

→ flagの値は何を表すのかなど

  • 綴りミスは誤読を招きます。

dnamdnum(たぶんnumberの意味だと思う)

  • 個人的に変数宣言以外でのステートメントの横並びは避けた方が見やすい

intp=0;decp=0;nsign=1;...など

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

投稿2018/12/26 07:36

編集2018/12/26 07:38
dice142

総合スコア5158

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kazumori102

2018/12/26 07:52

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

0

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

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

以下余談。

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

投稿2018/12/26 07:29

can110

総合スコア38233

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kazumori102

2018/12/26 08:36

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

0

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

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

C

1int main(void){ 2 double x = 0.25; 3 double y; 4 sscanf("0.25","%lf", &y); 5 double z = dnkscan(4); // 0.25と入力 6 7 printf("%.20lf\n",x); //0.25000000000000000000 8 printf("%.20lf\n",y); //0.25000000000000000000 9 printf("%.20lf\n",z); //0.25000000000000005551 10}

投稿2018/12/26 07:20

編集2018/12/26 07:21
ozwk

総合スコア13512

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kazumori102

2018/12/26 08:34

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

2018/12/26 10:08

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

0

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

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

投稿2018/12/26 09:11

編集2018/12/26 09:12
kasa0

総合スコア578

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kazumori102

2018/12/26 10:07

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

2018/12/26 10:24

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

2018/12/26 10:29

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

2018/12/27 00:12

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

0

fgets使った方が良いです。

C

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

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

投稿2018/12/26 07:17

編集2018/12/26 07:36
otn

総合スコア84423

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kazumori102

2018/12/26 08:42

わざわざサンプルありがとうございます。 powの件もありがとうございます。 読み飛ばしはいろいろ調べてみましたが、一番汎用性が高いのは何なんでしょうかね?気軽なfflushは環境依存?ですし。
otn

2018/12/26 15:52

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

0

安全高速綺麗

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

追記:

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

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

投稿2018/12/26 07:06

編集2018/12/26 08:47
cateye

総合スコア6851

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kazumori102

2018/12/26 08:38

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

2018/12/26 09:19 編集

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

2018/12/26 10:11

「+.01」といった入力は、許容する予定です。 なお、spaceは文字認識プロセスではじかれるはず…。 scanf使用時ではそのプロセスに行く前にscanf内部で処理されてしまっていますからダメでしたが。
guest

0

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

投稿2018/12/26 06:52

y_waiwai

総合スコア87719

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kazumori102

2018/12/26 06:55

投稿内容をもう一度しっかり見てからから発言願います。
y_waiwai

2018/12/26 07:00 編集

提示されたコードでは、スタックぶっ潰して暴走してしまう可能性があります。 切り詰められてOK出される方がよっぽどマシと言うもんですね
y_waiwai

2018/12/26 07:01

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

2018/12/26 07:10

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

2018/12/26 07:33

そこらへんの仕様がはっきりしないけど、 単純にある程度以上の長さの文字列ならNG出すとかなんとか。 #別回答にあるように改行があるかどうかでも
guest

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:

c

1printf("%.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/17 12:16

編集2019/02/17 15:02
yumetodo

総合スコア5850

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kazumori102

2019/02/17 12:21

仕様> 明らかに下の方が精度が死んでますね。 errno> ですね。サンプルには書いてないのですが、自分で試すやつにはつけてました、が、精度が死ぬ分まで入力しても特にエラーが返ってこないので???って感じです。
yumetodo

2019/02/17 12:24

あれ、同じコードなのにWandboxで走らせた結果(123456789012345677877719597056.00)とお示しの結果(123456789012345680000000000000.00)で合わないですね・・・
yumetodo

2019/02/17 12:29

手元のmingwで再現しますね msys2 mingw64 gcc 8.2.1
yumetodo

2019/02/17 14:38

MSVC(_MSC_FULL_VER=191627026)だと再現しないのでmingw側だろうか・・・
yumetodo

2019/02/17 14:41

mingw gccでビルドしたやつが $ldd a.exe ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7ffe0b2f0000) KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7ffe0a1b0000) KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7ffe07440000) msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x7ffe0a760000) MSVCでビルドしたやつが $ldd teratail_165987.exe ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7ffe0b2f0000) KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7ffe0a1b0000) KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7ffe07440000) apphelp.dll => /c/WINDOWS/SYSTEM32/apphelp.dll (0x7ffe052a0000) ucrtbase.dll => /c/WINDOWS/System32/ucrtbase.dll (0x7ffe07c00000) VCRUNTIME140.dll => /c/WINDOWS/SYSTEM32/VCRUNTIME140.dll (0x7ffdfa4f0000) msvcrt.dllの問題・・・?
Kazumori102

2019/02/17 22:47

これはこれは、ずいぶんと細かく原因究明していただきありがとうございます。
yumetodo

2019/02/17 23:30

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

2019/02/17 23:31

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

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

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問