C语言(二)

第二章 数据

  • 2.1 数据类型
    • 2.1.1 数据类型决定
      • 1. 数据占内存字节数
      • 2. 数据取值范围
      • 3. 其上可进行的操作
  • 2.2基本数据类型
    • 2.2.1分类 基本类型
        • 类型 符号 关键字 字节 16位/32位/64位 位数 32位 数的表示范围 32位
          • 整型  
              • [signed] int 2/4/4 4*8=32 -2^31 ~ 2^31-1
              • [signed] short [int] 2/2/2 2*8=16 -2^15 ~ 2^15-1
              • [signed] long [int] 4/4/4 32 -2^31 ~ 2^31-1
              • [signed] long long [int] 8/8/8 64 -2^63 ~ 2^63-1
              • unsigned short [int] 2/2/2 16 0 ~ 2^16-1
              • unsigned int 2/4/4 32 0 ~ 2^32-1
              • unsigned long [int] 4/4/8 32 0 ~ 2^32-1
              • unsigned long long [int] 8/8/8 64 0 ~ 2^64-1
          • 实型
            • 有 float 4/4/4 32 0&土1.2e土38
            • 有 double/longdouble有8有16 8/8/8 64 0&土1.2e土308
          • 字符(变量)
            • 有 [signed] char 1/1/1 8 -128~127 -2^7 ~ 2^7-1
            • 无 unsigned char 1/1/1 8 0~255 0~ 2^8-1
          • 字符(常量)     /4/ 字节
          • 指针类型
            • 指针 无 char*/int*(指针变量) 2/4/8 32
        • (1)指针大小
          • 指针的大小是由内存寻址空间决定的,即地址总线决定。
          • char*(即指针变量): 4个字节(32位的寻址空间是2^32,即32个bit,也就是4个字节。同理64位编译器)
          • 一般32位机寻址空间4G,所以指针占4字节;
          • 一般16位的单片机寻址空间是64k,所以指针占2字节。
        • (2)字符型符号
          • C标准规定为 Implementation Defined(由实作环境决定)
          • •arm-linux-gcc规定 char 为 unsigned char
          • •vc 编译器、x86上的 gcc 规定 char 为 signed char
          • 缺省情况下,编译器默认数据为signed类型,但是char类型除外。
          • 为了代码移植,一定不要用 char,用 signed char
        • (3)铁则
          • int,long int,short int的宽度都可能随编译器而异。
          • 但有几条铁定的原则(ANSI/ISO制订的):
          • •sizeof(shortint)<=sizeof(int)
          • •sizeof(int)<=sizeof(longint)
          • •shortint至少应为16位(2字节)
          • •longint至少应为32位。
    • 2.2.2  常量
        • 在程序运行中,其值不能被改变的量称为常量。
      • 2.2.2.1  分类
        • 符号常量:
          • 用标识符代表常量
          • 定义格式: #define   符号常量   常量
            • 一般用大写字母
            • 是宏定义预处理命令,不是C语句
        • 直接常量:
          • 整型常量
          • 实型常量
          • 字符常量
          • 字符串常量
          • 符号常量
      • 2.2.2.2  整型常量
        • (1)整型常量有3种形式:十进制整型常量、八进制整型常量和十六进制整型常量。
        • (注意:c语言中没有直接表示二进制的整型常量,在c语言源程序中不会出现二进制。)
        • 整型常量后可以用u或U明确说明为无符号整型;用l或L明确说明为长整型数
          •   二进制:
            • 所有数字由0,1构成,逢二进一,二进制数中不会出现2.。
            • 例:110101
          •   八进制:
            • 以数字0(注意不是以字母O,o)开头,所有数字由0~7构成,逢八进一,八进制数中不会出现8。
            • 八进制整型常量:051 ,-026 ,0773 等
          •   十进制:
            • 所有数字由0~9构成,逢十进一,十进制数中不会出现10。
            • 十进制整型常量:123 , 0 ,-24 , 85L(长整型常量)等
          •   十六进制:以0x或者0X(数字0加字母x)开头,
            • 所有数字由0~9,A~F(或者a~f)构成,逢十六进一
            • (其中A、B、C、D、E、F分别代表10、11、12、13、14、15)
            • 十六进制整型常量:0x55 , 0x1101 , 0x , 0x5AC0 , -0xFF。
      • 2.2.2.3  实型常量
        • 实型常量有两种表示形式:小数形式和指数形式。
        • 编码形式存储。
          • 小数形式:5.4   0.074     -23.0
          • 指数形式:5.4e04.3e-3 -3.3e4 -3.3E4    e可大小写
        • (1)小数部分为0的实型常量,可以写为453.0 或453。
        • (2)用小数表示时,小数点的两边必须有数,不能写成“ .453“和“453.“,而应该写成“0.453“和“453.0“。
        • (3)用指数写法时,e前必须有数字,e后面的指数必须为整数
        • (注意:整数阶码可以是正数,负数,也可以是八进制数、十六进制数,但必须为整数)。
      • 2.2.2.4  字符常量
        • 字符常量的标志是一对单引号‘ ’,c语言中的字符常量有两类:
        • (1)    由一对单引号括起来的一个字符,如‘a ’, ‘r’,‘#’。
          • 注意: ′a′ 和 ′A′ 是两个不同的字符常量。
          • '1' 是字符占一个字节,"1"是字符串占两个字节(含有一个结束符号)。
          • '0' 的ASCII数值表示为48,'a' 的ASCII数值是97,'A'的ASCII数值是65。
          • 一般考试表示单个字符错误的形式:'65'    "1"
          • 字符是可以进行算术运算的,记住:'0'-0=48
          • 大写字母和小写字母转换的方法:'A'+32='a'  相互之间相差32。
        • (2)由一对单引号括起来,以反斜杠\开头,后跟若干数字或者字母,比如‘\n’,其中“\“是转义的意思,后面跟不同的字符表示不同的意思,这类字符常量叫转义字符。具体如图所示 。
          • ‘A’  =  ‘\101’ =  ‘\x41’  =  65
          • 在程序中 int a = 0x6d,是把一个十六进制的数给变量a 注意这里的0x必须存在。
          • 在程序中 int a = 06d, 是一个八进制的形式。
          • 在转义字符中
          • ’\x6d’ 才是合法的,0不能写,并且x是小写。
          • ‘\141’ 是合法的, 0是不能写的。
          • ‘\108’是非法的,因为不可以出现8。
      • 2.2.2.5  字符串常量
        • C语言中,以双引号括起来的,由若干个字符组成的序列即为字符串常量。
          • 例:“nihao”   “happy”等等
        • 字符串的概念
        • 简单而言,字符串是若干有效字符的序列,可包含转义字符、ASCⅡ码表中的字符;
          • 形式为: 用双引号括起来的字符序列;
          • 例:"I am a  student."  , "Hello "
          • "a[5]="; "%f\n"。
        • 字符串的结束标志:‘\0’。
      • 2.2.2.6  符号常量
        • 符号常量是由宏定义“#define“定义的常量,在C程序中可用标识符代表一个常量。
          • 例:计算圆的面积的c程序。
            • #include<stdio.h>
            • #define  PI  3.14159
            • main()
            • {
            • float r,s;
            • r=12.5;
            • s=PI*r*r;
            • printf("s= %f",s);
            • }
        • 说明:
          • #define 是宏定义,此程序中所有出现PI的地方都代表3.14159,同时PI称为符号常量。习惯上我们用大写字母来表示符号常量,小写字母表示变量,这样比较容易区别。
          • define  f(x)(x*x) 和 define f(x) x*x 之间的差别。一定要好好的注意这写容易错的地方,替换的时候有括号和没有括号是很大的区别。
    • 2.2.3  变量
      • 定义
        • 变量就是其值可以改变的量。变量要有变量名,在内存中占据一定的存储单元,存储单元里存放的是该变量的值。不同类型的变量其存储单元的大小不同,变量在使用前必须定义。
      • 2.2.3.1  整型变量
        • 整型变量分为4种:
          • 基本型(int)、
          • 短整型(short int 或short)、
          • 长整型(long int 或 long)
          • 无符号型(unsigned int ,unsigned short,unsigned long)。
        • 不同的编译系统对上述四种整型数据所占用的位数和数值范围有不同的规定。
        • 说明:
          • 单词signed来说明“有符号”(即有正负数之分),不写signed也隐含说明为有符号,unsigned用来说明“无符号”(只表示正数)。
      • 2.2.3.2  实型变量
        • C语言中,实型变量分为单精度类型( float )和双精度类型( double )两种。如:
          • float  a , b ;
          • double  m ;
        • 单精度实数提供7位有效数字,双精度实数提供15~16位有效数字。
        • 实型常量不分float型和double型,一个实型常量可以赋给一个float 型或double型变量,但变量根据其类型截取实型常量中相应的有效数字。
        • 注意:实型变量只能存放实型值,不能用整型变量存放实型值,也不能用实型变量存放整型值。
      • 2.2.3.3  字符变量
        • 字符变量用来存放字符常量,定义形式:
          • char  变量名;
        • 其中关键字char定义字符型数据类型,占用一个字节的存储单元。
          • 例:char  cr1,cr2;
          • cr1= ‘A’ , cr2=‘B’ ;
        • 将一个字符赋给一个字符变量时,并不是将该字符本身存储到内存中,而是将该字符对应的ASCII码存储到内存单元中。
          • 例如,字符 ′A′ 的ASCII码为65,在内存中的存放形式如下:01000001
        • 由于在内存中字符以ASCII码存放,它的存储形式和整数的存储形式类似,所以C语言中字符型数据与整型数据之间可以通用,一个字符能用字符的形式输出,也能用整数的形式输出,字符数据也能进行算术运算,此时相当于对它们的ASCII码进行运算。
      • 2.2.4.4  类型的自动转换和强制转换
        • 当同一表达式中各数据的类型不同时,编译程序会自动把它们转变成同一类型后再进行计算。转换优先级为:
          • 即下边级别“低“的类型向上边转换。具体地说,若在表达式中优先级最高的数据是double型,则此表达式中的其他数据均被转换成double型,且计算结果也是double型;若在表达式中优先级最高的数据是float型,则此表达式中的其他数据均被转换成float型,且计算结果也是float型。
          • 在做赋值运算时,若赋值号左右两边的类型不同,则赋值号右边的类型向左边的类型转换;当右边的类型高于左边的类型时,则在转换时对右边的数据进行截取。
        • 除自动转换外,还有强制转换,表示形式是:
          • 一般形式:(类型说明符)(表达式)
          • 功能:把表达式的运算结果强制转换成类型说明符所表示的类型
          • 例:(int)(a+b)
            • 一定是 (int)a 不是  int(a),注意类型上一定有括号的。
            • 注意(int)(a+b) 和(int)a+b 的区别。 前是把a+b转型,后是把a转型再加b。
            • 例1表达式(int)((double)(5/2)+2.5)的值是4。
              • (int)((double)(5/2)+2.5)
              • →(int)((double)2)+2.5)
              • →(int)(2.000000+2.5)
              • →(int)(4.500000)
              • →4。
            • 例2:以下程序运行后的输出结果是(3) 。
              • main()
              • {     int a;
              • a=(int)((double)(3/2)+0.5+(int)1.99*2);
              • printf("%d\n",a);
              • }
              • (3/2)=1,
              • (double)(3/2)+0.5=1.5,
              • (int)1.99*2=2,(double)(3/2)+0.5+(int)1.99*2=3.5,故a=3。
        • 三种取整丢小数的情况:
          • 1、int a=1.6;
          • 2、(int)a;
          • 3、1/2; 3/2; //除法
      • 2.2.4.5  全局变量和局部变量
        • 在C语言中,用户命名的标识符都有一个有效的作用域。
        • 所谓标识符的“作用域”就是指程序中的某一部分,在这部分中,该标识符是有定义的,可以被C编译和连接程序所识别。
        • 我们知道每个变量都有自己的作用域,在一个函数内定义的变量,不能在其他函数中引用。显然,变量的作用域与其定义语句在程序中出现的位置有直接的关系,据此变量可以划分为局部变量和全局变量。
        • 注意“定义”和 “说明”两个词的区别。“定义”是指给变量分配确定的存储单元;“说明”只是说明变量的性质。
        • (一)局部变量
          • 在一个函数内部定义的变量,它们只在本函数范围内有效,即只有本函数才能使用它们,其他函数不能使用这些变量,我们将这些变量称为“局部变量”。不同函数中可以使用相同名字的局部变量,它们代表不同的对象,在内存中占不同的单元,互不干扰。
        • (二)全局变量
          • 在函数之外定义的变量称为外部变量,外部变量是全局变量。全局变量可以为本文件中其他函数所共用,它的有效范围从定义变量开始到本文件结束。
          • 如果在同一个源文件中,外部变量与局部变量同名,则在局部变量的作用范围内,外部变量被“屏蔽”,即它不起作用。
        • 变量作用域和生存期
          • 生存期:什么时候这个变量开始出现了,到什么时候消亡了
          • 作用域: 在(代码的) 什么范围内可以访问这个变量(这个变量可以起作用)
          • 对于本地变量,这两个问题的答案是统一的:大括号内一块
      • 2.2.4.6  变量的存储类别
        • 变量值存在的时间(即生存期)
        • 静态存储方式和动态存储方式。
          • 所谓静态存储方式是指在程序运行期间分配固定的存储空间的方式,而动态存储方式是在程序运行期间根据需要动态分配存储空间的方式
          • 在内存中供用户使用的空间可以分为程序区、静态存储区和动态存储区3个部分。数据分别被存放在静态存储区和动态存储区中。静态存储区中存放的是全局变量,在程序开始执行时就给全局变量分配存储区。程序执行过程中它们占据固定的存储单元,程序执行完毕这些存储单元就被释放。
          • 每一个变量和函数所具有的属性是:数据的存储类别和数据类型(在前面已经介绍过)。所谓的存储类别指的是数据在内存中存储的方法,其可分为两类:静态存储类和动态存储类。具体包括自动(auto)、静态(static)、寄存器(register)和外部(extern),共4种。
        • (一)auto变量
          • 当在函数内部或复合语句内定义变量时,如果没有指定存储类别,或使用了auto说明符,系统就认为所定义的变量具有自动类别。如:
            • float a;等价于auto float a;
            • auto变量的存储单元被分配在内存的动态存储区,每当进入函数体(或复合语句)时,系统自动为auto变量分配存储单元,退出时自动释放这些存储单元另做他用。因此,这类局部变量的作用域是从定义的位置起,到函数体(或复合语句)结束止。
            • 所有自动类局部变量的存储单元都是在进入这些局部变量所在的函数体(或复合语句)时生成,退出其所在的函数体(或复合语句)时消失(变为无定义)。这就是自动类局部变量的“生存期“。当再次进入函数体(或复合语句)时,系统将为它们另行分配存储单元,因此变量的值不可能被保留。随着函数的频繁调用,动态存储区内为某个变量分配的存储单元位置会随程序的运行而改变。
        • (二)register变量
          • 寄存器变量也是自动类变量。它与auto变量的区别仅在于:用register说明变量是建议编译程序将变量的值保留在CPU的寄存器中,而不是像一般变量那样占用内存单元。程序运行时,访问寄存器内的值要比访问内存中的值快得多。因此,当程序对运行速度有较高要求时,把那些频繁引用的少数变量,指定为register变量,有助于提高程序运行的效率。
            • 说明:
              • (1)CPU中寄存器的数目是有限的,因此只能说明少量的寄存器变量。在同一个函数中,允许说明为寄存器变量的数目不仅取决于CPU的类型,也与所用的C编译程序有关。当没有足够的寄存器来存放指定的变量,或编译程序认为指定的变量不适合放在寄存器中时,将自动按auto变量来处理。因此,register说明只是对编译程序的一种建议,而不是强制性的。
              • (2)由于register变量的值存放在寄存器内而不是存放在内存中,所以register变量没有地址,也就不能对它实行求地址运算。
              • (3)register变量的说明应尽量靠近其使用的地方,用完之后尽快释放,以便提高寄存器的利用效率。
        • (三)静态存储类别的局部变量
          • 当函数体(或复合语句)内部用static来说明一个变量时,可以称该变量为静态局部变量。它与auto变量、register变量的本质区别是:
            • (1)在整个程序运行期间,静态局部变量在内存中的静态存储区中占据着永久性的存储单元。即使退出函数后,下次再进入该函数时,静态局部变量仍使用原来的存储单元。由于不释放这些存储单元,这些存储单元中的值得以保留,因而可以继续使用存储单元中原来的值。由此可知,静态局部变量的生存期将一直延长到程序运行结束。
            • (2)静态局部变量的初值是在编译时赋予的,在程序执行期间不再赋以初值。对未赋值的局部变量,C语言编译程序自动给它赋初值为0。
        • (四)用static声明外部变量
          • 有时在程序设计中希望某些外部变量只限于本文件使用,而不能被其他文件引用,这时可以在定义外部变量时加一个static声明。
            • 并不是对外部变量加上static才是静态存储而不加static的是动态存储。两种形式的外部变量都是静态存储方式,只是作用范围不同而已。
        • (五)用extern声明外部变量
          • ① 在一个文件内声明外部变量
            • 当全局变量定义在后,引用它的函数在前时,应该在引用它的函数中用extern对此全局变量进行说明,以便通知编译程序,该变量是一个已在外部定义了的全局变量,已经分配了存储单元,不需要为它另开辟存储单元。这时其作用域从extern说明处起,延伸到该函数末尾。
            • 全局变量的说明与全局变量的定义不同。变量的定义(开辟存储单元)只能出现一次,在定义全局变量时,不可使用extern说明符;而对全局变量的说明,则可以多次出现在需要的地方,这时必须用extern进行说明。
          • ② 在多文件的程序中声明外部变量
            • 当一个程序由多个单独编译的源文件组成,并且在每个文件中均需要引用同一个全局变量时,若在每个文件中都定义了所需的同名全局变量,则在“连接”时将会产生“重复定义”的错误。在这种情况下,单独编译每个文件时并无异常,编译程序将按定义分别给它们开辟存储空间,而当进行连接时,就会显示出错信息。解决的方法是:在其中一个文件中定义所有全局变量,而在其他用到这些全局变量的文件中用extern对这些变量进行说明。
  • 2.3构造类型
    • 2.3.1  数组
      • 2.3.1.1  一维数组的定义和引用
        • (一)数组的概念
          • 数组是由属于同一个数据类型的有序数据集构成的。数组中的每一个数据称为“元素“。可以用一个统一的数组名和下标来唯一地标识数组中的元素。
        • (二)一维数组的定义
          • 一维数组的定义方式为:
          • 类型说明符  数组名[常量表达式];
          • 如:
            • char c[20];
          • c是数组名,此数组共有20个元素,并且每个元素的类型都为字符型。
        • (三)一维数组元素的引用
          • 数组元素的表示形式为:
            • 数组名[下标];
              • 引用数组元素时,数组的下标可以是整型常量,也可以是整型表达式。
              • 和变量一样,数组必须先定义后使用。数组元素只能逐个引用而不能把数组当做一个整体一次引用。
        • (四)一维数组的初始化
          • 当数组定义后,系统会为该数组在内存中开辟一串连续的存储单元,但这些存储单元中并没有确定的值。可以在定义数组时为所包含的数组元素赋初值,如:
            • int a[6]={ 0,1,2,3,4,5 };
            • 所赋初值放在一对花括号中,数值类型必须与所说明类型一致。所赋初值之间用逗号隔开,系统将按这些数值的排列顺序,从a[0]元素开始依次给a数组中的元素赋初值。以上语句将a[0]赋值0,a[1]赋值1, …… ,a[5]赋值5。在指定初值时,第一个初值必定赋给下标为0的元素。也就是说数组元素的下标是从0开始的。同时,不可能跳过前面的元素给后面的元素赋初值,但是允许为前面元素赋值为0。当所赋初值个数少于所定义数组的元素个数时,将自动给后面的其他元素补以初值0;当所赋初值个数多于所定义数组的元素个数时,也就是说超出了数组已经定义的范围,在编译时系统将给出出错信息。
          • C语言规定可以通过赋初值来定义数组的大小,这时一对方括号中可以不指定数组大小。
    • 2.3.1.2  一维数组与函数
      • 一、 一维数组元素作为实参
        • 无论是一维数组元素或者二维数组元素,和普通变量的使用没有任何区别,他们之间仅仅是变量名不同,一维数组元素作为实参传递给形参,形参数据改变不会影响实参变化。
      • 二、一维数组元素地址作为实参
        • 一维数组元素的地址作为实参,对应的形参必须是与实参基类型相同的指针变量。此时可以通过被调用函数改变调用函数中的数据。
          • 例 有以下程序段
            • #include<stdio.h>
            • voidfun(int x,int *p)
            • {
            • x*=2;
            • p[0]=p[-1]+p[1];
            • }
            • main()
            • {
            • int a[10]={1,2,3,4,5,6,7,8,9,10};
            • fun(a[2],&a[6]);
            • printf("%d  %d\n",a[2],a[6]);
            • }
              • 程序运行后的输出结果是 3  14
              • 本题主函数中有函数调用 fun (a[2],&a[6]);
              • 第一个传递的实参是a[2]的值3,对原数不影响
              • 第二个传递的实参是&a[6] (a[6]的地址),指针变量p存储a[6]的地址,根据指针变量加下标表示数据的方法,p[0]存储的是a[6]的值,p[-1]存储的是a[5]的值6,p[1]存储的是a[7]的值8。经过p[0]=p[-1]+p[1]计算;值为14。
      • 三、一维数组名作为实参
        • 一维数组名为地址常量,表示数组的首地址,如果一维数组名作为实参,对应的形参应该是一个指针变量,此指针变量的基本类型必须与数组的类型一致。
        • 通常被调用函数的首部可以有以下3中方式:
          • (1)fun(int *a)
          • (2)fun(int a[N])
          • (3)fun(int a[ ])
            • 例  有以下程序段
              • #include<stdio.h>
              • int fun(int *x,int n)
              • {
              • int i , sum = 0 ;
              • for (i=0;i<n;i++)
              • sum=sum+x[i];
              • return sum;
              • }
              • main()
              • {
              • int a[]={1,2,3,4,5 },s=0;
              • s=fun (a,5);
              • printf("%d  \n",s);
              • }
                • 程序运行后的输出结果是15
                • 本题main函数中定义了一维数组a,含有5个int类型的数组元素,所以地址常量a的基本类型为int类型,对应的形参x的基本类型也是int类型,可以进行参数的传递,传递后依然用指针变量加下标的方式表示数据。
    • 2.3.1.3 二维数组的定义和引用
    • (一)二维数组的定义
      • 在C语言中,二维数组中元素排列的顺序是:按行存放,即在内存中先顺序存放第一行的元素,再存放第二行的元素。因此,二维数组元素的存储与一维数组元素存储相类似,总是占用一块连续的内存单元。
      • 二维数组的一般形式为:
        • 类型说明符  数组名[常量表达式][常量表达式];
        • 如:int c[3][4]; 定义c为3×4 (3行4列)的数组。
          • 注意:不能写成c[3,4]。C语言对二维数组采用这样的定义方式:我们可以把二维数组当做是一种特殊的一维数组。
            • 例如,可以把c看成是一个一维数组,它有3个元素c[0]、c[1]、c[2],每个元素又是一个包含4个元素的一维数组。可以把c[0]、c[1]、c[2]看做是3个一维数组的名字。
    • (二)二维数组的引用
      • 二维数组的表示形式为:
        • 数组名[下标][下标]
          • 数组的下标可以是整型表达式,如 c[3-1][3×2-2];
      • 数组元素可以出现在表达式中,也可以被赋值。
      • 定义数组时用的c[3][4]和引用元素时的c[3][4]的区别:前者用来定义数组的维数和各维的大小,共有3行4列;后者中的3和4是下标值,c[3][4]代表该数组中的一个元素。如果a[3][4]是二维数组中最后一个元素,那么该数组共有4行5列。
    • (三)二维数组的初始化
      • 可以在定义二维数组的同时给二维数组的各元素赋初值。
        • 如:
          • float m[2][2]={{1.5,3.2},{0.8}};
      • 全部初值放在一对花括号中,每一行的初值又分别括在一对花括号中,之间用逗号隔开。当某行一对花括号内的初值个数少于该行中元素的个数时,系统将自动地给后面的元素补初值0。同样,不能跳过每行前面的元素而给后面的元素赋初值。
    • (四)通过赋初值定义二维数组的大小
      • 对于一维数组,可以在数组定义语句中省略方括号中的常量表达式,通过所赋初值的个数来确定数组的大小;对于二维数组,只可以省略第一个方括号中的常量表达式,而不能省略第二个方括号中的常量表达式。
      • 如:
        • int a[][3]={{1,2,3},{4,5},{6},{8}};
      • a数组的第一维方括号中的常量表达式省略,在所赋初值中,含有4个花括号,则第一维的大小由花括号的个数来决定。因此,该数组其实是与a[4][3]等价的。当用以下形式赋初值时:
        • int c[][3]={1,2,3,4,5};
      • 第一维的大小按以下规则决定:
        • (1)当初值的个数能被第二维的常量表达式的值除尽时,所得商数就是第一维的大小。
        • (2)当初值的个数不能被第二维的常量表达式的值除尽时,则:
          • 第一维的大小 =所得商数 + 1 。
      • 因此,按此规则,以上c数组第一维的大小应该是2,也就是说语句等同于
      • int c[2][3]={{1,2,3},{4,5}};。
    • 2.3.1.4 二维数组与函数
      • 二维数组名作为实参
        • 二维数组元素作为实参,与普通变量作为实参没有任何区别,二维数组名为行指针常量,如果二维数组名作为实参,对应的形参必须是一个行指针变量。
        • 例如:有以下定义和函数调用语句:
          • #define  M  5
          • #define  N  3
          • main()
          • {
          • int s[M][N];
          • …….
          • fun(s);
          • …….
          • }
          • 此时,实参为二维数组名(即行指针),则fun函数的首部可以是以下3中形式之一:
            • (1)fun ( int  (*a)[N])
            • (2) fun ( int  a[ ][N])
            • (3) fun ( int  a[M][N])
          • 注意:行下标可以省略,列下标不可以省略。无论哪种方式,系统都把a看作一个行指针变量。
      • 二、指针数组名作为实参
        • 指针数组名师指向指针的指针常量,因此当指针数组名作为实参时,对应的形参应该为一个指向指针的指针变量。
        • 例如:有以下定义和函数调用语句:
          • #define  M  5
          • #define  N  3
          • main()
          • {
          • int  s[M][N],*p[M];
          • …….
          • for(i=0;i<M;i++)  p[i]=s[i];
          • fun(p);
          • …….
          • }
          • 此时,实参为一维数组名(即行指针),则fun函数的首部可以是以下3中形式之一:
            • (2)fun ( int  *a[M])
            • (2) fun ( int   *a[ ])
            • (3) fun ( int  **a)
      • 2.3.1.5  字符数组
        • 注:C语言无字符串类型,字符串是存放在字符数组中的。
        • (一)字符数组的定义
          • 字符数组就是数组中的每个元素都是字符,定义方法同普通数组的定义相同,即逐个对数组元素赋值。如:
          • char c[11];
          • c为该数组名,该数组共有11个元素,并且每个元素都为字符型。
        • (二)字符数组的初始化及引用
          • 对字符数组初始化,可逐个元素地赋值,即把字符逐个赋给数组元素。如:
            • char a[9]={ ′T′, ′h′, ′a′, ′n′, ′k′, ′′, ′y′, ′o′,′u′};
            • 如果花括号中提供的初值个数(即字符个数)大于数组长度,则按语法错误处理。
            • 如果初值个数小于数组长度,则将这些字符赋给数组中前面那些元素,其余的元素自动定为空字符(′\0′)。如:
              • char c[6]={′G′,′o′,′o′,′d′};
            • 字符数组的引用形式与其他数组的引用形式相同,采用下标引用,
              • 即 数组名[下标]。
                • 例如:
                • #include<stdio.h>
                • main()
                • { char c[9]={'T','h','a','n','k',',','y','o','u'};
                • int i;
                • for(i=0; i<9; i++)
                • printf(“%c“,c[i]);
                • }
                • 输出的结果为Thank,you。
        • (三)字符串和字符串结束标志
          • C语言中,将字符串作为字符数组来处理。为了测定字符串的实际长度,C语言规定了一个字符串结束标志,以字符'\0'代表。就是说,在遇到字符'\0'时,表示字符串结束,由它前面的字符组成字符串。
          • 系统对字符串常量也自动加一个'\0'作为结束符。
            • 例如:
            • char c[]=“c program“;
            • 数组c共有9个字符,但在内存中占10个字节,最后一个字节'\0'是由系统自动加上的。有了结束标志'\0'后,在程序中往往依靠检测 '\0'的位置来判定字符串是否结束,而不是根据数组的长度来决定字符串长度。
            • 说明:'\0'代表ASCII码为0的字符,是一个“空操作符“,它什么也不干。在输出时也不输出'\0',它只是一个结束的标志。
        • (四)字符数组的初始化
          • 方法:将字符常量以逗号分隔写在花括号中
          • ①在定义字符数组时进行初始化
            • charch[7]={‘s’,’t’,’u’,’d’,’e’,’n’,’t’};
          • ②在对全部元素指定初值时,可省写数组长度。
            • char ch[]={‘s’,’t’,’u’,’d’,’e’,’n’,’t’};
          • ③如果花括弧内提供的初值个数大于数组长度?
        • (五)用字符串来直接初始化字符数组
          • 可直接把字符串写在花括号中来初始化字符数组.
            • 如:charch[9]={“student”};
          • 系统将双引号括起来的字符依次赋给字符数组的各个元素, 并自动在末尾补上字符串结束标志字符'\0'。
          • 几点说明:
            • (1)字符串结束标志'\0'仅用于判断字符串是否结束,输出字符串时不会输出。
            • (2)在对有确定大小的字符数组用字符串初始化时,数组长度应大于字符串长度。如: char s[7]={"student"};是错误的.
            • (3)在初始化一个一维字符数组时,可以省略花括号。
            • 如:  char s[8]="student";
            • ( 4 )不能直接将字符串赋值给字符数组名。下面的操作是错误的。
            • 如:  s=”student”;
      • 2.3.1.6 注意
        • 两种重要的数组长度
          • char a[]={‘a’,’b’,’c’};   //不安全,无 \0
            • 数组长度为3,字符串长度不定。sizeof(a)为3。
          • char a[5]={ ‘a’,’b’,’c’}
            • 数组长度为5,字符串长度3。sizeof(a)为5。
            • #include"stdio.h"
            • #include"string.h"
            • main()
            • {
            • char m[] = "abc";
            • //     char n[] = {'a','b','c','\0'};       //4 4 3 3
            • char n[] = {'a','b','c'};            //4 3  3 3
            • printf("%d %d\n",sizeof(m),sizeof(n));
            • printf("%d %d\n",strlen(m),strlen(n));
            • return 0;
            • }
        • 数组的重要概念
          • 对a[10]这个数组的讨论。
            • 1、a表示数组名,是第一个元素的地址,也就是元素a[0]的地址。
            • 2、a是地址常量,所以只要出现a++,或者是a=a+2赋值的都是错误的。
            • 3、a是一维数组名,所以它是列指针,也就是说a+1是跳一列。
          • 对a[3][3]的讨论。
            • 1、a表示数组名,是第一个元素的地址,也就是元素a[0] [0]的地址。
            • 2、a是地址常量,所以只要出现a++,或者是a=a+2赋值的都是错误的。
            • 3、a是二维数组名,所以它是行指针,也就是说a+1是跳一行。
            • 4、a[0]、a[1]、a[2]也都是地址常量,不可以对它进行赋值操作,同时它们都是列指针,a[0]+1,a[1]+1,a[2]+1都是跳一列。
            • 5、注意a和a[0] 、a[1]、a[2]是不同的,它们的基类型是不同的。前者是一行元素,后三者是一列元素。
        • 数组的初始化
          • 一维和二维的,一维可以不写,二维第二个一定要写
            • int a[]={1,2} 合法。  
            • int a[][4]={2,3,4}合法。  
            • int a[4][]={2,3,4}非法。
        • 二维数组中的行指针
          • int a[1][2];
          • 其中a现在就是一个行指针,a+1跳一行数组元素。 搭配(*p)[2]指针
          • a[0],a[1]现在就是一个列指针。a[0]+1 跳一个数组元素。搭配*p[2]指针数组使用
        • 脱衣服法则
          • a[2]  变成  *(a+2)
          • a[2][3]变成 *(a+2)[3]再可以变成  *(*(a+2)+3)
          • 这个思想很重要!
    • 2.3.2结构体
      • 2.3.2.1  结构体类型
        • 在实际工作中,当我们需要把一些不同类型,但相互之间又存在着联系的信息组合应用时,就要用到结构体。结构体是一种看似复杂但却非常灵活的构造型数据类型。在通常情况下,一个结构体类型由若干个称为成员(或称为域)的部分组成。不同的结构体类型可根据需要由不同的成员组成。但对于某个具体的结构体类型,其成员的数量必须固定,这一点与数组相同;但该结构体中各个成员的类型可以不同,这是结构体与数组的重要区别。例如,我们常用的“时间“可以由以下3个部分描述:小时(hour)、分(minute)、秒(second)。它们都可以用整型数表示,可以把这3个成员组成一个整体,并给它取名为time,这就是一个简单的结构体。
      • 2.3.2.2  结构体声明
        • 声明一个结构体类型的一般形式为:
          • struct  结构体名
          • {  成员表列  };
          • struct是C语言中的关键字,是结构体类型的标志。“结构体名“用做结构体类型的标志,它又称“结构体标记“(structure tag)。大括弧内是该结构体中的各个成员,成员表列是由若干个变量类型名及变量名组成的。这些成员共同组成一个结构体。
            • 例如,上面提到的“时间“结构体类型可以说明如下:
              • structtime
              • {
              • inthour;
              • intminute;
              • intsecond;
              • };
                • 其中,time就是结构体名, hour、minute、second都是成员,并且各成员都应进行类型声明,每个成员也就是结构体中的一个域。
          • 成员名命名规则与变量名相同。所以结构体类型也可以用以下形式说明:
            • struct 结构体标识名
            • {
            • 类型名1    结构体成员名表1;
            • 类型名2    结构体成员名表2;
            • ……
            • 类型名n    结构体成员名表n;
            • };
            • 说明:
              • (1)“结构体标识名”和“结构体成员名表”都必须是合法的用户定义的标识符。
              • (2)每个“结构体成员名表“中都可以含有多个同类型的成员名,它们之间以逗号分隔。
              • (3)结构体类型说明中的“类型名1”~“类型名n”,不仅可以是简单数据类型,也可以是某种结构体类型。当结构体说明中又包含结构体时,称为结构体的嵌套。
              • (4)ANSI C标准规定结构体至多允许嵌套15层,并且允许内嵌结构体成员的名字与外层成员的名字相同。
      • 2.3.2.3  结构体类型变量的定义
        • 前面只是指定了一个结构体类型,为了能在程序中使用结构体类型的数据,就需要定义结构体类型的变量,并在其中存放具体的数据。可以用如下方法定义结构体类型变量。
          • (一)先声明结构体类型再定义变量名
            • 如上面已经定义了一个结构体类型struct  time,可以如下定义:
            • struct time    time1,time2;
            • 结构体类型名    结构体变量名;
            • time1和time2为struct time类型变量,即它们都具有struct time类型的结构。
          • (二)在声明类型的同时定义变量
            • 其一般形式为:
            • struct  结构体名 { 成员表列 } 变量名表列;
          • (三)直接定义结构体类型变量
            • 其一般形式为: struct { 成员表列 } 变量名表列;
            • 即不出现结构体名。
            • 类型与变量是两个不同的概念,使用时应注意区别。只能对变量赋值、存取或运算,而不能对一个类型进行赋值、存取或运算。可以单独使用结构体中的成员,它与普通变量的作用相同。
      • 2.3.2.4  结构体变量引用
        • 在定义了结构体变量以后,当然可以引用这个变量。但应注意:
        • (1)结构体变量不能作为一个整体而对其进行任何操作,只能对结构体变量中的各个成员分别进行输入和输出等操作。结构体变量中的成员用以下方式引用:
          • 结构体变量名.成员名
        • (2)如果结构体的某个成员本身又是一个结构体类型,则可以使用若干个成员运算符一级一级地找到最低的一级成员,只能对最低一级的成员进行赋值或存取及运算。
        • (3)结构体变量的初始化,是指逐个对结构体变量的各个成员进行初始化的过程。
      • 2.3.2.5  结构体数组
        • 和普通数组一样,结构体数组中的每个元素都属于同一数据类型(结构体类型),只不过各个元素本身又都包含多个成员项。例如,一个结构体变量中存放着一组数据(如某产品的名称、型号、尺寸、颜色等数据),现在如果有10个这样产品的数据需要参加运算,显然应当用到结构体数组。和定义结构体变量的方法相仿,只需说明其为数组即可。
        • 其一般形式为:
          • struct  结构体变量名 { 成员表列} 数组名[常量表达式];
        • 结构体数组的初始化
          • 结构体数组的初始值应顺序地放在一对花括号中,由于数组中的每一个元素都是一个结构体,因此通常将其成员的值依次放在一对花括号中,以便区分各个元素。
      • 2.3.2.6 指向结构体类型数据的指针
        • 一个结构体变量的指针就是用来指向该结构体类型的存储单元,并指向结构体变量所占据的内存段的起始地址。
        • (一)指向结构体变量的指针
          • 结构体变量.成员名”、“(*结构体指针变量名).成员名“和“结构体指针变量名->成员名”这3种形式是等价的,其中“->“称为指向运算符,它由两部分组成:“-”减号和“>”大于号,它们之间不能有空格
          • 看下面的例子:
            • #include<stdio.h>
            • #include<string.h>
            • main()
            • {
            • struct objects
            • {
            • char name[20];
            • int size;
            • char color[10];
            • float weight;
            • float height;
            • };
            • struct objects obj1;
            • struct objects *p;
            • p=&obj1;
            • strcpy(obj1.name,"pen");
            • obj1.size=10;
            • strcpy(obj1.color,"black");
            • obj1.weight=50.5;
            • obj1.height=18.5;
            • printf("name: %s\nsize: %d\ncolor:%s\nweight: %f\nheight:%f\n",obj1.name,obj1.size,obj1.color,obj1.weight,obj1.height);   printf("name: %s\nsize: %d\ncolor: %s\nweight:%f\nheight: %f\n",(*p).name,(*p).size,(*p).color,(*p).weight,(*p).height);
            • }
            • 我们声明了一个struct objects类型,并且定义了一个该类型的变量obj1,又定义了一个指向struct objects类型的数据的指针p,并且将p指向obj1,接下来是对各成员赋值。第一个printf语句用“.”的方式将obj1的成员的值输出。第二个printf语句用(*p)将obj1的成员的值输出,因为成员运算符“.”的优先级高于“*”运算符,所以(*p)的两侧的圆括号不能省略。以上两个printf函数语句的输出结果是相同的,我们可用p->name来代替(*p).name
        • (二)指向结构体数组的指针
          • 结构体数组及其元素也可以用指针变量来指向。在使用指针变量指向结构体数组时,只要把该结构体数组中的每个元素当做普通的结构体变量使用就可以了,例如:
            • #include<stdio.h>
            • #include<string.h>
            • structobjects
            • {
            • char name[20];
            • int size;
            • char color[10];
            • float weight;
            • float height;
            • };
            • structobjects obj[3]= {{"pen",10,"black",50.5,18.5},
            • {"notebook",20,"blue",180,19.5},
            • {"bag",50,"red",2000,37.5}};
            • main()
            • {
            • struct objects *p;
            • printf("name size color weightheight\n");
            • for(p=obj;p<obj+3;p++)
            • printf("%10s%d%-20s%6.5f%6.5f\n",p->name,p->size,p->color,p->weight,p->height);
            • }
          • 这样就可以利用指针变量来逐个把结构体数组中的元素的各个域输出。
          • 说明:
            • 如果p的初值为obj,即指向第一个元素,则p+1就指向下一个元素。
              • 例如:
                • (++p)->name;先使p自加1,然后得到它指向的元素中的name成员值。
                • 而(p++)->name;先得到p->name的值,然后使p自加1,指向obj[1];。
                • p只能指向一个structobjects类型的数据,不能指向obj数组元素中的某一成员(即p的地址不是成员的地址)。例如,p=&obj[1].name;是不对的。对结构体变量中的每个成员,都可以像普通变量一样,对它进行同类变量所允许的任何操作。
        • (三)用结构体变量和指向结构体的指针作为函数参数
          • 将一个结构体变量的值传递给另一个函数,有如下方法:
            • (1)结构体变量的成员作为实参传递给主调函数。
            • (2)可以用结构体变量作为一个整体实参。
            • (3)C语言中,允许将结构体变量的地址作为实参传递,这时,对应的形参应该是一个基类型相同的结构体类型的指针。
    • 2.3.3共用体
      • 共用体的类型说明和变量的定义方式与结构体的类型说明和变量定义的方式完全相同。不同的是,结构体中的成员各自占有自己的存储空间,而共用体的变量中的所有成员占有同一个存储空间。可以把一个整型变量、一个字符型变量、一个实型变量放在同一个地址开始的内存单元中。以上3个变量在内存中所占的字节数不同,但都从同一个起始地址开始存放,也就是使用覆盖技术,几个变量相互覆盖。
      • (一)共用体类型的说明
        • 共用体类型说明的一般形式为:
          • union 共用体标识名
          • {
          • 类型名1共用体成员名1;
          • 类型名2共用体成员名2;
          • .
          • 类型名n共用体成员名n;
          • };
            • 例如:
              • unionexample
              • {
              • int a;
              • float b;
              • char  c;
              • };
            • 其中,union是关键字,是共用体类型的标志,example是共用体标识名。“共用体标识名“和“共用体成员名“都是由用户定义的合法标识符,按语法规定共用体标识名是可选项,在说明中可以不出现。
        • (二)共用体变量的定义
          • 和结构体相似,共用体变量的定义也可采用3种方式,一种方法如下:
            • union un
            • {
            • int i;
            • float x;
            • }s1,s2,*p;
          • 说明:
            • (1)共用体变量在定义的同时只能用第一个成员的类型的值进行初始化。
            • (2)“共用体“与“结构体“的定义形式相似,但它们的含义是不同的。结构体变量所占内存长度是各成员占的内存长度之和,每个成员分别占有其自己的内存单元,而共用体变量所占的内存长度等于变量中所占字节最长的成员的长度。例如,上面的共用体占4字节(因为一个实型变量占4字节)。
        • (三)共用体变量中成员的引用
          • 共用体变量中每个成员的引用方式与结构体完全相同,可以使用以下3种形式之一:
            • (1)共用体变量.成员名
            • (2)(*共用体指针变量名).成员名
            • (3)共用体指针变量名->成员名
          • 共用体中的成员变量同样可参与其所属类型允许的任何操作,但在访问共用体成员时应注意:共用体变量中起作用的是最近一次存入的成员变量的值,原有成员变量的值将被覆盖。
          • 另外,ANSIC标准允许在两个类型相同的共用体变量之间进行赋值操作。同结构体变量一样,共用体类型的变量可以作为实参进行传递,也可以传递共用体变量的地址。
          • 共用体的考查:
            • union TT
            • { int a;
            • char ch[2];}
            • 考点一:sizeof (structTT) = 2;
            • 考点二:TT t1 ;  t1=0x1234;
            • 那么 ch[0]=0x 34;  ch[1]=0x12
  • 2.4指针类型
    • 2.4.1 关于地址和指针
      • 在内存区中每一个字节都有一个编号,这个编号就是“地址“,它相当于每个变量的房间号。变量的数据就存放在地址所标识的内存单元中,变量中的数据其实就相当于仓库中各个房间存放的货物。如果内存中没有对字节进行编号,系统将无法对内存进行管理。内存的存储空间是连续的,因此内存中的地址号也是连续的,并且用二进制数表示,为了直观起见,在这里我们将用二进制数进行描述。
      • 一般微机使用的C系统为整型变量分配4个字节,为实型变量分配4个字节,为字符型变量分配1个字节,为双精度类型变量分配8个字节。当某一变量被定义后,其内存中的地址也就确定了。
      • 在一般情况下,我们在程序中只需定义变量并指出变量名,无须去知道每个变量在内存中的具体地址,由C编译系统来完成每个变量与其具体地址发生联系的操作。在程序中我们对变量进行存取操作,实际上也就是对某个变量的地址存储单元进行操作。这种直接按变量的地址存取变量的方式称为“直接存取“方式。
      • 在C语言中,还可以用另一种称为“间接存取“的方式来完成对变量进行存取的操作,即将变量的地址存放在另一种类型的变量中,从而通过这种新的变量类型来得到变量的值。按C语言规定,可以在程序中定义整型变量、实型变量、字符变量等,也可以定义这样一种特殊的变量,它是专门用来存放地址的。
      • 由于通过地址能找到所需的变量单元,我们就可以说:地址“指向“该变量单元。 所谓“指向“就是通过地址来体现。
      • 在C语言中,将地址形象地称为“指针“,意思是通过它能找到以它为地址的内存单元,这里包含有一个方向指向的意思。一个变量的地址称为变量的“指针“。一个专门用来存放另一个变量的地址的变量(即指针),则称它为“指针变量“。变量的指针就是变量的地址。存放变量地址的变量是指针变量。即在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。因此,一个指针变量的值就是某个变量的地址或称为某变量的指针。
      • 为了表示指针变量和它所指向的变量之间的关系,在程序中用“*”符号表示“指向”,例如,i_p代表指针变量,而*i_p是i_p所指向的变量。因此,下面两个语句作用相同:i=3;  语句是把3赋值给变量单元i。*i_p=3;  语句是把3赋值给i_p指向的变量单元。两者都能正确存储数据。
    • 2.4.2 变量的指针和指向变量的指针变量
      • (一)指针变量的定义
        • 定义指针变量的一般形式如下:
          • 类型名  *指针变量名1,*指针变量名2,… ;
          • 如:int *p,*t;
            • int *p;和int* p;一样
        • 以上定义语句中,p和t都是合法用户标识符,在每个变量前的星号(*)是一个类型说明符,用来标识该变量是指针变量。
        • 变量前的星号不可省略,若省略了星号说明符,就变成了把p和t定义为整型变量(int是类型名)。在这里,说明了p和t是两个指向整型(int 类型)变量的指针,也就是说变量p和t中只能存放int类型变量的地址,这时我们称int是指针变量p和t的“基类型“。基类型用来指定该指针变量可以指向的变量的类型。
          • 例如:    int *p1;
            • 表示p1是一个指针变量,它的值是某个整型变量的地址。或者说p1指向一个整型变量。至于p1究竟指向哪一个整型变量,应由向p1赋予的地址来决定。
          • 再如:
            • int*p2;        /*p2是指向整型变量的指针变量*/
            • float*p3;      /*p3是指向浮点变量的指针变量*/
            • char*p4;       /*p4是指向字符变量的指针变量*/
              • 应该注意的是,一个指针变量只能指向同类型的变量,如P3 只能指向浮点变量,不能时而指向一个浮点变量,时而又指向一个字符变量。
      • (二)指针变量的引用
        • 指针变量中只能存放地址(指针),将一个整型变量(或任何其他非地址类型的数据)赋给一个指针变量是不允许的。
          • 如:
            • int  *p; /*定义一个指向整型变量的指针*/
            • p=300;  /*300为整数*/
            • 是不合法的赋值。
        • 与指针相关的两个运算符是“&“(取地址运算)和“*“(指针运算符)。
          • 1)       &:取地址运算符。
          • 2)       *:指针运算符(或称“间接访问” 运算符)。
        • C语言中提供了地址运算符&来表示变量的地址。
          • 其一般形式为:
            • &变量名;
            • 如:
              • int   i,*p;
              • P=&i;
        • 注意
            • #include"stdio.h"
            • main1()          //错误:运行时出错,P没有指向明确的单元,p是随机数,非常危险,万一是重要地址
            • {
            • int *p;            //解决方法 int k, *p = &k;                        inta, *p;        p = &a;是对的
            • *p = 5;
            • printf("%d\n", *p);
            • }
            • //*p=NULL是将p指向的内存赋值为NUll,而p本身不会变
            • //p=NULL是改变了p本身的值,将它指向的地址改为NULL,
            • //所以在使用地址指针的时候出错的时候,都是将指针变量赋值为NULL的
            • swap(int*p1, int *p2)           //错误,运行报错,指针变量在使用前必须赋值
            • {
            • int*p;                   // 和int *p;   *p = 5;一个错误 ,p指向不明确 ,即不知道5存在哪
            • //  int k; p = &k;         //改正
            • *p =*p1;
            • *p1= *p2;
            • *p2= *p;
            • }
            • main2()
            • {
            • int a, b;
            • int *ip1, *ip2;
            • scanf("%d%d",&a, &b);
            • ip1 = &a; ip2 = &b;
            • if(a < b)
            • swap(ip1,ip2);
            • printf("%d %d\n", a, b);
            • }
            • main()                   //错的,指针类型不同,编译报错 [Error] cannot convert 'float*' to 'int*' in assignment
            • {
            • int*p;
            • floatc;
            • p =&c;
            • }
            • /*
            • int a[5] = {1, 2, 3, 4, 5}, i; //a地址常量,数组首地址   a[i]电脑存储方式 *(a + i)
            • int *p = a;
            • 下标法                                指针法
            • 第1个元素:
            • 地址:    a            p                      a                   p
            • &a[0]      &p[0]
            • 内容:    a[0]  p[0]               *a                  *p
            • 第i个元素:
            • 地址:    &a[i]       &p[i]                  a +i       p + i
            • 内容:    a[i]   p[i]                *a                  *(p + i)
            • */
            • inta[10];
            • printf("%p\n",&a);               //0xbff8dd44
            • printf("%p\n",a);              //0xbff8dd44
            • printf("%p\n",&a[0]);           //0xbff8dd44
            • printf("%p\n",&a[1]);           //0xbff8dd48
      • (三)指针变量作为函数参数
        • 前面的章节中介绍过,函数参数可以是整型、实型、字符型等数据,指针类型数据同样也可以作为函数参数来进行传递。它的作用是将一个变量的地址传送到另一个函数中,参与该函数的运算。
        • 如果想通过函数调用从而得到n个要改变的值,可以采用如下方法:
          • (1)在主调函数中设n个变量,分别用n个指针变量指向它们。
          • (2)然后将各个指针变量作为实参,即将这n个变量的地址传给所调用的函数的形参。
          • (3)通过形参指针变量的改变,从而改变这n个变量的值。
          • (4)主调函数中就可以使用这些改变了值的变量。
          • 形参指针变量的值的改变不能使实参指针变量的值发生改变。
    • 2.4.3 数组与指针
      • 一个数组包含若干个元素(变量),在定义时被分配了一段连续的内存单元。因此,可以用一个指针变量来指向数组的首地址,通过该首地址就可以依次找到其他数组元素,同样指针变量也可以指向数组中的某一个元素。所谓数组的指针是指数组的起始地址,数组元素的指针是各个数组元素的地址。
      • (一)指向数组元素的指针
        • C语言规定数组名代表数组的首地址,也就是数组中第0号元素的地址。有如下语句:
          • int c[10]={0};
          • int *p;
          • p=c;
        • 则语句p=c;与语句p=&c[0];是等价的。
        • 数组c不代表整个数组。上述“p=c;“的作用是把数组c的首地址赋值给指针变量p,而不是把数组c中各元素的值赋给p。
        • 定义指向数组元素的指针变量的方法,与定义指向变量的指针变量相同。例如:
          • int c[10],*p;
          • p=&c[5];
          • 指针变量p指向了数组c中下标为5的那个元素,即p用来保存c[5]的地址。
      • (二)通过指针引用数组元素
        • 按C语言的规定:如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的下一个元素(而不是将p的值简单加1),这里的加1是指增加一个长度单位(与数组基类型所占存储单元相同)。例如,数组元素是浮点型,每个元素占4个字节,则p+1意味着使p的值(是一个地址)加4个字节,以使它指向下一个元素。
        • 将++和--运算符用于指针变量是十分有效的,可以使用指针变量自动向前或向后移动,指向下一个或上一个数组元素。不过要小心利用,否则会导致内存错误。
      • (三)用数组名作为函数参数
        • 数组名可以用做函数的形参和实参。当数组名作为参数被传递时,若形参数组中各元素发生了变化,则原实参数组各元素的值也随之变化。因为数组名作为实参时,在调用函数时是把数组的首地址传送给形参,因此实参数组与形参数组共占一段内存单元。而如果用数组元素作为实参的情况就与用变量作为实参时一样,是“值传递“方式,单向传递,即使形参数组元素值发生了变化,原实参的数组元素值也不会受影响。
      • (四)指向多维数组的指针和指针变量
        • 有如下定义:
          • int c[3][4],*p;
        • c是该二维数组的数组名,它代表了整个数组的首地址,也就是第0行的首地址,c+1代表第1行的首地址,即c+1是c[1]的地址。c[0]、c[1]、c[2]既然是一维数组名,而C语言又规定了数组名代表数组的首地址,因此c[0]代表第0行的一维数组中第0列元素的地址,即&c[0][0]。同样地,c[1]的值是&c[1][0]。
        • 第0行第1列元素的地址怎么表示呢?可以用c[0]+1来表示。此时“c[0]+1“中的1代表1个列元素的字节数,即两个字节。如c[0]的值是2020,则c[0]+1的值是2022。
        • 根据上面的介绍,c[i]和*(c+i)等价,因此,c[0]+1和*(c+0)+1的值都是&c[0][1],都是用来表示第0行第1列的元素的地址。c[i]从形式上看是数组c中第i个元素。如果c是二维数组名,则c[i]代表一维数组名,c[i]本身并不占实际的内存单元,它也不存放数组中各个元素的值,它只是一个地址。c、c+i、c[i]、*(c+i)+j、c[i]+j都是地址,*(c[i]+j)、*(*(c+i)+j)是元素的值。
    • 2.4.5 字符串与指针
      • (一)字符串的表示形式
        • (1)用字符数组存放一个字符串,然后输出该字符串。
          • 例如:
            • char str[]=“I am a student??“;
            • printf(“%s\n“,str);
        • (2)用字符指针指向一个字符串。
          • 可以不定义数组,而定义一个字符指针,用字符指针指向字符串中的字符。
          • 例如:
            • char*str=“I am a student??“;
            • /*定义str为指针变量,并指向字符串的首地址*/
            • printf(“%s\n“,str);
          • 在这里没有使用字符数组,而是在程序中定义了一个字符指针变量str,并使该指针变量指向一个字符串的首地址。C语言对字符串常量是按字符数组进行处理的,在内存中开辟了一个字符数组来存放字符串常量。程序在定义字符指针变量str时,把字符串的首地址赋给str。str只能指向一个字符变量或其他字符类型数据,不能同时指向多个字符数据,更不能理解为把字符串中的全部字符存放到str中(指针变量只能存放地址)。在输出时,利用字符型指针str的移动来控制输出,直到遇到字符串结束标志'\0'为止。
          • 通过字符数组名或字符指针变量可以一次性输出的只有字符数组(即字符串),而对一个数值型的数组,是不能企图用数组名输出它的全部元素的,只能借助于循环逐个输出元素。
          • 显然,用%s可以控制对一个字符串进行整体的输入输出。对字符串中字符的存取,与操作其他数组的方法相同,既可以用下标方法,又可以用指针方法。
      • (二)字符串指针作函数参数
        • 将一个字符串从一个函数传递到另一个函数,可以用地址传递的办法,即用字符数组名作为参数或用指向字符串的指针变量作为参数,进行传递。
        • 字符串指针变量作为函数实参,形参可以是字符指针变量,同样也可以是字符数组名。当字符数组名作为函数实参时,形参可以是字符数组名,同样也可以是字符指针变量。
      • (三)字符指针变量和字符数组的区别
        • 虽然字符数组和字符指针变量都能实现对字符的存储和运算,但它们两者之间有如下区别:
        • (1)字符数组是由若干个元素组成的,每个元素中存放一个字符,而字符指针变量中存放的是地址(字符串的首地址),绝不是将字符串的内容存放到字符指针变量中。
        • (2)赋值方式。
          • 只能对字符数组各个元素赋值,不能用以下方法对字符数组赋值:
            • char str[20];
            • str=“Iam happy“;
          • 而对字符指针变量,可以采用以下方法赋值:
            • char *s;
            • s=“I amhappy??“;
        • (3)字符数组可以在定义时对其整体赋初值(即初始化),但在赋值语句中不能完成整体赋值。下面的做法是不允许的:
          • char s[30];
          • s[]=“I am so happy“;
          • 而字符指针变量既可以在定义时赋初值,也可以出现在赋值语句中,相对来说要比字符数组使用起来灵活一点。
        • (4)如果定义了一个字符数组,在编译时,系统会为它分配一段连续的内存单元,它的地址是确定的。而当定义了一个字符指针变量后,就要即时给该指针变量分配内存单元,该指针变量中可以存放一个地址值,也就是说,该指针变量可以指向一个字符型数据,但如果未对它赋以一个地址值,则它并未具体指向一个确定的字符数据。如:
          • char s[10];
          • scanf(“%s“,s);
          • 是可以的。但用下面的方法是极其危险的:
            • char *s;
            • scanf(“%s“,s);
          • 因为编译时虽然给指针变量s分配了内存单元,s的地址(即&s)已经指定了,但s的值并未指定。在s单元中是一个不可预料的值,在执行scanf()函数时要求将一个字符串输入到s所指向的一段内存单元中,即以s的值(地址)开始的一段内存单元。而s的值如今却是不可预料的,它可能指向内存中空白的存储区(未用的用户存储区),这样固然可以,但它也有可能指向内存中已存放指令或数据的有用内存段。这就会破坏程序,甚至破坏系统,造成严重的后果。应当这样:
            • char*a,s[10];
            • a=s;
            • scanf(“%s“,a);
            • 先使a有确定值,也就是使a指向一个数组的首地址,然后输入一个字符串,把它存放在以该地址开始的若干单元中。
        • (5)在程序中指针变量的值可以改变。例如:
          • char*s=“china”;
          • s=s+2;
          • 指针变量s的值可以改变,当要输出字符串时,从s当前所指向的单元开始输出各个字符(本题中从字符i开始输出),直到遇到'\0'为止。而数组名虽然代表了地址,但它的值是一个固定的值,是不能改变的。下面是错的:
          • char str[]={“china”};
          • str=str+2;
    • 2.4.6  指向函数的指针
      • 用函数指针变量调用函数
        • 我们已经知道,可以用指针变量指向整型变量、字符型变量、字符串、数组,同样指针变量也可以指向一个函数。编译时,一个函数将被分配给一个入口地址,这个入口地址就称为该函数的指针。因此,可以通过使用一个指向函数的指针变量调用此函数。
        • 说明:
          • (1)指向函数的指针变量的一般定义形式为:
            • 数据类型 (*指针变量名)( );
              • 如int(*s)();,“数据类型”指该函数返回值的类型。
          • (2)(*s)()表示定义了一个指向函数的指针变量,但目前它不是固定指向哪一个函数,而只是表示定义了这样一个类型的变量,它的作用是专门用来存放函数的入口地址。在程序中实现把某一个函数的地址赋给它,它就指向那一个函数,这样它的值也就确定了。在一个程序中,一个指针变量以先后指向不同的函数,也就是说指向函数的指针变量和普通指针变量一样,可以多次使用。
          • (3)在给函数指针变量赋值时,只需给出函数名而不必给出参数。如:
            • s=fun;  /*  fun为已有定义的有参函数*/
            • 因为是将函数入口地址赋给s,不涉及到参数的问题,不能写成:
            • s=fun(a,b);
          • (4)用函数指针变量调用函数时,只需将(*s)代替函数名即可(s为已经定义过的指向函数的指针变量名),在(*s)之后的括号中根据需要写上实参。
          • (5)对指向函数的指针变量,有些运算,如++s、--s、s+3等都是没有意义的。
        • 函数指针的用法(*f)()记住一个例子:
          • intadd(int x, int y)
          • {....}
          • main()
          • {int  (*f)();
          • f=add;
          • }
          • 赋值之后:合法的调用形式为
            • 1、add(2,3);
            • 2、f(2,3);
            • 3、(*f)(2,3)
    • 2.4.7 返回指针的函数
      • 返回指针值的函数
      • 一个函数的返回值可以是一个整型值、字符型值、实型值等,同样地,函数的返回值也可以是指针类型的数据,即地址。这种返回指针值的函数,一般定义形式为:
      • 类型名    *函数名(参数表);
      • 例如:
      • int*fun(int a,int b);
      • fun是函数名,调用它可以得到一个指向整型数据的指针(地址)。a、b是两个整形变量,是函数fun()的形参。注意:*fun在两侧没有括弧,在fun的两侧分别为*运算符和()运算符。而()优先级高于*,因此fun先与()结合,显然这是函数形式。这个函数前面有一个*,表示此函数是指针型函数。
    • 2.4.8 指针数组和指向指针的指针
      • (一)指针数组的概念
        • 若在一个数组中,其元素均为指针类型数据,这样的数组称为指针数组,也就是说,指针数组中的每一个元素都相当于一个指针变量。一维指针数组的定义形式为:
        • 类型名  *数组名[数组长度];
          • 例如:
          • int *a[10];
          • 由于[]运算符的优先级比*运算符的优先级高,因此a先与[10]结合,形成a[10]形式,然后与前面的“*“结合,“*“表示此数组是指针类型的,每个数组元素都可指向一个整型变量。
          • 指针数组的一个重要用途是可以用来指向若干个字符串。例如,在对库房物品进行管理时,想把物品名称放在一个数组中,然后对这些物品进行统计和查询,我们可以分别定义一些字符串来存放各物品名称,然后利用指针数组中的元素分别指向各字符串。如果还想对字符串排序,不必改动字符串的位置,只需改动指针数组中各元素的指向(即改变各元素的值,这些值是各字符串的首地址)。这样,各字符串的长度可以不同,而且移动指针变量的值(地址)要比移动字符串所花的时间少。
      • (二)指向指针的指针
        • 指向指针数据的指针变量,简称为指向指针的指针,通常称为二级指针。定义一个指向指针数据的指针变量的形式:
          • 类型名  **a;
          • a前面有两个“*“号,*a是指针变量的定义形式,现在它前面又有一个“*“号,表示指针变量是指向某种类型的指针变量的。
    • 2.4.9 注意
      • 二级指针和行指针区别
        • #####################################################################################################################################################################################
      • 指针变量的本质是用来放地址,而一般的变量是放数值的
        • int  *p 中   *p和p的差别:
          • *p可以当做变量来用;*的作用是取后面地址p里面的数值
          • p是当作地址来使用。
        • *p++ 和(*p)++的之间的差别:
          • *p++是 地址会变化。
          • (*p)++ 是数值会要变化。
      • 三名主义:
        • 数组名:表示第一个元素的地址。数组名不可以自加,他是地址常量名。
        • 函数名:表示该函数的入口地址。
        • 字符串常量名:表示第一个字符的地址。
      • 指针变量是存放地址的。并且指向哪个就等价哪个,所有出现*p的地方都可以用它等价的代替。
        • 例如:int a=2,*p=&a;
        • *p=*p+2;
        • (由于*p指向变量a,所以指向哪个就等价哪个,这里*p等价于a,可以相当于是a=a+2)
      • 指针变量两种初始化
        • 方法一:int a=2,*p=&a;(定义的同时初始化)
        • 方法二:int a=2,*p; p=&a;  (定义之后初始化)
    • 2.4.10 指针应用--链表
      • (一)链表的概念
        • 链表是一种常见的重要的数据结构,它是动态地进行存储单元分配的一种结构。
        • 链表中的各元素在内存中不一定是连续存放的。要找链表中某一元素,必须先找到上一个元素,根据该元素提供的下一元素的地址才能找到下一个元素。所以,如果没有头指针(head),则整个链表都无法访问。另外一点,这种链表的数据结构,必须利用指针变量才能实现。即一个节点中应包含一个指针变量,用它存放下一节点的地址。当然也可以不通过指针变量,用其他方式也可以构建简单链表,请参考有关数据结构的教材。
        • 下面通过一个例子来说明如何建立和输出一个简单链表。
          • #include<stdio.h>
          • #include<string.h>
          • structnode
          • {
          • int data;
          • struct node *next;
          • };
          • typedefstruct node NODETYPE;
          • main()
          • {
          • NODETYPE s1,s2,s3,*begin,*p;
          • s1.data=100;/*给变量中的data域赋值*/
          • s2.data=200;
          • s3.data=300;
          • begin=&s1;
          • s1.next=&s2;/*使s1的域next指向s2*/
          • s2.next=&s3;
          • s3.next='\0';
          • p=begin;/*移动p,使之依次指向s1、s2、s3,输出它们data域中的值*/
          • while(p)
          • {
          • printf("%d",p->data);
          • p=p->next;   /* p顺序后移 */
          • }
          • printf("\n");
          • }
        • main()函数中定义的变量s1、s2、s3都是结构体变量,它们都含有data和next两个成员。变量begin和p是指向NODETYPE结构体类型的指针变量,它们与结构体变量s1、s2、s3中的成员变量next类型相同。执行赋值语句后,begin中存放s1变量的地址,变量s1的成员s1->next中存放变量s2的地址……最后一个变量s3的成员s3->next置成'\0'(NULL),从而把同一类型的结构体变量s1、s2、s3“链接”到一起,形成“链表”。
        • 在此例中,链接到一起的每个节点(结构体变量s1、s2、s3)都是通过定义,由系统在内存中开辟了固定的存储单元(不一定连续)。在程序执行的过程中,不可能人为地再产生新的存储单元,也不可能人为地使已开辟的存储单元消失。从这一角度出发,可称这种链表为“静态链表“。在实际中,使用更广泛的是一种“动态链表”。
      • (二)建立动态链表(主要针对单向链表)
        • 建立单向链表的主要步骤如下:
          • (1)读取数据。
          • (2)生成新节点。
          • (3)将数据存入节点的成员变量中。
          • (4)将新节点插入到链表中,重复上述操作直至输入结束。
        • 编写函数creatlist(),建立带有头节点的单向链表。节点数据域中的数值从键盘输入,以-1作为输入结束标志。链表的头节点的地址由函数值返回。
        • 我们在函数中定义了一个名为begin的指针变量,用于存放头节点的地址,另外还定义了两个工作指针:current和end。其中指针current用来指向新生成的节点,指针end总是指向链表当前的尾节点。每当把current所指的新开辟的节点连接到表尾后,end便移向这一新的表尾节点。这时又可以用current去指向下一个新开辟的节点。链表最后一个节点的指针域中置'\0'(NULL值)作为单向链表的结束标志。
        • 链表建成后,头节点的地址由creatlist()返回,赋给main()函数中的指针变量head。函数如下:
          • #include<stdio.h>
          • #include<string.h>
          • #include<stdlib.h>
          • structnode
          • {
          • int data;
          • structnode *next;
          • };
          • typedefstruct node NODETYPE;
          • NODETYPE*creatlist()
          • {
          • int i;
          • NODETYPE *begin,*end,*current;
          • begin=(NODETYPE*)malloc(sizeof(NODETYPE));/*生成头节点*/
          • end=begin;
          • scanf("%d",&i);/*输入数据*/
          • while(i!=-1)/*未读到数据结束标志时进入循环*/
          • {
          • current=(NODETYPE*)malloc(sizeof(NODETYPE));/*生成一个新节点*/
          • current->data=i;/*读入的数据存入新节点的data域*/
          • end->next=current;/*新节点连到表尾*/
          • end=current;/*end指向当前表尾*/
          • scanf("%d",&i);/*读入数据*/
          • }
          • end->next='\0';/*置链表结束标志*/
          • return begin;/*返回表头指针*/
          • }
          • main()
          • {
          • NODETYPE *head;
          • head=creatlist();/*调用链表建立函数,得到头节点的地址*/
          • }
        • 以上creatlist()函数中,当一开始输入-1时,并不进入while循环,而直接执行循环之后的end->next='\0';语句,这时建立的是一个“空链表“。由此可见,可用条件begin->next=='\0'来判断链表是否为空。
      • (三)顺序访问链表中各节点的数据域
        • 所谓“访问“,可以理解为取各节点的数据域中的值进行各种运算、修改各节点的数据域中的值等一系列的操作。
        • 输出单向链表各节点数据域中内容的算法比较简单,只需利用一个工作指针(p),从头到尾依次指向链表中的每个节点,当指针指向某个节点时,就输出该节点数据域中的内容,直到遇到链表结束标志为止。如果是空链表,就只输出提示信息并返回调用函数。
        • 函数如下:
          • voidprintlist(NODETYPE *head)
          • {
          • NODETYPE*p;
          • p=head->next;/*指向头节点后的第一个节点*/
          • if(p=='\0')/*链表为空时*/
          • printf(“Linklistis null\n”);
          • else/*链表不为空时*/
          • {
          • printf(“head”);
          • do
          • {
          • printf(“->%d”,p->data);/*输出当前节点数据域中的值*/
          • p=p->next;/*move指向下一个节点*/
          • }while(p!=′\0′);/*未到链表尾,继续循环下去*/
          • }
          • printf(“->end\n”);
          • }
      • (四)在链表中插入节点
        • 在单向链表中插入节点,首先要确定插入的位置插入节点在指针p所指的节点之前称为“前插“,插入节点在指针p所指的节点之后称为“后插“。“前插“操作中各指针的指向
        • 当进行前插操作时,需要3个工作指针:s指向新开辟的节点,用p指向插入的位置,q指向要插入的前趋节点。
      • (五)删除链表中的节点
        • 为了删除单向链表中的某个节点,首先要找到待删除的节点的前趋节点(即当前要删除节点的前面一个节点),然后将此前趋节点的指针域去指向待删除节点的后续节点(即当前要删除节点的下一个节点),最后释放被删除节点所占的存储空间即可。
  • 2.5用typedef说明一种新类型名
    • C语言可以用typedef说明一种新类型名,说明新类型名的语句一般形式为:
      • typedef   类型名    标识符;
      • 其中,“类型名“一定是在此语句之前已有定义的类型标识符。“标识符“是一个用户定义标识符,用来标识新的类型名。typedef语句的作用仅仅是用“标识符“来代表已存在的“类型名“,并没有产生新的数据类型,因此,原有的类型名依然有效。
    • 声明一个新的类型名的具体步骤如下:
      • (1)先按定义变量的方法写出定义的主体(如float a;)。
      • (2)将变量名换成新类型名(如将a换成FLO)。
      • (3)在最左面加上关键字typedef(如 typedef  float FLO;)。
      • (4)然后可以用新类型名去定义其他的变量(如FLO  b;)。
  • 2.6 sizeof()计算实际所占字节数
    • 概念
      • sizeof是C语言的一种单目操作符,如C语言的其他操作符++、--等。它并不是函数。sizeof操作符以字节形式给出了其操作数的存储大小。操作数可以是一个表达式或括在括号内的类型名。操作数的存储大小由操作数的类型决定。作用就是返回一个对象或者类型所占的内存字节数。
    • 2.6.1语法
      • sizeof有三种语法形式,如下:
        • 1) sizeof( object ); // sizeof( 对象 );
        • 2) sizeof( type_name ); // sizeof( 类型 );
        • 3) sizeof object; // sizeof 对象;
      • 举例
        • sizeof(int);    //64位 4
        • int a=3; sizeof(a);      //64位 4   建议使用此方法
        • sizeof(3);  //64位 4
        • sizeof 3     //64位4    是对的,不建议使用
        • sizeof int     //Error
    • 2.6.2数组
      • 数组的sizeof值等于数组所占用的内存字节数,如:
        • char a1[] = "abc"; sizeof( a1 ); // 结果为4,字符末尾还存在一个NULL终止符
        • int a2[3]; sizeof( a2 ); // 结果为3*4=12(依赖于int)
      • sizeof不是求数组元素的个数,求数组元素的个数,通常有下面两种写法:
        • int c1 = sizeof( a1 ) / sizeof( char ); // 总长度/单个元素的长度
        • int c2 = sizeof( a1 ) / sizeof( a1[0] ); // 总长度/第一个元素的长度
      • 举例
        • char array[8] = “China”; sizeof(array) //64位 8    数组实际长度
        • sizeof(“hi”);  //64位 3
        • char a[5]="CHINA";   printf("%d",sizeof(a));    //64位 5
        • char a[5]="CHINAA";  printf("%d", sizeof(a));    //64位 警告 结果5
        • int a[3]; sizeof(a);  //64位 3*4=12                数组实际长度,整形一个元素4个字节
    • 2.6.3字符
      • sizeof(char)    //64位 1 s 字符型变量是1字节
      • sizeof(‘s’)     //64位 4     ‘s’转换为一个数,数占四个字节
      • sizeof(‘\101’)     //64位 4     ‘\101’为一个数,数占四个字节
        • ‘A’  =  ‘\101’ =  ‘\x41’  =  65
      • char c;  sizeof(c)             //64位 1
      • C和C++比较
        • C语言环境下:
          • char a = 'a' ;
          • sizeof(char) = 1 ;
          • sizeof(a) = 1 ;
          • sizeof('a') =4  ;
        • C++语言环境下:
          • char a = 'a';
          • sizeof(char) = 1;
          • sizeof(a) = 1 ;
          • sizeof('a') =1;
        • 字符型变量是1字节这个没错,奇怪就奇怪在C语言认为'a'是4字节,而C++语言认为'a'是1字节。
        • 原因如下:
          • C99标准的规定,'a'叫做整型字符常量(integer  character constant),被看成是int型,所以在32位机器上占4字节。
          • ISO C++标准规定,'a'叫做字符字面量(character literal),被看成是char型,所以占1字节
          • 字符是指计算机中使用的字母、数字、字和符号。1个汉字字符存储需要2个字节,1个英文字符存储需要1个字节。ASCII是一个字节,Unicode是两个字节。Java的字符是Unicode的,所以是两个字节。
    • 2.6.4结构体
      • 1.对齐
        • structS1
        • {
        • char c;
        • int i;
        • };
        • sizeof(s1)为8,字节对齐,有助于加快计算机的取数速度,否则就得多花指令周期了。为此,编译器默认会对结构体进行处理(实际上其它地方的数据变量也是如此),让宽度为2的基本数据类型(short等)都位于能被2整除的地址上,让宽度为4的基本数据类型(int等)都位于能被4整除的地址上,以此类推。这样,两个数中间就可能需要加入填充字节,所以整个结构体的sizeof值就增长了。
      • 2.字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:
        • 1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
        • 2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internaladding);
        • 3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
        • 举例
          • struct S3 整除4 一共3*4=12 测试###############################################################################################################################################################################
          • {
          • char c1; 整除1
          • S1 s; 整除4
            • structS1
            • {
            • char c;
            • int i;
            • };
          • char c2 整除1
          • };
          • S1的最宽简单成员的类型为int,S3在考虑最宽简单类型成员时是将S1“打散”看的,所以S3的最宽简单类型为int,这样,通过S3定义的变量,其存储空间首地址需要被4整除,整个sizeof(S3)的值也应该被4整除。
      • 3.“空结构体”(不含数据成员)的大小不为0,而是1。试想一个“不占空间”的变量如何被取地址、两个不同的“空结构体”变量又如何得以区分呢于是,“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占位了。如下:
        • structS5 { };
        • sizeof(S5 ); // 结果为1
    • 2.6.5联合体
      • 结构体在内存组织上是顺序式的,联合体则是重叠式,各成员共享一段内存,所以整个联合体的sizeof也就是每个成员sizeof的最大值。结构体的成员也可以是复合类型,这里,复合类型成员是被作为整体考虑的。
      • 所以,下面例子中,U的sizeof值等于sizeof(s)。
        • structS1
        • {
        • char f1;
        • int f2;
        • char*f3;
        • };
        • union U
        • {
        • int i;
        • char c;
        • S1 s;
        • };
    • 2.6.6表达式和调用函数
      • sizeof可以对一个表达式求值,编译器根据表达式的最终结果类型来确定大小,一般不会对表达式进行计算
        • 1.# include <stdio.h>
        • int main()
        • {
        • int i;
        • i = 10;
        • printf("i : %d\n",i);
        • printf("sizeof(i++) is: %d\n",sizeof(++i));
        • printf("i : %d\n",i);
        • return 0;
        • }
          • 结果:i : 10
          • sizeof(i++) is: 4i : 10
        • 2.    chara="255";printf("%d",sizeof(a++));
          • /*打印的值是1,这一步并不对表达式a++进行计算,所以char a="255"*/
        • 3.     sizeof( 2 + 3.14 );
          • // 3.14的类型为double,2也会被提升成double类型,所以等价于 sizeof( double );但是不计算,相当于比较里面最大类型##########################测试#################sizeof(i=2+3.14)###########################################################################
      • sizeof也可以对一个函数调用求值,其结果是函数返回类型的大小,函数并不会被调用,我们来看一个完整的例子:
        • charfoo()
        • {
        • printf("foo()has been called.\n");
        • return'a';
        • }
        • intmain()
        • {
        • size_tsz = sizeof( foo() );
        • /*foo() 的返回值类型为char,所以sz = sizeof(char ),foo()并不会被调用*/
        • printf("sizeof(foo() ) = %d\n", sz);
        • }
    • 2.6.7 不可用
      • C99标准规定,函数、不能确定类型的表达式以及位域(bit-field)成员不能被计算sizeof值,即sizeof操作符不能用于函数类型,不完全类型或位字段。不完全类型指具有未知存储大小的数据类型,如未知存储大小的数组类型、未知内容的结构或联合类型、void类型等。下面这些写法都是错误的:
        • 1. sizeof(foo );  // error
        • 2. void foo2() { } sizeof(foo2() );  // error
        • 3. struct S
          • {
          • unsignedint f1 : 1;  // error 属于位字段
          • unsignedint f2 : 5;
          • unsignedint f3 : 12;
          • };
          • sizeof(S.f1 );  // error
    • 2.6.8 sizeof的常量性
      • sizeof的计算发生在编译时刻,所以它可以被当作常量表达式使用,如:
        • charary[ sizeof( int ) * 10 ]; // ok
      • 最新的C99标准规定sizeof也可以在运行时刻进行计算,如下面的程序在Dev-C++中可以正确执行:
        • int n;
        • n = 10;// n动态赋值
        • charary[n]; // C99也支持数组的动态定义
        • printf("%d\n",sizeof(ary)); // ok. 输出10
        • 但在没有完全实现C99标准的编译器中就行不通了,上面的代码在VC6中就通不过编译。所以我们最好还是认为sizeof是在编译期执行的,这样不会带来错误,让程序的可移植性强些。

猜你喜欢

转载自www.cnblogs.com/ZanderZhao/p/10013860.html
今日推荐