文章目录
1. 基本数据类型
-
变量允许的格式:可用
大小写字母
、数字
和下划线
,但首字符不能为数字
; -
数据类型关键字:
-
基本数据类型概括:
1.1 位/比特(bit)、字节(byte)、字(word)
位、字节和字都是描述计算机数据单位或内存存储单位的术语;
- 位/比特(bit):计算机的基本存储单位,
- 字节(byte):计算机最常用的存储单位,1字节=8位,而1字节,即8位数据就有256(2的8次方)种0、1组合,通过二进制编码,便可表示为0~255个整数或一组字符;
- 字(word):计算机的自然存储单位,1字等于多少位取决于该微机的字长单位,如8位微机,则1字=8位,16位微机,则1字=16位,32位微机,则1字=32位;
如在32位微机中,1字=32位=4字节,数据类型的取值范围为:
1.1.1 用sizeof
查看数据类型、给定数据所占字节数
sizeof
的参数可以是数组、指针、类型、对象、函数等
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char **argv) //主程序
{
//查看C语言内置类型的所占字节数
printf("size of int is: %d\n",sizeof(int )); //整形数据
//查看字符及字符串数组所占字节数
char a[] ={'a','b','c','d','e'};
printf("size of a[] is: %d\n",sizeof(a)); //输出数组a[]所占字节数
char c[][3]={{'a','b','c'},{'d','e','f'},{'g','h','i'},{'j','k','l'}}; //初始化二维字符型数组
printf("size of c[][] is: %d\n",sizeof(c)); //二维数组c所占字节数
printf("size of c[0] is: %d\n",sizeof(c[0])); //二维数组中的某行所占的字节数,例如第0行
printf("size of c[0][0] is: %d\n",sizeof(c[0][0])); //某行某个元素,例如第0行第0个元素
//对指针所占字节数的测量
char *p=0;
printf("size of *p is: %d\n",sizeof(p)); //字符型指针
return 0;
}
1.2 整数
即没有小数部分的数,计算机以二进制编码储存整数,图示如下:
1.2.1 有符号整型_int
存储一个int
类型要占用1个机器字长(机器字长位数取决于微机的字长单位),其中取字长的最高位为符号位,0
为正,1
为负,其他位为数字位;
- 如:16位微机,机器字长为16位,则第15位为符号位,0~14位为数字位,在这种情况下,一个被
int
定义的变量的取值范围为-32768
~32767
,即1000 0000 0000 0000
~0111 1111 1111 1111
,即-(2的15次方)
~(2的15次方-1)
;
- C语言规定
int
类型不得小于16位(2字节)
int variable; //声明int变量,此时计算机为该变量创建内存空间,但不赋值;
int cows = 32, goats = 1 //声明int变量并赋值,此时计算机为该变量创建内存空间,并且赋值;
// 内存操作如下图所示:
1.2.2 无符号整型_unsigned
与有符号整型int
相比,无符号整型unsigned
字长的最高位为有效数字位,它只用于表示非负值的场合;
如:16位微机,机器字长为16位,则其0~15位全部用于表示数字,一个被
unsigned
定义的数据的取值范围为0
~65535
,即0000 0000 0000 0000
~1111 1111 1111 1111
,即0
~(2的16次方-1)
;
1.2.3 字符型_char
-
字符型本质为整数类型:在底层代码,字符型属于整型,存储一个
char
类型数据要占用1字节内存空间; -
字符编码:在计算机中使用数字编码来映射字符,即用特定的整数表示特定的字符,最常用的编码方案为🔗ASCII编码,而标准ASCII码的取值范围为0~127,即只需7位二进制数(2的7次方=128)即可表示,使用
char
型即可满足; -
转义字符(非打印字符):对于 ASCII 编码,在0~31(十进制)范围内的字符为控制字符,它们都不能在显示器上显示,属于非打印字符,甚至无法从键盘输入,只能用转义的形式表达其功能(转义:如用
\n
表示ASCII码值010),因直接使用 ASCII 码记忆不方便,也不容易理解,所以,针对常用的控制字符,C语言定义了如下转义字符:
char grade = 'A'; // 定义一个字符,实际上在ASCII编码中 'A'=65
char grade = 65; // 底层原理与上面代码相同,但增加阅读难度
1.2.4 short
、long
、long long
- 有符号短整型_
short
:占用的内存可能比int
型少,但C语言规定short
类型至少占16位(2字节); - 有符号长整型_
long
:占用的内存可能比int
型多,C语言规定long
类型至少占32位(4字节); - 有符号双长整型_
long long
:占用的内存可能比long
型多,C语言规定long long
类型至少占64位(8字节); - 无符号
short
、long
、long long
型同理,把最高位作为有效数字位;
上述整型实际占用内存空间根据实际操作系统而定;
1.2.5 整型常量
- 整型常量:声明一个整型数据变量,然后把一个常量赋值给它,那么它就是一个整型常量;
- 整型常量的声明:
- 无特殊声明,除非常大的整型常量外,编译器默认整型常量为
int
类型; - 前缀
0
:表示用八进制表示整型常量; - 前缀
0x
或0X
:表示用十六进制表示整型常量;
- 无特殊声明,除非常大的整型常量外,编译器默认整型常量为
int some0 = 20; // 用十进制表示整型常量
int some1 = 020; // 用八进制表示整型常量,八进制20 = 十进制16
int some2 = 0x20; // 用十六进制表示整型常量,十六进制20 = 十进制32
- 注:不论使用何种方式表示常量,都不会影响数被储存的方式,他们都是以二进制形式被存储的,以上表示方式是为了方便阅读;
- 八进制和十六进制记数法的优点:八进制和十六进制记数法在表达与计算机相关的值时很方便,因为8和16都是2的幂,而10不是,另外,十六进制数的每一位的数恰好由4位二进制数表示;
例如:十六进制数0x53
=0101 0011
,这种对应关系使得十六进制和二进制间的转换非常方便;
1.3 浮点数
即带小数点和小数部分的数据类型;
如:
7.00
、7.
、.7
;
- 浮点数的存储方式:在计算机中,浮点数的表示类似于科学记数法,它把浮点数分成
小数部分
和指数部分
来表示,且分开储存,所以,即使浮点数7.00
和整数7
在数值上相同,但它们在计算机内的储存方式不同;
- 如:在十进制下,可以把
7.0
写成0.7E1
,其中0.7
是小数部分,1
是指数部分;- 浮点数在内存中的存储方式:如下图:
- 浮点数的精度损失:浮点数可以表示的范围比整数大得多,但对于一些算术运算(如,两个很大的数相减),浮点数损失的精度也更多,因为在任何区间内(如在1.0 到 2.0 之间)都存在无穷多个实数,计算机的浮点数不能完全表达区间内所有的值,计算机中的浮点数只是实际值的近似值,如
7.0
在计算机中可能被储存的真实值为6.99999
;
1.3.1 浮点型_float
、双精度浮点型_double
、长双精度浮点型_long double
、双长双精度浮点型_long long double
- float:C标准规定,
float
类型至少能精确表示6位有效数字(如333.333
);- 一般计算机储存一个
float
类型变量要占用 32位。其中 8位用于表示指数的值及其符号,剩下24位用于表示小数的值及其符号;
- 一般计算机储存一个
- double:在C标准中,规定其必须至少能精确表示10位有效数字;
- 一般计算机存储一个
double
占用64位,有的系统将多出的 32 位全部用来表示非指数部分,这不仅增加了有效数字的位数(即提高了精度),而且还减少了舍入误差;另一些系统把其中的一些位分配给指数部分,以容纳更大的指数,从而增加了可表示数的范围;
- 一般计算机存储一个
- long double:是C标准中为了满足比
double
类型更高的精度要求而提供的; - long long double:储存浮点数的范围可能比
double
大,实际占用内存空间根据实际操作系统而定;
1.3.2 浮点型常量
- 浮点型常量:声明一个浮点型数据变量,然后把一个常量赋值给它,那么它就是一个浮点型常量;
float f1=3.0;
float f1=3.0f;
浮点型常量可表示为:
-.56E12
= -0.56 * 10的12次方;(允许无整数部分)2e-3
= 2.0 * 10的-3次方;(允许无小数点,但不建议)3.
= 3.0;(允许无小数部分).3
= 0.3;3.14
= 3.14;
- 浮点型常量的声明:
- 无特殊声明下,编译器默认浮点型常量为
double
类型,计算时使用双精度浮点型进行计算,计算结果再截断成float
类型,这样提高了计算精度,但由于计算量大,代码运算速度变慢; - 后缀
f
或F
:浮点型常量加后缀f
或F
,编译器会将其始终视为float
类型; - 后缀
l
或L
:浮点型常量加后缀l
或L
,编译器会将其始终视为long double
类型; - 前缀
0x
或0X
(C99标准):浮点型常量加前缀0x
或0X
,表示用十六进制表示浮点型常量,用p和P别代替e和E,用2的幂代替10的幂;
- 无特殊声明下,编译器默认浮点型常量为
float some0 = 4.0 * 2.0; // 编译器默认some常量为double类型
float some1 = 4.0F; // 编译器默认some常量为float类型
long double some2 = 4.0L; // 编译器默认some常量为long double类型
float some3 = 0xa.1fp10; // 十六进制a等于十进制10,.1f是1/16(即十进制1/2的4次方)加上15/256(十六进制f等于十进制15,即十进制15/2的5次方),p10是210或1024;
// 综上,0xa.1fp10表示的值是(10 + 1/16 +15/256)×1024(即,十进制10364.0);
1.4 布尔型__bool
又叫逻辑型,用于表示布尔值(逻辑值)true
和false
,在C语言中true
=1
、false
=0
,每个布尔类型的数据占用1位内存空间;
bool
定义在<stdbool.h> 头文件中;
1.5 字符数组(字符串_string
)
关于更多字符串的应用,见下面附录一节。
- 字符串(character string):是一个或多个字符数组,C中没有专门用于储存字符串的变量类型,它们都被储存在
char
类型的数组中,该字符数组以空字符(null character)\0
标记字符串的结束,其字符数组名为该字符数组在内存中的首地址;
我们无需手动对字符串添加空字符,该操作在字符串被定义时或函数
scanf()
在读取时已由计算机自动完成;
1.5.1 字符数组只能在定义时进行一次性赋值
- 字符数组只有在定义时能将整个字符串一次性赋值给它,一旦定义完,就只能一个字符一个字符地赋值;
char str[7];
str = "abc123"; //错误
//正确
str[0] = 'a'; str[1] = 'b'; str[2] = 'c';
str[3] = '1'; str[4] = '2'; str[5] = '3';
str[6] = '\0' // str[6] = 0 也可以,因为在ASCII中 \0 等于 0
// 实际中一般通过for循环对字符数组进行赋值
1.6 数组
- 数组(Array):一组具有相同数据类型的元素集合,,数组中的所有元素在内存中是连续排列的;
- 数组名:定义数组时,要给出数组名和数组长度,数组名是一个指针,它默认指向数组的第 1 个元素,即数组的首地址;
- 数组下标:数组使用下标来访问,下标从0开始
- 数组访问越界:数组的下标是有范围限制的。数组的下标规定从0开始,如果输入有n个元素,最后一个元素的下标就是n-1 ,所以数组的下标如果小于0,或大于n-1,即产生数组越界访问,即超出了数组合法空间的访问。C语言本身是不做数组下标的越界检查
- 计算整个数组大小:
sizeof(数组名)
- 定义数组:
char arr3[10];
float arr4[1];
double arr5[20];
- 定义数组并初始化:
int arr1[10] = { 1,2,3 };
int arr2[] = { 1,2,3,4 }; // 数组长度被默认定义为 4
int arr3[5] = { 1,2,3,4,5 };
char arr4[3] = { 'a',98, 'c' };
char arr5[] = { 'a','b','c' };
char arr6[] = "abcdef";
初始化数组时,若赋值的元素少于数组总体元素的时候,剩余的元素自动初始化为
0x00
;
- 以指针的方式遍历数组元素:
#include <stdio.h>
int main()
{
int arr[5] = { 99, 15, 100, 888, 252 };// 在内存中连续分配了4个int类型的内存空间
int len = sizeof(arr) / sizeof(int); //求数组长度
int i;
for(i=0; i<len; i++)
{
printf("%d ", *(arr+i) ); //*(arr+i)等价于arr[i]
}
printf("\n");
return 0;
}
1.6.1 一维数组在内存中的存储
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int i = 0;
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i)
{
printf("&arr[%d] = %p\n", i, &arr[i]); // &arr[i] 表示取当前访问的数组元素的地址
}
return 0;
}
运行结果:
上图可见数组在内存中是连续存放的,随着数组下标的增长,元素的地址也在有规律的递增。
1.6.2 数组指针和指针数组
- 数组指针:Array Pointer,一个指向数组的指针;
- 指针数组:如果一个数组中的所有元素保存的都是指针,那么称它为指针数组;
int a = 16, b = 932, c = 100;
//定义一个指针数组
int *arr[3] = {&a, &b, &c};
//定义一个指向指针数组的指针
int **parr = arr;
// 以下指针数组定义都是相同的
char *str[3] = {
"abcd",
"2333",
"7766"
};
char *str0 = "abcd";
char *str1 = "2333";
char *str2 = "7766";
char *str[3] = {str0, str1, str2};
注意:字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的,也只有当指针数组中每个元素的类型都是char *时,才能像上面那样给指针数组赋值;
1.6.3 二维数组
- 二维数组的数组元素是数组本身:即二维数组的数组元素仍然为数组,二维数组按行排列,在内存中先为第一行元素分配内存,紧接着再为第二行元素分配,如此类推;
// 定义了一个3行4列的二维数组,并初始化(下面两种初始化方式效果相同)
int a[3][4] = { {80,75,92,55}, {61,65,71,56}, {59,63,70,90} };
int a[3][4] = { 80,75,92,55, 61,65,71,56, 59,63,70,90};
// 该二维数组的全部元素如下
a[0][0], a[0][1], a[0][2], a[0][3]
a[1][0], a[1][1], a[1][2], a[1][3]
a[2][0], a[2][1], a[2][2], a[2][3]
- 遍历二维数组:
#include <stdio.h>
int main(){
int arr[3][4] = {0};
int i = 0;
for(i=0; i<3; i++)
{
int j = 0;
for(j=0; j<4; j++)
{
printf("%d ", arr[i][j]);
}
}
return 0;
}
- 二维数组在内存中的存储:
#include <stdio.h>
int main(){
int arr[3][4];
int i = 0;
for(i=0; i<3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]);
}
}
return 0;
}
上图可见二维数组在内存中也是连续存储的。
1.6.4 查询数组内的元素
- 对无序数组的查询:循环遍历数组中的每个元素;
#include <stdio.h>
int main(){
int nums[10] = {1, 10, 6, 296, 177, 23, 0, 100, 34, 999};
int i, num, thisindex = -1;
printf("Input an integer: ");
scanf("%d", &num);
for(i=0; i<10; i++){
if(nums[i] == num){
thisindex = i;
break;
}
}
if(thisindex < 0){
printf("%d isn't in the array.\n", num);
}else{
printf("%d is in the array, it's index is %d.\n", num, thisindex);
}
return 0;
}
- 对有序数组的查询:查询有序数组只需要遍历其中一部分元素。
如:有一个长度为 10 的整型数组,它所包含的元素按照从小到大的顺序(升序)排列,假设比较到第 4 个元素时发现它的值大于输入的数字,那么剩下的 5 个元素就没必要再比较了,肯定也大于输入的数字,这样就减少了循环的次数,提高了执行效率;
#include <stdio.h>
int main(){
int nums[10] = {0, 1, 6, 10, 23, 34, 100, 177, 296, 999};
int i, num, thisindex = -1;
printf("Input an integer: ");
scanf("%d", &num);
for(i=0; i<10; i++){
if(nums[i] == num){
thisindex = i;
break;
}else if(nums[i] > num){
break;
}
}
if(thisindex < 0){
printf("%d isn't in the array.\n", num);
}else{
printf("%d is in the array, it's index is %d.\n", num, thisindex);
}
return 0;
}
1.7 指针
- 指针(Pointer):指针被定义后,指针本身就占用一部分内存
- 指针变量:指针指向的用于存储数据的内存地址
以上两者概念不要混淆;
int *p = NULL; // 假设指针 p的内存地址是 0x0001
int a=0; // 假设整型变量 a的内存地址是 0x0002
p = &a; // 取存储整型变量a的内存地址赋值给p,那么此时指针 p的值(内存变量)就等于 0x0002,但它本身的内存地址仍然是定义时的 0x0001
指针的大小:指针的长度跟CPU的位数相等。
如:CPU是32位,那么指针的长度就是32bit,也就是4个字节。CPU是64位,那么指针的长度就是64bit,也就是8个字节。
-
几何解析:假设有一个
char
类型的变量c
,它存储了字符'K'
(ASCII码为十进制数 75),并占用了地址为0X11A
的内存空间,另外有一个指针变量p
,它的值为0X11A
,正好等于变量 c 的地址,这种情况就叫指针p
指向了变量c
,或者说p
是指向变量c
的指针;- 但注意,这其中只有
0X11A
是指针变量(内存地址),p
和c
是数据变量,数据变量里的是数据;
- 但注意,这其中只有
-
所有的数据都必须放在内存中方可处理:不同类型的数据占用的字节数不一样,如在32位操作系统中
int
占用 4 个字节,char
占用 1 个字节,为了正确地访问数据,必须为内存中每个字节都编上号码,像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到指定字节;
//以下代码参考自c语言中文网
以十六进制形式输出变量的内存地址
#include <stdio.h>
int main(){
int a = 100;
char str[20] = "c.biancheng.net";
printf("%#X, %#X\n", &a, str);
return 0;
}
1. 输出结果是变量a和字符数组的首地址,如0X28FF3C, 0X28FF10
2. %#X 表示以十六进制形式输出,并附带前缀0X;
3. a 是一个变量,用来存放整数,需要在前面加取地址运算符&来获得它的地址;
4. str 本身就表示字符串的首地址,不需要加&;
- 变量名和函数名只是地址的一种助记符:CPU 访问内存时需要的是地址,而不是变量名和函数名,当源文件被编译和链接成可执行程序后,变量名和函数名都会被替换成地址,编译和链接过程的一项重要任务就是找到这些名称所对应的地址;
假设变量 a、b、c 在内存中的地址分别是
0X1000
、0X2000
、0X3000
那么加法运算c = a + b
,将会被转换成类似下面的形式:
0X3000 = (0X1000) + (0X2000);
其中()
表示取值操作,整个表达式的意思是,取出地址0X1000
和0X2000
上的值,将它们相加,把相加的结果赋值给地址为0X3000
的内存;
1.7.1 指针的定义和使用
- 定义指针:
int a = 100;
int b = 3;
double c = 3.0;
int *p = &a; // 定义一个指针,同时指向变量a 的地址
double *pd = &c;
int *p2;
p2 = &a; // 这种方法也行
//修改指针变量的值
p = b; // 赋值时不用带`*`
注意:在指针定义中`*` 不是间接运算符,是定义指针的一种格式,表明该变量为指针变量;
-
间接运算符_
*
:上述程序中指针变量通过间接运算符*
取得指针指向的内存地址处的数据(也即是变量a
的值),如下图所示,相对于直接从a
中取值,这种通过指针取值的方式是间接的,故称之为间接运算符;
-
读取指针指向地址处的数据:
#include <stdio.h>
int main(){
int a = 15;
int *p = &a;
printf("%d, %d\n", a, *p); // `*`为间接运算符,用于取出指针指向地址处的数据,与取值运算符`&`作用相反
return 0;
}
- 修改指针指向地址处的数据:
#include <stdio.h>
int main(){
int a = 15, b = 99, c = 222;
int *p = &a; //定义指针变量
*p = b; //通过指针变量修改内存上的数据
c = *p; //通过指针变量获取内存上的数据
printf("%d, %d, %d, %d\n", a, b, c, *p);
return 0;
}
- 指针运算:指针变量保存的是地址,地址本质上是一个整数,所以指针变量可以进行部分运算,如加法、减法、比较等;
#include <stdio.h>
int main(){
int a = 10, y=10, *pa = &a, *paa = &a, *py = &y;
double b = 99.9, *pb = &b;
char c = '@', *pc = &c;
//最初的值
printf("&a=%#X, &b=%#X, &c=%#X\n", &a, &b, &c);
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//加法运算
// 让该指针移动至数组的下一个元素,移动单位根据指针的数据类型决定,如32为系统中,`int`定义的指针变量,每次自增运算后,移动4个字节
pa++; pb++; pc++;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
// 下面是数值自增,而不是地址自增
y = ++*px; //px的内容加上1之后赋给y,++*px相当于++(*px)
//减法运算
pa -= 2; pb -= 2; pc -= 2;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//比较运算
if(pa == paa){
printf("%d\n", *paa);
}else{
printf("%d\n", *pa);
}
return 0;
}
pa++
的内存显示:
1.7.2 指针变量作为函数参数
- 用指针变量作函数参数:可将函数外部的地址传递到函数内部,使得在函数内部可操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁;
#include <stdio.h>
int max(int *intArr, int len){
int i, maxValue = intArr[0]; //假设第0个元素是最大值
for(i=1; i<len; i++){
if(maxValue < intArr[i]){
maxValue = intArr[i];
}
}
return maxValue;
}
int main(){
int nums[6], i;
int len = sizeof(nums)/sizeof(int);
//读取用户输入的数据并赋值给数组元素
for(i=0; i<len; i++){
scanf("%d", nums+i);
}
printf("Max value is %d!\n", max(nums, len));
return 0;
}
1.7.3 指针作为函数返回值
- 指针函数:C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数;
注:函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针尽量不要指向这些数据;
#include <stdio.h>
#include <string.h>
char *strlong(char *str1, char *str2){
if(strlen(str1) >= strlen(str2)){
return str1;
}else{
return str2;
}
}
int main(){
char str1[30], str2[30], *str;
gets(str1);
gets(str2);
str = strlong(str1, str2);
printf("Longer string: %s\n", str);
return 0;
}
1.7.4 多级指针
- 二级指针:若一个指针指向的是另外一个指针,就称它为二级指针,即指向指针的指针;
- 多级指针:同理,有三级、四级、…n级指针;
#include <stdio.h>
int main(){
int a =100;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;
printf("%d, %d, %d, %d\n", a, *p1, **p2, ***p3);
printf("&p2 = %#X, p3 = %#X\n", &p2, p3);
printf("&p1 = %#X, p2 = %#X, *p3 = %#X\n", &p1, p2, *p3);
printf(" &a = %#X, p1 = %#X, *p2 = %#X, **p3 = %#X\n", &a, p1, *p2, **p3);
return 0;
}
分析:
1. ***p3等价于*(*(*p3));
2. *p3 得到的是 p2 的值,也即 p1 的地址;
3. *(*p3) 得到的是 p1 的值,也即 a 的地址;
4. 经过三次“取值”操作后,*(*(*p3)) 得到的才是 a 的值
1.7.5 关键字const
与指针
int main()
{
int a = 1;
int const *p1 = &a; //const后面是*p1,实质是数据a,则修饰*p1,通过p1不能修改a的值
const int*p2 = &a; //效果同上
int* const p3 = NULL; //const后面是数据p3。也就是指针p3本身是const .
const int* const p4 = &a; //通过p4不能改变a 的值,同时p4本身也是 const
int const* const p5 = &a; //效果同上
return 0;
}
typedef int* pint_t; //将 int* 类型 包装为 pint_t,则pint_t 现在是一个完整的原子类型
int main()
{
int a = 1;
const pint_t p1 = &a; //同样,const跳过类型pint_t,修饰p1,指针p1本身是const
pint_t const p2 = &a; //const 直接修饰p,同上
return 0;
}
1.8 结构体_struct
与int
、float
等一样,结构体也是数据类型的一种,也可以用它来定义变量。他与数组原理相近,不同和的是,在同一个结构体内可存放不同类型(甚至结构体本身)的元素;
- 结构体就像一个模板:用该结构体定义的结构体变量相当于拷贝了这个模板然后进行填充(这一点有点像面向对象的类)。所以一定要区分好结构体和由结构体定义的结构体变量。
- 编译器不会为结构体模板分配内存空间:结构体是一种数据类型,是一种创建变量的模板,就像
int
、float
、char
这些关键字本身不占用内存一样,而由它们定义的变量才包含实实在在的数据,才需要内存来存储。
1.8.1 定义结构体和结构体变量
- 定义结构体,相当于定义一个模板
struct stru{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
};
// 其中:
// struct为结构体标签,即结构体的名字
// name、num是各种数据类型的结构体元素
- 定义结构体变量:结构体定义好之后,就相当于模板定义好,用该结构体定义的结构体变量就相当于以该模板复制一个副本,然后往模板填入相应内容。结构体变量的定义有以下两种形式。
// 定义结构体的同时定义结构体变量
struct stru{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2; // 将结构体变量放在结构体定义的最后即可
// 像int定义变量那样定义结构体变量的形式如下:
struct stru stu1;
1.8.2 结构体成员的访问
使用点号.
获取单个成员,
- 给结构体成员赋值:
stu1.name = "Tom";
stu1.num = 12;
stu1.age = 18;
stu1.group = 'A';
stu1.score = 136.5;
- 读取结构体成员的值:
printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", stu1.name, stu1.num, stu1.age, stu1.group, stu1.score);
- 也可以在结构体定义的时候进行整体初始化赋值:
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1, stu2 = { "Tom", 12, 18, 'A', 136.5 };
1.8.3 结构体数组
即数组中的每个元素都是结构体,在C语言的实际应用中,结构体数组常被用来表示一个拥有相同数据结构的群体,比如一个班的学生、一个车间的职工等;
- 定义结构体数组:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
}class[5];
- 定义结构体数组的同时初始化:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
}class[] = {
{"Li ping", 5, 18, 'C', 145.0},
{"Zhang ping", 4, 19, 'A', 130.5},
{"He fang", 1, 18, 'A', 148.5},
{"Cheng ling", 2, 17, 'F', 139.0},
{"Wang ming", 3, 17, 'B', 144.5}
};
- 结构体数组成员的获取和赋值:
class[0].group = 'B';
1.8.4 结构体指针
当一个指针变量指向结构体时,就称其为结构体指针,语法为:struct 结构体名 *结构体指针名;
. 表示该结构体指针只能指向由这个结构体定义的结构体变量。
- 结构体变量名和数组名不同:数组名是数组中第一个元素的首地址,而结构体变量名不是。无论在任何表达式中结构体名表示的都是整个结构体本身,要取得结构体变量的地址,必须在前面加取地址符
&
。见如下例程:
// 定义结构体和结构体变量,同时初始化结构体变量
struct structName{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 };
- 定义一个指向结构体 stu 的结构体指针 structPointer
struct structName *structPointer = &stu1;
-
获取结构体指针指向的结构体成员:
(*pointer).structMemberName
或更常用的pointer->structMemberName
; -
结构体指针作为函数参数:已知结构体变量名代表整个结构体本身,若把结构体作为函数参数,传递给函数的实参是结构体的所有成员,而不是像数组一样被编译器转换成一个指针。若结构体的成员数量较多,特别是结构体成员含数组时,传递的时间会很长,内存空间开销会很大,影响程序运行效率,所以最好使用结构体指针,这时由实参传向形参的只是一个地址,效率更高。
struct stu{ // 定义结构体和结构体变量,同时初始化结构体变量
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
}stus[] = {
{"Li ping", 5, 18, 'C', 145.0},
{"Zhang ping", 4, 19, 'A', 130.5},
{"He fang", 1, 18, 'A', 148.5},
{"Cheng ling", 2, 17, 'F', 139.0},
{"Wang ming", 3, 17, 'B', 144.5}
};
//
- 结构体指针作为函数参数
void average(struct stu *ps, int len){
int i, num_140 = 0;
float average, sum = 0;
for(i=0; i<len; i++){
sum += (ps + i) -> score;
if((ps + i)->score < 140) num_140++;
}
1.8.5 用typedef
给结构体改名
// 定义结构体的同时定义结构体变量
typedef struct
{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} structName;
// 此后就可以使用structName来直接定义结构体变量
// 用`typedef`改名前,结构体变量的定义方式:
struct stru stu1; // 定义结构体变量
struct stur *structPointer = &stu1; // 定义结构体指针
// 用`typedef`改名后,结构体变量的定义方式:
structName stru1; // 定义结构体变量
structName *structPointer = &stu1; // 定义结构体指针
// 即直接用名字structName来替代struct stru
1.8.6 结构体、联合体的位域
在结构体、联合体中有些数据在存储时并不需要占用一个完整的数据类型宽度,只需要占用一个或几个二进制位即可。因此C语言提供了一种叫位域的数据结构。
不同的编译器对位域有不同的存储方式,但其根本的目的都是尽量地压缩数据存储空间。
若变量的值超过了其位域限定的范围,则发生数据溢出,多出的位被截去(详见1.8.6.1节)。
C语言标准还规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int(int 默认就是 signed int) 和 unsigned int;到了 C99,_Bool 也被支持了。但编译器在具体实现时都进行了扩展,额外支持了 char、signed char、unsigned char 以及 enum 类型,所以下面的代码虽然不符合C语言标准,但它依然能够被编译器支持。
- 在结构体总使用位域:
struct bs{
unsigned m;
unsigned n: 4; // 符号`:`后面的数字用来限定成员变量占用的位数,该语句表示无符号整型变量n只能占用4位内存
unsigned char ch: 6;
};
1.8.6.1 位域溢出现象例程
#include <stdio.h>
int main()
{
struct bs
{
unsigned m;
unsigned n: 4;
unsigned char ch: 6;
} a = { 0xad, 0xE, '$'};
//第一次输出
printf("%#x, %#x, %c\n", a.m, a.n, a.ch);
//更改值后再次输出
a.m = 0xb8901c;
a.n = 0x2d;
a.ch = 'z';
printf("%#x, %#x, %c\n", a.m, a.n, a.ch);
system("pause");
return 0;
}
可见,对于 n 和 ch,第一次输出的数据是完整的,第二次输出的数据是残缺的。
- 第一次输出时,n、ch 的值分别是 0xE、0x24(‘$’ 对应的 ASCII 码为 0x24),换算成二进制是1110、10 0100,都没有超出限定的位数,故能正常输出。
- 第二次输出时,n、ch 的值变为 0x2d、0x7a(‘z’ 对应的 ASCII 码为 0x7a),换算成二进制分别是 10 1101、111 1010,都超出了限定的位数。超出部分被直接截去,剩下 1101、11 1010,换算成十六进制为 0xd、0x3a(0x3a 对应的字符是 :)。
1.9 联合体_union
- 联合体:又叫共用体,语法:
union 联合体名{成员列表};
,联合体也是一种数据类型,可通过它定义联合体变量; - 结构体和联合体的区别:结构体的各个成员会占用不同的内存,互相之间没有影响,而联合体的所有成员占用同一段内存,修改一个成员会影响其余所有成员;
- 联合体内存管理:联合体使用内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉;
- 联合体变量的大小:由联合体定义的联合体变量的大小取决于联合体成员中最大的那个;
- 定义联合体
union data{
int n;
char ch;
double f;
};
- 声明联合体变量
union data a, b, c;
- 定义联合体同时声明联合体变量
union data{
int n;
char ch;
double f;
} a, b, c;
- 以上联合体 `data` 中,成员 `f` 占用的内存最多,为 8 个字节,所以 `data` 类型的变量也占用 8 个字节的内存;
- 联合体成员的获取与赋值
a.n = 0x40;
- 联合体的赋值过程:以绝大多数 PC 机上的内存存储模式为例,成员
n
、ch
、m
在内存中“对齐”到一头,对ch
赋值修改的是前一个字节,对m
赋值修改的是前两个字节,对n
赋值修改的是全部字节,
1.9.1 参考代码
// 1. 先定义一个联合体
union union0
{
u16 value16;// 一个16 位的变量
struct
{
char bit0:1;
char bit1:1;
char bit2:1;
char bit3:1;
char bit4:1;
char bit5:1;
char bit6:1;
char bit7:1;
u8 value8;
}struct0; // 该结构体占用16 位内存空间,与前面的`u16 value16` 内存重叠
};
// 2. 声明联合体变量a0
union union0 a0;
// 3. 改变联合体内结构体变量的值
a0.struct0.bit0 = 0;
// 4. 打印`a0.value16`的值,得到其值为0x00
// 5. 改变联合体内结构体变量的值
a0.struct0.bit0 = 1;
// 6. 打印`a0.value16`的值,得到其值为0x01
// 8. 改变联合体内结构体变量的值
a0.struct0.bit7 = 1;
// 9. 打印`a0.value16`的值,得到其值为0x81
// 10. 改变联合体内结构体变量的值
a0.struct0.value8 = 0xF8;
// 11. 打印`a0.value16`的值,得到其值为0xF881
1.10 枚举体_enum
- 枚举:
enum
与int
、float
等也是数据类型关键字,专门用来定义枚举类型,枚举体内的枚举值都为整数,语法为:enum typeName{ valueName1, valueName2, valueName3, ...... };
;
- 定义枚举体
enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun }; // 枚举值默认从 0 开始,往后逐个加 1(递增)
enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };
- 只给第一个枚举值指定值,其后的枚举值逐一递增,下面的写法和上面的两条语句写法同理
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
- 枚举值是常量:枚举值都是整数常量,只可读,不能写;
- 枚举和宏定义非常相似:宏定义
#define
在预处理阶段将目标替换成对应的值,枚举在编译阶段将目标(枚举值名)替换成对应的值,可将枚举理解为编译阶段的宏;- 枚举值不能用
&
取其地址:在程序编译过程中,枚举体都被替换成了对应的数字,本质上它们已经不是变量,不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是直接被编译到命令里面,放到代码区,所以不能用&
取得它们的地址;
- 枚举变量:枚举体是一种数据类型,那么就可以定通过它义枚举变量;
- 定义枚举体
enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };
- 定义枚举变量
enum week day1,day2,day3;
- 定义枚举体的同时定义枚举变量
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day1,day2,day3;
- 给枚举变量赋值
day1=Mon; // day1=1; 也可以
day2=Wed;
- 例程:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day;
scanf("%d", &day); // 用户输入1~7
switch(day){
case Mon: puts("Monday"); break;
case Tues: puts("Tuesday"); break;
case Wed: puts("Wednesday"); break;
case Thurs: puts("Thursday"); break;
case Fri: puts("Friday"); break;
case Sat: puts("Saturday"); break;
case Sun: puts("Sunday"); break;
default: puts("Error!");
}
1.11 位域
- 位域:在结构体定义时,可限定某个成员变量所占用的二进制位数(Bit);
C语言标准规定,位域的宽度不能超过它所依附的数据类型的长度,即结构体成员变量都是有类型的,这个类型限制了成员变量的最大长度,
:
后面的数字不能超过这个长度;
struct bs{
unsigned m;
unsigned n: 4; // 限定该成员变量所占内存位4位
unsigned char ch: 6;
};
- 位域溢出问题:给被位域限定的结构体成员变量赋值若超过其限定位数时,会发生数据溢出问题;
#include <stdio.h>
int main(){
struct bs{
unsigned m;
unsigned n: 4;
unsigned char ch: 6;
} a = { 0xad, 0xE, '$'};
//第一次输出
printf("%#x, %#x, %c\n", a.m, a.n, a.ch);
//更改值后再次输出
a.m = 0xb8901c;
a.n = 0x2d;
a.ch = 'z';
printf("%#x, %#x, %c\n", a.m, a.n, a.ch);
return 0;
}
- 第一次输出时,n、ch 的值分别是 0xE、0x24('$' 对应的 ASCII 码为 0x24),换算成二进制是 1110、10 0100,都没有超出限定的位数,能够正常输出;
- 第二次输出时,n、ch 的值变为 0x2d、0x7a('z' 对应的 ASCII 码为 0x7a),换算成二进制分别是 10 1101、111 1010,都超出了限定的位数。超出部分被直接截去,剩下 1101、11 1010,换算成十六进制为 0xd、0x3a(0x3a 对应的字符是 :);
1.12 数据溢出问题
如果超出了相应类型的取值范围,数据会从溢出值返回到该数据类型的起始点(重置点)两边的值之一;
如:在16位微机中
int
:32767
+1
--溢出–>-32768
,-32768
-1
–溢出->32767
unsigned
:65535
+1
--溢出–>0
,0
-1
–溢出->65535
1.13 数据类型转换问题
当参加同一个运算的变量的数据类型不同时,变量就会发生类型转换,类型转换的方法有两种,自动转换和强制转换;
-
自动转换:当不同类型的变量进行混合运算时,编译系统将按照一定的转换规则自动将它们转换成同一类型,再进行运算;
- 按数据长度增加的方向进行转换:以保证精度不降低 ;
如:
int
型数据和long
型数据进行运算时,系统会先将int
型数据转换成long
型,然后再进行运算,以保证运算结果精度不会降低;浮点数
->double
:所有的浮点数运算都是以双精度double
型进行运算的,因为CPU 在运算时有字节对齐的要求,这样运算的速度是最快的;char
、short
->int
:字符型char
和 短整型short
数据参与运算时,必须先转换成int
型,与上同理;int
->unsigned
:有符号整型int
和无符号整型unsigned
混合运算时,int
要转换成unsigned
;整型
->浮点型
:整型和浮点型混合运算时,整型先转换成浮点型,运算的结果是浮点型;- 赋值转换:在赋值过程中,当赋值号两边的数据类型不同时,右边的类型先转换为左边的类型,再赋给左边,所以,若右边数据类型的长度比左边长,则它将发生数据丢失,这样会降低数据精度,编译时会产生警告;
微软MSDN关于数据类型转换的翻译:
- 类型转换规则:由程序员通过代码强制对数据进行类型转换;
int cost = 12.99; // 变量cost 会被强制转换为 int类型,而小数部分将被截去,数据精度遭受损失
float pi = 3.1415926536; // 变量pi会被强制转换为 float类型,而后 6位有效数值将被截去,数据精度遭受损失
1.14 关于变量初始化
在敲代码的时候,我们会给变量一个初始值,以防止因为编译器编译等原因造成变量初始值的不确定性。
- 数值类变量:初始化为
0
int inum = 0;
float fnum = 0.00f;
double dnum = 0.00;
- 字符型变量:初始化为
\0
char ch = '\0';
- 字符串:字符串本质上是由一个个字符组成的字符数组,所以其初始化的最终目的,就是将字符数组里面的一个个字符都初始化为
\0
,下面介绍三种方法:
// 方法一:
char str[10] = "";
// 方法二:【推荐】
char str[10];
memset(str, 0, sizeof(str));
// 方法三:
char str[10];
for(int i = 0; i < 10; i++)
{
str[i] = '\0';
}
- 指针:一般来说,指针都是初始化为
NULL
,但指针如果初始化为NULL
后,没有给该指针重新分配内存,则会出现难以预料的错误(最常见的就是操作空指针引起的段错误)。在动态内存管理中,由于变量的内存是分配在堆中的,所以一般用malloc
、calloc
等函数申请过动态内存,在使用完后需要及时释放,一般释放掉动态内存后要及时将指针置空
char *p = NULL;
p=(char *)malloc(100);
if(NULL == p)
{
printf("Memory Allocated at: %x\n",p);
}
else
{
printf("Not Enough Memory!\n");
}
free(p);
p = NULL; //这一行给指针置空必不可少,否则很可能后面操作了这个野指针而不自知,从而导致出现严重的问题
- 结构体:采用
memset
进行初始化
typedef struct student
{
int id;
char name[20];
char sex;
}STU;
STU stu1;
memset((char *)&stu1, 0, sizeof(stu1));
2. 函数
- 函数定义:
返回值类型 函数名(输入参数类型1 输入参数1, 输入参数类型2 输入参数2,....){ 函数体 }
// 函数定义
int sum(int m, int n) // m、n为形参
{
int i, sum=0;
for(i=m; i<=n; i++)
{
sum+=i;
}
return sum;
}
result = sum(1,2); // 1、2为实参
- main函数:
main()
函数是主函数,它可以调用其它函数,而不允许被其它函数调用。因此,C程序的执行总是从 main() 函数开始,完成对其它函数的调用后再返回到main()
函数,最后由main()
函数结束整个程序;
2.1 形参与实参
- 形式参数(形参):函数定义时给定的参数称为形参;
- 形参的作用:在函数定义时,形参看作是一个占位符,没有实际数据,作用是在函数被调用时接收传递进来的数据,并规定输入数据的数据类型和数量;
- 形参的内存管理:形参只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用
- 实际参数(实参):函数调用时给定的参数(即实际传递的数据)称为实参;
- 实参的作用:在函数被调用时给定的实际数据,并传递给函数内部代码使用;
- 实参是确定值:实参可以是常量、变量、表达式、函数等,在进行函数调用时,实参必须是确定值;
- 形参与实参的关系:函数调用时,将实参的值传递给形参,相当于一次赋值操作,在函数调用中发生的数据传递是单向的,只能把实参传递给形参,换句话说,它们一旦完成数据传递的工作,实参和形参再无关系,故在函数调用过程中,形参的值发生改变并不会影响实参;
2.2 返回值与return
语句
- 函数返回值:函数的返回值是指函数被调用之后,执行函数体中的代码所得到的结果,这个结果通过
return
语句返回; return
语句:在函数体中,return
语句可以有多个,可以出现在函数体的任意位置,函数一旦运行完return
语句就立即输出返回值,后面的所有语句都不会被执行到了,函数到此结束,所以不论函数体中return
语句有多少个,只有一个被执行,所以函数只有一个返回值;
2.3 全局变量和局部变量
- 作用域(Scope):变量的作用范围;
- 决定变量作用域的是变量被定义的位置;
- 作用域不同的变量,分配的内存不同,即使变量名相同也不会发生冲突,但是同名的局部变量可掩盖同名的全局变量;
- 在同一个作用域中不能出现两个同名的变量,否则会发生命名冲突;
- 局部变量:定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于从变量定义处到函数结束, 离开该函数后就是无效的,再使用就会报错,局部变量在函数调用完毕后,其内存会呗自动释放;
- 全局变量:在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域是从变量定义处到整个程序结束,即本工程的所有代码文件;
- 静态全局变量:给全局变量加上
static
关键字,它的作用域就变成了当前文件,在其它文件中就无效了; - 静态局部变量:给局部变量加上
static
关键字,在函数调用完毕后,其内存不会被释放,其变量值保持原值,等待下次被调用;
int a, b, m; //全局变量
static float f; // 静态全局变量
void func1()
{
int n = 20; //局部变量
static m=20 //静态局部变量
m++;
printf("func1 n: %d\n", n);
}
void func2(int n)
{
printf("func2 n: %d\n", n);
}
ain()
{
int a,b; // 局部变量
//DoSomething
return 0;
}
2.4 函数声明
- 函数声明:C语言代码由上到下依次执行,原则上函数定义要出现在函数调用之前,否则就会报错 ,若要调用在其后定义的函数,要对所调函数进行声明;
- 声明(Declaration):即在编程时告诉编译器我现在要使用这个函数,你现在往上没有找到它的定义不要紧,请不要报错,稍后我会把定义补上;
- 有了函数声明,函数定义就可以出现在任何地方:甚至是其他文件、静态链接库、动态链接库等;
- 函数声明格式:
返回值类型 函数名( 形参数据类型1 形参名1, 形参数据类型2 形参名2 ... );
, 最后记得加分号
#include <stdio.h>
// 函数声明
long factorial(int n); //也可以写作 long factorial(int);
long sum(long n); //也可以写作 long sum(long);
int main(){
printf("1!+2!+...+9!+10! = %ld\n", sum(10));
return 0;
}
//函数定义
//求阶乘
long factorial(int n){
int i;
long result=1;
for(i=1; i<=n; i++){
result *= i;
}
return result;
}
// 求累加的和
long sum(long n){
int i;
long result = 0;
for(i=1; i<=n; i++){
result += factorial(i);
}
return result;
}
2.5 内部函数与外部函数
- 内部函数_
static
:又叫静态函数,若函数只能被本文件中的其他函数调用,称为内部函数,声明时在函数首加关键字static
,这样就可以在别的文件中声明同名的函数; - 外部函数_
extern
:若函数能被其他源文件调用,称为外部函数,声明时在函数首加关键字extren
;
2.6 代码块
- 代码块:由
{ }
包围起来的代码; - 代码块局部变量:C语言允许在代码块内部定义变量,这样的变量具有块级作用域,即在代码块内部定义的变量只能在本代码块内部使用;
int gcd(int a, int b)
{
//若a<b,那么交换两变量的值
if(a < b)
{
int temp1 = a; //块级变量
a = b;
b = temp1;
}
//求最大公约数
while(b!=0)
{
int temp2 = b; //块级变量
b = a % b;
a = temp2;
}
return a;
}
2.7 函数的嵌套调用
- 函数的嵌套调用:在一个函数的定义或调用过程中允许出现对另外一个函数的调用;
#include <stdio.h>
//求阶乘
long factorial(int n)
{
int i;
long result=1;
for(i=1; i<=n; i++){
result *= i;
}
return result;
}
// 求累加的和
long sum(long n)
{
int i;
long result = 0;
for(i=1; i<=n; i++){
result += factorial(i); // 在函数定义过程中进行函数嵌套调用
}
return result;
}
int main(){
printf("1!+2!+...+9!+10! = %ld\n", sum(10)); // 在函数调用过程中进行函数嵌套调用
return 0;
}
2.8 递归
- 递归函数:一个函数在其函数体内调用它自身称为递归调用,这种函数叫递归函数,执行递归函数将反复调用其自身,每调用一次就进入新的一层,当最内层的函数执行完毕后,再一层一层地由里到外退出;
- 递归的限制条件:每个递归函数都应该只进行有限次的递归调用,否则会进入死循环,永远不能退出,这样的程序是没有意义的;
- 三种递归形式:
- 尾递归:发生递归调用的位置在函数体的尾部;
- 中间递归:发生递归调用的位置在函数体的中间;
- 多层递归:在一个函数里面多次调用自己;
- 递归的缺陷:递归函数的时间开销和内存开销都非常大,极端情况下会导致程序崩溃;
//求n的阶乘
long factorial(int n)
{
if (n == 0 || n == 1)
{
return 1;
}
else
{
return factorial(n - 1) * n; // 递归调用,尾递归
}
}
- 递归的进入与退出过程:
以上述代码为例
2.9 main
函数
main
函数是程序的入口,其返回值为int
型,程序运行正常则向操作系统返回0
,否则返回非零值,一般为-1
;
3. 输入/输出函数
这两个函数能让用户可以与程序交流,称为输入输出函数,I/O函数;
3.1 格式转换说明符和转换说明修饰符
注:格式转换说明符(conversion specification)中只能用小写;
在计算机底层数据都以二进制0
和1
存储,对于同一数据段,使用不同的转换说明符可在printf()
函数输出不同的结果;
- 转换说明修饰符:在
%
和转换字符之间插入修饰符可修饰转换说明;
如:
%5.2f
打印一个浮点数,其宽度为5个字符,小数点后保留两位小字;
- 转换说明符、修饰符、标记速查列表:🔗格式转换说明符
- 如:打印
unsigned long
类型的值,使用组合的格式转换说明符lu
进行输出。
unsigned long data = 4294967295;
sprintfU4("data : %lu\r\n", data);
同理打印unsigned long long
类型的值,就使用组合的格式转换说明符llu
进行输出。
3.2 printf()
函数
请求printf()
函数打印的数据要与转换说明符相匹配;
如:打印整数时使用
%d
,打印字符时使用%c
,它们指定了数据的显示形式;
- 格式:
printf("格式字符串", 待打印项1, 待打印项2,...);
int number = 7;
float pies = 12.75;
printf("The %d contestants ate %f berry pies.\n", number,pies);
3.3 printf()
的返回值
- 输出正确:返回所打印字符个数;
- 输出错误:返回负值;
3.4 printf()
的宽精度控制
在转换说明符的%
和说明符之间添加数字,可对打印的数值进行宽精度控制;
// 宽度:
printf("%2d\n" ,1); // 打印2位宽度的整型数,不够2位则向右对齐,超出2位则按实际打印,实际打印:`(空格)1`
printf("%02d\n" ,1); // 对打印数值用前导零而不是用空格填充多余字段宽度,实际打印:`01`
// 精度(仅浮点数有效):
printf("%.2f\n" ,1.565); // 打印2位精度的浮点型数,即打印数值保留小数点后2位有效数字,遵循四舍五入法则,实际打印:`1.57`
// 若精度大于实际数值精度,则按实际打印;
// 宽度 + 精度:
printf("%9.2f\n" ,1.565); // 表示输出9位宽度、2位精度的浮点数,即整数取6位, 小数点占一位,保留小数点后2位有效数字,宽度不够9位则向右对齐,实际打印:`(5个空格)1.57`
3.5 打印较长的字符串
- 方法1(使用多个printf()语句):因第1个字符串没有以
\n
结束字符串,故第2个字符串紧跟第1个字符串末尾输出; - 方法2(使用反斜杠(
\
)衔接字符串):第一行字符串以\
结尾,编译器会自动把第一行字符串与第二行字符串进行衔接;
注:下一行代码必须从最左边开始,否则多出的部分会算入字符串中;
- 方法3(使用双引号(
"
)衔接字符串):第一行字符串用双引号包括,下一行同理也用双引号包括;
// 方法1:
printf("Here's one way to print a ");
printf("long string.\n");
// 方法2:(不推荐)
printf("Here's another way to print a \
long string.\n");
// 方法3:
printf("Here's the newest way to print a "
"long string.\n");
3.6 scanf()
函数
scanf()
是最通用的一个输入函数:它可以读取不同格式的数据,与printf()
类似,也使用格式字符串和参数列表,不同的是,scanf()
函数使用指向变量的指针;- 使用规则:
- 如果用
scanf()
读取基本变量类型的值,在变量名前加上一个取地址符&
; - 如果用
scanf()
把字符串读入字符数组中,不需使用&
; - 如果用
scanf()
读取字符串,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串,此时要用到下面提到的get()
函数;
- 如果用
int age; // 变量
float assets; // 变量
char pet[30]; // 字符数组,用于储存字符串
printf("Enter your age, assets, and favorite pet.\n");
scanf("%d %f", &age, &assets); // 这里要使用&
scanf("%s", pet); // 字符数组不使用&
scanf()
的转换说明符、修饰符速查:🔗格式转换说明符
3.7 scanf()
的返回值
- 读取成功:返回成功读取的项数;
- 读取失败:如果没有读取到任何项,或读取内容与转换说明符不匹配,如程序需要读取一个数字而用户却输入一个非数字的字符串,
scanf()
便返回0
;
3.8 输入单个字符_getchar()
、getche()
、getch()
getchar()
:是scanf("%c", c)
的替代品,除了更加简洁,没有其它优势;getche()
:它没有缓冲区,输入一个字符后会立即读取,不用等待用户按下回车键,这是它和scanf()
、getchar()
的最大区别;
注:
getche()
位于conio.h
头文件中,而这个头文件是 Windows 特有的,Linux 和 Mac OS 下没有包含该头文件,即getche()
并不是标准函数,默认只能在 Windows 下使用,不能在 Linux 和 Mac OS 下使用;
getch()
:与getche()
相同,它也没有缓冲区,输入一个字符后会立即读取,不用按下回车键,也是默认只能在 Windows 下使用,另外它的特点是,它没有回显,看不到输入的字符;
回显:就是在控制台上显示出用户输入的字符;没有回显,就不会显示用户输入的字符,就好像根本没有输入一样;
3.9 输入字符串_get()
gets()
:专用的字符串输入函数,它有缓冲区的,每次按下回车键,就代表当前输入操作结束;- 可读取含空格的字符串:这是它最大的特点,也是区别于
scanf()
函数的地方,它认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束;
3.10 输出单个字符_putchar()
putchar()
:与getchar()
相对应,都在stdio.h
头文件中,用于打印字符,相当于printf(ch,%c)
的简洁版;
3.11 缓冲区
- 数据区:缓冲区(Buffer)是在内存中预留指定大小的空间用于对输入/输出的数据作临时存储,这部分预留的内存空间就叫做缓冲区,缓冲区的大小取决于系统;
- 缓冲区优势:
- 减少实际物理读写次数;
- 缓冲区在创建时就被分配内存,这块内存区域会一直被占用,可以减少动内存态分配和回收内存的次数;
- 无缓冲区的优势:输入/输出响应更快;
- 例子:如A地有1w块砖要搬到B地由于没有工具(缓冲区),一次只能搬一块,那么就要搬1w次(实际读写次数)如果A,B两地距离很远的话(IO性能消耗),那么性能消耗将会很大,但若此时有辆大卡车(缓冲区),一次可运5000块,2次就够了相比之前,性能肯定是大大提高了
- 两种缓冲方式:
- 完全缓冲:当缓冲区被填满时才刷新缓冲区(把缓冲区内的内容发送到目的地),全缓冲的典型代表是对磁盘文件的读写 ;
- 行缓冲:当出现换行符
\r
时刷新缓冲区(把缓冲区内的内容发送到目的地),键盘输入通常是行缓冲输入,所以在按下Enter键后才刷新缓冲区;
像getche()
和getch()
输入数据后无需等待用户按下回车,数据就字节传输到CPU的函数,叫不带缓冲区的函数,
--------------------------------------------------
附录一. 字符串应用
1.1 字符串大小写转换
- 将字符串转换为大写
/***************************************************************************
** 函数名称 : TurnStringToCapital
** 功能描述 : 将字符串中的英文字母转换为大写
** 输入变量 : *str:字符串
** 返 回 值 : 无
** 作 者 : xxx
** 最新更新日期: 20201229
** 说 明 :
***************************************************************************/
void TurnStringToCapital(char *str)
{
u8 i=0;
u8 len=strlen (str);
for(i=0;i<len;i++)
{
if( (str[i]>='a')&&(str[i]<='z') )
{
str[i]=str[i]-0x20;
}
}
}
- 将字符串转换为小写
/***************************************************************************
** 函数名称 : TurnStringToLowercase
** 功能描述 : 将字符串中的英文字母转换为小写
** 输入变量 : *str:字符串
** 返 回 值 : 无
** 作 者 : xxx
** 最新更新日期: 20201229
** 说 明 :
***************************************************************************/
void TurnStringToLowercase(char *str)
{
u8 i=0;
u8 len=strlen (str);
for(i=0;i<len;i++)
{
if( (str[i]>='A')&&(str[i]<='Z') )
{
str[i]=str[i]+0x20;
}
}
}
1.2 计算字符串长度
// 函数所在头文件
#include <string.h>
// 函数声明
size_t strlen(const char *str)
// 函数应用举例
len = strlen(str);
-
参数:
str
– 要计算长度的字符串;
-
功能:
- 计算字符串
str
的长度,直到空结束字符\0
,但不包括空结束字符\0
;
- 计算字符串
1.3 字符串比较函数
1.3.1 strncmp
int strncmp ( const char * str1, const char * str2, size_t n );
功能是把 str1 和 str2 进行比较,最多比较前 n 个字节,若str1与str2的前n个字符相同,则返回0;若s1大于s2,则返回大于0的值;若s1 小于s2,则返回小于0的值;
- 应用案例
if(strncmp("SET NAME ",ptr,9)==0&&strlen(ptr)<=39) // strlen 用于限制字符串长度
{...}
1.3.2 strcmp
与strncmp
不同的是,int strcmp( const char * str1, const char * str2);
是比较整个字符,直到出现不同的字符或遇"\0"为止,比较的返回结果与strncmp
一样;
- 应用案例:
if(strcmp("ALS1 ON",ptr)==0)
{...}
1.4 提取字符串中的目标信息
保存字符串特定位后的所有目标信息;
#define EEPROMAdd 0
u8 targetStringHloder[30]; // 定义targetString的容器
const char *command1 = "SET ID ";
const char *command1_Pass ="Set ID %s Pass\r\n@_@";
char *ptr = "SET ID 001";
/***************************************************************************
** 函数名称 : SaveTargetString
** 功能描述 : 读取字符串目标信息并写入到EEPROM中
** 输入变量 :
stringStyle : 字符串样式
stringSample : 完整的字符串
Address : EEPROM写入地址
stringForPrint : 一个用于打印的字符串
targetString : 一个用于保存的字符数组
** 返 回 值 : 无
** 最后修改人 : xxx
** 最后更新日期: 20200901
** 说 明 : 无
***************************************************************************/
void SaveTargetString(const char *stringStyle,char *stringSample,u16 Address,const char *stringForPrint,char *targetString)
{
u8 stringStyleLength=strlen(stringStyle);
u8 stringSampleLength=strlen(stringSample);
u8 i,j;
char *p=&stringSample[stringStyleLength]; //定义一个指针,并指向字符串最后的位置
for(i=0;i<stringSampleLength;i++)
{
targetString[i]=p[i]; //字符串逐位传给字符数组
}
i++;
targetString[i]='\0'; //添加字符数组结束符
//Uart1_Printf("Result:%s\r\n",targetString); //调试指令,查看转换结果指令
for(j=0;j<10;j++)
{AT24CXX_WriteOneByte(Address+j,targetString[j]);} //写入EEPROM
// targetString= atof(targetString);//把字符串转换成浮点数
Uart1_Printf(stringForPrint,targetString); //打印转换结果
return;
}
// 应用举例:
if(strncmp(command1,ptr,7)==0&&strlen(ptr)<=14)
{SaveTargetString(command1,ptr,EEPROMAdd,command1_Pass,targetStringHloder);return;}
如:
stringStyle为:Set ID
stringSample为:Set ID 001
stringForPrint为:"Set ID %s Pass\r\n*_*";
那么该函数的作用是把字符串stringSample
中的目标信息001
保存到EEPROM中;
1.5 提取字符串中的目标信息并返回
功能与“提取字符串中的目标信息”差不多,只是增加了返回值;
/**********************************************************************************************************
* 函 数 名: SaveTargetStringAndRetrun
* 功能说明: 保存目标字符串并返回转换后的浮点数
* 形 参:
length:字符串中提取信息的位置
string:传入字符串
Address:EEPROM写入地址
stringForPrint:一个待被打印的字符串
* 返 回 值: targetValue:返回转换后的目标参数(浮点数)
* 修改日期:2020.9.3
**********************************************************************************************************/
u16 SaveTargetStringAndRetrun(u8 length,char *string,u16 Address,const char *stringForPrint)
{
u8 i = 0;
char targetString[8];
u16 targetValue = 0;
for(i=0;i<8;i++) //i<8表示最多提取8个字符
{
if(string[length + i] != '\0')
targetString[i] = string[length + i];
else
{
targetString[i] = '\0';
break;
}
}
for(i=0;i<8;i++)
{
AT24CXX_WriteOneByte(Address+i,targetString[i]);
}
targetValue = atof(targetString);//把字符串转换成浮点数
Uart1_Printf(stringForPrint,targetString);
return targetValue; //返回数值以方便参数保存
}
如:
length为:16
,因为要从string的第16位开始提取后面的2000
string为:"SET MOTOR SPEED 2000"
stringForPrint为:"Set Motor Speed %s Pass\r\n*_*"
那么该函数的作用是把字符串string
中的目标信息2000
提取、保存到EEPROM中并返回浮点数2000
;
1.6 将字符串转换为数字的系列函数
1.6.1 atof
atof(ascii to floating point numbers),将字符串转换为双精度浮点数(double);
- 函数原型:
double atof(const char *str);
- 函数说明:扫描字符串参数
str
,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,在再遇到非数字或字符串结束时(‘\0’)才结束转换,并将结果返回。参数nptr
字符串可以包含正负号、小数点或E(e)来表示指数部分,如123.456或123e-2;如果字符串 str 不能被转换为 double,那么返回 0.0; - 应用案例:
#include <stdlib.h> // 函数定义在头文件stdlib.h中
int main()
{
char*a="-100.23";
char*b="200e-2";
double c;
c=atof(a)+atof(b);
printf(“c=%.2lf\n”,c); // 打印结果为c=-98.23
return 0;
}
1.6.2 strtod
strtod() 用来将字符串转换成双精度浮点数(double);
- 函数原型:
double strtod (const char* str, char** endptr);
- 输入参数:
- str 为要转换的字符串;
- endstr 为第一个不能转换的字符的指针;
- 函数说明:扫描参数str字符串,跳过前面的空白字符(例如空格,tab缩进等,可以通过 isspace() 函数来检测),直到遇上数字或正负符号才开始做转换,到出现非数字或字符串结束时(‘\0’)才结束转换,并将结果返回。参数 str 字符串可包含正负号、小数点或E(e)来表示指数部分。如123. 456 或123e-2;如果字符串 str 不能被转换为 double,那么返回 0.0;若endptr 不为NULL,则会将遇到的不符合条件而终止的字符指针由 endptr 传回;若 endptr 为 NULL,则表示该参数无效,或不使用该参数(若 endptr 为 NULL,该函数的功能与
atof
相同); - 应用案例:
#include <stdio.h>
#include <stdlib.h>// 函数定义在头文件stdlib.h中
int main()
{
char string[] = "365.24 29.53";
char* endPoint;
double data1, data2;
data1= strtod (string, &endPoint); //函数运行后,指针endPoint指向string中数字4之后的那个空格符
data2= strtod (pEnd, NULL);
printf ("%.2f\n", data1/data2);
system("pause"); //程序暂停,按任意键继续
return 0;
}
1.6.3 atoi
atoi() 函数用来将字符串转换成整数(int);
- 函数原型:
int atoi (const char * str);
- 函数说明:扫描参数 str 字符串,跳过前面的空白字符(例如空格,tab缩进等,可以通过 isspace() 函数来检测),直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时(‘\0’)才结束转换,并将结果返回;如果 str 不能转换成 int 或者 str 为空字符串,那么将返回 0;与
atof
一样,该函数定义在头文件stdlib.h
中;
1.6.4 atol
atol() 函数用来将字符串转换成长整型数(long);
- 函数原型:
long atol(const char * str);
- 函数说明:扫描参数 str 字符串,跳过前面的空白字符(例如空格,tab缩进等,可以通过 isspace() 函数来检测),直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时(‘\0’)才结束转换,并将结果返回;如果 str 不能转换成 long 或者 str 为空字符串,那么将返回 0;与
atof
一样,该函数定义在头文件stdlib.h
中;
1.6.5 strtol
strtol() 函数用来将字符串根据参数 base 来转换成长整型数(long);
- 函数原型:
long int strtol (const char* str, char** endptr, int base);
- 输入参数:
- str 为要转换的字符串;
- endstr 为第一个不能转换的字符的指针;
- base 为字符串 str 所采用的进制,取值范围为从2 到36或0(0与10一样,表示采用十进制),如base 值为10 则采用10 进制,base 值为16 则采用16 进制等;
注:
- 形参endstr 的工作原理与函数
strtod
的第二个形参同理;- 如果遇到 ‘0x’ / ‘0X’ 前置字符则会使用 16 进制转换,遇到 ‘0’ 前置字符则会使用 8 进制转换。
-
函数说明:扫描参数 str 字符串,跳过前面的空白字符(例如空格,tab缩进等,可以通过 isspace() 函数来检测),直到遇上数字或正负符号才开始做转换,再遇到非数字或字符串结束时(‘\0’)结束转换,并将结果返回。如果不能转换或者 str 为空字符串,那么返回 0(0L);如果转换得到的值超出 long int 所能表示的范围,函数将返回 LONG_MAX 或 LONG_MIN(在 limits.h 头文件中定义),并将 errno 的值设置为 ERANGE。与
atof
一样,该函数定义在头文件stdlib.h
中; -
应用案例:
#include <stdio.h>
#include <stdlib.h>
int main ()
{
char string[] = "2001 60c0c0 -1101110100110100100000 0x6fffff";
char * endPoint;
long int longInt1, longInt2, longInt3, longInt4;
longInt1= strtol (string,&endPoint,10); //转换结果为十进制的2001
longInt2= strtol (endPoint,&endPoint,16); //转换结果为十六进制的 60c0c0
longInt3= strtol (endPoint,&endPoint,2); //转换结果为二进制的 -1101110100110100100000
longInt4= strtol (endPoint,NULL,0); //转换结果为十六进制的0x6fffff
printf ("转换成10进制: %ld、%ld、%ld、%ld\n", longInt1, longInt2, longInt3, longInt4);
// 打印结果:转换成10进制: 2001、6340800、-3624224、7340031
system("pause"); //程序暂停,按任意键继续
return 0;
}
- 该函数的另一个应用
int32_t pluse;
char str[] = "move -5000";
char *p = NULL;
char *s = &str[5]; // 从字符串第5位开始读取数值
if(strncmp(str,"MOVE ",5)==0) // 检验字符串是否正确
{
pluse = strtol(s,&p,10);
printf("Move:%d\r\n",pluse);
}
输出结果:
pluse 的值为 -5000
1.6.6 strtoul
strtoul(string to unsigned long),用来将字符串转换成无符号长整型数(unsigned long);
- 函数原型:
unsigned long strtoul (const char* str, char** endptr, int base);
- 输入参数:
- str 为要转换的字符串;
- endstr 为第一个不能转换的字符的指针;
- base 为字符串 str 所采用的进制;参数 base 代表 str 采用的进制方式,如 base 值为10 则采用10 进制,若 base 值为16 则采用16 进制数等。其取值范围从2 至36或0(0与10一样表示十进制);
注:
- 形参endstr 的工作原理与函数
strtod
的第二个形参同理;- 如果遇到 ‘0x’ / ‘0X’ 前置字符则会使用 16 进制转换,遇到 ‘0’ 前置字符则会使用 8 进制转换。
- 函数说明:扫描参数 str 字符串,跳过前面的空白字符(例如空格,tab缩进等,可以通过 isspace() 函数来检测),直到遇上数字或正负符号才开始做转换,再遇到非数字或字符串结束时(‘\0’)结束转换,并将结果返回;如果 str 不能转换成 long 或者 str 为空字符串,那么将返回 0;与
atof
一样,该函数定义在头文件stdlib.h
中; - 应用案例:
#include <stdio.h>
#include <stdlib.h> //函数定义在该头文件中
int main ()
{
char buffer [256];
unsigned long ul;
printf ("Enter an unsigned number: ");
fgets (buffer, 256, stdin);
ul = strtoul (buffer, NULL, 0);
printf ("Value entered: %lu.\n", ul);
//打印结果:Enter an unsigned number: 017cyuyan
// Value entered: 15.
system("pause");//程序暂停,
return 0;
}
1.7 按照字节填充字符串memset
- 函数声明
void *memset(void *str, int c, size_t n)
复制整型数据 c 按字节覆盖填充到参数 str 所指向的字符串的前 n 个位
- 例程:
int num;
memset(&num, 0, sizeof(int));
printf("step1=%d\n", num);
memset(&num, 1, sizeof(int));
printf("step2=%d\n", num);
- 运行结果:
step1 = 0
step2 = 16843009
- 工作原理:
- 首先
sizeof(int)
的结果是 4*8=32 位 memset(&num, 0, sizeof(int));
就是按字节填充0
,即00000000 00000000 00000000 00000000
memset(&num, 1, sizeof(int));
就是按字节填充1
,即00000001 00000001 00000001 00000001
,换算成十进制就是16843009
- 例程:
#include <stdio.h>
#include <string.h> //函数定义在该头文件中
int main ()
{
char str[50];
strcpy(str,"This is string.h library function");
puts(str);
memset(str,'$',7);
puts(str);
return(0);
}
运行结果:
This is string.h library function
$$$$$$$ string.h library function
1.8 查找多个字符串中的关键字
Temp=atoi(&p[16]); // 从第16个字符开始提取
freq=Temp;
sprintfU4("Set F %d Pass\r\n@_@",freq);
Temp=16;
while(p[Temp]!=0)
{
if(p[Temp]=='D'){break;}
Temp++;
}
Temp=atoi(&p[Temp+1]);
cycle=Temp;
如:
字符串为:open green led f5 d50
,要提取关键字5和50
其他
1. 局部变量与全局变量
- 局部变量:在函数内定义的变量是内部变量,它只在本函数范围内有效
- 全局变量:在函数外部定义的变量就是全局变量,其特点如下:
- 全局变量可以为本文件中的其他函数所共用。它的有效范围为从定义变量的位置开始到本源文件结束。
- 设置全局变量的作用是增加函数间数据联系的渠道。
- 如果在同一个源文件中,外部变量和局部变量同名,则在局部变量的作用范围内,外部变量将被“屏蔽”,即外部变量将不起作用。
- ❌使用全局变量的缺点:
- 全局变量在程序的执行过程中始终占用存储单元,而不是仅在需要时才占用存储单元。
- 函数的通用性降低了,因为函数在执行时要依赖于其所在的外部变量。如果将一个函数移植到另一个文件中,还要将有关的外部变量及其值一起移植过去。
- 使用全局变量过多,会降低程序的清晰性,特别是多个函数都调用此变量时。
2. static
关键字
在希望函数中的局部变量的值在函数调用结束后不消失而保留原值,即占用的存储单元不释放,在下一次该函数调用时,该变量的值为上一次函数调用结束时的值。这时可以使用关键字 static
进行局部变量的声明。
- 用
static
声明一个变量的作用:- 对局部变量用
static
声明,则使用该变量在整个程序执行期间不释放,为其分配的的空间始终存在。 - 对全局变量用
static
声明,则该变量的作用域只限于本文件中。
- 对局部变量用
参考链接: