2016年5月25日 星期三

給新手的C++教學 (上冊) - 11. 結構 (Structure)

回到「給新手的C++教學 (上冊)」

上一章

第九章,我們學到了函式
函式真的好好用耶!

咦?真的嗎?
現在,給你三個直線方程式
請你輸出這三條直線相交所形成的三角形的三個頂點座標 (也就是直線的三個交點的座標)

輸入格式:有三行,第$i$行包含第$i$條直線的資訊:三個數字$a_i$、$b_i$、$c_i$,代表第$i$條直線的方程式$a_i x+b_i y=c_i$ (保證三條直線可以形成一個三角形)
輸出格式:有三行,第$i$行包含第$i$個頂點 (交點) 的座標資訊:兩個數字$x_i$、$y_i$,代表第$i$個頂點座標$(x_i,y_i)$ (請輸出小數,頂點順序隨意)

提示:兩條直線$a_{1}x+b_{1}y=c_{1}$、$a_{2}x+b_{2}y=c_{2}$的交點求法:

$\begin{cases}
    &a_{1}x+b_{1}y=c_{1}\\
    &a_{2}x+b_{2}y=c_{2}
\end{cases}\cdots\cdots ①$

$①\Rightarrow
\begin{cases}
    &a_{1}a_{2}x+b_{1}a_{2}y=c_{1}a_{2}\cdots\cdots ②\\
    &a_{1}a_{2}x+a_{1}b_{2}y=a_{1}c_{2}\cdots\cdots ③
\end{cases}$

$②-③\Rightarrow (b_{1}a_{2}-a_{1}b_{2})y=c_{1}a_{2}-a_{1}c_{2}$

$\Rightarrow y=\frac{c_{1}a_{2}-a_{1}c_{2}}{b_{1}a_{2}-a_{1}b_{2}}$

$①\Rightarrow
\begin{cases}
    &a_{1}b_{2}x+b_{1}b_{2}y=c_{1}b_{2}\cdots\cdots ④\\
    &b_{1}a_{2}x+b_{1}b_{2}y=b_{1}c_{2}\cdots\cdots ⑤
\end{cases}$

$④-⑤\Rightarrow (a_{1}b_{2}-b_{1}a_{2})x=c_{1}b_{2}-b_{1}c_{2}$

$\Rightarrow x=\frac{c_{1}b_{2}-b_{1}c_{2}}{a_{1}b_{2}-b_{1}a_{2}}$

p.s. 以上推導過程看不懂也沒關係

$\therefore$兩線交點為$(\frac{c_{1}b_{2}-b_{1}c_{2}}{a_{1}b_{2}-b_{1}a_{2}},\frac{c_{1}a_{2}-a_{1}c_{2}}{b_{1}a_{2}-a_{1}b_{2}})$

啊不就三條直線兩兩求交點就好?
懶得寫三遍同樣的東西,就弄成一個函式吧~
引數 (甚麼是引數?) 就傳遞兩條線的資訊,然後函式就可以進行計算,最後回傳交點的座標......
等等,怎麼回傳「一個座標」?
float、int、char,都不對呀
我們需要一次回傳「兩個float」

沒關係,我們退而求其次
寫出兩個函式,一個函式會回傳$x$座標,另一個函式會回傳$y$座標
注意,這題的計算需要小數,所以要用「float」哦~
範例程式碼:

#include<cstdio>
float CalculateX(float a1,float b1,float c1,float a2,float b2,float c2)
{
    return (c1*b2-b1*c2)/(a1*b2-b1*a2);
}
float CalculateY(float a1,float b1,float c1,float a2,float b2,float c2)
{
    return (c1*a2-a1*c2)/(b1*a2-a1*b2);
}
int main()
{
    float a1,b1,c1,a2,b2,c2,a3,b3,c3;
    scanf("%f%f%f%f%f%f%f%f%f",&a1,&b1,&c1,&a2,&b2,&c2,&a3,&b3,&c3);
    printf("%f %f\n",CalculateX(a1,b1,c1,a2,b2,c2),CalculateY(a1,b1,c1,a2,b2,c2));
    printf("%f %f\n",CalculateX(a1,b1,c1,a3,b3,c3),CalculateY(a1,b1,c1,a3,b3,c3));
    printf("%f %f\n",CalculateX(a2,b2,c2,a3,b3,c3),CalculateY(a2,b2,c2,a3,b3,c3));
    return 0;
}

你寫出來了嗎?來測試看看吧~
輸入 (表示三條直線為$1x+0y=0$、$0x+1y=0$、$1x+1y=1$):
1 0 0
0 1 0
1 1 1

輸出應該要是:
0.000000 0.000000
0.000000 1.000000
1.000000 0.000000

如果數字差一點沒關係,因為這是float本身的誤差造成的

三條直線$1x+0y=0$、$0x+1y=0$、$1x+1y=1$會在第一象限形成一個等腰直角三角形
三角形會經過原點、$x$軸、$y$軸
其中範例程式碼會把交點$(0,0)$算成$(0.000000,-0.000000)$
這是float的誤差造成的,讓接近 (或等於) 0的數字正負號不準確
對於輸入 (表示三條直線為$0x+1y=-0.5$、$2x+1y=1.5$、$-1.5x+1y=1$):
0 1 -0.5
2 1 1.5
-1.5 1 1

輸出應該要是:
1.000000 -0.500000
-1.000000 -0.500000
0.142857 1.214286

三條直線$0x+1y=-0.5$、$2x+1y=1.5$、$-1.5x+1y=1$會在原點附近形成一個銳角三角形
三角形範圍遍及全部四大象限
注意有一個交點的座標為$(\frac{1}{7},\frac{17}{14})$,包含分數
其中範例程式碼會把交點$(\frac{1}{7},\frac{17}{14})$算成$(0.142857,1.214286)$
這是float的儲存和計算方式造成的,會用小數而不是分數的模式去計算

耶,過關!
但是,回想一下撰寫這份程式碼的過程
當你寫出「float CalculateX(float a1,float b1,float c1,float a2,float b2,float c2)」這行
或者「printf("%f %f\n",CalculateX(a1,b1,c1,a2,b2,c2),CalculateY(a1,b1,c1,a2,b2,c2));」這行的時候
會不會覺得有點煩呢?
引數也太多了吧!

注意到這些引數是可以分組的
也就是
前三個引數 (「float a1,float b1,float c1」以及「a1,b1,c1」) 屬於第1條直線的數據
後三個引數 (「float a2,float b2,float c2」以及「a2,b2,c2」) 屬於第2條直線的數據

那我們就將它們分組吧~
方法是
自己定義一個型別!(甚麼是型別?)
我們稱這種自定義的型別為「結構 (Structure)」
定義方式如下(此方法在C (而非C++) 失效,見註14) (假設你想要將結構取名為「StructureName」,這個結構會將三個float變數a、b、c分成一組):

struct StructureName
{
    float a,b,c;
};

「結構」定義完之後,就可以像宣告一個變數一樣,用這個「結構」宣告一個「物件 (Object)」來使用了!
宣告方式如下 (假設你想要將這個物件取名為「object_name」):

StructureName object_name;

這樣一來,假如你想要存取「『object_name』裡面的『a』變數」,可以直接寫出「object_name.a」
同理
「『object_name』裡面的『b』變數」就是「object_name.b」
「『object_name』裡面的『c』變數」就是「object_name.c」

所以,我們來修改一下這份程式碼吧~
我們來把類似意義的a、b、c都包進一個結構裡面,取名叫「Line」,三條線分別取名叫「L1」、「L2」、「L3」吧~
修改前 (放在這裡供您方便比較):

#include<cstdio>
float CalculateX(float a1,float b1,float c1,float a2,float b2,float c2)
{
    return (c1*b2-b1*c2)/(a1*b2-b1*a2);
}
float CalculateY(float a1,float b1,float c1,float a2,float b2,float c2)
{
    return (c1*a2-a1*c2)/(b1*a2-a1*b2);
}
int main()
{
    float a1,b1,c1,a2,b2,c2,a3,b3,c3;
    scanf("%f%f%f%f%f%f%f%f%f",&a1,&b1,&c1,&a2,&b2,&c2,&a3,&b3,&c3);
    printf("%f %f\n",CalculateX(a1,b1,c1,a2,b2,c2),CalculateY(a1,b1,c1,a2,b2,c2));
    printf("%f %f\n",CalculateX(a1,b1,c1,a3,b3,c3),CalculateY(a1,b1,c1,a3,b3,c3));
    printf("%f %f\n",CalculateX(a2,b2,c2,a3,b3,c3),CalculateY(a2,b2,c2,a3,b3,c3));
    return 0;
}

修改後:

#include<cstdio>
struct Line
{
    float a,b,c;
};
float CalculateX(Line L1,Line L2)
{
    return (L1.c*L2.b-L1.b*L2.c)/(L1.a*L2.b-L1.b*L2.a);
}
float CalculateY(Line L1,Line L2)
{
    return (L1.c*L2.a-L1.a*L2.c)/(L1.b*L2.a-L1.a*L2.b);
}
int main()
{
    Line L1,L2,L3;
    scanf("%f%f%f%f%f%f%f%f%f",&L1.a,&L1.b,&L1.c,&L2.a,&L2.b,&L2.c,&L3.a,&L3.b,&L3.c);
    printf("%f %f\n",CalculateX(L1,L2),CalculateY(L1,L2));
    printf("%f %f\n",CalculateX(L1,L3),CalculateY(L1,L3));
    printf("%f %f\n",CalculateX(L2,L3),CalculateY(L2,L3));
    return 0;
}

趕快跟著寫一遍程式碼,體驗「結構」的精隨吧!
寫出來後,測試看看,確定寫法是正確的!

對於輸入1,程式成功正確輸出三個座標!
對於輸入2,程式成功正確輸出三個座標!

如果想挑戰更進階的寫法
把「scanf("%f%f%f%f%f%f%f%f%f",&L1.a,&L1.b,&L1.c,&L2.a,&L2.b,&L2.c,&L3.a,&L3.b,&L3.c);」這一行精簡掉
可以套用「陣列」和「迴圈」的概念哦~

#include<cstdio>
struct Line
{
    float a,b,c;
};
float CalculateX(Line L1,Line L2)
{
    return (L1.c*L2.b-L1.b*L2.c)/(L1.a*L2.b-L1.b*L2.a);
}
float CalculateY(Line L1,Line L2)
{
    return (L1.c*L2.a-L1.a*L2.c)/(L1.b*L2.a-L1.a*L2.b);
}
int main()
{
    Line L[3];
    int i=0;
    while(i<3)
    {
        scanf("%f%f%f",&L[i].a,&L[i].b,&L[i].c);
        i=i+1;
    }
    printf("%f %f\n",CalculateX(L[0],L[1]),CalculateY(L[0],L[1]));
    printf("%f %f\n",CalculateX(L[0],L[2]),CalculateY(L[0],L[2]));
    printf("%f %f\n",CalculateX(L[1],L[2]),CalculateY(L[1],L[2]));
    return 0;
}

還記得本章最初提到的問題嗎?
我們需要一次回傳「兩個float」,也就是回傳一個「座標」
這也可以用結構來解決!
聰明的你如果想到了做法,趕快自己先寫出來再對答案吧!
如果還沒想到,沒關係,以下提供解答讓你可以更了解結構的性質!

#include<cstdio>
struct Coordinate
{
    float x,y;
};
Coordinate Calculate(float a1,float b1,float c1,float a2,float b2,float c2)
{
    Coordinate answer;
    answer.x=(c1*b2-b1*c2)/(a1*b2-b1*a2);
    answer.y=(c1*a2-a1*c2)/(b1*a2-a1*b2);
    return answer;
}
int main()
{
    float a1,b1,c1,a2,b2,c2,a3,b3,c3;
    scanf("%f%f%f%f%f%f%f%f%f",&a1,&b1,&c1,&a2,&b2,&c2,&a3,&b3,&c3);
    Coordinate answer;
    answer=Calculate(a1,b1,c1,a2,b2,c2);
    printf("%f %f\n",answer.x,answer.y);
    answer=Calculate(a1,b1,c1,a3,b3,c3);
    printf("%f %f\n",answer.x,answer.y);
    answer=Calculate(a2,b2,c2,a3,b3,c3);
    printf("%f %f\n",answer.x,answer.y);
    return 0;
}

其實啊,我們只是讓我們的「Calculate函式」直接回傳一個型別為「Coordinate」(「Coordinate」是我們自己定義的結構) 的變數而已

是不是很有趣呢?
其實啊
結構遠遠不止這種玩法,之後還會介紹更多有趣的功能!

下一章

感謝:
(版權所有 All copyright reserved)

4 則留言:

  1. 超有趣的~太感謝啦~

    回覆刪除
    回覆
    1. 哈哈哈小莫也覺得超有趣的,開心!\(^o^)/

      刪除
  2. 請問最後直接回傳一個座標那邊
    Coordinate Calculate(float a1,float b1,float c1,float a2,float b2,float c2)
    這邊我可以再多用一個結構嗎
    struct Line{
    float a,b,c;
    };
    Line L1,L2,L3;
    C Line(L1.a,L1.b,L1.c,L2.a,L2.b,L2.c,L3.a,L3.b,L3.c) 像這樣
    我是想說這樣的話下面這邊
    float a1,b1,c1,a2,b2,c2,a3,b3,c3;
    scanf("%f%f%f%f%f%f%f%f%f",&a1,&b1,&c1,&a2,&b2,&c2,&a3,&b3,&c3);
    就可以用陣列簡化

    如果可以的話可以問要怎麼寫嗎! 謝謝!

    回覆刪除
    回覆
    1. 忘記說 我把你用的Coordinate換成C

      刪除

歡迎留言或問問題~
若您的留言中包含程式碼,請參考這篇
如果留言不見了請別慌,那是因為被google誤判成垃圾留言,小莫會盡快將其手動還原

注意:只有此網誌的成員可以留言。