C语言学习笔记
一、初识
标准输入输出函数
printf()
:%3.2f: 总宽度是3,小数部分是两位。这里宽度是算上 .这个符号的
但这里有个问题,如果数值的整数部分超过3位会怎么样?
比如123.45,这时候宽度会自动扩展,不会截断,所以3可能只是最小宽度。
这时候用户可能会误解,认为总宽度被限制为3,但实际上不够的话会自动扩展。
%10.2f会保留10个字符的宽度,不足的话用空格填充,右对齐,而%-10.2f则是左对齐
如果实际数值的小数位数超过指定的精度,会进行四舍五入。比如3.1415用%.2f会变成3.14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 学生信息输入输出,输出成绩需进行四舍五入且保留2位小数
int work_example4() {
int id = 0;
float c = 0.0f; // 不加f默认是double类型
float math = 0.0f;
float eng = 0.0f;
scanf("%d;%f,%f,%f", &id, &c, &math, &eng); // 1234567;80.845,90.55,100.00
printf("The each subject score of No. %d is %6.2f, %6.2f, %6.2f.\n",
id, c, math, eng); //// The each subject score of No. 1234567 is 空80.85, 空90.55, 100.00.
// 关于printf
// 1. %3.2f: 总宽度是3,小数部分是两位。这里宽度是算上 .这个符号的
// 但这里有个问题,如果数值的整数部分超过3位会怎么样?
// 比如123.45,这时候宽度会自动扩展,不会截断,所以3可能只是最小宽度。
// 这时候用户可能会误解,认为总宽度被限制为3,但实际上不够的话会自动扩展。
// 2. %10.2f会保留10个字符的宽度,不足的话用空格填充,右对齐,而%-10.2f则是左对齐
printf("%10.2f\n", 12.56); //空空空空空12.56
printf("%-10.2f\n", 12.56);//12.56空空空空空
// 3. 如果实际数值的小数位数超过指定的精度,会进行四舍五入。比如3.1415用%.2f会变成3.14
return 0;
}格式化输出数字
1
printf("%02d\n", 2); // 02, 2d 输出两个数字, %0 不满足时,左面补0
printf()
返回打印的字符个数,如果错误就返回一个负数1
2int n = printf("Hello world!"); // Hello world!
printf("\n%d\n",n); // 12
scanf()
:可以通过scanf函数的%m格式控制可以制定输入域宽(列数),按此宽度截取所需数据
1
scanf("%4d%2d%2d", &year, &month, &day); // 20130225
发展
什么是编译?
c/c++是编译型的语言
test.c —->编译—->链接—->test.exe
声明外部元素 extern
同一工程不同的文件
extern 只是声明变量,与声明文件不同
1 | // externFile.c |
常量
C语言的常量分为
1 | // const修饰 |
1 | // define 定义的标识符常量 |
1 | // 枚举类型:能够穷举的类型 |
字符串
2025-01-05 14:08:20
字符串的结束标志是
\0
的转义字符。在计算字符的长度时\0
是结束标识,不算作字符串内容。
1 | int string() { |
思考:有无\0是否有差别?
1
2
3
4char arr1[] = "abcdef";
char arr2[] = { 'a','b','c','d','e','f' };
printf("%s\n", arr1);
printf("%s\n", arr2);
打印结果的不同是因为没遇到转义字符\0
进行结束标识,会一直打印内存地址后面的内容直到遇到\0
停止打印。
\0
对使用库函数strlen()
函数的影响使用
strlen()
库函数需要引入string.h
头文件1
2
3
4
5
6
7
8
9
10
11printf("\"abc\"的长度为:%d\n", strlen("abc"));
// "abc"的长度为:3
char arr3[] = "abcdef";
char arr4[] = { 'a','b','c','d','e','f' };
char arr5[] = { 'a','b','c','d','e','f','\0' };
printf("arr3的长度为:%d\n", strlen(arr3));
// arr3的长度为:6
printf("arr4的长度为:%d\n", strlen(arr4));
// arr4的长度为:38
printf("arr5的长度为:%d\n", strlen(arr5));
// arr5的长度为:6原因同上,计算长度一直计算到
\0
转义字符作为结束标识。
转义字符
printf打印类型 | 含义 |
---|---|
%d | 打印整型 |
%c | 打印字符型 |
%s | 打印字符串 |
%f | 打印float浮点型 |
%lf | 打印double浮点型 |
%zu | 打印sizeof的返回值 |
转义字符 | 含义 |
---|---|
\n | 换行 |
\‘ | 打印字符常量单引号\’ |
\“ | 打印双引号 |
\\ | 打印斜杠 |
\r | 回车 |
\t | 一个制表符,类似 tab键 |
\ddd | 八进制,例如 \101 -> A |
\xdd | 十六进制 |
1 | int char_escape() { |
笔试题:
1 | // 程序输出什么 |
数组
1 | int array() { |
二维数组的初始化
1 | int arr3[][3] = {{1,2,3},{2,3,4},{3,4,5}...}; // 列必须得初始化 |
操作符
算术操作符
1 | + - * / % |
除号
/
两端都是整数时,执行的是整数除法,如果两端有一个是浮点型就执行浮点数的除法1
2
3
4
5
6
7
8
9
10
11int a = 7 / 2;
printf("%d\n", a); // 3
double b = 7 / 2;
printf("%f\n", b); // 3.0000
int c = 7.0 / 2;
printf("%d\n", c); // 3
printf("%f\n", c); // 0.0000
double d = 7.0 / 2;
printf("%f\n", d); // 3.5000
// 如果只想保留一位小数
printf("%.1f\n", d);// 3.5取模
%
两端必须是整数,不能是浮点数
移位操作符
1 | >> << |
位操作符
1 | & ^ | |
赋值操作符
1 | = -= += *= /= &= ^= \= >>= <<= |
- 双目运算符:操作符旁边有两个操作数
1 | ! 取反:**0-假,非0-真** |
1 | > |
逻辑操作符
1 | && 逻辑与 |
三目表达式
1 | exp1 ? exp2 : exp3 |
- 特点: 从左向右依次计算,整个表达式的结果是最后一个表达式的结果
1 | exp1, exp2, exp3, ... |
下标引用、函数调用和结构成员
1 | [] () . -> |
常见关键字
1 | auto break case char const continue default do double else enum |
C语言提供了丰富的关键字,这些关键字都是语言本身预先设定好的,用户自己是不能创造关键字的。
注:关键字,先介绍下面几个,后期遇到讲解
- 循环类
- for
- while
- do…while
- break
- continue
- 分支类
- if…else..
- switch..case..default
- goto
- 字符类
- char,short,int,float,double,long
- signed - 有符号的, unsigned - 无符号类型的
- enum - 枚举, struct - 结构体, union - 联合体(共用体)
- void - 空类型(一般用于函数的返回类型,函数参数)
- sizeof - 计算类型大小,单位字节
- typedef - 类型重命名
- 外部类
- extern - 声明外部符号的
- register - 寄存器,一般操作系统中使用, 建议变量放入寄存器
- static - 静态的
- return - 函数返回值
变量的命名:
有意义:
1
2int age;
float salary;名字必须是字母、数字、下划线组成,不能有特殊字符,同时不能以数字开头
1
2int 2b; // err
int _2b; // ok变量名不能是关键字
关键字typedef
typedef 顾名思义是类型定义,这里应该理解为类型重命名
比如:
1 | // 类型重命名 |
关键字 static
在 C 语言中:
static 是用来修饰变量和函数的
- 修饰局部变量:将局部变成内部全局
- 修饰全局变量:将具有外部链接属性的全局变量变成只有一个内部链接属性
- 修饰函数:只允许一个文件内部使用,工程的其他源文件不可使用
修饰局部变量
- 局部变量出了作用域,不销毁的。与全局变量不同的是,全局变量可以在任意位置进行读取,局部变量只有在特定的作用区域才能读取。
- 本质上,static 修饰局部变量的时候,改变了变量的存储位置,从栈区变成了静态区。栈区是退出栈后变量销毁,静态区的变量保持和程序一致
- 影响了变量的生命周期,生命周期变长,和程序的生命周期一样
- 在编译期间就创建了地址,程序运行时不修改存储地址
1 |
|
1 | void test() { |
修饰全局变量
static 修饰全局变量的时候
这个全局变量的外部链接属性
就变成了内部链接属性
其他源文件(.c)就不能再使用这个全局变量了
修饰函数
#define 定义常量和宏
定义变量名 => #define 定义常量
定义函数名 => #define 定义宏
- 宏是有参数的,参数无类型
- 宏名(宏参) 宏体
1 | // define定义标识常量 |
指针
内存1
内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的.
所以为了有效的使用内存, 就把内存划分成一个个小的内存单元, 每个内存单元的大小是1个字节.
为了能够有效的访问到内存的每个单元, 就给内存单元进行了编号, 这些编号被称为该内存单元的地址.
1 |
|
指针变量大小
- 不管什么类型,都是在创建指针变量
- 指针变量是用来存放地址的
- 指针变量的大小取决于一个地址存放的时候需要多大的空间
- 32位机器上的地址:32bit位 - 4 byte,所以指针变量的大小是4个字节
- 64位机器上的地址:64bit位 - 8 byte,所以指针变量的大小是8个字节
1 | int main() { |
结构体
将单一类型组合在一起,相当于 java 中的类,比如
1 | struct people |
1 | // 指针变量接收 |
1 | // 字符溢出问题 |
函数
二、分支语句和循环语句
分支语句
- if
- else
循环语句
- while
- for
- do…while…
goto语句
1. 什么是语句?
C语言中语句可以分为以下五类:
- 表达式语句
- 函数调用语句
- 控制语句
- 复合语句
- 空语句
控制语句用于控制程序的执行流程, 以实现程序的各种结构方式, 它们由特定的语句定义符组成, C语言有就中控制语句
可分为以下三类:
- 分支语句: if语句, switch语句
- 循环语句: do…while…, for语句, while语句
- 转向语句: break语句, goto语句, continue语句, return语句
2. 分支语句(选择结构)
2.1 if语句
1 | // 双分支 |
在C语言中如何表示真假?
0表示假, 非0表示真.
思考: 以下代码执行后结果?
1 | int a = 0; |
注意⚠️: else 只会跟在最近的一个 if 匹配
1 | // 正确的格式 |
练习
- 判断一个数是否为奇数
1 | printf("请输入数是否为奇数:"); |
- 输出1~100所有的奇数
1 | for (int i = 1; i <= 100; i++) { |
2.2 switch语句
switch语句也是一种分支语句.
常常用于多分支得到情况.
例如:
输入1,输出星期一
输入2,输出星期二
…
输入7,输出星期天
如果使用if…else..语句就太复杂了,那我们就得有不一样的语法形式.
这就是switch语句.
1 | switch(整型表达式) |
注意⚠️:
switch
的入口判断必须是整型case
的判断条件也必须是整型
1 | double score = 82; |
2.2.1 在switch语句中的 break
在switch
语句中,我们没办法直接实现分支, 搭配break
使用才能实现真正的分支.
1 | // 正常写法 |
switch 的每个 case 后都必须加上 break 跳出循环, 否则会继续执行, 直至遇到 break 语句.
如果每个 case 都不等于 input 输入的话, 则执行 default 中的语句
若 case 不加入 break 语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
printf("星期一\n");
//break;
case 2:
printf("星期二\n");
//break;
case 3:
printf("星期三\n");
break;
default:
printf("不是星期一二三\n");
printf("这是default中的语句\n");
break;
}
/**
输入:1
星期一
星期二
星期三
输入:2
星期二
星期三
**/若最后一个 case 与 default 同时无 break语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
//break;
default:
printf("不是星期一二三\n");
printf("这是default中的语句\n");
//break;
}
/**
输入:2
星期二
输入:3
星期三
不是星期一二三
这是default中的语句
输入:5
不是星期一二三
这是default中的语句
**/case 都加上 break 语句, default 不加, 同时 switch 语句后加入代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
default:
printf("不是星期一二三\n");
printf("这是default中的语句\n");
//break;
}
printf("default 之外的语句\n");
/**
输入:2
星期二
default 之外的语句
输入:5
不是星期一二三
这是default中的语句
default 之外的语句
**/
总结:
若是
case
语句不加break
语句, 就会一直 “流下去” 直至遇到break
语句或者执行完所有的switch
代码块.在
switch
之外的代码, 每次都会执行.在 C 语言中,
switch
语句可以用于判断字符型变量。字符型变量实际上是整数类型(ASCII 码),因此可以直接在switch
语句中使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22char input;
printf("请输入一个字符 (A, B, C): ");
scanf("%c", &input);
switch (input) {
case 'A':
case 'a': // 可以处理大小写
printf("你输入了 A\n");
break;
case 'B':
case 'b':
printf("你输入了 B\n");
break;
case 'C':
case 'c':
printf("你输入了 C\n");
break;
default:
printf("输入的不是 A、B 或 C\n");
break;
}
2.2.2 default 语句
如果表达的值与所有的case
标签的值都不匹配怎么办?
其实也没什么, 结果就是所有的语句都被跳过而已.
程序并不会终止, 也不会报错, 因为这种情况在C语言中并不认为是个错误.
但是, 如果你并不想忽略不匹配所有标签的表达式的值时该怎么办呢?
你可以在语句列表中增加一条 default 子句, 把下面的标签:
default:
写在任何一个case
标签可以出现的位置.
当 switch
表达式的值并不匹配所有case
标签的值时, 这个default
子句后面的语句就会执行.
所以, 每个switch
语句中只能出现一条default
子句.
default
中break
存在且放在首位1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29int day = 0;
scanf("%d", &day);
switch (day)
{
default:
printf("不是星期一二三\n");
printf("这是default中的语句\n");
break;
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
}
printf("default 之外的语句\n");
/**
输入:2
星期二
default 之外的语句
输入:5
不是星期一二三
这是default中的语句
default 之外的语句
**/default
中break
存在且放在中间1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
default:
printf("不是星期一二三\n");
printf("这是default中的语句\n");
break;
case 3:
printf("星期三\n");
break;
}
printf("default 之外的语句\n");
/**
输入:1
星期一
default 之外的语句
输入:5
不是星期一二三
这是default中的语句
default 之外的语句
**/default
中break
存在且放在中间, 但case1
,case2
不存在break
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
printf("星期一\n");
//break;
case 2:
printf("星期二\n");
//break;
default:
printf("不是星期一二三\n");
printf("这是default中的语句\n");
break;
case 3:
printf("星期三\n");
break;
}
printf("default 之外的语句\n");
/**
输入:1
星期一
星期二
不是星期一二三
这是default中的语句
default 之外的语句
输入:2
星期二
不是星期一二三
这是default中的语句
default 之外的语句
输入:3
星期三
default 之外的语句
输入:5
不是星期一二三
这是default中的语句
default 之外的语句
**/default
中break
不存在且放在中间1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
default:
printf("不是星期一二三\n");
printf("这是default中的语句\n");
//break;
case 3:
printf("星期三\n");
break;
}
printf("default 之外的语句\n");
/**
输入:2
星期二
default 之外的语句
输入:3
星期三
default 之外的语句
输入:5
不是星期一二三
这是default中的语句
星期三
default 之外的语句
**/default
中break
不存在且放在中间, 同时case2
与case3
中的break
也不存在1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
//break;
default:
printf("不是星期一二三\n");
printf("这是default中的语句\n");
//break;
case 3:
printf("星期三\n");
//break;
}
printf("default 之外的语句\n");
/**
输入:2
星期二
不是星期一二三
这是default中的语句
星期三
default 之外的语句
输入:3
星期三
default 之外的语句
输入:5
不是星期一二三
这是default中的语句
星期三
default 之外的语句
**/
总结: 顺流直下, 从哪个 “口”(case/default
) 流下去, 若没遇到break
, 则会一直执行下去.
3. 循环语句
for
循环: 初始条件 => 循环条件判断 => 执行循环体 => 改变条件 => 再 进行循环条件判断 => …1
2
3
4
5
6
7
8
9
10
11
12
13
14// 与 if 类似,如果只执行一句循环体,可以省略括号,但若是多句循环体,必须加上括号,如不加,则只执行一句循环体,多的代码只是顺序执行下只执行一次
for (int i = 0; i < 3; i++)
printf("%d\t",i);
printf("xixi\n");
// 0 1 2 xixi
for (int i = 0; i < 3; i++) {
printf("%d\t", i);
printf("xixi\n");
}
/** 0 xixi
1 xixi
2 xixi **/提问: 下面打印了几个xixi?
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 提问; 下面打印了几个xixi
printf("下面打印为几个xixi?\n");
int i = 0;
int j = 0;
for (; i < 3; i++) {
for (; j < 3; j++) {
printf("xixi\n");
}
}
/* 因为 j 执行三次后未初始化,故第二次i进入的时,j=3不进入循环体
xixi
xixi
xixi
*/
while
循环: 先判断, 后循环while
循环中的break
是用于永久的终止内层的循环continue
跳过本次循环后面的代码, 直接去进行条件判断, 进行下一次循环的判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// continue 导致陷入死循环
int i = 1;
while (i <= 10)
{
if (5 == i)
{
continue; // error, 这里会导致陷入死循环
// 因为不会执行 continue 之后的代码,导致 i 恒等 5, 然后一直循环跳过
}
printf("%d\t", i);
i++;
}
/**
输出结果: 1 2 3 4 死循环
**/1
2
3
4
5
6
7
8// 下面代码执行结果?
int j = 1;
while (j <= 10) {
j++;
if (5 == j)
continue;
printf("%d\t", j); // 2 3 4 6 7 8 9 10 11
}再看几个代码 :
1
2
3
4
5
6
7
8
9
10
11// 代码1
// getchar(): 从键盘上获取字符,返回字符的 ASCII 码值,即 int 类型
// putchar(int): 打印字符,等于 printf("%c",97); 不会自动换行!
// EOF: end of file, 鼠标右键转到定义, 发现=-1, 是个整型
int ch = 0;
while ((ch = getchar()) != EOF) {
putchar(ch);
}
/**
实现效果: 输入一个数,打印一个数,一直循环
**/这里有一个输入缓冲区的问题.
输入 a, 按下回车, 输入缓冲区里有两个字符
a
,\n
putchar()
其实执行了两次.举一个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 清理缓存解决方法,中间加上getchar();抵消掉缓冲区的内容.
char password[20] = { 0 };
printf("请输入密码:>");
scanf("%s", password); // 数组本身存储的就是地址,无需取地址符号&
// 读取 \n
// getchar(); // 这样只能抵消掉一个字符,如果是多个字符,则失效
// 清理多个字符的缓存
int ch = 0;
while((ch = getchar()) != '\n') {
; // 一直读取到 \n, 中间不进行操作
}
printf("请确认密码Y/N:>");
int res = getchar();
if ('Y' == res)
printf("YES\n");
else
printf("NO\n")1
2
3
4
5
6
7
8
9
10
11
12
13// 代码2
char ch = '\n';
while ((ch = getchar()) != EOF) {
if (ch < '0' || ch > '9')
continue;
putchar(ch);
}
/*
输出数字字符, 只打印数字字符,跳过其他字符
但是从第二个输入数字的时候,不会换行
输入: qwer123qwe
123
*/
do...while...
循环: 先循环, 后判断1
2
3
4
5
6
7
8
9// do...while...中使用continue出现死循环,与while中原因一致
// 因为不会执行 continue 之后的代码,导致 i 恒等 5, 然后一直循环跳过
int i = 1;
do {
if (i == 5)
continue;// 因为不会执行 continue 之后的代码,导致 i 恒等 5, 然后一直循环跳过
printf("%d\t", i);
i++;
} while(i <= 10);
练习
计算 n 的阶乘
1
2
3
4
5
6
7
8// 1. 计算 n 的阶乘
int loop_work_example1(int n) {
int sum = 1;
for (; n >= 1; n--) {
sum *= n;
}
return sum;
}计算
1!+2!+3!+...+10!
1
2
3
4
5
6int sum = 0;
for (int i = 1; i <= 10; i++) {
int res = loop_work_example1(i);
sum += res;
}
printf("1!+2!+3!+...+10!=%d\n", sum);在一个有序数组中查找具体的某个数字n.(二分查找)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// 3. 在一个有序数组中查找具体的某个数字n.(二分查找)
int binary_search(int arr[], int n, int length) {
int left = 0;
//int right = sizeof(arr) / sizeof(arr[0]) - 1; // error
// 等效于:sizeof(int*) / sizeof(int) - 1, 解决办法, 外部传入
int right = length - 1;
int mid = 0;
while (left <= right) {
mid = (left + right) / 2;
if (arr[mid] == n) return mid; // 返回下标
if (n < arr[mid])
right = mid - 1;
if (arr[mid] < n)
left = mid + 1;
}
return -1; // 未查到
}
// 3.二分查找
int arr[] = { 1,5,6,7,9,12,13,16,29,31 };
int length = sizeof(arr) / sizeof(arr[0]);
int index1 = binary_search(arr, 13, length);
printf("13所在的下标为:%d\n", index1); // 6
int index2 = binary_search(arr, 15, length);
printf("15所在的下标为:%d\n", index2); // -1注意:在C语言中,当数组作为函数参数传递时,它会退化为指针。函数中的
sizeof(arr)
实际上计算的是指针的大小(而非整个数组的大小)函数内部无法通过
sizeof
获取数组的实际长度,必须通过额外参数传递。编写代码,演示多个字符从两端移动,向中间汇聚.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 4. 编写代码,演示多个字符从两端移动,向中间汇聚.
int loop_work_example4() {
char arr[] = "welcome to China!!!";
char arr2[] = "###################";
int length = strlen(arr);
for (int i = 0; i <= length / 2; i++) {
printf("%s\n", arr2);
arr2[i] = arr[i];
arr2[length - 1 - i] = arr[length - 1 - i];
// 导入 Windos.h 头文件
Sleep(1000);
// 清空屏幕,需要导入 stdlib.h
system("cls"); // system 是一个库函数,可以执行系统命令
}
/*
###################
w#################!
we###############!!
wel#############!!!
welc###########a!!!
welco#########na!!!
welcom#######ina!!!
welcome#####hina!!!
welcome ###China!!!
welcome t# China!!!
*/
return 0;
}编写代码实现,模拟用户登陆情景,并且只能登陆三次.(只允许输入三次密码,如果密码正确则提示登陆成功, 如果三次均输入错误,则退出程序).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// 5. 编写代码实现,模拟用户登陆情景,并且只能登陆三次.
// (只允许输入三次密码,如果密码正确则提示登陆成功,
// 如果三次均输入错误,则退出程序).
int loop_work_example5() {
char password[] = "123456";
char input[7] = "";
int length = strlen(password);
int count = 1;
int flag_error = 0;
do {
flag_error = 0;
printf("请输入密码(六位):>");
scanf("%s", input);
for (int i = 0; i < length; i++) {
if (password[i] != input[i]) {
printf("密码错误!%d次\n", count);
flag_error = 1;
count++;
break;
}
}
if (!flag_error) {
printf("登录成功!\n");
break;
}
} while (count <= 3);
if (flag_error)
printf("登陆失败,错误次数:%d\n", count - 1);
return 0;
}1
2
3
4
5// 还可以使用 strcmp() 进行字符串比较
// 两个字符串若是相等,则返回 0
if (strcmp(password,"123456") == 0) {
printf("登陆成功");
}猜字大小的游戏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53// #include <stdlib.h>
// void srand(unsigned int seed); // 设置随机数起始点
// #include <time.h>
// int time(); // 返回时间戳
// 6. 猜数字大小
int game() {
// 0~99 => 1~100
int res = rand() % 100 + 1;
int guess = 0;
while (1) {
printf("请输入数字:>");
scanf("%d", &guess);
if (guess == res) {
printf("猜对了!结果是%d\n", guess);
break;
}
if (guess < res)
printf("猜小了\n");
if (guess > res)
printf("猜大了\n");
}
return 0;
}
int loop_work_example6() {
//开始界面
int input = 0;
// srand -> #include <stdlib.h>
// time -> #include <time.h>
srand((unsigned int)time(NULL)); // 设置随机值起始种子为时间戳
do {
printf("*************************\n");
printf("****** 1.play *******\n");
printf("****** 0.exit *******\n");
printf("*************************\n");
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入非法,请重新选择\n");
break;
}
} while (input);
return 0;
}
三、函数
- 函数是什么
- 库函数
- 自定义函数
- 函数参数
- 函数调用
- 函数的嵌套调用和链式访问:函数返回值作为其他函数的参数
- 函数的声明和定义
- 函数递归
1. 函数是什么?
函数: 子程序
- 是一个大型程序重点某部分代码, 由一个或多个语句快自称. 他负责完成某项特定任务, 而且相较于其他代码, 具备相对的独立性.
- 一般会有输入参数并有返回值, 提供对过程的封装和细节的隐藏. 这些代码通常被集成为软件库.
2. C语言中函数的分类:
- 库函数
- 自定义函数
2.1 库函数:
为什么会有库函数?
- 我们知道在我们学习C语言变成的时候, 总是在一个代码编写完成之后迫不及待的想知道结果, 想把这个结果打印到我们的屏幕上看看. 这个时候我们会频繁的使用一个功能: 将信息按照一定的格式打印到屏幕上(printf)
- 在编程的过程中,我们会频繁的做一些字符串的拷贝(strcpy)
- 在编程时我们也会计算n的k次方这样的运算(pow)
像上面我们描述的基础功能,他们不是业务性的代码。我们在开发的过程中每个程序员都可能用得到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
4. 函数参数
4.1 实参
真实传给函数的参数,叫实参。
实参可以是: 常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,她们都必须有确定的值,以便把这些值传送给形参。
4.2 形参
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
注意接收值还是地址
- 当实参传递给形参的时候,形参是实参的一份临时拷贝
- 对形参的修改不能修改实参
1 |
|
5. 函数调用:
5.1 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
5.2 传址调用
- 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
- 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量
5.3 练习
- 写一个函数可以判断一个数是不是素数。
- 写一个函数判断一年是不是闰年。
- 写一个函数,实现一个整型有序数组的二分查找。
- 写一个函数,每调用一次这个函数,就会将 num 的值增加1
7. 函数的声明和定义
7.1 函数声明:
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是否存在,函数声明决定不了。
- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中。
7.2 函数定义:
函数的定义是指函数的具体实现,交代函数的功能实现。
1 | // 在本文件中的声明 |
1 | // 在头文件中的声明 |
递归求斐波那契
1 | // 求第n个斐波那契数列 |
五、操作符详解
- 各种操作符的介绍
- 表达式求值
1. 操作符分类:
算术操作符:+ - * / %
移位操作符:<< >>
位操作符:& | ^
赋值操作符:+= -= *= /= …
单目操作符
关系操作符
逻辑操作符
条件操作符:
1
2 // 三目表达式
condition ? yes : no逗号表达式
下标引用、函数调用和结构成员
2. 算术操作符
1 | + - * / % |
- 除了
%
操作符之外,其他的几个操作符可以作用于整数和浮点数 - 对于
/
操作符如果两个操作数都为整数,执行整数除法.但是只要有浮点数执行的就是浮点数除法. %
操作符的两个操作数必须为整数.返回的是整除之后的余数.
3. 移位操作符
1 | << 左移操作符 |
注: 移位操作符的操作数只能是整数
3.1 左移操作符
- 左移有乘2的n次方的效果 (扩大)
- 移位规则:
左边抛弃、右边补0
1 | // 正的整数的原码、反码、补码相同 |
3.2 右移操作符
- 移位规则:
首先右移运算分两种:具体根据编译器的不同而不同
逻辑移位
左边用0填充,右边丢弃
算术移位(vs2019)
左边用原改制的符号位填充,右边丢弃
1 | int a = 7; |
1 | int c = -7; |
警告⚠️:
对于移位运算符, 不要移动负数位, 这个是标准未定义的.
1 | int num = 10; |
4. 位操作符
位操作符有:
1 | & // 按补码位与 |
1 | int a = 3; |
异或的特点:
a^a = 0
0^a = a
例如:
3^3 : 011 ^ 011 = 000 = 0 5^0 : 101 ^ 000 = 101 = 5 进而: 3^3^5 = 5 那么: 3^5^3 = ? 3^5^3 = 011^101 = 110^011 =101 = 5
满足交换律
所以:3^3^5 = 3^5^3, 满足交换律
练习: 若不增加额外变量,将两个数交换?
1 | int num1 = 3; |
编写代码实现: 求一个整数存储在内存中的二进制中1的个数
1 | //问题2:求一个整数存储在内存中的二进制中1的个数 |
5. 赋值操作符
赋值操作符可以连续使用, 比如
1 | int a = 10; |
复合赋值符
+=、-=、*=、/=、%=
<<=、>>=
&=、|=、^=
6. 单目操作符
详情见:单目操作符
需要注意的是:
sizeof
是操作符, 不是函数, 计算结果包含\0
strlen
是库函数, 用来求字符串长度, 计算结果不包含\0
7. 关系操作符
详情见:关系操作符
1 | if ("abc" == "abcdef") // error,这样比较的是首地址 |
8. 逻辑操作符
逻辑操作符有哪些:
1 | && 逻辑与 |
区分逻辑与和按补码位与
区分逻辑或和按补码位或
1 | 1&2 = 001&010 = 000 => 0 |
短路求值:只有第一个运算的逻辑值无法判断结果,才对第二个运算数进行求值
1 | int i = 0, a = 0, b = 2, c = 3, d = 4; |
9. 逗号表达式
详情见: 逗号表达式
1 | a = get_val(); |
10. 表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定.
同样, 有些表达式的操作数在求值的过程中可能需要转换为其他类型.
10.1 隐式类型转换
C的整形算术运算总是至少以缺省整型类型的精度来进行的.
为了获得这个精度, 表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换成为整型提升
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行, CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度, 同时也是CPU的通用寄存器的长度
因此, 及时两个
char
类型的相加, 在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度.通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器执行中可能有这种字节相加指令).所以, 表达式中各个长度可能小于int长度的整型值,都必须先转换为
int
或unsigned int
,然后才能送入CPU去执行运算.
如何进行整型提升呢?
整型提升是按照变量的数据类型的符号位来提升的
1 | // 负数的整型提升 |
计算:
1 | char a = 5; |
1 | char a = 5; |
10.2 算术转换
如果某个操作符的各个操作数属于不同的类型, 那么除非其中一个操作数的转换为另一个操作数的类型, 否则操作就无法进行. 下面的层次体系称为寻常算术转换
1 | long double |
上述能从下往上转换, 例如 int
+ double
会转换成 double
10.3 操作符的属性
复杂表达式的求值有三个影响的因素.
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
两个相邻的操作符先执行哪个? 取决于他们的优先级. 如果两者的优先级相同, 取决于他们的结合性.
算术操作符(+-*/) > 关系操作符(==,>=) > 位操作符 > 赋值操作符 > 逗号操作符
操作符 | 描述 | 用法示例 | 结果类型 | 结合性 | 是否控制求值顺序 | ||||
---|---|---|---|---|---|---|---|---|---|
() | 聚组 | (表达式) | 与表达式相同 | N/A | 否 | ||||
() | 函数调用 | rexp(rexp, rexp…) | rexp | L-R | 否 | ||||
[] | 下标引用 | rexp[rexp] | lexp | L-R | 否 | ||||
. | 访问结构成员 | lexp.member_name | lexp | L-R | 否 | ||||
-> | 访问结构指针成员 | rexp->member_name | lexp | L-R | 否 | ||||
++ | 后缀自增 | lexp++ | rexp | L-R | 否 | ||||
— | 后缀自减 | lexp— | rexp | L-R | 否 | ||||
! | 逻辑反 | !rexp | rexp | R-L | 否 | ||||
~ | 按位取反 | ~rexp | rexp | R-L | 否 | ||||
+ | 单目,表示正值 | +rexp | rexp | R-L | 否 | ||||
- | 单目,表示负值 | -rexp | rexp | R-L | 否 | ||||
++ | 前缀自增 | ++lexp | rexp | R-L | 否 | ||||
— | 前缀自减 | —lexp | rexp | R-L | 否 | ||||
* | 间接访问 | *rexp | lexp | R-L | 否 | ||||
& | 取地址 | &lexp | rexp | R-L | 否 | ||||
sizeof | 返回字节长度 | sizeof rexp | rexp | R-L | 否 | ||||
(类型) | 类型转换 | 类型(rexp) | rexp | R-L | 否 | ||||
* | 乘法 | rexp * rexp | rexp | L-R | 否 | ||||
/ | 除法 | rexp / rexp | rexp | L-R | 否 | ||||
% | 取余 | rexp % rexp | rexp | L-R | 否 | ||||
+ | 加法 | rexp + rexp | rexp | L-R | 否 | ||||
- | 减法 | rexp - rexp | rexp | L-R | 否 | ||||
<< | 左移位 | rexp << rexp | rexp | L-R | 否 | ||||
>> | 右移位 | rexp >> rexp | rexp | L-R | 否 | ||||
> | 大于 | rexp > rexp | rexp | L-R | 否 | ||||
>= | 大于等于 | rexp >= rexp | rexp | L-R | 否 | ||||
< | 小于 | rexp < rexp | rexp | L-R | 否 | ||||
<= | 小于等于 | rexp <= rexp | rexp | L-R | 否 | ||||
== | 等于 | rexp == rexp | rexp | L-R | 否 | ||||
!= | 不等于 | rexp != rexp | rexp | L-R | 否 | ||||
& | 按补码位与 | rexp & rexp | rexp | L-R | 否 | ||||
^ | 按补码位异或 | rexp ^ rexp | rexp | L-R | 否 | ||||
\ | 按补码位或 | rexp \ | rexp | rexp | L-R | 否 | |||
&& | 逻辑与 | rexp && rexp | rexp | L-R | 是,短路求值,提前终止 | ||||
\ | \ | 逻辑或 | rexp | rexp | rexp | L-R | 是 | ||
?: | 条件操作符 | rexp ? rexp : rexp | rexp | N/A | 是 | ||||
= | 赋值 | lexp = rexp | rexp | R-L | 否 | ||||
+= | 加等 | lexp += rexp | rexp | R-L | 否 | ||||
-= | 减等 | lexp -= rexp | rexp | R-L | 否 | ||||
*= | 乘等 | lexp *= rexp | rexp | R-L | 否 | ||||
/= | 除等 | lexp /= rexp | rexp | R-L | 否 | ||||
%= | 取模等 | lexp %= rexp | rexp | R-L | 否 | ||||
<<= | 左移等 | lexp <<= rexp | rexp | R-L | 否 | ||||
>>= | 右移等 | lexp >>= rexp | rexp | R-L | 否 | ||||
&= | 按补码位与等 | lexp &= rexp | rexp | R-L | 否 | ||||
^= | 按补码位异或等 | lexp ^= rexp | rexp | R-L | 否 | ||||
\ | = | 按补码位或等 | lexp \ | = rexp | rexp | R-L | 否 | ||
, | 逗号 | rexp, rexp | rexp | L-R | 是, 最后一个是最终值 |
1 | int aa = 10; |
计算以下结果(问题表达式:有问题的写法,不推荐):
1 | int a = 1; |
1 | int a2 = 1; |
六、指针
- 指针是什么
- 指针和指针类型
- 野指针
- 指针运算
- 指针和数组
- 二级指针
- 指针数组
1. 指针是什么
指针是什么
指针理解的2个要点:
- 指针是内存中一个最小单元的编号, 也就是地址
- 平时口语中说的指针, 通常指的是指针变量, 是用来存储内存地址的变量
总结: 指针就是地址, 口语中所说的指针通常指的是指针变量.
那我们可以这样理解:
内存: 内存
指针变量
我们可以通过
&
(取地址操作符)取出变量的内存真实地址, 把地址可以存放到一个变量中, 这个变量就是指针变量
1 | int a = 10; // 在内存中开辟一块空间 |
总结:
指针变量, 用来存储地址的变量. (存放在指针中的值都被当成地址处理).
那这里的问题是:
- 一个小的单元到底是多大? (一个字节)
- 如何编址?
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是较为合理的.
详情见:指针变量大小
2. 指针和指针类型
我们知道, 变量有不同的类型, 整型, 浮点型等. 那指针有没有类型呢?
准确的说: 有的.
当有这样的代码:
1 | int num = 10; |
要将&num(num的地址)保存到p中, 我们知道p是一个指针变量, 那么它的类型是什么样的呢?
我们给指针变量相应的类型.
1 | char* pc = null; |
总结:
2.1 指针的解引用
1. 指针变量的类型决定了它被解引用操作的时候到底访问几个字节
- 如果是
int*
的指针, 解引用访问四个字节 - 如果是
char*
的指针, 解引用访问一个字节等等
1 | // 00010001 00100010 00110011 01000100 |
2.2 指针的+-问题
2. 指针的类型决定了指针+-1时跳过几个字节
1 | int aa = 0x11223344; |
3. 野指针
概念: 野指针就是指针指向的位置是不可知的(随机的, 不正确的, 没有明确限制的)
3.1 野指针成因
指针未初始化
1
2
3
4
5// 未初始化
// p没有初始化,就意味着没有明确的指向
// 一个局部变量不初始化的话,放的是随机值:0xcccccccc
int* p;
*p = 10; // 非法访问内存了,这里的p就是野指针指针越界访问
1
2
3
4
5
6
7// 2.指针越界
int arr[10] = { 0 };
int* p = arr; // 等价于: &arr[0],数组名就是首地址
for (int i = 0; i <= 10; i++) {
*p = i;
p++; // 这里在 i=10 时p指向的地址arr[10],是未定义的,下标越界了
}指针指向的空间释放
这里放在动态内存开辟的时候讲解,
4. 指针运算
- 指针+-整数
- 指针-指针
- 指针的关系运算
4.1 指针+-整数
1 |
|
4.2 指针-指针
结论: 指针-指针的绝对值=指针和指针之间元素的个数
1 | // 指针-指针的绝对值=指针和指针之间元素的个数 |
1 | int my_strlen(char *s) { |
4.3 指针的关系运算(比较大小)
1 | // ok,因为vp最终指向了values[0] |
代码简化,将代码修改为:
1 | // error, 不推荐, vp最终指向了values[-1] |
实际在绝大部分的编译器上可以顺利完成任务, 然而我们还是避免这样写, 因为标准并不保证它可行.
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较, 但是不允许与指向第一个元素之前的那个内存位置的指针进行比较
5. 指针和数组
我们看一个例子:
1 | int arr[10] = { 1,2,3,4,5,6,7,8,9,0 }; |
可见数组名和数组首元素的地址是一样的
结论: 数组名表示的数组首元素的地址. (两种情况除外)
作为
sizeof
运算符的操作数时
sizeof(数组名)
返回整个数组占用的内存大小,而非指针的大小。
1
2 int arr[5];
printf("%zu\n", sizeof(arr)); // 输出 5 * sizeof(int),而非 sizeof(int*)作为取地址运算符
&
的操作数时
&数组名
返回整个数组的地址,类型为指向数组的指针(而非指向首元素的指针)。
1
2 int arr[5];
int (*p)[5] = &arr; // p的类型是 int(*)[5],指向整个数组的指针关键区别
虽然
arr
和&arr
的地址值相同,但类型不同:
arr
的类型是int*
(指向首元素的指针)。&arr
的类型是int(*)[5]
(指向包含5个整数的数组的指针)。
那么这样写代码是可行的:
1 | int arr[0] = { 1,2,3,4,5,6,7,8,9,0 }; |
既然可以把数组名当成地址存放到一个指针中, 那么使用指针访问一个数组也成为了可能.
例如:
1 | int arr[3] = { 1,2,3 }; |
总结:
1 | int arr[] = { 1,2,3 }; |
6. 二级指针
1 | // 二级指针变量是存放一级指针变量的地址的 |
7. 指针数组
指针数组是指针还是数组?
答案: 是数组. 是存放指针的数组.
1 | int a = 10; |
1 | // 普通的遍历二维数组 |