对C结构的理解

记录一下学习C结构的想法


我认为结构这种数据类型为C++奠定了面向对象的基础。这是一种很自由的数据类型,我们甚至可以用指针和结构实现面向对象。

   struct name{  
      type1 a;
      type2 b;  
   };       //注意这里的分号   

这里的声明并未创建一个实际的数据对象,而是描述了这类对象的元素形式,我们也可以将结构声明称之为模板,因为他勾勒出数据将如何存储。
之后我们声明name结构的变量:

  struct name dyf;    

当编译器读到这条指令时,它将以name模板为dyf分配内存空间,即使未初始化,该结构的大小也由type1 与 type2 的大小决定。这就意味着结构的大小可能会大于数据集本身,因为系统对数据的对齐要求会导致存储裂缝。
再者,我们可以如下声明:

  struct name {  
    type1 a;  
    type2 b;    
  } dyf;  

即声明结构与定义结构的过程合二为一,如果要多次使用一个模板我们也可以用typedef。

struct book math{ “高等数学”, 20 };

 
非指定初始化应当保持初始化项目与结构成员类型一一对应。  
而指定初始化则类似于数组:  
     
 
```c  
     
    struct book dyf{
      .name="高等数学",
      .weight=10      
    };  

其中的.name类似于数组的下标,寻址自然与数组类似。


struct book library[20];
library[2].name="高等数学";  //代表library的第三个元素的那么成员

此时,[2]是library的下标,应当注意区别:

library[2].name;
library.name[2];  

后者指的是library的第一个成员的name的第三个字符。


有时候我们会在一个结构中嵌套另一个结构例如:

struct name{ 
  char firstname[20];
  char lastname[20];
};

struct person {
  struct name dyf;
  int age;
  int height;
};  

只需在外层结构中声明即可,同理,使用两次点运算符进行访问:

person.dyf.name="dyf";  

      struct person {
        struct name dyf;
        int age;
        int height;
      };

      struct person * p;    //定义一个只想person结构类型的指针p

      p = &dyf;    //将dyf的地址赋值给指针p  

p指针在被定义后只能指向person的结构类型,储存person结构的地址。
与数组不同的是,结构的名并不代表首个成员的地址,因此必须使用&操作符。

我们可以通过指针来传递并访问结构,这种操作非常舒服。

      struct person {
        struct name dyf;
        int age;
        int height;
      };
     
      struct person * p;    //定义一个指向person结构类型的指针p
     
      p = &dyf;    //将dyf的地址赋值给指针p  
      
      struct book * m;
      m = &library[2];    //同理,结构数组内的结构如图赋值

p指针在被定义后只能指向person的结构类型,储存person结构的地址。
与数组不同的是,结构的名并不代表首个成员的地址,因此必须使用&操作符。


只要结构具有单个值的数据类型,即:int及其相关类型、char、float、double、指针等,就可以把它作为一个参数传递给函数,如:

#include<stdio.h>
struct name{
  char firstname[20];
  char lastname[20];
};
struct person {
  struct name myname;
  int age;
  int height;
};
void getInfo(struct person * p);
void outIbfo(struct person * p);
int main(void){ 
  struct person dyf;
  struct person * p;
  p = &dyf;
  getInfo(p);
  outInfo(p);
return 0;
}

void getInfo(struct person * p){
  printf("please enter your firstname\n");
  scanf("%s",&((*p).myname.firstname));        //注意理解这里
  printf("please enter your last name\n");
  scanf("%s",&((p->myname).lastname));         //注意理解这里
  printf("please enter your age\n");
  scanf("%d",&(p->age));
  printf("please enter your height\n");
  scanf("%d",&(p->height));
}

void outInfo(struct person * p){
  printf("\nname\t);
  printf("%s %s\n",(p->myname).firstname,(p->myname).lastname);
  printf("age   %d\n",p->age);
  printf("height    %d\n",p->height);
}

以上是利用指针来传递结构参数,应当深刻理解’->’的意义。

p->dyf;    //这仅仅是获得dyf变量的名而不是其地址  等价于(*dyf)  

scanf()需要传递给地址,因此我们需要使用&操作符。如果你理解了以上两种寻址方式,那么你对->的理解算是合格了。但距离用结构和指针实现面向对象还有一定距离。顺便说一句,我们通常用结构和指针实现队列的数据结构,好好理解指针吧。

当然除了以上这种用指针传递参数的方式,我们还可以直接用结构的名传递参数。

struct yourmark {
  int math;
  int English;
};

double mark(struct yourmark mark ){
  return mark.math + mark.English;
}  

这种传参方式很自然也很好理解,但是这毕竟只是赋值给形参,因此如果想改变元数据,我们依旧要使用指针。

如果要返回struct则:

  struct yourmark{
    int math;
    int English;
  };
  struct yourmark getmark(struct yourmark mark){      // 此处的返回类型为yourmark结构类型
    printf("please enter your math mark and English mark\n");
    scanf("%d%d",&(mark.math),&(mark.English));
    return mark
  }
  
  struct yourmark mark;
  mark = getmark(mark);   // 注意,给结构赋值时直接用其名而不是其地址  

同理,要返回指针只需要struct yourmark * mark getmark(struct yourmark mark)

好了到这里,把结构在函数里传来传去已经差不多说完了。


C99引入了一些新的概念,比如变长数组(VLA)、复合文字(compound literal)、指针的兼容性等。

复合文字的意思:
假如我要给函数传递参数,我可以传递一个变量也可以传递一个常量,例如:

  int a=2,b=3;
  sum(a,b)==sum(2,3);  

但是对于数组或者结构来讲我们之前没有说过常量这个概念,在传递参数时或者向另一结构传递时可能要定义新的变量,很浪费内存。此时,便引入了复合文字这一概念。
声明如下:

  struct person {
    char name[20];
    int age;
  };
  
  struct person guy;     //定义一个person结构类型的结构
  
  guy = (struct person){"dyf",18};   //把复合文字赋值给guy  
  
  outInfo((struct person){"麂皮"18})  //将一个匿名结构作为实参传递给函数  
  
  struct class23 {
    (struct person){"dyf",18};
    (struct person){"麂皮"18};
  };                                    //将两个匿名结构传递给class23
  
  -------------------------------------------------------------------  
  
  #include <stdio.h>
  struct mark {
    int math;
    int English;
  };
  int mark(struct mark * p);       //声明一个参数为mark结构的指针的函数
  int main(void){
    printf("%d",mark(&(struct mark ){150150}))   //传递复合文字的地址
    return 0;
  }
  int mark(struct mark * p){
    return p->math + p->English
  }
  
  /* 注:用G++编译会报错,因为其地址是temporary 而C99版本的GCC是可行的,因为临时具有自动储存时期,而在函数外具有静态储存时期 */

这是复合文字的大概用法,他能够创建一个匿名常量对象,直接在结构体或者函数中传递的常量。


C99加入了一个成为伸缩性数组成员(flexible array member)的新特性,该特性允许结构的最后一个成员是一个具有特殊属性的数组结构, 该数组的属性之一就是他并不立即存在。创建规则如下:

  1. 伸缩性数组成员必须是最后一个成员
  2. 结构中至少有一个其他成员
  3. 像普通数组那样声明,只是长度不定,例:int a[];

如下:

struct mark{
  int average;
  char subjects[]   //伸缩数组成员
}  

此时subjects[]并未被创建,系统没有为他分配足够的内存空间。通常我们要使用伸缩数组时,都会为其先分配足够的内存空间。

    struct mark * p;
    p=malloc(sizeof(struct mark) + 20 * sizeof(char));

这时我们已经有足够的内存来存放一个mark型结构,并且他可以存放一个19个字符的字符串。没错,开辟的内存空间要能存放结构本身和所需大小的数组。

    #include <stdio.h>
    #include <malloc.h>
    int main(void) {
        struct flex{
            int age;
            char * name;
            char * favobook[20];    //思考一下,他在main()中能直接赋值吗
            int favonumber[];
        };
        struct flex * p;
        p = malloc(sizeof(struct flex)+6* sizeof(int));
        p->age=18;
        p->favonumber[7]=1;
        p->name="dyf";
        printf("%s \t %d \t %d",p->name,p->age,p->favonumber[7]);
        return 0;
    }

这里我们声明里一个指针name,要注意在C语言中,字符串以数组的形式存储,也就是说其变量名实际是个地址,在我们对其进行声明时计算机已经为他在内存中开辟了空间,所以其地址实际上是个常量,即name是个常量。假如我要进行name="dyf";操作,编译器将报错。"dyf"的地址很明显与name本身冲突,故不能直接赋值。 这里我们看到favonumber能存8个整数,我也不知道为什么,回去查查资料再来修改。


结构的整套信息我们称之为记录(record),单个的项目称之为字段(field),下面,我们来进行讨论。
第一种方法,也是最笨拙的方法,使用fprinf()函数,例如:

    struct book{
    char title[20];
    char author[20];
    double value;
    };
    struct book math;
    fprintf(books, "%9s %9s %7.2d",math.title,math.author,math.value);

我们使用%9s来固定输入格式,以便于下一次读取,这里的books是文件流。

第二种方法,我们可以使用fread()和fwrite()以结构大小为单位来进行读写,例如:

    fwrite(&math,sizeof(struct book),1,books)

这时我们将定位到math的地址sizeof(struct book)将返回一块book结构的大小,'1'则告诉函数只需复制一块结构,最后将整个record写入books相关联的文件。同样fread()将record写入&math地址。


通过对结构体进行封装,C中还有联合又称为共用体(union)、枚举(enumerated type)两种类型。首先,union声明如下:

    union id{
        char id_string[20];
        int id_int;
    };

假如一个物体的id有可能是整数,也有可能是字符串,那么我们可以用以上操作。
union并不是复合结构,这其中的声明的类型只能同时存在一种,也就是说id可以是字符串类型,也可以是int类型。 因此,我们可以声明一个union数组来存放不同类型的数据,这样就实现了混合数据类型存储。这种数据类型封装的方法与结构相同,同样支持. ->等运算符,但是其意义却完全不同。 其次,枚举类型声明如下:

    
    enum subjects {math=,English=2,Chinese,CS};
    enum subjects my_favo_subject;
    for(my_favo_subject=math;my_favo_subject<=CS;my_favo_subject++){
      printf("%d\n",my_favo_subject);
    }

我们通常用枚举创建符号常量,例如, math,CS是枚举常量,默认为int类型,math是枚举对象的首元素,其默认值为0,这就好比数组的下标,方便我们进行枚举。我们也可以给枚举常量一个指定值,例如上面English=2,那么,其后面的元素依次从2递增。由于枚举类型是一个整数类型,所以我们常将其用于表达式当中,方便进行逻辑判断或者运算。
注:
C语言支持枚举变量自增,即my_favo_subject++;但是C++不支持,注意代码兼容性。


dyf is cool.