C理论之一—— 基本数据类型、函数、输入/输出函数

文章目录

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 shortlonglong long

  • 有符号短整型_short:占用的内存可能比int型少,但C语言规定short类型至少占16位(2字节);
  • 有符号长整型_long:占用的内存可能比int型多,C语言规定long类型至少占32位(4字节);
  • 有符号双长整型_long long:占用的内存可能比long型多,C语言规定long long类型至少占64位(8字节);
  • 无符号shortlonglong long型同理,把最高位作为有效数字位;

上述整型实际占用内存空间根据实际操作系统而定;

1.2.5 整型常量

  • 整型常量:声明一个整型数据变量,然后把一个常量赋值给它,那么它就是一个整型常量;
  • 整型常量的声明
    • 无特殊声明,除非常大的整型常量外,编译器默认整型常量为int类型;
    • 前缀0:表示用八进制表示整型常量;
    • 前缀0x0X:表示用十六进制表示整型常量;
int some0 = 20; // 用十进制表示整型常量
int some1 = 020; // 用八进制表示整型常量,八进制20 = 十进制16
int some2 = 0x20; // 用十六进制表示整型常量,十六进制20 = 十进制32
  • :不论使用何种方式表示常量,都不会影响数被储存的方式,他们都是以二进制形式被存储的,以上表示方式是为了方便阅读;
  • 八进制和十六进制记数法的优点:八进制和十六进制记数法在表达与计算机相关的值时很方便,因为8和16都是2的幂,而10不是,另外,十六进制数的每一位的数恰好由4位二进制数表示;
    例如:十六进制数0x53=0101 0011,这种对应关系使得十六进制和二进制间的转换非常方便;

1.3 浮点数

即带小数点和小数部分的数据类型;

如:7.007..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类型,这样提高了计算精度,但由于计算量大,代码运算速度变慢;
    • 后缀fF:浮点型常量加后缀fF,编译器会将其始终视为float类型;
    • 后缀lL:浮点型常量加后缀lL,编译器会将其始终视为long double类型;
    • 前缀0x0X(C99标准):浮点型常量加前缀0x0X,表示用十六进制表示浮点型常量,用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

又叫逻辑型,用于表示布尔值(逻辑值)truefalse,在C语言中true=1false=0,每个布尔类型的数据占用1位内存空间;

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] = { 12345 }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是指针变量(内存地址),pc是数据变量,数据变量里的是数据;
      在这里插入图片描述
  • 所有的数据都必须放在内存中方可处理:不同类型的数据占用的字节数不一样,如在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 在内存中的地址分别是0X10000X20000X3000
那么加法运算c = a + b,将会被转换成类似下面的形式:
0X3000 = (0X1000) + (0X2000);
其中()表示取值操作,整个表达式的意思是,取出地址 0X10000X2000 上的值,将它们相加,把相加的结果赋值给地址为 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

intfloat等一样,结构体也是数据类型的一种,也可以用它来定义变量。他与数组原理相近,不同和的是,在同一个结构体内可存放不同类型(甚至结构体本身)的元素;

  • 结构体就像一个模板:用该结构体定义的结构体变量相当于拷贝了这个模板然后进行填充(这一点有点像面向对象的类)。所以一定要区分好结构体和由结构体定义的结构体变量
  • 编译器不会为结构体模板分配内存空间:结构体是一种数据类型,是一种创建变量的模板,就像 intfloatchar 这些关键字本身不占用内存一样,而由它们定义的变量才包含实实在在的数据,才需要内存来存储。

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 机上的内存存储模式为例,成员 nchm 在内存中“对齐”到一头,对 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

  • 枚举enumintfloat等也是数据类型关键字,专门用来定义枚举类型,枚举体内的枚举值都为整数,语法为: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 的值分别是 0xE0x24'$' 对应的 ASCII 码为 0x24),换算成二进制是 111010 0100,都没有超出限定的位数,能够正常输出;
- 第二次输出时,n、ch 的值变为 0x2d0x7a'z' 对应的 ASCII 码为 0x7a),换算成二进制分别是 10 1101111 1010,都超出了限定的位数。超出部分被直接截去,剩下 110111 1010,换算成十六进制为 0xd0x3a0x3a 对应的字符是 :);

1.12 数据溢出问题

如果超出了相应类型的取值范围,数据会从溢出值返回到该数据类型的起始点(重置点)两边的值之一;

如:在16位微机中
int32767+1 --溢出–>-32768-32768-1–溢出->32767
unsigned65535+1 --溢出–>00-1–溢出->65535

在这里插入图片描述

1.13 数据类型转换问题

当参加同一个运算的变量的数据类型不同时,变量就会发生类型转换,类型转换的方法有两种,自动转换和强制转换;

  • 自动转换:当不同类型的变量进行混合运算时,编译系统将按照一定的转换规则自动将它们转换成同一类型,再进行运算;

    1. 按数据长度增加的方向进行转换:以保证精度不降低 ;

    int 型数据和 long 型数据进行运算时,系统会先将 int 型数据转换成 long 型,然后再进行运算,以保证运算结果精度不会降低;

    1. 浮点数->double:所有的浮点数运算都是以双精度double型进行运算的,因为CPU 在运算时有字节对齐的要求,这样运算的速度是最快的;
    2. charshort->int:字符型char 和 短整型short 数据参与运算时,必须先转换成 int 型,与上同理;
    3. int->unsigned:有符号整型int和无符号整型unsigned混合运算时,int要转换成unsigned
    4. 整型->浮点型:整型和浮点型混合运算时,整型先转换成浮点型,运算的结果是浮点型;
    5. 赋值转换:在赋值过程中,当赋值号两边的数据类型不同时,右边的类型先转换为左边的类型,再赋给左边,所以,若右边数据类型的长度比左边长,则它将发生数据丢失,这样会降低数据精度,编译时会产生警告;

微软MSDN关于数据类型转换的翻译
在这里插入图片描述

  • 类型转换规则:由程序员通过代码强制对数据进行类型转换;
int cost = 12.99; // 变量cost 会被强制转换为 int类型,而小数部分将被截去,数据精度遭受损失
float pi = 3.1415926536; // 变量pi会被强制转换为 float类型,而后 6位有效数值将被截去,数据精度遭受损失

1.14 关于变量初始化

🔗链接:C语言编程时,各种类型的变量该如何初始化?

在敲代码的时候,我们会给变量一个初始值,以防止因为编译器编译等原因造成变量初始值的不确定性。

  • 数值类变量:初始化为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后,没有给该指针重新分配内存,则会出现难以预料的错误(最常见的就是操作空指针引起的段错误)。在动态内存管理中,由于变量的内存是分配在堆中的,所以一般用malloccalloc等函数申请过动态内存,在使用完后需要及时释放,一般释放掉动态内存后要及时将指针置空
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)中只能用小写;

在计算机底层数据都以二进制01存储,对于同一数据段,使用不同的转换说明符可在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()函数使用指向变量的指针;
  • 使用规则
    1. 如果用scanf()读取基本变量类型的值,在变量名前加上一个取地址符&
    2. 如果用scanf()把字符串读入字符数组中,不需使用&
    3. 如果用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); // 字符数组不使用&

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)是在内存中预留指定大小的空间用于对输入/输出的数据作临时存储,这部分预留的内存空间就叫做缓冲区,缓冲区的大小取决于系统;
  • 缓冲区优势
    1. 减少实际物理读写次数;
    2. 缓冲区在创建时就被分配内存,这块内存区域会一直被占用,可以减少动内存态分配和回收内存的次数;
  • 无缓冲区的优势:输入/输出响应更快;
  • 例子:如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 进制等;

注:

  1. 形参endstr 的工作原理与函数strtod的第二个形参同理;
  2. 如果遇到 ‘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一样表示十进制);

注:

  1. 形参endstr 的工作原理与函数strtod的第二个形参同理;
  2. 如果遇到 ‘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
  • 工作原理:
  1. 首先sizeof(int) 的结果是 4*8=32 位
  2. memset(&num, 0, sizeof(int));就是按字节填充0,即00000000 00000000 00000000 00000000
  3. 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. 局部变量与全局变量

  • 局部变量在函数内定义的变量是内部变量,它只在本函数范围内有效
  • 全局变量在函数外部定义的变量就是全局变量,其特点如下:
    1. 全局变量可以为本文件中的其他函数所共用。它的有效范围为从定义变量的位置开始到本源文件结束。
    2. 设置全局变量的作用是增加函数间数据联系的渠道。
    3. 如果在同一个源文件中,外部变量和局部变量同名,则在局部变量的作用范围内,外部变量将被“屏蔽”,即外部变量将不起作用。

  • ❌使用全局变量的缺点
    1. 全局变量在程序的执行过程中始终占用存储单元,而不是仅在需要时才占用存储单元。
    2. 函数的通用性降低了,因为函数在执行时要依赖于其所在的外部变量。如果将一个函数移植到另一个文件中,还要将有关的外部变量及其值一起移植过去。
    3. 使用全局变量过多,会降低程序的清晰性,特别是多个函数都调用此变量时。

2. static关键字

在希望函数中的局部变量的值在函数调用结束后不消失而保留原值,即占用的存储单元不释放,在下一次该函数调用时,该变量的值为上一次函数调用结束时的值。这时可以使用关键字 static 进行局部变量的声明。

  • static 声明一个变量的作用
    1. 局部变量static 声明,则使用该变量在整个程序执行期间不释放,为其分配的的空间始终存在。
    2. 全局变量static 声明,则该变量的作用域只限于本文件中。

参考链接

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Truffle7电子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值