目录
1. 基本数据类型
也叫 内置类型 ,即编程语言自带的基本数据类型,无需额外定义或导入,直接就能定义变量,如下:
1:char //字符数据类型
典型的比如 ASCII 字符表:
第一部分:控制字符
从0 ~ 31,这些字符并不是用来显示可见符号的,即不会在屏幕/终端上作为文字内容展示出来,而是用于控制文本的格式和行为,影响显示或设备的操作。如下:
举个例子:
- 换行符(LF, 10):将光标移动到下一行。
- 回车符(CR, 13):将光标移动到当前行的开头。
Windows下:win键 + R,打开PowerShell
输入:Write-Host ("Hello" + [char]13 + "World!" + [char]10 + "Test")
【Write-Host
是 PowerShell 中的一个命令,用于将文本直接输出到控制台;[char]13
表示回车符(CR),[char]10
表示换行符(LF)】
输出:World!
Test
第二部分:可打印字符(0 ~ 126)
相对的,就是在文本中可以直接显示并可见的字符。如下:
同样的测试方法:
输入:Write-Host ([char]72 + [char]101 + [char]108 +[char]108 + [char]111 + [char]33)
输出:Hello!
回到编程的角度上:
常用的就三小段:字符0 ~ 9,A ~ Z,a ~ z;只需记住0,A,a对应的十进制,往后递增加1,大小写间的十进制差是32。并且 char 类型(字符类型)的数据在参与表达式运算时用其对应的十进制数,所以也把它被归为 整形 一类,即数学中的整数。
接着是:每个 char 类型数据的大小为1个字节(byte)。
字节是计算机划分数据存储的基本单位,1个字节又划分成8个比特位(bit),即8个0或1组成的串, 比如,ASCII表用二进制表示就是:00000000 ~ 01111111;那么8个比特位能表示的数据范围就是 00000000~11111111,即0~255,所以是不是说 char 可表示的十进制数据范围就是0~255?
其实不是的。
char 可定义变量的有效数据范围是:-128 ~ 127;0~255指 unsigned(无符号的) char 的范围,这是一种 无符号型的数据,char是有符号的。现在你只需要知道的是:不同类型可定义变量的有效数据范围是不同的,范围外的就叫 “溢出”。
同属整形家族还有:
2. short //短整型,2byte == 16bit,范围:-2^15 ~ 2^15 - 1,即-32768 ~ 32767
unsigned short 范围 0 ~ 2^16,即0~65535
3. int //整形,定义整数常用,4byte == 32bit,范围:-2^31 ~ 2^31 -1,即:正负21亿多
unsigned int 范围 0~2^32,即42亿多;但是经常使用的是:size_t(无符号整形),注意 其在32(x86)位系统下,4个字节;在64(x64)位系统下,8个字节。
4. long //长整形,在 32(x86)位系统上,通常是 4 byte;在 64(x64)位系统上, 通常是 8 byte
5. long long //更长的整形,8byte
然后是浮点数,和整形的规则不同,现在大家把它们当 普通小数类型 来用就行
6. float //单精度浮点数,4byte
7. double //双精度浮点数,8byte
8. 指针类型(后面讲)
2. 变量
2.1 定义格式 和 命名规范
定义格式:数据类型 变量名;
形象的解释一下就是:拿着图纸造房子 ——> 不同的设计图纸对应不同的数据类型,造出来的房子是一个个实体呀,属于开了物理空间的,其大小就对应着设计图纸的参数,也就是数据类型的大小,比如int有4个字节,char只有一个字节; 至于这个房子里放什么,可能随着时间一直在变化,所以称 这个房子 就是个变量;但不管你放什么,都不能超过其大小,比如这个房子的大小就200平,你偏要往里修个1000平的游泳池,放不下呀,也就是我们前面说的 “溢出”了;还有,这个房子 最后叫什么名,也不是设计图纸要管的事,属于开发商的自定义,即 变量名。
【***.c文件叫源文件,就是写代码的地方;一切的字母和符号都要用 英文输入法;“ ; ” 是一条语句结束的标志,不可遗漏】
比如,在 test.c 源文件中:
int main()//程序执行入口,只能有一个,一定要写!!!
{
char a, b = 'b', c, d;//定义并局部初始化;定义多个变量用逗号隔开;单个字符的使用要用单引号' '
int age = b-18;//定义并赋值(=)初始化,这是好的编程习惯;编译器默认向上查找变量b,就是说:一定要:先定义,再使用!!!
//常见错误:int x = y = 1; 因为赋值是从左往右,1赋给y,y再赋给x,但是y并没有定义
float weight = 53.5f;//小数默认为double类型,定义float类型可带后缀f
return 0;//返回执行结果
}
命名规范有以下要求:
1.只能由字母(包括大写和小写)、数字和下划线( _ )组成。
2.不能以数字开头。
3.长度不能超过63个字符。
4.变量名中区分大小写的。
5.变量名不能使用关键字(语言本身预先设定好的),比如:
2.2 格式化输入和输出(scanf 和 printf)
包含头文件:#include<stdio.h>
(关于<头文件(***.h)>,现在你只要知道是:C语言本身已经写好的东西(比如下面的scanf和printf)都是被打包好的,你安装C编译环境时,它们就被下载到你的本地PC上;你要用的时候,就要告诉编译器去哪里找,方式就是:#include<****.h>)
输入:scanf
(如果你用Visual Studio,加上:#define _CRT_SECURE_NO_WARNINGS 1 否则错误信息会建议你使用 scanf_s,但这个是微软自己搞的,不属于官方原生)
从stdin(标准输入流,就是你的键盘输入,其实被放到一个类似文件的管理系统中,我们在屏幕上看到的输入就是从这个系统里读取出来,再呈现给我们)读取数据,并根据参数格式将其存储到附加参数(指针变量)所指向的位置,也就是我们定义好的变量。
比如:
关于这个字符序列(C字符串):
空白字符: 该函数将读取并忽略下一个非空白字符之前遇到的任何空白字符(空白字符包括空格、换行符\n和制表符\t。
非空白字符,除了格式说明符(%): 都会导致scanf函数从流中(键盘)读取下一个字符,将其与该非空白字符进行比较,如果匹配,则丢弃该字符,继续读下一个字符。如果字符不匹配,则函数失败,返回并保留流的后续字符未读。
格式说明符: 由初始百分比符号(%)组成的序列表示格式说明符,用于指定要从流中检索的数据的类型和格式,并将其存储到附加参数所指向的位置。
常用的有:%c —— 字符类型
%d —— 有符号十进制整数
%u —— 无符号十进制整数
%f —— 十进制单精度浮点数
%lf —— 十进制双精度浮点数
%s —— C字符串
%x —— 无符号十六进制整数(0~9,a~f,A~F),一般以0x或0X开头(0是数字零)
%o —— 无符号八进制整数(0~7)
至于什么意思,看下面的例子:
也就是说,我们的输入根本就不是我们以为的整型,浮点型,而都是文本字符,程序是根据 格式说明符 来处理读到的字符,修改变量内容的。
scanf 格式说明符的完整原型其实是下面这个:
%[*][width][length]说明符
其中:* 表示一个“忽略”操作,即从stdin中读取字符,但是不对变量内容进行修改
width:宽度,指定当前读取操作中要读取的最大字符数(加上终止符\0),遇到空格字符提前结束读取(可选)
length:指的是数据类型的长度修饰符,用于指定数据类型的大小,以便于正确地读取或打印数据;不同的 length
修饰符影响与类型相关的转换。其通常和说明符搭配使用:
(黄色行表示C99引入的说明符和子说明符)
现在给大家举个测试例子:
大家平常的使用场景,一般用不到[*][width][length],直接 "%+常用说明符" 就行,但这些丰富的用法总得见一见,学一学,用一用。
接着是格式化输出:printf
和scanf不同,其作用是:把C字符串写到stdout(标准输出流),即 屏幕上。
关于这个C字符串,有两种情况:
情况一:不包含格式说明符(%):
也可以输出中文字符:printf("你好,我的名字是***")
情况二:包含格式说明符(%),则其后面的附加参数(变量的值)将被格式化并插入到结果字符串中,以替换它们各自的说明符。常用格式说明符和scanf的一样,这里再加几个:
%p —— 指针(地址)
%e(小写)和 %E(大写)—— 科学计数法,通常对浮点数使用。
比如:12345.6789 科学计数法表示为:1.23456789 x 10^4
%e格式输出就是:1.234568e+04 【默认保留6位小数,并且四舍五入;e+04表示将小数点向右移动 4 位,就是 12345.680000,较原数产生了精度损失】
-0.0034 科学计数法表示为:-3.4 x 10^-3
%E格式输出就是:-3.400000E-03 【默认保留6位小数,并且四舍五入;E-03表示将小数点向左移动 3 位,就是 -0.003400,较原数没有精度损失】
如下示例:
int main()//执行入口
{
int age = 18;
double weight = 56.5;
const char* name = "ZhangSan";//常量字符串的定义初始化,先学着用
printf("Name=%s;Age=%d;Weight=%lf\n", name, age, weight);
printf("addree(age)=%p\n", &age);
double d1 = 12345.6789;
double d2 = -0.0034;
printf("%e, %E\n", d1, d2);
return 0;
}
输出:
(至于为什么地址是一串数字编号,我们后面再看,现在你只要知道是什么就行)
printf的格式说明符遵循以下原型:
%[flags][width][.precision][length]说明符
和scanf一样,末尾的说明符字符是最重要的组成部分,其它的都是子说明符,属于可选可不选。
1:先来看[width]:(每个输出只能二选一)
number:要打印的最小字符数。如果要打印的值比这个数字短,结果将用空格填充。即使结果更大,值也不会被截断。
*: 宽度不是在格式字符串中指定的,而是作为必须格式化的参数之前的附加整数值参数指定的。
比如:
并且通过num1的输出可以发现:默认右对齐
如果要左对齐,就要添加 flags 标志。
2:[flags](可任意组合使用)
2.1 如果不写入任何符号,则在值之前插入空白,不是空格
2.2 ‘ - ’:左对齐
2.3 ‘+’:即使是正数,也强制在结果前加上正号(+),因为默认情况下,只有负数前面有-号
比如:输入一个整数,要求输出占20个字符,左对齐,带正负号
int num = 0;
scanf("%d", &num);
printf("num=%-+20d\n", num);//或者printf("num=%+-*d\n", 20, num);
如果就是要求默认的右对齐,但是不够的字符要指定用零(0)而不是空格左填充,就要用符号:
2.4 ‘0’ 字符零,修改为如下:
printf("num=%+020d\n", num);//或者printf("num=%0+*d\n", 20, num);
(注意:左对齐时,指定填充就失效了)
2.5 ‘#’:
2.5.1 与o、x或X说明符一起使用时,对于不同于零的值,值的前面分别加0、0x或0X
比如:
int num4 = 100;
printf("%#o, %#x, %#X\n", num4, num4, num4);
输出:0144, 0x64, 0X64
2.5.2 如果与 g 或 G 一起使用时,即使后面没有更多数字,它也会强制写入输出包含小数点;说明符‘g'和'G'的意思是:使用最短形式表示浮点数。
比如:
double num5 = 100.00000;
printf("%lf, %g, %#g\n", num5, num5, num5);
float num6 = 100.12056;
printf("%lf, %G, %#G\n", num6, num6, num6);
输出: 'g'说明符会根据数值的大小自动选择使用 常规小数形式(%f
)或 科学计数法(%e
),以确保输出尽可能简洁。具体来说,%g
会根据浮点数的大小和精度选择最合适的格式,去除不必要的尾随零和小数点。
3. [.precision] 精度 (每个输出只能二选一)
.number
对于整数说明符(d, u, o, x, X):效果相当于[flags]的'0'指定左填充;精度为0表示无操作。
(常用)对于浮点数(l, lf, e, E): number就是小数点后要打印的位数(默认是6位)
对于'g'和’G'说明符:有效数字的最大数目
对于's'说明符:这是要打印的最大字符数。默认情况下,将打印所有字符,直到遇到结束的\0字符。 如果指定的周期没有明确的精度值,则假定为0
.*:精度不是在格式字符串中指定的,而是作为必须格式化的参数之前的附加整数值参数指定的
看下面的例子:
int num7 = 10;
printf("%.0d,%.1d,%.*u\n", num7, num7, 10, num7);
float num8 = 67.34508;
printf("%f, %.2f; %E, %.*E\n", num8, num8, num8, 3, num8);
printf("%g, %.1g, %.*g, %.4g\n", num8, num8, 2, num8, num8);//有效数字是从左往右第一个非零数开始
const char* str = "hello world!";
printf("%s, %.s, %.3s, %.20s", str, str, str, str);
输出:
4:[length] 长度
和前面scanf的[length]用法大致一致,此处不再赘述,贴一张参考表格:
2.3 作用域和生命周期
变量分为:局部变量(变量所在的局部范围) 和 全局变量(整个项目代码)
举个例:
int global = 10;//全局变量
int main()
{
int local = 20;//局部变量
printf("global=%d, local=%d\n", global, local);
return 0;
}
总结一下就是:{ } 就叫一个局部域,这个域里定义的变量就叫局部变量,这些变量只能在这个域里使用(作用域),出了这个域,这些变量就会被销毁,即所占空间还给操作系统,不受你的代码程序管理了,从创建到销毁的时间段就叫做这个变量的生命周期;不在任何{}的变量,就是全局的,可被任意一个局部域使用,且其生命周期是程序结束时。
其次,一个局部域可以通过{ }被不断划分成多级局部域;并且同级域内不能重定义(重名),全局域也是如此;不同级局部域之间可以重名,因为生命周期不同。
如下示例:
最后,当局部变量和全局变量冲突时(重名),局部优先。
int agc = 10;
int main()
{
int agc = 20;
printf("agc=%d\n", agc);//输出:agc=20
return 0;
}
3. 常量
不同于变量,比如:年龄,体重,薪资等; 生活中的有些值是不变的,比如:圆周率,身份证号码,血型,......
C语言中的常量和变量的定义的形式有所差异,分为以下以下几种:
1. 字面常量:比如:3.14,100,'a',"hello world!",......
2. const 修饰的常变量:本质还是变量,但是不能直接通过变量名修改其内容;且必须初始化!比如:
修饰局部变量:
int main()
{
const int a = 10;//局部变量
a = 20;//报错
return 0;
}
修饰全局变量:它会默认具有内部链接属性,即变量只在定义它的源文件中可见,其他源文件无法引用,看对比:
3. #define 定义的标识符常量:就一个词 “直接替换”,比如:
#define NUM1 10
int main()
{
#define NUM2 20
int a = NUM1 + NUM2;//处理成:int a = 10 + 20;
printf("%d\n", a);//输出:20
return 0;
}
4. 枚举常量:整形常量,关键字(enum),可穷举,比如:
enum Colour//颜色
{
Red = 2,
Green = 4,
Yellow = 8
};
int main()
{
enum Option//选项
{
Stop,//默认为0,往下递增+1
Pass,
Caution
};
printf("%d :> %d\n%d :> %d\n%d :> %d\n", Red, Stop, Green, Pass, Yellow, Caution);
return 0;
}
输出:
这里只是简单介绍让大家能看懂,学着用;实际的运用场景是更丰富,更灵活的,需要不断的项目代码积累量,才能体会。
4. 字符串+转义字符+注释
关于 字符串,前面已经介绍过并使用过了,这里就简单提一下:
用双引号 " " 引起来的一串连续字符,结束标志是'\0'
定义一个常量字符串:const char* str = "*********";
输出:printf("%s\n", str); 或者 printf(str);
一个常用的库函数:strlen,返回字符串的长度,即字符数,不包含‘\0'
包含头文件:#include<string.h>
如下代码:
int main()
{
const char* str = "hello world!";
size_t len_str = strlen(str);
printf(str);
printf("\nlen_str=%u\n", len_str);
return 0;
}
输出:
下面,我们来看 转义字符:顾名思义就是转变意思
常见的有:
注:
三字符序列是 C 语言标准中为支持某些不包含全套 ASCII 字符集的键盘而引入的一组字符组合,这些组合会被编译器解释为其他符号。例如,??)
被解释为 ]
,而 ??(
被解释为 [
。为了防止这些三字符序列在代码中意外地被编译器解释为其他符号,C 语言提供了 \?
这个转义字符。虽然三字符序列在现代编译器中已经很少使用,但 \?
作为一个转义字符仍然存在,以避免与这些序列发生冲突。
看下面的例子:
int main()
{
char c1 = '\'';
printf("%c\n", c1);
const char* str1 = "\"hello\", \"world\"\f";
printf(str1);
printf("\nD:\code\test.c\n");
printf("D:\\code\\test.c\n\n");
int i = 0;
for (; i < 10; i++)
{
printf("\a");
}
//\b将输出光标向左移动一个字符,但不会删除字符,但是会被紧接着的输出覆盖
printf("start test char of '\\b'!\b\n");
printf("start test char of '\\b'!\b\b\b\n");
printf("start test char of '\\b'!\b\b\b\b\b\b\b****\n\n");
printf("start test char of '\\r'!\n");
printf("start test char of '\\r'!");
printf("\rstart test\r char of '\\r'!\n\n");//覆盖
printf("\130\t\x30\test\n");//补空格
return 0;
}
输出:
(注意:输出的都是单个字符)
注释:
其实在前面的示例中小编就一直在用,目的之一就是:有些代码需要标注一下是干啥的;或者代码太多,避免逻辑混乱;再或者你要把代码给别人看/用,减少他人的理解成本;...... 这绝对是个利己利他,调高效率的编程好习惯!
另一个常见用途是,有的代码需要拿来做测试,但是发行版本又不需要,那么此时可以不用删除,注释掉就行。
注释有两种风格(C/C++都能用):
C语言风格的注释: /*xxxxxx*/
缺陷:不能嵌套注释,即每个/*配对最近的*/
C++风格的注释: //xxxxxxxx
可以注释一行也可以注释多行
再简单举个例子:
int main()
{
/*
int age = 10;
const char* name = "zhangsan";
double weight = 56.8;
*/
//接下来是一段测试代码,探究scanf和printf
int a = 10;//定义变量a并初始化为10
scanf("%2d", &a);//只读取2个字符,当整数处理
printf("%-+10.6o\n", a);//输出:左对齐,带正负号,占10位,精度为6,以八进制输出
//......
return 0;
}
当然,这里只是演示,告诉大家怎么用,实际中的注释应该尽量言简意赅,重点突出!
5. 操作符
是由操作数(如变量、常量)和操作符组成的一段代码,就叫 表达式,它会被计算并返回一个值;单独的一个操作数也可以被视为一个表达式,这种表达式的计算结果就是操作数本身。
5.1 双目操作符
即,需要左右两个操作数。
5.1.1 算数操作符
+(加) -(减) *(乘) / %(取余数)
%操作符只能用于整数间,比如:5%2 = 1;其它的整数,浮点数都行。
对于 / 操作符:如果两个操作数都为整数,执行整数除法 取模(商),比如:5/2=2
只要有一个操作数是浮点数,则执行正常的浮点数相除,比如:5/2.0=2.5
5.1.2 移位操作符
<< 左移操作符
>> 右移操作符
移位操作符的操作数只能是整数,位指 “比特位”
需要预备知识:《C语言---数据的存储》点2.1和点2.2
先看:<< 左移操作符
代码验证一下:
int main()
{
int num1 = 10;
int num2 = num1 << 2;
printf("num1=%d, num2=%d\n", num1, num2);
return 0;
}
输出:
接着看:>> 右移操作符
代码验证一下,你当前的编译器是逻辑右移,还是算数右移:
int main()
{
int num3 = -1;
int num4 = num3 >> 2;
printf("num3=%d, num4=%d\n", num3, num4);
return 0;
}
小编当前演示示例是算数右移:
警告: 对于移位运算符,不要移动负数位,这个是标准未定义的。
5.1.3 位操作符
这里的位,同样是指 “比特位”。两个操作数也必须是整数。
&:按位与,有0就是0
| :按位或,有1就是1
^:按位异或,相同为0,相异为1
需要预备知识:《C语言---数据的存储》点2.1和点2.2
如下示例:
代码验证一下:
int main()
{
int num5 = -1;
int num6 = 2;
int num7 = num5 & num6;
int num8 = num5 | num6;
int num9 = num5 ^ num6;
printf("num5=%d, num6=%d, num7=%d, num8=%d, num9=%d", num5, num6, num7, num8, num9);
return 0;
}
输出:
运用示例:实现两个整数的交换
int main()
{
int a = 0;
int b = 0;
scanf("a=%d, b=%d", &a, &b);
//思路一:创建中间临时变量tmp
int tmp = a;
a = b;
b = tmp;
//要求:不能创建其它变量
//思路二:运用加减法
a = a + b;
b = a - b;//b=a+b-b=a
a = a - b;//a=a+b-a=b
//(重点!!!)思路三:利用 ^
a = a ^ b;
b = a ^ b;//b=a^b^b = a ^ 0=a;特性1:两个相同值^为0,并与顺序无关;特性2:任何值与0异或,都不变
a = a ^ b;//a=a^b^a=b^0=b
printf("a=%d, b=%d\n", a, b);
return 0;
}
5.1.4 赋值操作符
=
前面就一直在用,覆盖变量原先的值
double f1 = 34.5;
f1 = 70.0;
int a = 1;
int b = 2;
int c = 3;
a = c = b * a + 4;//连续赋值,从右往左
//但是同样的语义,可以写成:(更加清晰爽朗而且易于调试,推荐)
c = b * a + 4;
a = c;
复合赋值符:
+=:a += b 就是 a = a + b;
同理还有: -=,*=,/=,%=,>>=,<<=,&=,!=,^=
5.1.5 关系操作符
> 大于 >=大于或等于
<小于 <=小于或等于
!=不等于 ==等于
关系比较的结果:0为假,非零为真
如下代码:
int main()
{
int a = 10;
int b = 20;
printf("%d, %d, %d, %d, %d, %d\n", a > b, a >=b, a < b, a<=b, a != b, a == b);
return 0;
}
输出:
5.1.6 逻辑操作符
&& 逻辑与:左右两个表达式同时为真(非0)才返回真,否则为假(0)
|| 逻辑或 :左右两个表达式只要一个为真,就返回真
(从左往右)
如下代码:
int main()
{
int c = 20;
int d = 30;
int e = c < d && c == 20;
int f = c >= d && d == 30;
int g = e || f;
int h = f || d <= c;
printf("%d, %d, %d, %d\n", e, f, g, h);
return 0;
}
输出:
易错写法:比如:a > b > c
正确写法:a > b && b > c
5.1.7 下标引用、函数调用和结构成员
分别在数组,函数,结构体部分讲解。
5.2 单目操作符
即,只需要一个操作数。
5.2.1 不改变原操作数
- 负值:将正值变负,负值变正
+ 正值:不能把负值变正,表示该数值为正,为了代码的可读性或语义明确性,但较少使用
! 逻辑反操作:真变假,假变真
~ 对一个数的二进制按位取反:0变1,1变0
sizeof 操作数的类型长度(以字节为单位)
(类型) 强制类型转换
&(取地址)和 *(解引用)在 指针 部分细讲。
如下代码:
int main()
{
int a = 10;
int _a = -a;
int b = -20;
int _b = +b;
int c = _a > b;
int c1 = !c;
int c2 = !c1;
int d = 1;//二进制:0~1
int _d = ~d;//二进制:1~0,输出十进制为:-2
printf("%d, %d; %d, %d; %d, %d, %d; %d, %d\n", a, _a, b, _b, c, c1, c2, d, _d);
//常用:sizeof,注意我的书写形式
int e = sizeof(char);
double f = 3.14;
int g = sizeof(f);
printf("%d, %d\n", e, g);
printf("%d, %d, %d\n", sizeof(int), sizeof(a), sizeof a);
//强制类型转换(相近类型: 整形,浮点型,指针)
int num1 = 10;
double num2= (double)(num1 / 4);
double num3 = (double)num1 / 4;
printf("%lf, %lf\n", num2, num3);
return 0;
}
输出:
5.2.2 改变原操作数
前置/后置: ++ , -- ;每次+/- 1
直接来看例子:
int main()
{
int a = 10;
int b = ++a;//前置++:a先加1:a = a +1==11;然后再把a赋值给b,为11
printf("a=%d, b=%d\n", a, b);
int c = b++;//后置++:先使用,把b赋值给c, 为11;b再加1:b = b+1==12
printf("c=%d, b=%d\n", c, b);
//前置/后置--,也是同样的道理
int d = --c;
printf("d=%d, c=%d\n", d, c);//10, 10
int e = d--;
printf("e=%d, d=%d\n", e, d);//10, 9
return 0;
}
输出:
上点难度(如果对运算符的优先级和结合性存疑,先去看一下点5.5):
//输出结果是什么?
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && --b || d++;//从左往右依次判断:表达式"a++":先使用,a==0,为假,a再加1==1,那么(a++ && --b)为假;接着看"d++",为真,所以i=1
printf("i=%d, a=%d, b=%d, c=%d, d=%d\n", i, a, b, c, d);//1,1, 2, 3, 5
i = --d && a-- || ++c;//"--d"先减1==4,再用,为真;"a--"先用==1,为真,那么(--d && a--)为真,后面的不再判断,i = 1, a再减1==0
printf("i=%d, a=%d, b=%d, c=%d, d=%d\n", i, a, b, c, d);//1, 0, 2, 3, 4
//计算
i = c++ + b-- * --d - ++a;//c==3,先用其+(b--*i), 再c加1==4;其中,b-- * --d ==6,b==1, d==3;则(c++ + b-- * i)为9;++a为1,那么i=9-1==8
printf("i=%d, a=%d, b=%d, c=%d, d=%d\n", i, a, b, c, d);//8, 1, 1, 4, 3
return 0;
}
输出:
5.3 三目运算符
也叫 条件表达式: exp1 ? exp2 : exp3;
如果表达式exp1为真,则执行并返回表达式exp2的结果,否则返回exp3的结果。
如下代码:
int main()
{
int a = 10;
int b = 20;
int c = a >= b ? a = b : a += 1;
printf("c=%d, a=%d, b=%d\n", c, a, b);
int d = a <= b && a != b ? a = b : b - 1;
printf("d=%d, a=%d, b=%d\n", d, a, b);
return 0;
}
输出:
5.4 逗号表达式
就是用 ( ) 括起来,并用逗号隔开多个表达式:(exp1, exp2, exp3, …, expN);
从左向右依次执行,整个表达式的结果是最后一个表达式的结果。
如下代码:
int main()
{
int a = 10;
int b = 20;
int c = (a--, b + a, --b, a += a, b);//(10, 20+9==29, 19, a = a+a==9+9==18, 19) == 19
printf("a=%d, b=%d, c=%d\n", a, b, c);
return 0;
}
输出:
5.5 操作符的属性
复杂表达式的求值有三个影响的因素:
1. 操作符的优先级。即,先算哪个后算哪个,比如a+b*c,先算乘再算加
2. 操作符的结合性。比如: 判断 a!=b,从左往右;赋值 a=b,从右往左
3. 是否控制求值顺序。比如:(a+b)*c,先算括号里的加,再算括号外的乘
点3是我们自己控制的,C语言只规定了点1和2,如下表:
两个相邻的操作符先执行哪个?取决于他们的优先级;如果两者的优先级相同,取决于他们的结合性。
但是还是会产生一些问题表达式:不能确定唯一的计算路径!
比如:a*b + c*d + e*f
在计算的时候,由于*比+的优先级高,只能保证,*的计算是比+早,但是优先级并不 能决定第三个*比第一个+早执行 ,因此此表达式的计算路径可能是:
1:
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
2:
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
如果,abcdef都是单个变量/常量值且互不影响,那么不管哪个顺序都不会影响最终结果;
但是,如果是复合表达式呢?
比如:c + --c;
操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得 知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
同样的还有:int i = 10; int ret = i-- - --i * ( i = -3 ) * i++ + ++i;
int i = 1; int ret = (++i) + (++i) + (++i);
那么,怎么算,就要看具体的编译器了,我们来对比一下VS2022和gcc:
int main()
{
int c = 10;
int d = c + --c;
printf("c=%d, d=%d\n", c, d);
int i1 = 10;
int ret1 = i1-- - --i1 * (i1 = -3) * i1++ + ++i1;
printf("i1=%d, ret1=%d\n", i1, ret1);
int i2 = 1;
int ret2 = (++i2) + (++i2) + (++i2);
printf("i2=%d, ret2=%d\n", i2, ret2);
return 0;
}
输出对比:
所以,我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
6. 整形提升和类型转换
已经总结在小编的另一篇文章:《C语言---数据的存储》点击直达 (到这,你已经可以通篇学习此链接文章了)
7. 分支语句和循环语句
程序的执行流程有3种结构,分别是:顺序结构(从上往下,依次执行),选择结构(条件判断),循环结构
7.1 分支语句(选择结构)
7.1.1 if语句
语法结构:
//无分支
if (表达式)
{
语句;
}
//单分支
if (表达式)
{
语句1;
}
else
{
语句2;
}
//多分支
if (表达式1)
{
语句1;
}
else if (表达式2)
{
语句2;
}
//......
/*
else if (表达式n)
{
语句n;
}
*/
else
{
语句;
}
注意:1:条件判断,即为表达式的真假,0表示假,非0表示真,真则执行语句块{ };并且之后的所有分支都不进行判断执行。
2:如果不加 { }, 则表达式为真后 只默认执行一条语句!
如下示例代码:
int main()
{
int opt1 = 0;
printf("请选择:1(继续) / 0(结束):>");
scanf("%d", &opt1);
if (1 == opt1)//"=="判断是否相等
{
int opt2 = 0;
printf("请选择:1(好好学习) / 2(有米,不慌) / 3(摆烂):>");
scanf("%d", &opt2);
if (1 == opt2)
{
printf("好Offer!\n");
}
else if (2 == opt2)
{
printf("继承家产!\n");
}
else
{
printf("要去卖红薯了哟!\n");
}
}
return 0;
}
3. else是和它离的最近的if匹配的,所以适当的使用{}可以使代码的逻辑更加清楚,是种好的代码风格。否则如下示例:
int main()
{
int a = 0;
int b = 2;
if (a == 1)
if (b == 2)
printf("hehe\n");
else
printf("haha\n");
return 0;
}
事与愿违,没有输出!
7.1.2 switch语句
语法结构:
switch (整型表达式)
{
case 整形常量表达式://其结果和switch()中的表达式结果进行匹配;如果匹配就执行;不匹配就下一个case继续比较
空 / 一条 / 多条语句;
break;//跳出switch分支结构,如果不加,将往下执行所有case分支,直到遇到break或结束
//如果表达式的值与所有的case标签的值都不匹配,那么没有任何一个case分支被执行
//如果你并不想忽略掉不匹配所有标签的表达式的值时,可以加一条default子句
default://位置是任意的(一般都放在末尾),但只能有一条,
//......
break;//好的编程习惯,最后一条语句都加break
}
如下示例:
int main()
{
int opt = 0;
printf("请选择:1(好好学习) / 2(有米,不慌) / 3(摆烂):>");
scanf("%d", &opt);
switch (opt)
{
case 1:
printf("好Offer!\n");
break;
case 2:
printf("继承家产!\n");
break;
case 3:
printf("要去卖红薯了哟!\n");
break;
default://可选
//......
break;
}
return 0;
}
或者:
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
case 2:
case 3:
case 4:
case 5:
printf("Weekday:Live like an ox and a horse!\n");
case 6:
case 7:
printf("Rest days:You've survived another week!");
default:
//......
break;
}
return 0;
}
小练习:
int main()
{
int n = 1;
int m = 2;
switch (n)//n==1
{
case 1:
m++;//m==3
case 2:
n++;//n==2
case 3:
switch (n)//switch允许嵌套使用
{
case 1:
n++;
case 2:
m++;//m==4
n++;//n==3
break;
}
case 4:
m++;//m==5
break;
default:
--n;
break;
}
printf("m = %d, n = %d\n", m, n);
return 0;
}
7.2 循环语句
7.2.1 while
语法结构:
while (表达式)//为真(非0)就继续,假(0) 就结束 while { }
{
//被循环执行的语句;
//......
//break; 语句:直接提前跳出 一层while循环
//......
//continue; 语句:终止本次循环,跳到while()表达式部分
}
如下代码:
#include<stdlib.h>
#include<time.h>
#define Money 100 //假设你有100元
#define UnitPrice 7 //假设每张彩票单价7元
int main()
{
//随机数种子
srand(time(NULL));//包含头文件:stdlib.h,time.h
int opt1 = 0;
printf("请选择:0【退出】,1【继续】:>");
scanf("%d", &opt1);
if (1 == opt1)
{
int money = Money;
int flag = 1;//标记是否因为余额不足而退出循环
int opt2 = 0;
printf("请输入你要购买的彩票号(整数0~5):>");
while (scanf("%d", &opt2))//scanf返回读到的数据个数
{
int answer = rand() % 6;//产生0~5的随机数,可自定义
if (opt2 == answer)
{
printf("恭喜你,中奖500万!");
break;
}
//没有中
int opt3 = 0;
printf("很遗憾,没有中!【当前余额:%d】\n接下来,你是要好好学习成为大牛【1】;还是当菜鸟,继续买彩票【0】请选择:>", money-=UnitPrice);
scanf("%d", &opt3);
if (1 == opt3)
{
printf("汗水会助力每一个梦!");
break;
}
//继续买彩票
if (money < UnitPrice)
{
printf("余额不足,还是好好学习吧!\n");
flag = 0;
break;
}
printf("请输入你要购买的彩票号(整数0~5):>");
}
//如果中彩票或者成为大牛
if (1 == flag)
{
printf("人生巅峰,迎娶白富美!\n");
}
}
return 0;
}
有兴趣的话,你可以拷贝到你的编译环境下玩一下,运气好的话,可能一次就中,但也可能一次不中,尽管只要在 规定范围的6个随机数里猜一个。而现实生活中,规则更复杂,数字是组合的,且随机性更大...... 可想而知,能中几十万,几百万的概率得有多低了。
在Debug(调试模式)或 Release(可发行模式)下运行完代码后,在当前项目目录下会产生 ***.exe可执行文件
双击就可执行。
如果要发给别人,确保可运行,要设置 静态链接,以VS为例:
7.2.2 for
语法结构:
for (表达式1; 表达式2; 表达式3)
{
//循环语句......:
//break; 语句:直接跳出一层for循环
//continue;语句:终止本次循环,跳过其后的循环语句,直接到for(......)
}
//同样:如果不写{},默认只执行紧跟着的一条语句
说明:表达式1为初始化部分,用于初始化循环变量
表达式2为条件判断部分,用于判断循环是否继续,真(非0)就继续,假(0)就终止
表达式3为调整部分,用于循环条件的调整
第一次执行for(......),先执行表达式1,再执行表达式2判断,表达式3不执行;第二次及其以上执行for (......),表达式1不执行,先执行表达式3,再执行表达式2判断。
比如:
//打印1~n的素数
int main()
{
int n = 0;
printf("请输入打印范围(1~n) :>");
scanf("%d", &n);
int i = 0;
for (i = 1; i <= n; ++i)
{
//素数判断:大于1的自然数,除1和它本身不能被任何数整除
if (i > 1)
{
int j = 0;
for (j = 2; j < i; j++)
{
if (i % j == 0)
{
break;//不是素数
}
}
//执行到这,有两种情况:1:for正常结束,j == i,为素数; 2:中途break,不是素数
if (j == i)
{
printf("%d ", i);
}
}
}
}
测试输出:
接着来看一些for循环的变种写法:
for (; ; )//三个表达式都为空:死循环
{
//......
}
int i = 0;
for (; i < 10; )
{
i += 3;//调整部分还可以放到{ }中
}
int k = 0;
for (i = 1, k = 6;i < 3, k >= 1 ; i++, --k)//每一部分都可以是 逗号分隔的组合表达式,即逗号表达式
{
printf("%d ", k);
}
......
总之就是:非常灵活。
现在,如果要把7.2.1 的示例代码改成for结构,只需要修改一个地方:
for (; scanf("%d", &opt2); )
{
//......
}
7.2.3 do...while
语法结构:
do
{
//循环语句;
//break; 语句:直接跳出
//continue;语句:跳到表达式判断部分
} while (表达式);//真(非0)继续循环;假(0)终止
先至少执行一次循环语句之后,再while判断是否继续循环。
比如,现在来写个猜数字的小游戏:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main()
{
int input = 0;
srand(time(NULL));
do
{
//打印菜单
printf("**********************************\n");
printf("*********** 1.play **********\n");
printf("*********** 0.exit **********\n");
printf("**********************************\n");
//输入
printf("请选择>:");
scanf("%d", &input);
int random_num = rand() % 100 + 1;//生成1~100的随机数
int num = 0;
switch (input)
{
case 1:
//开始游戏逻辑
while (1)
{
printf("请输入猜的数字>:");
scanf("%d", &num);
if (num > random_num)
{
printf("猜大了\n");
}
else if (num < random_num)
{
printf("猜小了\n");
}
else
{
printf("恭喜你,猜对了\n");
break;
}
}
break;
case 0:
break;
default:
printf("选择错误,请重新输入!\n");
break;
}
} while (input);
return 0;
}
7.2.4. goto语句
适用场景:在深度嵌套中,一次可跳出多层循环,到达任意指定的位置
比如:
for(...)
for(...)
{
for(...)
{
if(disaster)
goto error;
}
}
…
error:
//......
除此之外, goto
可能使得代码执行流程变得复杂,会让人困惑,尤其是在大型程序中,可能产生不可达代码和资源管理问题,降低可维护性。
goto语句都能改成其它循环语句,因此,尽量避免使用 goto!
这里用goto 写个关机小程序:
#include <stdio.h>
#include<string.h>
#include<Windows.h>
int main()
{
char input[10] = { 0 };
system("shutdown -s -t 300");//秒
printf("电脑将在5分钟后关机,如果输入:“cat!” 就取消关机!\n请输入:>");
again:
scanf("%s", input);
getchar();
if (0 == strcmp(input, "cat!"))//比较字符串是否相等
{
system("shutdown -a");
}
else
{
printf("请重新输入:>");//故意输错,进行测试
goto again;
}
return 0;
}
如何改为其它循环语句,就交给你吧。
7.3. 一段有趣的代码
(声明:纯测试,无任何恶意!)
弹窗消息:
#include<windows.h>
int main()
{
while (1)
{
// 显示带有“确定”和“取消”按钮的消息框
int result = MessageBoxW(NULL, L"叫爸爸!", L"今日问候:", MB_OKCANCEL | MB_ICONQUESTION);
// 根据用户选择的按钮执行相应的操作
if (result == IDOK)
{
break;
}
// 用户点击“取消”
MessageBoxW(NULL, L"大胆,逆子!", L"Result", MB_OK);
}
// 用户点击“确定”
MessageBoxW(NULL, L"这就对了嘛,爸爸给你买糖吃!", L"Result", MB_OK);
return 0;
}
如果你使用VsCode:在终端编译时:gcc .c源文件 -o 程序名.exe -mwindows -static
以窗口应用程序运行,而不是控制台应用程序;并设置成静态链接。
如果你使用Visual Studio:
然后修改代码:
//在 Windows 应用程序中,使用 WinMain 作为入口点,而不是 main。
//这可以帮助编译器更好地识别你的程序为 GUI 应用程序。
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
//代码
}
(确保静态链接)
这下,就可以把生成的可执行程序 发送给你的 “好厚米” 了。
如果本文对你有所帮助,就是对小编最大的鼓励了,可以的话,三连并留下你的宝贵评论,予以斧正!