C语言学习笔记
数据类型
- char 字符型,点一个字节。
- int 整型,通常代表机器中整数的自然长度。
- short int 短整型,通常为16位(int可以省略)。
- long int 长整型,通常为32位(int可以省略)。
- float 单精度浮点型
- double 双精度浮点型
- long double 高精度的浮点数
signed
和unsigned
用于限定char类型和任何整型,unsigned
类型的数值总是正值或0。
常量
语法:#define 常量名 常量值
- 整数常量
包含int
和long
类型常量。long
类型以l
或L
结尾。
当一个整数无法用int
表示时,也被当作long
类型处理。
无符号常量以u
或U
结尾,无符号长整型使用ul
或UL
结尾。
前缀0
表示八进制形式,0x
表示十六进制形式。 - 浮点数常量
没有后缀的常量为double
类型。
后缀加f
或F
表示float
类型。
后缀加l
或L
表示long double
类型。 - 字符常量
一个字符常量是一个整数,如'0'
值为48,它与数值0无关。
转义字符通常只表示一个字符,如'\013'
。
字符常量'\0'
表示值为0的字符,即空字符(null)。 - 字符串常量
与字符常量的区别是字符串常量用" "
双引号括起来。其实就是字符数组,内部使用空字符('\0')
作为结尾,因此,字符串常量占据的存储单元比双引号内的字符数大1。 - 枚举常量
语法:enum 枚举名 {枚举列表}
枚举常量是另外一种类型常量,是一个常量整型值的列表,如:
未显示声明的枚举,第一个枚举名的值为0,第二值为1,依此类推。enum boolean {NO, YES};
如果指定部分枚举值,未指定枚举值将向后递增。enum colors {WHITE=0,BLACK,RED,YELLOW} BLACK-->1 RED-->2 YELLOW-->3
常量表达式是仅仅包含常量的表达式。这种表达式只在编译时求值,而不在运行时求值。
变量
变量必须先声明再使用,一个变量声明只能指定一种类型,后面可以有一个或多个该类型变量。如:int lower,upper…;
任何
-
外部变量
定义在函数之外的变量叫做外部变量。由于定义在函数之外,因此可以在所有函数中使用。由于C语言不允许在一个函数中定义其它函数,因此函数本身是“外部的”。变量都可以使用const
限定符限定为不可被修改变量。如果要在外部变量定义之前使用变量,或者外部变量的定义与变量的使用不在同一个源文件中,必须在相应变量声明中强制使用关键字
extern
。外部变量的定义中数组必须指定长度,但extern
声明不一定要指定数组长度。#define MAXSIZE 10; int a; double b[MAXSIZE];
extern int a; //使用a.c文件中的变量a extern double b[]; //使用a.c文件中的b省略了数组大小
-
自动变量
定义在函数内的变量叫做“局部变量”,也叫“自动变量”。由于定义在函数之内,因此只可以函数内使用,多次调用函数不保留前次调用时的赋值。 -
静态变量
用static
修饰的变量,叫做静态变量。静态变量的存储方式与全局变量相同,都是静态存储方式。全局变量的作用域是整个源程序,即源程序源的所有文件中有效。静态变量作用域则是只在当前变量所在源文件中可以使用,其次静态变量的值在函数调用后一直保持不会消失。即使在函数中声明的,每次调用函数,其值都会保存上一次调用后值。 -
寄存器变量
使用register
关键字声明的变量,叫做寄存器变量。register
变量放在机器的寄存器中,这样可以使程序更小,执行速度更快。register
声明只适用于自动变量或函数的形式参数形式:test(register variA,register variB) { register int variC; ... }
实际上,底层硬件环境对寄存器变量的使用会有一些限制。每个函数中只有很少的变量可以保存在寄存器中,且只允许某些变量类型的变量。编译器可以忽略过量的或不支持的寄存器变量声明,因此过量的寄存器变量声明并没有什么害处。但是注意,无论寄存器变量实际上是不是存放在寄存器中,它的地址都是不能访问的。不同的机器,对寄存器变量的限制不同。
运算符
-
自述运算符
+
、-
、*
、/
、%
c/c++
和java
语言中取模运算(%)就是取余运算,而python则有些不同 -
关系运算符
>
、>=
、<
、<=
、==
、!=
-
逻辑运算符
||
、&&
、!
-
按位运算符
按位运算就是将数值转换为二进制位,然后进行运算得到最终值:
&
按位与(AND)运算规则是两个为真才为真
1&1=1, 1&0=0, 0&1=0, 0&0=0
。如3
二进制位是0000 0011
,5
的二进制位是0000 0101
,由按位与规则可得,001 & 101
等于0000 0001
,最终值为1
。求模运算时当被除数为 2 的 n 方时,可以用按位与运算替换更高效,公式为 (不分前后也可以写成,) 即,
|
按位或(OR)运算规则是一个为真则为真
1|0=1, 1|1=1, 0|0=0, 0|1=1
。如6
二进制位是0000 0110
,2
的二进制位是0000 0010
,由按位或规则可得,110 | 010
等于110
,最终值为6
。^
按位异或(XOR)运算规则是如果两个值不相同,则为真,相同则为假
1^0=1, 1^1=0, 0^1=1, 0^0=0
。如5
二进制位是0000 0101
,9
的二进制位是0000 1001
,由按位异或规则可得,0110 ^ 1001
等于1100
,最终值为12
<<
左位移运算规则是将左侧数值的二进制位向左移动右侧数值位。移动后右边补
0
,正数左边第一位补0
,负数补1
,结果相当于乘以 2 的 n 次方。如:5<<2
,就是5
的二进制位向左移2
位,即0000 0101
把101
向左移两位得到0001 0100
,最终值为5
乘以 2 得 2 次方,等于20
。>>
右位移运算规则是将左侧数值的二进制位向右移动右侧数值位。移动后正数第一位补
0
,负数补1
,结果相当于除以 2 的 n 次方。如:5>>2
,就是5
的进制位向右移动2
位,即0000 0101
把101
右移两位后得到0000 0001
,最终值为5
除以 2 得 2 次方,等于1
。~
按位求反(一元运算符)运算规则是取位数值相反值
~0=1, ~1=0
。 如5
二进制位是0000 0101
,取反后为1111 1010
,最终值为-6
。 -
自增运算符
++
可以作为前缀运算符,表示先作自增,后赋值;也可以作为后缀运算符,表示先赋值,再作自增。int x, n; n = 1; x = ++n; //x值为2,n为2 x = n++; //x值为2,n为3
-
自减运算符
--
用法同自增运算符 -
三元运算符
表达式 ? 表达式 : 表达式
流程控制
-
if…else 语句
if (表达式) 语句 else 语句
-
switch 语句
switch (表达式) { case 常量表达式:语句 case 常量表达式:语句 default:语句 }
注意,case后必须为整数值常量或常量表达式。
-
while 循环
while(表达式) 语句
如果希望while循环体至少被执行一次可以使用
do...while
循环:do 语句 while (表达式);
-
for 循环
for(表达式1;表达式2;表达式3) 语句
-
break / continue 语句
用于继续或结束循环语句。 -
goto 语句
for ( ... ) for ( ... ) { if (disaster) goto error; } error:
大多数情况,使用goto语句比不使用goto语句程序段要难以理解和维护,少数情况除外。尽管该问题不太严重,但还是建议尽可能少的使用。
函数
函数定义:
返回值类型 函数名(参数列表){ 函数体 }
函数定义可以不带有
返回类型
,默认返回int类型。函数在源文件中出现的次序可以是任意的,只要保证一个函数不被分离到多个文件中即可。被调用函数通过return 表达式
向调用者返回值,return后面表达式可以省略。
预处理器
预处理器是编译过程中单独执行的第一个步骤,最常用的预处理器指令是#include
和#define
。
-
文件包含
文件包含指令(#include
)用于在编译期间把指定文件的内容包含进当前文件中。形式如下:
#include "文件名"
或
#include <文件名>
。当文件名用引号引起来(通常用于包含程序目录中的文件),则在源文件所在位置查找该文件;如果该位置没有该文件或者文件名用尖括号括起来(通常用于包含编译器的类库路径里面的头文件),则根据相应规则查找该文件,该规则同具体实现有关。如果某个包含文件内容发生了变化,那么所有依赖于该包含的文件的源文件都必须重新编译。
-
宏替换
宏替换指令(#define
)用于用任意字符序列替代一个标记。形式如下:
#define 标识符 记号序列
替换文本前后空格会被忽略,两次定义同一标识符是错误的,除非两次记号序列相同(所有空白分割符被认为是相同的)。
该定义的名字作用域从其定义点开始,到被编译的源文件末尾处结束。定义超过一行使用反斜杠(\
)换行。替换的文本可以是任意的,如:
//为无限循环定义一个名字 #define forever for(;;) //定义带参数宏 #define max(A,B) ((A) > (B) ? < (A) : (B)) #define square(x) x * x main(){ int i, z; i = z = 2; max(++i,i++); // 结果为4 square(z+1); // 结果为5 }
宏定义也有一些缺陷,如上面
max
,它对每个参数执行两次自增操作。square
没有增加括号而导致计算次序错误。可以通过
#undef
取消名字的宏定义:#undef max
#
运算符可以使得宏定义的实际参数替换为带引号的字符串:#define dprint(expr) printf(#expr + "=%d") 调用结果x = 4,y = 2: dprint(x/y) --> x/y=2
##
运算符可以使得宏定义的实际参数相连接:#define paste(x,y) x ## y 调用结果: paste(1,2) --> 12
-
条件编译
#if 常量表达式 文本 #elif 常量表达式 文本 #else 文本 #endif
当预处理器检测到常量表达式值为非0时,对相应表达式下面文本进行编译,后续表达式及文本将会被抛弃。常量表达式可以使用
define 标识符
或define(标识符)
表达式,当标识符
已经定义时,其值为1,否则为0。//检测HDR标识符,没有定义时将其定义 #if !defined(HDR) #define HDR #endif 等价于 #ifdef HDR #define HDR #endif
如上所示,可以使用
#ifdef 标识符
和#ifndef 标识符
控制指令替换#if define 标识符
。 -
预定义标识符
识符 说明 __LINE__ 当前所在源文件行数的十进制常量 __FILE__ 被编译的源文件名字的字符串 __DATE__ 被编译的源文件编译日期的字符串,格式:“Mmm dd yyyy” __TIME__ 被编译的源文件编译时间的字符串,格式:“hh:mm:ss” __STDC__ 整型常量1(只有在遵循标准的实现中该标识符才被定义为1) -
其他预处理指令
#line 常量 "文件名"
以十进制整型常量的形式定义下一行源代码的行号。其中"文件名"
可以省略,表示设置当前编译的源文件。
#error 信息
当预处理器遇到此指令时停止编译并输出定义的错误消息。通常与#if...#endif
等指令一起使用。
#pragam 记号序列
使处理器执行一个与具体实现相关的操作。无法识别的pragma(编译指示)将被忽略掉。
#
空指令。预处理器行不执行任何操作。
指针
为了便于记忆,指针的声明形式是在变量声明的基础上加一个*
间接寻址或间接引用运算符:
int *p; //声明一个int类型的指针*p
通过一元运算符&
获取一个对象的地址:
int x = 1;
p = &x;
printf("%d",*p); --> 打印1
一元运算符*
和&
的优先级比算术运算符优先级高,因此在进行算术运算时不需要加括号:
*p += 1;
或
++*p;
或
(*p)++;
语句
(*p)++
中的圆括号是必需的,否则,表达式将对p进行加一运算,而不是对ip指向的对象进行加一运算,原因在于一元运算符表达式遵循从右到左的顺序。
由于指针也是变量,所以可以直接使用,而不必通过间接引用的方法使用:
int *pp;
pp = ip; //通过变量的形式将指针pp指向指针ip指向的对象
同其他类型变量一样,指针也可以初始化,对指针有意义的初始化值只能是0
或表示地址的表达式。C语言保证0
永远不是有效的数据地址,因此,返回0
可用来表示发生了异常事件。
指针与整数之间不能相互转换,但
0
是唯一的例外。常量0
可以赋值给指针,指针也可以与常量0
进行比较。程序中通常使用符号常量(NULL)
代替常量0,其定义在stddef.h
头文件中。
-
指针与函数参数
C语言是以传值的方式将参数值传递给被调用函数,因此,被调用函数不能直接修改主调函数中变量的值。void swap(int x,int y){ int temp; temp = x; x = y; y = temp; } 调用: swap(x,y);
由于参数传递是传值方式,所以上述函数无法成功交换变量。可以通过将交换的变量的指针传递给被调用函数的方法实现该功能:
void swap(int *px,int *py){ int temp; temp = *px; *px = * py; *py = temp; } 调用: swap(&x,&y);
-
指针与数组
数组其实是由N个对象组成的集合,这些对象存储在相邻的内在区域中。因此可以将指针变量指向数组的每个对象。int a[10]; int *pa; pa = &a[0]; //将pa指向数组第0个元素
根据指针运算的定义,
pa+1
指向数组下一个对象,pa+i
指向pa所指向数组对象之后的第i个对象,pa-i
指向pa所指向数组对象之前的第i个元素。
由于数组名所代表的就是该数组最开始的一个元素的地址,因此下面两等式作用相同:pa = &a[0]; 或 pa = a;
由上面等式,对数组元素
a[i]
的引用也可以写成*(a+i)
形式。实际上,在C语言计算a[i]
元素时就是先将其转换成*(a+i)
的形式,然后再求值。数组名和指针的不同之处在于,指针是一个变量,数组名却不是变量。因此语句
pa=a
和pa++
是合法的,而a=pa
和a++
形式是非法的。当两个指针指向同一个数组的成员时,两个指针可以进行比较运算(===、!=、<、>=):
char c[] = "hello"; char *pc1 = c; char *pc2 = &c[1]; 运算: pc2 > pc1 --> True //比较运算返回True pc2 - pc1 + 1 --> 2 //返回两指针指向的元素之间元素的数目
由上面代码可知,指向数组元素位越置靠前的指针,指针值越大。但是,指向不同数组的元素的指针之间的算术或比较运算没有意义。
根据指针上面的特性,可以写出返回字符串长度函数的两个指针实现版本:
/* strlen函数,返回客串s的长度 */ 版本一: int strlen(char *s){ int n; for(n = 0;*s != '\0';s++) n++; return n; } 版本二: int strlen(char *s){ char *p = s; while (*p != '\0') p++; return p - s; }
- 指针算术运算具有一致辞性,如果处理的数据类型是比字符型占据更多的存储空间的浮点类型,并且p是一个指向浮点类型的指针,那么在执行
p++
后,p将指向下一个浮点数的地址。所有的指针运算都会自动考虑它所指向的对象长度。 - 有效的指针运算包括相同类型指针之间的赋值运算;指针同整数之间的加减法运算;指向相同数组中元素的两个指针间的减法或比较去处;将指针赋值为0或与0之间的比较运算。其他所有形式的指针运算都是非法的。
C语言数组可以使用花括号
{}
括起来初值表进行初始化。同时也支持多维数组,如果将二维数组作为参数传递给函数,函数的参数声明中可以不指定数组的行数,但必须指明数组的列数,因为,二维数组在内存中的排列方式是按行排列的,即第一行排完之后再排列第二行,依此类推。当给出数组的列数时,通过列数与行数的关系,即可找到对应的地址。f (int array[2][13]); //可以写成 f (int array[][13]); //还可以写成 f (int (*array)[13]); //错误写法 f (int *array[13]); --> 因为[]的优先级高于*的优先级,如果声明时不使用()时,相当于声明了一个指向指针的一维数组。
由于指针本身也是变量,所以它也可以像其他变量一样被存储在数组中。因为
[]
优先级高于*
的优先级,所以上例中int *array[13];
相当于声明了一个指向指针的一维数组。指针数组与二维数组的区别是,二维数组是分配了固定存储空间(行*列)的,而指针数组只是定义了指定个数的指针,而没有对它们初始化,它们的初始化必须以显式的方式进行。因此,指针数组优于二维数组的重要一点是,指针数组每一行长度可以不同。
//二维数组 int array[10][20]; --> 固定10行20列长度的二维数组 //指针数组 char *monthName = {"January","February","March"}; --> 长度不固定的指针数组 //调用 array[0][0]; monthName[0][0]; -->返回 J **monthName; -->等价于下标方式 返回 J
- 指针算术运算具有一致辞性,如果处理的数据类型是比字符型占据更多的存储空间的浮点类型,并且p是一个指向浮点类型的指针,那么在执行
-
命令行参数
调用主函数数(main
)时,有两个参数。第一个参数(argc
)用于参数计数,表示运行程序时命令行中参数的个数;第二个参数(argv
),是一个指向字符串数组的指针,其中每个字符串对应一个参数。另外,ANSI标准要求,argv[argc]的值必须为一个空指针。main函数返回值为0表示正常退出,返回非0值表示代表程序异常退出。如echo程序,它将命令行参数回显在屏幕上的一行中,其中命令行中各参数之间用空格隔开:echo hello, world # 打印输出 hello, world
// 版本一:将argv看成是一个字符指针数组 #include <stdio.h> main(int argc, char *argv[]) { int i; for (i = 1; i < argc; i++) printf("%s%s", argv[i], (i < argc - 1) ? " " : ""); printf("\n"); return 0; } // 版本二:通过指针方式实现 main(int argc,char *argv[]){ while (--argc > 0) printf("%s%s", *++argv, (argc > 1) ? " " : ""); printf("\n"); return 0; } 注意:printf的格式化参数也可以是表达式。如: printf((argc>1) ? "%s " : "%s", *++argv);
-
指向函数的指针
C语言中,函数本身不是变量,但可以定义指向函数的指针。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值。调用指向函数的指针时,它们是函数的地址,因为它们是函数,所以同数组名一样,前面不需要加&
运算符。
由于任何类型的指针都可以转换为void *
类型,并且在将它转换回原来的类型时不会丢失信息,因此,函数指针数组参数的类型通常用void指针类型。定义形式:
类型 (*指针变量名) (参数列表)
注意上面
(*指针变量名)
括号不能省略。/* 实现operate()函数传入不同函数指针实现相关函数功能 */ #include <assert.h> #include <stdio.h> double operate(double *num, int len, double (*nump)(double *num, int len)); double max(double *num, int len); double min(double *num, int len); double avg(double *num, int len); main(int argc, char *argv[]) { double num[] = {2.0, 2.0, 3.0, 1.5, 2.5, 5.3, 4.0, 2}; int len = 8; printf("%f", operate(num, len, min)); --> 1.5 printf("%f", operate(num, len, max)); --> 5.3 printf("%f", operate(num, len, avg)); --> 2.7875 } //获取数组最大值 double max(double *num, int len) { double temp; temp = *num++; for (int i = 1; i < len; i++, num++) if (temp < *num) temp = *num; return temp; } // 获取数组最小值 double min(double *num, int len) { double temp; temp = *num++; for (int i = 1; i < len; i++, num++) if (temp > *num) temp = *num; return temp; } //获取数组平均值 double avg(double *num, int len) { assert(len != 0); double ti; ti = 0; for (int i = 0; i < len; i++, num++) ti += *num; return ti / len; } // 通过函数指针调用不同的方法 double operate(double *num, int len, double (*nump)(double *num, int len)) { return nump(num, len); }
-
指针别名(Pointer aliasing)
指两个及以上的指针指向同一数据,即不同的名字指针指向同一内在地址,则称一个指针是另一个指针的别名。如:int i = 0; int j = 0; int *a = &i; int *b = &i; // 指针b是指针a别名
-
restrict
指针限定符
该关键字是 C99 标准中新引入的一个指针类型修饰符,它只可应用于限定和约束指针,主要作用是限制指针别名,表明当前指针是访问一个数据对象的唯一方式,所有修改该指针所指向的内存中内容操作都必须通过该指针来修改,而不能通过其他途径修改。这样做的用处是帮助编译器更好的优化代码,生成更有效率的汇编代码。如果该指针与另外一个指针指向同一对象,将会导致未定义行为。// 未加指针限定符的指针参数 int add(int *a, int *b){ *a = 10; *b = 12; return *a + *b; } // 添加指针限定符的指针参数 int add2(int *restrict a, int *restrict b){ *a = 10; *b = 12; return *a + *b; } // 调用两个方法 int i,j; add(&i, &j); // 返回值22,编译器无法确定内存是否被其他指针别名修改(即函数指针参数未设置 restrict 限定符),无法作出优化 add2(&i, &j); // 返回值22,生成的汇编代码会进行优化操作 add2(&i, &i); // 返回值24,因为传递参数违反了 restrict 限定符对函数内部实现的约束(两个参数指向同一内存地址,导致互为指针别名),导致未定义行为。
结构
结构是一个或多个变量的集合,这些变量可以是不同的类型。ANSI标准定义了定义了结构的赋值操作————结构可以拷贝、赋值、函数参数,函数返回值。声明形式如下:
struct 结构标记 { 结构成员 }
结构成员、结构标记和普通变量(非成员)可以使用相同名字,而不会冲突,因为通过上下文分析可以对它们进行区分。
struct声明定义了一种数据类型。在标志结构成员表结束的右花括号之后可以跟一个变量表,这与其他基本类型的变量声明是相同的:
struct {...} x, y, z;
如果结构声明的后面不带变量表,则不会为它分配存储空间,它仅仅描述了一个结构的模板或轮廓。如果结构声明中带有标记,就可以使用该标记定义结构实例:
struct 结构标记 结构名 [ = {结构初始化值} ]
上面结构初始化值可以省略,在表达式中可以使用结构成员运算符(.)
引用某个特定结构中的成员:
结构名.成员
// 声明结构
struct point {
int x;
int y;
}
// 定义结构实例
struct point pt = {100,200}; --> 可以通过花括号的方式进行初始化
// 引用结构成员
printf("%d",pt.x); --> 打印100
结构可以进行嵌套,例如:
// 声明嵌套结构
struct react {
struct point pt1;
struct point pt2;
}
// 定义结构实例,并初始化
struct rect screen = {
{100, 200}, --> 花括号可以省略,但不建议
{300, 400}
};
// 引用结构成员
printf("%d",screen.pt2.x); --> 打印300
结构类型的参数和其他类型的参数一样,都是值传递。因此,下面例子不会改变原结构 p1 的值:
#include <stdio.h>
struct point {
int x;
int y;
};
main() {
struct point addpoint(struct point p1, struct point p2);
struct point p1 = {100, 200};
struct point p2 = {200, 300};
struct point p = addpoint(p1, p2);
printf("\n%d\t%d", p.x, p.y); // 打印300 500
printf("\n%d\t%d", p1.x, p1.y); // 打印100 200
}
struct point addpoint(struct point p1, struct point p2) {
p1.x += p2.x;
p1.y += p2.y;
return p1;
}
-
结构数组
当有一组信息需要存储到结构体时,可以使用结构数组。结构数组和普通数组声明类似,就是在定义结构实例时增加一个中括号[数组大小]
即可,如:struct point pa[50]; // 定义结构数组pa
当使非字符数组时,结尾不是以
\0
结束,所以不容易判断数组长度。通常有三种解决方法:- 手工计算,直接写入具体长度。缺点是不得扩展,当列表变更时,需要手动维护,不安全。
- 在初值表的结尾处加上一个空指针,然后遍历循环,直到讲到尾部的空指针为止。
- 使用编译时一元运算符
sizeof 对象
或sizeof (类型名)
,它可以计算任一对象的长度,即指定对象或类型占用的存储空间字节数。因为数组的长度在编译时已经完全确定,它等于 数组项的长度 * 项数,因此,得出数组项数为 数组长度 / 数组项的长度。
sizeof
反回一个无符号整型值,其类型为size_t
,该类定义在头文件<stddef.h>中。一般用法如下:// 预处理器中的应用,如返回上面结构数组的大小 #define PA_LENGTH (sizeof(pa) / sizeof(pa[0])) 或 #define PA_LENGTH (sizeof(pa) / sizeof(struct point)) --> 两者作用相同,但当类型改变时此种写法需要同步修改,因此,建议使用前者方法。 // 同结构一样其他类型数组也可使用上面方法获取数组大小 int a[] = {1,12,123,1234}; sizeof(a) / sizeof(a[0]) --> 4 或 sizeof(a) / sizeof(int) --> 4
-
结构指针
当传递给函数的结构很大时,使用结构指针方式的效率比复制整个结构的效率高。结构指针和普通指针声明类似,如:// 声明结构指针 struct point *pp; // 访问结构成员 (*pp).x; (*pp).y;
上面示例,访问指针结构成员
(*pp).x
中的圆括号,是必需的。因为 结构成员运算符(.
)的优先级高于 指针运算符(*
)。结构指针使用频率非常高,为了用不用方便,C语言提供了另一种简写方式引用结构成员:
p->结构成员
//声明结构数组指针*ppa和结构指针*pp struct point *ppa, *pp; // 结构数组指针指向结构数组 ppa = pa; // 结构指针指向结构数组第二项 pp = &pa[1]; ppa->y; --> 200 pp->y; --> 400 // 运算符 . 和 -> 都是从左至右结合,所以下面表达式等价 struct rect r, *rp = &r; r.pt1.x ; rp->pt1.x ; (r.pt1).x ; (rp->pt1).x ;
在所有运算符中,结构运算符
.
、->
、用于函数用的()
及用于下标的[]
优先级最高。 -
自引用结构
一个包含自身实例的结构是非法的,但将实例声明为指针是允许的。如:struct tnode { struct tnode left; --> 非法声明 } struct tnode { struct tnode *left; --> 合法声明 } // 结构互相引用 struct t { struct s *p; } struct s { struct t *q; }
类型定义
C语言可以通过typedef
来建立新的数据类型名,形式如下:
typedef 类型 类型名
typedef
声明的类型在变量名的位置出现,而不是紧接在关键字typedef
之后。建议使用大写字母开头定义类型名,以示区分。
typedef
声明并没有创建一个新类型,只是为某个已存在的类型增加一个新的名称而已。
// 定义一个 String 类型
typedef char *String;
// 定义一个结构类型
typedef struct point Point;
Point pp = {5,6};
pp.x; --> 5
pp.y; --> 6
typedef
类似于#define
语句,但typedef
是由编译器解释的,因此它的文本替换功能要超过预处理器的能力。如:
// 定义类型`PFI`是一个指向函数的指针,该函数接收两个`char *`类型的参数,返回`int`类型
typedef int (*PFI) (char *, char *);
function func(char *,char *){
return 0;
}
PFI pfi = &func;
pfi(0,0); --> 返回0
typedef
除了表达方式更简洁之外,使用它还有两个重要原因。一是它可以使程序参数化,以提高程序的可移植性。如声明的数据类型同机器有关,当程序需要移植到其他机器上时,只需改变typedef
类型定义就可以了。另一个原因是它可以为程序提供更好的说明性。如 PFI 类型明显比一个指向复杂结构的指针更容易让人理解。
联合
联合实际上就是一个结构,只不过联合的不同成员都保存在同一个存储空间,也就是联合中所有成员相对于基地址的偏移量都为0,因此联合空间要大到足够容纳最“宽”的成员。
定义:union 联合标记 { 联合成员 }
联合可以给任何一个成员赋值,但每次的赋值将会覆盖上一次赋值,因此读取的类型必须是最近一次存入的类型,否则其返回结果取决于计算机的具体实现。
// 定义联合u
union myunion {
int i;
float f;
char c;
char *pc;
} u;
// 赋值
u.i = 10;
printf("\n%d", u.i); --> 10
u.c = 'a';
printf("\n%c", u.c); --> a
printf("\n%d", u.i); --> 97 // 因为 u.i 被最后一次 u.c 赋值覆盖,所以字符 'a' 对应的ASCII整数值为 97
如同上示例,访问联合成员与访问结构成员方式相同:
联合.成员
或 联合指针->成员
输入与输出
-
格式化输出/输出
输出函数:
int printf(char *format,...)
输入函数:
int scanf(char *format,...)
函数格式化参数以
%
开始,并以一个转换转换字符结束。在%
和转换字符之间依次可以包含:负号
,用于指定被转换的参数按照左对齐的形式输出。数
,用于指定最小字段宽度。小数点
,用于将字段和精度分开。数
,用于指定精度,即要打印的最大字符数、浮点数点后的位数、整型最少最少输出的数字数目。- 字母
h
或l
,表示将整数作为short
类型或long
类型打印。
转换符 | 描述 |
---|---|
d, i | int 类型;十进制数 |
o | int 类型,打印无符号八进制数(没有前导0)。 |
x, X | int 类型,打印无符号十六进制数(没有前导0x或0X)。 |
u | int 类型,打印无符号十进制数。 |
c | int 类型,单个字符。 |
s | char *类型,打印字符串。 |
f | double 类型十进制小数,精度默认为6。 |
e, E | double 类型,输入格式为指数形式,精度默认是6。如:m.dddddd e +/-。 |
g, G | double 类型,尾部的0和小数不打印。 |
p | void *类型。 |
% | 打印 % 号。 |
-
可变参数函数
像printf
函数一样,函数参数的数量和类型是可变的。使用...
定义可变参数。
头文件<stdarg.h>
中提供了va_list
类型用于声明一个**参数指针(ap)**变量;宏va_start
将ap
针初始化为指向第一个无名参数的指针,参数表必须至少包括一个有名参数(如:char *format),va_start
将最后一个有名参数作为起点。
每次调用va_arg
,该函数将返回一个参数,并将ap
指向下一个参数。va_arg
使用一个类型名来决定返回的对象类型、指针移动的步长。最后,在函数返回之前调用va_end
以完成一些必要的清理工作。void myprintf(char *fmt,...){ va_list ap; char *p, *sval; int ival; double dval; va_start(ap, fmt); for (p = fmt; *p; p++) { if (*p != '%') { putchar(*p); continue; } switch (*++p) { case 'd': ival = va_arg(ap, int); printf("%d", ival); break; case 'f': dval = va_arg(ap, double); printf("%f", dval); break; default: putchar(*p); break; } } }