文章目录
- 一、语句和语句分类
- 二、注释
- 🍕注释是什么?为什么写注释?
- 1. /**/的形式
- 2. //的形式
- 3. 注释会被替换
- 三、随机数的生成
- 1.rand函数
- 2.srand函数
- 3.time函数
- 4.设置随机数的范围
- 四、C99中的变长数组
- 五、问题表达式解析
- 表达式1
- 表达式2
- 表达式3
- 表达式4
- 表达式5
- 六、位段补充
一、语句和语句分类
C语言的代码是由一条一条的语句构成的,C语言中的语句可为以下五类:
🍑空语句
🍑表达式语句
🍑函数调用语句
🍑复合语句
🍑控制语句
(1) 空语句
空语句是最简单的语句,一个分号就是一条语句,即空语句。
#include<stdio.h>
int main()
{;//空语句return 0;
}
空语句一般出现的地方是:这里需要一条语句,但是这条语句不需要做任何事,就可以写一个空语句。
(2) 表达式语句
表达式语句就是在表达式的后边加上分号。如下所示:
#include<stdio.h>
int main()
{int a = 20;int b = 0;b = a + 5; //表达式语句return 0;
}
(3) 函数调用语句
函数调用的时候,也会加上分号,就是函数调用语句。
#include<stdio.h>
int Add(int x, int y)
{return x + y;
}
int main()
{printf("hehe\n");//函数调用语句int ret = Add(2, 3);//函数调用语句return 0;
}
(4) 复合语句
成对括号中的代码就构成一个代码块,也被称为复合语句。
#include<stdio.h>
void print(int arr[], int sz) //函数的大括号中的代码也构成复合语句
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}
}
int main()
{int i = 0;int arr[10] = { 0 };for (i = 0; i < 10; i++) //for循环的循环体的大括号中的就是复合语句{arr[i] = 10 - i;printf("%d\n", arr[i]);}return 0;
}
(5) 控制语句
控制语句用于控制程序的执行流程,以实现程序的各种结构方式(C语言支持三种结构:顺序结构、选择结构、循环结构),它们由特定的语句定义符组成,C语言有九种控制语句。可分成以下三类:
🍊1.条件判断语句也叫分支语句:if语句、switch语句;
🍊2.循环执行语句:do while语句、while语句、for语句;
🍊3.转向语句:break语句、goto语句、continue语句、return语句。
二、注释
🍕注释是什么?为什么写注释?
● 注释是对代码的说明,编译器会忽略注释,也就是说,注释对实际代码没有影响。
● 注释是给程序员自己,或者其他程序员看的。
● 好的注释可以帮我们更好的理解代码,但是也不要过度注释,不要写没必要的注释。
● 当然不写注释可能会让后期阅读代码的人抓狂。
写注释一定程度上反应了程序员的素质,建议大家写必要的注释,在未来找工作的时候,写代码时留下必要的注释也会给面试官留下更好的印象。
C 语言的注释有两种表示方法。
1. /**/的形式
第一种写法是将注释放在 /*…*/ 之间,内部可以分行。
⚾形式1:
/* 注释 */
🏐形式2:
/*
这是一行注释
*/
这种注释可以插在一行内部。
int fopen(char* s /* file name */, int mode);
上面示例中,/*file name*/ 用来对函数参数进行说明,跟在它后面的代码依然会有效执行。这种注释一定不能忘记写结束符号*/,否则很容易导致错误。
例如:
上面示例的原意是,第五行和第七行代码的尾部,有两个注释。但是,第一行注释忘记写结束符号*/,导致注释一延续到第三行才结束。
🍏🍏 /**/ 这种注释不支持嵌套注释,从/*开始注释后,遇到第一个 */就认为注释结束了。
2. //的形式
第二种写法是将注释放在双斜杠//后面,从双斜杠到行尾都属于注释。这种注释只能是单行,可以放在行首,也可以放在一行语句的结尾。这是C99标准新增的语法。
🍆形式1:
//这是一行注释
🌽形式2:
int x = 1; //这也是注释
不管是哪一种注释,都不能放在双引号里面。放在双引号里面的注释符号,会成为字符串的一部分,会被解释成普通符号,失去注释作用。
#include<stdio.h>
int main()
{printf("//hello /* world */ ");return 0;
}
程序运行结果:
上面示例中,双引号里面的注释符号,都会被视为普通字符,没有注释作用。
3. 注释会被替换
编译时,注释会被替换成一个空格,所以 min/*这里是注释*/Value 会变成 min Value ,而不是minValue 。
三、随机数的生成
1.rand函数
C语言提供了一个函数叫rand,这个函数是可以生成随机数的,函数原型如下所示:
int rand (void);
rand函数会返回一个伪随机数,这个随机数的范围是在0~RAND_MAX之间,这个RAND_MAX的大小是依赖编译器上实现的,但是大部分编译器上是32767。
rand函数的使用需要包含一个头文件是:stdlib.h
测试一下rand函数,这里多调用几次,产生5个随机数:
#include<stdio.h>
#include<stdlib.h>
int main()
{printf("%d\n", rand());printf("%d\n", rand());printf("%d\n", rand());printf("%d\n", rand());printf("%d\n", rand());return 0;
}
程序运行结果:
我们先运行一次,看看结果,再运行一次再看看结果,多运行次呢?
可以看到虽然一次运行中产生的5个数字是相对随机的,但是下一次运行程序生成的结果和上一次一模一样,这就说明有点问题。
如果再深入了解一下,就不难发现,其实rand函数生成的随机数是伪随机的,伪随机数不是真正的随机数,是通过某种算法生成的随机数。真正的随机数的是无法预测下一个值是多少的。而rand函数是对一个叫“种子”的基准值进行运算生成的随机数。之所以前面每次运行程序产生的随机数序列是一样的,那是因为rand函数生成随机数的默认种子是1。如果要生成不同的随机数,就要让种子是变化的。
2.srand函数
C语言中又提供了一个函数叫srand,是用来初始化随机数的生成器的,srand的原型如下:
void srand (unsigned int seed);
程序中在调用 rand 函数之前先调用 srand 函数,通过 srand 函数的参数seed来设置rand函数生成随机数的时候的种子,只要种子在变化,每次生成的随机数序列也就变化起来了。那也就是说给srand的种子是如果是随机的,rand就能生成随机数;在生成随机数的时候又需要一个随机数,这咋办呢?看下面:
3.time函数
在程序中我们一般是使用程序运行的时间作为种子的,因为时间是时刻在发生变化的。在C语言中有一个函数叫 time,就可以获得这个时间,time函数原型如下:
time_t time (time_t* timer);
🍇time 函数会返回当前的日历时间,其实返回的是1970年1月1日0时0分0秒到现在程序运行时间之间的差值,单位是秒。返回的类型是time_t类型的,time_t类型本质上其实就是32位或者64位的整型类型。
🍇time函数的参数 timer 如果是非NULL指针的话,函数也会将这个返回的差值放在timer指向的内存中带回去。
🍇如果 timer 是NULL,就只返回这个时间的差值。time函数返回的这个时间差也被叫做:时间戳。time函数的使用需要包含头文件:time.h
如果只是让time函数返回时间戳,我们就可以这样写:
time(NULL);//调用time函数返回时间戳,这里没有接收返回值
那我们就可以让生成随机数的代码改写成如下:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main()
{//使用time函数的返回值设置种⼦//因为srand的参数是unsigned int类型,我们将time函数的返回值强制类型转换srand((unsigned int)time(NULL));printf("%d\n", rand());printf("%d\n", rand());printf("%d\n", rand());printf("%d\n", rand());printf("%d\n", rand());return 0;
}
程序运行结果:
(注:截图只是当时程序运行的结果,你的运行结果不一定和这个一样)
srand函数是不需要频繁调用的,一次运行的程序中调用一次就够了。
4.设置随机数的范围
如果我们要生成0~99之间的随机数,方法如下:
rand() % 100 //余数的范围是0~99
如果要生成1~100之间的随机数,方法如下:
rand()%100+1 //%100得到的余数是在0~99之间(包括0和99),0~99的数字+1,范围是1~100
如果要生成100~200的随机数,方法如下:
100 + rand()%(200-100+1) //余数的范围是0~100,加100后就是100~200
所以如果要生成a~b的随机数,方法如下:
a + rand()%(b-a+1)
四、C99中的变长数组
在C99标准之前,C语言在创建数组的时候,数组大小的指定只能使用常量、常量表达式,或者如果我们初始化数据的话,可以省略数组大小。
int arr1[10];
int arr2[3+5];
int arr3[ ] = {1,2,3,4,5};
省略数组大小这种情况,虽然[ ]中没指定元素个数,但是编译器会根据你后面初始化元素的多少来确定数组的大小。
这样的语法限制,让我们创建数组就不够灵活,有时数组大了浪费空间,有时候数组又小了又不够用。
C99中给了一个变长数组(variable-length array,简称VLA)的新特性,允许我们可以使用变量指定数组的大小。
int n = a+b;
int arr[n];
上面示例中,数组 arr 就是变长数组,因为它的长度取决于变量n的值,编译器没法事先确定,只有运行时才能知道 n 是多少。
变长数组的根本特征,就是数组长度只有运行时才能确定,所以变长数组不能初始化。它的好处是程序员不必在开发时,随意为数组指定一个估计的长度,程序可以在运行时为数组分配精确的长度。有一个比较迷惑的点,变长数组的意思是数组的大小是可以使用变量来指定的,在程序运行的时候,根据变量的大小来指定数组的元素个数,而不是说数组的大小是可变的。数组的大小一旦确定就不能再变化了。
遗憾的是在VS2022上,虽然支持大部分C99的语法,但没有支持C99中的变长数组的语法,没法测试;但可以在gcc编译器上测试下面的代码:
#include <stdio.h>
int main()
{int n = 0;scanf("%d", &n);//根据输入的数值确定数组的大小int arr[n];int i = 0;for (i = 0; i < n; i++){scanf("%d", &arr[i]);}for (i = 0; i < n; i++){printf("%d ", arr[i]);}return 0;
}
五、问题表达式解析
表达式1
我们都知道,通过运算符优先级可以知道一个表达式优先级高的部分要比优先级低的先执行。但不能知道有相同优先级的几个部分会不会同时执行,还是又先后顺序?比如:
//表达式的求值部分由操作符的优先级决定。
//表达式1:
a*b + c*d + e*f
表达式1在计算的时候,由于*比+的优先级高,只能保证,*的计算比+的早,但是优先级并不能决定第三个 * 比第一个 + 早执行。
所以表达式的计算机顺序就可能是:
1 a*b
2 c*d
3 a*b + c*d
4 e*f
5 a*b + c*d + e*f
或者
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
表达式2
//表达式2
c + --c;
同上,操作符的优先级只能决定自减 – 的运算在 + 运算的前面,但是我们并没有办法得知,+ 操作符的左操作数的获取(使用)在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
表达式3
#include<stdio.h>
//表达式3
int main()
{int i = 10;i = i-- - --i * (i = -3) * i++ + ++i;printf("i = %d\n", i);return 0;
}
表达式3在不同编译器中的测试结果:(非法表达式程序的结果)
上面的表达式在VS2022中的运行结果为:
表达式4
#include<stdio.h>
int fun()
{static int count = 1;return ++count;
}
int main()
{int answer;answer = fun() - fun() * fun();printf("%d\n", answer);//输出多少?return 0;
}
程序运行结果:
虽然在大多数的编译器上求得的结果都是相同的。
但是上述代码 answer = fun() - fun() * fun();中我们只能通过操作符的优先级得知:先算乘法,再算减法。
但函数的调用先后顺序却无法通过操作符的优先级确定。
表达式5
#include <stdio.h>
int main()
{int i = 1;int ret = (++i) + (++i) + (++i);printf("%d\n", ret);printf("%d\n", i);return 0;
}
VS2022的运行结果:
但如果你在gcc中运行上面的代码得到的结果就和上面的结果有差异。
看看同样的代码产生了不同的结果,这是为什么?简单看一下汇编代码,就可以分析清楚
这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个+和第三个前置 ++ 的先后顺序的。
总结:
即使有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性唯一确定表达式的计算路径,那这个表达式就是存在潜在风险的,建议大家不要写出特别复杂的表达式。
六、位段补充
在位段中,位段的成员必须是int、unsigned int或者signed int(包括char类型, char也属于整型家族)。上面的意思是如果使用位段,那这个结构体中的成员一般是同类型的。比如:
struct S
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
};
struct S
{char _a : 3;char _b : 4;char _c : 5;char _d : 4;
};
特别是位段成员如果出现在表达式中,则会进行整型提升,自动转换为int类型或者unsigned int类型。
在C语言中,位段是用来节省内存空间的。内存分配是遵循以下规则的:
🏉1.成员大小限制:位段中的成员大小不能超过机器定义的类型大小。例如,int类型在大多数平台上是32位,因此位段中的成员大小(bit位数)不能超过32位。
🏉2.内存对齐:位段的内存对齐是基于其中成员的最大类型来确定的。如果一个位段中包含多个成员,且这些成员的类型大小不同,则内存对齐将基于最大的成员类型。
🏉3.内存分配:位段通常不直接分配内存,而是作为结构体的一部分。结构体在内存中的分配遵循常规的内存分配规则,即通过 malloc 函数分配内存,并由程序员使用free函数释放内存。
位段中是可以有不同的类型成员的,但如果位段的成员有不同类型的话,那位段大小的计算就比较复杂。建议位段成员都以相同类型出现(只要是整型家族即可)。