2015年8月16日 星期日

物件編譯和前置宣告

參考 http://www.dotblogs.com.tw/v6610688/archive/2013/12/05/how_to_class_include_each_other.aspx

像之前講的,編譯器會將每個.cpp編譯成物件檔.o,最後再用連結器link起來變成exe

那至於怎麼編譯呢,就我理解只要把它想成您編譯的.cpp,裡面有使用到的變數,函式及class,他可以找的到定義,而您的語法又沒問題,就可以編一成一個.o

我這邊舉一連串的例子來讓您理解,舉個例:
a.cpp
void abc()
{
        A b;
}

這個編譯是會出錯的,他會說您沒有定義A,所以您要如何解決咧,就是要在他前面宣告一個實體的structclass,像這樣
class a
{
public:
     a(){};
     ~a(){};
};

void abc()
{
        a b;
}

這樣編譯就會過了


要是您的.cpp上方沒有宣告,其實您可以拿到.h中宣告,並include

a.h
class a
{
public:
     a(){};
    ~a(){};
};

a.cpp
#include “a.h”
void abc()
{
        a b;
}

這樣他就會去include的地方找定義

其實這樣寫法是有問題的,因為通常您程式寫很大時,常常會亂掉,一不小心include太多層,導致同一個.h不小心會include兩次,造成重複定義

所以我們保險起見,有兩種寫法:
1.      舊式
a.h
#ifndef A_H
#define A_H
class a
{
public:
     a(){};
    ~a(){};
};
#endif

a.cpp
#include “a.h”
void abc()
{
        a b;
}

2.      新式

a.h
class a
{
public:
    a(){};
    ~a(){};
};

a.cpp
#pragma once
#include “a.h”
void abc()
{
        a b;
}

當然也可以兩個都加,因為只不過多個一行,不礙事

這樣就不會發生include多次的問題了(我記得之前的文章有講過原因)


在這邊要注意一件事情,當您在程式中宣告的是一個物件(不管在哪,classstruct),他都會去找他的定義(向上面範例一樣),那要是您宣告的是一個物件指標呢,其實用前置宣告就可以解決了,如下:

註:前置宣告就是向下面紅字一樣,告知編譯器,您這個class存在,在其他地方會實體化

1.      在函式內,
Class a;
void abc()
{
        a b;
}

2.      class
Class a;
Class a
{
Public:
       A* b;
}

3.      就任何地方,只要他程式碼往上找找的到就可以(包括寫在標頭檔),像下面這樣也行(還記得之前提的生命週期吧)
Class a
{
Public:
        Class a;
        A* b;
}


這樣就達到基本的寫法,通常我們在class內定義都會定義指標,不會直接宣告物件,因為會發生我參考網頁上面的那種情況,就是兩個class互相include,因為編譯時互相跳來跳去(連結有寫,我就不打了),導致有定義沒找到,

也就是,您只要照我下面寫法,會比較不會遇到一些Bug
1.      就是.h有使用到的物件,請宣告成指標,並在前面加上前置宣告(不直接include標頭檔)
2.      .cpp裡就include您物件定義的標頭檔
3.      .h不是實體化任何東西(函式或是class裡的函式),請都移至.cpp定義

a.h
//前置宣告
class b;
class a
{
public:
    a();
    ~a();
    Void zzz();
    B* a;
};

a.cpp
#pragma once
#include “b.h”
#include “a.h”

A::a()
{}

A::~a()
{}

 

Void a::zzz()
{
        A = new b();
        Delete A;
}

b.h
//前置宣告
class a;
class b
{
public:
    b();
    ~b();
    zzz();
    a* b
};

b.cpp
#pragma once
#include “a.h”
#include “b.h”

b::b()
{}

b::~b()
{}

Void b::zzz()
{
        A = new b();
        Delete A;
}

這樣基本上是我目前寫Code寫得最成功的寫法(目前都這樣寫),真的,不這樣寫Code會卡死,當然還是鼓勵自己試試看錯誤寫法,畢竟您沒看到真的跳出錯誤,您怎麼知道我不是騙您的(),程式設計師一開始就摸不會有問題的東西,是不會成長的(要牢記)


八月份就到這裡了,原本沒空弄得(加班加到吐血,還好之前有多打一點)
Blogger還真的很麻煩,排版很累,基本上就看看就好,上班很累,沒時間做得很完美,
12月份應該會按時間貼文章

C++物件導向的基本知識(階層關係)

C++物件導向其實是一層一層的,namespace是一個大集合(也就是一個大物件拉),他底下的其他東西(class,struct,function)都可以互相包來包去,並構成一個物件,而您可以根據C++的語法使用他們來完成您的程式


這篇先不講這個,這篇主要是說在windows程式下您要怎樣來利用IDE(就是Visual Studio)幫你研究這種階層的關係


廢話不多說,直接進入正題


有寫過Windows下的視窗軟體的人都知道,當您有一個元件,例如Panel,假設它叫Panel1,它是一個指標,您要查他的屬性或者是命令(例如repaint()),您只要使用”->”它就會有提示屬性出來,要是您有一個自創物件,假設是class A{};您宣告一個A a;,您要查他指令只需在a後面”.”,它就會有提示變數或是函數給您選,這功能十分方便,我打算用這個功能來讓您更了解物件導向的關係


物件導向其實十分複雜,但是當你剛入門時,通常是不會碰到AOOP(高等物件導向),所以您在看c++ primer plus 6時,其實後面都可以跳過,因為它算是工具書,什麼都寫,所以像是tempalte(模板),繼承,虛函數,或是奇奇怪怪的建構元,您都可以先跳過(但是不代表您以後不用學,您進業界就知道,這些東西都是用來兜出您的軟體系統的基本知識,您只要看別人應用過了,你會發現他其實很好用,然後您也會自己下去用它…),這通常要進一些比較有技術底子的公司才看的到,不然您說我碩士畢業後,會這些嗎,真的是想太多了,我那時候要是沒有課程壓力,我還不想學咧


剛入門您只要知道functionclassstruct#define,和學校教的那些基本變數型態便可以了,其他都是當您對物件導向比較通時,並且有寫過幾個小程式時,在看您接的案子需要再做學習(通常你摸到中大型案子=全學拉),你有東西不會用,卡在一個小地方或小技巧,您寫的code會很長,寫的時間會很久,總之壞處很多


這裡是重點

很多啦,都是例子,您不懂可以把程式碼貼到程式裡跑跑看,如您真懂0.0,大概看不懂的地方就可以了,其實很快拉
 
 
物件導向他的規則是:
1. 一層接一層,上層看不到下層的東西,外層看不到內層,在下層可以看到上層的東西,在內層可以看到外層前面的東西

 2. 當您在該層宣告一個變數或定義,該變數或定義在這層離開後便看不到了(也就是網路上面人家寫的生命週期)

3. 當你在不同層宣告兩個相同名稱的變數,假設您要使用到那個變數,他會向上找最近的定義那個變數當作他找到的

4. 當您使用任何一個變數,一定要有定義,而Compiler會從您那行向上回頭找,直到找到您的定義,他才會Compiler過


這裡講的是垂直關係,下一篇會講水平關係,就是不同.o檔之間的物件導向關係

//////////////////////////////////////////////////////////////////////////////

一層接一層,上層看不到下層的東西,外層看不到內層,在下層可以看到上層的東西,在內層可以看到外層前面的東西

//////////////////////////////////////////////////////////////////////////////

舉個例子您就懂了,您可依照我的例子將程式碼按照順序寫進寫進程式中,其中main_指的是您撰寫的那個函式,可以是一個buttonclick,又或者是一個執行中的函式,總之您將它貼到程式碼按編譯就對了

1.      上層看不到下層的東西
int a;
Void abc(int b)
{
    ++a;
}
Void main_()
{
        a = 5;
        abc(a);
}
這可以正常編譯

////////////////////////////

Void abc(int b)
{
    ++a;
}
int a;
Void main_()
{
        a = 5;
        abc(a);
}
這裡
Compiler會說找不到變數i

註:通常會出現很多Bug,一般解Bug是由上往下解,第一個解完可能後面的錯誤會跟著消失,但如果您從其他地方開始解呢,你可能永遠解不到正確的Bug(相信我)

///////////////////////////

int a;
Void main_()
{
        a = 5;
        abc(a);
}
Void abc(int b)
{
    ++a;
}
Compiler會說找不到函式abc

///////////////////////////

有沒有看到當順序錯時,要是您不符合這種通用的Compiler編譯規則,就會跳出錯誤訊息,所以在寫程式前必須了解這種先後關係

除了第一個會編譯過,其實您還有其他種解法,

您可以使用extern關鍵字來告知Compiler這個變數或函式已經在其他地方宣告過,這樣編譯也會過

extern int a;
extern Void abc(int b);
Void main_()
{
        a = 5;
        abc(a);
}
int a;
Void abc(int b)
{
    ++a;
}

註:這裡只是弄清楚關係,其實您將定義寫到頭檔一樣是這個問題,可以這樣解,這裡就不示範,如何寫(因為這很基本,您不會代表C沒學好)

///////////////////////

2.      外層看不到內層的東西,何況是順序錯誤

Void main_()
{
    Int a = 5;
    For(int I =0;i<10;i++)
    {
         ++a;
    }
}

這裡Compiler會過,原因當您使用a這個變數進行任何動作,您在他for迴圈外(上方)宣告了,因此他回頭找有找到定義

/////////////////////////////

Void main_()
{
      a = 5;
      For(int I =0;i<10;i++)
      {
          int a;
     }
}

這裡Compiler會說編譯器沒有找到a
原因a是在for迴圈裡宣告的,你a=5上面並沒有看到任何定義

/////////////////////////////

Void main_()
{      
    For(int I =0;i<10;i++)
    {
        int a;
    }
    a++;
}

這裡Compiler一樣會說a沒有定義,原因是a是在for迴圈裡宣告的,他生命週期只有在for迴圈離開前(也就是說這個定義在for迴圈結束後就沒用了)

/////////////////////////////

Void main_()
{        
    Int a;
    {
        ++a;
    }
}

有沒有看過這樣寫,其實這樣編譯器也會過,也就是{}是下一層,所以您要是像下面這樣寫的化

Void main_()
{        
    {
        Int a;
    }
    ++a;
}

一樣Compiler會出現a沒有定義錯誤,原因a這個定義在{}離開後就沒用了


再舉個比較特殊的例子

Void main_()
{      
    for(int I =0;i<10;i++)
    {
        Int a;
    }
    for(int I =0;i<10;i++)
    {
        A++;
    }
}

這樣一樣Compiler會出現a沒有定義錯誤,都是之前那個原因

//////////////////////////////////

變數可以這樣搞

您確定classstruct不能這樣做嗎(這很重要喔,這會打通您對學校教的C++以偏概全的錯誤概念,因為您在學校應該也跟我一樣,classstruct只能放在頭檔或是函式外宣告吧)

void main_()
{
    struct aaa
    {
        int a;
    };
    aaa abc;
}

您可以試試這樣,您會發現Compiler會過,但您會說這對您有什麼好處咧,這在模組化設計時很常用在函式裡宣告結構使用,這裡您只要知道他是對的就可以了

//////////////////////////

void main_()
{
    {
        struct aaa
        {
            int a;
        };
    }
    aaa abc;
}

老問題這Compiler也會說您找不到aaa定義,這要慢慢理解,class一樣這個道理

///////////////////////////////

變數可以在函式內宣告,函式可以嗎(也就是區域函式),就我試的狀況是不行,但是可以宣告在struct裡面

void main_()
{
    struct aaa
    {
        int a;
        void abcc(){++a;}
    };
    aaa abc;
    abc.abcc();
}

這樣可以

//////////////////////////////////

void main_()
{  
    void abc(){}
    abc();
}

這樣會錯誤,說您區域函式定義不合法

//////////////////////////////////

void main_()
{  
    extern void abc();
    abc();
}

void abc()
{
    int a = 0;
    ++a;
}

這樣就可以了,但是要是下面狀況咧?

void bcd()
{
    int b = 0;
    abc();
    ++b;
}

void main_()
{  
    extern void abc();
    abc();
    bcd();
}

void abc()
{
    int a = 0;
    ++a;
}

這樣bcd裡面的abc會找不到定義,有沒有很神奇,要怎樣改

void bcd()
{
    extern void abc();
    int b = 0;
    abc();
    ++b;
}

void main_()
{  
    extern void abc();
    abc();
    bcd();
}

void abc()
{
    int a = 0;
    ++a;
}

您看,這樣是不是就過了

註:要注意喔,Compiler是很死的,在前面或上層沒看到宣告或者是extern的方式表示已經宣告,就會編譯不過,一層一層的向上層或向外層找定義,找不到就去頭檔找,要是都沒有,就會找不到識別項:D


再來給您看生命週期奧妙的地方

void main_()
{
    int b = 1;
    //1.這裡b是多少
    ++b;
    //2.這裡b是多少
    {
        //3.這裡b是多少
        int b = 5;
        //4.這裡b是多少
        ++b
        //5.這裡b是多少
    }
    //6.這裡b是多少
    ++b
    //7.這裡b是多少
}


您可用下中斷點的方式,執行程式,去看他跳到那行後的b是多少,以幫助您了解我下面說的答案

//1.這裡b是多少
1,因為她才剛宣告
//2.這裡b是多少
2,因為++b給他加一了
//3.這裡b是多少
還是2,因為他找到的變數是外層的那個,所以當然得到是他的值
//4.這裡b是多少
他找到的定義是內層的b,我重新宣告為5,所以b的值是5
//5.這裡b是多少
他找到的定義是內層的b,又加一,所以是6
//6.這裡b是多少
由於他回到外層,看不到內層的宣告,因此他重新找到外層的b,所以是2
//7.這裡b是多少
就外層的繼續加一,所以是3


搞懂這個有助於您階層的概念

///////////////////////////

假設兩個頭檔,一個使用前置宣告,一個正常宣告

a.h
class aaa
{
        Int abc;
};

很多例子吧0.0,不難,就我知道的就那五項規則,但要搞懂您基本上要寫很多程式,光嘴砲、看文章讀懂或者是冥想是沒意義的,請多加實作,

當然實作要一個主題,這種事通常要接案子或者是研究生才摸得道的,所以為什麼碩士投資報酬率很高,跟這點有很大關係,當然前提是您有題目時您也要認真寫code,不然是沒用的(當然出社會一樣啦,但是出社會要懂得亙多,像我做AOI要涉略機械,光學,電子學,軟體(但主要還是在寫軟體拉,不過其他的也要消化吸收(有空或有機會時,是不致於要詳細研究,但是要略懂)))

生命週期這個東西,網路上好像叫它叫做scope
http://openhome.cc/Gossip/CppGossip/Scope.html