前言
C语言的学习是计算机学习的基石!很多人的第一门语言都是C语言,把C语言打好基础,不论是对新语言的学习,还是数据结构的学习都会有一定的帮助,博主也是从小萌新开始老实啃C语言的,本博客可能没有非常全面,根据以下目录没有需要的内容请见谅^ _ ^, 若对你的学习有帮助,可以给个点赞收藏吧,接下来进入C语言的学习吧~~~


目录

基础知识

1、C语言的简单介绍(语言类型、程序框架、输入输出、注释、分号;,注意事项)

程序是用特殊的编程语言写出来表达如何解决问题的,是描述计算机如何做事情的过程和方法
程序的执行有两种,解释型和编译型C语言是编译型语言

  • 解释型:借助一个程序,那个程序能理解你的程序,然后按照你的要求执行,需要使用的转换工具是解释器
  • 编译型:借助一个程序,就像一个翻译,把你的程序翻译成计算机真正能够懂的语言(机器语言)写的程序,然后,这个机器语言写的程序就能直接执行了,需要使用的转换工具是编译器

简单来说,解释型语言就像一种实时翻译,而编译型语言是把一段文档打包整体翻译过来(可执行文件)让计算机去执行。
编译型语言:C语言、C++、汇编等。
解释型语言:Python、MATLAB、Javascript等。
在这里插入图片描述

所以写C语言程序,完成程序先确保编译通过,再运行程序
C语言的用途:操作系统、嵌入式系统、驱动程序、底层驱动、图像引擎、图像处理、声音效果

C语言的程序框架:

#include <stdio.h>
int main()
{
	
	return 0;
}

初学阶段可以先记住这个框架,在学到函数之前基本用这个框架,学到函数,结构体,可能才需要在框架外写代码

注释
注释的作用是对代码分析说明,让别人看得懂,在后面的学习做笔记和写项目,写注释是非常重要的

// 单行注释

/*
多行注释
*/

分号;
在C语言中,一段代码的结束需要分号;结尾,这就像我们平常语文书写要加标点符号一样的。
初学者经常忘记加分号,导致代码编写错误,这需要长时间的写代码来养成习惯,所以初学者在学习过程中,手一定要动起来,把代码都敲一遍

注意事项:
在计算机编程学习中,一般是使用英文输入法进行编写,初学者经常疑因为输入法的使用错误导致编译失败,所以在学习的初期就要开始培养良好的编码习惯,确定自己的编码风格,为以后的学习和工作有一定的帮助

2、输入与输出(printf、scanf)

要对数据进行操作,就要学会如何获取数据以及输出数据,在C语言中,框架中的“stdio.h”库文件包含了输出输入的函数
输出——printf
C语言的输出语句为printf( );根据输出的的数据类型不同,有两种编写方式:

  • 输出常量字符串
    #include <stdio.h>
    int main()
    {
    	printf("hello world!!!");
    	return 0;
    }
    

输出常量字符串直接在括号中用双引号修饰即可。

  • 输出变量
    #include <stdio.h>
    int main()
    {
    	int a = 1;
    	printf("23+25=%d\n",23+25);
    	printf("%d\n",a);
    	return 0;
    }
    

变量输出步骤多了点,在双引号内写%d,%d是让printf在%d里填一个数据,%d后面有什么就填说明值,用,隔开,后面是运算表达式会自行运算,是变量会自动访问内部数据

%d这里是整型int类型的特殊情况,不同类型输出,写法不同,如:
int —> %d
double —> %f
char —> %c
之后会有总结

输入——scanf
输入语句与输出语句相似,差别在于输入要调用取址符

#include <stdio.h>
int main()
{
	int a;
	scanf("%d",&a);
	printf("%d",a);
	return 0;

这段程序,申请了一块内存,之后把键盘输入的数据,通过&取址符,进行准确的赋值
取址符是用来访问变量内存地址的,不理解没关系,之后学到指针就可以理解了

格式说明符——%
在 C/C++ 中,printf 和 scanf 等格式化输入输出函数使用格式说明符来指定变量的类型,当学完数据类型后会统一总结对应的格式

3、标识符命名规则

实际上,计算机的学习是对杂乱数据的处理,数据需要被储存到计算机中,这就要和内存联系上,我们在学习C语言,硬件方面不过多阐述,但C语言的学习多少会和计算机内部某些硬件扯上关系,所以我简单概述。

在C语言中,我们一般会申请一块内存空间并给它起名,方便操作这段内存以及其内部储存的数据,因为这块内存的数据可以被改变,所以也叫变量,说到可以被改动,就会联想到其对立面,即申请的这块内存空间内部的数据,一旦被储存就不可以被更改,所以也叫常量

变量和常量统称为标识符,标识符是用来识别这个和那个不同的名字,初学者需要注意,标识符的命名有自己的规则!
规则:

  1. 数据类型 + 变量名称 = 变量初始化
    C语言变量的定义要按这个模板进行编写
    int a = 10;
    double b = 0.5;
    char c = 'c';
    //bool bol = true; 	//之后数据类型会讲解
    
  2. 标识符不能是关键字
    数据类型和一些修饰词在C语言中都有自己专属的名称,比如:整型 int,静态修饰词static,常量修饰词const,这些都是关键字
    所以关键字是已经被定义好的,在命名标识符时不能使用到关键字
    int int = 0; 
    int static = 1;		//error
    

C语言的关键字: 在这里插入图片描述

  1. 标识符的字符组成
    标识符一般由字母、数字、下划线组成

    int A_32 = 2;
    int abc = 0;
    int ___abc = 4;
    
  2. 第一个字符必须为字母或下划线

    int 123abc = 0;  //error!
    int a_1 = 0;	// right!
    
  3. 标识符中字母区分大小写
    标识符是区分字母大小写的,可以通过大小写来区分

    int a = 0;
    int A = 0;		//这是两个变量
    
  4. const定义常量
    常量的定义非常简单,在变量的基础上加上const常量修饰词

    const int Month = 12;
    //Month = 24;	//这样会报错,表达式必须是可修改的左值,Month已经是常量,不可以修改
    
  5. 变量与函数名称定义规则
    在编程中,我们定义变量的时候,最好是能够做到见名知意,这样有利于我们之后的复习以及提供给他人阅读,其中有两个比较常用的命名方法:

    1. 驼峰命名法(CamelCase):
    • 小驼峰:userName (变量、函数)
    • 大驼峰:UserModel (类、构造函数)
      //类和构造函数不理解没关系,了解即可
    1. 蛇形命名法(snake_case):
    • 用于常量或脚本语言:MAX_SIZEfile_path

4、四则运算

四则运算包括加法、减法、乘法和除法四种基本数学运算。它们是数学中最基础的运算方式,广泛应用于日常生活和科学计算中,C语言中还有取余运算符。
在这里插入图片描述

#include <stdio.h>
int main()
{
	int a = 3;
	int b = 4;
	printf("a+b=%d",a+b);
	printf("a-b=%d",a-b);
	printf("a*b=%d",a*b);	//C语言中,乘法运算符是*
	printf("a/b=%d",a/b);	//C语言中,除法运算符是/
	printf("a%%b=%d",a%b);	//因为%有较多作用,在四则运算中是取余,在输入输出是做格式说明,这里是两个%表示(可以理解为第一个%说明要输入的格式是%)
	return 0;
}

在这里插入图片描述

  • 以上代码中,加减乘与一般常识无二,这里要对除法和取余讲解:

a/b =0的原因,首先a,b变量为整型变量,只能接收整型数据,a/b在科学计算中为整数除法,结果应该为整数商而不是一般认为的小数商(浮点数(实型)),所以有区别,在做除法运算的时候,整型接收浮点数数据时会做强制类型转换,即直接提取小数点前的数据,这里a/b的小数商结果为0.75,转换为整型后结果为0,即输出的结果。

a/b的余数运算,余数运算要求结果为整数,即a/b商为0,余数为3,因为4无法整除3,所以余下了3
注意:只有整数才可以进行取余运算!

5、赋值与初始化

赋值和初始化是C语言非常常规的使用语句,因为我们要对数据进行操作就要有其他容器来装下数据,前面讲到创建变量是为了得到内存空间,赋值的作用就是在内存空间中进行数据存储(或者数据更改)

赋值:
赋值对象一定是也被创建并且合法的变量,然后要使用的工具是赋值运算符=

int temp;
temp =  10;
printf("temp = %d\n",temp);

注意:

  • =需要转换思想,和数学不同,a = b , temp = 10 表示关系,即a,b一样,但在程序设计中不同,我们需要转变思想,a = b表示要求计算机做了一个动作:将10 赋值给 temp ,b赋值给a。关系是静态的,而动作是动态的,所以要理解为把10 赋值给 temp ,是一种逆向赋予的动作而不是简单的等于关系。(将‘=’右边的值赋给左边的变量)
    int a,b,c,temp;
    a = 10;
    b = 20;
    c = 30;
    temp = a = b = c;
    printf("temp = %d\n",temp);
    
    在这里插入图片描述
    可以试着自己理解输出结果
    分析: 首先是c赋值给b变量,此时a =10,b = 30, c = 30,然后是b赋值给a,此时a = 30, b = 30, c = 30,最后是 a赋值给temp,此时 a = 30 ,b = 30,c = 30, temp = 30。
  • 赋值的数据要满足被赋值变量的类型,减少不必要的错误。

初始化:
初始化是在创建变量的同时给变量赋值。初始化操作在程序设计中非常重要。在程序设计中的内存空间是使用电脑的虚拟内存,在内存中,不是所有数据都是有意义的,大多都是随机数,当我们创建变量,但没有马上初始化,即定义有意义的数据,之后可能会因为忘记赋值而导致程序不能达到理想结果或程序错误

int a;
printf("%d\n",a);

在这里插入图片描述
由此可以知道,初始化的重要性,为了减少这样的失误,我们要养成初始化的良好习惯

6、运算符

  • 运算符(operator)是指进行运算的动作,比如加法运算符“+”,减法运算符“-”。
  • 算子(operand)是指参与运算的值,这个值可能是常数,也可能是变量,还可能是一个方法的返回值(函数调用)
    在这里插入图片描述

以下将要学习的复合赋值运算符递增递减运算符是程序设计中的快捷计算方法,方便程序编写,其次就是比较运算符逻辑运算符,对逻辑判断和大小比较计算有帮助。

算术运算符

算数运算符包括日常算数计算的加减乘除,取余,赋值

在基础知识部分已有讲解!
具体查看基础知识——四则运算/赋值与初始化
下面进行简单带过

赋值运算符 =
	赋值运算符要求计算机做一个动作,将赋值运算符右边的右值赋给赋值运算符左边的左值
	(右值赋给左值)
	int a =  12;
----------------------------------------------------
加法运算符 +
	加法运算符是将运算符左右两边的算子相加。
	int a = 12int b = 24;
	int c;
	c= a+b;
----------------------------------------------------
减法运算符 -
	减法运算符是将运算符左右两边的算子相减
	c = a-b;
----------------------------------------------------
乘法运算符 *
	乘法运算符是将运算符左右两边的算子相乘
	c = a*b;
----------------------------------------------------
除法运算符 /
	除法运算符是做运算符左算子除于右算子。
	c = a/b;
----------------------------------------------------
取余运算符	%
	取余运算符是做运算符左算子除以右算子后得到的余数
	c = a%b;

复合赋值运算符

5个算术运算符,+ - * / %,可以和赋值运算符=结合起来,形成复合赋值运算符:
+=-=*=/=%=
复合赋值运算符有些许抽象,但为之后递增递减运算符的学习做了铺垫,了解其运算原理,自然就水到渠成

复合赋值运算符的原理:
以+=为例,a+=b,等价于a=a+b,即+=就是左值和右值相加后的数据,重新赋值给的左值变量
所以其余复合赋值运算符使用原理一样:

int main()
{
	int m =10;
	int n = 5;
	m+=n;		//m=m+n; 
	printf("m = %d\n",m);
	m-=n;		//m=m-n;
	printf("m = %d\n",m);
	m*=n;		//m=m*n;
	printf("m = %d\n",m);
	m/=n;		//m=m/n;
	printf("m = %d\n",m);
	m%=n;		//m=m%n;
	printf("m = %d\n",m);
}

在这里插入图片描述

(这里方便讲解,把左值和右值表示等号左边和右边,但了解过C++11右值引用后就知道左值和右值不是单纯的等号左值和右值了)

递增递减运算符

++是两个很特殊的运算符,它们是单目运算符(只需要一个算子),这个算子还必须是变量。这两个运算符分别叫做递增和递减运算符,他们的作用就是给这个变量+1或-1。

++可以放在变量的前面,叫做前缀(前置)形式,也可以放在变量后面,叫做后缀(后置)形式,接下来将以递增运算符为例讲解。

递增运算符++分为前置(前缀)递增后置(后缀)递增,这个是由递增运算符放在变量的位置来定的;这两种递增都做了加1的动作,但其结果是不一样的;

在讲解前先要解释,递增递减运算符是单目运算符,无论是什么运算符,都会在计算完之后返回计算结果,这个时候可以把整个表达式当成这个计算结果,如:“a = 2+3”,2+3为表达式,可以把2+3看成它们的计算结果5,即“a = 5”,之后的讲解可以根据这里理解

  • 前置递增(++a)
    ++a 是先让a+=1,再把a+=1结果赋给++a,即++a这个表达式的结果是a+1,其操作的副产物是a+=1,记住其逻辑是先计算后赋值(先计算a+=1,后把a+=1的结果赋给++a)
  • 后置递增(a++)
    a++ 是先对a++表达式赋值,之后再做a+=1的计算,即a++这个表达式的结果是a,a++这个操作的副产物是a+=1,记住其逻辑是先赋值后计算(先对a++这个表达式赋给a,后计算a+=1)
int a = 6;
printf("(++a) -> %d\n",++a);	//(++a) -> 7
printf("a = %d\n",a);			//a=7
printf("(a++) -> %d\n",a++);	//(a++) -> 7
printf("a = %d\n",a);			//a=8

在这里插入图片描述

比较运算符

在计算机中,做数据的比较,要通过比较运算符来操作,比较运算符是双目运算符,调用比较运算符会对比较的数据进行比较,比较结果只有1(真/true)或0(假/false)。
这里需要记住,程序设计中,除了0为假,其他数都为真

比较运算符有==!=><<=>=
==的含义就是a等于b,!=是a不等于b,>=和<=两个运算符的理解是大于或等于小于或等于,其他运算符和日常计算含义一样。调用这些比较运算符后,会对数据进行判断并返回判断结果。
这里的思想类似于上面学习的递增递减运算符,即(a == b)这个表达式的含义只有1或0。

int a = 3;
int b = 1;
printf("(a == b)= %d\n",(a == b));
printf("(a != b)= %d\n",(a != b));
printf("(a > b) = %d\n",(a > b));
printf("(a < b) = %d\n",(a < b));
printf("(a <= 3)= %d\n",(a <=3));
printf("(a >= b)= %d\n"(a >= b));

在这里插入图片描述

逻辑运算符

逻辑表达式主要有逻辑非!、逻辑与&&、逻辑或||

  • 逻辑非!是单目运算符,是判断数据是否为假的操作,若数据为假(0)则逻辑非结果为真,数据为真则逻辑与结果为假(口诀:假为真,真为假
  • 逻辑与&& 是双目运算符,是判断两个数据是否都为真的操作,这个操作结果为真的情况只有两个数据都为真的时候,否则都为假。(口诀:同真为真,其余为假
  • 逻辑或|| 是双目运算符,是判断两个数据中有没有为真的操作,只要两个数据中有一个为真,逻辑或的结果就为真,否则结果为假。(口诀:有真为真,无真为假)

注意:逻辑与与逻辑或的相似,注意区分二者的区别,逻辑或包含逻辑与。

int a = 7;
int b = 4;
int c =0;
printf("!a = %d\n",!a);
printf("!c = %d\n",!c);
printf("!!c = %d\n",!!c);

printf("(a && b) = %d\n",a && b);
printf("(a && c) = %d\n",a && c);

printf("(a || b)= %d\n",a || b);
printf("(a || c)= %d\n",a || c);
printf("(c || 0)= %d\n",c || 0);

在这里插入图片描述

7、表达式

  • 一个表达式是一系列运算符和算子的组合,用来计算一个值

分为算术表达式、关系表达式、逻辑表达式,赋值表达式

//算术表达式
amount = x * (1 + 0.033) * (1 + 0.033) * (1+0.033);
//赋值表达式
total = 100;
//关系表达式
int a=5,b= 10;
int temp = a < b 	//结果为1(真)
//逻辑表达式
int x = 1, y = 0;
int res =x && !y	//结果为1(真) 

从上面例子也能看到,表达式从简单到复杂,运用范围广,之后的学习会运用到,记住概念即可


数据类型(补码、越位)

C语言的变量必须在使用前定义并且确定类型,C语言是有类型的语言,在C语言后的语言向两个方向发展:

  • C++/Java更强调类型,对类型的检查更严格
  • Javascript、Python、PHP不看重类型,甚至不需要实现定义
    C语言的类型主要有整型、浮点型、字符型、布尔型、指针型,自定义类型等。

类型的不同:

类型名称:int(整型)、long(长整型)、double(浮点型)
输入输出的格式化:%d、%ld、%lf
所表达的数的范围:char < short < int < float < double
内存所占据的大小: 1字节到16字节
内存中的表达形式:二进制数(补码)、编码

1、整型

C语言的整型类型有char、short、int、long int、long long int五种数据类型。

这里需要注意,在 C/C++ 中,char是字符型数据类型,但char 本质上是一种整型数据类型。它占用 1 字节的内存,可以存储一个字符的 ASCII 码值。由于字符在计算机中是以整数形式存储的,char 可以直接参与整数运算。

这里介绍一个运算符sizeof()来访问不同数据类型所占用的内存
sizeof是一个运算符,给出某个类型或变量在内存中所占据的字节数(一字节8比特)

  • sizeof(int)
  • sizeof(i)
int main()
{
	int a;
	a = 6;
	printf("sizeof(int)=%d\n", sizeof(int));
	printf("sizeof(a)=%d\n", sizeof(a));
}

在这里插入图片描述
sizeof是静态运算符,它的结果在编译时刻就决定好了,所有不在sizeof中做计算,这个计算是不会做的
![在这里插在这里插入图片描述

数据类型占有的内存与这个类型的存储的范围息息相关,所以了解数据在计算机内部是如何表达以及储存范围是至关重要的,这可以解决数据存储越位的问题。

内部表达与存储范围
接下来要介绍整数的内部表达以及储存范围,这里主要以char类型来讲解

计算机内部的一切都是二进制数据(0或1组成的数据串)例如:
18 —> 00010010 、 0—> 00000000
那负数整型数据在计算机中如何用二进制表示呢?(-18 --> ?)
在十进制中,负数是用“-”来表示的,而计算机中是用不同的方式表示 —— 补码

补码

在说补码之前,我们先了解一些基本知识,上面提到:“一字节等于8比特1”,比特是二进制数据的位,即一字节可以存放八位的二进制数据(00000000 ~ 11111111 —> (0 ~ 255)255=27+26+25+24+23+22+21+20

补码(Two’s Complement)是一种用于表示有符号二进制数的编码方式,广泛应用于计算机系统中。补码的核心优势在于可以统一处理加减法运算,无需额外硬件判断符号位。
我们想要知道的是负数的表现形式,就拿-1来说,我们都知道-1 + 1 = 0 ,所以转换成二进制想要有的效果-1(未知表达形式) + 00000001 = 00000000,但我们要知道,我们是以char型来讨论,char只有一字节即八比特,可以存放八位二进制数据,超过八位的数据将视为越位,超过八位的数据将会被丢弃。所以会想到00000000是不是有可能越位了,即:00000001 + 11111111 = (1)00000000,为了验证这一猜想,我们还可以
用 0 - 1 = -1 的形式来验证:(1)00000000 - 00000001 = 11111111,和上面的形式一样,则-1用二进制补码表示为11111111,所以补码的意义就是拿补码和原码可以加出一个越位的“零”((1)00000000),这样就得到了负数在二进制中表示的形式了
所以单纯看二进制数据,11111111是255,但对于补码来说,11111111是表示-1,
即:对于-a来说,其补码为0-a,实际是2n- a(n表示这种类型的位数)

以上理解后,可以学习另一个计算补码的方法,以上方法也可以求得补码,但以上方法是为了方便理解,另一个方法便于计算:(绝对值源码的反码加一)

  • 正数的补码:与原码相同,例如,+5的8位补码为 00000101。
  • 负数的补码,通过以下步骤计算:
    • 取绝对值的二进制表示(原码)
    • 按位取反(反码)
    • 末位加1
  • 例如,-5的8位补码计算过程:
    绝对值原码:0000 0101
    按位取反:1111 1010
    末位加1:1111 1011(最终补码)

按位运算符

补码部分提到取反运算符,接下来讲解按位运算符,按位运算符是C语言中直接对整数类型的二进制位进行操作的运算符。它们适用于char、int、short、long等整型数据(包括有符号和无符号类型),但不适用于浮点类型。按位运算常用于底层编程、硬件操作、数据压缩等领域。
按位或(|)、按位与(&)

  • 按位与(&)
    对两个操作数的每一位进行逻辑与操作。仅当两个对应位均为1时,结果位才为1。
  • 按位或(|)
    对两个操作数的每一位进行逻辑或操作。只要有一个对应位为1,结果位即为1。
    //&
    char a = 9 & 7;
    //9的八位二进制:0000 1001
    //7的八位二进制:0000 0111
    //9&7 = 0000 0001 = 1
    
    //|
    char b = 9 | 7;
    //9|7 = 0000 1111 = 15
    

按位取反(~)、按位异或(^)

  • 按位异或(^)
    对两个操作数的每一位进行逻辑异或操作。当对应位不同时,结果位为1;否则为0。
  • 按位取反(~)
    对操作数的每一位进行逻辑取反操作(0变1,1变0)。
    //^
    char c = 9 ^ 7;
    // 9 ^ 7 = 1111 0001 = 
    //~
    char d = ~7;
    // ~7 = 1111 1000 = -8
    

左移(<<)、右移(>>)

  • 左移(<<)
    将操作数的二进制位向左移动指定的位数,低位补0。左移一位相当于乘以2。
  • 右移(>>)
    将操作数的二进制位向右移动指定的位数。对于无符号数,高位补0;对于有符号数,高位补符号位(算术右移)。右移一位相当于除以2(向下取整)。
    char e = 9 << 2;
    // 9 << 2 = 0000 1001 --> 0010 0100(低位补0,相当于 9 * 2²)= 36	
    char f = 9 >> 2;
    // 9 >> 2 = 0000 1001 --> 0000 0010
    

在这里插入图片描述

整数越界与取值范围

从上面补码的学习我们了解到了负数在二进制的表达形式,接下来我们要了解整型的取值范围,还是以char为案例讲解
在这里插入图片描述
上图表示的是char类型的取值范围,从0开始升序,并且求出对应的补码形式,在这里我们可以发现一个现象,就是在0和2n-1两个数的原码和补码是相同的,对于0和-0,我们倾向于使用0,所以省略-0(此处-0的表示为了更好理解)
1~2n-1-1的数都对应的补码,这里由于0的存在,若最后的+2n-1这个是保留的,可以从上图比对出来左右不平衡(左边多出一个0),所以要保留-2n-1,这样表示的数才平衡。
所以可以推导出整型数据类型的取值范围为-2n-1 ~ 2n-1-1(n表示这种类型的位数(比特)),所以当储存的数据超过了对应类型的取值范围就是整数越界,这样数据的错误使用会导致程序不能达到理想的结果甚至程序崩溃

unsigned(无符号操作符)

在一些特殊的情况下,数据为非负数,这个时候不需要使用整型范围的负数部分,为了扩大整型数据类型在非负数的储存范围,可以通过unsigned修饰数据类型,是将有符号数据类型转变为无符号类型,即非负数整数数据类型,最直白的说,无符号数据类型不需要进行补码,纯粹地表达二进制数,unsigned的初衷并非扩展数据的储存范围,而是为了做纯二进制运算,主要是为了位移(通过按位运算符操作)

unsigned修饰的整型取值范围 以char为例
在上面整型取值范围中,我们以char为例子,char表示的是八位二进制数据,unsigned表示的是无符号的数据类型,即纯二进制数,所以unsigned char 的取值范围为00000000 ~ 11111111 —> (0 ~ 255)255=27+26+25+24+23+22+21+20
在这里插入图片描述

2、浮点型

浮点型是储存浮点数的数据类型,浮点数在C语言中用于表示实数(带小数点的数),遵循IEEE 7542标准。常用类型包括:

  • float:单精度浮点,占用4字节,精度约6-7位小数。
  • double:双精度浮点,占用8字节,精度约15-16位小数。
  • long double:扩展精度,占用通常为10字节或更多,具体取决于编译器。

主要介绍float和double,long double 初学用处不多

类型字节位数范围有效数字输入输出
float432±(1.20x10-38~3.40x1038),0,±inf,NaN7%f%f,%e
double864±(2.2x10-308~1.79x10308),0,±inf,NaN15%lf%f,%e

范围图:
请添加图片描述
解释
inf无穷大(infinity),超出表示范围的正/负数。
NaN:非数(not a number),无效运算结果,也叫不存在的数(如0/0)。
上图可知,浮点型表达的范围有限,有效范围可以表示的数叫做规格化数,非规格化数填补0和极小值之间的空白

科学计数法

C语言支持用科学计数法为变量赋值,在数学中,为了简洁,科学计数法经常用于非常大或非常小的数字,一般-1.23x1020这样的表述,在C语言中略微不同,用%e输出的值是以科学计数法表示的数
请添加图片描述在这里插入图片描述

输出精度

%f之间加上.n可以指定输出小数点后几位数,这样的输出是做4舍5入的
在这里插入图片描述
上图中,保留小数点后30为数的输出,不是想象中的形式(0.005200000000000000000000000),而是离散的数据(- 0.005199999999999999761302049706
),所以我们可以知道计算机中的浮点数数据是离散的,对于数学来说,任意两个之间有无数个数,但对于计算机来说,最终只能用离散的数字来表达这个数
请添加图片描述
这就是为什么double相对于float来说更加精确,但一些数在double里面可能还是不准确的

这里的舍入规则不是四舍五入原则,而是更加精确的四舍六入五留双3原则

规则详解:
四舍六入:当需要保留的位数后一位数字小于5时直接舍去;大于5时进位。
五留双:当需要保留的位数后一位数字等于5时,
若5后的数字不全为0,则进位;
若5后的数字全为0,则检查保留位数的最后一位数字是否为奇数,奇数则进位,偶数则舍去。
具体案例
以保留小数点后两位为例:
1.234 → 1.23(4 < 5,舍去)
1.236 → 1.24(6 > 5,进位)
1.2350 → 1.24(5后全为0且保留位3为奇数,进位)
1.2450 → 1.24(5后全为0且保留位4为偶数,舍去)
1.2351 → 1.24(5后不全为0,进位)

浮点数的内部表达

浮点类型的储存方式不同于整型,浮点数由三部分组成:符号位、指数部分和尾数部分
以double内部为例:

在这里插入图片描述

整数在计算机内部是纯二进制数,而浮点数的内部是一种编码形式,最左边一个bit用来表示正负号,其次的11个bit用来表达指数(一个数的次方)部分是多少,剩下的52bit用来表示分数部分是多少,计算机cpu内部会有专门进行浮点数计算的硬件,浮点数计算先将这个浮点数的编码传到这个硬件,这个硬件会将这个编码拆开并运算,运算完后再组成这样的编码,输出

注意

  • 如果没有特殊需要,只使用double。
  • 现代CPU能直接对double做硬件运算,性能不会比float差,在64位的机器上,数据存储的速度也比float慢。
    -在对浮点型赋值时,编译器默认浮点数为double型数据,所以在对float赋值时,要在赋值数据后加上f/F后缀表明身份。
    double d = 0.52;	//0.52默认为double型
    //float f = 0.52;	错误,double赋给float
    float f = 0.52f;	//0.52f为float型
    
  • 输出为inf表示超过范围的浮点数±∞
    输出为nan表示不存在的浮点数

3、字符型

字符型(char)是C语言中用于存储单个字符的基本数据类型,占用1字节(8位)内存空间。其值可以是ASCII字符或转义字符,范围通常为-128到127(有符号)或0到255(无符号),一般用于对单个字符的操作。

为什么是char?
我们可以自己通过宏定义的方式规定一些数字来代表某个字符,为了规范字符的使用,国际上,英语字符统一以ASCII码为标准,ASCII码是一个7位编码表,只能表示128个字符,即储存字符的数据类型只用储存128个整数,用一个字节储存即可,所以使用char型最合适,应为char型是8位内存的数据类型,可以减少资源的浪费。

ASCII(American Standard Code for Information Interchange,美国信息互换标准代码)是一套基于拉丁字母的字符编码,共收录了 128 个字符,用一个字节就可以存储,它等同于国际标准 ISO/IEC 646。

ASCII码表

ASCII 控制字符

十进制符号中文解释十进制符号中文解释
0NULL空字符16DLE数据链路转义
1SOH标题开始17DC1设备控制1
2STX正文开始18DC2设备控制2
3ETX正文结束19DC3设置控制3
4EOT传输结束20DC4设置控制4
5ENQ询问21NAK拒绝接收
6ACK收到通知22SYN同步空闲
7BEL23ETB传输块结束
8BS退格24CAN取消
9HT水平制表符25EM介质中断
10LF换行键26SUB替换
11VT垂直制表符27ESC换码符
12FF换页键28FS文件分隔符
13CR回车键29GS组分隔符
14SO移出30RS记录分离符
15SI移入31US单元分隔符

ASCII 可打印符

十进制符号中文解释十进制符号中文解释
32空格80P大写字母P
33感叹号81Q大写字母Q
34"双引号82R大写字母
35#井号83S大写字母S
36$美元符84T大写字母T
37%百分号85U大写字母U
38&86V大写字母V
39单引号87W大写字母W
40左括号88X大写字母X
41右括号89Y大写字母Y
42*星号90Z大写字母Z
43+加号91[左中括号
44,逗号92|反斜杠
45-连字号或减号93]右中括号
46.句号或小数点94^音调符号
47/斜杠95_下划线
480096`重音符
491197a小写字母a
502298b小写字母b
513399c小写字母c
5244100d小写字母d
5355101e小写字母e
5466102f小写字母f
5577103g小写字母g
5688104h小写字母h
5799105i小写字母i
58:冒号106j小写字母j
59;分号107k小写字母k
60<小于108l小写字母l
61=等于109m小写字母m
62>大于110n小写字母n
63问号111o小写字母o
64@电子邮件符号112p小写字母p
65A大写字母A113q小写字母q
66B大写字母B114r小写字母r
67C大写字母C115s小写字母s
68D大写字母D116t小写字母t
69E大写字母E117u小写字母u
70F大写字母F118v小写字母v
71G大写字母G119w小写字母w
72H大写字母H120x小写字母x
73I大写字母I121y小写字母y
74J大写字母J122z小写字母z
75K大写字母K123{左大口号
76L大写字母L124垂直线
77M大写字母M125}右大括号
78N大写字母N126~波浪号
79O大写字母O127删除

字符的输入输出(缓冲区)

输入
C语言中,以字符输入,用%c格式说明符,输入通常有两个方式输入,一是直接以字符输入,二是输入对应字符的ASCII码值,再把这个ASCII值赋值给char类型。
输出
C语言输出单个字符也用%c格式说明符

char c;
scanf("%c",&c);
int i;
scanf("%d",&i);
c = i;

printf("%c",c);

在这里插入图片描述
混合输入

scanf("%d %c",&i,&c);
scanf("%d%c",&i,c&);

两个写法的区别是什么呢?
若scanf中%d后面不加空格:直到读到整数结束为止,后面的字符直接输入给%c(包括空格‘ ’)
若scanf中%d后面加空格:整数部分不仅仅是读完整数,还连带着后面的空格一并数取(但不输入int中,因为空格是字符,这样做的目的是要区分开输入的数据,防止错误输入),若输入时后面没加空格,后面的字符直接读入
在这里插入图片描述

我们循环输入三次来具体分析

  • 第一次循环,中间有空格的输入,12 1,中间有空格12输入到 i 中,字符’1’输入到c中,中间无空格的输入,12a,到非数字之前的数都输入到整型中,之后的第一个字符输入给字符型,12输入到 i 中,字符’a’输入到c中。
  • 第二次循环,中间有空格的输入,12 1(中间有两个空格),12输入到 i 中,字符’1’输入到c中,空格结束之后的第一个字符将被输入,中间无空格的输入,12 1,注意空格也是字符,所以空格之前的数将被输入到 i 中,空格将被输入到c中。
  • 第三次循环,中间有空格的输入,12a,没有空格,直接将第一个字符出现之前的数输入到 i 中,字符’a’输入到c中,中间无空格的输入,12 1 空格是第一个出现的字符,所以12输入到 i 中,空格输入到c中

综上,混合输入相对复杂,把理论本质了解,具体情况要求,具体方法使用,了解即可,

补充
上图代码中有个语句getchar(),你知道这个语句在这里的作用吗?不知道没关系,我具体讲解,getchar是另一个输入函数,这个语句专属于读取字符,在具体解析getchar之前,我们要提到一个新概念;
我们输入数据,是通过键盘输入,但输入函数不是直接通过键盘来读取我们的数据,在键盘和调用输入函数之间还有个区域——缓冲区,输入函数先来看缓冲区是否有数据,如果有,它就直接读取缓冲区的数据,不需要再从键盘输入,如果缓冲区什么都没有,则需要从键盘输入吗,再拿走;
再讲到getchar(),getchar()输入函数在缓冲区找到一个字符,返回一个字符的ASCII码值,所以一般需要有字符型变量来接收数据,但是这里是特殊情况,在中间无空格输入中,输入的数据是12(空格)1,在缓冲区有12(空格)1这样的数据,scanf(“%d%c”,&i,&c);在缓冲区读取了12给整型,把空格读取给字符型,此时缓冲区还剩下1这个数据,进入下一循环继续输入12 1;此时输入的结果和我们预期的不同,这是为什么呢?
在这里插入图片描述
先说结论,因为缓冲区没有清除干净导致的错误,进入第二循环,缓冲区有数据1,scanf(“%d %c”,&i,&c)输入函数查看缓冲区,发现有数据1,将数据1读取给 i ,继续输入发现没有数据,继续从键盘获得新数据12(空格)1输入到缓冲区,此时缓冲区的数据为12(空格)1,数据1将以字符’1’的形式读取给字符型,在接下来的scanf(“%d%c”,&i,&c)输入函数查看缓冲区,发现缓冲区内有数据直接读取,将2读取到整型,空格读取到字符型中;
那如何修改呢?很显然要用到我们的getchar函数来清除缓冲区,所以getchar输入函数不仅有输入字符数据的功能,还要清空缓冲区的功能。

字符计算

一个字符加一个数字得到ASCII码表中那之后的字符,两个字符相减得到它们在表中的距离
在这里插入图片描述

字符计算的运用可以实现英语字母的大小写转换,字母在ASCII码中是顺序排序的,且大写与小写并不是排列在一起的,通过’a’-'A’可以得到两段之间的距离,
则大写字母转换为小写字母的方法是a+‘a’-‘A’,
小写字母转换为大写字母的方法是a+‘A’-‘a’

逃逸字符

在ASCII码表中,前0~31是ASCII的不可打印字符,也是控制字符,我们在编写程序的过程中,想要使用一些控制字符,但调用ASCII码值又太麻烦,所以有了逃逸字符来表达控制字符或特殊字符,逃逸字符是由一个‘\’字符开头后面在跟上一个字符,这两个字符组合起来,组成一个逃逸字符

字符意义字符意义
\b回退一格\"双引号
\t到下一个制表位\’单引号
\n换行\\反斜杠本身
\r回车

以上是用到比较多的逃逸字符

  • 这里\b是回退到前一格,若\b后面未有数字补充,这不会被替换,若有数字跟上,这个数字会覆盖原来的数字
  • 回车\n和换行\r是源于打字机的使用,当打字过程中,字车会随打字的进行而移动,当写完一行后,要把字车拉回,这过程叫回车,而换行是在打字机单项棘轮机构完成,它会使纸张往上移动,达成换行的过程,而计算机把这两个步骤直接合并成立回车键enter键
    在这里插入图片描述

讲讲终端,终端其实就是程序运行出来的那个黑色框框,终端(shell)本身就是语言制作人做的程序,终端在背后会执行我们写的程序。
我们的键盘和终端联系,我们眼镜看到的也是终端发送显示的。
终端把键盘上输入的信息输入到我们的程序,而程序输出的信息通过终端发送给我们的眼镜,也就是展现在我们电脑屏幕上。
在这个过程中,终端不是那么诚实的,终端会进行翻译,它会对我们程序中的一些操作进行处理,如:回退、回车、程序中的逃逸字符(也叫控制字符(\“、\n、\t等)),对这些事物进行处理,让它们变成我们可以看得见的结果。
请添加图片描述

自动与强制类型转换

自动类型转换
当运算符的两边出现不一致的类型时,会自动转换成范围更大的类型,为什么是选择更大的类型呢?在做运算时,加乘运算符的结果可能超过小类型的范围;对于printf,任何小于int的类型会转换成int,float会被转换成double

强制类型转换
当需要把一个量强制转换成另一个类型(通常是较小的类型),需要通过 (类型)值 的方式实现

如:

int i = (int)10.2;
short s = (short)32;
但是要注意安全性,小类型不一定能表达大的数据
short s1 = (short)32768;  //错误

注意:强制类型转换优先级高于四则运算

4、布尔型

在C语言中,布尔型(bool)并非原生类型,而是通过标准库 <stdbool.h>引入的。需包含该头文件后,方可使用 booltruefalse关键字;在计算机中,我们用什么数据类型来表示一句话的对错?一个条件的成立与否?布尔型可以解决,布尔类型只有两个值,即ture(真,通常为1)false(假,通常为0),一般用于逻辑判断

#include <stdio.h>
#incldue <stdbool.h>
int main()
{
	bool b1 = 0;
	bool b2 = 1;
	if(b1)
	{
		printf("假\n");
	}
	else if(b2)
	{
		printf("真\n");
	}
}

5、其他

指针类

指针是C语言学习的核心,这里简单提到指针,因为后面会仔细讲解指针,指针类一开始学会感觉比较抽象,这里为了理解,想到一个相符的场景

我们可以把计算机内存想象为一个大酒店,计算机每块内存都会有专属于当前位置的地址,每块内存相当于酒店房间,地址相当于房间门牌号,而指针变量是用于存储其他变量或函数的地址,相当于房间的钥匙,在编写过程中,我们可以通过指针来调用对于变量或函数,对其进行修改或使用, 相当于客户使用钥匙进入酒店房间。

这里只是稍微提及指针,想要具体学习,可以跳转到指针大类。

自定义数据类型

自定义数据类型的特性就是自定义,有开发者定义的数据类型,C语言使用的是结构体,在C++中会有的概念
可以在自定义数据类型中定义多个别的类型为一体
这块后面也会细讲,这里稍微提及。

6、总结 输入输出格式说明符%

类型对应格式说明符

类型名输入输出
char整型或字符型:%d%c整型或字符型:%d%c
short%hd%hd
int%d%d
long%ld%ld
long long%lld%lld
八进制%o%o
十六进制%x%x
float%f%f %e
double%lf%f%e
(类型)*%p%p

程序流程结构

1、顺序结构

顺序结构是程序设计中最基础的流程控制方式,指代码按照书写顺序逐行执行,没有分支或循环的跳转。这种结构逻辑简单,适合解决线性任务。

特点

  • 线性执行:代码从上到下依次运行,每条语句执行一次。
  • 无跳转:不包含条件判断(如if)或循环(如for)等控制语句。
  • 依赖性:前一条语句的执行结果可能影响后续语句。
#include <stdio.h>
int main()
{
	int a = 0;
	a = 12+23;
	printf("12+23%d",a);	//这样的简单程序就是顺序结构
	retrun 0;
}

优点

  • 逻辑清晰,易于理解和测试
  • 适合处理步骤明确且无分支的任务
    缺点
  • 灵活性低,无法应对复杂逻辑需求
  • 冗余可以较高,需重复代码处理相似操作

2、选择结构

依据条件判断是否满足,有选择的执行相应的功能

(1)if语句(if 、if-else、if嵌套、三目运算符)

有三种形式:

  • 单行格式if语句
    语法:if(条件) { 条件满足执行此块语句 }
    (条件满足(非0数据)就执行代码,不满足(数据为0)直接跳过)

ps:这里条件判断就会使用到上面学习的比较和逻辑运算符的知识。

模拟情况:用户输入分数,如果分数大于600,视为考上一本大学,在终端上输出

#include <stdio.h>
int main()
{
	int score = 0;
	printf("请输入一个分数(单行)\n");
	scanf("%d",&score);//用户输入分数
	printf("您输入的分数为:%d\n",score);//打印用户输入的分数
	if (score > 600) //判断分数是否大于600,如果大于,那么输出
	{
		printf("恭喜你考上了一本大学\n");
	}
}
  • 多行格式if语句
    通过else(有其他,别的,另外的意思)来延长if的条件判断,在逻辑判断中比较常用,比较灵活

    • 若判断条件结果只有两个可以直接使用else,例如:
    #include <stdio.h>
    int main()
    {
    	int score = 0;
    	printf("请输入一个分数(单行)\n");
    	scanf("%d",&score);//用户输入分数
    	printf("您输入的分数为:%d\n",score);//打印用户输入的分数
    	if (score > 600) //判断分数是否大于600,如果大于,那么输出
    	{
    		printf("恭喜你考上了一本大学\n");
    	}else
    	{
    		printf("继续努力!\n");
    	}
    }
    

    上面例子中,条件 score > 600 只有两种可能,大于或小于,如果大于条件不成立,那么剩下的只能 是小于的结果,所以else不需要条件

    • 若判断条件结果有两个以上的结果,使用else要添加条件判断,
      例如:
    //输入一个考试分数,
    //如果大于600分,视为考上一本大学,在终端输出
    //	  大于500,视为考上二本大学,在终端输出
    // 	  大于400,视为考上三本大学,在终端输出
    // 	  小于等于400分,视为为未考上本科
    #include <stdio.h>
    int main()
    {
    	int score = 0;
    	printf("请输入一个分数(多条件)\n")
    	//cin >> score;
    	printf("您输入的分数为:%d",score);
    	if (score > 600) {
    		 printf("恭喜你考上了一本大学\n");
    	}
    	else if (score > 500) {
    		printf("恭喜你考上了二本大学\n");
    	}
    	else if (score > 400) {
    		printf("恭喜你考上了三本大学\n");
    	}
    	else {
    		printf("你没有考上本科\n");
    	}
    }
    
  • 嵌套if语句
    嵌套的意思是,在一个大盒子里继续放小盒子,根据极限思想,可以无限嵌套,所以在if条件判断中用嵌套可以达到更加精确的条件判断
    在上面的分数判断情景中,在加一个判断,在一本分数中,如果大于700分,考入清华,大于650分,考入北大,大于600分,考入人大

    #include <stdio.h>
    int main()
    {
    	int score = 0;
    	printf("请输入一个分数(嵌套if)\n")
    	//cin >> score;
    	printf("您输入的分数为:%d",score);
    	if (score > 600) {
    		 printf("恭喜你考上了一本大学\n");
    		 if(score > 700)
    		 {
    		 	printf("恭喜你考上了清华\n");
    		 }else if(score > 650)
    		 {
    		 	printf("恭喜你考上了北大\n");
    		 }else
    		 {
    		 	printf("恭喜你考上了人大\n");
    		 }
    	}
    	
    }
    
  • 三目运算符(条件运算符)
    上面的条件判断代码比较多,在一定的情况中,可能需要做一些简单的条件判断,继续用上面的写法写,一是会影响代码美观,二是会影响代码的阅读,所以三目运算符就可以很好的解决
    语法:表达式1(条件) ? 表达式(条件为真的执行) : 表达式3(条件为否的执行)
    解释:

  • 如果表达式1的值为真,执行表达式2,并返回表达式2的结果

  • 如果表达式1的值为假,执行表达式3,并返回表达式2的结果

    注意:真假判断是依据这个值是否是0,非零数为真,0为假,通过这个规则也可以知道,逻辑判断的结果也只要0或(a > b 、200 > 100、a != 100 )

    int a = 10;
    int b = 20;
    int c = 0;
    c = (a > b ? a : b);	//三目运算符返回的是变量,还可以继续赋值
    printf("c=%d\n",c);
    
    (a > b ? a : b) = 100;	//这里三目运算符返回的是一个 b变量,
    printf("a=%d\n",a);		//即(a>b?a:b)这个的结果为 b,然后 b=100进行赋值
    printf("b=%d\n",b);
    

(2)switch语句(switch-case)

switch是执行多条件判断的分支语句
语法:(以电影评分为例)

#include <stdio.h>
int main()
{
	int score;
	printf("请给电影打分:\n");
	scanf("%d",&score);
	switch(score)//当和表达式的结果匹配上时,就会跳转到对应的入口地址处
	{
	case 10:
		printf("您认为是经典电影\n");
		break;	//break有跳出一个函数,一个循环的作用		
		//主要是跳出作用
	case 9:
		printf("您认为是经典电影\n");
		break;//退出当前分支
		
	case 8:
		printf("您认为是非常好的电影\n");
		break;
		
	...	//此处省略
	
	case 5:
		printf("您认为是非常好的电影\n");
		break;
		
	default://此语句作用是“以上结果都没有满足表达式的结果,没满足的条件就跳转到此语句,优点类似于if-else中else的作用
		printf("您认为是烂片\n");
		break;
	//default(违约,不履行),可以和英文单词意思辅助理解
	}
}

(3)ifswitch的优缺点

  • if
    • 优点:条件判断可以进行一个区间的判断或个别值的判断,支持使用复杂的逻辑判断,灵活度高。
    • 缺点:代码结构可能冗长,尤其在多分支情况可读性较差,每次条件都需要逐一执行,性能低下
  • switch
    • 优点:代码结构清晰,适合多分支固定值匹配的场景。执行效率高(通常通过跳转表实现),可读性强。
    • 缺点:仅支持常量值匹配(如 case 1:),无法直接处理范围或复杂逻辑。必须配合 break 避免穿透,忘记写 break 会导致意外行为。

3、循环结构

循环结构是编程中用于重复执行特定代码块的控制结构,适用于需要重复操作的场景。
当程序运行到循环,首先会进行条件判断,条件为真,进入循环,执行完函数体后再次判断(执行完),条件为假,离开循环,继续执行后面语句
在这里插入图片描述
根据上图,我们可以初步了解循环的运行流程,从这里我们要知道,当循环条件满足,在循环体中要有可以改变条件中的变量的语句,不然会变成死循环,无法跳出,但死循环不一定都是坏的,前提是要使用跳出语句,所以循环结构和跳转语句的学习是捆绑掌握的

总之,使用循环的要点有:

  • 确保循环条件最终能变成false(假) 或者使用跳转语句。
  • 包含循环条件中的变量修改逻辑,如i++。
  • 了解清楚循环的嵌套逻辑。

(1)while循环

while的英语翻译是“当”,那么一个while循环的意思是:当条件满足时,不断重复循环体内的语句,循环执行之前判断是否继续循环,所以有可能循环一次也没有被执行,所以条件成立是循环继续的条件。
语法:
while ( 循环条件) { 循环语句 }
只要循环条件的结构为真,就执行循环语句

//打印0~9的数字
int e =0;
while(e < 10)
{
	printf("%d ",e++);//e++改变了条件中的变量,当e=10时候退出循环
}

在这里插入图片描述

(2)do-while循环

do-while循环和while循环最直观区别就是多了一个do,do(做)表示行动,所以,do-while循环是先执行循环体,再做判断;与while循环不同就是do-while循环可以保证循环体至少执行一次。即使条件初始即为false
关键特点:

  • 后置条件检查:循环体执行后才检查条件,因此第一次循环无需条件满足。
  • 至少执行一次:适用于必须执行操作再验证结果的场景,如:输入校验,初始化操作
    语法:
    do{ 循环语句 } while(条件判断);
    接下来,以一个猜数字小案例来理解
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
	srand((unsigned int)time(NULL));
	int Number = rand()%100+1;  //%n->[0,n-1]的随机数,%n+1->[1,n]的随机数
	int a = 0;	   //储存用户数据
	int count = 0; //计数
	printf("我已经想好了一个1~100的数!\n");
	do{
		printf("请猜这个1~100的数:");
		scanf("%d",&a);
		count++;
		if(a > Number){
			printf("你猜的数大了!\n");
		}else if(a < Number){
			printf("你猜的数小了!\n");
		}
	}while(a != Number);
	printf("你用来%d次就猜到了答案!\n",count);
}

在这里插入图片描述

这里是do-while循环检查校验的应用,若使用while循环,在进入循环之前,我们还要先获取用户输入的数据,但在while循环中我们也要获取用户输入的数据,所以while循环解决这个案例时,有重复书写,完全可以用do-while更好的解决,所以do-while和while要看案例情况来使用

(3)for循环

for循环像一个计数循环:设定一个计数器,初始化它,然后在计数器到达某值之前,重复执行循环体,而每执行一轮循环,计数器值以一定步骤进行调整,比如加1或减1
语法:for(起始表达式;条件表达式;循环体末尾语句){循环语句}
例子:求n!= 1x2x3x…xn

int n;
scanf("%d",&n);
int fact =1;
for(int i=1;i<=n;i++)
{
	fact*=i;
}
printf("%d! = %d",n,fact);
-----------------------------
		||	等价于	
		\/
int n;
scanf("%d",&n);
int fact =1;
int i= 1;
while(i<=n)
{
	fact*=i;
	i++;
}
printf("%d! = %d",n,fact);

在这里插入图片描述
从上面例子,也可以看出来,for循环起始等价于while循环,但为什么要有for循环呢?为了方便,在解决已知循环次数的循环,通过初始化、条件判断、迭代语句的方式控制循环,可以快速做好循环的设置,之后代码只写需要执行的语句,控制循环变量的迭代操作明显,减少失误,所以以后遇到已知循环次数的循环,就用for循环解决问题,必须执行一次的循环用do-while循环,其他不确定循环次数或特定情况循环的循环使用while解决

循环次数
循环的控制变量i是选择从0开始还是从1开始,是判断i<n,还是i <=n,这都对循环的次数,循环结束后变量的值有影响。

  • for(int i = 0; i < n; i++)
    这个循环的次数为n次,循环结束时i等于n
  • for(int i = 0;i <=n;i++)
    这个循环的次数为n+1次,循环结束后i等于n+1
  • for(int i =1;i < n ;i++)
    这个循环的次数为n-1次,循环结束的时i等于n
  • for(int i =1;i<=n;i++)
  • 这个循环的次数为n次,循环结束后i等于n+1

(4)嵌套循环

嵌套循环就是在一个循环的基础上,在其循环体内再定义循环,嵌套循环的使用伴随着一些问题,一些变量的定义时机,对多层循环的解读
使用嵌套循环需要注意两点:
一是嵌套循环之间的循环控制变量是不一样的(一般使用i、j、k等字母)
**二是嵌套循环可以随机组合上面的三种循环不受制约,只要能确保使用情景正确。
例子:打印矩阵星图

for(int i =0;i < 10;i++){//外层循环
	for(int j =0;j<10;j++){//内层循环
		printf("* ");
		if(j==9){	//内层循环最后一次循环j=9
			printf("\n");
		}
	}
}
//分析:
//嵌套循环本来就是一包含的关系,外层循环包含着内层循环,外层循环的每次循环中都有一个以j为控制变量,循环十次的循环,
//即i=0时,做一个for(int j =0;j<10;j++)循环,
//i=1时,做一个for(int j =0;j<10;j++)循环,
//i=2时做一个for(int j =0;j<10;j++)循环,
//...以此类推
//综上,只要把握好循环的层级关系,以循环的定义思考即可解决

在这里插入图片描述


跳转语句

跳转语句通常在循环、函数中使用,具体解释4个语句:
break语句、continue语句、goto语句、return语句。

(1)break语句

break语句是一个控制语句,用于立即停止所在的循环或switch语句的执行。它通常用于在满足某些条件时提前退出循环或跳出switch语句的某个分支。
作用:用于跳出选择结构或循环结构

break语句在循环中使用
break可以用于for、while和do-while循环中。当break被执行时,程序会;立即终止当前循环,并继续执行循环之后的代码

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
	srand((unsigned int)time(NULL));
	int Number = rand()%100;  //%n->[0,n-1]的随机数,%n+1->[1,n]的随机数
	for(int i =0; i <10;i++){
		for(int j=0;j<10 ;j++){
			if((i*10+j) == Number)
			{
				printf("Find Number %d",(i*10+j));
				break;
			}
		}
	}
}

break 在switch-case中使用
break在switch条件语句中,作用是终止case并跳出switch条件语句

printf("请选择副本难度\n1、普通\n2、中等\n3、困难\n"
int select = 1;
//scanf("%d",&select);
switch (select)
{
case 1:
	printf("您选择的是普通难度\n"); 
	break;
case 2: 
	printf("您选择的是中等难度");
	break;
case 3:
	printf("您选择的是困难难度");
	break;
default:
	break;
}

break的注意事项

  • break只能终止当前所在的循环或switch语句。如果有多层嵌套循环,break只会终止当前所在的循环层级
  • 在switch语句中,break不是强制性的,但 通常建议使用以避免逻辑错误
int main()
{
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            if (j == 1) {
                break; // 只终止内层循环
            }
            printf("i=%d, j=%d\n", i, j);
        }
        /*if (i == 1)
        {
            break;
        }*/
    }

}

在这里插入图片描述在这里插入图片描述

(2)continue语句

在C语言中,continue语句用于跳过当前循环中剩余的代码,直接进入下一次循环的条件判断。它仅适用于循环结构(for、while、do-while),不用于switch或其他控制语句。
比如:输出一段数据,但不输出偶数

for(int i =0; i < 10;i++)
{
	if(i%2 == 0)
	{//跳过偶数
		continue;//筛选条件,执行到此就不再向下执行,执行下一次循环
		//break 会退出循环,而continue不会
	}
	printf("%d ", i); // 仅打印奇数
}

continue与break的区别

  • continue仅跳过当前迭代(当前这次循环),循环仍会继续执行后续迭代(执行下一行循环)。
  • break会直接终止整个循环,不再执行后续任何迭代。

(3)goto语句

goto 是 C 语言中的一种无条件跳转语句,允许程序直接跳转到指定标签(label)处继续执行。标签由标识符和冒号组成,例如 label_name:。语法格式为:

goto label_name;
//....
label_name:
//代码块

解释:如果标记的名称存在,执行到goto语句时,会跳转到标记的位置
使用场景:

  • 跳出多重循环
    在嵌套循环中,直接使用break只能跳出当前层级循环,而goto可以一次性跳出所有循环
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (condition) {
                goto exit_loops; // 直接跳出所有循环
            }
        }
    }
    exit_loops:  
    // 后续代码  
    
    • 代替方式(函数形式)
    void loop()
    {
    	for (int i = 0; i < n; i++) {
       		for (int j = 0; j < m; j++) {
            	if (condition) {
                	return;// 直接退出循环函数
            	}
       		}
    	}
    }
    
  • 错误处理与资源清理
    在函数中需要集中处理错误时,goto 可以跳转到统一的错误处理逻辑,避免重复代码。
    if (error1) {
        goto cleanup;
    }
    if (error2) {
        goto cleanup;
    }
    cleanup:  
    

goto注意事项

  • 避免滥用
    goto 可能导致代码逻辑混乱(俗称“面条代码”),降低可读性,导致读者无法读懂。现代编程更推荐使用结构化控制语句(如 break、continue、函数返回return等)。

  • 禁止跨函数跳转
    goto 只能在同一个函数内跳转,不能跳转到其他函数的标签。

  • 初始化变量问题
    跳转时可能跳过变量初始化语句,导致未定义行为。例如:

    goto go_to;
    int x =10;//被跳过
    go_to:
    printf("%d",x);//未初始化,行为未定义
    

goto 在特定场景下(如资源清理、快速退出)有一定实用性,但应谨慎使用。优先考虑结构化控制语句(如 break、continue、函数返回return等),以提升代码可维护性。

(4)return语句(涉及函数)

在C语言中,return是用于从函数中返回值的语句。它可以终止当前函数的执行,并将控制权交还给调用者。return可以返回一个值(带表达式)或不返回任何值(仅用于结束函数)。
语法:

return expression;
//其中expression是可选的,具体取决于函数的返回类型
无返回值函数中的return

如果函数声明为 void 类型(即无返回值),return 可以不跟任何表达式,仅用于提前退出函数。

void printMessage(int count) {
    if (count <= 0) {
        return; // 直接退出函数
    }
    printf("Count= %d\n", count);
}
有返回值函数中的return

如果函数声明时指定了返回类型(非 void),则必须使用 return 返回相应类型的值。
比如:两数相加函数

int add(int a,int b)
{
	return a+b; //返回整数类型的计算结果
}
  • 注意事项
    • 返回值类型必须与函数声明一致,否则可能引发警告或错误。
    • main 函数的return 0 表示程序正常结束,非零值通常表示异常
    • 不可在 void 函数中返回具体值,例如 return 0; 会导致编译错误。

函数

我们通过一个案例来引入函数:

求出1到10、20到30和35到45的三个和
用已经学过的知识,我们会想到使用三个循环来得到三个和值。

#include <stdio.h>
int main()
{
	int i;
	int sum;

	for(i =1,sum=0; i<=10 ; i++)
	{
		sum += i;
	}
	printf("%d带%d的和是%d",1,10,sum);
	for(i =20,sum=0; i<=30 ; i++)
	{
		sum += i;
	}
	printf("%d带%d的和是%d",20,30,sum);
	for(i =35,sum=0; i<=45 ; i++)
	{
		sum += i;
	}
	printf("%d带%d的和是%d",35,40,sum);
	return 0;
}

上面的代码虽然可以解决问题,但是增加要计算的和值岂不是要一直添加循环代码,代码的内存岂不是非常大,“代码复制”是程序质量不良的表现,并且循环代码逻辑上是相同的,只是数据有不同,函数优化这一现象,细节以下讲解,先看优化后的代码——求和函数

#include <stdio.h>
void sum (int begin ,int end)
{
	int i;
	int sum = 0;
	for (i=begin; i < end; i++)
	{
		sum+=i;
	}
	printf("%d带%d的和是%d",35,40,sum);
}
int main()
{
	sum(1,10);
	sum(20,30);
	sum(35,45);
	return 0;
}

在这里插入图片描述

1、函数的定义

通过上面的例子,我们可以知道函数的作用,就是减少相同代码的重复编写,集合一个解决问题的方法,增强程序的逻辑联系。

所以什么是函数?
函数是一块代码,接收零个或多个参数做一件事情,并返回零个或一个值

  • 函数对参数和返回值没有一定要求,函数可以没有参数,但返回类型一定要确定

在C语言中,函数定义由返回类型、函数名、参数列表和函数体组成

请添加图片描述

函数定义是赋予这个集合有合法的内存占用与实际的使用意义,通过为函数命名,规范函数的返回类型,确定函数参数列表这些操作,为之后能准确无误的调用对应函数,而没有歧义做了重要准备

返回类型

  • 指定函数返回的数据类型(如int、float、void和自定义类型等)
  • 若函数无需返回值,使用void
  • 当函数需要返回值,返回值需要通过return语句传递,且类型必须与声明一致

函数名

  • 需要遵循C语言标识符规则(字母、数字、下划线,且不以数字开头)。
  • 通常采用小写字母和下划线的组合(如 calculate_sum)。
  • 注意不能和数据类型或自定义数据类型名重名

参数列表

  • 声明函数接收的输入参数,格式为数据类型 参数 ,多个参数用逗号分隔。
  • 若无参数,可写 void或留空(如 int func (void) 或 int func()

函数体

  • 包含实现功能的代码块,用{}包裹
  • 局部变量需在函数体内声明。

2、函数的调用

调用函数即使用函数,格式为 函数名(参数值)()在这里的作用就是调用函数,是调用函数的标志,在表示和调用函数有重要作用,即使没有参数也要也需要()

  • 若有参数:需要在()内给出正确数据和顺序以及数量,这些值会按参数列表的顺序依次为函数参数初始化
    运用上面求和函数为例子
    sum(1,10);
    sum(20,30);
    sum(35,45);
    求和函数是无返回值有参函数,以上数据会依次为函数参数初始化,进而在函数中使用这些参数
    
函数返回

函数返回需要使用return语句,return停止函数的执行,并送回一个值

  • 无返回值:return;
  • 有返回值:return 表达式;

无返回值类型
无返回值的函数 格式为 void 函数名 (参数表),不能使用带值的return语句,可以没有return语句,注意调用的时候不能做返回值的赋值

在上面的学习,我们知道程序是顺序结构,当运行到函数调用语句时,会跳转并运行对应函数,当函数运行完,函数知道每一次哪里调用它,函数运行完会返回到正确的地方(即函数调用位置,并继续顺序运行),可以抽象的理解,函数调用语句是一扇任意门,通过任意门会到另一块大陆(函数体),走完这块大陆会从任意门回到原来的大陆(主函数)

请添加图片描述
有返回值类型
函数一般只能返回一个值,更多的返回值方式需要使用指针,之后讲;这里只讲只放回一个数,当函数要返回一个值的时候,必须使用return语句来实现,返回格式为 return 表达式;返回值类型要与函数头返回类型一致。

int max(int a, int b)  //最大值函数
{
	int ret; // 储存最大值
	if(a>b)	//做比较
	{
		ret = a;
	} else {
		ret = b;
	}
	return ret; //返回最大值
}

这个最大值函数的返回结果往往会需要一个变量来承接与储存,即会为变量赋值,不然函数调用会无意义,为什么无意义在之后讲本地(局部)变量可以解决

int main()
{
	//max(10,100); //错误调用
	int Max = max(10,100);
}

函数调用的()填入的参数可以是数据,可以是变量,可以是函数返回值,可以是**计算结果(表达式)**但这个函数的返回值与函数参数的类型要一致

int main()
{
	int a = 10;
	int b = 100;
	int MAX = max(1,max(a,b));
	MAX = max(24+26,b);
}

3、函数原型声明

在上面的函数展示代码中,我们都是把函数写在主函数之前的,这是为什么呢?这主要与C语言的编译器有关,C语言的编译器是自上而下顺序分析代码的,当运行函数调用语句(sum(10,20))时,编译器需要知道sum函数的样子,也就是说编译器在用函数之前,需要知道这个函数的返回类型,几个参数,参数是什么类型这些因素,这样才能保证程序的安全运行

只有一个函数,可能不会影响我们对主函数的阅读或编写,那要是有十多个函数呢,主函数在最下面,感觉会很麻烦,那我们直接吧主函数放在最上面可以吗?显然是不可行的,当运行到主函数的函数调用语句时,编译器并不认识这个函数,所以程序会编译失败,那要任何解决这个问题呢?这就需要使用函数的原型声明

#include <stdio.h>
int main()
{
	sum(1,10);
	sum(20,30);
	sum(35,45);
	return 0;
}
void sum (int begin ,int end)
{
	int i;
	int sum = 0;
	for (i=begin; i < end; i++)
	{
		sum+=i;
	}
	printf("%d带%d的和是%d",35,40,sum);
}

在第四行,我们在主函数中已经使用sum函数,但编译器还没有看到整个sum函数,所以编译器假设int sum(int,int),但在13行遇到真正的整个sum函数,sum函数是不返还数据的void类型,与编译器假设的函数不匹配,这样程序出错了

主函数在上的正确写法:

#include <stdio.h>

void sum (int begin ,int end);		//声明

int main()
{
	sum(1,10);
	sum(20,30);
	sum(35,45);
	
	return 0;
}
void sum (int begin ,int end)	//定义
{
	int i;
	int sum = 0;
	for (i=begin; i < end; i++)
	{
		sum+=i;
	}
	printf("%d带%d的和是%d",35,40,sum);
}

void sum (int begin ,int end);这就是函数原型声明,函数原型声明提醒编译器有sum这样的函数,函数原型声明不仅是让编译器检查函数是否调用正确,而且可以让编译器来检查我们函数的定义与函数原型声明是否正确

函数原型
函数头以;结尾,就构成了函数原型,函数原型的目的就是告诉编译器这个函数长什么样子

  • 名称
  • 参数(数量及类型)
  • 返回类型

请添加图片描述

问题:类型不匹配
调用函数时给的值与参数的类型不匹配是C语言传统上最大的漏洞,当你调用函数时输入的参数和函数定义的参数,数据类型不一样,虽然编译器会帮你修改成对应类型,但这也是个弊端,当我们要输入一个函数定义类型的数据时,不小心输错,这个时候编译器没有发现这个问题,而我们以为输入正确,这可能会导致一些错误

特殊写法:

#include <stdio.h>

void sum(int,int);	//声明

int main()
{
	sum(1,10);
	sum(20,30);
	sum(35,45);
	
	return 0;
}
void sum (int begin ,int end)	//定义
{
	int i;
	int sum = 0;
	for (i=begin; i < end; i++)
	{
		sum+=i;
	}
	printf("%d带%d的和是%d",35,40,sum);
}

对于函数原型声明,参数的名字可以不写,这是因为原型声明是告诉编译器,这个函数的名字,参数类型与个数,返回类型

4、函数传递方式

(1)值传递(主要掌握)

函数传过去的是什么?
先看一段代码

void swap(int a ,int b);
int main()
{
	int a = 5;
	int b = 6;
	swap(a,b);
	printf("a=%d b=%d\n",a,b);
	
	return 0;
}
void swap(int a,int b)
{
	int t =a;
	a = b;
	b = t;
}

这样的数据交换函数能实现吗?先说结论,不能!

每个函数都有自己的变量空间,参数也位于这个独立的空间中,和其他函数没有关系,是独立的。
过去,对于函数参数表中的参数,叫做形式参数,调用函数时给的值,叫做实际参数
由于容易让初学者误会实际参数就是实际在函数中进行计算的参数,误会调用函数的时候把变量而不是传进去了,所以我们先不用这样的方式称呼,我们可以认为它们是参数和值的关系,等理解后再用这样的称呼

实际参数是函数调用时,在参数位的数据,这里的数据才是最初的,原本的数据,实际参数只是为传值提供复制的模板
为什么叫形式参数,在函数空间中,一开始是没有数据的,只能通过拷贝已有的数据,为这个函数空间使用,所以说到底,实参是函数外原本的数据,而形参是实参在函数空间的拷贝数据

接下来围绕本地变量变量的生存期和作用域展开讲解

本地变量
函数的每次运行都会产生一个独立的变量空间,在这个空间的变量,是函数这次运行所独有的,称为本地变量,在函数内部定义的变量就是本地变量,参数也是本地变量

void sum (int a, int b)	//参数是最先定义在变量空间的变量,将从别的变量空间中获取数据并赋值(就是我们说的传值)
{
	int Sum;	//Sum变量是在参数之后定义的变量,会做赋值或计算
	Sum = a+b;
	printf("总数为:%d\n",Sum);
}

变量的生存期和作用域

  • 生存期:什么时候这个变量开始出现了(定义变量),到什么时候它消亡了(释放变量

我们要知道,定义变量以及储存代码等数据的内存空间是使用电脑虚拟内存的一部分,当为了解决某个问题而产生一个程序,程序开始运行,才会依据程序在内存空间储存,当程序运行完,一般会释放其内存,方便之后运行别的程序,防止内存爆满。

  • 作用域:在(代码的)什么范围内可以访问这个变量(这个变量可以起作用)

对于本地变量,这两个问题的答案是统一的:大括号{}内——块
离开函数的作用域(块)后,函数已经结束,现在函数内的本地变量不存在,不是因为离开了作用域,而是函数已经结束了
请添加图片描述

把每个函数的变量空间想象成一个宇宙,而几个函数变量空间之间的关系犹如平行宇宙:
我们在main主函数(主宇宙)里面的操作,不会影响到swap函数,而当我们在主函数中,调用swap函数时(如同打开了通往swap平行宇宙的传送门),swap函数会产生一个变量空间(创造一个平行宇宙),我们进入swap函数,在swap函数里做的操作同样也不会影响到主函数(在swap宇宙,我们所做的事情,主宇宙是感受不到的也不受影响的),当我们在swap函数里的操作结束,离开swap函数,回到主函数时,swap函数的变量空间会释放(如同swap这个宇宙毁灭并等待重启)

本地变量的规则

  • 本地变量是定义在块内的
    • 它可以是定义在函数的块内
    • 也可以定义在语句的块内
    int main()
    {
    	int a = 5;	//定义在函数的块内本地变量
    	int b = 6;
    
    	if(a < b)
    	{
    		int i = 10;	//定义在语句的块内本地变量
    	}
    }
    

    在if语句中,i变量的生存期和作用域都在这个if语句的块语句内,离开这个块,这个i变量就不存在了
    在这里插入图片描述
    未定义标识符"i",表示在main函数变量空间中,未查找到定义过的i变量,这也表明离开if语句后,i变量和if语句的变量空间一起被释放了

    • 甚至可以随便拉一对大括号来定义变量
    {
    	int i = 0;
    	printf("%d\n",i);
    }
    
    这样的块语句也会产生变量空间,在里面做的操作不会影响外部,所以往往会通过这个方式来对代码进行测试。
  • 程序运行进入这个块之前,其中的变量不存在,离开这个块,其中的变量就消失了
    所以变量的作用域和生存期就仅仅限于这对大括号
  • 块外面定义的变量在里面仍然有效
    int main()
    {
    	int a = 5;	
    	int b = 6;
    
    	if(a < b)
    	{
    		int a = 0;	
    		printf("a = %d\n",a);	// a = 0
    	}
    	printf("a = %d ,b = %d\n",a,b);		//a = 5 ,b =6
    	return 0;
    }
    
    在一个语句的块内,若块内块外都有一个相同的变量,优先服从局部变量

注意:

  • 块里面定义了和外面同名的变量则掩盖外面的变量,使用块内的变量
  • 不能在一个块内定义同名的变量
  • 本地变量不会被默认初始化
  • 参数在进入函数的时候被初始化了
(2)指针传递(学完指针要掌握)

这一部分的学习是建立在了解指针的前提下!

在函数返回部分,我们讲到只返回一个值的函数写法,学完函数传递方式后,我们知道主函数与定义的函数有联系的有且只有一个部分,即函数返回的数据,除了返回值,函数其他数据都会被释放,但是有时候我们需要返回的数据不只有一个(比如上面的交换函数swap),这个时候我们要怎么实现多数据的返回呢? ——显然是使用指针来解决

5、函数的常见样式

//1、无参无返
void test01 ()
{
		printf("this is test01\n");
 }
//2、有参无返
 void test02 ()2
 {
		printf("this is test02\n"); }
//3、无参有返
 int test03 ()
 {
		printf("this is test03\n");
		return 1000;
 }
//4、有参有返
int test04 ()
{
		printf("this is test04\n");
		return 1000;
}

关于main函数
在这里插入图片描述

主函数return 0;,是将0这个值返回给调用的地方,返回给一小段代码,这段代码会来检查这个return 返回来一个什么东西,然后会报告给我们的操作系统(Windows)
传统来说,如果一个程序返还一个0,说明运行成功,如果返还一个非0的值,说明程序运行出现错误


数组

引言

如何写一个程序计算用户输入的数字的平均数?

int x;
double sum = 0;	//总数
int cut = 0;	//计数
scanf("%d",&x);
while(x != -1){
	sum += x;
	cnt ++;
	scanf("%d",&x); 
}
if( cnt > 0 ){
	printf("%f",sum/cnt);
}

这个是不需要记录输入的每一个数,那之后要与计数平均数的数据进行其他操作(如何写一个程序计数用户输入的数字的平均数,并输出所以大于平均数的数?),这样的程序不能完成,数据被之后输入的数据覆盖而丢失,所以需要一个大的集合来储存数据,也就接触到数据结构——线性表中的数组

概念

数组是全新的类型,是集合性的类型,可以以某个已知类型来创建一个线性集合,集合的大小是有限的,是静态创建的

请添加图片描述

使用数组来解决上面问题
如何写一个程序计数用户输入的数字的平均数,并输出所以大于平均数的数?

int main()
{
	int x;					//输入值
	double sum = 0;			//总数
	int cnt = 0;			//下标值与计数
	int number [100];		//定义数组
	scanf("%d",&x);			//输入
	while( x != -1){
		number [cnt] =  x;	//在对应下标插入输入值
		sum += x;			//累加输入值
		cnt ++;				//移动下标值
		scanf("%d",&x);		//继续输入
	}
}

这样的程序可以解决问题,在第九行的代码,将输入值储存到数组中,之后这个数据被覆盖也不受影响,计算完平均值后还可以对整体数据进行操作

	if( cnt > 0 ){
		printf("%f\n",sum/cnt);				//输出平均值
		for (int i = 0 ; i <cnt ; i++){
			if(	number[i] > sum/cnt){		//判断大于平均值的数
				printf("%d\n",number[i]);	//输出大于平均值的数
			}			
		}
	}

出了上面的循环后,我们已经可以算出平均数了,所以还要做一个判断(数组储存的数据中高于平均数的数)
出循环后的cnt表示的是number数组中有多少组数,则通过这个for循环来判断数组中的数据,从而判断高于平均数的数,并输出

  • 为了更好的理解,通过块语句来测试程序的准确性,那如何测试准确性呢?,每次插入数据到数组后,我们输出数组中的数据来观测程序的准确性!
    while( x != -1){
    	number [cnt] =  x;	//在对应下标插入输入值
    	{
    		printf("%d\t",cnt);
    		for(int i = 0 ; i <cnt; i++){
    			printf("%d\t",number[i]);
    		}	
    		printf("\n");	
    	}
    	sum += x;			//累加输入值
    	cnt ++;				//移动下标值
    	scanf("%d",&x);		//继续输入
    }
    

在这里插入图片描述

定义

定义格式: <类型> 变量名称[元素数量];

int grades[100];
double weight[20];
注意:

  • 元素数量必须是整数
  • <类型> 是这个数组每个元素的类型
  • 定义数组与普通变量的关键是[]
  • 数组是一个容器,其中所以元素具有相同的数据类型,一旦创建,不能改变大小,且数组中的元素在内存中是连续依次排列的

计算机下标习惯性从0开始计数,所以int a[10]这样的数组,总共有10个元素a[0],a[1],a[2],·····,a[9],数组可以在赋值的左边或右边,这说明数组的元素可以被赋值,也可以参与赋值(a[2] = a[1] + 6

数组的单元

  • 数组的每个单元就是数组类型的一个变量
    使用数组中的某个元素/单元,要以[下标]的方式调用,这样的方式

有效的下标范围

  • 编译器和运行环境都不会检查数组下标是否越界,无论是对数组单元做读还是写(越界问题多数出现在数组和指针中)
  • 一旦程序运行,越界的数组访问可能造成问题,错误的访问一些空间,导致程序崩溃
  • 所以这个是程序员的责任来保证程序只使用有效的下标值:[0,数组的大小 - 1]

之前判断大于平均数问题时,不知道大家有没有发现这段代码的问题,这段代码是有漏洞,有bug的,问题再于数组的容量,加入输入数组的数超过了100个,这个数组不久饱满了吗?
解决问题的方法:

  • 降低功能,即在输入的时候计数,并判断数组中的数据是否到达100个,到达后停止输入
  • 使用动态数组,这个是之后C++的学习,编译器会判断容器的容量,通过动态数组容器代码的逻辑,实现动态扩容,完美解决这个问题

在来一个小问题,来巩固数组的使用过程

输入数量不确定的数据,这个数据的范围是[0,9]的整数,统计每个数字出现的次数,输入-1表示结束

const int number = 10;				//数组的大小
int x;
int count [number];					//定义数组
int i;

for ( i = 0 ; i < number ; i++){	//初始化数组
	count[i] = 0;
}
scanf("%d",&x);
while( x != -1){
	if(x >= 0 &&  x <= 9){
		count[x]++;					//数组参与计算
	}
	scanf("%d",&x);			
}
for(i =0; i < number ; i++){		//遍历数组输出
	printf("%d:%d\n",i,count[i]);
}

数组的集成初始化
集成初始化是将一个集成的数据储存到数组中的过程,不用输入数组的大小,让编译器帮我们数,通过编译器方便数组的初始化

int a[]={2,4,6,1,3,5,7,9,11,13,15,17,19};
  • 这样写在{}中的数据,会按照顺序依次初始化到数组对应位置,数组大小也是由编译器来决定
  • 也可以确定数组大小,集成化个别数据,编译器可以将未集成化部分数组自动初始化为0
    int a[13] = {2};
    ->  2  0  0  0  0  0  0  0  0  0  0  0  0
    //集成化有意义的数据,其他未集成化部分初始化为0
    
    这样省略了遍历初始化的操作
  • 集成初始化的定位操作
    int a[13] = { [1] = 2,4, [5] = 6,6,};
    ->{ 0  2  4  0  0  6  6  0  0   0  0  0  0};
    
    {}中用[n]在初始化数据中给出定位,没有定位的数据在前面定位数据的后面(如:[5] = 6, 6),其他位置的值补零,也可以不给数组大小,让编译器决定,这样的初始化方式适合一些数据稀疏的数组(少部分数据有效,大部分数据为0)

数组大小

在数组初始化时,数组的大小是我们确定,但在接下来的程序操作中,我们对数组的操作全看我们给出的指令,但我们给的指令并不是绝对正确,所以在初始化之后,要如何知道数组的大小?通过一个指令——sizeof(a)/sizeof(a[0])

可以通过使用sizeof来得到数组的大小,这个sizeof得出的结果是这个数组的总字节
与数组定义有关,若是int数组,一个单元为4个字节,则sizeof的结果除于定义类型的字节,为数组的大小
sizeof(a)/sizeof(a[0])这样求数组大小,好处是当我们要修改数组时,这个代码求数组大小的功能不会变

数组的赋值与遍历

  • 赋值

    int a[] = {2 ,4, 6, 7, 1, 3, 5, 7, 9, 11};
    //int b[] = a; 错误
    

    容器与容器之间不能直接这样赋值,数组变量名单独使用时是表示这个数组首位置的地址,所以逻辑是错误的
    要把一个数组的所有元素交给另一个数组,必须采用遍历

  • 遍历
    计算机做的是数据的处理,无非就是读写操作,之前数据类型的学习,让我们知道如何对一般的类型进行读写,而这样集合性的类型,我们要如何读写(处理数据)
    数组是一个集合性的类型,而数组变量也是变量,我们定义了一个新变量,需要向其中存储数据,这个容器变量才有意义;在特定情况,我们需要使用整个数组的数据来完成某个计算(计数平均值),有什么方法可以实现这些操作呢?无论是对每个数组赋值还是调用数组每个数据,都是要把数组从头到尾运行一遍;
    所以数组遍历简单来说就是通过在循环中做一些操作来实现数组的全部调用
    在这里插入图片描述

数组传入函数(指针学完好理解)

数组传入函数的方式与指针有关系,数组名其实是这个集合内存存储地址的首位地址,通过一系列操作往下遍历集合,为什么是首地址?这很好理解,一切都从头开始!所以函数传入数组,是将数组指针传入,但传入数组就可以了吗?忘记了什么?我们在函数中不知道数组的个数,也就是说不知道数组的边界,这肯定是不安全的,无法遍历数组,所以规定,函数参数表中,数组地址后,跟上一个整型参数,用于传数组大小的参数。

#include <stdio.h>
void print_a (int a[],int size)
{
	for(int i = 0 ; i < size ; i++)
	{
		printf("%d ",a[i]);
	}
}

int main()
{
	int a[] = { 1,2,3,4,5,6,7};
	print_a(a,sizeof(a)/sizeof(int));
}

这就是数组传入函数的方式!

二维数组

在以往的学习中,我们知道数学几何中,线->面->体 表示从一维->二维->三维,我们之前学的数组,就是一个一维数组,因为这个数组的存储方式是单一线性的存储,与线的性质相符,线是如何成面的?面是多个线平行的排序而成,即线动成面,数组同理,物理层面上是在一个数组的基础上平行建立数组,这样的数组就是二维数组
请添加图片描述

我们这里要清楚,上图是我们认知的二维数组,为了方便理解罢了,实际的二维数组在内存中的存储方式是线性的,并非像这样的形式

二维数组的定义

在计算机中的二维数组,我们有别的方法来理解它,我们可以认为二维数组是一个以数组为元素的数组这样看非常抽象,但看上图,是长为12,宽为5的长方形,我们可以认为这个二维数组是 a[5] = {[A1[12],A2[12],A3[12],A4[12],A5[12]};的数组,也可以认为是一个a[12] = {A1[5],A2[5],A3[5],A4[5],A5[5],A6[5],A7[5],A8[5],A9[5],A10[5],A11[5],A12[5]};,所以这样定义二维数组就会产生二义性,所以C语言就规定,以a[行数][列数] = { };的形式定义二维数组,所以上图的二维数组的定义因为a[5][12];,这样定义有好处,一是规范定义二维数组,二是方便二维数组查询

二维数组的集成初始化

二维数组也有集成初始化,而且比较形象

a[3][3] = {{1,2,3},{4,5,6},{7,8,9}};
//或者
a[][3] = {{1,2,3},
		   {4,5,6},
		   {7,8,9}};

通过集成初始化二维数组,列数一定要给,行数可以让编译器来帮你数

二维数组的遍历

因为是二维数组,对二维数组的操作肯定与一维数组的操作不一样,一维数组通过一层循环完成遍历,二维数组就是用两层循环来完成遍历,通过循环的嵌套完成,之前循环部分的学习内容,通过循环变量的不同使用,可以进行行和列的操作

  • 对行操作
    int a[4][3] ={{1,2,3},
    		      {4,5,6},
    		      {7,8,9},
    		      {10,11,12}
    		   };
    for(int r=0; r <4 ; r++) // 行变量
    {
    	for(int c ; c < 3 ; c++)	//列变量
    	{
    		printf("%d ",a[r][c]);
    	}
    	printf("\n");//每行做完换行
    }
    

在这里插入图片描述

  • 对列操作
    int a[4][3] ={{1,2,3},
    		      {4,5,6},
    		      {7,8,9},
    		      {10,11,12}
    		   };
    for(int c=0; c <3 ; c++) // 行变量
    {
    	for(int r ; r < 4 ; r++)	//列变量
    	{
    		printf("%d ",a[r][c]);
    	}
    	printf("\n");//每行做完换行
    }
    

在这里插入图片描述


指针

我们之前学习,在输入输出部分,scanf的写法和printf不一样,在学指针之前可能都是硬记这个写法scanf("%d",&i),其中的&提过,叫取址运算符,这里能够将取得的变量地址传递到函数,能否通过这个地址在那个函数访问这个变量,这个函数原型需要一个参数来保存别的变量的地址,那如何表达能够保存地址的变量?
指针就是这个用来存储变量地址的!

1、基本知识

指针其实也是一个数据类型,就像整型存储整数,浮点型储存浮点数,指针就是存储地址的类型

指针变量

指针变量的值是内存的地址,不同变量的值是实际的值,指针变量的值是具有实际值的变量的地址

有实际值的地址在是合法安全的,其他无实际值的未开辟内存的数据是随机的,是非法不安全的,所以指针只能储存有实际值的变量地址

int i;这个是一个普通的整型变量
int * p = &i;这就是指针变量的定义和初始化,指针要定义储存什么类型变量的地址,这里就是存储整型变量的地址
指针定义还有其他形式:

int * p,q;
int *p,q;
上面 * 的写法都可以,看个人喜好,只要在类型和变量名之间就可以
这里int * p,q要注意,我们在定义之前的内置类型时候,经常这样连续定义,但这里定义的并不是两个指针,p是指向int的指针,而q是一个int类型
请添加图片描述
请添加图片描述
正确的连续定义方式是 int *p,*q;

通过一个```示意图````来理解指针:
请添加图片描述

指针是C语言的核心,指针的应用非常重要(比如:函数通过指针来建立联系指针与数组的关系函数的多返回动态内存管理等),之后我们都会讲解到
经过上面的介绍,我们对指针已经有一定的认识了,接下来继续学习指针的其他应用吧!

*运算符

我们在基础部分已经提到,指针是存储地址的变量,那我们只是单纯的存储地址吗?肯定不是!我们必然要通过指针的地址去访问调用数据,这里就是通过*运算符去调用指针地址的,也叫解引用

*是一个单目运算符,用来访问指针的值所表示的地址上的变量
可以做右值也可以做左值

  • intk=*p;
  • p=k+1;
    这个运算符很简单,是指针类型的特有运算符,理解这个运算符可以调用访问指针所指向的地址的数据即可

这展示指针的使用

#include <stdio.h>
void f(int* p);
void g(int x);

int main()
{
	int i = 6;
	printf("&i = %p\n", &i);
	f(&i);
	g(i);

	return 0;
}
void f(int* p)
{
	printf("p  = %p\n", p);
	printf("*p = %d\n", *p);
	*p = 26;
}
void g(int k)
{
	printf("k = %d\n", k);
}

在这里插入图片描述
上面案例就是初步的指针调用,通过地址传递,建立了main函数和g函数的联系,g函数中获得了i变量的地址,通过解引用可以访问到main函数中的i变量并修改,这就是指针的应用之一——建立函数联系

我们在刚刚开始学习C语言,学习使用scanf时,这样写scnaf("%d",i)但编译没有问题,这是什么原因呢?

scanf中,外面没有加上&,编译器以为这个i变量传进去的是0x……的十六进制数的地址,但它传进去的是i的值,以为这个i的值是i变量的地址,拿这个值做事情,所以编译不一定会报错,但运行一定会错(因为这个scnaf会把读到的数据写入不是i变量的地址,会写到别的地方,这个地方可能很小,是不能写的,那个地方可能有很重要的的作用)

2、指针和函数

通过上面的案例,我们也知道指针是可以连接两个函数,接下来就介绍函数与指针的应用

  • 交换两个变量的值

    void swap (int *pa , int *pb)
    {
    	int t = *pa;
    	*pa = *pb;
    	*pb = t;
    }
    

    交换值这个做法,以前只能写到主函数里,因为写到函数里,要返回两个值,但函数只能返回一个值,所以不能写到函数里,但现在使用指针,传入函数的是这两个变量的地址,交换这两变量的地址,依次交换这两个变量的数值
    在这里插入图片描述

  • 函数将结果以指针返回,状态用函数返回return返回
    常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错:
    -10(在文件操作会看到大量的例子)
    但是当任何数值都是有效的可能结果时,就得分开返回了
    在这里插入图片描述

3、指针和数组

接着我们来讨论数组和指针的关系
我们之前讲解数组,有提到,数组传入函数,那传入函数的数组是什么呢?

void func(int a[], int t);
int main(void)
{
	int a[] = { 1000,1,2,3,4,5,6,7,8,9 };
	int temp = sizeof(a);
	printf("main sizeof(a) = %d\n",temp);
	printf("main &a = %p\n", a);
	printf("%d",a[0]);
	func(a, 10);
	return 0;
}
void func(int a[], int t)
{
	int temp = sizeof(a);
	printf("func sizeof(a) = %d\n",temp);
	printf("func &a = %p\n", a);
	printf("%d",a[0]);
}

通过上面的测试代码,我们可以发现,其实传入函数的数组和在主函数中的数组是同一个,因为这里的数组的首地址相同,并且在两个函数输出的数组数据也一样
在主函数中sizeof(a)就是数组的容量,但在func函数中,sizeof(a)为4,和一个指向整型的指针变量大小一样

综上所述,函数参数表上的int a[ ]其实就是指针,在主函数定义好数组,在主函数中把数组地址传入func中,在函数里面通过指针来调用数组
在这里插入图片描述

数组参数

数组有以下四种传参方式,四个都是等价的:

int sum(int *ar,int n);
int sum(int *,int);
int sum(int ar[],int n);
int sum(int [],int );

以上四种都可以用于数组传参,但是我们平时最常用的是第一个和第三个
为什么数组传参,要带一个整型变量呢?我们传数组进入函数,函数内可以获得数组的首地址,但不知道数组的大小,在之后的遍历操作会导致错误发生,所以到一起传递一个数组大小参数

数组遍历是特殊的指针

在这里插入图片描述

数组变量本身表达地址,所以
int a[10];int*p=a;//无需用&取地址
但是数组的单元表达的是变量,需要用&取地址
a == &a[0]
[]运算符可以对数组做,也可以对指针做
[] 和 *都可以调用指针,只是[]的功能更多的是作用于数组等容器的
在这里插入图片描述

指针与const

cosnt之前讲过,通常作为变量类型的前缀,作用是要求变量一旦被初始化就不能被修改,如同常量般,在指针这边,const有两种用法,指向不变(地址)和指向内容不变(数据)
我们先看指针的定义:
<类型> * 变量名;定义处,解引用符*以前,已经可以看成指针指向的变量了,而在解引用符*后,可以看出的指针变量部分,这地方有些抽象,举个例子:

int a = 10;
int * p = a; //为初始化的指针变量
//指向不变
int * const p1 = a; //指针p1在生命周期内只能指向a变量,但a变量可以修改
//指向内容不变	//只要const在*以前就=就行
const int * p2 = a;	//指针p2、p3可以修改指向,但指向的变量被const修饰,变量不可修改
int const * p3 = a;

简单说,一个是指针变为常量,一个是变量变为常量,由此可以根据定义的前后顺序,int *const p;是指针常量,而const int* p;是常量指针,二者结合为常量指针常量:const int * const p;这个就是在生命周期内指针指向不变,指向的变量也不变

由此,我们可以知道,常量指针通常用于对容器的遍历,函数传参等,可以修改移动指向,而指向数据收到指针保护,不被修改;而指针常量更多是用于维护一个变量,用于快速调用重要变量
【图片】解释图

const数组
const int a[]  = {1,2,3,4,5,6};

数组变量已经是const的指针了,这里的constt表明数组的每个单元都是const int,数组内的数据都不可以被修改,所以必须通过初始化进行赋值

意义——保护数组值
因为把数组传入函数时传递的是地址,所以那个函数内部可以修改数组的值
为了保护数组不被函数破坏,可以设置参数为const
int sum(const int a[], int length)

4、指针运算

指针运算包括加减和比较,但比较使用的比较少
这些算术运算可以对指针做:

  • 给指针加、减一个整数(+,+=,-,-=)
  • 递增递减(++/--)
  • 两个指针相减

指针运算放到实际中大多是移位,+n是往下n个单元,-n是往前n个单元
在这里插入图片描述
指针的加减计算,不是单纯的直接与地址运算,而是根据容器的类型大小做运算!
在char类型的数组,char内存为1字节,所以指针q+1相比于q只加了一,而在int类型的数组,int类型内存为4字节,p+1不是p的地址加一,而是p地址加上(sizeof(int))*1才对,所以指针运算的公式为:指针变量±(n*sizeof(指针所指向的类型)),所以我们平时运算±n是前后移动几个元素,而不是移动地址,地址的改变要考虑到类型的内存大小
请添加图片描述

这里要注意:解引用运算符是单目运算符,优先级高于加号(+),则想表示调用当前位置的下个数据应该是*(q+1)才对

也可以用*p++表示,因为解引用运算符的优先级没有++递增运算符的优先高,对于*p++来说,是去除p所指的那个数据,之后再把p移动到下一个位置,这个更多用于数组类的连续空间操作

指针比较

平时的逻辑判断(<,<=,==,>,>=,!=)在指针这里也可以进行比较,指针比较是比较他们在内存中的地址,数组中的单元的地址肯定是线性递增的,所以一般有a[0] <= a[n]

0地址

0地址在C语言中有一定的意义,0地址的内容通常不能随便修改,所以一个有意义的指针不应该具有0地址,所以0地址可以表示特殊的事情:

  • 返回的地址是无效的
  • 指针没有被真正的初始化(先初始化为无意义的0)
    NULL是一个预定义的符号,表示0地址,即NULL == 0

小技巧: 当我们定义一个指针,却不知道这个指针该指向谁,又不能不对指针初始化,这个时候就可以对这个指针进行NULL初始化了!
(但一定要注意,不能对NULL地址进行数据修改,程序会崩溃)

4、指针的类型

无论指向什么类型,所有的指针的大小都是一样的,因为都是地址但是指向不同类型的指针是不能直接互相赋值的
这是为了避免用错指针

char c = '0';
int i = 1;
int *p = &c;
int *q = &i;
q = p //错误

q = p这样的赋值会警告,说明数据类型不匹配

但指针和别的数据类型不一样,指针可以接收不同类型的指针的值,但是可能会有错误,即在p,q指针都指向同一个char类型的数组时,当int*q=0时,会出现在char数组一起将四个字节赋值为0
【图片讲解】

指针的类型转换

这里先介绍void*,这个表示不知道指向什么东西的指针,即无类型指针,void*的指针运算和char型指针相同(但不相通)
指针类型转换
int *p = &i; void *q = (void*) p;
这里并没有改变p所指向的变量类型,而是后人用不同的眼光通过p看到它所指向的变量(我不再当你是int了,我认为你是void了)

这里的void* 的指针变量,是指我们对这个指针指向哪,我们不知道,先定义为void它所指向的变量空间是什么类型(char,int,double,……)不知道也不在意,
所以我们用void*的指针来表达说,我们内存里面有个内存地址,我用这个指针来表达它,这一般用到底层程序里
int * p = &i;void* q =(void*)p;
这里的操作,不会影响之前p所指向的变量i的类型,q也指向i,只是我们通过q来看变量i的眼光变了,我们用p看i是int,而通过q来看i,我们不把它看出int了,我们把它看成void了

5、动态内存管理

内存分配有静态和动态内存分配,像我们之前的数组提前定义好(程序运行以前)数组大小的即静态内存分配,而在程序运行后申请内存空间的,这样的内存分配叫动态内存分配

所以可以知道内存申请的时机的不同造就了不同的内存分配模式,显而易见静态内存分配是在我们编码阶段就要确定的,对于程序运行时,这个是不变的既定事实;而在运行程序时,通过语句申请空间,这个空间大小,不是固定不变的事件,而是会个无规律的动态事件,即不知道申请空间的大小

静态内存分配在之前的学习就一直在使用,接下来好好讲讲动态内存分配要如何实现,并且为什么要在指针这里讲解!

malloc - free函数

在这里插入图片描述
这个是malloc函数的函数声明,我们可以知道malloc函数参数是一个正数(size_t == unsigned int),函数返回是返回void型,所以开辟出来的内存不知道作用于什么类型的数据,我们只需要提供数据所需要的内存大小,之后再做强制类型转换

这里做一个拓展知识讲解,我们程序内存是有一定规划的,通常程序内存分区有四大分区,代码区、全局区、栈区、堆区,其中代码区和全局区是在程序运行前就处理好的,而栈区和堆区是在程序运行后才进行处理分配,这里主要说说栈区和堆区
栈区的控制者是编译器,是储存函数产生的内存,以栈帧为一个小单元,遵从先进后出的申请释放内存原则,这一部分我们感受的比较少,毕竟是编译器在操作
堆区的控制者就是程序员自己了,这块区域管理就全全交给程序员个人,数据的开辟释放都是程序员操作,这也意味着堆区申请的数据的生命周期是由程序员把控,不会像栈区,因为函数的结束,函数数据都释放掉
这里动态内存分配就是在堆区操作,向堆区申请空间使用
堆区在函数结束之前由程序员掌管,在程序结束后,编译器会统一将堆区数据清空,防止内存泄漏

具体的定义公式为:Type* name = (Type*)malloc(n*sizeof(Type);

Type 是数据类型
name 是指针变量名
malloc函数返回类型为void*,通过(Type*)转化为需要的类型
n*sizeof(Type)是内存大小,n是这个类型数据的个数,而sizeof(Type)是单个数据的内存大小

malloc讲了基本定义,还要提及和malloc联合使用的函数free函数
之前提到,堆区空间由程序员管理,malloc申请空间,那还需一个方法来释放空间,这个方法就是free函数,free函数需要的参数为申请空间的首地址
具体写法: free(指针变量);指针变量是指向堆区开辟内存首地址,堆区不能释放非法空间也不能重复释放统一内存,这都会造成程序问题

malloc申请空间失败会返回0,或者叫做NULL
那我们要如何知道堆区的内存有多大呢?

#include<stdio.h>
int main()
{
	void *p;
	int cnt = 0;
	while((p = malloc(100*1024*1024)) )
	{
		cnt++;
	}
	printf("分配了%d00MB的空间\n",cnt);
	return 0;
}

在这里插入图片描述
在初期学习,堆区内存是完全够用的


字符串

字符串其实在之前的学习就已经见过了,我们在""内写的内容,就是字符串的一种,叫字符串常量

"Hello world"

接下来具体介绍字符串

1、基础知识

字符串是以0(不能是字符'0')为结尾的字符数组

字符数组
char word[] ={'H','e','l','l','o','!'};
在这里插入图片描述
字符串
char word[] = {'H','e','l','l','o','!','\0'}
在这里插入图片描述

通过上面的例子,可以初步的认识到字符串
字符串是以0(整数0)结尾的一串字符,0或'\0'都是一样的,但是和字符'0'不同,这里0是标志字符串结束,但计算字符串长度的时候不包含这个0,字符串是以数组的形式存在,以数组或指针的形式访问

空字符串
char buffer[100] = "";
这是一个空的字符串,buffer[0] = "\0"
char buffer[] = ""
这个数组的长度只有1

字符串变量与常量

字符串也可以当成一个数据类型,字符串变量的定义方式有多种

char *str = "hello";
char word[] = "hello";
char word[10] = "hello";
  • 第一个是char类型的指针,指向了一个字符数组,这个字符数组内容是hello
  • 第二个是有个char类型的数组,这个数组的内容内容就是hello
  • 第三个是这是有个字符数组,大小有十个字节这么大,往里面放了hello,有五个字符
    ,这五个字符在这个数组占据6个字节的空间,因为还有后面那个‘\0’

所以写一个字符串,编译器会自动加上这个0,这三个都是字符串

上面也提到过字符串常量,"Hello"就是字符串常量,"Hello"会被编译器变成一个字符数组放在某处,这个数组的长度是6,结尾还有表示结束的0

注意:两个相邻的字符串常量会被自动连接起来

printf("请分别输入身高的英尺和英寸,"
	"如输入\"5 7\"表示5英尺7英寸:\n");
	// \"5 7\"--> "5 7" 使用了转义字符

在这里插入图片描述
C语言的字符串是以字符数组的形态存在的
不能用运算符对字符串做运算
通过数组的方式可以遍历字符串
唯一特殊的地方是字符串字面量可以用来初始化字符数组

#include <stdio.h>
int main(void)
int i=0;

char *s = "Hello World"
// s[0]='B';
char *s2 ="Hello World";
printf("&i=%p\n"&i);
printf('s =%p\n",s);
printf("s2=%p\n",s2);
printf("Here!s[0]=%c\n",s[0]);
return 0;

两个字符串变量所指向的字面量是一样的,但这两个字符串的地址指向同一个位置
而i变量的地址比较大,即i这个本地变量和所指向的地方不是同一个地方
在这里插入图片描述
请添加图片描述

编译器要把“hello world”这个字符串,找位置去放,但是因为这个字符串是在编译时刻
就已经有值的东西,所以编译器会把这个值放在一个只能读不能写的地方(全局区的字符串常量区域储存),然后让指针指向它,并且在一个程序里有多个相同的东西,那些指针会指向同一个地方

char *s="Hello, world!";
s是一个指针,初始化为指向一个字符串常量,由于这个常量所在的地方,所以实际上s是const char*s,但是由于历史的原因,编译器接受不带const的写法

但是试图对s所指的字符串做写入会导致严重的后果如果需要修改字符串,应该用数组:
char s[]=“Hello, world!”;

char* 是字符串?
字符串可以表达为char* 的形式,char*不一定是字符串
本意是指向字符的指针,可能指向的是字符的数组(就像int *一样)
只有它所指的字符数组有结尾的0,才能说它所指的是字符串

字符串数组

字符串数组的原理和二维数组相似
char **a;这个是什么意思呢?
a是一个指针,指向另一个指针,那个指针指向一个字符(串),这个就二级指针,指向指针的指针
字符串数组的定义方式char a[][];char *a[]

int main()
{
	// a[0] ---> char [10]
	char a[][10] = {	
		"Hello",
		"world"
	};
	// a[0] --> char*
	char *a[] = {
		"Hello",
		"World"
	};
}

【图片】

字符串赋值

char *t ="title";
char *s;
s=t;
并没有产生新的字符串,只是让指针s指向了t所指的字符串,对s的任何操作就是对t做的
请添加图片描述

2、字符串输入输出

scnaf与printf

语法:

char string[8];
scanf("%s",string);
printf("%s",string);

这里要注意,scanf读入一个单词(到空格、Tab或回车为止)
通过案例更好的认识

#include<stdio.h>
int main()
{
	char word[8];
	char word2[8];
	scnaf("%s",word);
	scnaf("%s",word2);
	printf("%s##%s##\n",word,word2);
	return 0;
}

这里## ##的作用是判断是否会有空格输出,若输出,会在##和##中间
在这里插入图片描述
这里Hello world 中间的空格用来区分两个单词的分隔符
所以%s是读一个单词,即读到一共空格、一个tab或回车为止
但以上的写法是不安全的,因为这是一个八个字节的char字符串,我们输入的字符数量不能确定,可能会超出范围

在这里插入图片描述
这样是安全的操作,在%和s之间加一个数字,这个数字的目的是告诉scanf最多可以输入7个字符,超过7个字符的内容不要了(即输入的范围是<=数字
在这里插入图片描述
在这里插入图片描述
上图是怎么回事?
当在scanf中规定了可读范围,超出了有效范围,空格、tab和回车的作用就没有了,而是用这个个数来划分单词,则多出的内容会继续交给下一个scanf去阅读
缓存区的知识点)

安全输入
输入时,在%与s之间的数字表示最多允许读入的字符数量,这个数字应该比数组的大小小一

输入常见错误

char *string;
scanf("%s",string);

以为char*是字符串类型,定义了一个字符串类型的变量string就可以直接使用了
由于没有对string初始化为0,所以不一定每次运行都出错

单字符输入输出getchar与putchar

putchar
函数声明为int putchar(int c);
putchar函数把一个字符输入到标准输出(即终端–程序运行的那个黑框框),这个putchar函数,函数参数是int类型,但是这个int只能接受一个字符,我们给一个char,它放入int内,而不是这个int可以放四个字符
在某些情况,造成这个输出不能正常进行,编译器会返还一个特殊的符号:EOF(-1)(end of file),这是c语言中定义的宏
getchar
getchar函数声明为:int getchar(void);
功能:从标准输入读入一个字符,
这里返回类型是int是为了返回EOF(-1),表示输入结束

常见用法

int main()
{
	char ch;
	while((ch = getchar()) != '\n')
	{...}
	//这是常见的使用方式,循环获得终端字符,直到换行,
}

3、string.h库函数

strlen

size_t strlen(const char *s);
功能:返回s的字符串长度(不包括结尾的0)

#include <stdio.h>
#include <string.h>
int main()
{
	char line[] = "Hello";
	printf("stlen = %lu\n",strlen(line));
	printf("sizeof= %lu\n",strlen(line));
	return 0;
}

在这里插入图片描述

这个strlen函数的内部原理,其实就是遍历字符串,用计数器来计算字符串的长度,这个过程运用到循环,当循环 遍历识别到\0时循停止,并将这个计数值作返回值返回
函数实现

size_t mylen(const char*s)
{
	int idx = 0;
	while (s[idx] != '\0'){
		idx++;
	}
	return cnt;
}

strcmp

int strcmp(const char *s1,const char *s2);
功能:比较两个字符串 ,返回结果:

  • 0:s1 == s2
  • 正数:s1 > s2
  • 负数: s1 < s2

共三种返回情况

#include <stdio.h>
#include <string.h>
int main()
{
	char s1[] = "abc";
	char s2[] = "ABC";
	printf("%d\n",strcmp(s1,s2));
	printf("%d\n",'a'-'A');
	return 0;
}

strcmp函数实际上是对字符的ASCII码进行比较:首先比较两个字符串的第一个字符,若不相等,则停止比较并得出两个ASCII码大小比较的结果;如果相等就接着向后第二个字符,以此类推。无论两个字符串是什么样,strcmp函数最多比较到其中一个字符串遇到结束符’/0’为止,就能得出结果。
特别注意:strcmp(const char *str1,const char * str2) 这里面只能比较字符串,即可用于比较两个字符串常量,或比较数组和字符串常量,不能比较数字等其他形式的参数。
规定返回值为正数,负数,0 。而确切数值是依赖不同的C实现的。
当两个字符串不相等时,C 标准没有规定返回值会是 1 或 -1,只规定了正数和负数。
有些会把两个字符的 ASCII 码之差作为比较结果由函数值返回
函数实现

int mycmp(const char* sl,const char* s2)
{
	int idx=0;
	while(s1[idx] == s2[idx] && s1[idx]!='\0'){
	idx ++;
	}
	while(*s1==*s2 && *s1 !='\0')
	S1++;
	S2++;
	return *s1-*s2;
	//底层代码两种写法

在这里插入图片描述

strcpy

char* strcpy(char *restrict dst, const char *restrict src);
功能:

  • 把scr的字符串拷贝到dst
  • restrict 表明src和dst不重叠
  • 返回的是dst(为了符合编程的链式思想a = b = strcpy()
    这里为了要用不可折叠的呢?
    有可能dst的地址紧挨着src,而src为“hello”,想把src拷贝到det,达到图片的效果,但这样两个数组是重叠的,这样是不能使用strcpy拷贝复制
    请添加图片描述
    代码实现
char* mycpy(char* dst , const char* src)
{
	char *ret = dst;
	while(*src != '\0'){
	*dst = *src
	dst++;
	scr++;
	}
	*dst = '\0';
	return dst
}

注意,复制一个字符串,复制的对象内存开辟方式

char *dst = (char*) malloc (strlen(src)+1); 
strcpy(dst,src);

strcat

char *strcat(char *dst, const char *src)
功能:将字符串src 连接到dst的尾部,返回dst
注意:目标字符串dst要有足够的空间,来链接别的字符串

int main()
{
	char *s = "World";
	char S[20] = "Hello";
	strcat(S,s);
	printf("%s",S);
}

在这里插入图片描述

strchr

char *strchr(const char *s, int c);
char * strrchr(const char *s, int c);
函数返回NULL表示没有找到
功能:
strchr
在字符串中字符,从左往右寻找这个字符第一次出现的位置,没找到就是返回NULL
strrchr
这个就是从右往左查找

#include <stdio.h>
#include <string.h>
int main()
{
	char s[] = "hello";
	char *p = strchr(s,'l');
	printf("%s\n",p);
	
	return 0;
}

在这里插入图片描述
查询第二个出现的目标字符

#include <stdio.h>
#include <string.h>
int main()
{
	char s[] = "hello";
	char *p = strchr(s,'l');
	p = strchr(p+1,'l');
	printf("%s\n",p);
	
	return 0;
}

在这里插入图片描述
以第一次出现的位置的后一个字符开始查找


  1. 比特(BIT,Binary digit),计算机专业术语,是信息量单位,是由英文BIT音译而来。同时也是二进制数字中的位,信息量的度量单位,为信息量的最小单位。 ↩︎

  2. IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number)),一些特殊数值(无穷(Inf)与非数值(NaN)),以及这些数值的“浮点数运算符”;它也指明了四种数值舍入规则和五种例外状况(包括例外发生的时机与处理方式)。 ↩︎

  3. 四舍六入五留双(也称为银行家舍入法)是一种更精确的舍入规则,常用于统计学和金融领域。其核心思想是减少传统四舍五入带来的系统性偏差。 ↩︎

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐