C語言實現多態
如果你已經很久沒有寫C代碼了,為了確保你的編譯器還能工作,先運行下面的一個基礎程序:
#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}
ok,我們現在開始加快步伐
我們將要創建三個繼承於抽象Shape類的類,但是C語言並沒有類或繼承的概念,但是有struct,下面就開始吧。
struct Square{
int width;
int height;
};
struct Circle{
float radius;
};
struct Triangle{
int base;
int height;
};
沒啥特別的,讓我們更進一步的研究,將下面的代碼放入main函數
printf("size of square is %d\n", sizeof(struct Square));
printf("size of Circle is %d\n", sizeof(struct Circle));
printf("size of Triangle is %d\n", sizeof(struct Triangle));
輸出結果如下(不能擔保,取決於平台):
size of cube is 8
size of circle is 4
size of triangle is 8
涉及到內存對齊問題可以參考《C語言內存對齊詳解》
如果用struct來模擬類,那類中的方法呢,可以采用函數指針,C語言對待函數指針就像其他成員一樣,
考慮下面的函數:
void print_square( void ){
printf("Hello Square\n");
}
這個函數接受一個空的參數,返回也為空,意味著指向這個函數的指針是一個指向接受空參數返回空類型的函數,這裡你可以聲明這樣的一個變量:
struct Square{
int width;
int height;
//now for functions
void (* print)( void );
float (* area)( struct Square * this );
};
關於C語言變量聲明的規則,可以參考名著《C陷阱與缺陷》
print是一個指向參數為空、返回值為空的函數的指針,現在我們需要給Square一個面積函數,接受自身為參數,並且返回一個float類型來表示面積。讀取的時候和print函數類似。
指針是很強大的,但是必須指向有用的東西,但是你怎麼知道一個函數在內存中的地址,好消息來了,函數名就表示它在內存中的地址,下面舉一個例子:
struct Square square;
square.print = print_square;
函數指針的行為就像其他的成員一樣,當我們創建一個Square類型的square,它的值包含垃圾值,我們需要手動地給它們賦一些正確的值。這樣的工作需要一個構造器,C語言也沒有,所以我們需要創建一個自己的構造函數。
void init_square( struct Square * square, int w, int h ){
(*square).print = print_square;
(*square).width = w;
(*square).height = h;
(*square).area = calc_square_area;
}
這個構造器僅僅是另一個函數,需要傳遞改變square的值的參數,於是,參數必須是一個指向Square的指針以及需要傳遞的值
下面來測試一下我們所做的工作(以square為例):
#include<stdio.h>
struct Shape{
void (* print)( void );
float (* area)( struct Shape * this );
};
struct Square{
float width;
float height;
void (* print)( void );
float (* area)( struct Square * this );
};
void print_square( void ){
printf("Hello Square\n");
}
float calc_square_area(struct Square * this){
return (*this).width * (*this).height;
}
void init_square(struct Square * square, float w, float h ){
(*square).width = w;
(*square).height = h;
(*square).print = print_square;
(*square).area = calc_square_area;
}
int main(void)
{
struct Square square;
init_square( &square, 2.5, 2.6 );
square.print();
printf("the area of the square is %.2f\n", square.area(&square));
return 0;
}
我們必須創建一個Shape結構作為其他三種形狀的父類,需要創建Shape結構的邏輯結構:
//抽象父類
struct Shape{
void (* print)( void );
float (* area)( struct Shape * this );
};
既然知道了從哪開始,接著就思考我們希望什麼發生,假如你希望創建一個Square多次,初始化它
struct Square square;
init_square(&square, 2, 4);
如果希望打印它,調用函數:
square.print();
如果有一個指向Shape的指針試圖打印square:
struct Shape * pointer = (struct Shape *)□
(*pointer).print(); //?? 將會發生什麼??
將得到一個中斷錯誤,結果不是期望的那樣。我們期待的是指向Shape的指針對square進行調用
現在來看看內存相關,Shape和Square對比
Square:
width hight print area
16B
4 4 4 4
Shape:
print area
8B
4 4
print()函數在Shape的內存模型中是前4個字節,在Square的內存模型是第三個4字節的位置
square.print();
上面的代碼使用第三個字節的指針來調用print函數,下面的代碼期望完成一樣的功能
(*pointer).print();
但是在最前4個字節沒有指針,而是int類型的值,2個字節,但是我們的指針不會意識到是2個字節,於是找到內存的位置為2的位置(可能是一些BIOS驅動\操作系統內存地址),從而導致中斷錯誤的產生
既然我們知道了原因,那麼可以找到一個解決方案(不是最好),只有當Shape結構中的print函數的指針在第三個4字節的位置,那麼可以填充Shape的前8個字節,這種方法就是C語言中的“填充”技術,代碼如下:
struct Shape{
char padd[8];
void (* print)( void );
float (* area)( struct Shape * this );
};
修改後測試代碼如下:
#include<stdio.h>
//abstract father class
/*
struct Shape{
void (* print)( void );
float (* area)( struct Shape * this );
};
*/
struct Shape{
char padd[8];
void (* print)( void );
float (* area)( struct Shape * this );
};
struct Square{
float width;
float height;
//now for functions
void (* print)( void );
float (* area)( struct Square * this );
};
void print_square( void ){
printf("Hello Square\n");
}
float calc_square_area(struct Square * this){
return (*this).width * (*this).height;
}
void init_square(struct Square * square, float w, float h ){
(*square).width = w;
(*square).height = h;
(*square).print = print_square;
(*square).area = calc_square_area;
}
int main(void)
{
struct Square square;
struct Shape *p = (struct Shape*)□
init_square( &square, 2, 2 );
//square.print();
(*p).print();
printf("the area of the square is %.2f\n", square.area(&square));
return 0;
}