<center id="qkqgy"><optgroup id="qkqgy"></optgroup></center>
  • <menu id="qkqgy"></menu>
    <nav id="qkqgy"></nav>
    <xmp id="qkqgy"><nav id="qkqgy"></nav>
  • <xmp id="qkqgy"><menu id="qkqgy"></menu>
    <menu id="qkqgy"><menu id="qkqgy"></menu></menu>
    <tt id="qkqgy"><tt id="qkqgy"></tt></tt>

  • 我們在編寫程序時,通常采用以下步驟:

    * 將問題的解法分解成若干步驟
    * 使用函數分別實現這些步驟
    * 依次調用這些函數
    這種編程風格的被稱作面向過程。除了面向過程之外,還有一種被稱作面向對象的編程風格被廣泛使用。面向對象
    采用基于對象的概念建立模型,對現實世界進行模擬,從而完成對問題的解決。

    C語言的語法并不直接支持面向對象風格的編程。但是,我們可以通過額外的代碼,讓C語言實現一些面向對象特性。在這一節當中,我們將探究什么是面向對象,以及怎樣用C語言來實現它。
    單純理論上的討論可能比較難以理解,為了能夠讓我們的討論能夠落地到實際中,我們選取學校為場景,展開對面向對象風格編程的討論。
    一般而言面向對象風格的編程具有以下3大特性:

    * 封裝
    * 繼承
    * 多態
    我們將以這3個特性為線索,討論C語言如何面向對象編程。

    <>封裝

    我們來看看學校里面最重要的主體是什么?是學生。學生肯定擁有很多屬性,比如學生的學號、姓名、性別、考試分數等等。自然地,我們會聲明一個結構體用于表示學生。
    struct student { int id; // 學號 char name[20]; // 姓名 int gender; // 性別 int mark;
    // 成績 };
    學生的學號由 入學年份 、 班級 、 序號 拼接構成。
    例如,某一個同學的是 2022 年入學的 123 班的 26 號學生。那么,它的學號為 202212326 。
    為了方便設置學號,我們有一個 makeStudentId 函數,參數為 入學年份 、 班級 、 序號
    ,它將這些數據拼接成字符串,再將字符串轉換為整型數據,最后將這個整型數據作為學生的 id 并返回。
    int makeStudentId(int year, int classNum, int serialNum) { char buffer[20];
    sprintf(buffer, "%d%d%d", year, classNum, serialNum); int id = atoi(buffer);
    return id; }
    sprintf和printf函數類似,printf函數會將占位符"%d%d%d"替換為其后的參數,將結果打印到控制臺上。而sprintf
    不會將結果打印在控制臺上,而是將結果存放在第一個參數buffer所指示的字符數組當中。
    函數atoi能將buffer指示的字符串轉換為整型并返回結果。
    性別在結構體中存儲為整型數值,0代表女生、1代表男生。而顯示時,我們希望0顯示為女,1顯示為男。因此,還需要有一對用于操作性別的函數。在函數命名中,使用
    numGender代表使用整型表示的性別。strGender代表使用字符串表示的性別。
    我們將定義兩個函數:
    numGenderToStrGender表示,將整型表示的性別轉換為字符串表示的性別。
    strGenderToNumGender表示,將字符串表示的性別轉換為整型表示的性別。
    const char* numGenderToStrGender(int numGender) { if (numGender == 0) { return
    "女"; } else if (numGender == 1) { return "男"; } return "未知"; } int
    strGenderToNumGender(const char* strGender) { int numGender; if (strcmp("男",
    strGender) == 0) { numGender = 1; } else if (strcmp("女", strGender) == 0) {
    numGender= 0; } else { numGender = -1; } return numGender; }
    我們將使用以下方式,調用這個結構體和這3個函數。
    int main() { struct student stu; // 設置數值 // 學號:202212326 // 姓名:小明 // 性別: 男 //
    成績:98 stu.id = makeStudentId(2022, 123, 26); strcpy(stu.name, "小明"); stu.gender
    = strGenderToNumGender("男"); stu.mark = 98; // 打印這些數值 printf("學號:%d\n", stu.id);
    printf("姓名:%s\n", stu.name); const char* gender = numGenderToStrGender(stu.
    gender); printf("性別:%s\n", gender); printf("分數:%d\n", stu.mark); return 0; }
    現在,我們使用面向過程風格寫了3個函數和一個結構體,并且調用了這些函數,將函數返回的結果賦值給了結構體。接下來,讓我們以面向對象風格來重新審視這段代碼。
    在面向對象風格中,結構體被看做數據(data),而操作數據的函數稱作方法(method)
    。目前函數和數據是分離的,函數并不直接操作數據,我們需要拿到函數返回的結果,再將其賦值給數據。面向對象風格編程的第一大特性——封裝,它希望方法直接操作數據
    ,并且將數據和方法結合在一起,它們構成一個整體。而這個整體被稱作對象。
    此外,還有一個方法命名上的規則。一般來說,獲取數據的方法會被命名為getXXX,設置數據的方法會被命名為setXXX。
    我們對這3個函數做如下修改:

    * 將函數的第一個參數設置為struct student *,讓函數直接操作student結構體。
    * 修改函數名,獲取數據的方法命名為getXXX,設置數據的方法命名為setXXX。 void setStudentId(struct student* s
    , int year, int classNum, int serialNum) { char buffer[20]; sprintf(buffer,
    "%d%d%d", year, classNum, serialNum); int id = atoi(buffer); s->id = id; } const
    char* getGender(struct student* s) { if (s->gender == 0) { return "女"; } else if
    (s->gender == 1) { return "男"; } return "未知"; } void setGender(struct student* s
    , const char* strGender) { int numGender; if (strcmp("男", strGender) == 0) {
    numGender= 1; } else if (strcmp("女", strGender) == 0) { numGender = 0; } else {
    numGender= -1; } s->gender = numGender; }
    現在,我們用修改后的函數,直接操作student結構。
    int main() { struct student stu; // 學號:202212326 // 姓名:小明 // 性別: 男 // 分數:98
    setStudentId(&stu, 2022, 123, 26); strcpy(stu.name, "小明"); setGender(&stu, "男");
    stu.mark = 98; // 打印這些數值 printf("學號:%d\n", stu.id); printf("姓名:%s\n", stu.name)
    ; const char* gender = getGender(&stu); printf("性別:%s\n", gender); printf(
    "分數:%d\n", stu.mark); }
    目前,函數可以直接操作數據了。但是,函數和數據依然是兩個獨立的部分。我們要將函數和數據結合到一起,這樣,這個整體就能被稱作對象,函數可以稱作屬于這個對象的方法

    大多數面向對象語言都提供了以下的格式調用一個對象的方法。
    對象.方法(對象指針,參數1,參數2, 參數3...)
    接下來,我們舉幾個這種格式的例子:
    stu.setGender(&stu, "男");
    以上代碼中,對象為stu,方法為setGender。通過對象 + 點 + 方法的形式,可以調用屬于對象stu的setGender方法。在方法的參數中傳入性別男
    。這樣,方法會把性別男轉換為整形,并設置到對象stu的數據當中。
    const char* gender = stu.getGender(&stu);
    以上代碼中,對象為stu,方法為getGender。通過對象 + 點 + 方法的形式,可以調用屬于對象stu的getGender方法。getGender
    方法從對象數據中獲取整形表示的性別,并返回性別對應的字符串。
    在C語言中,若要實現對象 + 點 + 方法的形式,我們可以借助于函數指針。
    在結構中,聲明這3個函數的函數指針。
    struct student { void (*setStudentId)(struct student* s, int year, int classNum
    , int serialNum); const char* (*getGender)(struct student* s); void (*setGender)
    (struct student* s, const char* strGender); int id; // 學號 char name[20]; // 姓名
    int gender; // 性別 int mark; // 分數 };
    為了讓函數指針有正確的指向,我們需要通過一個initStudent函數,為結構體初始化。
    void initStudent(struct student* s) { s->setStudentId = setStudentId; s->
    getGender= getGender; s->setGender = setGender; }
    現在,我們可以使用對象 + 點 + 方法的形式,調用對象的方法了。
    struct student stu; // 初始化student initStudent(&stu); // 學號:202212326 // 姓名:小明
    // 性別: 男 // 分數:98 stu.setStudentId(&stu, 2022, 123, 26); strcpy(stu.name, "小明");
    stu.setGender(&stu, "男"); stu.mark = 98; // 打印這些數值 printf("學號:%d\n", stu.id);
    printf("姓名:%s\n", stu.name); const char* gender = stu.getGender(&stu); printf(
    "性別:%s\n", gender); printf("分數:%d\n", stu.mark);
    這里有一個需要注意的地方,結構體聲明后,結構體內的函數指針是無效的。必須先調用initStudent
    函數,將其設置正確的指向,才能使用這些函數指針。否則,將有可能導致程序崩潰。
    為了讓方法修改或訪問對象,方法的參數中必須要有對象的指針。實現的形式中,第一個參數就是被操作對象指針。其它語言中,被操作對象指針是隱式傳遞
    的。不需要你在傳參時寫明參數,它會自動傳入函數。例如,C++中會自動將一個名為this
    的對象指針作為方法的參數。而C語言中,無法做到自動將對象的指針傳入方法,所以我們需要手動寫上需要操作的對象的指針。
    // C++的寫法 stu.setGender("男"); // C語言的寫法 stu.setGender(&stu, "男");
    <>繼承

    除了學生之外,學校里面還需要有老師,老師也具有很多屬性。例如:

    * 工號
    * 姓名
    * 性別
    * 任課科目
    聲明一個結構體用于表示老師。
    struct teacher { int id; // 工號 char name[20]; // 姓名 int gender; // 性別 char
    subject[20]; // 任課科目 };
    比較一下學生和老師的結構體,看看它們之間有什么共同之處與不同之處。
    struct teacher { int id; // 工號 char name[20]; // 姓名 int gender; // 性別 char
    subject[20]; // 任課科目 }; struct student { int id; // 學號 char name[20]; // 姓名 int
    gender; // 性別 int mark; // 分數 };
    共同之處如下:

    * 編號
    * 姓名
    * 性別
    不同之處:

    * 學生有考試分數
    * 老師有任課科目
    我們可以把兩個結構體中的共同之處抽象出來,讓它共同之處成為一個新的結構。這個結構體具有老師和學生的共性,而老師與學生它們都是人,可以把這個結構體命名為
    person。
    struct person { int id; // 編號 char name[20]; // 姓名 int gender; // 性別 };
    接下來,我們可以讓老師和學生結構包含這個person對象。
    struct teacher { struct person super; char subject[20]; // 任課科目 }; struct
    student { struct person super; int mark; // 分數 };
    讓我們比較一下原有代碼與現有代碼。

    原有代碼:
    // 原有代碼 struct teacher { int id; // 工號 char name[20]; // 姓名 int gender; // 性別
    char subject[20]; // 任課科目 }; struct student { int id; // 學號 char name[20]; //
    姓名 int gender; // 性別 int mark; // 分數 };
    現有代碼
    // 現有代碼 struct person { int id; // 編號 char name[20]; // 姓名 int gender; // 性別
    }; struct teacher { struct person super; char subject[20]; // 任課科目 }; struct
    student { struct person super; int mark; // 分數 };
    原有代碼中,老師和學生結構體中,均有id、name、gender三個變量。現有代碼中,將這3個變量抽象成結構體person。這樣一來,有兩個好處:

    * 減少重復代碼
    * 代碼層次更清晰
    由于student和teacher擁有person的一切,因此,我們可以說,student與teacher均繼承于person。person是student與
    teacher的父對象。student與teacher是person的子對象。

    剛剛我們只討論了數據,現在我們結合上方法一起討論。
    struct person { int id; // 編號 char name[20]; // 姓名 int gender; // 性別 }; struct
    teacher { struct person super; char subject[20]; // 任課科目 }; struct student {
    struct person super; int mark; // 分數 void (*setStudentId)(struct student* s, int
    year, int classNum, int serialNum); const char* (*getGender)(struct student* s)
    ; void (*setGender)(struct student* s, const char* strGender); };
    之前我們為student寫了3個方法

    * 設置性別
    * 獲取性別
    * 設置學號
    其中,性別相關的方法也屬于共性的方法。可以把這兩個函數指針移動到person對象里面去,注意,要把方法的第一個參數struct student *修改為
    struct person *。移動后,子對象student與teacher均可以使用這一對性別相關的方法。而設置學號的方法,為student
    獨有的方法,因此保持不變,依然將其放置在student對象內。
    struct person { int id; // 編號 char name[20]; // 姓名 int gender; // 性別 // 設置性別
    void (*setGender)(struct person* s, const char* strGender); // 獲取性別 const char*
    (*getGender)(struct person* s); }; struct teacher { struct person super; char
    subject[20]; // 任課科目 }; struct student { struct person super; int mark; // 分數
    // 設置學號 void (*setStudentId)(struct student* s, int year, int classNum, int
    serialNum); };
    對應上面的更改,函數getGender與setGender的第一個參數也要由struct student *修改為struct person *。
    const char* getGender(struct person* p) { if (p->gender == 0) { return "女"; }
    else if (p->gender == 1) { return "男"; } return "未知"; } void setGender(struct
    person* p, const char* strGender) { int numGender; if (strcmp("男", strGender) ==
    0) { numGender = 1; } else if (strcmp("女", strGender) == 0) { numGender = 0; }
    else { numGender = -1; } p->gender = numGender; }
    此外,setStudentId函數中,id成員,不在student中,而是在student中的person中。這里也要對應的修改一下。
    void setStudentId(struct student* s, int year, int classNum, int serialNum) {
    char buffer[20]; sprintf(buffer, "%d%d%d", year, classNum, serialNum); int id =
    atoi(buffer); s->super.id = id; // 由s->id = id 修改為 s->super.id = id }
    還有,別忘了給結構初始化函數指針。
    void initPerson(struct person* p) { p->getGender = getGender; p->setGender =
    setGender; } void initStudent(struct student* s) { initPerson(&(s->super)); s->
    setStudentId= setStudentId; } void initTeacher(struct teacher* t) { initPerson(&
    (t->super)); }
    下面我們即可使用這些對象了。
    struct student stu; // 初始化student initStudent(&stu); // 學號:202212326 // 姓名:小明
    // 性別: 男 // 分數:98 stu.setStudentId(&stu, 2022, 123, 26); strcpy(stu.super.name,
    "小明"); stu.super.setGender(&stu.super, "男"); stu.mark = 98; // 打印這些數值 printf(
    "學號:%d\n", stu.super.id); printf("姓名:%s\n", stu.super.name); const char* gender
    = stu.super.getGender(&stu.super); printf("性別:%s\n", gender); printf("分數:%d\n",
    stu.mark); putchar('\n'); struct teacher t; // 初始化teacher initTeacher(&t); //
    工號:12345 // 姓名:林老師 // 性別: 男 // 科目:C語言 t.super.id = 12345; strcpy(t.super.name,
    "林老師"); t.super.setGender(&t.super, "男"); strcpy(t.subject, "C語言"); // 打印這些數值
    printf("學號:%d\n", t.super.id); printf("姓名:%s\n", t.super.name); gender = t.super
    .getGender(&t.super); printf("性別:%s\n", gender); printf("科目:%s\n", t.subject);
    <>多態

    我們以繪制各種圖形為背景,展開對多態這一特性的討論。

    <>繪制圖形

    現在,我們有3種圖形,它們分別為:

    * 矩形
    * 圓形
    * 三角形
    我們把這3種圖形均看做對象,這些圖形對象,分別需要有哪些屬性呢?

    * 矩形:左上角坐標、右下角坐標
    * 圓形:圓心x坐標、圓心y坐標、半徑
    * 三角形:三個頂點坐標
    現在,我們用代碼分別實現這幾個對象。
    struct Rect { int left; int top; int right; int bottom; }; struct Circle { int
    x; int y; int r; }; struct Triangle { POINT p1; POINT p2; POINT p3; };
    為了能夠在屏幕上繪制這些圖形,每個圖形都設置一個名為draw的方法。
    struct Rect { void (*draw)(struct Rect*); int left; int top; int right; int
    bottom; }; struct Circle { void (*draw)(struct Circle*); int x; int y; int r; };
    struct Triangle { void (*draw)(struct Triangle*); POINT p1; POINT p2; POINT p3;
    };
    分別實現3個不同的繪制函數。

    繪制矩形:

    調用 easyx 中的 rectangle 函數,傳入左上角坐標與右下角坐標。
    void drawRect(struct Rect* r) { rectangle(r->left, r->top, r->right, r->bottom)
    ; }
    繪制圓形:

    調用 easyx 中的 circle 函數,傳入圓心坐標與半徑。
    void drawCircle(struct Circle* c) { circle(c->x, c->y, c->r); }
    繪制三角形:

    調用 easyx 中的 line 函數,分別繪制點 p1 到 p2 的線段, p2 到 p3 的線段,以及 p3 到 p1 的線段。
    void drawTriangle(struct Triangle* t) { line(t->p1.x, t->p1.y, t->p2.x, t->p2.y
    ); line(t->p2.x, t->p2.y, t->p3.x, t->p3.y); line(t->p3.x, t->p3.y, t->p1.x, t->
    p1.y); }
    下面,分別寫3個初始化函數,用于給對象中的函數指針draw進行賦值。
    void initRect(struct Rect* r) { r->draw = drawRect; } void initCircle(struct
    Circle* r) { r->draw = drawCircle; } void initTriangle(struct Triangle* r) { r->
    draw= drawTriangle; }
    現在,準備工作都做好了,我們開始繪制這些圖形吧。
    int main() { initgraph(800, 600); setaspectratio(1, -1); setorigin(400, 300);
    setbkcolor(WHITE); setlinecolor(BLACK); cleardevice(); struct Rect r = { -200,
    200, 200, 0 }; struct Circle c = { 0, 0, 100 }; struct Triangle t = { {0, 200},
    {-200, 0}, {200, 0} }; initRect(&r); initCircle(&c); initTriangle(&t); r.draw(&r
    ); c.draw(&c); t.draw(&t); getchar(); closegraph(); return 0; }
    創建一個800 * 600的繪圖窗體,設置x軸正方向為從左到右,y
    軸正方向為從下到上。將原點坐標從窗體左上角更改為窗體中心。設置背景顏色為白色,描邊顏色為黑色,并使用背景色刷新整個窗體。下面分別聲明矩形、圓形、三角形三個對象,并將需要的屬性初始化。之后,三個對象分別調用各自的
    init函數,為對象內的函數指針賦值。完成準備工作后,即可使用對象 + 點 + 方法的形式,調用各自的draw方法繪制圖形了。

    <>多態
    struct Rect { void (*draw)(struct Rect*); int left; int top; int right; int
    bottom; }; struct Circle { void (*draw)(struct Circle*); int x; int y; int r; };
    struct Triangle { void (*draw)(struct Triangle*); POINT p1; POINT p2; POINT p3;
    };
    我們仔細觀察這3個對象,看看它們分別有什么共性?可以發現,這3個對象,它們都有一個draw方法。那么,我們可以將draw
    這個方法抽象出來,單獨放置到一個對象當中。由于這三個對象都是形狀。我們可以把單獨抽象出來的對象,命名為shape。shape對象中的draw
    方法,應當是一個共性的方法,所以,它的參數應當設置為struct Shape *。
    struct Shape { void (*draw)(struct Shape*); };
    接下來,讓Rect、Circle、Triangle三個對象分別都包含Shape對象。這樣,它們就都能使用draw這個方法了。

    struct Rect { struct Shape super; int left; int top; int right; int bottom; };
    struct Circle { struct Shape super; int x; int y; int r; }; struct Triangle {
    struct Shape super; POINT p1; POINT p2; POINT p3; };
    這里有一個需要注意的地方,父對象與子對象的內存排布必須重合。
    例如:下圖中,上面的兩個對象內存排布可以重合。而下面的兩個對象的內存排布無法重合。

    像下面一樣的聲明Rect是正確的。
    // 正確 struct Rect { struct Shape super; int left; int top; int right; int
    bottom; };
    而下面一樣的聲明Rect是錯誤的。
    // 錯誤 struct Rect { int left; int top; int right; int bottom; struct Shape
    super; };
    接著,我們需要修改各對象的初始化函數。將原有的r->draw改為r->super.draw。
    void initRect(struct Rect* r) { r->super.draw = drawRect; } void initCircle(
    struct Circle* c) { c->super.draw = drawCircle; } void initTriangle(struct
    Triangle* t) { t->super.draw = drawTriangle; }
    注意,這里還有一個問題,函數內賦值運算符左邊的函數指針r->super.draw的類型為void (*)(struct Shape*),參數為struct
    Shape*。而賦值運算符右邊的函數指針類型分別為:
    void (*)(struct Rect*) void (*)(struct Circle*) void (*)(struct Triangle*)
    函數指針參數類型不一致,無法進行賦值。我們可以把右邊的函數指針強制類型轉換為void (*)(struct Shape*)。
    void initRect(struct Rect* r) { r->super.draw = (void (*)(struct Shape*))
    drawRect; } void initCircle(struct Circle* c) { c->super.draw = (void (*)(struct
    Shape*))drawCircle; } void initTriangle(struct Triangle* t) { t->super.draw = (
    void (*)(struct Shape*))drawTriangle; }
    我們考慮一下怎樣來使用這些對象。
    struct Rect r = { {}, - 200, 200, 200, 0 }; struct Circle c = { {}, 0, 0, 100 }
    ; struct Triangle t = { {}, {0, 200}, {-200, 0}, {200, 0} };
    首先,聲明Rect、Circle、Triangle這3個對象,并使用初始化列表將其初始化。注意,由于它們的第一個成員為super,所以,這里使用空列表{},將
    super成員初始化為零。
    initRect(&r); initCircle(&c); initTriangle(&t);
    讓三個對象分別調用各自的初始化函數,給各自對象super成員中的draw設置為各自對應的繪圖函數。

    * r.super.draw設置為drawRect
    * c.super.draw設置為drawCircle
    * t.super.draw設置為drawRTriangle struct Shape* arrShape[3] = { (struct Shape*)&r
    , (struct Shape*)&c, (struct Shape*)&t };
    聲明一個元素類型為struct Shape *的數組,元素個數為3。分別用r的指針,c的指針,t
    的指針初始化。注意,這里也需要進行強制類型轉換,否則初始化列表里面的指針類型和數組元素的指針類型不一致。
    for (int i = 0; i < 3; i++) { arrShape[i]->draw(arrShape[i]); }
    到了關鍵的一步,使用循環,依次調用draw函數。由于3次循環中的draw函數分別為各個圖形各自的繪圖函數。所以,雖然統一調用的是draw
    ,但是,卻可以執行它們各自的繪圖函數。至此,不同實現的方法,在此得到統一。

    <>總結實現多態的步驟

    * 抽離出各個對象中共有的方法draw,將其單獨放置在一個對象Shape內。
    * 各個對象均繼承于Shape對象。
    * 將各個子對象中的draw方法,設置為各自的實現方法。
    * 聲明一個Shape對象的指針,并將其賦值為一個子對象的指針。
    * 通過上述對象指針,調用方法共有方法draw,執行的是第三步中設置的方法。

    技術
    下載桌面版
    GitHub
    百度網盤(提取碼:draw)
    Gitee
    云服務器優惠
    阿里云優惠券
    騰訊云優惠券
    華為云優惠券
    站點信息
    問題反饋
    郵箱:ixiaoyang8@qq.com
    QQ群:766591547
    關注微信
    巨胸美乳无码人妻视频