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型变量
1 bool a; 2 if(a) 3 { 4 printf("ok"); 5 } 6 else 7 { 8 printf("error"); 9 }
C语言中是没有bool型变量的,但是很多编译器内置了bool型变量,所以有的时候能看到像上面一样在程序中直接写出来了。实际上,一般bool型变量通常是用枚举来定义的,完整的if语句中的零值比较的示例应该是:
1 typedef enum _bool 2 { 3 false=0, 4 true=1 5 }bool; 6 7 bool a=ture; 8 if(a) 9 { 10 printf("ok"); 11 } 12 else 13 { 14 printf("error"); 15 }
(2)整形变量
1 int a; 2 if(0==a) 3 { 4 printf("ok"); 5 } 6 else 7 { 8 printf("error"); 9 }
(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
只能用于循环语句,不能用于分支语句,表示终止本次循环,进入下次循环。
1 //示例:从键盘输入3个数,求其中非负数之和 2 void test() 3 { 4 int input=0,count=0,sum=0; 5 while(count<3) 6 { 7 scanf("%d",&input); 8 count++; 9 if(input<0) continue; 10 sum+=input; 11 } 12 printf("%d\n",sum); 13 }
PS:这两个可以放到一组,相同点是都能跳出循环,不同点是程度不同。
9、do
循环语句,先执行,后判断,循环体至少执行一次。
10、while
循环语句,先判断,后执行,循环体可能不执行。
11、for
循环语句,先判断,后执行。
PS:这3个可以放到一组,都是循环语句。
1 //示例:求1到n的和 2 void test() //do 3 { 4 int n=0,sum=0; 5 scanf("%d",&n); 6 if(n>1) 7 { 8 do 9 { 10 sum=sum+n; 11 n--; 12 }while(n); 13 printf("the sum is %d\n",sum); 14 } 15 else 16 { 17 printf("please input a number greater than 1"); 18 } 19 } 20 void test2() //while 21 { 22 int n=0,sum=0; 23 scanf("%d",&n); 24 if(n>1) 25 { 26 while(n) 27 { 28 sum=sum+n; 29 n--; 30 } 31 printf("the sum is %d\n",sum); 32 } 33 else 34 { 35 printf("please input a number greater than 1"); 36 } 37 } 38 void test3() //for 39 { 40 int i=0,n=0,sum=0; 41 scanf("%d",&n); 42 if(n>1) 43 { 44 for(i=n;i>0;i--) 45 { 46 sum=sum+n; 47 n--; 48 } 49 printf("the sum is %d\n",sum); 50 } 51 else 52 { 53 printf("please input a number greater than 1"); 54 } 55 }
12、sizeof
它是编译器内置的一个指示符,并不是一个函数,用于计算各个数据类型所占的内存大小。
(1)sizeof常用来计算数组长度,int a[]={1,2,3,4,5,6,7,8}; 数字长度就是:sizeof(a)/sizeof(a[0])
(2)sizeof与strlen的差别
#include<stdio.h> #include<string.h> int main() { char a[]="abcdefg"; printf("%d\n", sizeof(a)); //结果:8 printf("%d\n", strlen(a)); //结果:7 return 0; }
strlen是一个C库函数,用来返回一个字符串的长度(注意,字符串的长度是不计算字符串末尾的'\0'的)。strlen接收的参数必须是一个字符串(字符串的特征是以'\0'结尾)
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函数的入栈问题
原则是先从右边至左计算,然后从左至右打印。理解下面的代码为什么是这个结果。
1 void test() 2 { 3 int a=1; 4 printf("%d %d %d\n",a++,a,a++); //2 3 1 5 } 6 void test2() 7 { 8 int a=1; 9 printf("%d %d %d\n",a++,a++,a++); //3 2 1 10 } 11 void test3() 12 { 13 int a=1; 14 printf("%d %d %d\n",a++,++a,a); //2 3 3 15 } 16 void test4() 17 { 18 int a; 19 a=1; 20 printf("%d %d %d\n",a++,++a,a++); //3 4 1 21 printf("%d %d %d\n",a++,++a,a); //5 6 6 22 }
16、const
(1)首先弄清楚一点,在标准C中,const定义的不是一个常亮,它定义的是一个只读的变量。本质上只对编译器有用,告诉编译器这个变量不能作为左值,例如下面的代码编译时会报错。
1 void test() 2 { 3 const int cc=1; 4 printf("%d\n",cc); 5 cc=3; //函数会在这行报错,因为 cc用const修饰了,代表只读,不能作为左值。 6 printf("%d\n",cc); 7 }
(2)在上面讲到用const修饰的变量只对编译器有用,换句话说就是在运行时无用,还是可以同过指针修改变量的值。(const修饰数组与修饰变量类似)
1 void test2() 2 { 3 const int cc=1; 4 int *p=(int *) &cc; //注意强制类型转换 5 printf("%d\n",cc); 6 *p=3; 7 printf("%d\n",cc); 8 }
(3)现代C编译器中的const将具有全局生命周期的变量存储于只读存储区,不客修改。
#include<stdio.h> const int g_array[5] = {0}; void modify(int* p,int v) { *p = v; } int main() { const int i=0; const static int j=0; const int array[5] = {0}; modify((int*)&i,1); //ok //modify((int*)&j,2); //运行崩溃 modify((int*)&array[0],3); //ok //modify((int*)&g_array[0],4); //运行崩溃 printf("%d\n",i); printf("%d\n",j); printf("%d\n",array[0]); printf("%d\n",g_array[0]); return 0; }
(4)const修饰指针。要领:左数右指。当const出现在*的左边时,指针指向的数据不能改变,当const出现在*右边时,指针本身(指针指向的地址值)不能改变,当const出现在*的两边时,指针指向的数据和指针本身(指针指向的地址值)都不能改变。
17、volatile
(1)作用:编译器警告指示字--告诉编译器必须每次去内存中取变量值,不要做优化。
(2)理解:在C语言中,如果定义的变量没有被当做左值(都是出现在程序语句的右边)使用,编译器就会认为它的值不会被改变, “聪明”的对其优化(能加快程序运行效率),每次默认对它取所赋的右值。举个例子:
1 int result=1; 2 int a=0; 3 int b=0; 4 a=result; 5 delay(200); 6 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; char name2[10]; int age; char c; //定义一个字符 float height; }stu={"coco","wade",23,'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); return 0; } #endif //1.2 完全初始化--先定义结构体,再定义变量 #if 0 #include<stdio.h> struct student{ char *name1; char name2[10]; int age; char c; //定义一个字符 float height; }; struct student stu={"coco","wade",23,'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); return 0; } #endif //2.1 部分初始化--定义结构体的同时定义变量 #if 0 #include<stdio.h> #include<string.h> struct student { char *name1; //定义字符串常亮 char name2[10]; //定义字符串变量 int age; //定义整形数 char c; //定义一个字符 float height; //定义一个浮点数 }stu; int main() { stu.name1="pual"; //字符串常量初始化 --这样子初始化在结构体部分初始化中也会有警告,故实践中,部分初始化字符串时用数组的形式。 strcpy(stu.name2,"wade"); //字符串变量初始化 stu.age=15; //整形数初始化 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); return 0; } #endif //2.2 部分初始化--先定义结构体,再定义变量 #if 1 #include<stdio.h> #include<string.h> struct student { char *name1; //定义字符串常亮 char name2[10]; //定义字符串变量 int age; //定义整形数 char c; //定义一个字符 float height; //定义一个浮点数 }; struct student stu; int main() { stu.name1="pual"; //字符串常量初始化 --这样子初始化在结构体部分初始化中也会有警告,故实践中,部分初始化字符串时用数组的形式。 strcpy(stu.name2,"wade"); //字符串变量初始化 stu.age=15; //整形数初始化 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); return 0; } #endif /*“特别注意”: stu.name="abc"; stu.age=15; stu.height=1.72; 这些部分初始化要写在函数里面,不能写在函数外面,如下这种初始化是会报错的: #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; } */
补充:一种结构体初始化方法,可选择性初始化。
#include<stdio.h> struct student { char name[7]; int height; }; int main() { struct student a = { .name[0]='m', .name="machael", .height=20, }; printf("%c\n", a.name[0]); printf("%s\n", a.name); printf("%d\n", a.height); return 0; }
1.2 结构体不允许对本身的递归定义,即结构体内部不能包含结构体本身,但是可以包含别的结构体。
struct date { int year; int month; int day; }; struct student { int age; struct date birthday; }; void test() { struct student stu={27,{2015,1,1}}; // {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的差别
//先参考基本数据类型修改其变量值-比较test3和test4的差别
void change3(int p3)
{
p3=10;
}
void test3(void)
{
int person3;
person3=30;
printf("%d\n",person3); //30
change3(person3);
printf("%d\n",person3); //30
}
void change4(int* p4)
{
*p4=10;
}
void test4(void)
{
int person4;
person4=30;
printf("%d\n",person4); //30
change4(&person4);
printf("%d\n",person4); //10
}
//再看结构体修改其变量值
struct Person { int age1; int age2; }; void change1(struct Person p1) { p1.age1=9; } void test1(void) { struct Person person1={27}; printf("%d\n",person1.age1); //27 change1(person1); printf("%d\n",person1.age1); //27 } void change2(struct Person* p2) { (*p2).age2=9; } void test2(void) { struct Person person2; person2.age2=27; printf("%d\n",person2.age2); //27 change2(&person2); printf("%d\n",person2.age2); //9 }
1.4 结构体与数组
struct student { int age; char *name; float height; }a[2]={{27,"mj",1.8},{28,"lmj",1.90}}; void test(void) { printf("%d\n",a[0].age); printf("%s\n",a[0].name); printf("%.1f\n",a[0].height); printf("%d\n",a[1].age); printf("%s\n",a[1].name); printf("%.2f\n",a[1].height); }
1.5 结构体与指针
struct Person { int age; char name[5]; }; void test() { struct Person person={12,1}; struct Person *p; p=&person; printf("%d\n",person.age); printf("%d\n",(*p).age); printf("%d\n",p->age); printf("%d\n",p->name[0]); }
(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(sizeof(FlexibleArray)+sizeof(int)*8); //siezeof(int)*8 决定柔性数组的大小 (*space).length=8; for(i=0;i<(*space).length;i++) { (*space).a[i]=i+1; 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={'H',10,12}; //正确方式 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() { int m=0; m=test(); printf("%d\n",m); return 0; } int test(void) { union Test a; a.i = 1; 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; } return 0; }
(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; int a = 1; Integer b = 2; UInteger c = 3; MyInteger d = 4; 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 = {10,10}; 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=3, 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); int receive_sum=0; p = sum; receive_sum=(*p)(4, 5); printf("%d\n",receive_sum); } //给指向函数的指针定义一个别名(这个跟之前的不一样,在这里SumPoint就是别名--int (* )(int,int) ) void test5_2(void) { typedef int (*SumPoint)(int,int); int receive_sum=0; SumPoint p = sum; receive_sum=(*p)(4, 5); printf("%d\n",receive_sum); }
2.7 给数组类型起一个别名
//数组类型由元素类型和数组大小共同决定 //标准:typedef type(name)[size]; //举例:typedef int(INT5)[5]; void test6(void) { typedef int(AINT5)[5]; AINT5 a={1,2,3,4,5}; //这样就定义就等价与int a[5]; printf("%d\n",a[0]); printf("%d\n",a[4]); }
(3)给一种数据类型起一个别名最好用typedef,而不用宏定义。理由如下:
typedef char* String1; //typedef是数据类型替换 #define String2 char* //宏定义纯粹是字符串替换 String1 s1,s2; //相当于 char* s1; char* s2; String2 s3,s4; //相当于 char* s3; char s4;
22、goto
这个关键字尽量不要用。据说高手编程不用goto,因为C语言是一门面向过程的结构化语言,它程序的结构有3种,顺序执行,选择执行,循环执行。再复杂的C程序也是由这三种结构组合而成的。goto带有浓厚的汇编特性,类似于汇编里面的跳转指令,使得C程序以第四种方式运行,破坏了程序的结构化特性。