目錄表

國立屏東大學 資訊工程學系 程式設計(二)

15. 使用者自定資料型態


15.1 Structures

我們可以將相關的資料項目集合起來,定義為一個Structure(結構體)。從程式的角度來看,所謂的結構體是一個包含有多個變數的資料集合,可用以宣告變數–稱為結構體變數,進行相關的程式處理。

15.1.1 結構體變數

結構體變數的宣告在語法上以「struct{ … } structVarName;」定義一個結構體,其中可包含一個或一個以上的欄位(field)定義。每個欄位定義其實就是一個變數的宣告,不可包含初始值的給定,其語法結構如下:

struct
{
   [type fieldName;]+
} structVarName[,structVarName]*;

例如,下面的程式碼片段定義了兩個平面上的點,每個點包含有x與y軸座標:

struct
{
   int x;
   int y;
} p1, p2;

當然,你也可以這樣寫:

struct
{
   int x,y;
} p1, p2;

在上述程式碼片段中,p1與p2為所宣告的結構體的變數,我們可以p1.x,p1.y,p2.x與p2.y來存取這些結構體中的欄位(變數)。下面提供另一個例子:

struct
{
   int  productNo;
   char *productName;
   float price;
   int   quantity; 
} mfone;

這個例子宣告了一個名為mfone的結構體變數,其中包含有產品編號、名稱、單價與庫存數量。現在讓我們來看看在記憶體中的空間配置,如figure 1

Fig. 1

現在請同學動動腦,想想看在前述例子中的p1, p2與mfone各佔用了多少的記憶體空間呢?

<note>

結構體所佔用的記憶體空間

</note>

<這邊要進行一些討論…structdef.c>

關於結構體的變數其初始值可以在結構變數宣告時以「={ … }」方式,依欄位的順序進行給定,請參考下面的例子:

struct
{
   int x;
   int y;
} p1 = {0,0},
  p2 = {100, 100};
 
struct
{
   int  productNo;
   char *productName;
   float price;
   int   quantity; 
} mfone = {12, "mPhone 6s", 15000, 100};  

從C99開始,還支援了指定初始子(designated initializers)的方法,讓我們可以在宣告其初始值的同時,給定欄位的名稱,且不受原本順序的限制,請參考下例:

struct
{
   int x;
   int y;
} p1 = {.x=0, .y=0},
  p2 = {.y=100, .x=100};
 
struct
{
   int  productNo;
   char *productName;
   float price;
   int   quantity; 
} mfone = {.productName="mPhone 6s", .price=15000, .quantity=100}; //productNo並沒給定初始值  

15.1.2 結構體變數的操作

結構體變數的操作相當簡單,你可以「結構體變數名稱.欄位名稱」來存取其值,例如:

struct
{
   int x;
   int y;
} p1 = {0,0},
  p2 = {100, 100};
 
p1.x = 100;
 
float z;
z = sqrt(p1.x*p1.x + p1.y*p1.y);
 
p2.x+=5;
 
scanf("%d", &p1.x);

但結構體變數最方便的地方在於可以使用「=」運算子,讓兩個結構體間可以互相給定其值,包含了其中所有的欄位,甚至也包含了字串的值。請參考下面的程式:

struct
{
   int  productNo;
   char *productName;
   float price;
   int   quantity; 
} mfone = {12, "mPhone 6s", 15000, 100},
  mfone2;  
 
mfone2=mfone;

15.1.3 結構體型態

我們也可以將單獨地宣告結構體,而不用同時將其變數加以宣告。這樣做有很多的好處,尤其是當程式碼需要共享時,我們可以將結構體的宣告獨立於某個檔案,爾後其它程式可以直接以該定義來宣告其變數,其語法如下:

struct structName
{
   [type fieldName;]+
};

有了先宣告好的結構體之後,就可以在需要時再宣告之變數,請參考下面的程式:

struct point
{
   int x;
   int y;
};
 
 
struct point p1, p2;
struct point p3 ={6,6};

當然,你也可以這樣寫:

struct point
{
   int x,y;
} p1, p2;
 
struct point p3;

另一種方式,則是直接將結構體定義為一種新的資料型態,其語法如下:

struct structName
{
   [type fieldName;]+
};
 
typedef struct structName typeName;
 
typedef struct
{
   [type fieldName;]+
} typeName;

如此一來,我們就可以利用這個新的資料型態來宣告變數,請參考下面的程式:

struct point
{
   int x,y;
};
 
typedef struct point Point;

或是

typedef struct
{
   int x,y;
} Point;

接著你就可以在需要的時候,以「Point」做為資料型態的名稱來進行變數的宣告,例如:

#include <stdio.h>
 
struct point
{
  int x,y;
};
 
typedef struct point Point;
 
int main()
{
  Point p1;
  Point p2 = {5,5};
 
  p1=p2;
  p1.x+=p1.y+=10;
 
  printf("p1=(%d,%d) p2=(%d,%d)\n", p1.x, p1.y, p2.x, p2.y);
 
  return 0;
}


15.1.4 結構體與函式

在結構體與函式方面,我們將先探討以結構體變數做為引數的方法,請參考下面的例子:

#include <stdio.h>
 
struct point
{
  int x,y;
};
 
typedef struct point Point;
 
void showPoint(Point p)
{
  printf("(%d,%d)\n", p.x, p.y);
}
 
int main()
{
  Point p1;
  Point p2 = {5,5};
 
  p1=p2;
  p1.x+=p1.y+=10;
 
  showPoint(p1);
  showPoint(p2);
 
  return 0;
}

請看下一個程式,我們設計另一個函式,讓我們修改所傳入的Point的值:

#include <stdio.h>
 
struct point
{
  int x,y;
};
 
typedef struct point Point;
 
void resetPoint(Point p)
{
  p.x=p.y=0;
}
 
void showPoint(Point p)
{
  printf("(%d,%d)\n", p.x, p.y);
}
 
int main()
{
  Point p1;
  Point p2 = {5,5};
 
  p1=p2;
  p1.x+=p1.y+=10;
 
  showPoint(p1);
  showPoint(p2);
 
  resetPoint(p1);
  showPoint(p1);
 
  return 0;
}

請先猜猜看其執行結果為何?然後再實際執行看看其結果為何?

<在這裡要討論一下 … >

#include <stdio.h>
 
struct point
{
  int x,y;
};
 
typedef struct point Point;
 
void resetPoint(Point *p)
{
  p->x=p->y=0;
}
 
void showPoint(Point p)
{
  printf("(%d,%d)\n", p.x, p.y);
}
 
int main()
{
  Point p1;
  Point p2 = {5,5};
 
  p1=p2;
  p1.x+=p1.y+=10;
 
  showPoint(p1);
  showPoint(p2);
 
  resetPoint(&p1);
  showPoint(p1);
 
  return 0;
}

讓我們做一個總結:若採用call-by-reference的方式,將變數所在的記憶體位址傳入函式,那麼在函式中,做為一個指向記憶體中某個結構體的指標,其要存取結構體內的欄位時,須以「→」運算子為之。

當我們在呼叫某個函式時,也可以直接產生一個新的結構體做為引數,例如:

showPoint( (Point) {5,6}  );

我們也可以讓結構體做為函式的傳回值,請參考下面的例子:

#include <stdio.h>
#include <math.h>
 
struct point
{
  int x,y;
};
 
typedef struct point Point;
 
Point addPoints(Point p1, Point p2)
{
  Point p;
  p.x = p1.x + p2.x;
  p.y = p1.y + p2.y;
  return p;
}
 
void showPoint(Point p)
{
  printf("(%d,%d)\n", p.x, p.y);
}
 
int main()
{
  Point p1;
  Point p2 = {5,5};
  Point p3;
 
  p1=p2;
  p1.x+=p1.y+=10;
 
  showPoint(p1);
  showPoint(p2);
 
  p3=addPoints(p1,p2);
  showPoint(p3);
  return 0;
}

現在,讓我們看看下面這個程式:

#include <stdio.h>
 
struct point
{
  int x,y;
};
 
typedef struct point Point;
 
Point * addPoint(Point *p1, Point p2)
{
  p1->x = p1->x + p2.x;
  p1->y = p1->y + p2.y;
  return p1;
}
 
void showPoint(Point p)
{
  printf("(%d,%d)\n", p.x, p.y);
}
 
int main()
{
  Point *p1;
  Point p2 = {5,5};
  Point *p3;
 
  p1=&p2;
  p1->x += p1->y+=10;
 
  showPoint(*p1);
  showPoint(p2);
 
  p3=addPoint(p1,p2);
  showPoint(*p1);
  showPoint(*p3);
  return 0;
}

15.1.5 巢狀式結構體

我們也可以在一個結構體內含有另一個結構體做為其欄位,請參考下面的程式:

#include <stdio.h>
 
typedef struct { char firstname[20], lastname[20]; } Name;
 
typedef struct
{
  Name name;
  int phone;
} Contact;
 
void showName(Name n)
{
  printf("%s, %s", n.lastname, n.firstname);
}
 
void showContact(Contact c)
{
  showName(c.name);
  printf(" %d\n", c.phone);
}
 
int main()
{
  Contact someone={.phone=12345, .name={.firstname="Jun", .lastname="Wu"}};
 
  showContact(someone);
  return 0;
}

15.1.6 結構體陣列

我們也可以利用結構體來宣告陣列,請參考下面的片段:

Point a={3,5};
Point twoPoints[2];
Point points[10] = { {0,0}, {1,1}, {2,2}, {3,3}, {4,4}, {5,5}, {6,6}, {7,7}, {8,8}, {9,9} };
 
twoPoints[0].x=5;
twoPoints[0].y=6;
twoPoints[1] = a;

15.2 Unions

Union與結構體非常相像,但Union在記憶體內所保有的空間僅能存放一個欄位的資料,適用於某種資料可能有兩種以上不同型態的情況,例如: 一個在程式中會使用到的數值資料,在某些應用時是整數,但在另一些應用時可能會是浮點數。在這種情況下,我們可以宣告一個union來解決此問題:

union
{
   int i;
   double d;
} data;

當我們需要它是整數時,就使用其i的欄位;若需要它是浮點數時,就使用其d的欄位,例如:

#include <stdio.h>
 
union
{
  int i;
  double d;
} data;
 
 
int main()
{
  data.i=5;
  printf("%d %f\n", data.i, data.d );
 
  data.d=10.5;
  printf("%d %f\n", data.i, data.d );
 
  return 0;
}

請執行看看這個程式,想想看為何會有這樣的執行結果,也想想看這樣的工具可以用在哪?

當然,前面在結構體時可以應用的宣告方式,定義方式等都可以適用在union上,例如下面的程式碼:

union
{
   int i;
   double d;
} data = {0} // 只有第一個欄位可以有初始值

union
{
   int i;
   double d;
} data = {.d=3.14} // 可以指定其它的欄位做為初始值

union num
{
   int i;
   double d;
};
 
typedef union num Num;

typedef union
{
   int i;
   double d;
} Num;

15.3 Enumerations

我們可以用以下的宣告,來限制變數s1與s2可能的數值:

enum { SPADE, HEART, DIAMOND, CLUB } s1, s2;

也可以把enum定義好,然後再宣告變數:

enum suit { SPADE, HEART, DIAMOND, CLUB };
enum suit s1, s2;
或是
 
typedef enum { SPADE, HEART, DIAMOND, CLUB } Suit;
Suit s1,s2;

其實enum的實作是把列舉的數值視為整數,第一個數值視為0,第二個為1,依此類推。不過,我們也可以自行定義其數值:

enum suit { SPADE=1, HEART=13, DIAMOND=26, CLUB=39 };

15.4 綜合演練

請參考下面這個比較完整的例子:

#include <stdio.h>
 
typedef enum { Sunday=0, Monday=1, Tuesday=2, Wednesday=3, Thursday=4,
              Friday=5, Saturday=6, Undef=-1 } Weekday;
 
char weekdayString[][10] ={"Sunday", "Monday", "Tuesday", "Wednesday",
                          "Thursday", "Friday", "Saturday" };
 
Weekday getWeekday(char day[])
{
  Weekday i=Sunday;
 
  for(i=Sunday;i<=Saturday;i++)
  {
    if(strcmp(day, weekdayString[i])==0)
      return i;
  }
  return Undef;
}
 
int main()
{
  Weekday startDay, endDay;
  char today[10];
  int days;
 
  printf("What day (of the week) is it today? ");
  scanf(" %s", today);
 
  if((startDay = getWeekday(today))!=Undef)
  {
    printf("How many days after today? ");
    scanf(" %d", &days);
    endDay = (startDay+days)%7;
    printf("%d days later is %s.\n", days, weekdayString[endDay]);
    if(endDay==Sunday) // how about if(endDay==0)?
      printf("The end day is Sunday. We extend it to Monday!\n");
  }
  else
    printf("unknown weekday");
 
  return 0;
}

union也常和結構體一起使用,用以定義更有彈性的結構體,請參考下例:

#include <stdio.h>
#include <string.h>
 
typedef enum {BOOK, KEYCHAIN, T_SHIRT} Type;
enum color {red, gold, silver, black, blue, brown};
enum _size {XS, S, M, L, XL, XXL, XXXL};
typedef enum _size Size;
 
typedef struct {
  int stock_number;
  float price;
  Type type;
  union
  {
    char author[20];
    enum color color;
    Size size;
  } attribute;
} Product;
 
int main()
{
  Product littlePrince;
  Product bubbleBear;
  Product transformer;
 
  littlePrince.stock_number=10;
  littlePrince.price = 280.0;
  littlePrince.type = BOOK;
  strcpy(littlePrince.attribute.author, "Antoine");
 
  bubbleBear.stock_number=20;
  bubbleBear.price = 55.0;
  bubbleBear.type = KEYCHAIN;
  bubbleBear.attribute.color = gold;
 
  transformer.stock_number=23;
  transformer.price = 350.0;
  transformer.type = T_SHIRT;
  transformer.attribute.size = L;
 
  return 0;
}

下面這個例子,應用到了本章所介紹的相關方法:

#include <stdio.h>
#include <string.h>
 
typedef enum {BOOK, KEYCHAIN, T_SHIRT} Type;
typedef enum {red, gold, silver, black, blue, brown} Color;
char colorName[][10]={"red", "gold", "silver", "black", "blue", "brown"};
typedef enum {XS, S, M, L, XL, XXL, XXXL} Size;
char sizeName[][5]={"XS", "S", "M", "L", "XL", "XXL", "XXXL"};
 
typedef struct {
  int stock_number;
  float price;
  Type type;
  union
  {
    char author[20];
    Color color;
    Size size;
  } attribute;
} Product;
 
void showInfo(Product p)
{
  printf("Stock Number: %d\n", p.stock_number);
  printf("Price: %.2f\n", p.price);
  switch(p.type)
  {
    case BOOK:
      printf("Author: %s\n", p.attribute.author );
      break;
    case KEYCHAIN:
      printf("Color: %s\n", colorName[p.attribute.color]);
      break;
    case T_SHIRT:
      printf("Size: %s\n", sizeName[p.attribute.size]);
      break;
  }
  printf("\n");
}
 
int main()
{
  Product p[3];
 
  p[0].stock_number=10;
  p[0].price = 280.0;
  p[0].type = BOOK;
  strcpy(p[0].attribute.author, "Antoine");
 
  p[1].stock_number=20;
  p[1].price = 55.0;
  p[1].type = KEYCHAIN;
  p[1].attribute.color = gold;
 
  p[2].stock_number=23;
  p[2].price = 350.0;
  p[2].type = T_SHIRT;
  p[2].attribute.size = L;
 
  showInfo(p[0]);
  showInfo(p[1]);
  showInfo(p[2]);
 
  return 0;
}