1、auto
修饰局部变量,编译器默认所有局部变量都是用auto来修饰的,所以在程序中很少见到。
2、static
它作用可大了,除了可以修饰变量,还可以修饰函数,修饰变量,改变其作用域和生命周期,修饰函数,改变作用域。
(1)修饰变量:如果该变量是局部变量,那么生命周期会变化,作用域限定在本文件中被使用,如果该变量是全局变量,生命周期没变化,作用域限定在本文件中被使用。
(2)修饰函数:该函数只能在当前文件下被调用。
3、register
(1)寄存器变量的处理速度是最快的,适合频繁使用的变量。
(2)寄存器变量时没有地址的,不能用取地址的方式访问。
(3)寄存器变量不能用来做全局变量,因为全局变量的生命周期很长,贯穿整个运行周期,这样CPU的内存一直被占据,影响运行的效率,甚至会影响CPU的运行,一般定义全局寄存器变量编译器会直接报错。
PS:上面3个可以放到一组,因为它们有共同的特点,都决定变量的属性。C语言中的变量是有属性的,auto属性的变量存储在栈中,static属性的变量存储在程序静态区中(解释了为什么用static修饰的局部变量生命周期和静态区的变量一样长),register属性的变量存储在CPU的寄存器中。
4、extern
它的作用就是声明变量或者函数,本来应该和static放到一组,因为它也修饰变量和函数。
(1)修饰变量:只能修饰全局变量,生命周期没变化,作用域延伸到其他文件。
(2)修饰函数:一般不用写出来,和auto类似。
(3)一般写在头文件中,然后文件用到时直接调用头文件。
5、if
if语句中的零值比较总结:
(1)bool型变量
bool a; if(a) { printf("ok"); } else { printf("error"); }
C语言中是没有bool型变量的,但是很多编译器内置了bool型变量,所以有的时候能看到像上面一样在程序中直接写出来了。实际上,一般bool型变量通常是用枚举来定义的,完整的if语句中的零值比较的示例应该是:
typedef enum _bool { , }bool; bool a=ture; if(a) { printf("ok"); } else { printf("error"); }
(2)整形变量
int a; ==a) { printf("ok"); } else { printf("error"); }
(3)浮点型变量
#define FLOAT 0.00000001 float a=0.0; if((-FLOAT<=a)&&(a<=FLOAT)) { printf("ok"); } else { printf("error"); }
如果定义float a=5.0,相应的if语句修改为:if((5.0-FLOAT<=a) && (a<=5.0+FLOAT))。
6、switch
它是if语句的同胞兄弟,当有单个条件,多个分值时,一般用switch。
PS:上面两个可以放到一组,都是分支语句。从功能上讲,if可以代替switch语句,但是,switch不能代替if语句。
7、break
既能用于循环语句,又能用于分支语句,表示终止整个循环。
8、continue
只能用于循环语句,不能用于分支语句,表示终止本次循环,进入下次循环。
//示例:从键盘输入3个数,求其中非负数之和 void test() { ,sum=; ) { scanf("%d",&input); count++; ) continue; sum+=input; } printf("%d\n",sum); }
PS:这两个可以放到一组,相同点是都能跳出循环,不同点是程度不同。
9、do
循环语句,先执行,后判断,循环体至少执行一次。
10、while
循环语句,先判断,后执行,循环体可能不执行。
11、for
循环语句,先判断,后执行。
PS:这3个可以放到一组,都是循环语句。
//示例:求1到n的和 void test() //do { ,sum=; scanf("%d",&n); ) { do { sum=sum+n; n--; }while(n); printf("the sum is %d\n",sum); } else { printf("please input a number greater than 1"); } } void test2() //while { ,sum=; scanf("%d",&n); ) { while(n) { sum=sum+n; n--; } printf("the sum is %d\n",sum); } else { printf("please input a number greater than 1"); } } void test3() //for { ,n=,sum=; scanf("%d",&n); ) { ;i--) { sum=sum+n; n--; } printf("the sum is %d\n",sum); } else { printf("please input a number greater than 1"); } }
12、sizeof
它是编译器内置的一个指示符,并不是一个函数,用于计算各个数据类型所占的内存大小。
13、void
(1)void的作用:修饰函数的返回值和参数,如果一个函数没有返回值和参数,就用void声明。
(2)不允许有void类型的变量,但允许有void*类型的变量。当void*修饰变量时,void*类型变量作为左值用于接收任意类型的指针变量,void*类型变量作为右值赋给其他指针的时候需要进行强制类型装换。
(3)拓展:在学习线性表的时候,接触到了void*函数,里面用它进行数据封装,这也是产品级代码经常使用的方法。
PS:void用来修饰函数,void*用来修饰函数和变量。
14、scanf
scanf语句的作用是等待接收键盘上输入的数据,特点是阻塞式,只有当键盘输入相应的字符之后才能接着执行后面的语句。
15、printf
(1)作用:打印输出数据,做练习和调试的时候经常用到。
(2)printf中常用的格式符及含义
格式符 | 功能 |
%d | 以带符号的10进制形式输出整数(正数不输出正号+) |
%u | 以不带符号的10进制形式输出整数 |
%o | 以不带符号的8进制形式输出整数 |
%x | 以不带符号的16进制形式输出整数 |
%c | 输出一个字符 |
%s | 输出一个或者多个字符 |
%f | 以小数形式输出单、双精度数,默认输出6位小数 |
%e | 以标准指数形式输出单、双精度数,数字部分小数位数为6位 |
(3)printf函数的入栈问题
原则是先从右边至左计算,然后从左至右打印。理解下面的代码为什么是这个结果。
void test() { ; printf("%d %d %d\n",a++,a,a++); //2 3 1 } void test2() { ; printf("%d %d %d\n",a++,a++,a++); //3 2 1 } void test3() { ; printf("%d %d %d\n",a++,++a,a); //2 3 3 } void test4() { int a; a=; printf("%d %d %d\n",a++,++a,a++); //3 4 1 printf("%d %d %d\n",a++,++a,a); //5 6 6 }
16、const
(1)首先弄清楚一点,在标准C中,const定义的不是一个常亮,它定义的是一个只读的变量。本质上只对编译器有用,告诉编译器这个变量不能作为左值,例如下面的代码编译时会报错。
void test() { ; printf("%d\n",cc); cc=3; //函数会在这行报错,因为 cc用const修饰了,代表只读,不能作为左值。 printf("%d\n",cc); }
(2)在上面讲到用const修饰的变量只对编译器有用,换句话说就是在运行时无用,还是可以同过指针修改变量的值。(const修饰数组与修饰变量类似)
void test2() { ; int *p=(int *) &cc; //注意强制类型转换 printf("%d\n",cc); *p=; printf("%d\n",cc); }
(3)const修饰指针。要领:左数右指。当const出现在*的左边时,指针指向的数据不能改变,当const出现在*右边时,指针本身(指针指向的地址值)不能改变,当const出现在*的两边时,指针指向的数据和指针本身(指针指向的地址值)都不能改变。
17、volatile
(1)作用:编译器警告指示字--告诉编译器必须每次去内存中取变量值,不要做优化。
(2)理解:在C语言中,如果定义的变量没有被当做左值(都是出现在程序语句的右边)使用,编译器就会认为它的值不会被改变, “聪明”的对其优化(能加快程序运行效率),每次默认对它取所赋的右值。举个例子:
; ; ; a=result; delay(); b=result;
上面的代码这样写没问,但是在嵌入式系统中,如果延时的200ms里出现一个中断,要把result的值(内存中的值)改变为5,如果不加volatile修饰int result=1,系统优化后,b的值还会是1,就得不到我们预期的值,出现不可预计的错误。而加上volatile,表示每次都去内存中去取值,这样中断后我们就能得到预期的值。
(3)思考
思考一:const和volatile是否可以同时修饰一个变量?
能。关键搞清楚 编译期 和 运行期 的关系。编译期就是编译器将源代码转化为汇编再到机器代码的过程。运行期就是实际的机器代码在CPU执行的过程。const在编译的时候起作用,它告诉编译器它修饰的变量是只读的,不能而出现在赋值符号左边。实际运行的时候就不是编译器所能管的了。volatile在运行的时候起作用,保证其修饰的变量不被优化,每次每次读取它都会在内存中取值。
思考二:const volatile int i = 0; 这个时候i具有什么属性?编译器如何处理这个变量?
属性:在编译期,i是一个只读变量,不能而出现在赋值符号左边。在运行期,i不会被优化,每次读取它都会在内存中取值。
处理:如果i出现在赋值符号左边,编译报错。
18、struct
(1)结构体的出现时为了解决数组的不足之处,它是一种构造数据类型。结构体使用总结如下:
1.1 结构体初始化--分为完全初始化和部分初始化
//1.1 完全初始化--定义结构体的同时定义变量 #if 0 #include<stdio.h> struct student{ char *name1; ]; int age; char c; //定义一个字符 float height; }stu={,'M',1.75}; //字符串常量初始化这一项会有警告,故实践中,完全初始化字符串时用数组的形式。 int main() { printf("姓名: %s\n",stu.name1); printf("姓名: %s\n",stu.name2); printf("年龄: %d\n",stu.age); printf("age = %c\n",stu.c); printf("身高: %.2f\n",stu.height); ; } #endif //1.2 完全初始化--先定义结构体,再定义变量 #if 0 #include<stdio.h> struct student{ char *name1; ]; int age; char c; //定义一个字符 float height; }; ,'M',1.75}; //字符串常量初始化这一项会有警告,故实践中,完全初始化字符串时用数组的形式。 int main() { printf("姓名: %s\n",stu.name1); printf("姓名: %s\n",stu.name2); printf("年龄: %d\n",stu.age); printf("age = %c\n",stu.c); printf("身高: %.2f\n",stu.height); ; } #endif //2.1 部分初始化--定义结构体的同时定义变量 #if 0 #include<stdio.h> #include<string.h> struct student { char *name1; //定义字符串常亮 ]; //定义字符串变量 int age; //定义整形数 char c; //定义一个字符 float height; //定义一个浮点数 }stu; int main() { stu.name1="pual"; //字符串常量初始化 --这样子初始化在结构体部分初始化中也会有警告,故实践中,部分初始化字符串时用数组的形式。 strcpy(stu.name2,"wade"); //字符串变量初始化 stu.age=; //整形数初始化 stu.c='m'; //字符初始化 stu.height=1.72; //浮点数初始化 printf("姓名: %s\n",stu.name1); printf("姓名: %s\n",stu.name2); printf("age = %d\n",stu.age); printf("age = %c\n",stu.c); printf(" %.2f\n",stu.height); ; } #endif //2.2 部分初始化--先定义结构体,再定义变量 #if 1 #include<stdio.h> #include<string.h> struct student { char *name1; //定义字符串常亮 ]; //定义字符串变量 int age; //定义整形数 char c; //定义一个字符 float height; //定义一个浮点数 }; struct student stu; int main() { stu.name1="pual"; //字符串常量初始化 --这样子初始化在结构体部分初始化中也会有警告,故实践中,部分初始化字符串时用数组的形式。 strcpy(stu.name2,"wade"); //字符串变量初始化 stu.age=; //整形数初始化 stu.c='m'; //字符初始化 stu.height=1.72; //浮点数初始化 printf("姓名: %s\n",stu.name1); printf("姓名: %s\n",stu.name2); printf("age = %d\n",stu.age); printf("age = %c\n",stu.c); printf(" %.2f\n",stu.height); ; } #endif /*“特别注意”: stu.name="abc"; stu.age=15; stu.height=1.72; 这些部分初始化只能写在函数里面,不能写在主函数外面,如3 //3 #include<stdio.h> struct student { char *name; int age; float height; } stu; stu.name="abc"; stu.age=15; stu.height=1.72; int main() { printf("姓名: %s\n",stu.name); printf("age = %d\n",stu.age); printf(" %.2f\n",stu.height); return 0; } 为什么写在外面不行呢?总结如下: 我写了几个简单程序: (a)#include<stdio.h> int i=1; void main() { printf("%d\n",i); } 这是正常的,相当于完全初始化。 (b)#include<stdio.h> int i; void main() { i=1; printf("%d\n",i); } #include<stdio.h> int i; void test() { i=1; printf("%d\n",i); } int main() { test(); return 0; } 这也是正常的,相当于部分初始化。 (c)#include<stdio.h> int i; i=1; void main() { printf("%d\n",i); } 这是不正常的,相当于3。 总的来说,在定义一个全局变量以后,要么在定义的时候就赋初值(如1或a),要么在主函数里面再复制(2或b),千万不能先定义,然后再赋值(如3或c) 还可以这样理解,i=1;是一条语句,放在主函数外面,不能执行到。 */
1.2 结构体不允许对本身的递归定义,即结构体内部不能包含结构体本身,但是可以包含别的结构体。
struct date { int year; int month; int day; }; struct student { int age; struct date birthday; }; void test() { ,{,,}}; // {2015,1,1}这个括号可加可不加,加了思路更清晰 printf("%d\n",stu.age); printf("%d\n",stu.birthday.year); printf("%d\n",stu.birthday.month); printf("%d\n",stu.birthday.day); }
1.3 结构体与函数--作为函数参数
//修改结构体成员变量的值--比较test1和test2的差别
struct Person { int age1; int age2; }; void change1(struct Person p1) { p1.age1=; } void test1(void) { }; printf("%d\n",person1.age1); change1(person1); printf("%d\n",person1.age1); } void change2(struct Person* p2) { (*p2).age2=; } void test2(void) { struct Person person2; person2.age2=; printf("%d\n",person2.age2); change2(&person2); printf("%d\n",person2.age2); }
1.4 结构体与数组
struct student { int age; char *name; float height; }a[]={{,,"lmj",1.90}}; void test(void) { printf(].age); printf(].name); printf(].height); printf(].age); printf(].name); printf(].height); }
1.5 结构体与指针
struct Person { int age; ]; }; void test() { ,}; struct Person *p; p=&person; printf("%d\n",person.age); printf("%d\n",(*p).age); printf("%d\n",p->age); printf(]); }
(2)思考:空结构体占多大内存?这是C语言的灰色地带,不同的编译器下面不同,在gcc编译器下面,占用0个字节,a和b的地址相同。在g++下面运行,占用1个字节,a和b的地址相差1。
struct test { }; void Test(void) { struct test a; struct test b; printf("%d\n",sizeof(struct test)); printf("%d, %x\n",sizeof(a), &a); printf("%d, %x\n",sizeof(b), &b); }
(3)由结构体实现柔性数组。 柔性数组的概念:数组大小待定的数组。因为C语言中结构体的最后一个元素可以是大未知的数组,所以可以根据这个特性由结构体产生柔性数组。
//柔性数组的大小是4,也就是 int array[];占用0个字节
typedef struct { int length; int a[]; }FlexibleArray; void test(void) { int i; FlexibleArray* space=(FlexibleArray*)malloc(); //siezeof(int)*8 决定柔性数组的大小 (*space).length=; ;i<(*space).length;i++) { (*space).a[i]=i+; printf("%d\n",(*space).a[i]); } free(space); }
19、union
(1)用法和结构体类似,初始化的时候也可以分完全初始化和部分初始化。但是完全初始化的时候只能写一个初始化值,它结构体不同,它虽然定义了几个变量,实际上每次只有一个变量起作用,
所以完全初始化的时候只能共用一个,实际应用不用完全初始化。
//会报错--[Error] too many initializers for 'name' ,只能初始化一个 union name { char c; int a; int b; }d={,}; //正确方式 union name { char c; int a; int b; }d={'H'};
(2)union和struct比较
相同点:都可以定义多个成员,且定义方式类似。
不同点:结构体大小由所有成员决定,等于所有成员所占大小之和,而union的大小由最大的成员的大小决定。 结构体所有成员都有自己的内存空间,而union所有成员共享同一块大小的内存,一次只能使用其中的一个成员。
(3)对某一个成员赋值,会覆盖其他成员的值。
(4)union的使用受操作系统的大小端的影响。小端模式:低位存低字节,高位存高字节。大端模式:低位存高字节,高位存低字节。下面程序中,如果操作系统是小端模式,打印结果为1,如果操作系统是大端模式,打印结果为0。
#include <stdio.h> union Test { int i; char j; }; int test(void); int main() { ; m=test(); printf("%d\n",m); ; } int test(void) { union Test a; a.i = ; return a.j; }
反过来,可以用union验证操作系统的大小端。上面程序中,如果打印结果为1,那么操作系统是小端模式。如果打印结果为0,操作系统是大端模式。
20、枚举
(1)枚举与宏定义有关系,它是宏定义的一种优化,现实中两种都有人用。但是用枚举的话有两个好处:第一,阅读代码时更清晰,用枚举列举定义,一看这几个就是一伙的。第二,人非圣贤,万一程序中将变量赋值了一个N,用宏定义定义的话,会执行:default:break; 而用枚举的话,直接报错,我们能迅速找到错误。举个例子:
#include <stdio.h> #if 0 #define SUN 0 #define MON 1 #define TUE 2 #define WEN 3 #define THR 4 #define FRI 5 #define SAT 6 #define N 10 #endif #if 1 enum week { SUN,MON,TUE,WEN,THR,FRI,SAT, }; #endif int main(void) { enum week today; // 使用enum week类型,来定义一个枚举变量today today = SAT; switch (today) { case MON:printf("hao kun a.\n");break; case TUE:printf("2\n");break; case WEN:printf("3.\n");break; case THR:printf("4\n");break; case FRI:printf("5.\n");break; case SAT:printf("6\n");break; case SUN:printf("ha ha.\n");break; default:break; } ; }
(2)枚举定义类似于结构体和共用体,但初始化方式不同。默认第一个枚举常量为0,其他枚举常量在前一个值的基础上一次加1。
(3)枚举类型和#define的区别:①宏常量只是简单的进行值替换,无类型信息,不是真正意义上的常量,枚举常量是一种特定类型的常量。②#define宏常量无法被调试,枚举常量可以。
21、typedef
(1)作用:给各种数据类型起一个别名。
(2)用法归纳
2.1 给基本数据类型起别名
void test(void) { typedef int Integer; typedef Integer MyInteger; typedef unsigned int UInteger; ; Integer b = ; UInteger c = ; MyInteger d = ; printf("%d\n",a); printf("%d\n",b); printf("%d\n",c); printf("%d\n",d); }
2.2 给指针类型起别名
void test1(void) { char* s = "pual"; printf("%s\n",s); typedef char* String; String s1 = "wade"; printf("%s\n",s1); }
2.3 给结构体起别名
void test2(void) { typedef struct { float x; float y; }Point; Point p = {,}; printf("%f\n",p.x); printf("%f\n",p.y); }
2.4 给结构体指针起别名
//先给结构体起一个别名,再给指向结构体的指针起一个别名 void test3_1(void) { typedef struct { float x; float y; }Point; typedef Point* PP; Point point = {10.0f, 20.0f}; PP pp = &point; printf("x=%f, y=%f\n", pp->x, pp->y); printf("x=%f, y=%f\n", (*pp).x,(*pp).y); printf("x=%f, y=%f\n", point.x, point.y); } //直接给指向结构体的指针起一个别名 void test3_2(void) { typedef struct Point { float x; float y; }* PP; struct Point point={1.0f, 2.0f}; PP pp = &point; printf("x=%f, y=%f\n", pp->x, pp->y); printf("x=%f, y=%f\n", (*pp).x,(*pp).y); printf("x=%f, y=%f\n", point.x, point.y); }
2.5 给枚举起别名(类似于结构体)
void test4(void) { typedef enum { spring, summer, autumn=, winter, //最后一个逗号可加可不加 }Season; Season s1 = spring; Season s2 = autumn; Season s3 = winter; printf("%d\n",s1); printf("%d\n",s2); printf("%d\n",s3); printf("%d\n",winter); }
2.6 给指向函数的指针定义一个别名
int sum(int a, int b) { int c = a + b; printf("%d+%d=%d\n", a, b, c); return c; } //不用typedef起别名时的指向函数的指针 void test5_1(void) { int (*p)(int, int); ; p = sum; receive_sum=(*p)(, ); printf("%d\n",receive_sum); } //给指向函数的指针定义一个别名(这个跟之前的不一样,在这里SumPoint就是别名--int (* )(int,int) ) void test5_2(void) { typedef int (*SumPoint)(int,int); ; SumPoint p = sum; receive_sum=(*p)(, ); printf("%d\n",receive_sum); }
2.7 给数组类型起一个别名
//数组类型由元素类型和数组大小共同决定 //标准:typedef type(name)[size]; //举例:typedef int(INT5)[5]; void test6(void) { typedef ]; AINT5 a={,,,,}; //这样就定义就等价与int a[5]; printf(]); printf(]); }
(3)给一种数据类型起一个别名最好用typedef,而不用宏定义。理由如下:
typedef char* String1; //typedef是数据类型替换 #define String2 char* //宏定义纯粹是字符串替换 String1 s1,s2; //相当于 char* s1; char* s2; String2 s3,s4; //相当于 char* s3; char s4;
最后一个goto,尽量不用,因为它有很大的副作用,可能导致一些本该执行的语句被跳过!