章 9. HelloWorld 及基本功能

HelloWorld

在這裡以 C 語言傳統的 HelloWorld 為範例. 這程式很簡單, 其實不是 Objective-C, 但是 Objective-C 可以使用 C 的語法. 這程式主要是說明如何編譯 Objective-C 程式. Objective-C 的副檔名是 m.

main.m:

#include <stdio.h>

int main(void)
{
   printf("Hello World\n");
}

stdio.h 是 C Library, 在這裡使用了 printf(), 所以需要 stdio.h, 除了 main.m 之外還需要 GNUmakefile 檔才能編譯.

GNUmakefile:

include $(GNUSTEP_MAKEFILES)/common.make

APP_NAME = HelloWorld
HelloWorld_HEADERS =
HelloWorld_OBJC_FILES = main.m
HelloWorld_RESOURCE_FILES =

include $(GNUSTEP_MAKEFILES)/application.make

GNUmakefile 使用固定的格式. APP_NAME 就是指定的程式名稱. HelloWorld_HEADERS 是使用的 Header 檔(.h), 目前沒有. HelloWorld_OBJC_FILES 是要編譯的檔案, 也就是上面的程式碼, 檔名為 main.m, 最後是 HelloWorld_RESOURCE_FILES, 是這個程式的資源. 要注意的地方是 APP_NAME 指定了程式名稱後, 接下來的參數 (HelloWorld_*) 都是跟著這個程式名稱. 一旦程式名稱有所修改, 則參數都要跟著改變. 這類文字模式的程式, 在 GNUstep 中應該歸類為 Tool, 而不是 Application. 在 GNUmakefile 的寫法會有些許的差點, 但不影響這個範例. 有關 GNUmakefile 的使用, 可以參考 GNUstep programming mini tutorials, GNUstep Makefile Package.

這兩個檔案都寫好之後, 執行 gmake 即可. (在 FreeBSD 內定的 make 不是 GNU make, 所以要自行安裝 GNU make. 對大部份的 Linux, make 就是 gmake.) 如果想要清除編譯過程中產生的中間碼, 而保留最後的執行碼, 可以使用 gmake clean. 如果想把所有編譯出來的程式都清除, 只留下程式碼, 則使用 gmake distclean. 這些命令有助於在程式開發中需要清除舊的中間碼及執行碼.

之前指定 APP_NAME = HelloWorld. 等程式編譯好之後, 就會出現一個 HelloWorld.app 的目錄, 程式就在裡面. 執行 GNUstep 程式的標準動作是使用 openapp 這個程式, 例如: openapp HelloWorld, 這樣就會看到 Hello World 的字串.

藉這個例子順便說明一些 GNUstep 的特色.

首先是 Objective-C 的檔案內可以使用 C 語言, 在這個例子便使用 stdio.h 和 printf(). OpenStep 當初是設計給圖形介面的作業系統使用的, 所有的輸出都是在圖形介面上, 所以沒有專給文字輸出用的 Class, 因此這個例子只好使用 printf().

GNUmakefile 基本上就是常用的 Makefile. 在前後各引入了 common.makeapplication.make, 所以只要加上自己程式的名稱和相關檔案, 就完成了. 不用自己寫 Makefile, 方便許多. 在 GNUmakefile 中使用了 $(GNUSTEP_MAKEFILES), 這個變數要設對位置. 如果成功安裝 GNUstep 就應該沒有問題.

最後是 GNUstep 的程式都是 Bundle, 也就是和資源結合在一起的, 這在圖形介面的程式中常用, 像是 icon 就是資源的一部份. 在這個例子中, 所有的資源都會放在 HelloWorld.app 目錄中, 使用 openapp 這個工具程式就會找到所有需要的東西. 所以這時 HelloWorld.app 這個目錄, 可以視為是一個包含資源的程式, 用 openapp 來執行 HelloWorld 這個目錄名稱, 就是執行這個程式. 使用 Bundle 的好處是如果想要移動 GNUstep 程式或是刪除, 以整個目錄為單位即可, 不會遺漏任何重要的檔案而使得程式無法執行, 或是刪除不完整. 這在程式的管理上相當方便, 所有的東西都在同一個目錄之下.

了解如何編譯 GNUstep 程式之後, 可以開始寫 Objective-C 程式. 在這還是以 Hello World 做例子.

在物件導向語言中, 最重要的就是 Class. 在這裡實作一個 Say 的 Class, 需要 say.h 和 say.m

say.h:

#ifndef _Say_H_
#define _Say_H_

#include <Foundation/NSObject.h>
@interface Say: NSObject
{
}
- (void) sayHello;
- (void) sayHelloTo: (NSString *)name;
@end

#endif /* _Say_H_ */

say.m:

#include "say.h"
#include <Foundation/Foundation.h>

@implementation Say
- (void) sayHello
{
   NSLog(@"Hello World");
}

- (void) sayHelloTo: (NSString *)name
{
   NSLog(@"Hello World, %@", name);
}
@end

接著改寫 main.mGNUmakefile.

main.m:

#include "say.h"
#include <Foundation/Foundation.h>

int main (void)
{
   id speaker;
   NSString *name = @"GNUstep !";
   NSAutoreleasePool *pool;

   pool = [NSAutoreleasePool new];
   speaker = [[Say alloc] init];

   [speaker sayHello];
   [speaker sayHelloTo:name];

   RELEASE(speaker);
   RELEASE(pool);
}

GNUmakefile:

include $(GNUSTEP_MAKEFILES)/common.make

APP_NAME = HelloWorld
HelloWorld_HEADERS = say.h
HelloWorld_OBJC_FILES = main.m say.m
HelloWorld_RESOURCE_FILES =

include $(GNUSTEP_MAKEFILES)/application.make

同樣使用 gmakeopenapp HelloWorld 來編譯及執行程式.

執行的結果應該類似這樣:


Jan 30 09:50:34 HelloWorld[961] Hello World
Jan 30 09:50:34 HelloWorld[961] Hello World, GNUstep !

回到程式碼的部份. say.h 是宣告 (interface), say.m 是實作 (implementation). 在 Objective-C 中, 可以使用 C 的 #include 或是 #import. 在 GCC 中, 建議使用 #include, 編譯速度較快. 為了避免重複引入相同的宣告 (Header files). 使用 #ifndef ... #define... #endif. say.h 接著用 @interface 宣告一個 Say 的 Class, 並繼承 NSObject. NSObject 是 GNUstep 的最基本 Class (root class), 沒有特別需要時繼承 NSObject 是最容易的. { } 中是用來宣告變數, 這個例子中沒有任何的變數. 再來是 -(void) sayHello, 是宣告這個 Class 的 Method. - 表示這是個 Instance method, 如果是 + 則是 Class method. 函式的宣告和 C 一樣. 第二個 method 是同樣道理, 只是加入了傳入值, name. 在 Objective-C 中, 因為使用 message, 所以傳入值用分號分開, 如果有兩個傳入值, 就設兩個, 如 - (void) sayHelloTo:(NSString *)name1 and:(NSString *)name2. NSString 是 GNUstep 處理字串的類別 (Class). 最後用 @end 表示宣告結束.

say.m 則是實作 say.h 的宣告. 在 say.h 中已經宣告了要 import 的檔案, 也宣告了 Say 的繼承類別, 所以 say.m 中予以都省略, 如果 say.h 中有宣告變數, say.m中也不需要重寫. say.m 用 @implementation 表示實作, 把 method 要實作的部份寫好. 這裡使用 NSLog 輸出字串. NSLog 類似 printf(), 但主要是輸出程式執行時的記錄, 因此會自動加上其他的訊息, 在這裡暫用做字串輸出. GNUstep 是為圖形介面設計的 Framework, 因此沒有專為文字介面設計的輸出, 如果有需要, 可以改用 C 的 printf() 等. NSLog 中, @"..." 表示字串, 字串中的 %@ 相當於 printf() 的 %s, 但是 %@ 可以輸出任何的物件, 在 debug 時相當好用, 可以隨時隨地輸出要追蹤的物件. 利用 NSLog, sayHello 會輸出 Hello World, sayHelloTo 會加上傳入的變數.

最後是 main.m. 先引入 (#include) "say.h", 這是我們唯一要用的 Class, 其他的 classes 都在 Foundation.h 中. 再來傳告一個 speaker 和一個字串, name. Objective-C 是 C 的延伸, 所以還是要照 C 的規矩: 變數的宣告要放在程式碼前. 因此不能有 for (int i = 0, ...) 之類的程式碼, 這是與 Java/C++ 不同的地方. 因為 Objective-C 使用 Dynamic Typing 和 Dynamic Binding, 因此變數所屬的 Class, 在編譯時不重要, 宣告成 id 就好了, 表示是一個 Object. 也可以宣告成 Say, 可以在編譯時做預先的檢查. 真正決定 speaker 所屬的 Class 的是: speaker = [Say alloc]. Say 取得一個記憶體的位置, 然後將 speaker 指到該記憶體位置, 所以 speaker 是個指標, 即然是指標, 屬於那個 Class 就不重要了, Objective-C 有另外的方法知道一個指標所指的 Class, 在宣告時使用一種通用格式, id, 就好了.

取得記憶體位置之後還要設定啟始值, 也就是 [speaker init], 不過一般都是取得記憶體和設定啟始值一起執行, 就成了 speaker = [[Say alloc] init]. Say 沒有任何的變數, 所以在 say.m 中沒有實作 - (id)init, GNUstep 找不到 Say 的 init, 會往上找到 NSObject 的 init. 有了 speaker, 就可以把 message 傳進去了, 這也就是 [speaker sayHello] 和 [speaker sayHelloTo:name].

至於 NSAutoreleasePool 及 RELEASE() 的部份, 則先留到記憶體管理的部份再談.

透過以上的例子, Objective-C 就算學會了一半, 再加上原有的 C 的基礎, 多數的程式都可以應付了.

檔案輸出及輸入

要做到跨平台, 檔案的輸出入就很重要. 在 GNUstep 中提供兩個類別 (Class), NSFileHandle 和 NSFileManager. NSFileHandle 是處理檔案的內容, 而 NSFileManager 是處理檔案管理. 這兩者有部份重疊, 可以憑個人喜好做選擇.

main.m:

#include <Foundation/Foundation.h>
   
int main (void)
{
   NSString *path;
   NSAutoreleasePool *pool;
   NSFileHandle *readFile, *writeFile;
   NSData *fileData;
   
   pool = [NSAutoreleasePool new];
   path = @"main.m";
   
   readFile = [NSFileHandle fileHandleForReadingAtPath:path];
   fileData = [readFile readDataToEndOfFile];
   
   writeFile = [NSFileHandle fileHandleWithStandardOutput];
   [writeFile writeData:fileData];
   RELEASE(pool);

   return 0;
}

GNUmakefile:

include $(GNUSTEP_MAKEFILES)/common.make
   
APP_NAME = FileHandle
FileHandle_HEADERS =
FileHandle_OBJC_FILES = main.m
FileHandle_RESOURCE_FILES =
   
include $(GNUSTEP_MAKEFILES)/application.make

如果有寫過 Java, 就像發現兩者之間有些相似. readFile, writeFile 建立輸入及輸出的通道, fileData 則儲存檔案的內容.

path 設為 main.m (path=@"main.m"). 其中 main.m 可以為任何的字串, 在這裡設為 main.m, 使這程式顯示出 main.m 的內容. GNUstep 中字串以@加在前面表示是字串.

path 設定完成之後, 用 NSFileHandle 的 fileHandleForReadingAtPath 取得 path 所代表的檔案, 從此就可以利用 readFile 這個物件來讀檔. 同理, 用 NSFileHandle 的 fileHandleWithStandardOutput 取得要輸出的物件, 也就是螢幕 (標準輸出). 有了 readFile 和 writeFile 這兩個物件後, 就可以隨意的輸出入檔案.

使用 [readFile readDataToEndofFile] 將檔案內容輸入到 fileData 中, 接著用 [writeFile writeData: fileData] 將 NSData 輸出到螢幕就即可.

在這例子中輸入輸出之間不做任何的處理, 可以合併成:

[writeFile writeData: [readFile readDataToEndOfFile]];

在上述的例子中, NSData 像個黑盒子一樣, 裡面的資料不知要如何用, 只能原封不動的傳來傳去, 用處不大. 在這裡將 NSData 內的資料取出, 以一般 C 語言的方式處理, 如此就可以直接處理 NSData 內的資料了. 透過這樣子的方式, 可以將 NSData 中的資料讀出, 呼叫許多現成的 C Library 來處理, 再送回 NSData 供 GNUstep 使用. 這是結合 GNUstep 與許多 C Library 的方法之一.

main.m:

#import <Foundation/Foundation.h>

int main (void)
{ 
  NSString *path;
  NSAutoreleasePool *pool;
  NSFileHandle *readFile;
  NSData* fileData;
   
  char *buffer;
  unsigned int length;

  pool = [NSAutoreleasePool new];
  // path = @"main.m";
  path = @"GNUmakefile";
  readFile = [NSFileHandle fileHandleForReadingAtPath:path];
  fileData = [readFile readDataToEndOfFile];

  length = [fileData length];
  buffer = malloc(sizeof(char)*length);
  [fileData getBytes: buffer];

  printf("%s\n", buffer);
  printf("%d\n", length);
  free(buffer);
   
  RELEASE(pool);
   
  return 0;
}

Foundation 常用功能