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

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

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

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

アルゴリズム

アルゴリズムとは、定められた目的を達成するために、プログラムの理論的な動作を定義するものです。

Q&A

4回答

3993閲覧

電卓プログラム(atoi、scanf、str系禁止)

pi_________man

総合スコア6

C

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

アルゴリズム

アルゴリズムとは、定められた目的を達成するために、プログラムの理論的な動作を定義するものです。

0グッド

0クリップ

投稿2018/06/21 13:07

atoi、scanf、str系の関数を使わずに入力した数式を計算するプログラムを作っています。
1.数式の入力を求める
2.数式を入力し、enterを押したら(+,-,*/が使用可能)
3.計算結果を表示する
4.1に戻る
5‘q’が入力されたら終了
といったものです。

問題には以下の条件があります。
1.入力した数値の計算はmein関数以外で実施
2.scanf、atoi、str系禁止
3.0除算はの場合エラー処理を行う
4.一行に入力できる演算子は1のみ
5.小数点、()には対応せず数値演算子以外の文字を含んだ場合エラー処理を行う
6.ただし、数値と演算子の間と数式の前後のスペースは許容する(1 23*4はNG)
以上です

ようやく思うとおりに挙動するようになったのですが、もう少し綺麗に書けないかと考えております。

該当のソースコード

C

1#include <stdio.h> 2#include <stdlib.h> 3 4//配列の長さを一度に設定できるように宣言する 5#define STRLEG 16 6 7 8 9//四則演算を行う関数 10int calc(int num1, int num2, char *op){ 11 //演算結果を格納する変数ans 12 int ans; 13 14 //取得した演算記号べつに計算を行う 15 switch(*op){ 16 case '+': 17 ans = num1 + num2; 18 break; 19 case '-': 20 ans = num1 - num2; 21 break; 22 case '*': 23 ans = num1 * num2; 24 break; 25 case '/': 26 ans = num1 / num2; 27 default: 28 break; 29 } 30 //演算結果を返す 31 return ans; 32 33} 34 35 36int main(){ 37 //入力された数式を入れるための変数str 38 char *p, str[STRLEG]; 39 //演算子を格納する変数op 40 char op; 41 //数式の中に現れた数値をカウントする変数countnum 42 int countnum = 0; 43 //数式の中に現れた演算子の数をカウントする変数 44 int kigo = 0; 45 //数式の中で最初に現れる数値を格納する 46 int num1 = 0; 47 //数式の中で2番目に現れる数値を格納する 48 int num2 = 0; 49 //演算結果を入れる 50 int ans; 51 52 //qが入力されるまでループさせる 53 do { 54 //メッセージを表示して数式を入力させる 55 printf("数式を入力してください\n"); 56 57 //入力した数式をstrに入れる 58 fgets(str,sizeof str,stdin); 59 60 //qが入力されたらループを抜ける 61 if(str[0]=='q') break; 62 63 //一文字ずつ読み取り数値と演算子に分ける 64 for (p = str; *p != '\n'; p++ ) { 65 //空白でない場合に処理を行う。 66 if(*p != ' '){ 67 //数値である場合の処理 68 if(*p >= '0' && *p <= '9'){ 69 //カウントする 70 countnum++; 71 /*これから読み取る数値の前に演算子が来ていなければ 72 一つ目の数値として認める*/ 73 if (countnum == 1 && kigo == 0) { 74 //数字以外の文字がくるまで一度に代入する 75 while(*p >= '0' && *p <= '9'){ 76 num1=num1*10+*p-'0'; 77 p++; 78 } 79 p--; 80 //二つめの数値の前に演算子が一つ来ていることを確かめる 81 } else if (countnum == 2 && kigo == 1) { 82 while(*p >= '0' && *p <= '9'){ 83 num2=num2*10+*p-'0'; 84 p++; 85 } 86 p--; 87 //その他はエラー 88 } else { 89 printf("数値の間にスペースを入れないでください\n"); 90 exit(1); 91 } 92 //演算子を読み取ったときの処理 93 }else if(*p == '+' || *p == '-' || *p == '*' || *p == '/'){ 94 kigo++; 95 if(kigo==2){ 96 printf("演算子は二つ以上入れないでください\n"); 97 exit(1); 98 } 99 op = *p; 100 //空白以外で数値・演算子以外の文字は無効 101 }else{ 102 printf("不正な文字が含まれています\n"); 103 exit(1); 104 } 105 } 106 } 107 108 //0除算を行えないようにする 109 if(op=='/'&&num2==0){ 110 printf("0除算はできません。\n"); 111 exit(1); 112 } 113 114 //計算を行う関数calcに二つの数値と演算子を送る 115 ans = calc(num1,num2,&op); 116 //計算結果の出力 117 printf("%d %c %d = %d\n",num1,op,num2,ans); 118 119 //入力された文字列と数値、カウンタを初期化 120 str[STRLEG]; 121 num1 = num2 = kigo = countnum = 0; 122 123 }while(1); 124 125 return 0; 126}

特に文字列を変換する作業に関して、同じ処理を別に書いていることと
数値に変換した後のデクリメントを直したいと考えております。

同じ処理のまとめ方と帳尻合わせのデクリメントを対処する方法
その他何か、お気づきの点があればご教授いただけると幸いです。

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

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

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

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

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

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

guest

回答4

0

こんにちは。

同じ処理のまとめ方

num1、num2ではなく、int num[2];を使って1つめと2つめの数字を記録すればまとめることができる筈です。

帳尻合わせのデクリメントを対処する方法

for文のp++を削除しop=*p++;まではスマートにできると思います。
問題はスペースの時のp++をどう忍び込ませるかですね。結局、デクリメントが良いのか、あちこちにあるインクリメントが良いのかの差となります。私は悩んだ結果デクリメントを取ることが多いです。ungetc()がわざわざ用意されているように、意外に必要なのですよ。
人は我儘ですから、マンマシン・インタフェースを綺麗に書けない数多くの例の1つと思います。
単語の区切りは必ずスペースで区切ることという制約をつければ綺麗にかけますが、それを許さないのが人なのです。

その他何か、お気づきの点があればご教授いただけると幸いです。

インデントが深いことが気になります。
if(*p != ' '){ ... }ではなく、if(*p == ' ') continue;にすれば1段インデントを節約できます。
if文による場合分けが複雑な印象を受けます。数値、スペース、行末、その他の4つに分けるとスッキリできるかも知れません。数値、行末、その他の頭で最初のエラー・チェックです。その他が不正文字かどうかはcalc関数のswitchのdefault:でできると良いですね。

投稿2018/06/21 18:36

Chironian

総合スコア23272

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

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

pepperleaf

2019/06/24 15:11

> スペースの時のp++をどう忍び込ませる この程度だったら、 switch()文にしてしまうとか。 (case ' ': p++;) あと、数字の判定は、isdigit()を使いたい。
guest

0

質問が 1年前なので、もう見てもらえないかもしれませんが、
こんなのを書いてみました。

C

1#include <stdio.h> // printf, fgets, puts 2#include <ctype.h> // isspace, isdigit 3 4const char *p; 5unsigned char c; 6 7int get(void) { while (isspace(c = *p++)) ; return c; } 8 9int num(const char *p, const char **q) 10{ 11 int n = *p++ - '0'; 12 while (isdigit((unsigned char)*p)) n = n * 10 + *p++ - '0'; 13 *q = p; 14 return n; 15} 16 17int calc(const char *s, int *v) 18{ 19 p = s; 20 if (get() - '0' >= 10u) return 4; 21 int n1 = num(p-1, &p); 22 int op = get(); 23 if (op == 0) return 3; 24 if (get() - '0' >= 10u) return 4; 25 int n2 = num(p-1, &p); 26 switch (op) { 27 case '+': *v = n1 + n2; break; 28 case '-': *v = n1 - n2; break; 29 case '*': *v = n1 * n2; break; 30 case '/': 31 if (n2 == 0) return 1; 32 *v = n1 * n2; 33 break; 34 default: return 3; 35 } 36 if (get() == '+' || c == '-' || c == '*' || c == '/') return 2; 37 if (c != 0) return 4; 38 return 0; 39} 40 41int main(void) 42{ 43 int v; char s[1024]; 44 while (printf(">> "), fgets(s, sizeof s, stdin) && *s != 'q') 45 switch (calc(s, &v)) { 46 case 0: printf(" %d\n", v); break; 47 case 1: puts(" division by zero"); break; 48 case 2: puts(" too many operators"); break; 49 case 3: puts(" invalid operator"); break; 50 default: puts(" invalid character"); 51 } 52}

投稿2019/06/24 12:55

kazuma-s

総合スコア8224

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

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

0

同じ処理のまとめ方

基本的には簡単で、関数化する、もしくはマクロを上手く使うことです。

帳尻合わせのデクリメントを対処する方法

p--; が必要になる理由は、 for (p = str; *p != '\n'; p++) というループの中で数字やら演算子やらを切り出しているからです。ですから Chironian さんがおっしゃるように、for (p = str; *p != '\n'; ) とする手もあるでしょうが、、、

もうひとつ、Chironianさんご指摘の、インデントが深いことも考慮する必要があります。インデントが深いほど理解しにくく、メンテナンスしにくくなるのだから。
インデントが深くなる原因は、関数化していないことに加えて、 for (p = str; ...) という大きなループの中で処理していることです。このforループの中で

  • ひとつめの数字
  • 演算子
  • ふたつめの数字

と、今見ているものをif文で区別しながら処理する必要があるので、それだけでもインデントは1〜2段深くなるのです。私には諸悪の根源に思えてきました(笑)。

要は、入力した文字列から2つの数字と演算子を切り出すことです。そこで、こんなふうに組み直したらどうか(まずは、空白は無い、エラーチェックも要らないと仮定し、正常な処理の本筋だけを考えてみる)。

C

1 p = str; // 入力した行の先頭から見ていく 2 while (*p >= '0' && *p <= '9') { // 数字ひとつめを変換する 3 num1 = (num1 * 10) + (*p - '0'); 4 p++; 5 } 6 // ここで p は数字の次の文字(=演算子)を指す 7 op = *p++; // 演算子 8 while (*p >= '0' && *p <= '9') { // 数字ふたつめ 9 num2 = (num2 * 10) + (*p - '0'); 10 p++; 11 }

--p; は不要になり、インデントは浅くなり、数字を切り出す機能の関数化も見えてきたので、私が以前参考書で覚えたw空白を読み飛ばす関数skipspaces()を使い、isdigit() も使わず即席のマクロにして、次の様にしてみました

C

1#define ISDIGIT(X) (((X) >= '0' && (X) <= '9') ? 1 : 0) 2 3char *skipspaces(char *ptr) // 空白を読み飛ばす 4{ 5 while (*ptr == ' ') 6 ++ptr; 7 8 return ptr; // 次の文字位置を返す 9} 10 11char *conv2num(char *ptr, int *num) // 数字に変換する 12{ 13 int result = 0; 14 15 ptr = skipspaces(ptr); // 空白を読み飛ばす 16 if (ISDIGIT(*ptr) == 0) // そこに数字が無ければエラー 17 return NULL; // エラー終了 18 19 while (ISDIGIT(*ptr)) { 20 result = (result * 10) + (*ptr - '0'); 21 ++ptr; 22 } 23 *num = result; // 変換値を呼出し元に返す 24 return ptr; // 次の文字位置を返す 25} 26 27main() 28{ 29 // 変数定義などは省略 30 while (1) { 31 printf("数式を入力してください\n"); 32 fgets(str, sizeof str, stdin); // 入力した数式をstrに 33 if (str[0] == 'q') // qでループを抜ける 34 break; 35 36 p = str; // 入力行の先頭から 37 p = conv2num(p, &num1); // 数字ひとつめ 38 op = *p++; // 演算子 39 p = conv2num(p, &num2); // 数字ふたつめ 40 41 if (op == '/' && num2 == 0) { // 0除算しない 42 // 0除算エラー 43 } 44 // 計算を行う関数calcに二つの数値と演算子を送る 45 ans = calc(num1, num2, op); 46

演算子直前の空白はどうすべきか、エラーチェックなどまだまだ不完全・・・ですが、流れがシンプルで見通しがよくなったと思います。この線で私なりの完成形を作ることができました。

その他何か、お気づきの点があれば

  • do 〜 while (1); で無限ループするのは、よくありません。ループの先頭で無限ループだとわかるように、while (1) か for (;;) を使いましょう。

  • calc() の3番目の仮引数を char *op としていますが、ポインタ渡しにする必要はありません。

  • ここ↓に2つ問題を指摘したい。

C

1 //入力された文字列と数値、カウンタを初期化 2 str[STRLEG]; 3 num1 = num2 = kigo = countnum = 0;
  1. str[STRLEG]; はおかしい。バッファクリアなど行われず、少なくとも再初期化ではありません。それどころか、バッファオーバーランの危険性さえ考えられるのでは?

  2. 繰り返すことを見越してループの最後で num1 〜 countnum に0を代入していますが、初期値の代入はループに入った先頭で行うべきです。

さらに、main() の先頭は int num1; と変数定義だけにして、初期化は不要です。不要なことは書かない。一箇所でできることは一箇所で行ったほうがよい。

  • STRLEG は string length を短縮したのだと思いますが、LEG だと長さではなく「脚」になって良くない(脚にも長さあるがw)。重箱の隅のようだけど、格好悪いし、名前は案外大事だということで。6文字ならSTRLENかな。

  • #define STRLEG 16 ですが、int 型が 32bit なら10進数で10桁まで扱えるので、16文字では少ないと言えます。fgets()で入力するなら \n と \0 の2文字分を含めて、最低でも 10 + 1 + 10 + 2 = 23 文字、さらに空白文字の分もあったほうが良いでしょうね。

投稿2018/06/22 05:58

編集2018/06/22 09:03
rubato6809

総合スコア1380

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

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

0

fgets、strtol、charの比較

投稿2018/06/21 13:09

y_waiwai

総合スコア87719

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

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

ikadzuchi

2018/06/22 00:31

脊髄反射で無意味な内容を書き込むとユーザーランキング月間2位を取れるんですねw。
yumetodo

2018/06/22 00:52

回答として機能していないのでは
y_waiwai

2018/06/22 00:59

ご心配なく。 うかうかしてたら月間1位になってたりしますんでw
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

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

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

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

ただいまの回答率
85.50%

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

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

質問する

関連した質問