一、B站慕课视频学习链接
【浙江大学】C语言入门与进阶 翁恺(全129讲)_哔哩哔哩_bilibili
C语言程序设计_浙江大学_中国大学MOOC(慕课) (icourse163.org)
二、计算机和c的一些基础认知
- 计算机本身最擅长的能力是 重复。
- 程序的执行有两种方案:一、解释:借助一个程序,它能理解你的程序如何按照你的要求执行。二、编译:借助一个程序,把你的程序翻译成计算机真正能懂的语言——机器语言,然后给计算机直接执行。(所有计算机的编程语言都可以解释执行和编译执行,只是人们常不常用而已:C语言习惯编译执行,python习惯解释执行。)
- 语言的能力/适用领域主要是由库和传统所决定的。
- 计算机做的所有事情都叫做计算,计算的步骤就是算法。
- C语言用在哪里?可以写操作系统、嵌入式系统、驱动程序、底层驱动、图形引擎、图像处理、声音效果。是一种工业语言,开发效率>>学习过程,开发效率>>开发乐趣。
- C语言需要编译才能被运行,因此需要编辑器、编译器或者包含这两个的IDE(集成开发环境)。
- 推荐的编程软件是Dev C++,免费 安装简单,不用建工程。轻量级的编程软件,如Geany+MinGW或Sublime Text+MinGW,与专门的IDE如Dev C++相比,没有了编译程序。
- 什么是GCC?GCC,全称为"GNU Compiler Collection",是一套由GNU开发的编译器工具集,用于将高级编程语言(如C、C++等)的源代码转换成目标机器代码。
三、C语言的历史
- C语言是在1969年-1972年诞生的。C语言是从B语言发展而来的,B语言是从BCPL发展而来的,BCPL是从FORTRAN发展而来的。BCPL和B都支持指针间接方式,C也支持。1973年3月,第三版的Unix上出现了C语言的编译器,11月发布的第四版Unix是完全用C语言重新写的。
- C语言的版本最初是经典C又被叫做“K&&R the C”。
- 1989年ANSI发布了一个标准——ANSI C
- 1990年ISO接受了ANSI的标准——C89(也称为C90)是C语言的最早版本的标准,于1989年发布。它定义了C语言的基本语法、关键字和数据类型,并引入了标准库函数,如stdio.h和stdlib.h等。C89的特点是简洁、可移植且易于理解,被广泛应用于各种计算机平台。
- C99是C语言的第二个官方标准,正式名称为ISO/IEC 9899:1999。C99是在1999年发布的,作为C89(也称为ANSI C)的继承者,对C语言进行了扩展和改进。它引入了一些新特性,如变长数组、复合字面量、单行注释等。C99还提供了更灵活的变量声明和初始化方式,允许在代码中声明变量的同时进行初始化。
- C11标准于2011年发布,是对C语言的又一次改进和扩展。它引入了一些新特性,如匿名结构体、泛型选择表达式、多线程支持等。C11还对一些现有特性进行了细微的改进和修正,提高了语言的表达能力和可靠性。
- C17标准于2018年发布。C17主要是对C11标准的修订和更新,旨在进一步改进语言的特性和可用性。C17引入了一些新特性,如初始化宏、属性和线程局部存储等。
- 最新的标准是2024年三四月份发表的C23。继C17之后发布,进一步扩展和改进了C语言的功能和特性。
四、断点调试
- 在C语言中,断点调试可以通过使用调试器(Debugger)来实现。常用的调试器包括GDB、Visual Studio的调试器等。断点调试的基本步骤包括设置断点、运行程序至断点、单步执行、查看变量值等。
-
断点调试的概念:断点调试是一种程序调试技术,它允许程序员在程序的特定位置设置断点,当程序执行到这些断点时,程序会暂停执行,以便程序员可以查看程序的状态并进行调试。通过断点调试,程序员可以逐步执行程序,查看变量的值,找出程序中的错误并进行修复。
- 在调试前要记得先编译运行一下。
- 可以在旁边数字那里点一下,那行就会变成红色。然后调试运行。
- 若显示“这里项目没有调试信息”,则用管理员身份打开工具-编译选项-代码生成/优化-连接器,工具-环境选项勾选浏览Debug变量,按如下图操作打开产生调试信息,然后重启devc++。调试的具体步骤可以看:Dev-c++断点调试闪退和调试信息不显示的解决方法来了!!!_哔哩哔哩_bilibili
若出现下图所示结果,可以直接点确定,没啥影响。
然后左边可以看到a、b、t的值,点击下一步可以看到一步步的改变过程。
五、注释
注释插入在程序代码中,用来向读者提供解释信息。它们对于程序的功能没有任何影响,但是往往能使得程序更容易被人类读者理解。
- 以两个斜杠“//”开头的这一行语句是注释部分(是C99的注释,ANSI C不支持)
- 延续数行的注释,要用多行注释的格式来写。由一对字符序列“/*”开始,以“*/”结束。它也可以用于一行内的注释。例如
- 编译时,所有注释语句会被替换为一个空格。
六、常量
-
C语言中的常量分为字面常量、const 修饰的常变量、#define 定义的标识符常量、枚举常量。
这段套用了一些【C语言】常量_字面常量-CSDN博客的内容。
6.1字面常量
- 字面常量是指直接出现在代码中的固定值,也称为直接量。表示不同类型的常量数据。
- 包括:
1、整数常量:整型常量可用十进制、八进制、十六进制三种形式表示。
- 十进制整型常量:由正负号和阿拉伯数字0~9的数字组成,没有前缀,不能以0开头(比如:0是可以的,但是09是不可以的)。
- 八进制整型常量:由正负号和阿拉伯数字0~7组成,首位数字必须是0,没有小数部分。(比如:023)
- 十六进制整型变量:由正负号和阿拉伯数字0~9、英文字符a~f或A~F组成,首位数字必须有前缀0x或者0X,没有小数部分。比如(0x00FF0000)。
- 例如,10、010和0x10分别是十进制、八进制、十六进制整数,它们表示不同数值的整数。10是十进制数值,010的十进制是8,0x10的十进制是16;又如,0386和0x1g是非法的整型常量,因为0386是八进制但是含有非法数字8,0x1g是十六进制但是含有非法字符g。
- 转换工具:二进制转十进制 | 菜鸟工具
2、浮点数常量:以小数形式出现的常量,包含一个或多个数字、小数点、指数标记和可选的正负号。
例如:3.14、-0.5、6.02e23(科学计数法表示的浮点数)等。
3、字符常量:表示一个字符的常量,使用单引号括起来。
例如:'A'、'7'、'?'等。另外,一些特殊字符可以通过转义字符来表示,如'\n'(换行符)、'\t'(制表符)等。
4、字符串常量:表示一个字符串的常量,使用双引号括起来。
例如:"Hello, World!"、"C Programming"等。字符串常量实际上是由多个字符组成的字符数组,以空字符(\0)结尾。
5、布尔常量:表示逻辑值的常量,只有两个取值:0表示假(false)和非零值表示真(true)。
6、空指针常量:表示空指针的常量,使用特殊关键字NULL表示。
这些字面常量可以直接在代码中使用,用于赋值、比较、计算等操作。
6.2const修饰的常变量
在C语言中,使用const
关键字来修饰变量,表示该变量是一个常变量(constant variable)。
注意:
1.被
const
修饰的常变量必须在声明时进行初始化,并且不能被后续的赋值操作改变。它的值在初始化后就不能再被修改。具有常量不变不能被改变的属性。2.虽然它被赋有固定值,但它依旧有变量的属性,不能用来定义数组的长度。(因为定义数组时,长度是固定的)。
6.2.1示例1
int num = 10;
num=20;
printf(“%d\n”,num); //这时num可被修改,从10变为20。最后打印出来为20
const int num = 10;
num = 20; //出错,说“count”是只读的。意味着被const修饰的变量不可被修改
因此
const int num = 10; //const修饰的num是常变量-具有常属性(不能被改变的属性)
6.2.2示例2
const修饰的常变量不止有常量不能被二次赋值的属性,还有变量的属性,用数组证明:
int n=10;
int arr[n]=0; //这里的n是用int定义的变量,而数组是存储数据长度固定、数据类型一致的容器。因此错误。
const int n=10;
int arr[n]=0; //这里的n使用const定义的常变量,但依旧报同样的错误。
因此,说明const修饰的n的本质属性还是变量,只不过是被const修饰了一下,所以它也有变量的属性。
6.3#define 定义的标识符常量
在C语言中,使用 #define预处理指令可以定义标识符常量(Identifier Constant),也称为宏常量写在开头,格式:#define 常量名 值,在这里常量名和值之间不需要等号。如 #define PI 3.1415926
在使用#define定义标识符常量时,需要注意以下几点:
1.不需要分号:#define指令不需要以分号结尾。因为其实define定义的宏常量在预编译是会被直接替换为变量名后边的值,如果加上分号,就会导致计算可能出现错误。
2.不进行类型检查:#define只是进行文本替换,将标识符替换为指定的常量值,不进行类型检查。因此,在使用标识符常量时,要确保替换的文本与所需的类型兼容。3.常量名称全大写:通常约定使用全大写字母来命名标识符常量,以便与变量区分开来。
4.没有作用域限制:标识符常量的作用域可以是整个源代码文件,而不像变量一样受到作用域规则的约束。
4.不可更改:通过 #define定义标识符常量不可更改。
5.使用 #define定义标识符常量可以提高代码的可读性和可维护性,并且在编译时进行文本替换,避免了运行时额外的开销。但要注意,过度使用宏可能会导致代码难以理解和调试,建议合理使用。 数组大小常用宏定义。不占用运行时间,只占用编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)
那么,如何终止一个宏?
#undef 标识符
undef 后面的标识符表示你所要终止的宏。比如前面在程序开头用 define 定义了一个宏 MAX,它原本的作用范围是一直到程序结束,但如果现在在程序中某个位置加了一句:
#undef MAX
那么这个宏的作用范围到此就结束了。
6.4枚举常量
可以用于列举常量,使用枚举可以提高代码的可读性和可维护性,并使代码更具表达力。
假设我们要用到星期一到星期五7个变量,如果用#define的方法定义变量,我们需要用7行代码
#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7
而使用enum(枚举)常量能用更少的代码表示相同的意思。
enum DAY
{
MON = 0, TUE, WED, THU, FRI, SAT, SUN
};
如果不指定枚举常量的值,C语言会自动为它们分配整数值,默认从0开始(赋值0的过程可以省略)。每个后续的枚举常量会递增1。
当然我们可以在定义时修改任意枚举成员的值:
enum DAY
{
MON = 1, TUE, WED = 100, THU, FRI, SAT, SUN
};
对于没有指定值的枚举元素,其值为前一元素加 1。在这里TUE = 2, THU = 101,以此类推。
打印结果如下:
需要注意的是:
1.枚举常量的作用域是全局的。这意味着在定义后你可以在任意地方访问它们,但注意避免命名冲突。
2.枚举常量的默认整数值从0开始,并按照声明的顺序递增。但也可以手动为枚举常量指定特定的值,例如:enum Weekday { MONDAY = 1, TUESDAY = 2, ... };。(枚举常量是常量,在main函数内部不可修改其值。)
3.可以通过枚举常量的名称来引用其对应的整数值,例如:int value = WEDNESDAY。意思是将WEDNESDAY其表示的常量赋值给value。
如果想要让用户输入一个值,而不是定义它的初始值,这个叫做变量
七、变量
7.1变量的定义
- 变量类型(空格)变量名(自己起);
- 最好初始化变量,避免脏数据影响后续调试,如 int a = 0;
- 如果变量名有实际含义,最好用对应的英文(再不行就拼音,或英文简写)表示,提高代码的可读性 。
例如:
由于未初始化,所以a和b里原来是什么值,就会输出什么值,并不是我们自己定义的值。
C语言变量名的命名规则
- 变量名只能包含字母(a-z、A-Z)、数字(0-9)和下划线(_)。
- 变量名必须以字母或下划线开头,不能以数字开头。
- 变量名区分大小写,例如 myVariable 和 myvariable 是两个不同的变量。
- 变量名不能使用C语言的关键字,如 int、float、return 等都不能作为变量的名字。(main算有效的变量名,它不是关键字,而是特殊的函数名)
7.2全局变量&&局部变量
- 全局变量顾名思义,全局变量就是整个工程都能用的。
- 一般定义在最前面。
- 全局变量可以解决函数多结果返回的问题,但时更多地用于多函数间的全局数据表示。
而局部变量只能在{}内部使用,这里的{}不仅限于主函数的大括号,在后面的循环或判断语句中的{}也同样适用。
由于全局变量和局部变量的作用范围不同,因此可以同名。当局部变量和全局变量同名时,全局变量不起作用,而由局部变量起作用。
如果全局变量和局部变量名字一样呢?是谁起作用呢?
显然,在这里局部变量会覆盖同名的全局变量的值。
- 这里简要介绍printf(),scanf(),他们都是函数,顾名思义,前者是打印,后者是扫描、读取,从键盘读取输入,它们是被包含在stdio.h头文件中的,所以前面要写#include<stdio.h>来引入。gets() && puts()也用同样的功能。
- printf是打印的意思而f则代表format(格式化)的意思。因此printf就叫格式化打印。
7.3 变量的作用域和生命周期
7.3.1变量的作用域
作用域(scope,这个词要记住,后面编译器报错可能会出现)是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
- 局部变量的作用域是变量所在的局部范围。(就是大括号里边的变量只能在大括号里用)
- 全局变量的作用域是整个工程。(就是大括号外边的变量在哪都能用)
7.3.2生命周期
变量是保存数据的工作单元,计算机用内存单元来对应实现。一旦在程序中定义变量,计算机在执行过程中就会根据类型分配相应的内存单元供变量保存数据。
就一般程序而言,计算机都是从主函数开始运行的,使得main()函数中的所有局部变量,一开始就在内存数据中分配了存储单元。而其他函数在被调用前,其局部变量并未分配存储单元,只有当函数被调用时,其形参和局部变量才被分配相应存储单元;一旦函数被调用结束返回主调函数,在函数中定义的所有形参和局部变量将不复存在,相应的存储单元由系统收回。
变量从定义开始分配存储单元,到运行结束存储单元被回收,整个过程称为变量生命周期。
变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段。
- 局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。(大括号里)
- 全局变量的生命周期是:整个程序的生命周期。(在哪都行)
总结:作用域就是变量在哪能用,生命周期就是它啥时候产生,啥时候消失。
7.4变量存储的内存分布
为方便计算机存储管理,C语言把保存所有变量的数据区分成动态存储区和静态存储区。它们的管理方式完全不同,动态存储去是使用堆栈来管理的,适合函数动态分配与回收存储单元。而静态存储区相对固定,管理较简单,它用于存放全局变量和静态变量。
程序代码和数据变量是分开存放的,且动态存储区的变量按函数组织。
系统存储区 | 操作系统(如windows)、语言系统(如Devc++) | ||
用户存储区 | 程序区(C程序代码)如主函数、其他函数 | ||
数据区 | 静态存储区 | 全局变量 | |
静态局部变量 | |||
动态存储区 | main()变量区 | ||
其他函数1变量区 | |||
其他函数2变量区 | |||
... |
7.5静态变量
在静态存储区中,除了全局变量外,还有一种特殊的局部变量——静态局部变量。它存放在静态存储区,不会像普通局部变量那样因为函数调用结束而被系统回收,它的生存周期会持续到程序结束。由于存储单元被保留,一旦含有静态局部变量的函数被再次调用,则静态局部变量会重新激活,上一次函数调用后的值仍然被保留,可供本次调用继续使用。
static 类型 变量名;//可以赋值
举例:输入正整数n,输出1!~n!的值。要求定义调用含静态变量的函数fact_s(n)计算n!
正常循环:
#include<stdio.h>
int fact_s(int n) {
int prod=1;
for(int i=n;i>=1;i--){
prod*=i;
}
return prod;
}
int main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
printf("%d!=%d\n",i,fact_s(i));
}
return 0;
}
定义静态变量,第一次赋值为1;用上一次调用时的值×n :
#include<stdio.h>
int fact_s(int n){
static double f=1;
f=f*n;//上一次调用时的值再*n
return f;
}
int main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
printf("%d!=%d\n",i,fact_s(i));
}
return 0;
}
八、数据存储
8.1整型数据的存储
- 计算机处理的所有信息都以二进制形式表示,即数据的存储和计算都采用二进制。只是不同的类型位数不同,数的范围就不同。
- 首先介绍整型数据的存储格式,不妨假设每个整数在内存中占用两个字节存储,最左边的一位(最高位)是符号位,0代表正数,1代表负数,非符号位为该数字绝对值的二进制。
- 数值可以采用原码、反码和补码等不同的表示方法。为了便于计算机内的运算,一般以补码表示数值。
8.1.1正数数据的存储
- 正数的原码、反码和补码相同,即符号位是0,其余各位表示数值。
- 原码反码和补码只能实现正负整数的表示,不能表示小数部分。
- 原码的存在是为了将补码转为原码而方便程序员认识。
以一个字节为8位为例。
剩下7位 0000 0000~0111 1111即0~127 ;
1000 0000=-128;1111 1111=-127;1000 0001=-1
(这就能解释后续为什么char类型有符号位和无符号位 数的范围不一样)
8.1.2负数数据的存储——补码
- 在计算机中用补码表示负数,负数是以补码得形式存在的,补码=原码取反+1。
- 负数的原码、反码、补码不同:
负数的原码:符号位是1,其余各位表示数值的绝对值。
5的原码 0000 0101,最高位为0,表示正数,剩下七位为十进制的绝对值5的二进制。
-5的原码 1000 0101 ,最高位为1,表示负数,剩下七位为十进制的绝对值5的二进制。
反码:符号位是1,其余各位对原码取反。
5的反码 0000 0101,与原码一样。
-5的反码 1111 1010,最高位不变,其余按位取反。
补码:反码+1。
5的补码 0000 0101,与原码一样。
-5的补码 1111 1011,其反码+1。
0b表示二进制:
考虑-1怎么用二进制表示,我们希望-1+1=0
0(十进制)=00000000(二进制)
1(十进制)=00000001(二进制)
11111111+00000001=100000000(进位后八位会多出来一个1,变成九位数字。但是在计算机中,一个字节可以表达的数只有8位数字,多出来的那一位1会被丢掉)因此11111111+00000001=00000000=0 即表达了-1+1=0
又可以写成-1=0-1.
所以-1=(1)00000000-00000001=256-1=255=11111111。
11111111被当作纯二进制看待时,(11111111)₂ = (1 × 2⁷) + (1 × 2⁶) + (1 × 2⁵) + (1 × 2⁴) + (1 × 2³) + (1 × 2²) + (1 × 2¹) + (1 × 2⁰) = (255)₁₀=255;
被当作补码看待时,11111111=-1(由以上结果可知)。
同理对于-a来讲,-a=0-a,实际是2^n-a,n是位数即8。由此可以直接得到负数的补码 对应的正数。
例如 -1 -1=0-1 2^8-1=255=11111111
-5 -5=0-5 2^8-5=256-5=251=11111011
若想以补码倒推原码,则补码-1除最高位取反。举例:
补码:0000 0101,最高位为0,是正数。因为正数的原码反码补码都一样。所以原码:0000 0101
补码:1111 1011 ,最高位为0,是负数。-1得反码:1111 1010。最高位不变,其余取反:1000 0101。所以原码1000 0101。
为什么1000 0000=-128?
负数在计算机中都由补码表示。
11111111=-127是补码 补码是原码取反+1得到的
减1得11111110,取反 10000001=-127是原码 其补码111111110+1=100000001
-127-1=-128 10000000=-128是原码
1个字节可以表达的数:10000000 ~ 00000000 ~ 11111111(-128~ 255)
2个字节可以表达的数:10000000 00000000 ~ 00000000 00000000 ~ 01111111 11111111,即
8bit/比特位 = 1Byte/字节,1024Byte/字节 = 1KB,1024KB=1MB,1024MB=1GB,1024GB=1TB。1024=2^10。
int是32位就代表是4个字节,代表10000000 00000000 00000000 00000000 ~ 0 ~01111111 11111111 11111111 11111111,即
数值 | 补码 | |||||||||||||||||
十进制 | 十六进制 | |||||||||||||||||
32768 | 7fff | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | |
32767 | 7ffe | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | |
... | ... | ... | ||||||||||||||||
2 | 0002 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | |
1 | 0001 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | |
0 | 0000 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
-1 | ffff | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | |
-2 | fffe | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | |
... | ... | ... | ||||||||||||||||
-32767 | 8001 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | |
-32768 | 8000 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
8.2实型数据的存储(浮点型)
存储实型数据时,分为符号位、阶码、尾数三部分,如下图所示。例如:实数-1.234 5e+02是负数,阶码是2,尾数是1.2345。实型数据的存储格式我暂时了解到这里。
符号位 | 阶码 | 尾数 |
8.3字符型数据的存储
每个字符在内存中占用一个字节,存储它的ASCII码值,例如字符型常量'A'的ASCII码值为65,它在内存中以下列形式存放:
0 1 0 0 0 0 0 1 |
九、基本数据类型
类别 | 名称 | 类型名 | 数据长度 | 取值范围 |
整型 | [有符号]整型 | int | 32位 | -2,147,483,648 ~ 2,147,483,647 即 |
[有符号]短整型 | short[int] | 16位 | -32,768~32,767 | |
[有符号]长整型 | long[int] | 32位 | -2,147,483,648 ~ 2,147,483,647 | |
无符号整型 | unsigned[int] | 32位 | 0~4,294,967,296 即 | |
无符号短整型 | unsigned short[int] | 16位 | ||
无符号长整型 | unsigned long[int] | 32位 | ||
字符型 | 无符号字符型 | char | 8位 | 0~255 |
有符号字符型 | char | 8位 | -128~127 | |
实型 (浮点型) | 单精度浮点型 | float | 32位 | 约 |
双精度浮点型 | double | 64位 | 约 |
方括号中的内容可以省略。此表与Devc++编译系统的规定一致。在Turbo C编译系统中, int和unsigned型数据的长度只有16位。
解释:例如:char的范围(一个字节):
无符号:0~255;这是因为在无符号二进制数中,所有的8位都用于表示数值,所以最大数是11111111(二进制),即255(十进制)。
有符号:-128~127;这是因为在有符号二进制数中,最高位(第8位)被用作符号位,0表示正数,1表示负数。剩下的7位用于表示数值,所以最大正数是0111 1111(二进制),即+127(十进制),最小负数是1000 0000(二进制),即-128(十进制)。
其中,
00000000=0
00000001~01111111 纯二进制看待时 -> 1 ~ 127
01111111=(1 × 2⁶) + (1 × 2⁵) + (1 × 2⁴) + (1 × 2³) + (1 × 2²) + (1 × 2¹) + (1 × 2⁰) = (127)
1000 0000~1111 1111 纯二进制看待时 -> 128~255
1111 1111 ~1000 0000 当作补码看待时 -> -127~-128
1000 0001~1000 0000 当作补码看待时 -> -1~-128
-a=2^8-a 即 -128的表达256-128=128=2^7=1000 0000
9.1数据的范围
当范围不一样时,0±1也不一样。
0-1=-1逆时针 -1+1=0顺时针 255+1=0
-128-1=127逆时针 127+1=-128顺时针 0-1=255
若数据类型不同,字节就不同,所表示的数字就不同。例如:
- int有32位,255表示最低的那个字节8位被填成了255,最高位仍然保持为0,是正数:00000000 00000000 00000000 11111111。(11111111)₂ = (1 × 2⁷) + (1 × 2⁶) + (1 × 2⁵) + (1 × 2⁴) + (1 × 2³) + (1 × 2²) + (1 × 2¹) + (1 × 2⁰) = (255)₁₀
- 对于char来说,在计算机中只有11111111,因此为负数,是补码,除最高位-1取反得原码为10000001,因此是-1。
因此,不同的数据类型有不同的整数范围。
- 若想要当作纯二进制看待,则可以在数据类型前面加一个unsigned,它表示这个数不以补码表示,没有负数部分,只有0和正数部分。如下图。它所表示的正数范围会扩大,就不能表示负数部分了。如unsigned char:0~255。
- 若一个字面量常量想要表达自己是unsigned,可以在后面加u或U,如255U。
- 若想表达自己是long,用l或L表示。
同理:可以用这个逻辑去求int的最大数:0一直+1,直到它等于最大数,再+1就会变成负数。
9.2整型与整型常量(整数)
9.2.1整型
整型是不存在小数部分的数据类型。除了基本整型int以外,为了处理不同取值范围的整数,C语言提供了扩展的整数类型,它们的表达方式是在int之前加上限定词short、long或unsigned。
无符号的整型数据指不带符号的整数,即零或正整数,不包括负数。
存储有符号的整型数据时,存储单位的最高位是符号位,其余各位表示数值;存储无符号(指定unsigned)的整型数据时,存储单元全部用于表示数值。
C语言并未规定各类整型数据的长度,只要求short型不长于int型,long型不短于int型,long>int>short。
9.2.2整型常量(整数)
整型常量即常说的常数。只要整型常量的值不超出上表中列出的整型数据的取值范围,就是合法的常量。
整数的表示
C语言中的整数有十进制、八进制、十六进制三种形式表示。
- 十进制整型常量:由正负号和阿拉伯数字0~9的数字组成,没有前缀,不能以0开头(比如:0是可以的,但是09是不可以的)。
- 八进制整型常量:由正负号和阿拉伯数字0~7组成,首位数字必须是0,没有小数部分。(比如:023)
- 十六进制整型变量:由正负号和阿拉伯数字0~9、英文字符a~f或A~F组成,首位数字必须有前缀0x或者0X,没有小数部分。比如(0x00FF0000)。
- 例如,10、010和0x10分别是十进制、八进制、十六进制整数,它们表示不同数值的整数。10是十进制数值,010的十进制是8,0x10的十进制是16;又如,0386和0x1g是非法的整型常量,因为0386是八进制但是含有非法数字8,0x1g是十六进制但是含有非法字符g。
整数的类型
- 首先根据整数后的字母后缀判断整数的类型。后缀l或L表示long型常量,如-12L,01234567890L;后缀u或U表示unsigned型常量,如12u、034u、0x2fdU;后缀l和u或L和U表示unsigned long型常量,如4294967295LU。
- 若整数后没有字母,就根据整型常量的值判断类型。如:取值在-2,147,483,648 ~ 2,147,483,647之间的整数是int或long型常量;超出上述范围,蛋取值在2,147,483,648~4,294,967,295之间的非负整数可以看成unsigned或unsigned long型常量。
9.2.3选择整数类型
为了准确表达内存,做底层程序的需要,没有特殊需要,选择int。
现在的CPU的字长普遍是32位或64位,一次内存读写就是一个int,一次计算也是一个int,选择更短的类型不会更快,甚至可能更慢。
现代的编译器一般会设计内存对齐,所以更短的类型short、char实际在内存中有可能也占据一个int的大小(虽然sizeof告诉你更小)。除非做底层程序,面对硬件的时候,必须用什么就用什么,其他都用int。
unsigned与否只是输出的不同,内部计算是一样的。
python和Java对于整数没有区分这么多。
9.3字符型与字符型常量
9.3.1字符型
每个字符型数据在内存中占用一个字节,用于存储它的ASCII码。所以C语言中的字符可以写成字符常量的形式,也可以用整数(相应的ASCII码)表示。
例如:设ch是字符变量,字符型常量A的ASCII码值是65,则ch='A'和ch=65等价。
整型变量和字符型变量的定义和值可以互相交换。交换时,整数数据的取值范围时有效的ASCII码。
9.3.2字符型常量
字符型常量指单个字符,用一对单引号及其所括起的字符来表示。如'a'、'X'、'?'、' '(空格符)等都是字符型常量。
ASCII字符集:
每个字符在内存中占用一个字节,用于存储它的ASCII码。所以C语言中的字符具有数值特征,可以像证书一样参加运算,此时相当于对字符的ASCII码进行运算。
例如:字符'A'的ASCII码时65,则'A'+1=66,对应于字符'B'。
两个字符相减,得到他们在表中的距离。
- 字母在ASCII表中是顺序排列的。
- 大小写字母是分开排列的,不表示一个数字不在一起。
字符大小写转换方式:
一:C语言标准库<ctype.h>中提供了用于大小写转换的函数,包括函数toupper()转换为大写、tolower()转换为小写。
二:使用位运算
利用ASCII值大小写字母差值是32进行转换。
转义字符
有一些字符,如回车符、退格符等控制码,它们不能在屏幕上显示,也无法从键盘输入,由一个反斜杠"\"开头,后面跟上一个字符,这两个字符合起来,组成一个字符。
字符 意义 \b 回退一格 \t 到下一个表格位 \n 换行
\r 回车 \" 双引号 \' 单引号 \\ 反斜杠本身 \ddd 1~3位八进制整数所代表的字符 \xhh 1~2位十六进制整数所代表的字符 例如:\102表示ASCII码是八进制数102的字符,即字母'B';\x41表示ASCII码是十六进制数41的字符,即字母'A'。这样,ASCII字符集中所有的字符都可以用转义字符表示。
\b:回退一格,如果后面有输出则输出,没有输出则无反应。
\t:到下一个表格位、用一个\t可使得输出从下一个制表位开始,用\t使得上下两行对齐。与数量无关,与位置有关。
9.4实型与实型常量(实数)
9.4.1实型
- 实数类型又称为浮点数,指存在小数部分的数。
- 浮点型数据有单精度浮点型(float)和双精度浮点型(double)两种,它们表示数值的方法是一样的,主要区别在于数据的精度和取值范围不同。与float型数据相比,double型数据的精度高,取值范围大。
- 每个单精度浮点型数据在内存中占用4个字节存储空间,它的有效数字一般有7-8位,取值范围为(
);双精度浮点型数据占用的存储空间是单精度浮点型数据的两倍,即8个字节,它的有效数字一般有15~16位,取值范围为
,这些指标与具体的计算机系统和C语言编译系统有关。
- 就浮点型数据而言,数值精度和取值范围是两个不同的概念。例如:实数1234567.89在单精度浮点型数据的取值范围内,但它的有效数字超过了8位,如果将它赋值给单精度浮点型变量,该变量值就是1234567.80,其中最后一位是随机数,损失了有效数字,降低了精度。因此实数在计算机中只能近似表示,没办法表示区间内的所有小数,运算中也会产生误差。
- 浮点型是带小数点的数值。浮点这个词的本意就是指小数点是浮动的,小数点出现在一个数的第几位是可以动的,是计算机内部表达非整数(包含分数和无理数)的一种方式。另一种方式叫做定点数,小数点的位置是固定的,不过在C语言中你不会遇到定点数。人们借用浮点数这个词来表达所有的带小数点的数。
- 在运算中,有整数有浮点数,C语言会先把整数变成浮点数,结果用%f表示。
- 在实际开发中,哪个语言都建议弃用float,只采用double就可以,long double暂时没有必要。
9.4.1.1输出精度
但是发现2种数据类型的输出结果是相同的,因为系统默认会输出小数点后6位小数。那他们的区别是什么呢?
float的数据的精度有效位是6~7位,double的数据的精度有效位是15~16位。这里的有效位指的是从左边数不为0的那一位开始,他们的意义区别就是精度不同。
若想指定输出多少位小数怎么办?
方法1:可以在输出语句的%后面加上.n,n表示你要输出的数 小数后面有几位。输出是四舍五入的。
方法2:也可以用%m.nf,指定输出数据整数部分+小数部分+小数点共占m位,其中有n位是小数。如果数值长度小于m,则左端补空格,若数值长度大于m,则按实际位数输出。
如下图示例,float类型的输出2位小数,double类型的输出8位小数。
9.4.1.2超过范围的浮点数
- printf输出inf表示超过范围的浮点数:±∞
- printf输出ind或nan表示不存在的浮点数
若改成%d就会出错,整数不能除以0,无穷不能用整数但是可以用浮点数表达。
9.4.1.3浮点数的运算精度
10位数是真实数字。若判断两个浮点数是否相等,判断答案可能是错误的。可以求两个浮点数的差的绝对值是否小于很小的数字。float类型的fabs(f1-f2)<1e-8;double类型的fabs(f1-f2)<1e16
9.4.2浮点数在计算机的内部表达
浮点数在计算时是由专用的硬件部分实现的。使用一种编码形式。一个比特sign部分表示正负,11个比特表示指数,好多比特(不一定是52个比特)表示分数部分。这个专用的硬件在计算时会分开计算,算完再编码编回来展示。计算double和float所用的方法和硬件部件是一样的。整数类型没有特殊需求用int,浮点数没有特殊需要用double。
9.4.3实型常量
实型常量即常说的常数,又称浮点数,可以用十进制浮点表示法和科学计数法表示。实型常量都是双精度浮点型。
- 浮点表示法:实数由正号负号、0~9和小数点组成,必须有小数点,而且小数点的前后至少一边要有数字。实数的浮点表示法又称实数的小数形式。
- 科学计数法:实数由正号负号、数字和字母e(E)组成,e是指数的标志,在e之前要有数据,e之后的指数只能是整数。实数的科学计数法又称实数的指数形式。一般用于表示很大或很小的数,如普朗克常数6.026*10^-27表示为6.02E-271
9.4.4整数与浮点数的转换
在浮点数的取值范围内,整数转换为浮点数不会有精度的损失,浮点数转换为整数后,会丢弃小数位。
9.4.5整型浮点型的运算
若运算符两边一个是整型,一个是浮点型,会把整型变成浮点型再运算,输出浮点型。
例:
int a =6;
printf("sizeof(a+1.0)=%d",sizeof(a+1.0)); //输出8
因为a是整型,1.0是double型,所以把a整型改成double型6.0进行运算,6.0+1.0=7.0,是double型,因此输出sizeof(8.0)=sizeof(double)=8。
9.5逻辑bool
9.5.1逻辑类型(表示关系运算和逻辑运算结果的量)
#include<stdbool.h>
C语言c89之后确定有bool这个类型
输入输出时候没有特别的办法,只能输出为一个整数,没有办法printf出true或false,只能输出1或0.
9.5.2逻辑运算 对逻辑量进行与或非运算
逻辑运算是逻辑量进行的运算,结果只有0或1,逻辑量是关系运算或逻辑运算的结果。
运算符 | 描述 | 实例 | 结果 |
! | 逻辑非 | !a | 如果a是true,结果就是false; 如果a是false,结果就是true。 |
&& | 逻辑与 | a&&b | 如果a和b都是true,结果就是true; 否则就是false。 |
|| | 逻辑或 | a||b | 如果a和b有一个是true,如果为true; 如果两个都是false,结果为false。 |
若要表达数学中的区间,x∈(4,6)或x∈[4,6],应该如何写表达式?
x>4&&x<6;
x>=4&&x<=6;
如何判断一个字符c是否是大写字母?
c>='A'&&c<='Z'
小写字母?
c>='a'&&c<='z'
age>20 && age<30 // true:age∈(20,30)之间
index<0 || index>99 // true:index<0或index>99 false:index∈[0,99]
!age<20 //如果age=0,!age=1;age!=0,!age=0;!age从始至终都<20,所以表达式永远都是1
! (age<20) //表示age>=20
优先级:! > && > ||
不要做嵌入式赋值,容易出错,不易阅读。应该拆成若干表达式用明显的正确顺序表达。
短路
逻辑运算是自左向右的,如果左边的结果已经能够决定结果了,就不会做右边的计算。
- a==6&& b==1
对于&&,左边是false时就不做右边了
对于||,左边是true时就不做右边了
因此不要把赋值,包括复合赋值 组合进表达式。
十、数据的输入输出
C语言中,数据的输入输出都是通过函数调用来实现的。
10.1整数的输入输出
调用函数scanf()和printf()实现整型数据的输入和输出时,应根据数据的类型和输入输出的形式,在函数调用的格式字符串中使用相应的格式控制说明。
数据类型 | 输入输出形式 | ||
十进制 | 八进制 | 十六进制 | |
int | %d | %o | %x |
long | %ld | %lo | %lx |
unsigned | %u | %o | %x |
unsigned long | %lu | %lo | %lx |
格式 | 含义 |
%d | 以十进制形式输入输出一个整数 |
%u | 以十进制形式输入输出一个无符号整数 |
%o | 以八进制形式输入输出一个整数 |
%x | 以十六进制形式输入输出一个整数 |
指定输出宽度
可以加宽度限定词,指定整型数据的输出宽度。例如输出格式控制说明%md,制定了数据的输出宽度为m(包括符号位),若数据的实际位数(含符号位)<m,则左端补空格;若>m,则按实际位数输出。
如果系统中int和long的大小相同,使用%d转换说明;移植到其他系统(int 和long类型的大小不同)中会无法正常工作;
10.2浮点数的输出
调用函数scanf()和printf()实现实型数据的输入和输出,在函数调用的格式字符串中使用相应的格式控制说明。
函数 | 数据类型 | 格式 | 含义 |
scanf | float | %f | 以小数形式或指数形式输入一个单精度浮点数,其中%e输出的是科学计数法。 |
%e | |||
double | %lf | 以小数形式或指数形式输入一个双精度浮点数,其中%e输出的是科学计数法。 | |
%le | |||
printf | float | %f | 以小数形式输出浮点数(保留6位小数) |
%e | 以指数形式输出浮点数(小数点前有且仅有一位非零的数字) | ||
double | %f | 以小数形式输出浮点数(保留6位小数) | |
%e | 以指数形式输出浮点数(小数点前有且仅有一位非零的数字) |
10.3字符型数据的输入和输出
字符的输入输出可以调用函数getchar()、putchar()、scanf()、printf()。其中getchar()函数和putchar()函数只能处理单个字符的输入和输出。scanf()和printf()在控制字符串中相应的格式控制说明为%c。
C语言中,一个字符型数据在内存中用一个字节存储它的ASCII码,可以按字符形式输出,也可以按整数形式输出。按字型形式输出时,可以调用函数putchar()或print(),格式控制说明用%c,系统自动将存储的ASCII码转换成相应的字符后输出;按整数形式输出时,可以调用函数printf(),格式控制说明用%d、%o、%x等,直接输出它的ASCII码。
同样,一个整数在有效的ASCII码范围内,也可以转换为字符形式输出。
十一、类型转换
C语言中,不同类型的数据可以混合运算。但首先要转换成同一类型,然后再做运算。自动转换由C语言编译系统自动完成,强制转换则通过特定的运算完成。
11.1自动类型转换
1、非赋值运算的类型转换
当运算符的两边出现不一致的类型时,会自动转换成范围大的类型。
char->short->int->long->long long
int->float>double
unsigned short->unsigned
int->unsigned->long->unsined long->double
对于printf来说,任何<int的类型会被转换成int;float会被转换成double。所以%f足以输出double不需要%lf因为无论是float还是double传给printf时都变成double了
但是scanf不会,要输入short,需要%hd、longlong用%ld、int%d
2、赋值运算的类型转换
将赋值号右侧表达式的类型自动转换成赋值号左侧变量的类型。
因此,若赋值号右侧表达式的类型比赋值号左侧变量的类型级别高,运算精度会降低。
11.2强制类型转换
通常是转换成较小的类型需要 :(类型)值
(int)10.2表示把double型变成int型
(short)32表示把int型变成short型
注意安全性,小的变量不能总能表达大的量。
用i的值计算一个新的值s,而不是改变i .
强制类型转换的优先级高于四则运算。
double a=1.0;
double b=2.0;
int i=(int)a/b;
//表示先把a转换成int类型,再除以b,结果是double,把这个值转换成int赋值给i。
int a=5;
int b=6;
double d=(double)(a/b); //会先计算a/b整数的结果,再强制转换为double,赋值给double的d。
十二、表达式
一个表达式是一系列运算符和算子的组合,用来计算一个值。
amount=x*(1+0.033)*(1+0.033)*(1+0.033);
total=57;
count=count+1;
value=(min/2)*lastValue;int sides=8;
- 上面的式子整个一行都是表达式。
- 运算符(operator)是指进行运算的动作,比如加法运算符“+”,减法运算符“-”。
- 算子(operand)是指参与运算的值,这个值可能是常数,也可能是变量,还可能是一个方法的返回值。
- a=b+5 中a、b、5都是算子;=、+是运算符
12.1部分运算符的优先级和结合性
优先级 | 运算符种类 | 运算符 | 结合关系 |
1 | 逻辑运算符 | !(非) | 从右向左 |
2 | 算术运算符 | + - ++ --(正、负、加加、减减)强制类型转换 | 从右向左(单目) |
* / %(乘除取余) | 从左向右(双目) | ||
+ -(加、减) | 从左向右(双目) | ||
3 | 关系运算符 | < <= > >= | 从左向右 |
== != | |||
4 | 逻辑运算符 | && | 从左向右 |
|| | |||
5 | 条件运算符 | ?: | 从右向左 |
6 | 赋值运算符 | = += -= *= /= %= (赋值) | 从左向右 |
7 | 逗号运算符 | , | 从左向右 |
12.2逻辑表达式
12.2.1逻辑运算符
C语言提供了三种逻辑运算符,单目!(逻辑非)、双目&&(逻辑与)和||(逻辑或)。
单目 | 双目 | ||
! | && | || | |
逻辑非 | 逻辑与 | 逻辑或 |
用1代表“真”,0代表“假”。例如,在逻辑表达式(x>=3)&&(x<=5)中,&&是逻辑运算符,关系表达式x>=3或x<=5是逻辑运算对象,逻辑运算的结果是1或0。例如,a和b是逻辑量,则对a和b可以进行的基本逻辑运算包括!a(或!b)、a&&b和a||b三种。
a | b | !a | a&&b | a||b |
非0(真) | 非0(真) | 0 | 1 | 1 |
0(假) | 0 | 1 | ||
0(假) | 非0(真) | 1 | 0 | 1 |
0(假) | 0 | 0 |
12.2.2逻辑表达式
用逻辑运算符将关系表达式或逻辑量连接起的式子成为逻辑表达式。逻辑运算对象是值为“真”或“假”的逻辑量,它可以是任何类型的数据,如整型、浮点型、字符型等。
与其他表达式的运算不同,求解用逻辑运算符&&或||连接的逻辑表达式时,按从左到右的顺序计算该运算符两侧的操作数,一旦能得到表达式的结果,就停止计算。
12.3算术表达式
12.3.1算术运算符
算术表达符分为单目运算符和双目运算符两类,单目运算符只需要一个操作数,双目运算符需要两个操作数。运算对象包括常量、变量、函数等
目数 | 单目 | 双目 | |||||||
运算符 | ++ | -- | + | - | + | - | * | / | % |
名称 | 自增 | 自减 | 正值 | 负值 | 加 | 减 | 乘 | 除 | 模(求余) |
取余(需要特别注意运算数的类型,取余操作只能用于整数类型)
计算时间差
int hour1,minute1;
int hour2,minute2;
scanf("%d %d",&hour1,&minute1);
scanf("%d %d",&hour2,&minute2);
//无法考虑到借位的情况。//2:30
//1:40
因此可以转化成以分钟或者小时为单位计算
若统一到小时 要带小数点 ,计算会有误差,但整数没有误差。
hour*60+minute->转换为以分钟为单位。
time/60->小时部分;time&60->分钟部分。
同样说一个人5英尺7寸,也同样要foot+inch/12转换成英寸计算
12.3.2自增运算符和自减运算符
自增运算符和自减运算符有两个功能。
1、使变量的值增1或减1。
例如:++n和n++都相当于n=n+1;--n和n--都相当于n=n-1。
2、取变量的值作为表达式的值。
例如:计算++n和n++的值。则:
++n的运算顺序是:先执行n=n+1;再将n的值作为表达式++n的值。
n++的运算顺序是:先将n的值作为表达式++n的值;再执行n=n+1。
- 注意:n和++n 值的不同可能会影响运算的结果与预期不相符合。
- 可以单独使用,但不要组合进表达式。
- 自增运算符和自减运算符的运算对象只能是变量,不能是常量或表达式。形如3++、++(i+j)的都是非法表达式。
12.4关系表达式
12.4.1关系运算符
关系运算符时双目运算符,用于对两个操作数进行比较。
运算符 | < | <= | > | >= | == | != |
名称 | 小于 | 小于或等于 | 大于 | 大于或等于 | 等于 | 不等于 |
优先级 | 高 | 低 |
12.4.2关系表达式
用关系运算符将两个表达式连接起来的式子,称为关系表达式。关系表达式的值就是1或0,它的类型是整型。
12.5条件表达式
条件表达符时C语言中的一个三目运算符,它将三个表达式连接在一起,组成条件表达式。条件表达式的一般形式是:表达式1?表达式2:表达式3。条件表达式的运算过程是:先计算表达式1的值,如果它的值为非0(真),将表达式2作为条件表达式的值;否则将表达式3的值作为条件表达式的值。
例如:以下两种代码等价
if(a>b){
z=a;
}
else a=b;
z=(a>b)?a:b;
12.5.1条件运算
- 条件运算:
count=(count>20)?count-10:count+10;
- 日常:
if (count>20)
count=count-10;
else
count=count+10;
- 这两者表达的含义相等。
- 条件运算符的优先级高于赋值运算符,但是低于其他运算符。
- 条件运算符是自右向左结合的
- 不希望使用嵌套表达式,如下,理解程序太困难。
12.6赋值表达式
12.6.1赋值运算符
赋值运算符=的左边必须是一个变量,作用是把一个表达式的值赋给一个变量。
12.6.2赋值表达式
用赋值运算符将一个变量和一个表达式连接起来的式子称为赋值表达式。简单形式是:
变量=表达式
运算过程:
- 计算赋值符右侧表达式的值。
- 将赋值运算符右侧表达式的值赋值运算符左侧的变量。若两侧的数据类型不同,则将赋值运算符右侧表达式的类型自动转换成赋值运算符左侧变量的类型。
- 将赋值运算符左侧表达式的值作为赋值表达式的值。变量的类型就是赋值表达式的类型.
交换变量:
使用第三个变量:
int a=3;
int b=5;
int t=0;
t=a;
a=b;
b=t;
printf("a=%d、b=%d\n",a,b);可以使用前面提到的断点进行调试,观察一步一步的变化。
12.6.3复合赋值
- +-*/%,可以和赋值运算符=结合起来形成复合赋值运算符:+=、-=、*=、/=、%=
- 注意两个运算符中间不要有空格。
total+=5;即total=total+5;
total*=sum+12;即total=total*(sum+12);
total+=(sum+100)/2;即total=total+(sum+100)/2;
12.7逗号表达式
C语言中,逗号既可以作分隔符,也可以作运算符。作为分隔符使用时,用于间隔说明语句中的变量或函数中的参数,例如:
int a,b,c;
printf("%d %d",x,y);
逗号作为运算符使用时,将若干个独立的表达式连接在一起,组成逗号表达式。
一般形式是:表达式1,表达式2,...,表达式n 先计算表达式1,然后计算表达式2的值,最后计算表达式n的值,并将表达式n的值作为逗号表达式的值。
例如a,b,c都是整型(a=2),(b=3),(c=a+b) ,从左到右求解这三个表达式,该逗号表达式的值和类型由最后一个表达式c=a+b决定,值是5,类型是整型。
12.6.1逗号运算
- 逗号用来连接两个表达式,并以其右边的表达式的值作为它的结果。
- 逗号的优先级是所有运算符中最低的,所以它两边的表达式会先计算;
- 逗号的组合关系是自左向右,所以左边的表达式会先计算,而右边表达式的值就留下来作为逗号运算的结果。
- 目前只在for中使用到。
12.7位运算
运算符 | 名称 |
& | 按位“与” |
| | 按位“或” |
^ | 按位“异或” |
~ | 按位取反 |
<< | 左移 |
>> | 右移 |
12.8其他运算
12.8.2长度运算符
sizeof是一个单目运算符,用来返回变量或者数据类型的字节长度。使用长度可以增强程序的可移植性,使之不受具体计算机数据类型长度的限制。
12.8.3特殊运算符
C语言中还有一些特殊的、具有专门用途的运算符。
()括号:用来改变运算顺序。
[]下标:用来表示数组元素。
* 和 &:与指针运算有关。
->和. :用来表示结构分量。
十三、判断语句
当时在4.5表达式里说到的“计算时间差”,也可以用if语句表达。
这里的if语句意思是如果(im小于0),那么执行大括号里所有的语句;如果条件不被满足,那么语句内的将不被执行。
if(条件){
...
}//条件成立,执行大括号里面的语句;如果不成立,则不执行。
13.1判断的条件
这里if后的()里是条件,是计算两个值之间的关系,所以叫做关系运算。
运算符 | 意义 |
== | 判断两个值是否相等 |
!= | 判断两个值是否不相等 |
> | 判断两个值是否大于 |
>= | 判断两个值是否大于或等于 |
< | 判断两个值是否小于 |
<= | 判断两个值是否小于或等于 |
关系运算的结果
- 当两个值得关系符合关系运算符得预期时,关系运算得结果为整数1,否则为整数0。
- printf("%d\n",5==3); //判断5是否=3,显然≠,因此输出为0
- printf("%d\n",5>3); //判断5是否 > 3,显然>,因此输出为1
- printf("%d\n",5<=3); //判断5是否<=3,显然>,因此输出为0
优先级
- 所有的关系运算符的优先级比算数运算得低,但是比赋值运算的高。
- 从左到右进行
13.1.1判断里有两个条件怎么写
- if (1<=n<=10)是可以通过编译的,但是最后的结果为判断表达式真or假,不代表1<=n<=10
- 1<=n&&n<=10 才表示1<=n<=10
找零计算器
- 需要用户做两个操作:输入购买的金额,输入支付的票面,找零计算器则根据用户的输入做出相应的动作:计算并打印找零,或告知用户余额不足以购买。
- 从计算机程序的角度看,这就是意味着程序需要读用户的两个输入,然后进行一些计算和判断,最后输出结果。
#include <stdio.h>
int main(){
int payvalue=0;
int amount=0;//初始化
scanf("%d %d",&amount,&payvalue);//价值多钱 票面
int change;
if(payvalue<amount){
printf("钱不够支付");
}
else if (payvalue==amount){
printf("正好,不找回");
}
else{
change=payvalue-amount;
printf("%d",change);
}
return 0;
}
13.1.2条件不成立时
if(条件){
...//满足这个条件 执行这个大括号里的语句
}
else{
...//上面条件不满足时执行这个大括号里的语句
}
若if语句这一行结束的时候并没有表示语句结束的“;”,而后面的赋值语句写在if的下一行,并且缩进了,在这一行结束的时候有一个表示语句结束的“;”。这表明这条赋值语句是if语句的一部分,if语句拥有和控制这条赋值语句,决定它是否要被执行。
例题三个数比较大小
当大括号里只有一句,大括号可以省略。
13.3嵌套的判断
当if的条件满足或者不满足的时候要执行的语句也可以是一条if或if-else语句。例如:
没有大括号的话,else总是和最近的那个if匹配。缩进不能暗示else的匹配。例如下面这个图片,else是和if(player2move==2)这个if并列,与缩进并没有关系。
13.4级联的判断
分段函数:
f(x)=-1;x<0
=0;x=0
=2x;x>0
可以用以下代码表示
int f;
if(x<0){
f=-1;
}
else if(x==0){
f=0;
}
else{
f=2*x;
}printf("%d",f); //单一出口的更划算
通用表达方式
if(条件1){
满足条件1执行的语句;
}
else if(条件2) {
满足条件2执行的语句;
} //else if的这部分可以有很多个
else{
不满足上面的条件执行的语句;
}
十四、多路分支:switch-case语句
#include <stdio.h>
int main(){
int type;
scanf("%d",&type);
if(type==1){
printf("你好");
}
else if(type==2){
printf("早上好");
}
else if(type==3) {
printf("晚上好");
}
else if(type==4){
printf("再见");
}
else
printf("啊,什么啊?");
return 0;
}
这么一大段啰嗦的if-else语句,并且判断有很多步骤,可以用switch-case语句简单表示:
#include <stdio.h>
int main(){
int type;
scanf("%d",&type);
switch(type) { //type必须是int,不能是double等非整数型
case 1: //可以是case 2-1:
printf("你好");
break;
case 2:
printf("早上好");
break;
case 3:
printf("晚上好");
break;
case 4:
printf("再见");
break;
default:
printf("啊,什么啊?");
}
return 0;
}
因此swith-case语句可以表达为:
switch(控制表达式) { //控制表达式只能是整数型的结果
case 常量: //常量可以是常数,也可以是常数计算的表达式如“1+1”等,也可以是前面用const int定义的一个常变量
语句
....
case 常量:
语句
....
default:
语句
....
}
分段函数可以用switch-case吗?划算吗?
分段函数可以用switch-case,但不划算,因为switch需要整数型表达式,且case需要常数。
14.1实现原理
语言的底层就是算法,所以switch-case的底层也是算法: 数组和二分查找。
switch-case是一个条件语句,也就是说: 如果满足条件,那么就执行对应的指令,也就是: 找条件!那么就是查找!也就是算法里的查找!那么为什么是数组和二分查找呢?
其实switch-case的状态值只能存放一个int的大小,比如byte,char,shor,int等。如果大于一个int的大小就不可以,比如long,double等。那么String为什么也可以呢?因为switch(String)会取String对应的hashcode(哈希码),而hashcode正好是一个int的大小。
正是因为状态值是一个int的大小,所以可以对状态值进行排序,如果状态值是紧凑的(例如case的值是0、1、2、3、4),那么就会将状态值优化为tableswitch(可以理解为数组),查找效率就是O(1)的;如果状态值是分散(例如case值是2、5、9)的,那么就会使用二分查找来找条件。
14.2break
根据表达式的结果,寻找匹配的case,并执行case后面的语句,一直到break为止跳出循环,如果没有遇到 break 那就接着执行下面的语句。如果所有的case都不匹配,那么就执行default 后面的语句;如果没有default,那么程序就什么都不会执行,直接跳过switch case 语句。
接力break——退出所有循环:(题目:输入一个小于10的金额,让计算机输出如何用1角、2角、5角凑出这个金额。找到第一种情况就退出所有循环不再继续)
变换exit的值,查看是否是从最里层循环退出的。
#include<stdio.h> int main(){ int amount=0; scanf("%d",&amount);//3元 30角 int exit=0; int one,two,five; for(one=1;one<amount*10;one++){ for(two=1;two<amount*10/2;two++){ for(five=1;five<amount*10/5;five++){ if(one+two*2+five*5==amount*10){ printf("可以用%d个1角、%d个2角%d个5角凑成%d\n",one,two,five,amount); exit=1; break;//只让他离开了这个循环而进入了下一层循环 } } if(exit==1) break; } if(exit==1) break; } return 0; }
14.3continue
跳过循环这一轮剩下的语句进入下一轮。
14.4continue VS break
异:
- break:跳出循环;
- continue:跳过循环这一轮剩下的语句进入下一轮;
同:
- 都只能对它所在的那层循环生效。
14.5goto
需要离开的语句后写 “goto out;” ,离开后去到的地方写“out:”
适合用于多层嵌套循环的最内层跳出所有循环。
以下两类代码作用一致(更好的认识goto的作用):
#include<stdio.h> int main(){ int amount=0; scanf("%d",&amount);//3元 30角 int one,two,five; for(one=1;one<amount*10;one++){ for(two=1;two<amount*10/2;two++){ for(five=1;five<amount*10/5;five++){ if(one+two*2+five*5==amount*10){ printf("可以用%d个1角、%d个2角%d个5角凑成%d\n",one,two,five,amount); goto out; } } } } out: return 0; }
#include<stdio.h> int main(){ int amount=0; scanf("%d",&amount);//3元 30角 int exit=0; int one,two,five; for(one=1;one<amount*10;one++){ for(two=1;two<amount*10/2;two++){ for(five=1;five<amount*10/5;five++){ if(one+two*2+five*5==amount*10){ printf("可以用%d个1角、%d个2角%d个5角凑成%d\n",one,two,five,amount); exit=1; break;//只让他离开了这个循环而进入了下一层循环 } } if(exit==1) break; } if(exit==1) break; } return 0; }
但是因为goto可能会破坏程序的结构性:
- 当一个函数内部使用了GOTO语句来跳转到另一个函数时,那么就可能会破坏函数调用栈,导致程序崩溃或产生不可预料的结果;
- 当程序中大量使用GOTO语句时,执行流程会变得复杂和混乱,不易于阅读和理解;
- 由于GOTO语句可以随意跳转到代码中的任何位置,因此很容易出现逻辑错误和数据冲突等问题,例如,如果一个循环体内使用了GOTO语句来跳过某个条件判断,那么就可能会导致无限循环或死锁的问题;
- 此外,由于GOTO语句不受任何控制结构的约束,因此也很容易被恶意攻击者利用,通过篡改程序的执行流程来实现非法操作。
因此除了用于在从多层嵌套循环的最内层跳出所有循环外,谨慎运用最好不要用goto。
十五、循环
判断一个数字的位数是几?
#include <stdio.h>
int main(){
int x=0;
scanf("%d",&x);
int n=0;
n++;
x/=10;
while(x>0){
n++;
x/=10;
}
printf("%d",n);
return 0;
}
核心重点是循环的条件
15.1while循环
就像if一样,条件满足就不断做后面的句子。不同的是,if只做一次而while要反复不断做直到条件不满足结束。
循环体内要有改变条件的机会,不然就会变成死循环。pta中一般会表现为“超时”。
while后面的大括号叫循环体。
while语句的一般表达式为:while(表达式){循环体}
15.2do-while循环
在进入循环的时候不做检查,而是在执行完一轮循环体的代码之后,再来检查循环的条件是否满足,如果满足则继续下一轮循环,不满足则结束循环。
15.3do-while和while的区别
do-while循环和while循环很像,区别是do-while在循环体执行结束的时候才判断条件;while要在判断满足时执行循环,条件不满足时结束循环。
do-while无论如何循环都会执行至少一遍;while则有可能一遍也不做。
15.4for循环
for(循环初始条件表达式;循环继续的条件表达式;循环每轮要做的表达式){
循环体
}
- for循环像一个计数循环:设定一个计数器,初始化它,然后在计数器达到某个值之前,重复执行循环体,而每执行一轮循环,计数器值以一定步进行调整,比如+1或-1。
- 循环控制变量i只在循环里被使用了,循环外它没有用到,属于局部变量。因此可以把变量i的定义写到for语句里面去。(只有c99可以使用)
若出现
![]()
或者 'for' loop initial declarations are only allowed in C99 or C11 mode 这样的问题,解决如下:
1、可以改变编译器环境
2、修改编译设置如下即可在for语句里定义变量:
-std=c99
-static-libgcc
15.5循环的计算和选择
#include<stdio.h>
int main(){
int i=0;
for(i=0;i<5;i++){
printf("i=%d ",i);
}
printf("\n最后i=%d",i);
return 0;
}
这里循环次数是5,最后i=4时 还要i++之后等于5,再次判断i不小于5而退出循环。
十六、函数
函数是一个完成特定工作的独立程序模块,包括库函数和自定义函数两种。例如scanf()和printf()等为库函数,由C语言系统提供定义,编程时只要直接调用即可;库函数中没有的函数,用户可以自己来定义,来得到一个明确的计算结果,使得代码变得更简洁明了。
函数定义的一般形式为:大括号{}和小括号()必须存在。
函数类型 函数名(形式参数表) /*函数首部*/
{
函数实现过程 /*函数体*/
}
16.1函数的定义——函数首部
由函数类型、函数名、形式参数表(简称形参表)组成,位于函数的第一行。函数首部中,函数名是函数整体的称谓,需要一个合法的标识符表示。函数类型指函数结果返回的类型,一般与函数最后一句的return语句中表达的类型一致。形参表中给出函数计算所要用到的相关已知条件,以类似变量定义的形式给出,其格式为:
类型1 形参1,类型2 形参2,...,类型n 形参n
形参表中各个形参之间用逗号分隔,每个形参前面的类型必须分别写明。数量可以是一个,多个,也可以没有形参。
例如: double cylinder(double r,double h)
表明函数类型、函数结果类型是double;函数名是cylinder;形参有两个r和h,都是double型,在cylinder()函数被调用时,这两个形参由主函数给出。
我们常见的主函数框架也符合如上定义。
#include<stdio.h> int main(){ ... return 0; }
- int指主函数返回int类型的结果,所以return后是一个整型。
- main是主函数名。
- 这种写法的形参为void,已经省略了,表明它在调用的时候不能传入任何参数,那么它也就不能获取命令行参数了。int main(void)
16.2函数的定义——函数体
函数体是函数的实现过程,由一对大括号内的若干条语句组成,用以计算或特定的工作,并用return返回运算的结果。大括号必须存在。
注意:在函数体中定义的变量,是普通变量,是在这个函数中使用的局部变量,不是形参。
16.3函数的定义——函数的调用
定义一个函数后,就可以在程序中调用这个函数。就像调用标准库函数时,只需要在程序的最前面用#include命令包含相应的头文件;调用自定义函数时,程序中必须先有此函数的定义。
1、函数调用过程:
任何C语言都是从主函数main()开始执行的,若遇到某个函数调用,则主函数被暂停执行,转而执行相应的函数,该函数执行完后再返回函数继续执行主函数。
2、函数调用的形式:
一般形式:函数名(实际参数表)
实际参数(简称实参),可以是常量、变量和表达式。这些值会被按照顺序依次来初始化函数中的参数。
3、参数传递:
- 函数定义时,位于其首部的参数被称为形参,主函数的参数被称为实参。
- 形参必须是变量,用于接收实参传递过来的值;而实参可以是常量、变量或表达式,作用是把常量、变量或表达式的值传递给形参。二者可以同名,也可以不同名。
- 形参除了能接受实参的值外,使用方法与普通变量类似。形参和实参必须一一对应,两者数量相同,类型尽量一致,建议初学者都保持一致,因为这可能不是预期的结果。
- 程序运行遇到函数调用时,实参的值依次传给形参,这就是参数传递。
- 参数传递的过程是单向的,只允许实参把值复制给形参,形参的值不会影响实参的值,永远只能传值给函数,因此若想定义一个函数交换两个数的值,是不可以的。
举例:自定义求和函数去使用:
#include<stdio.h>
void sum(int begin,int end) {
int sum=0;
for(int i=begin;i<=end;i++){
sum+=i;
}
printf("%d到%d的和是%d\n",begin,end,sum);
}
int main(){
int a=0;
int b=0;
scanf("%d %d",&a,&b);
sum(a,b);
sum(1,10);
sum(20,30);
sum(35,45);
return 0;
}
/*
void指不返回任何东西,没有结果被返回。
begin和end是函数的形参。
a和b是实参。(作为参数要传到自定义函数中)
*/
举例:验证形参的值不会影响实参的值:
#include<stdio.h>
int max(int a,int b) {
int c=0;
if(a>b) c=a;
else c=b;
return c;
}
int main(){
int a,b,c;
a=5;
b=6;
c=max(10,12);
printf("%d\n",c);
c=max(a,b);//依旧是5和6,不会被max里a、b的值改变
printf("%d\n",c);
c=max(c,23);
printf("%d\n",c);
return 0;
}
4、函数结果返回:
函数结果返回的形式如下:
return 表达式;
- 一般情况下,return语句中的表达式的类型应与函数类型一致;如果不一致,则以函数类型为准。
- return语句的作用有两个:一是结束函数的运行;二是带着运算结果(表达式的值)返回主函数。
- return语句中的表达式反映了函数运算的结果,通过return语句将改结果返回给主函数。
- return语句只能返回一个值,如果函数产生了多个运算结果,将无法通过return返回。
- 可以有多个的return语句。(在if-else中使用)
- 返回值可以赋值给变量,可以再传递给函数,甚至可以丢掉。
没有返回值的函数形式:可以没有return,不能使用带值的return,调用的时候不能做返回值的赋值。
void 函数名(参数表)
5、函数原型声明:
C语言要求函数先定义后调用,就像变量先定义后使用一样。如果自定义函数被放在主函数的后面,则需要在函数调用前,加上函数原型声明(或函数声明)。即与函数定义中的第一行(函数首部)相同,不同的是函数声明是一条语句,需要以分号结束,函数定义时不是语句,不能跟分号。
在先定义后调用的前提条件下,函数声明可以写在主函数内,也可以写在头文件那里。
函数声明的一般格式为:函数类型 函数名(参数表);
#include<stdio.h> int main(){ int a=0; int b=0; scanf("%d %d",&a,&b); void sum(int begin,int end); sum(a,b); sum(1,10); sum(20,30); sum(35,45); return 0; } void sum(int begin,int end) { int sum=0; for(int i=begin;i<=end;i++){ sum+=i; } printf("%d到%d的和是%d\n",begin,end,sum); }
如果在函数调用前,不定义,不声明,程序编译就会出错。
16.4函数的定义——函数程序设计
使用自定义函数程序有以下几个优点:
1、程序结构清晰,逻辑关系明确,程序可读性强;
2、解决相同或相似问题时不用重复编写代码,可通过调用函数来解决,减少代码量。
3、利用函数实现模块化编程,各模块功能相对独立,可以降低调试难度。
可以自定义函数判断这个数是否是完全平方数,本例中出现了两个return语句,执行时根据条件选择其中的一个,它们的作用相同,即结束函数与运行,并会送结果。return之后的语句将不会被执行。
例1、判断n是否是完全平方数:
#include<stdio.h>
int IsSquare(int n) {
//若n是完全平方数,则存在正整数m,使得m^2=n ,也有n=1+3+5+...+(2m-1)
for(int i=1;n>0;i+=2){
n=n-i;
}//25=1+3+5+7+9+11
if(n==0) return 1;//是完全平方数
else return 0;//不是完全平方数
}
int main(){
int n=0;
scanf("%d",&n);
if(IsSquare(n)==1) printf("是");
else printf("不是") ;
return 0;
}
例2、素数求和 其中判断是否是素数的函数
int IsPrime(int i) {
int ret=1;//是素数
for(int k=2;k<=i;k++) {
if(i%k==0){
ret=0;//不是素数
break;
}
}
return ret;
}
关于函数的一些细节:
- 不允许函数嵌套定义。
- 调用函数时的圆括号里的的逗号是标点符号,不是逗号运算符。f(a,b)是标点符号;f((a,b))是逗号运算符,要先做括号里的运算。
- 返回语句return (i)没错,返回i的值,但是会让人误以为是个函数,没有意义因此不建议这样写。
十三、数组
十四、指针
十五、验证
1、测试程序常使用边界情况:如有效范围两端的数据、特殊的倍数等。
例如判断一个数字的位数是几?可以测试个位数、10、负数、0。
2、可以在程序适当的地方插入printf来输出变量的内容、输出运行到哪里然后输出一句话。
十六、类型占用内存的情况
char、bool占用1个byte/字节;数的范围: 即
short占2个byte/字节;数的范围:即
int(32位)、float、long(32位)占用4个byte/字节;数的范围:
double、long long占用8个字节;
long double占用16个字节。
其中int和long类型的大小(即它所占用的字节数)都与cpu里面寄存器的位数有关,通常是一个字,一个寄存器可以传输的大小。除了这两个类型以外其他类型都有固定的大小。
在 16 位上,int通常占用 2 个字节(16 位),而在 32 位或 64 位上,
int
通常占用 4 个字节(32 位)或 8 个字节(64位)。然而,这并不是绝对的,因为 C 语言标准只规定了int
类型的最小取值范围(-32767 到 32767),并没有明确规定其大小。如果你需要在不同平台上保持一致的整数类型大小,可以使用 C99 标准引入的固定宽度整数类型,如
int32_t
、uint32_t
等。这些类型在<stdint.h>
头文件中定义,确保在所有平台上具有相同的大小。32或64:一台计算机的字长指的是 这台计算机寄存器是多宽=这台计算机是几个比特=cpu和ram内存之间(总线)每一次的传输数据是多少比特。
一个int表达的是一个寄存器的大小,比int小的表达的是计算机中的某些位,比int大的可能是几个寄存器拼起来。
8bit/比特位 = 1Byte/字节,1024Byte/字节 = 1KB,1024KB=1MB,1024MB=1GB,1024GB=1TB。1024=2^10。
bit是计算机中表示数据的最小单位,每个bit只有0和1两种状态。
字节(byte)是存储信息的基本单位,1字节等于8比特(bit)。
内存中的表达形式:int用二进制数(补码)、浮点数用编码。
sizeof()是一个运算符,给出某个类型或变量在内存中所占据的字节数。
十七、C以后的语言向两个方向发展:
- C++/Java更强调类型,对类型的检查更严格。
- JavaScript、Python、PHP不看重类型,甚至不需要事先定义
十八、如何输出变量的地址?
十九、习题题目&&解析
二十、0的故事
这是在devc++中打印出来的,第一个数字是零,第二个是大写字母O。
0之所以这样写是因为从19世纪末20世纪初,当人类开始有电报的时候,当时手写英文里数字0和大写O没什么区别,所以就规定要把0写成类似于空集的长相,这个传统一直延续到了有计算机,计算机最开始的程序也是程序员手写交给操作员打在计算机上,所以这个传统也是这样延续到今天。因此专门为0设计了这样的字体。