跳轉到內容

使用 C 和 C++/泛型容器的程式語言概念

來自 Wikibooks,開放世界的開放書籍

在本章中,我們將研究泛型容器的實現,即向量。我們的模組將使客戶能夠建立和操作包含不同資料型別的專案的向量。透過這樣做,我們將模擬 C++ 風格的容器,這些容器包含特定型別的物件。[1] 也就是說,特定的容器要麼是 int 的集合,要麼是 double 的集合,但不會是兩者的混合。這與 1.5 之前的 Java 風格容器形成對比,後者容器是 Object 的集合,因此可以包含屬於不相容類的專案。

我們將在這份講義中討論的另一個 C 程式設計方面是使用可變引數函式來彌補過載的缺失。可變引數函式使程式設計師能夠在命名引數之上擁有不定數量的未命名形式引數,可以用來為過載提供一種簡陋的替代方案。我們所要做的就是讓呼叫者提供一個額外的引數來區分過載函式候選者。

示例:不寫

void f(int); void f(int, int); void f(double);

f(3); f(3, 3); f(3.0);

我們現在寫

void f(int func_type, ...);

f(ONE_INT, 3); f(TWO_INTS, 3, 3); f(ONE_DOUBLE, 3.0)

我們的介紹將以關於靜態目標模組庫的一些內容結束。

General.h
#ifndef GENERAL_H

#define GENERAL_H

...
...

以下宏定義代表了在實現抽象資料型別過程中應考慮的建構函式型別。

#define COPY 0
#define DEFAULT 1

...

從已存在的 Vector 物件建立副本涉及複製其元件。稍加思考,我們就會發現沒有通用的函式可以滿足此目的。事實上,相同的資料型別可能在不同的上下文中需要不同的此類函式。相同的論據適用於銷燬 Vector 物件的情況。

那麼,我們(程式設計師)是否應該提供每一個可能的複製和銷燬函式?這違背了提供泛型容器的目的。我們必須讓使用者與我們合作。實現這一點的方法是透過以下指向函式型別的定義。

typedef Object (*COPY_FUNC)(Object);
typedef void (*DESTRUCTION_FUNC)(Object);

#endif
Vector.h
#ifndef VECTOR_H
#define VECTOR_H

#include "General.h"

struct _VECTOR;
typedef struct _VECTOR* Vector;

我們想要實現一個泛型向量。也就是說,一個可以將任何東西作為其元件的向量:int 的向量、字串的向量、學生記錄的向量等等。鑑於此要求以及元件結構只能由使用者(而不是實現者)知曉,我們需要讓使用者提供一些關於複製和銷燬向量元件的幫助。基本上,使用者必須實現這兩個函式,並以某種方式將其傳遞給實現者。換句話說,使用者和實現者必須合作;使用者必須填寫實現者提供的框架中的缺失部分。維護學生記錄向量的使用者必須提供學生記錄型別的複製和銷燬函式;處理員工記錄的使用者必須為員工記錄型別提供相同的函式;...

這種程式設計風格與傳統技術不同,在傳統技術中,庫的使用者開發呼叫庫函式的演算法,但每個新應用程式的應用程式的總體結構和控制流都不同。雖然幾乎所有 C 程式都使用 I/O 庫,但特定程式的總體結構獨立於其他程式;I/O 庫中提供的例程只是所有程式共有的“低階”操作,不需要針對特定應用程式的需求進行定製。這是在 Pascal 和 C 等程序式程式設計語言中使用的技術。[2]

Traditional libraries

然而,在軟體框架的使用中,是總體結構,演算法的高階檢視保持不變,並在應用程式之間重複使用。程式設計師(框架的使用者)專門化框架,但大多數程式碼在所有應用程式中都保持相同。由於庫程式碼和使用者提供的程式碼之間的關係發生了這種逆轉,軟體框架有時被稱為“顛倒”的庫。

Software frameworks

最著名的軟體框架是與圖形使用者介面相關的那些。執行圖形程式的主要任務 - 等待使用者移動或點選滑鼠,等待擊鍵,移動或調整視窗等等 - 在一個應用程式到另一個應用程式之間變化不大。因此,這些操作可以有效地組合在一個框架中,不僅可以節省開發新應用程式的程式設計工作量,還可以確保應用程式之間的一致性。區分一個程式與另一個程式的是諸如視窗內容以及響應滑鼠點選或擊鍵而執行的操作等功能。這些操作必須在框架的每個特定應用程式中重新程式設計。

這是面向物件程式語言中選擇的風格。要建立一個新應用程式,只需要從原始類中進行子類化(使用繼承),然後實現與這些延遲函式相關的行為即可。所有其他功能,包括應用程式的總體結構,都是透過使用繼承“免費獲得”的。流行的例子是基於 C++ 的 MFC(Microsoft Foundation Classes)和基於 Java 的 JFC(Java Foundation Classes)。

如果實現者可以“回撥”到某些使用者程式碼,這種程式設計風格(軟體框架)將是可能的。在面向物件程式語言中,這可以透過實現框架中定義的介面或對抽象類進行子類化並在該類中實現抽象函式(例如滑鼠點選、擊鍵等)來完成。在 C 中,我們可以透過使用函式指標來做到這一點。我們所要做的就是定義函式簽名(我們現在正在做),並將這些定義用作某些函式中引數的型別。回撥是在實現者透過傳遞給函式的函式指標呼叫使用者程式碼時發生的。

typedef struct _Object_funcs {
  COPY_FUNC copy_component;
  DESTRUCTION_FUNC destroy_component;
} Vector_Component_Funcs;

#define NON_EXISTING_VECTOR -1

#define INIT_CAP 2
#define BOTH 3

以下函式簽名是 C 中宣告可變引數函式的一個示例。在規範中使用的省略號表示在命名引數之後將出現未知數量的引數。[3] 傳遞給可變引數函式的引數數量是從一個或多個命名引數的內容中推斷出來的。以 printf 函式為例,其原型如下所示。

int printf(const char *format_string, ...);

佔位符,以“%”開頭的某些字元序列,有助於確定引數的數量和型別。

extern Vector Vector_Create(int constructor_type, ...);
extern int Vector_Destroy(Vector* this);
extern void Vector_Assign(Vector* lhs, const Vector rhs);
extern Object Vector_ElementAt(const Vector this, unsigned int pos);
extern Object Vector_InsertAt(const Vector this, Object item, unsigned int pos);
extern Object Vector_RemoveAt(const Vector this, unsigned int pos);
extern Object Vector_UpdateAt(const Vector this, Object new_item, unsigned int pos);
extern unsigned int Vector_Size(const Vector this);

#endif
Vector.c
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "General.h"
#include "utility/Vector.h"

為了控制在不同控制代碼(實際上是我們的指標)之間共享的特定物件的生存期,我們使用一個額外的欄位來儲存提供對相同底層物件的訪問的控制代碼數量。這樣的欄位被稱為引用計數

結構定義的最後一個欄位可以看作 Object container[];,為了教學目的。也就是說,一個 Vector 基本上是一個 Object 的陣列。使用 * 而不是 [] 使我們能夠動態地更改 Vector 的大小。如果我們使用 [],我們必須在編譯時指定大小,這對於我們的目的來說太嚴格了。

struct _VECTOR {
  COPY_FUNC copy_component;
  DESTRUCTION_FUNC destroy_component; 
  unsigned int ref_count;
  unsigned int cap;
  unsigned int cap_inc;
  unsigned int cur_size;
  Object *container;
};

static BOOL adjust_capacity(const Vector); 
static void shift_down(const Vector, unsigned int);
static void shift_up(const Vector, unsigned int); 

Vector Vector_Create(int type, ...) {
  int i;

以下行聲明瞭一個變數,該變數將在遍歷未命名引數列表時使用。

  va_list ap;
  Vector existing_vec;
  Vector_Component_Funcs comp_funcs;

  Vector ret_vec = (Vector) malloc(sizeof(struct _VECTOR));
  if (!ret_vec) {
    fprintf(stderr, "Out of memory...\n");
    return NULL;
  } /* end of (!ret_vec) */

  ret_vec->cur_size = 0; 
  ret_vec->ref_count = 1;
  ret_vec->container = NULL;

以下宏用於初始化未命名引數列表,以便 ap 中的內部指標指向最後一個命名引數之後的位置。

Initializing the variable argument list


  va_start(ap, type);
  switch (type) {
    case COPY:

下一行返回一個型別為 Vector 的引數,並使內部指標前進,使其指向該 Vector 引數之後的 location。執行此語句後的執行時堆疊如下所示:[4]

Unnamed parameters (conpy constructor)


      existing_vec = va_arg(ap, Vector);

在這裡,我們檢索使用者傳遞的函式指標。每次我們需要複製或銷燬元件時,我們將回調到在給定地址找到的函式。

      ret_vec->copy_component = existing_vec->copy_component;
      ret_vec->destroy_component = existing_vec->destroy_component;
      ret_vec->cap = ret_vec->cur_size = existing_vec->cur_size;
      ret_vec->cap_inc = existing_vec->cap_inc;

除了 malloc,我們也可以使用 calloc 從堆中分配一塊記憶體區域。該函式分配足夠大的記憶體來容納 n(作為第一個引數傳遞)個元素,每個元素的大小為 m(作為第二個元素傳遞);它返回指向陣列第一個元素的指標。這當然可以透過 malloc 來完成。作為額外功能,返回的區域將進行位清零操作,而 malloc 不會進行此操作。

使用 calloc 分配的記憶體可以使用 freecfree 返回。但請記住,儘管 cfree 非常普遍,但它並不是標準的 C 函式。

問題
我們可以使用 calloc 分配記憶體來儲存一個初始值為 0 的 int 嗎?對於初始值為 0.0 的 float 呢?你能保證答案總是相同嗎?

float *f = (float *) calloc(1, sizeof(float)); /* *f 中包含的值是什麼?它是 0.0 嗎? */ int *i = (int *) calloc(1, sizeof(int)); /* *i 中包含的值是什麼?它是 0 嗎? */

      ret_vec->container = calloc(existing_vec->cur_size, sizeof(Object));
      for(i = 0; i < existing_vec->cur_size; i++) 
        ret_vec->container[i] = existing_vec->copy_component(existing_vec->container[i]);
      break;
    case DEFAULT:
      comp_funcs = va_arg(ap, Vector_Component_Funcs);
      ret_vec->copy_component = comp_funcs.copy_component;
      ret_vec->destroy_component = comp_funcs.destroy_component;
      ret_vec->cap = ret_vec->cap_inc = 0;
      break;
    case INIT_CAP:
      comp_funcs = va_arg(ap, Vector_Component_Funcs);
      ret_vec->copy_component = comp_funcs.copy_component;
      ret_vec->destroy_component = comp_funcs.destroy_component;


Unnamed parameters (with initial capacity)


      ret_vec->cap = va_arg(ap, unsigned int);
      ret_vec->cap_inc = 0;
      if (ret_vec->cap <= 0) break; 
      ret_vec->container = calloc(ret_vec->cap, sizeof(Object));
      break;
    case BOTH: 
      comp_funcs = va_arg(ap, Vector_Component_Funcs);
      ret_vec->copy_component = comp_funcs.copy_component;
      ret_vec->destroy_component = comp_funcs.destroy_component;
      ret_vec->cap = va_arg(ap, unsigned int);


Unnamed parameters (with both initial capacity and capacity increment)


      ret_vec->cap_inc = va_arg(ap, unsigned int);
      if (ret_vec->cap <= 0) break; 
      ret_vec->container = calloc(ret_vec->cap, sizeof(Object));
      break;
  } /* end of switch(type) */

va_endap 執行必要的清理操作。換句話說,它充當解構函式的作用。在使用 va_arg 讀取所有引數後,程式設計師應該呼叫它。

  va_end(ap);

  return(ret_vec);
問題
以下哪種記憶體佈局更適合實現可變引數函式?
問題
在 C 中,您認為誰負責從執行時堆疊中移除引數,是呼叫方還是被呼叫方?
} /* end of Vector Vector_Create(Constructor_type, struct basic_funcs, ...) */

下面給出了 Vector 物件的結構。

插入圖片

使用者透過控制代碼(圖中的這個變數)訪問 Vector 物件。表示是隱藏的,它只能透過 Vector 公共介面中的函式來修改。

請注意,控制代碼是指向 Vector 的屬性,而不是指向其元件。這是實現集合型別時採用的典型方法。您擁有某些元資料的控制代碼,這些元資料(除了定義集合的屬性外)還擁有用於儲存元件的容器的控制代碼。

int Vector_Destroy(Vector* this) {
  int i;
  Vector this_copy = *this;

  if (this_copy == NULL) return(NON_EXISTING_VECTOR);

呼叫 Vector_Destroy 函式並不意味著底層物件將被銷燬。在多個控制代碼共享某個物件的情況下,引用計數將遞減,而底層物件保持不變。如果引用計數為 1,我們將切斷物件與其最後一個控制代碼之間的聯絡。在這種情況下,我們還必須銷燬底層物件!

  if(this_copy->ref_count > 1) {
    *this = NULL;
    return(--this_copy->ref_count);
  } /* end of if(this_copy->ref_count > 1) */

請注意,destroy_component 是指向函式的指標。也就是說,函式是透過指標呼叫的;我們可以透過分配不同的指標值來更改被呼叫的函式。這實際上賦予了我們處理不同資料型別的能力。[8]

  for (i = this_copy->cur_size - 1; i >= 0; i--)
    this_copy->destroy_component(this_copy->container[i]));

  free(this_copy->container); free(this_copy);
  *this = NULL;

  return 0;
} /* end of int Vector_Destroy(Vector*) */

Object Vector_ElementAt(const Vector this, unsigned int pos) {
  Object ret_object;

  if (pos >= this->cur_size) return NULL;

請注意,我們返回了所需元件的副本,而不是指向它的指標。這樣做的理由是避免對底層資料結構進行任何訪問。

  ret_object = this->copy_component(this->container[pos]);

  return(ret_object);
} /* end of Object Vector_ElementAt(const Vector, unsigned int) */

下一個函式將一個 Vector 物件分配給另一個物件。由於 C 中沒有運算子過載,我們必須想出一個函式名稱,並使用這個名稱來執行賦值。當然,我們仍然可以使用 '=' 來做到這一點。但是,這樣做會導致不一致的狀態,即引用計數和共享底層物件的控制代碼數量不一致。

Vector v1, v2; v1 = Vector_Create(DEFAULT, ...); Vector_InsertAt(v1, ...); ...; ...; Vector_InsertAt(v1, ...); ... ... Vector_Assign(&v2, v1); ...

上面的程式碼片段將生成以下圖片。

插入圖片
void Vector_Assign(Vector* lhs, const Vector rhs{
  Vector_Destroy(lhs);
  *lhs = rhs;
  rhs->ref_count++;
} /* end of void Vector_Assign(Vector*, const Vector) */

Object Vector_InsertAt(const Vector this, Object new_item, unsigned int pos) {
  if (this->cur_size < pos) pos = this->cur_size;

在進行插入之前,我們必須確保結構中有足夠的空間。如果沒有,我們會根據一些預定義的公式調整容器容量。如果堆記憶體不足,我們不會對結構進行任何更改,只需從函式中返回 NULL 值。否則,我們會擴充套件容器所需的記憶體區域並繼續插入:如果新的元件插入到末尾,則將其複製到向量的末尾,由 cur_size 欄位指示;如果新的元件插入到末尾以外的位置,則將緊隨新專案位置之後的元件向下移動一位,然後進行插入。

  if (this->cap == this->cur_size)
  if (!adjust_capacity(this)) return NULL;
  if (pos != this->cur_size) shift_down(this, pos);
  this->container[pos] = this->copy_component(new_item);
  this->cur_size++;

  return new_item;
} /* end of Object Vector_InsertAt(const Vector, Object, unsigned int) */

Object Vector_RemoveAt(const Vector this, unsigned int pos) {
  Object ret_object;

  if (pos >= this->cur_size) return NULL;
  ret_object = *(this->container + pos);
  if (pos != this->cur_size - 1) shift_up(this, pos);
  this->cur_size--;

  return(ret_object);
} /* end of Object Vector_RemoveAt(const Vector, unsigned int) */

Object Vector_UpdateAt(const Vector this, Object new_item, unsigned int pos) {
  Object old_value;

  if (pos >= this->cur_size) return NULL;

  old_value = *(this->container + pos);
  this->container[pos] = this->copy_component(new_value);

  return old_value;
} /* end of Object Vector_UpdateAt(const Vector, Object, unsigned int)*/

unsigned int Vector_Size(const Vector this) { return(this->cur_size); }

adjust_capacity 函式根據給定的公式擴充套件 Vector 的容量。在我們的實現中,我們採用大多數 C++ 編譯器中使用的公式:如果 Vector 具有非零的容量增量值,則容量將增加此值;如果使用者未為此欄位提供任何值或其值為零,則新容量將透過將先前容量加倍來計算。(如果當前容量值為零,則新容量將取為 1。)如果無法為 Vector 物件分配足夠的儲存空間,則將返回 FALSE 值。

static BOOL adjust_capacity(const Vector this) {
  int i;
  const Vector old_this = (Vector)  malloc(sizeof(struct _VECTOR));
  if(!old_this) {
    fprintf(stderr, "Out of memory...\n");
    return FALSE;
  } /* end of if(!old_this) */
  memcpy(old_this, this, sizeof(struct _VECTOR));

  if (this->cap_inc != 0) this->cap += this->cap_inc;
  else if (this->cap == 0) this->cap = 1;
    else this->cap *= 2;
  this->container = calloc(this->cap, sizeof(Object));

  if (!this->container) {
    this->cap = old_this->cap;
    this->container = old_this->container;
    free(old_this);
    fprintf(stderr, "Out of memory...\n");
    return FALSE;
  } /* end of if(!this->container) */

  if (old_this->cap == 0) {
    free(old_this);
    return TRUE;
  } /* end of if(old_this->cap == 0) */
  memcpy(this->container, old_this->container, 
  sizeof(Object) * old_this->cur_size);

  free(old_this->container);
  free(old_this);
  return TRUE;
} /* end of BOOL adjust_capacity(const Vector) */

只要在新專案插入到 Vector 末尾以外的任何位置,都需要向下移位。

static void shift_down(const Vector this, unsigned int pos) {
  unsigned int i;
  for(i = this->cur_size; i > pos; i--)
    memcpy(this->container + i, this->container + (i - 1),sizeof(Object));
} /* end of void shift_down(const Vector, unsigned int) */

類似地,只要從 Vector 末尾以外的任何位置刪除專案,都需要向上移位。

static void shift_up(const Vector this, unsigned int pos) {
  unsigned int i;
  for(i = pos; i < this->cur_size - 1; i++)
    memcpy(this->container + i, this->container + (i + 1), sizeof(Object));
} /* end of void shift_up(const Vector, unsigned int) */

測試程式

[edit | edit source]
Vector_Test.c
#include <stdio.h>

#include "General.h"
#include "Wrappers.h"
#include "utility/Vector.h"

enum {CREATE_COPY = 1, CREATE_DEFAULT, DESTROY, INSERT = 6, PEEK, REMOVE, UPDATE, DISPLAY, EXIT = 0};

Vector v[2];

void display_vec(unsigned int which) {
  unsigned int i;

  if (!v[which]) { 
    printf("-----\n");
    return; 
  } /* end of if(!v[which]) */
  printf("+++++\n");
  printf("Contents of vector #%d\n", which);
  for (i = 0; i < Vector_Size(v[which]); i++) 
    printf("#%d: %d\t", i, Integer_Value(Vector_ElementAt(v[which], i)));
  printf("\n");

  return;
} /* end of void display_vec(unsigned int) */

int main(void) {
  int val;
  Integer pval;
  unsigned int action, pos, which_vec, from_which_vec;
  Vector_Component_Funcs int_funcs = {&Integer_CopyInteger, &Integer_DestroyInteger};

  do {
    scanf("%d", &action);
    switch (action) {
      case CREATE_COPY:
        scanf("%d %d", &which_vec, &from_which_vec);
        printf("Trying to create a copy of vector#%d into vector#%d...", from_which_vec, which_vec);
        v[which_vec] = Vector_Create(COPY, v[from_which_vec]);
        if (v[which_vec]) printf("+++++\n");
          else printf("-----\n");
        break;
      case CREATE_DEFAULT:
        scanf("%d", &which_vec);
        printf("Trying to create vector #%d using default constructor...", which_vec);
        v[which_vec] = Vector_Create(DEFAULT, int_funcs );
        if (v[which_vec]) printf("+++++\n");
          else printf("-----\n");
        break;
      case DESTROY:
        scanf("%d", &which_vec);
        printf("Trying to destroy vector#%d...", which_vec);
        if (!Vector_Destroy(&v[which_vec])) printf("+++++\n");
          else printf("-----\n");
        break;
      case INSERT:
        scanf("%d %d %d", &which_vec, &pos, &val);
        printf("Trying to insert %d at position %d of vector#%d...", val, pos, which_vec);

請注意,我們插入 Integer 物件(它們基本上是包裝在 int 周圍的不透明型別物件),而不是 int。這是透過 void* 提供的泛型性的結果,它需要插入指標。

        pval = Integer_Create(val);
        if (Vector_InsertAt(v[which_vec], pval, pos)) printf("+++++\n");
          else printf("-----\n");
        break;


Partial memory layout before Vector_InsertAt is called


Partial memory layout after insertion (in Vector_InsertAt)


      case REMOVE:
        scanf("%d %d", &which_vec, &pos);
        printf("Trying to remove the item at position %d from vector#%d...", pos, which_vec);

現在,返回的值 (ret_obj) 將被分配給 pval,我們將透過 pval 擁有已刪除物件的控制代碼。

Partial memory layout before Vector_RemoveAt is called


Partial memory layout after removal (in Vector_RemoveAt)


        pval = Vector_RemoveAt(v[which_vec], pos);
        if (pval) printf("+++++%d\n", Integer_Value(pval));
          else printf("-----\n");
        break; 
      case PEEK:
        scanf("%d %d", &which_vec, &pos);
        printf("Trying to peek in vector#%d at position %d...", which_vec, pos);


Partial memory layout after peeking (in Vector_ElementAt)


請意識到物件被克隆然後返回。這個新物件在分配給 pval 後,可以獨立於原始副本進行操作。

        pval = Vector_ElementAt(v[which_vec], pos);
        if (pval) printf("+++++%d\n", Integer_Value(pval));
          else printf("-----\n");
        break;
      case UPDATE:
	scanf("%d %d %d", &which_vec, &pos, &val);
	printf("Trying to update position %d of vector#%d with %d...", pos, which_vec, val);
	pval = Integer_Create(val);
	if (Vector_UpdateAt(v[which_vec], pval, pos))
          printf("+++++\n");
          else printf("-----\n");
	break;
      case DISPLAY:
        scanf("%d", &which_vec);
        printf("Trying to display vector#%d...", which_vec);
        display_vec(which_vec);
        break;
      case EXIT:
        exit(0);
    } /* end of switch(action) */
  } while (1);
} /* end of int main(void) */

執行測試程式

[edit | edit source]

(使用 Cygwin)

gcc –c –ID:/include Vector.c↵
gcc –o Vector_Test.exe Vector_Test.c Vector.o –lWrappers –ID:/include –LD:/library↵

上面的命令將把在名為 libWrappers.a 的 [靜態] 庫中找到的必需目標檔案帶入可執行檔案。要使用的庫的名稱是透過簡單地在 –l 開關提供的檔名之前加上 "lib" 並附加 ".a" 來確定的。對該庫的搜尋將使用 –L 開關傳遞的目錄。

Vector_Test < Vector_Test.input > Vector_Test.output↵

此命令將把輸入和輸出重定向到磁碟檔案。它仍然會讓程式認為它正在從鍵盤接收輸入並將輸出傳送到螢幕。這可以透過將 stdin 和 stdout 重新對映到不同的物理檔案來實現。如果沒有重新對映,系統中的所有程序都將具有以下初始檔案表

每個程序開啟的檔案表
物理檔案 其他欄位
0 鍵盤 ...
1 螢幕 ...
2 螢幕 ...

重新對映後,我們有

重定向後每個程序開啟的檔案表
物理檔案 其他欄位
0 Vector_Test.input ...
1 Vector_Test.output ...
2 螢幕 ...

在重新對映之前和之後,我們都有以下宏定義

#define stdin 0 #define stdout 1 #define stderr 2

這一切都是由命令列直譯器完成的。使用者不需要在原始碼中進行任何修改;(他)她仍然可以像在鍵盤上輸入輸入並將輸出寫入螢幕一樣進行程式設計。這是因為我們透過控制代碼(邏輯檔案)讀取或寫入物理檔案。如果我們更改控制代碼對映到的物理檔案,即使我們寫入同一個邏輯檔案(控制代碼),最終受到影響的目標也會發生變化。

Vector_Test.input

2 0 6 0 10 5 6 0 10 15 6 0 8 20 6 0 8 25 6 0 2 30 6 0 0 35 10 0 8 0 2 8 0 3 8 0 100 10 0 1 1 0 8 1 1 7 1 0 9 1 2 26 10 1 7 0 2 7 0 5 10 0 3 0 10 0 0

Vector_Test.output

Trying to create vector #0 using default constructor...+++++ Trying to insert 5 at position 10 of vector#0...+++++ Trying to insert 15 at position 10 of vector#0...+++++ Trying to insert 20 at position 8 of vector#0...+++++ Trying to insert 25 at position 8 of vector#0...+++++ Trying to insert 30 at position 2 of vector#0...+++++ Trying to insert 35 at position 0 of vector#0...+++++ Trying to display vector#0...+++++ Contents of vector #0 #0: 35 #1: 5 #2: 15 #3: 30 #4: 20 #5: 25 Trying to remove the item at position 2 from vector#0...+++++15 Trying to remove the item at position 3 from vector#0...+++++20 Trying to remove the item at position 100 from vector#0...----- Trying to display vector#0...+++++ Contents of vector #0 #0: 35 #1: 5 #2: 30 #3: 25 Trying to create a copy of vector#0 into vector#1...+++++ Trying to remove the item at position 1 from vector#1...+++++5 Trying to peek in vector#1 at position 0...+++++35 Trying to update position 2 of vector#1 with 26...+++++ Trying to display vector#1...+++++ Contents of vector #1 #0: 35 #1: 30 #2: 25 Trying to peek in vector#0 at position 2...+++++30 Trying to peek in vector#0 at position 5...----- Trying to display vector#0...+++++ Contents of vector #0 #0: 35 #1: 5 #2: 30 #3: 25 Trying to destroy vector#0...+++++ Trying to display vector#0...-----

物件模組庫

[編輯 | 編輯原始碼]

無論你是否使用它們,與物件模組靜態連結都會引入模組中的所有功能。這意味著你的程式中從未用到的程式碼也將存在,從而增加可執行檔案的大小。可以透過將模組內容拆分成更小的塊來避免這種情況。使用這些更小的模組可能會減小可執行檔案的大小,但現在你面臨另一個問題:必須在命令列上單獨寫入物件模組的名稱。[9]

庫提供了兩個世界相結合的方案。[10] 與庫連結意味著單個模組內的所有外部實體都是可見的。但是,只有使用的模組才會被引入可執行檔案。從庫中的一個模組切換到另一個模組不會強迫你更改命令列。連結器和庫管理器會處理所有細節。

示例:假設 libWrappers.a 包含 Integer.o、Char.o、Short.o 等。在使用過 Integer.o 中的一些功能後,以下命令將導致僅引入 Integer.o。

gcc –o Vector_Test.exe Vector_Test.c Vector.o –lWrappers –ID:/include –LD:/library↵

如果我們稍後決定使用某個來自 Char.o 的函式,我們不需要在命令列上進行任何修改。連結器-庫管理器對將為我們處理細節,並僅引入 Char.o 和 Integer.o。

定義:一個是物件模組的存檔,它除了模組外,通常還包含一些元資料,以便更快地定位各個檔案。

庫標頭檔案

[編輯 | 編輯原始碼]

Wrappers.h 包含封裝基本型別資料和解封裝封裝資料的所需前向宣告和原型。請注意,可以透過將相關資訊放在單獨的標頭檔案中來完成相同的事情。沒有規定說必須為整個庫提供單個頭檔案。事實上,你可以為庫中的每個物件模組[甚至每個函式]提供單獨的標頭檔案。但是,選擇為每個物件模組提供單獨的標頭檔案有一個缺點:當你程式碼中新增一個新的函式呼叫時,你必須弄清楚它來自哪個標頭檔案,並[可能]新增一個相關的包含指令,這相當於在命令列上編寫多個物件檔案的問題的翻版。

在某些情況下,在兩個極端之間取得平衡可能是一個更好的選擇。例如,一個提供數十甚至數百個函式的數學庫。在這種情況下,篩選單個頭檔案無疑是一個費力的過程。為數學庫的不同方面提供多個頭檔案可能是一個好主意。

Wrappers.h
#ifndef WRAPPERS_H
#define WRAPPERS_H

#include "General.h"

由於我們的泛型向量期望一個不透明指標作為其元件,因此使用它來處理基本資料可能會很痛苦:你必須先封裝資料,並在你的交易中使用指向這個封裝資料的指標。稍後當你想要將資料列印到某個介質時,你必須進行一些解封裝——即檢索不透明指標指向的區域的內容——然後列印資料。關於比較、複製和銷燬資料,也可以說類似的事情。

這種模式非常常見,因此提供了一個特殊的庫(libWrappers.a)來方便使用者。如果他可能想要使用 Vector 模組和類似模組來處理基本資料型別,他不必重新發明輪子;他只需使用 libWrappers.a 中找到的函式即可。

整個事情可以比作 Java 中的包裝類和 C# 中的自動裝箱-拆箱的概念。事實上,這種使用容器的方式如此頻繁,以至於 Java 從 1.5 版本開始支援自動裝箱-拆箱。

struct _CHAR;
typedef struct _CHAR* Char;
extern Char Char_Create(unsigned char val);
extern void Char_Destroy(Char* this);

extern int Char_CompareChars(Object, Object);
extern Object Char_CopyChar(Object);
extern void Char_DestroyChar(Object);

extern unsigned char Char_Value(Char wrappedChar);

struct _SCHAR;
typedef struct _SCHAR* SChar;
extern SChar SChar_Create(signed char val);
extern void SChar_Destroy(SChar* this);

extern int SChar_CompareSChars(Object, Object);
extern Object SChar_CopySChar(Object);
extern void SChar_DestroySChar(Object);

extern signed char SChar_Value(SChar wrappedSChar);

struct _INTEGER;
typedef struct _INTEGER* Integer;
extern Integer Integer_Create(signed int val);
extern void Integer_Destroy(Integer* this);

extern int Integer_CompareIntegers(Object, Object);
extern Object Integer_CopyInteger(Object);
extern void Integer_DestroyInteger(Object);

extern signed int Integer_Value(Integer wrappedInt);

struct _UINTEGER;
typedef struct _UINTEGER* UInteger;
extern UInteger UInteger_Create(unsigned int val);
extern void UInteger_Destroy(UInteger* this);

extern int UInteger_CompareUIntegers(Object, Object);
extern Object UInteger_CopyUInteger(Object);
extern void UInteger_DestroyUInteger(Object);

extern unsigned int UInteger_Value(UInteger wrappedUInt);

struct _LONG;
typedef struct _LONG* Long;
extern Long Long_Create(signed long val);
extern void Long_Destroy(Long* this);

extern int Long_CompareLongs(Object, Object);
extern Object Long_CopyLong(Object);
extern void Long_DestroyLong(Object);

extern signed long Long_Value(Long wrappedLong);

struct _ULONG;
typedef struct _ULONG* ULong;
extern ULong ULong_Create(unsigned long val);
extern void ULong_Destroy(ULong*);

extern int ULong_CompareULongs(Object, Object);
extern Object ULong_CopyULong(Object);
extern void ULong_DestroyULong(Object);

extern unsigned long ULong_Value(ULong wrappedULong);

struct _SHORT;
typedef struct _SHORT* Short;
extern Short Short_Create(signed short val);
extern void Short_Destroy(Short*);

extern int Short_CompareShorts(Object, Object);
extern Object Short_CopyShort(Object);
extern void Short_DestroyShort(Object);

extern signed short Short_Value(Short wrappedShort);

struct _USHORT;
typedef struct _USHORT* UShort;
extern UShort UShort_Create(unsigned short val);
extern void UShort_Destroy(UShort*);

extern int UShort_CompareUShorts(Object, Object);
extern Object UShort_CopyUShort(Object);
extern void UShort_DestroyUShort(Object);

extern unsigned short UShort_Value(UShort wrappedUShort);

struct _DOUBLE;
typedef struct _DOUBLE* Double;
extern Double Double_Create(double val);
extern void Double_Destroy(Double*);

extern void Double_DestroyDouble(Object);
extern int Double_CompareDoubles(Object, Object);
extern Object Double_CopyDouble(Object);

extern double Double_Value(Double wrappedDouble);

struct _FLOAT;
typedef struct _FLOAT* Float;
extern Float Float_Create(float val);
extern void Float_Destroy(Float*);

extern void Float_DestroyFloat(Object);
extern int Float_CompareFloats(Object, Object);
extern Object Float_CopyFloat(Object);

extern float Float_Value(Float wrappedFloat);

#endif

一個示例模組

[編輯 | 編輯原始碼]

以下是型別 int 的包裝器介面的實現。可以類似地提供其他基本型別的包裝器,這些包裝器將不包括在本講義中。

Integer.c
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

#include "Wrappers.h"

struct _INTEGER { signed int value; };

Integer Integer_Create(signed int val) {
  Integer this = (Integer) malloc(sizeof(struct _INTEGER));
  if (!this) {
    fprintf(stderr, "Out of memory...\n");
    return NULL;
  } /* end of if(!this) */
  this->value = val;

  return this;
} /* Integer Integer_Create(signed int) */

void Integer_Destroy(Integer* this) {
  Integer this_copy = *this;
  if (this_copy == NULL) return;

  free(this_copy);
  *this = NULL;
} /* end of void Integer_Destroy(Integer*) */

void Integer_DestroyInteger(Object this) {
  Integer_Destroy((Integer*) &this);
} /* void Integer_DestroyInteger(Object) */

int Integer_CompareIntegers(Object this, Object rhs) {
  signed int thisValue = ((Integer) this)->value;
  signed int rhsValue = ((Integer) rhs)->value;

  if (thisValue == rhsValue) return 0;
  else if (thisValue < rhsValue) return -1;
    else return 1;  
} /* int Integer_CompareIntegers(Object, Object)*/

Object Integer_CopyInteger(Object this) {
  Integer retVal = Integer_Create(((Integer) this)->value);

  return retVal;
} /* Object Integer_CopyInteger(Object) */

signed int Integer_Value(Integer wrappedInt) {
  return wrappedInt->value;
} /* signed int Integer_Value(Integer) */

構建庫

[編輯 | 編輯原始碼]

使用 Cygwin

在構建庫之前,我們需要建立要放入庫中的各個物件模組。

gcc –c Char.c –ID:/include
gcc –c Integer.c –ID:/include
...
...

如果所有原始檔都在同一個目錄中,並且該位置不存在其他檔案,則可以一步完成。

gcc –c *.c –ID:/include

現在我們可以建立庫——或 Unix 中的歸檔檔案——並透過以下命令將物件模組追加到它。這將把所有以 .o 結尾的檔案放到 libWrappers.a 中。

ar qc libWrappers.a *.o # 有關 ar 的更多資訊,請在命令列中鍵入“man ar”。

如果你不確定,你可能想列出庫的內容。

ar t libWrappers.a
Char.o
Double.o
...
...
UShort.o

現在 libWrappers.a 已經構建完畢,我們可以將其放置在某個固定位置,並像在測試程式中那樣使用庫中的函式。

使用 Windows

在發出以下命令之前,請確保命令列工具已作為外部命令提供。這可以透過執行[包含在]名為 vcvars32.bat 的批處理檔案中找到的命令來輕鬆完成,該批處理檔案位於 Microsoft Visual C/C++ 編譯器的 bin 子目錄中。

cl /ID:/include /c /TC Char.c
cl /ID:/include /c /TC Integer.c
...
...

前面的行建立了構建庫所需的 Object 檔案。雖然它們相當簡短,但它們強調了我們使用的是編譯器驅動程式,而不是普通編譯器。為了成功引入所需的標題,我們使用 /I 選項將預處理器資訊傳遞給預處理器,告訴它在哪些地方查詢標題。由於使用了 /c,我們還告訴編譯器驅動程式在連結之前停止。最後,在我們這個示例中並不真正需要,透過使用 /TC,我們告訴適當的編譯器將命令列上傳遞的檔案視為 C 原始碼。

lib /OUT:Wrappers.lib Char.obj Integer.obj ... # 構建庫
lib /LIST Wrappers.lib # 列出庫內容
Microsoft (R) Library Manager Version 7.10.3077
Copyright ( C ) Microsoft Corporation. All rights reserved.
UShort.obj
ULong.obj
...
...
Char.obj

lib 是 Windows 中 ar 的對應物,是庫管理器命令。透過傳遞不同的選項,我們可以讓它構建靜態庫、列出庫內容等。

cl /ID:/include /FeVector_Test.exe Vector.obj Wrappers.lib Vector_Test.c # 要獲取有關選項的幫助,請將 /? 傳遞給 cl
/D_CRT_SECURE_NO_DEPRECATE /link /NODEFAULTLIB:libc.lib /DEFAULTLIB:libcmt.lib /LIBPATH:D:/library[11]

另一個令人信服的證明,即 cl 實際上是一個編譯器驅動程式!為了獲得 Vector_Test.exe;預處理和編譯 Vector_Test.c 並將生成的 Object 檔案與 Vector.obj 和來自 Wrappers.lib 的所需 Object 檔案連結,這些檔案可以透過使用 /LIBPATH 選項傳遞給連結器,從給定位置開始搜尋找到。

  1. 隨著繼承的引入,同一個容器最終可能包含屬於不同但相容型別項。
  2. 但是,這並不意味著我們不能使用其他技術。
  3. 傳統 C 允許沒有命名引數的可變引數函式,而標準 C 至少有一個命名引數。
  4. 請注意,existing_vec 是一個區域性變數,因此應該出現在圖的下部,準確地說是在 ap 下方。標有 existing_vec 的插槽實際上代表對應於第二個引數的未命名形式引數。
  5. 這可以作為編譯器可能選擇不按從左到右的順序計算實際引數的一個論據。實際上,從右到左的計算似乎是更好的選擇。畢竟,最右邊的引數是第一個被壓入執行時堆疊的。
  6. 在瀏覽 Windows 原始碼時,你可能會看到一些函式名字首有諸如 __cdecl(C 呼叫約定)、__stdcall(標準呼叫約定)或 __pascal 之類的指定符。這樣的指定符用於說明函式呼叫中使用的排列型別、命名約定和清理協議。__cdecl 用於可變引數函式,而 __stdcall 用於其他函式。
  7. 實際上,刪除引數通常只是透過一定數量調整堆疊指標。
  8. 還應該新增透過 void* 提供的泛型性。
  9. 如果使用視覺化工具,則意味著將目標檔案新增到專案中。
  10. 庫可以是靜態的或動態的,具體取決於與可執行檔案的連結時間。前者在連結時引入目標模組,而後者則推遲到載入或執行時。在本講義中的演示將僅限於靜態庫。
  11. 2005 年以後的 MS Visual Studio 命令列反映了微軟編譯器的非標準方面:為了使 C 成為更安全的程式語言,編譯器透過警告向程式設計師建議使用 scanf_s(這是一個微軟獨有的函式),而不是標準的 scanf。我們透過定義 _CRT_SECURE_NO_DEPRECATE 宏來忽略這一點。從 MS Visual Studio 2005 開始,微軟還從其發行版中刪除了 libc.lib - 單執行緒靜態 C 執行時。現在與可執行檔案連結的預設 C 執行時庫是 libcmt.lib,它是 libc.lib 的多執行緒版本。然而,出於某種原因,連結器可能仍然最終會尋找 libc.lib!為了解決這個明顯的缺陷,使用 /NODEFAULTLIB 選項,我們從要搜尋的庫列表中刪除 libc.lib,並在解決外部引用過程中,使用 /DEFAULTLIB 選項,在其位置新增 libcmt.lib。因此,在 2005 年以前的 MS Visual Studio 中,此命令列應替換為以下內容。
    cl /ID:/include /FeVector_Test.exe Vector.obj Wrappers.lib Vector_Test.c /link /LIBPATH:D:/library
華夏公益教科書