资源描述
C语言
1、 概述
C语言的数据类型有:整型(int)、实型(float)、字符型(char)、数组类型(int [])、指针类型(void *)、结构体类型(struct)、共用体类型(union)等
C语言允许直接访问物理地址,可以直接对硬件进行操作,可以像汇编语言同样对位(bit)、字节和地址进行操作
2、第一个函数分析
· #include 是C语言的预解决指令之一,所谓预解决,就是在编译之前做的解决,预解决指令一般以 # 开头
· #include 指令后面会跟着一个文献名,预解决器发现 #include 指令后,就会根据文献名去查找文献,并把这个文献的内容包含到当前文献中。被包含文献中的文本将替换源文献中的 #include 指令,就像你把被包含文献中的所有内容拷贝到这个 #include 指令所在的位置同样
一个C程序中一定会有一个main函数,也只能有一个main函数。main函数是整个C程序的入口。
1.在编译前先执行#include指令,拷贝stdio.h的内容到源程序中
2.编译源程序,生成目的文献
3.链接C语言函数库,生成可执行文献
4.运营可执行文献,在屏幕上输出"Hello, World!"
3、函数
C语言的函数可以大约分为3类:
1. 主函数,也就是main函数。每个程序中只能有一个、也必须有一个主函数。
2. 2.开发人员自定义的函数,可有可无,数目不限
3. 3.C语言提供的库函数
在标准C语言中,函数的定义顺序是有讲究的,默认情况下,只有后面定义的函数才可以调用前面定义过的函数
假如调换下sum函数和main的顺序,在标准的C编译器环境下是不合法的
假如想把其他函数的定义写在main函数后面,并且main函数能正常调用这些函数,那就必须在main函数前面作一下函数的声明
可以省略参数名称,如
在大型的C程序中,为了分模块进行开发,一般会将函数的声明和定义(即实现)分别放在2个文献中,函数声明放在.h头文献中,函数定义放在.c源文献中
运营环节分析:
1> 在编译之前,预编译器会将sum.h文献中的内容拷贝到main.c中
2> 接着编译main.c和sum.c两个源文献,生成目的文献main.obj和sum.obj,这2个文献是不能被单独执行的,因素很简朴:
* sum.obj中不存在main函数,肯定不可以被执行
* main.obj中虽然有main函数,但是它在main函数中调用了一个sum函数,而sum函数的定义却存在于sum.obj中,因此main.obj依赖于sum.obj
3> 把main.obj、sum.obj链接在一起,生成可执行文献
4> 运营程序
在定义函数时,函数名后面的()中定义的变量称为形式参数(形参);在调用函数时传入的值称为实际参数(实参)。
4、 printf函数scanf函数
3个%d之间是用空格隔开的,我们在每输入一个整数后必须输入一个分隔符,分隔符可以是空格、tab、回车
5、 基本数据类型
在Java中,你声明了一个局部变量后,假如没有通过初始化赋值就使用该变量,编译器直接报错
在C语言中,你声明看一个局部变量后,没有通过初始化赋值是可以使用的
但这是很危险的,不建议这样做,b也许会是任意大整数
假如是全局的int类型变量,系统会默认赋值为0
char的取值范围是:ASCII码字符 或者 -128~127的整数
下面的写法都是错误的:
有以下4种类型修饰符:
· short 短型
· long 长型
· signed 有符号型
· unsigned 无符号型
红色的代表常用的数据类型
6、 基本语句
C语言的基本语句跟Java中的差不多,所以,这里只是简朴地提一下
· 循环语句(do while、while、for)
· 条件语句(if 、if-else、switch)
· goto语句
7、 基本运算
1、 算术运算符
· + 加法运算符
· - 减法运算符,或负值运算符
· * 乘法运算符
· / 除法运算符
· % 模运算符,或称取余运算符,规定%两侧均为整型
2、 关系运算符
· < 小于运算符
· <= 小于等于运算符
· > 大于运算符
· >= 大于等于运算符
· == 等于运算符
· != 不等于运算符
3、 逻辑运算符
· && 逻辑与运算符
· || 逻辑或运算符
· ! 逻辑非运算符
4、 赋值运算符
1.简朴的赋值运算符 = :int a = 5;
2.复合赋值运算符
· += 加赋值运算符。如a += 3+1,等价于 a = a +(3+1)
· -= 减赋值运算符。如a -= 3+1,等价于 a = a -(3+1)
· *= 乘赋值运算符。如a *= 3+1,等价于 a = a *(3+1)
· /= 除赋值运算符。如a /= 3+1,等价于 a = a /(3+1)
· %= 取余赋值运算符。如a %= 3+1,等价于 a = a %(3+1)
5、 自增自减运算符
· ++ 自增运算符。如a++,++a,都等价于a = a+1
· -- 自减运算符。如a--,--a,都等价于a = a-1
6、 逗号运算符,逗号表达式
· 用逗号运算符连接起来的表达式称为逗号表达式,它的一般形式为:
· 表达式1, 表达式2, … …, 表达式n
· 逗号表达式的运算过程是:从左到右的顺序,先计算表达式1,接着计算表达式2,...,最后计算表达式n
· * 整个逗号表达式的值是最后一个表达式的值
++a的结果为3,a *= 2的结果为6,b = a * 5的结果为30。
因此,输出结果为:
7、条件运算符条件表达式
其实就是三目运算符,一般形式为:表达式1 ? 表达式2 : 表达式3
8、Sizeof
sizeof可以用来计算一个变量或者一个常量、一种数据类型所占的内存字节数。
sizeof一共有3种形式
· sizeof( 变量\常量 )
· sizeof 变量\常量
· sizeof( 数据类型 )
注意,不可以写成sizeof float;
8、 数组
一维数组定义的形式为:类型 数组名[元素个数] int a[5];
[]只能放在数组名的后面,下面的都是错误写法:
[]里面的个数必须是一个固定值,可以是常量(比如6、8)、常量表达式(比如3+4、5*7)。绝对不能使用变量或者变量表达式来表达元素个数,大多数情况下不要省略元素个数(当数组作为函数的形参和数组初始化时除外)
下面的都是对的写法:
int a[5]; // 整型常量
int b['A']; // 字符常量,其实就是65
int c[3*4]; // 整型常量表达式
下面的都是错误写法:
int a[]; // 没有指定元素个数,错误
int i = 9;
int a[i]; // 用变量做元素个数,错误
定义数组时,系统将按照数组类型和个数分派一段连续的存储空间来存储数组元素,数组名代表着整个数组的地址,也就是数组的起始地址。如int a[3] 其实a不算是变量,是个常量,它代表着数组的地址。a == &a[0]
初始化的一般形式是:类型 数组名[元素个数] = {元素1, 元素2, ...};
int a[2] = {8, 10};
其实相称于:
int a[2];
a[0] = 8;
a[1] = 10;
注意的是:C语言中编译器是不会对数组下标越界进行检查的,所以自己访问数组元素时要小心
元素值列表可以是数组所有元素的初值,也可以是前面部分元素的初值
int a[4] = {2, 5};
当数组为整型时,初始化未拟定初值的元素,默认为0,所以上面的a[2]、a[3]都为0
当对所有数组元素都赋初值时,可以省略元素个数
int a[] = {2, 5, 7};
说明数组a的元素个数是3
数组初始化时的赋值方式只能用于数组的定义,定义之后只能一个元素一个元素地赋值
下面的写法是错误的:
int a[3];
a[3] = {1, 2, 3}; // 错误
a = {1, 2, 3}; // 错误
其实为什么是错误的写法呢?我们可以简要分析一下。
1> 第2行的a[3]代表着访问数组的第4个元素,一方面这里已经是数组下标越界了;就算没有越界,给a[3]赋值时也应当赋一个int类型的整数,不应当是{}。
2> 第3行的a是数组名,代表着数组的地址,它是个常量!给常量赋值,那肯定错了!
一维数组的元素作为函数实参,与同类型的简朴变量作为实参同样,是单向的值传递,即数组元素的值传给形参,形参的改变不影响实参
假如一维数组的名字作为函数实参,传递的是整个数组,这样形参数组修改时,实参数组也同时被修改了。形参数组的元素个数可以省略。
二维数组定义形式:类型 数组名[行数][列数]
int a[2][3]; // 共2行3列,6个元素
a[0]、a[1]也是数组,是一维数组,并且a[0]、a[1]就是数组名,因此a[0]、a[1]就代表着这个一维数组的地址
按行进行初始化
int a[2][3] = { {2, 2, 3}, {3, 4, 5} };
按存储顺序进行初始化(先存放第1行,再存放第2行)
int a[2][3] = {2, 2, 3, 3, 4, 5};
对部分元素进行初始化
int a[2][3] = { {2}, {3, 4} };
int b[3][3] = { { }, { , , 2}, {1, 2, 3}};
假如只初始化了部分元素,可以省略行数,但是不可以省略列数
int a[][3] = {1, 2, 3, 4, 5, 6};
int a[][3] = {{1, 2, 3}, {3, 5}, {}};
int a[2][] = {1, 2, 3, 4, 5, 6}; // 错误写法
二维数组会先存放第1行的元素,由于不拟定列数,也就是不拟定第1行要存放多少个元素,所以这里会产生很多种情况,所以只指定行数是错误的
9、 字符串
在C语言中,我们可以用字符数组来存储字符串。
* 字符串可以看做是一个特殊的字符数组,为了跟普通的字符数组区分开来,应当在字符串的尾部添加了一个结束标志'\0'。'\0'是一个ASCII码值为0的字符,是一个空操作符,表达什么也不干。所以采用字符数组存放字符串,赋值时应包含结束标志'\0'。
* 字符串"mj"的存储情况如下(假设用字符数组char a[]来存储):
注意了,尾部有个'\0',假如没有这个结束标记,说明这个字符数组存储的并不是字符串
字符串的初始化:
当我们使用类似第3个的初始化方式时,系统会自动在字符串尾部加上一个\0结束符
字符串的输出我们可以使用stdio.h中两个函数来输出字符串,分别是printf和puts函数
%s表达盼望输出一个字符串,因此printf函数会从b的首地址开始按顺序输出字符,一直到\0字符为止,由于\0是字符串的结束标记。同样puts函数会从a的首地址开始输出字符,一直到\0字符为止。
所以,假如想要创建一个字符串,记得加上结束符\0,不然后果很严重,会访问到一些垃圾数据。
puts函数输出一个字符串后会自动换行,
puts函数一次只能输出一个字符串,printf函数则可以同时输出多个字符串
字符串的输入我们可以使用stdio.h中有2个函数可以用来接受用户输入的字符串,分别是scanf和gets
gets跟scanf同样,会从a的首地址开始存放用户输入的字符,存放完毕后,系统会自动在尾部加上一个结束标记\0。
* gets一次只能读取一个字符串,scanf则可以同时读取多个字符串
* gets可以读入包含空格、tab的字符串,直到碰到回车为止;scanf不能用来读取空格、tab
字符串数组:
10、 字符与字符串常用解决函数
字符解决函数
字符输出putchar():
putchar(65); // A
putchar('A'); // A
int a = 65;
putchar(a); // A
上面的3种用法,输出的都是大写字母A。
* putchar一次只能输出一个字符,而printf可以同时输出多个字符
printf("%c %c %c", 'A', 'B', 'a');
字符输入getchar():
char c;
c = getchar();
getchar会将用户输入的字符赋值给变量c。
* getchar函数可以读入空格、TAB,直到碰到回车为止。scanf则不能读入空格和TAB。
* getchar一次只能读入一个字符。scanf则可以同时接受多个字符。
* getchar还能读入回车换行符,这时候你要敲2次回车键。第1次敲的回车换行符被getchar读入,第2次敲的回车键代表输入结束。
字符串解决函数:
1.strlen函数
strlen函数会从s2的首地址开始计算字符串的字符个数,直到碰到空字符\0为止,不涉及\0
2.strcpy函数
strcpy函数会将右边的"lmj"字符串拷贝到字符数组s中。从s的首地址开始,逐个字符拷贝,直到拷贝到\0为止。当然,在s的尾部肯定会保存一个\0。
* 假设右边的字符串中有好几个\0,strcpy函数只会拷贝第1个\0之前的内容,后面的内容不拷贝
最后字符串s中的内容为:mj
3.strcat函数
char s1[30] = "LOVE";
strcat(s1, "OC");
strcat函数会将右边的"OC"字符串拼接到s1的尾部,最后s1的内容就变成了"LOVEOC"
strcat函数会从s1的第1个\0字符开始连接字符串,s1的第1个\0字符会被右边的字符串覆盖,连接完毕后在s1的尾部保存一个\0
* 注意下面的情况
char s1[30] = {'L', 'm', 'j', '\0', 'L', 'o', 'v', 'e', '\0'};
strcat(s1, "OC");
printf("%s", s1);
第1行初始化的s1有2个\0,通过第2行的strcat函数后,输出结果:
4.strcmp函数
* 这个函数可以用来比较2个字符串的大小
* 调用形式为:strcmp(字符串1, 字符串2)
* 两个字符串从左至右逐个字符比较(按照字符的ASCII码值的大小),直到字符不相同或者碰见'\0'为止。假如所有字符都相同,则返回值为0。假如不相同,则返回两个字符串中第一个不相同的字符ASCII码值的差。即字符串1大于字符串2时函数返回值为正,否则为负。
char s1[] = "abc";
char s2[] = "abc";
char s3[] = "aBc";
char s4[] = "ccb";
printf("%d, %d, %d", strcmp(s1, s2), strcmp(s1, s3), strcmp(s1, s4));
输出结果:
· s1和s2相同,所以返回0
· s1和s3是第2个字符不相同,b的ASCII码值是98,B的ASCII码值是66,b - B = 32,所以返回32
· s1和s4是第1个字符就不相同,a的ASCII码值是97,c的ASCII码值是99,a - c = -2,所以返回-2
11、 指针
通过变量名来直接引用变量,然后进行赋值:
char a;
a = 10;
通过变量名引用变量,由系统自动完毕变量名和其存储地址之间的转换,称为变量的"直接引用"方式
"直接引用"是直接通过变量名来读写变量
C语言中尚有一种"间接引用"的方式(以变量a为例):一方面将变量a的地址存放在另一个变量中,比如存放在变量b中,然后通过变量b来间接引用变量a,间接读写变量a的值。这就是"间接引用"。
假如程序通过"间接引用"的方式来修改a的值,可以这样做:先根据 变量名b 获取 变量b 的地址ffc2,取出变量b中存储的内容ffc1,也就是变量a的地址,再根据变量a的地址ffc1找到a的存储空间,然后修改里面的数据。
总结一句:用来存放变量地址的变量,就称为"指针变量"。在上面的情况下,变量b就是个"指针变量",我们可以说指针变量b指向变量a。
指针的定义 一般形式:类名标记符 *指针变量名;
int *p;
float *q;
指针的初始化
1. 先定义后初始化
2. 定义时同时初始化
指针变量是用来存放变量地址的,不要给它随意赋值一个常数。下面的写法是错误的
3. 指针运算符 *
1. 给指针指向的变量赋值
2. 取出指针所指向变量的值
4. 指针用途
写一个函数swap,接受2个整型参数,功能是互换两个实参的值。
默认情况下,一个函数只能有一个返回值,有了指针,我们可以实现函数有"多返回值"
每个指针变量所占用的内存空间是同样的,并且存储的都是地址,为什么指针变量还要分类型?
由于当运用*p获取变量c的值时,由于指针p会根据变量c的类型来取多少字节的数据。
5. 指针与数组
p是指针,a是一个数组
1> 假如p指向了一个数组元素,则p+1表达指向数组该元素的下一个元素。比如,假设p = &a[0],则p+1表达a[1]的地址
2> 对于不同类型的数组元素,p值的改变是不同的。假如数组元素为int类型,p+1代表着p的值加上2(16位编译器环境下)
3> 假如p的初值是&a[0],那么
· p+i和a+i都可以表达元素a[i]的地址,它们都指向数组的第i个元素。a代表数组首地址,a+i也是地址,它的计算方法与p+i相同
· *(p+i)和*(a+i)都表达数组元素a[i]
· 虽然p+i和a+i都指向数组的第i个元素,但两者使用时还是有区别的。由于作为指针变量的p可以改变自身值,如p++,使p的值自增。而数组名a是一个代表数组首地址的常量,它的值是不能改变的,即a++是不合法的,数组名a是个常量!不能进行赋值运算!
4> 引用一个数组元素可以有两种方法:
· 下标法: 如a[i]
· 指针法: 如*(p+i) 或 *(a+i)
6. 指针、数组和函数
· 函数的实参和形参都可以分别使用数组或指针。这样就有4种情况:
也就是说,假如一个函数的形参类型是一个数组,调用函数时,你可以传入数组名或者指针变量;
假如一个函数的形参类型是一个指针变量,调用函数时,你可以传入数组名或者指针变量。
7. 指针与字符串
用指针遍历字符串的所有字符
字符串解决函数的参数都是指向字符变量的指针类型,因此可以传入指针变量或者数组名。
我们也可以直接用指针指向一个字符串,省略定义字符数组这个环节
但是要注意:
· char a[] = "lmj";定义的是一个字符串变量!
· char *p2 = "lmj";定义的是一个字符串常量!
下面的做法也是错误的:
由于s指向的一个常量字符串,不允许修改它内部的字符
字符数组也是不允许这样做,下面的做法是错误的:
由于s是数组第一个元素的地址,但是可以*s = 'L';修改字符串变量内部字符
8.函数指针
函数作为一段程序,在内存中也要占据部分存储空间,它也有一个起始地址,即函数的入口地址。其中,函数名就代表着函数的地址。
指向函数的指针的定义的一般形式:
函数的返回值类型 (*指针变量名)(形式参数1, 形式参数2, ...);
注意:形式参数的变量名可以省略,甚至整个形式参数列表都可以省略
返回指针的函数的定义char *upper(char *str) 和 指向函数的指针的定义int (*p)(int a, int b)非常相似,使用时特别注意区分
指向函数的指针变量重要有两个用途:
· 调用函数
· 将函数作为参数在函数间传递。
假如以后想再增长一种乘法运算,非常简朴,主线不用修改counting函数的代码,只需要再增长一个乘法运算的函数.
12、 预解决指令
1.C语言在对源程序进行编译之前,会先对一些特殊的预解决指令作解释(比如之前使用的#include文献包含指令),产生一个新的源程序(这个过程称为编译预解决),之后再进行通常的编译
2.为了区分预解决指令和一般的C语句,所有预解决指令都以符号"#"开头,并且结尾不用分号
3.预解决指令可以出现在程序的任何位置,它的作用范围是从它出现的位置到文献尾。习惯上我们尽也许将预解决指令写在源程序开头,这种情况下,它的作用范围就是整个源程序文献
4.C语言提供的预解决指令重要有:宏定义、条件编译、文献包含
宏定义可以分为2种:不带参数的宏定义 和 带参数的宏定义。
不带参数的宏定义:
#define 宏名 字符串 比如#define ABC 10
它的作用是在编译预解决时,将源程序中所有"宏名"替换成右边的"字符串",常用来定义常量。
1> 宏名一般用大写字母,以便与变量名区别开来,但用小写也没有语法错误
2> 对程序中用双引号扩起来的字符串内的字符,不进行宏的替换操作。
3> 在编译预解决用字符串替换宏名时,不作语法检查,只是简朴的字符串替换。只有在编译的时候才对已经展开宏名的源程序进行语法检查
4> 宏名的有效范围是从定义位置到文献结束。假如需要终止宏定义的作用域,可用#undef命令
PI这个宏在第1行到第8行之间是有效的,第8行后就无效了
5> 定义一个宏时可以引用已经定义的宏名
带参数的宏定义
#define 宏名(参数列表) 字符串
在编译预解决时,将源程序中所有宏名替换成字符串,并且将 字符串中的参数 用 宏名右边参数列表 中的参数替换
输出结果为:
注意:
1> 宏名和参数列表之间不能有空格,否则空格后面的所有字符串都作为替换的字符串
如
2> 带参数的宏在展开时,只作简朴的字符和参数的替换,不进行任何计算操作。所以在定义宏时,一般用一个小括号括住字符串的参数。
将被替换成int b = 2*3+4;,输出结果:
应当将被替换成int b = 2*(3+4);,输出结果:
3> 计算结果最佳也用括号括起来
所以最佳是#define Pow(a) ( (a) * (a) )
从整个使用过程可以发现,带参数的宏定义,在源程序中出现的形式与函数很像。但是两者是有本质区别的:
1> 宏定义不涉及存储空间的分派、参数类型匹配、参数传递、返回值问题
2> 函数调用在程序运营时执行,而宏替换只在编译预解决阶段进行。所以带参数的宏比函数具有更高的执行效率
条件编译:
在很多情况下,我们希望程序的其中一部分代码只有在满足一定条件时才进行编译,否则不参与编译(只有参与编译的代码最终才干被执行),这就是条件编译。
#if 和 #elif后面的条件一般是判断宏定义而不是判断变量,由于条件编译是在编译之前做的判断,宏定义也是编译之前定义的,而变量是在运营时才产生的、才有使用的意义
#if 和 #elif后面的条件不仅仅可以用来判断宏的值,还可以判断是否认义过某个宏。比如:
#ifdef的使用和#if defined()的用法基本一致
#ifndef又和#if !defined()的用法基本一致
文献包含:
#include <文献名> 直接到C语言库函数头文献所在的目录中寻找文献
#include "文献名" 系统会先在源程序当前目录下寻找,若找不到,再到操作系统的path途径中查找,最后才到C语言库函数头文献所在目录中查找
注意:
1.#include指令允许嵌套包含,比如a.h包含b.h,b.h包含c.h,但是不允许递归包含,比如 a.h 包含 b.h,b.h 包含 a.h。
2.使用#include指令也许导致多次包含同一个头文献,减少编译效率
为了解决这种反复包含同一个头文献的问题,一般我们会这样写头文献内容:
13、 变量类型
不同类型的变量有不同的作用域、不同的存储类型、不同的生命周期,C语言也提供了一些关键字来设立变量的属性(比如设立存储类型、生命周期)。
C语言根据变量作用域的不同,将变量分为局部变量和全局变量。
1.局部变量
1> 定义:在函数内部定义的变量,称为局部变量。形式参数也属于局部变量。
2> 作用域:局部变量只在定义它的函数内部有效,只有在定义它的函数内部使用,其它函数不能使用它。
2.全局变量
1> 定义:在所有函数外部定义的变量,称为全局变量。
2> 作用域:全局变量的作用范围是从定义变量的位置开始到源程序结束,即全局变量可以被在其定义位置之后的其它函数所共享。
* 变量的存储类型就是指变量存储在什么地方。有3个地方可以用于存储变量:运营时堆栈、普通内存、硬件寄存器。变量的存储类型决定了变量何时创建、何时销毁以及它的值能保持多久,也就是决定了变量的生命周期。
C语言根据变量的存储类型的不同,可以把变量分为:自动变量、静态变量、寄存器变量。
1.自动变量
1> 定义:自动变量是存储在堆栈中的。
2> 哪些是自动变量:被关键字auto修饰的局部变量都是自动变量,但是很少使用这个关键字,基本上是废的,不用的省略的,由于所有的局部变量在默认情况下都是自动变量。
3> 生命周期:在程序执行到声明自动变量的代码块(函数)时,自动变量才被创建;当自动变量所在的代码块(函数)执行完毕后,这些自动变量就会自行销毁。假如一个函数被反复调用,这些自动变量每次都会重新创建。
变量a、b,变量c、变量d都是自动变量。
2.静态变量
1> 定义:静态变量是存储在静态内存中的,也就是不属于堆栈。
2> 哪些是静态变量:
· 所有的全局变量都是静态变量
· 被关键字static修饰的局部变量也是静态变量
3> 生命周期:静态变量在程序运营之前创建,在程序的整个运营期间始终存在,直到程序结束。
变量a、变量b都是静态变量
变量b是静态变量,所以它只会被创建一次,并且生命周期会延续到程序结束。由于它只会创建一次,所以第6行代码只会执行一次,下次再调用test函数时,变量b的值不会被重新初始化为0。
注意:虽然第6行的变量b是静态变量,但是只改变了它的存储类型(即生命周期),并没有改变它的作用域,变量b还是只能在test函数内部使用。
3.寄存器变量
1> 定义:存储在硬件寄存器中的变量,称为寄存器变量。寄存器变量比存储在内存中的变量访问效率更高(默认情况下,自动变量和静态变量都是放在内存中的)
2> 哪些变量是寄存器变量:
· 被关键字register修饰的自动变量都是寄存器变量
· 只有自动变量才可以是寄存器变量,全局变量和静态局部变量不行
· 寄存器变量只限于int、char和指针类型变量使用
3> 生命周期:由于寄存器变量自身就是自动变量,所以函数中的寄存器变量在调用该函数时占用寄存器中存放的值,当函数结束时释放寄存器,变量消失。
4> 使用注意:
· 由于计算机中寄存器数目有限,不能使用太多的寄存器变量。假如寄存器使用饱和时,程序将寄存器变量自动转换为自动变量解决
· 为了提高运算速度,一般会将一些频繁使用的自动变量定义为寄存器变量,这样程序尽也许地为它分派寄存器存放,而不用内存
变量a是个寄存器变量。
14、 static和extern关键字对函数作用
static和extern不仅可以用在变量上,还可以用在函数上
· 外部函数:假如在当前文献中定义的函数允许其他文献访问、调用,就称为外部函数。C语言规定,不允许有同名的外部函数。
· 内部函数:假如在当前文献中定义的函数不允许其他文献访问、调用,只能在内部使用,就称为内部函数。C语言规定不同的源文献可以有同名的内部函数,并且互不干扰。
外部函数extern
接下来就演示在一个源文献中调用此外一个源文献定义的函数,比如在main.c中调用one.c中定义的one函数。
这个extern跟auto关键字同样废,完全可以省略,由于默认情况下,所有的函数就是外部函数
extern关键字对函数的作用:用来定义和声明一个外部函数。其实extern又跟auto同样废,完全可以省略
为了模块化地开发,在正规的项目里面,我们会把one函数的声明写到另一个头文献中,当然,这个头文献的命名最佳故意义、规范一点,比如叫one.h。以后,谁想调用这个one函数,包含one.h这个头文献就行了。于是最后的代码结构是这样的:
内部函数static
有时候,我们也许想定义一个"内部函数",也就是不想让其他文献访问本文献中定义的函数。这个非常简朴,你只需要在定义函数的时候加个static关键字即可。
发现程序运营不起来了,在链接的时候就报错了。报错的因素很简朴:我们在main.c中调用了one.c中定义的one函数,但是现在one.c的one函数是个"内部函数",不允许其他文献访问
第1个红框中的Undefined symbols...意思是one这个标记符没有被定义,也就是找不到one;第2个红框的linker表白是链接器报错了。但这个程序是可以编译成功的,由于我们在main函数前面声明了one函数(函数的声明和定义是两码事)
所谓编译,就是单独检查每个源文献的语法是否合理,并不会检查每个源文献之间的关联关系,一个源文献编译成功就生成一个目的文献。
所谓链接,就是检查目的文献的关联关系,将相关联的目的文献组合在一起,生成可执行文献。
static、extern与函数的总结
1.static
在定义函数时,在函数的最左边加上static可以把该函数声明为内部函数(又叫静态函数),这样该函数就只能在其定义所在的文献中使用。假如在不同的文献中有同名的内部函数,则互不干扰。
static也可以用来声明一个内部函数
2.extern
* 在定义函数时,假如在函数的最左边加上关键字extern,则表达此函数是外部函数,可供其他文献调用。C语言规定,假如在定义函数时省略extern,则隐含为外部函数。
* 在一个文献中要调用其他文献中的外部函数,则需要在当前文献中用extern声明该外部函数,然后就可以使用,这里的extern也可以省略。
15、 static和extern关键字对变量作用
static用来定义一个内部函数,不允许其他文献访问;extern用来定义和声明一个外部函数,允许其他文献访问。static和extern对变量也有作用,但是跟函数有点差别。
1.多个源文献共享的全局变量 extern:
在C语言中,全局变量定义的位置是有限制的
默认情况下,一个函数不可以访问在它后面定义的全局变量
在第4行定义的main函数中尝试访问第9行定义的变量a,编译器直接报错了。
解决这个错误的话,有2种办法:
第1种办法:将变量a定义在main函数的前面
这样做编译器就不会找你麻烦了。
第2种办法:在main函数前面对变量a进行提前声明
也就是让main函数知道变量a的存在就行了,至于变量a定义在哪个位置,main函数不用管。
* 完整的变量声明需要用extern关键字
第3行是对变量a进行声明,第10行是定义变量a,再次强调,声明和定义是两码事。
注意:你不能省略第10行的定义,只留下第3行的声明,由于extern是用来声明一个已经定义过的变量。
* 其实,你也可以直接在main函数前面再定义一次a
看到这一幕,你也许很惊讶,但编译器是不会报错的。在这种情况下,第3行和第10行的变量a代表着同一个变量。
以此类推,假如我们写了无数遍全局变量int a;,它们代表的都是同一个变量。
第2、第10行代表着同一个全局变量,而第5、第6行则是一个局部变量,跟外面的那个全局变量没有半毛钱的关系
你在一个源文献中无论写多少遍全局变量int a;,它们代表的都是同一个变量。尚有一个事实,假如在另一个源文献中也有全局变量int a;,那么这两个源文献的所有全局变量int a;都代表着同一个变量。
extern是用来声明一个已经定义过的变量
和
或者是:
和
上面的两种情况下,test.c和main.c中使用的全局变量a都还是代表着同一个变量
注意了,不可以两个文献的所有所有变量a都用extern,下面的做法是错误的:
和
由于extern是用来声明一个已经定义过的变量,这两个文献都是在声明变量,没有人定义变量,在链接的时候肯定报错:
大体错误意思是:标示符a未定义
2.单个源文献私有的全局变量 static:
输出结果:
当static用来修饰全局变量时变为该源文献的私有全局变量
当static用来修饰局部变量时变为静态变量
3.static和extern的总结
1.extern可以用来声明一个全局变量,但是不能用来定义变量
2.默认情况下,一个全局变量是可以供多个源文献共享的,也就说,多个源文献中同名的全局变量都代表着同一个变量
3.假如在定义全局变量的时候加上static关键字,此时static的作用在于限制该全局变量的作用域,只能在定义该全局变量的文献中才干使用,跟其他源文献中的同名变量互不干扰
16、 结构体
struct是关键字,是结构体类型的标志。
结构体的定义:
结构体变量的定义:
1. 先定义结构体类型,再定义变量
2. 定义结构体类型的同时定义变量
3.直接定义结构体类型变量,省略类型名
结构体的注意点:
1.不允许对结构体自身递归定义
如下做法是错误的
2.结构体内可以包含别的结构体
3.定义结构体类型,只是说明了该类型的组成情况,并没有给它分派存储空间,就像系统不为int类型自身分派空间同样。只有当定义属于结构体类型的变量时,系统才会分派存储空间给该变量
4.结构体变量占用的内存空间是其成员所占内存之和,并且各成员在内存中按定义的顺序依次排列
结构体的初始化
将各成员的初值,按顺序地放在一对大括号{}中,并用逗号分隔,一一相应赋值。
只能在定义变量的同时进行初始化赋值,初始化赋值和变量的定义不能分开,下面的做法是错误的:
结构体的使用
一般对结构体变量的操作是以成员为单位进行的,引用的一般形式为:结构体变量名.成员名
假如某个成员也是结构体变量,可以连续使用成员运算符"."访问最低一级成员:
相同类型的结构体变量之间可以进行整体赋值:
结构体数组
跟结构体变量同样,结构体数组也有3种定义方式
初始化
也可以用数组下标访问每一个结构体元素,跟普通数组的用法是同样的
结构体作为函数参数
将结构体变
展开阅读全文