c语言学习(三)
来源:互联网 发布:维尔迪数据 编辑:程序博客网 时间:2024/05/16 04:59
20、外部函数和内部函数
外部函数和内部函数的定义也是从其作用域出发的。用static定义的函数时内部函数仅作用于本文件;用extern定义的函数时外部函数,可作用域其他文件(extern可省略)。
static函数也称为静态函数,这种函数可以在多个文件中同名。
21、预处理命令
C允许加入一些预处理命令,但这些命令本身不是C语言的组成部分,不能直接对其编译,需要在正式编译之前预处理。
预处理实质上是把根据预处理命令对程序中用到预处理的地方进行替换。如#define了一个符号常量A,在程序中出现A的地方替换成A后面的字符串。
预处理分为三种:宏定义、文件包含、条件编译,都以“#”开头。
(1)宏定义
①不带参数的宏定义【#define标识符字符串】
标识符称为宏名,预编译时将宏名替换成字符串的过程称为“宏展开”。由于预编译时只是进行原样替换,所以不对定义的字符串做正确性检查,也不会进行计算。只有替换后进行编译时才检查正确性。如果宏定义为如下(宏名与字符串之间有一个空格,字符串后面没有分号,否则算作字符串的一部分)
#defineA 3+5
inta=6*A;
在预编译时,将A原样替换得到int a=6*3+5;最后总是23,而不是的6*(3+5)。
宏定义的有效范围为定义命令之后到本源文件结束,可以用【#undef宏名】终止宏定义的作用域,可以重新用同一个宏名再次宏定义成其他字符串。
宏定义是专门用于预处理命令的专用名词,它与定义变量不同,只做字符替换不分配内存空间!
②带参数的宏定义【#define宏名(参数列表)字符串】
宏定义可以携带参数,像定义函数一样:
#defineS(a,b) a*b
intarea=S(3,2);//计算矩形面积的带参数宏定义
实际上是先用实参3,2代替形参a,b,用3*2替换了S(3,2),最终宏展开为int area=3*2;
带参数宏定义与函数的区别
函数
宏定义
调用
会将实参求值后代入形参
只是简单的字符替换
内存占用
形参占用内存
宏展开在编译前进行,不分配内存,无返回值
参数类型
实参和形参都有数据类型
宏名无类型,参数也无类型,只是一个符号代表。宏定义时的字符串可以是任何类型数据
返回值
只能得到一个返回值
可以设法得到多个结果
运行时间
函数调用占用运行时间
宏替换不占运行时间,只占编译时间
#definePI 3.14
#defineCIRCLE(R,L,S,V) L=2*PI*R;S=PI*R*R;V=4.0/3.0*R*R*R*PI
floatr=2.0f,l,s,v ;
CIRCLE(r,ls,v);
预编译后l=2*3.14*r;s=3.14*r*r;v=4.0/3.0*r*r*r*3.14;可计算出l、s、v多个结果。
宏定义还可以用于格式化输出——通过宏定义规定一个输出格式,在程序调用时可以省去书写麻烦。如
#definePR printf
#defineNL “\n”
#defineD "%d"
PR(DNL ,a);//格式化输出,替换后为printf("%d""\n",a);
可以将一些格式输出编写在一个头文件里,这样可以供多个源文件使用。
(2)文件包含
文件包含即用【#include <文件名>】或【#include “文件名”】的方式将另一个源文件的全部内容包含到当前源文件中。例如在file1.c中#include”file2.c”经过预处理之后,将file2.c包含到file1中,得到一个新的源程序,然后对这个文件进行编译,得到一个目标文件。被包含的文件称为新的源文件的一部分,而单独生成目标文件。
头文件可包括函数原型(即声明)、宏定义、结构体、全局变量定义等。通常在.h文件中声明函数,在同名的.c文件中定义它。事实上,stdio.h也是对printf.c中的printf函数进行了声明,printf函数的定义实在printf.c中进行的!
如上面的例子中,自定义的头文件会与包含它的文件一起编译,而非分别编译后连接。但库函数是编译系统自带的,已经是目标文件,所以一般文件编译后与之连接。
用<>包含的文件,系统会到存放C库函数的目录中找需要的文件,为标准方式。用””包含的文件会首先到用户当前目录中找,若找不到再到C库函数的目录下找。所以包含库函数是用<>,包含自定义的头文件用””,可减少查找时间——若文件不在当前目录中,可在””中给出文件路径,如#include “D:\file2.h”。
被包含的文件与其所在的文件,在预编译后已成为同一个文件。因此,在被包含的文件中的全局变量可以在包含它的文件中使用,不用extern声明。
综上头文件用于宏定义、函数声明、全局变量定义等,免去了重复的代码。
(3)条件编译
【# ifndef标识符1
程序段1
#elif 标识符2
程序段2
#else
程序段3
#endif】
条件编译即在某些条件下编译一段代码,而另一条件下编译另一段代码。与if…else…语句相比,条件编译可以减少目标文件长度,从而减少运行时间(不需要对每个else if都判断),提高了可移植性。
【#ifdef 标识符1
程序段1
#elif 标识符2
程序段2
#else
程序段3
#endif】
#ifedf为如果定义了标识符1则编译程序段1,若定义了标识符2则编译程序段2,否则编译程序段3。#ifndef为未定义了标识符1则编译程序段1。
【#if表达式
程序段1
#else
程序段2
#endif】
当表达式为真时,编译程序段1,否则编译程序段2。
上述的标识符或表达式都应该是#define的符号,如果是普通变量,就算是全局变量在编译时赋值,但条件编译是在编译之前的预处理,所以普通变量不管赋值0或1都是0。不会是“真”值。而#define也是预处理,所以能有效。
#define DEBUG 1
#ifdef DEBUG
….要调试的输出程序…..
#endif
(4)双下划线“__”
预编译程序可以识别一些特殊的符号。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
__FILE__ 包含当前程序文件名的字符串
__LINE__ 表示当前行号的整数
__DATE__ 包含当前日期的字符串
__STDC__ 如果编译器遵循ANSI C标准,它就是个非零值
__TIME__ 包含当前时间的字符串
printf("%s\n", __FILE__);//当前目录
printf("%d",__LINE__);//当前行数
(5)#运算符和##运算符(很少用)
出现在宏定义中的#运算符把跟在其后的参数转换成一个字符串。
##运算符用于把参数连接到一起。预处理程序把出现在##两侧的参数合并成一个符号。
22、指针
C语言中,将地址称为“指针”,所以指针即地址。
对一个变量i,直接对变量名i操作(取值等)称为“直接访问”。若有一个变量,存放的是i的地址,通过这个变量取得i的地址,通过这个地址获取i的值,这种方式称为“间接访问”。这个变量称为指针变量——存放另一个变量的地址的特殊变量。
(1)指针变量的定义
用“*”表示指针变量对变量的指向关系。
【基本类型 *指针变量名】
int *i_pointer;//声明一个指针变量
上述语句声明一个指针变量,变量名为i_pointer,在定义中“*”只是用来表示这个变量是一个指针变量。int是指针变量的类型。
i_pointer=&i;//将i的地址赋给指针变量i_pointer,i_pointer的值就是i的地址了。
指针变量的类型是很重要的。指针变量不能存放不同类型的变量的地址。如一个float变量的地址&a不能赋给一个int型的指针变量。指针类型的重要性在指针的移动、运算(指向数组元素时)体现出来。
(2)指针变量的引用
&表示取地址符号;
*是指针运算符,取其指向的内容(可以理解为取变量符号:若有int i=3; int *i_pointer;i_pointer=&i;则*i_pointer与i是等价的,即*i_pointer取了i_pointer代表的地址指向的变量i)。
指针变量的赋值应该是一个地址,而不是一个整除或其他非地址类型的数据。
指针变量作为函数参数时,是按值传递的。但由于指针变量存放的是地址,形参与实参存放同样的地址,所以形参变量可以改变实参指向的变量的值。可以理解为Java中的按引用传递——形参和实参指向同一个变量,当形参改变所存的地址时,就与实参没有关系了。所以不可能通过调用函数来改变实参指针变量所存的值(地址),但可以改变实参指针变量所指变量的值。
voidmain(){
int a=1,b=2;
int *a_pointer,*b_pointer;
a_pointer=&a,b_pointer=&b;
swap(a_pointer,b_pointer);
}
voidswap(int *p1,*p2){//交换p1,p2指向的变量的值
int temp;
temp=*p1;
*p1=*p2;
*p2=temp;
}
形参p1和实参a_pointer都存放a的地址,指向a。所以*p1就是a。
若swap函数中将temp定义成指针变量int*temp:
int *temp;
*temp=*p1;
*p1=*p2;
*p2=*temp;
此时就会有问题,*temp是temp指向变量,而在声明temp时,其值是不确定的,不知道它指向哪里,有可能指向原本重要的数据。若*temp=*p1;则会改变这个重要数据,可能会破坏系统正常工作。(主要原因是temp没有初始化,而一般情况下使用指针变量初始化指向一个明确的变量。而此处temp在一开始可能指向某个重要的存储单元。)
22、数组与指针
指针变量可以指向数组的元素,通过对指针变量的加减运算可以方便对数组元素操作。若有int a[10];int *p;则p=&a[0]等价于p=a;或者声明时初始化int*p=a;因为数组名表示了数组的首地址a等价于&a[0]。
指针的加减运算是基于指针类型的。有int *p,则p+1表示p的值加上2个(或4个)字节,而不是单纯将p的值加1。
有数组a[],指针变量p=&a,有以下关系:
p[i]<==>*(p+1) <==>*(a+i)<==>a[i]——数组的第i个元素
由于数组名是首地址,故也可以与指针变量一样参与地址元算。p作为指向数组的指针变量也可以带下标。可见[]实际上是变址元算符,即将a[i]按a+i计算地址,然后找出此地址单元中的值。
C编译系统将a[i]转换成*(a+i),即先计算元素地址,所以两者的执行效率相同。而用指针变量指向元素,指针的自加/自减操作不必每次重新计算地址,效率较高。但用指针变量的方式不如下标法直观。注意a是首元素地址,是一个常量,不是可以a++。
在C中,没有数组下标越界,指针变量可以指向数组长度之外的地址,不会出现错误。但是数组之外的数据是不可预知的。
经常会将指针运算符*和自加自减结合使用,要注意执行顺序:
*与++/--是同优先级运算符,自右至左结合。所以*p++和*(p++)是一样的(都是先取出p的值再对p移位),而*p++和*++p则不同。
p和a都是地址,可以进行关系比较:
p=a;
while(p<a+100)
printf(“%d”,*p++);
数组作为函数参数时,系统将实参数组的首地址传给形参数组。实际上,编译系统将形参数组按指针变量处理,也就是说形参在一开始是指向数组首元素地址的指针变量!因此当用sizeof(形参数组名)得到的是首地址元素所占的空间大小。但是在理解时,可以理解为有一个形参数组与实参数组共用同一段内存单元,所以形参数组可以改变实参数组。但实际上栈中只分配了一个指针变量的大小,而没有真正开辟一个数组空间——所以函数定义时形参可不指定大小,而在定义数组(实参)时必须指定大小以便开辟空间。
因此以下两者是等价的:
f(int a[]) f(int *a)
{ {
…… ……
} }
实参/形参是数组名或指针变量,两两组合的4种情况都可以实现对数组的元素的改变。
需要注意的是指针变量必须初始化后才能使用(传入形参),否则谈不上指向哪一个变量。
●多维数组与指针
对于一个二维数组a[3][4],可以理解为是3个一维数组组成的,每个一维数组有4个元素。而a[0]、a[1]、a[2]是每个一维数组的数组名,因此a[0]、a[1]、a[2]也是对应数组首元素的地址。
对于二维数组名a,代表第0行的首地址,a+1表示第1行的首地址。因此a、a[0]、&a[0][0]三者的值是相等的,都是a[0][0]元素的地址。但是a与a[0]虽然值相等,但指向的方向却不同,是不同类型的指针!
由上图可以看出,a和a[0]虽然都是a[0][0]的地址,但a是行的指针,a[0]是列指针,两者的移动方向是不一样的,所以a+1指向下一行即下一个一维数组(这里的1跨度是一行),而a[0]+1指向的第0行的第1个元素(这里的1跨度是一个元素所占字节)。
因此想要用指针法表示a[i][j],可以是*(a[i]+j)或者*(*(a+i)+j)。这里可以看出a[i]与*(a+i)是等价的。a+i指向的第i行,但*(a+i)并不是所指向地址的内容。因为a+i并不是指向一个变量的存储单元,而是指向行的,所以谈不上去它的内容。而a[i]、*(a+i)则是指向a[i][0]这个元素的存储单元,是在列方向上的指针,对其用*可以获得对应的元素内容。
所以,a+i是指向行的指针,在其前面加一个*可以转换为指向列的指针,即*(a+i)。同样的一个指向列的指针a[0],在其前面加取地址符号&可以转换为指向行的指针,&a[0]等价于a,也等价于&*a。
在一维数组中a+i指向第i个元素的存储单元,而在二维数组中是第i行,没有具体指向哪个存储单元。而二维数组作为函数参数时,实参传入的是一个行指针a,是首元素地址。a+1指向第1行。(就把形参数组当作实参数组一样使用,只是名字不同)
●指向数组元素的指针变量与指向一维数组的指针变量
由于二维数组各行在地址上是连续的,所以上述的数组a中a[0]+2即a[1][0]元素——虽然a数组只有两列。也就是说a[1][0]在内存上是a[0][1]的下一个存储单元,而a[0]是指向元素的指针,所以a[0]+2是a[0]基础上移动2个元素,到达a[1][0]的地址。
根据上述性质,若有指针变量指向二维数组a[n][m]的首元素a[0[0](p是指向变量的),对于任意位置元素a[i][j]的地址为p+i*m+j(m为列数)。
·指针变量可以指向二维数组的元素:
int *p=*a;//指向首元素的列指针
p可以在数组长度范围内移动,或者p不动,用p+i(0≤i≤a+len-1)。
指向数组元素的指针作为函数参数时。形参是*a,在对其*(*a+1)可能会出现寻址错误,因为系统不知道a是二维数组的指针。(*a+1已经得到一个数值内容,再根据这个数值作为地址取值可能进入操作系统占用的内存段)
·指针变量也可以指向一维数组:
int (*p)[4];//定义一个指向长度为4的一维数组的行指针。小括号不可以省略,[]的优先级高于*,得到的是*p[4]是一个指针数组。
此时可以对p进行*(*p+1)。因此要分清指针的类型——p=a;*(p+2)+3与(p+2)+3是不同的。(p+2)+3等价于p+5,表示a的第5行;而*(p+2)+3表示a[2][3]。*(p+2)+3式中的2和3由于经过指针类型的转变分别表示2行和3列,不可以混淆。
23、字符串与指针
C语言中可以用两种方法访问一个字符串:字符数组和字符指针。
①char string[]=”I love you”;
string作为数组名,是’I’这个字符的地址,也可以用*(string+i)的方式访问每一个元素。(系统为string数组末尾添加了’\0’)
②char *string=”I love you”;
string是指向一个字符变量的指针。字符串在内存中以字符数组的形式存放,string是首元素的地址。对string的初始化实际上把字符串第一个元素的地址赋给string。
上面两种形式都可以用%s输入输出。采用%s输出先对string指向的变量输出,然后对string自动加1,使之指向下一个字符,直到遇到\0。
注意:对字符串输出时,注意其是否带有结尾标志’\0’。
字符数组与字符指针变量的区别:
Ø数组在定义后有确定的地址,而定义一个字符指针变量虽然给这个变量分配了地址,但这个单元中的内容不可知。所以以下程序是不提倡的:
char *s;
scanf(“%s”,s);
企图输入一个字符串,使s指向这个字符串首地址。编译不会出错,但由于s没有初始化,它所指向的地址不确定,所以输入的字符串存放的地址可能是内存中以存有重要数据的单元,对程序甚至系统造成破坏。应当用数组:
char s[20]; scanf(“%s”,s);
Ø数组声明和初始化不可以分开,而指针变量可以。即char *a;a=”abc”;可行而charb[5];
b=”abc”;不可行。
Ø用指针变量指向一个格式字符串,可以代替printf函数中的格式字符串。如:
char *format=”%d,%d\n”;
printf(format,a,b);
也可以使用字符数组:
char format[]=”%d,%d\n”;
printf(format,a,b);
但是指针变量可以再赋值format=”%s,%s\n”;而数组名不可以整体赋值,所以指针变量更方便。
24、指向函数的指针
一个指针变量可以指向一个函数。一个函数在编译时会被分配一个入口地址,这个入口地址被称为函数的指针。
【数据类型 (*指针变量名)(函数参数列表)】
数据类型即该指针变量指向的函数的返回值类型。参数列表可以只有数据类型而不写形参名。如
int (*p)(int,int);//声明
p是指向一个返回值为int,有两个int参数的函数——这个函数可以是满足这两个条件的函数(可以是int max(int,int)、intmin(int,int)等)。(*p)的括号不可省,否则int *p(int,int)变成声明一个返回一个指向的函数。
p=max;//赋值
赋值时将函数名赋给指针变量p——函数名代表函数的入口地址。
c=(*p)(1,2);//调用。调用*p就是调用max。
注意:p是指向函数的指针变量,它只能指向函数的入口而不能指向函数中间的某一条指令,不能用*(p+1)来表示函数的下一条指令。
指向函数的指针的最通常的用途是把指针作为函数的参数传递到其他函数,可以用这个指针调用其他函数:
stub(int (*x1)(int),int (*x2)(int,int)){//指向函数的指针作为参数
int a,b,I,j;
a=(*x1)(a);
b=(*x2)(a,b);
…
}
在stub函数中可以用*x1和*x2调用传入的函数。在调用stub时,将要传入的函数的函数名作为实参传入stub。如要调用f1、f2,stub(f1,f2),就会在stub中用f1、f2算出a、b。这种做法的好处是当每次stub要调用的函数不固定时,可以灵活的变换要传入的函数,而不需要改变stub的内容。stub(f3,f4)、stub(f5,f6)。
例如process(inta,int b,int (*p)(int ,int))要根据传入的a、b值分别算出max、min和sum,只要将max、min和sum三个函数依次传入process(调用三次process)。
24、返回指针的函数
函数可以返回一个指针变量,【返回值类型 *函数名(参数列表)】
C语言不能返回一个数组,可以用返回一直指向数组或数组元素的指针来实现返回一个数组。
返回的指针若是指向一维数组,可能出现”int (*)[3]”与“int*”的间接级别不同的warning。因为在定义函数的时候只能是“*函数名(参数列表)”,不能是“(*)[3]函数名(参数列表)”。
25、指针数组和指向指针的指针
指针数组即存放指针的数组,其每一个元素都是一个指针变量。
【数据类型 *数组名[数组长度]】
char *name[5];
[]优先级高于*,所以先结合name[5],说明是一个数组,*表示这个数组存放的是指针变量。
Ø指针数组典型的应用就是用来指向字符串,每一个元素(指针变量)指向一个字符串,实现“字符串数组”。——不用指针数组就要使用二维数组存放,但定义二维数组必须指定列数,当每个字符串的长度不相同时,只能指定最大长度为列数,则会浪费内存。并且数组元素可以随时改变其指向的字符串,方便省空间。
char *name[3]={“abc”,”china”,”XYZ”};
name[0]、name[1]、name[2]分别是指向“abc”,”china”,”XYZ”的字符型指针变量。printf(“%s”,name[i]);即可输出对应字符串。
name作为数组名是name[0]的地址(name[0]本身也是地址),即*(name+i)等于name[i]。所以name是一个指向指针变量的指针。
【数据类型 **指针变量名】
char **p;
*p表明是一个指针,第一个*表示p是一个指向指针的指针。
p=name+1;
、
*p是name[1]的地址,**p是取*p所指向的内容,即china。
一个指针变量存放另一个目标变量的地址称为“单机间址”,也就是一级间接寻址。而指向指针的指针存放的是一个指针的地址,称为“二级间址”。
使用一级字符指针可以方便操作字符串,使用二级字符指针(指向字符指针的指针)可以方便操作字符串数组(指针数组)。
Ø指针数组的另一个应用(其实是第一个应用的延伸)是作为main函数的参数,在dos下将参数传入main函数。
void main(int argc,char *argv[]);
argc是传入的参数的个数,也就是argv数组的长度。argv是字符型指针数组,也就是传入的参数。
在dos下用【命令名参数1参数2……参数n】来执行main函数所在的可执行文件。在vs2010中,若工程名为project1(包含了main所在的c文件),则在编译系统处理后(编译、连接)生产可执行文件project1.exe。在对应的路径下输入
project1 test1 test2
上面project1即是命令名,即包含main函数的c文件所在工程对应的exe文件名。test1、test2是要传入的参数。argv数组包括可执行文件名project1。project1、test1、test2 都是字符串,argv存放三者的首地址。
●一维指针强制转为二维
int a=1;
int *p=&a;
int **q=(int **)p;//强制转型得到二维指针,而不是int **q=&p;
此时,q中的内容也是a的地址,所以*q=1;而对**q操作则会出错,因为那是要取地址值为1的内容。
我的理解是把p的内容赋给q,所以两个指针的内容是一样的,但是指针类型不同,所以要强制转型。就像float f=1.23;int a=(int )f;一样。
26、指针小结
定义
含义
int p;
整型数据
int *p;
指向整型数据的指针变量
int p[n];
整型数组
int *p[n];
指针数组:由n个指向整型数据的指针变量组成
int (*p)[n];
(指向)数组指针:p指向一个包含n个元素的一维数组
int p();
函数
int *p();
返回一个指针变量的函数
int (*p)();
指向函数的指针,p为函数的入口地址。即函数名
int **p;
指向指针变量的指针变量
根据()、[]、*的优先级判断定义的是什么类型(结合顺序)。
Ø指针可有空值,即该指针变量不指向任何变量。p=NULL;NULL是定义(#define)在stdio.h中的整数0。p=NULL表示p指向的地址为0,系统保证这个单元不会存放有效数据。(p=NULL和p未初始化是不同的)
Ø两个指针变量若都指向一个数组中的元素,则p1-p2表示两个指针之间的元素个数。
p1、p2也可以用关系运算符比较大小。但两个指针变量不指向同一个数组,相减、比较就没有意义了。
Øvoid指针:定义一个指针变量,但不指向任何一种类型数据。malloc等动态分配存储的函数返回一个void指针,它用来指向一个抽象的类型的数据。通过强制转型可以转化为任何类型的指针变量。如:
void*p1;
char*p2;
p2=(char*)p1;//将void指针转化为char型指针变量
同理,p2也可以用(void *)从char型转为void型。
27、结构体
数组中元素必须是同一数据类型,结构体是一种包含多个不同类型数据的数据结构。
Ø声明一个结构体类型如下:
【struct结构体名
{成员列表};】
struct date
{
int year;
int month;
int day;
};
struct student
{
char name[20];
char *number;
int age;
struct datebirthday;
};
一个结构体中可以包含另一个结构体变量。如student中包含一个date型变量birthday。
Ø声明一个结构体变量如下:
(1)【结构体类型名结构体变量名】如上面的struct date birthday;要求指定变量的类型struct date。不能只写一个date——结构体相当于面向对象语言的类,结构体的成员变量即类的成员变量。一个声明对象,一个声明结构体变量。
(2)在声明结构体时同时定义变量:
strcutstudent{
….成员变量
}stu1,stu2;
(3)直接定义结构体变量。不写结构体名。
strcut{
….成员变量
}stu1,stu2;
在编译时,对结构体类型并不分配内存,在声明变量后,对变量分配内存。内存大小为个成员变量所在大小之和(sizeof(结构体变量名)或sizeof(结构体类型),如sizeof(struct student)可得长度)——一个指针变量占4个字节(vs2010)。上述的student结构体中有number指针变量,它在结构体变量stu1中占4个字节。不论是char型指针变量还是int型指针变量都是用来存放一个地址值,占用4个字节。并不因为其指向的数据所占大小不同而不同,因为其指向的数据存放在结构体变量之外的内存段中——包括字符串常量或者栈中的变量。而数组根据指定的大小开辟空间,不管其初始化了几个字节。
结构体变量的大小理论上是成员变量的大小之和。但实际上并不是简单的将其中各元素所占字节相加,而是要考虑到存储空间的字节对齐问题。有助于加快计算机的取数速度,否则就得多花指令周期。
字节对齐三个准则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internaladding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailingpadding)。
一个结构体的最终对齐长度是制定的对齐长度与结构体最宽成员长度之间的较小值。
字节对齐不一定是自然对齐(以上是自然对齐,取最宽成员所占大小为对齐长度),也可能是1字节对齐,2字节对齐,4字节对齐等。所谓K字节对齐就是要求成员变量的首地址能被K整除。在前一个成员变量尾地址与下一个成员首地址之间(如果有空间)加上填充字节。再满足上面的准则3.使用预编译命令#pragmapack(对齐长度)来制定编译器的对齐长度。
不同的成员排列方式造成结构体所占空间大小不同,存在最小空间的排序,并且可以将用来对齐的补字节最为保留空间用于扩展。
所以对于一个struct s{inta;char b};结构体的大小并不是4+1=5,而是4+4=8。
只能对变量进行赋值、存取、运算,而不能对一个类型(结构体)赋值、存取、运算。
Ø初始化一个结构体变量
structdate date1={1991,1,12};//初始化一个structdate变量
structstudent s1={"sss","0123",20,1991,1,1};//初始化一个struct student变量
s与数组一样,用{}初始化只能与声明同时,而不能分开。student中包含一个date变量,在初始化赋值时,针对date的成员变量逐个赋值。不能是struct student s1= {"sss","0123", 20,day1};——student的成员变量在内存中根据数据类型连续开辟空间,birthday是一个结构体,本质上是三个int型数据的连续空间。而赋值(包括数组的赋值),是按地址从前往后依次初始化,所以对birthday的初始化也就是初始化那三个int所占的空间。
也可以值初始化部分,如struct student s1={"sss","0123",20 };
再对birthday单独赋值s1.birthday=day1;
“.”用来引用结构体变量中的成员——“.”是优先级最高的运算符。
可以引用结构体变量成员的地址,也可以引用结构体变量的地址:
&stu1.name;
&stu1;//&stu1的值等于&stu1.name——结构体变量名的地址即成员变量的首地址。
27、结构体数组
一个结构体变量只能反应一个变量的情况,如student1只能反应一个学生的情况。要同时处理多个学生的数据就要用到数组。结构体数组就是其每一个元素都是一个结构体变量的数组。
结构体数组的定义与结构体变量的声明一下有三种方式。
structperson
{
char name[10];
int count;
}candidate[3]={{"A",0},{"B",0},{"C",0}};
candidate[]数组中是三个person类型的结构体变量,成员变量在地址上连续。
28、指向结构体类型数据的指针
(1)指向结构体变量和指向结构体数组的指针
【结构体类型 *指针变量名】,如
structstudent *p;
structstudent stu1;
p=&stu1;//p指向了stu1——只有数组名才是地址,结构体名仍为普通变量名,要&。
使用指针的方式有:
①(*p).成员名<==>stu1.成员名
②p->成员名
->是指向运算符。->、()、[]、.都是最高等级的运算符,从左至右结合。在于++符号结合时要注意自加的是什么:
p->n++;//得到p指向的结构体变量的成员n的值,使用n后n+1。
++p->n;//->优先于++,所以p->n是一个整体,先得到p的n是n+1后使用。
(++p)->age;//p+1后输出age
p++->name//先使用p的name,再对p +1。等同于(p++)->name
当p指向的是结构体数组的元素时,p+1才有了意义,表示指向下一个结构体数组元素。所以p跳跃的地址长度是一个结构体变量占的内存字节数。如上面的person结构体,一个变量占14个字节。若p = candidate或p = &candidate[0],则p+1跨越14个字节指向candidate[1]。
当一个指针变量p指向结构体变量,不能用它指向变量中的成员,因为地址类型不匹配。如果要将一个成员的地址赋给p,可以用强制转型,先将成员的地址转化成p的类型,如:
p=(structstudent *)stu[0].name;//name是数组名,已经是地址,不用&。而此时p仍为struct student类型指针。
(2)结构体变量作为函数参数
当结构体变量作为函数形参,传入结构体变量实参时,按值传递。即会复制一个结构体形参传入函数。这种方式不能改变原变量,并且在结构体变量规模大时,开销很大。
使用指向结构体变量的指针变量作为形参,将结构体变量的地址传给形参,可以大大减小开销。
结构体和指向结构体的指针可以实现链表。线性表分为顺序表(数组)、链表(静态链表和动态链表),而静态链表也是存放在数组中的,包括也data和next,删除和插入不需要移动元素,指向修改next,在物理上还是按顺序存储,但数组长度是固定的——描述方法便于在没有指针类型的高级程序设计语言中使用链表结构。而动态链表各结点是动态开辟的。
在C中用malloc.h中的void *malloc(unsigned size)申请长度为size的连续空间和void*calloc(unsigned n, unsigned size)申请n个长度为size的连续空间。返回的都是开辟的空间的首地址(void型)。若申请不成功返回NULL。根据需要的类型,将void*强制转型。
int*p=(int *)malloc(sizeof(struct student));//申请一个结构体变量长度的空间
char*c=(char *)calloc(3,sizeof(char));//申请3个char类型数据连续空间。
这两个函数用于动态申请数组和结构体变量。上面的c为char型指针,所以c+1跳跃一个字节。
voidfree(void *p)释放p指向的内存区,使这部分内存区能被其他变量使用。
注意:在函数中创建一个数组,并返回一个指针。或者在函数中创建一条链表返回头节点指针时,新建的数组或链表都需要malloc动态开辟空间。不能是静态的一个数组或结构体变量。
int *test()
{
int*p;
inta[3]={1,2,3};
p=a;
returnp;
}
int *test2()
{
int*p=(int *)calloc(3,4);
inti;
for(i=0;i<3;i++)
*(p+i)=i+1;
returnp;
}
上面两个函数都企图返回一个数组(通过指向数组的指针实现)。但test中的数组时静态的,虽然能返回数组a的地址,但这个数组是在栈中的,test函数被调用后,这些局部变量可能会被回收,&a[0]、&a[1]、&a[2]地址上的数据不一定还是1,2,3。
而test2中用malloc申请的空间在堆中,在程序结束后才由系统释放,所以在主调函数中还能正常使用p。
在函数中创建一条链表,链表头节点也是同理,需要用malloc,而不是用structstudent来创建。
29、共用体
共用体union是使同几种不同类型的变量存放到同一段内存单元中。也就是几个成员变量公用同一个起始地址。共用体与结构体定义相似,但含义不同。
【union共用体名
{成员列表}变量列表;】
结构体变量所占内存为成员变量长度之和,而共用体变量所占内存为成员变量最长的长度。
uniondata
{
chara;
intb;
floatc;
}d1,d2,d3;
若char占1个字节、int占2个字节、float占4个字节。则三个成员变量共用以2000为起始的地址。即a占用2000,b占用2000、2001,c占用2000-2003。
Ø共用体的特点
①共用体变量的初始化不能在定义。要分开初始化各成员变量。如data d1={‘a’,1,3.1};的赋值是错误的。应该是data d1;d1.a=’a’;d1.b=1;d1.c=3.1;
②共用体成员变量是“覆盖赋值”的。后面赋值的会覆盖前面赋值的。在一个时刻只能有效的使用其中一个,其他被覆盖的变量失效,但仍可以引用。
③不能把共用体变量作为函数参数,也不能使函数返回共用体变量,但可以使用指向共用体变量的指针。
④共用体中可以定义结构体,结构体中也可以定义共用体。
对于占用多个字节的数据,在内存中,低位数据存在低地址中,高位数据存在高地址中。如一个占4字节的int型数据0x30313233,占用2000-2003 4个字节,2000存放0x33,2003存放0x30。所以由于共用体成员变量公用起始地址,占用字节小的变量(如1个字节)在被占用字节大的变量(如4个字节)覆盖赋值后,再引用占用字节小的数据实际上是从公用的起始地址开始获取所占字节数。如
int i=0x30313233;
char *c;
c=&i;
printf("%x\n",*(c));//33
printf("%x\n",*(c+1));//32
//---------------------------------
union test
{
char a;
int b;
}t;
t.a='a';
printf("ta=%c\n",t.a);
t.b=0x30000041; //b覆盖了a,但仍可引用a
printf("ta=%c\n",t.a);//a是低8位上的数据(A)
printf("tb=%x\n",t.b);
共用体用在用同一种结构表示多种数据的情况。可以节省空间。
29、枚举
如果一个变量只有几种可能的值,则可以定义为枚举类型。
enumweekday{sun,mon,tue,wed,thu,fri,sat};//声明一个枚举类型
enumweekday workday,weekend;//声明两个enumweekday类型的枚举变量
workday=mon;weekend=sun;//对枚举变量初始化
枚举变量只能是weekday中的列举的值的范围。称为枚举元素或枚举常量。
说明:
①在C编译中,枚举元素按常量处理,他们不是变量,不能赋值。sun=1;是错误的。
②枚举元素作为常量是有值的。在C语言编译时,按定义的顺序使它们的值为0,1,2…例如上面的sun=0;mon=1;……sat=6;是为默认的情况。可以对枚举值用”%d”输出。
也可以在定义枚举类型时自定义元素的值。如enum weekday {sun=7, mon=1, tue, wed, thu, fri,sat};mon=1,tue在其后加1。sat为6。
③枚举值可以用作判断if(workday==mon)或if(workday==1)
④可以对枚举变量++操作。(C++中不允许++操作)
enum weekdayday=sun;
for(;day<=sat;day++)
printf("%d",day);
- c语言学习(三)
- C语言学习(三)运算符
- C语言程序学习(三)笔记
- C语言学习笔记(三)
- 学习杂记(三)c语言
- C语言基础知识学习(三)
- C语言基础知识学习(三)
- C语言基础学习(三)--语句
- [C语言学习]作业三
- c语言学习笔记三
- C语言学习笔记<三 >
- C语言学习代码<三>
- C语言指针学习三
- iOS开发学习笔记-C语言学习(三)
- C语言学习知识点(三):简单的学习应用
- C语言(三)
- C语言学习笔记(三)——条件表达式
- 我的C语言学习日志(三)
- 线程与进程02
- 华为OJ 初级:查找输入整数二进制中1的个数
- Hive入门--6.表的基本操作
- hdu 1525 Euclid's Game (博弈规律)
- 让activity的逻辑业务快速切换到fragment
- c语言学习(三)
- 有关继承,实现接口,子类,实例化对象之间总结
- 实验 FileUpload 笔记
- java反射获得父类泛型参数
- 利用ThreadLocal & Filter 实现事务处理
- 使用 <base> 标签解决 相对路径问题
- Hibernate 多对多 HQL 查询
- Android Activity 全局管理 终极解决方案
- 设置字体加粗