C语言复习

https://www.bilibili.com/video/BV1cq4y1U7sg?p=1&vd_source=594d36a0860080a36fe599e0b84e5fb2

  1. 必须实践敲代码 (不是抄写 ! 抄写没有意义)
  • 理清别人代码的思路
  • 不看别人的代码, 按照代码的思路, 自己写代码
  • 写注释 ! 写注释 ! 写注释 !
  1. 必须画图, 理解内存布局

  2. 要不断调试

  3. 多写, 多刷题

  • 牛客 PTA 力扣

一、初识C语言

1.1 第一个C语言程序

1
2
3
4
5
6
7
8
9
10
//C语言从主函数main开始执行 (必须要有main函数且一个工程中只能有一个main函数)
//printf属于一个库函数
//编译+运行快捷键: ctrl+F5
#include<stdio.h> //引用头文件stdio.h

int main() {
printf("hello world");

return 0;
}

1.2 C语言数据类型

  • char 字符数据类型
  • short 短整型
  • int 整型
  • long 长整型
  • long long 长长整型
  • float 单精度浮点数
  • double 双精度浮点数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
//printf("hello world");

//sizeof--关键字 (操作符 !!不是函数!! ) --计算类型或变量所占空间大小
printf("%d\n", sizeof(char));
printf("%d\n", sizeof(short));
printf("%d\n", sizeof(int));
printf("%d\n", sizeof(long));
printf("%d\n", sizeof(long long));
printf("%d\n", sizeof(float));
printf("%d\n", sizeof(double));

return 0;
}

输出结果 (单位: 字节)

1
2
3
4
5
6
7
1
2
4
4 //sizeof(long) >= sizeof(int)
8
4
8

计算机中的单位

  • bit 比特位
  • byte 字节 1 byte = 8 bit
  • KB 1 KB = 1024 byte
  • MB 1 MB = 1024 KB
  • GB 1 GB = 1024 MB
  • TB 1 TB = 1024 GB
  • PB 1 PB = 1024 TB

1.3 常量&变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
//%d - 整型
//%f - float
//%lf - double
//%s - 字符串
//%p - 按地址格式打印, 十六进制的打印
//%u - 无符号10进制整数
//%x,%X 读入十六进制整数
int age = 20;
double weight = 80;
age = age + 1;
printf("%d\n", age);
printf("%lf\n", weight);


return 0;
}

局部变量&全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//全局变量 - {}外定义
int a = 100;

int main() {
/*
局部变量 - {}内定义
局部变量与全局变量名字冲突时, 局部优先
但是不能使局部变量与全局变量名字相同

定义变量的同时必须给变量初始化 (赋值)
*/
int a = 10;
printf("%d\n", a);

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
32
int main() {
//scanf 输入函数, 括号中间的分隔符是什么, 输入时候的分隔符就是什么 (本段代码中为空格)
int a = 0;
int b = 0;
int sum = 0;
scanf("%d %d", &a, &b);
sum = a + b;
printf("sum = %d\n", sum);

return 0;
}

/*
上面代码在VScode中报错
C4996 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.

在整个.c文件第一行加#define _CRT_SECURE_NO_WARNINGS 1即可解决
*/

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main() {
int a = 0;
int b = 0;
int sum = 0;
scanf("%d %d", &a, &b);
sum = a + b;
printf("sum = %d\n", sum);

return 0;
}

变量的作用域和生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* 局部变量作用域: 变量所在的局部范围
* 全局变量作用域: 整个工程(但是需要引入)
*
* extern int year //该语句用来引入其他文件中的全局变量
*/
int year = 2022; //变量的命名可以写在函数外, 其他运行代码必须写在函数体外

int main() {
printf("年: %d\n", year);
{
int month = 12;
printf("月: %d\n", month);
}
return 0;
}
/*
* 变量的生命周期: 变量的创建和销毁之间的时间段
*
* 局部变量生命周期: 进入局部范围开始, 出局部范围结束
* 全局变量生命周期: 程序的生命周期
*/

常量

  • 字面常量

  • const 修饰的常变量, 变量被 const 修饰后变量值就不允许被修改

    const修饰指针变量的时候:

    1. const 如果放在 * 的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改

    变。但是指针变量本身的内容可变。

    1. const 如果放在 * 的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指

    针指向的内容,可以通过指针改变。

  • #define 定义的标识符常量

  • 枚举常量

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
#define 一万 10000;
//枚举声明语句 枚举变量的所有可能取值
enum Ocean {
//枚举常量的第一个常量可以给其赋初值 PACIFIC = 6, 则此后常量值从6开始
PACIFIC, //注意逗号
INDIAN_OCEAN,
ATLANTIC,
ARCTIC_OCEAN //注意没有逗号
}; //注意分号

int main() {
//字面常量
12345; //整型
'a'; //字符
"abcsde"; //字符串

//const修饰的常变量
const int num = 123; //如果要修改num值, 会报错

//#define定义的标识符常量
int one_h = 一万;
printf("%d\n", one_h);

//枚举常量 (可以一一列举的常量)
enum Ocean place = ATLANTIC; //枚举赋值语句
printf("%d\n", PACIFIC);
printf("%d\n", INDIAN_OCEAN);
printf("%d\n", ATLANTIC);
printf("%d\n", ARCTIC_OCEAN);

return 0;
}

输出:

1
2
3
4
5
10000
0 //枚举常量是常量, 默认从0开始
1
2
3

1.4 字符串

  • 字符串就是一串字符 (用 "" 扩起来的字符)

    字符串在结尾的位置隐藏了一个 \0 字符作为字符串的结束标志,

    通过 strlen() 函数字符串长度时不算在里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<string.h> //使用 strlen() 函数做引入头文件
int main() {
char array[] = "hello";
char array2[] = { 'h', 'e', 'l', 'l', 'o' };
//求两个字符数组的长度
printf("%d\n", strlen(array));
printf("%d\n\n", strlen(array2));
//打印出两个字符数组
printf("%s\n", array);
printf("%s\n\n", array2);

//修改第二个数组
char array3[] = {'h', 'e', 'l', 'l', 'o', '\0'};
//求修改后两个字符数组的长度
printf("%d\n", strlen(array));
printf("%d\n\n", strlen(array3));
//打印出修改后两个字符数组
printf("%s\n", array);
printf("%s\n\n", array3);

return 0;
}

输出:

1
2
3
4
5
6
7
8
9
10
11
5
65 //随机值

hello
hello烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫虁鯵?

5
5

hello
hello

image-20221221172200680


1.5 转义字符

转义字符 释义
? 在书写连续多个问号时使用,防止他们被解析成三字母词
\‘ 用于表示字符常量’
\“ 用于表示一个字符串内部的双引号
\\ 用于表示一个反斜杠,防止它被解释为一个转义序列符。
\a 警告字符,蜂鸣
\b 退格符
\f 进纸符
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符
\ddd ddd 表示1~3个八进制的数字。 如: \130 X
\xdd dd 表示2个十六进制数字。 如: \x30 0

1.6 选择语句

1
2
3
4
5
6
7
8
9
10
11
int main() {
int complete = 0;
printf("今天的任务完成了吗? ( 1 or 0 ) :> ");
scanf("%d", &complete);
if (complete == 1)
printf("恭喜你!!!");
else
printf("不要摆啊!!!");

return 0;
}

1.7 循环语句

1
2
3
4
5
6
7
8
9
10
11
int main() {
int age = 0;
while (age < 100) {
printf("You are alive, %d\n", age);
age++;
}
if (age = 100)
printf("You died");

return 0;
}

1.8 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//必须把 函数定义 写在 函数使用 之前
int multiply(int a, int b) {
int c = 0;
c = a * b;

return c;
}

int main() {
int num1 = 12;
int num2 = 13;
int sum = 0;
sum = multiply(num1, num2);
printf("%d", sum);

return 0;
}

1.9 数组

1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
int array_int[3] = { 1, 2, 3 };
char array_char[5] = { 'a', 'b', 'c' }; //不完全初始化, 默认赋0
for (int i = 0; i < 3; i++)
printf("%d ", array_int[i]);
printf("\n");
for (int j = 0; j < 5; j++) {
printf("%c ", array_char[j]); //将未赋值的部分输出为空格
}
printf("end");

return 0;
}

输出:

1
2
1 2 3
a b c end

1.10 操作符

整数的二进制表示

  • 原码
  • 反码 – 符号位不变, 其余位置按位取反
  • 补码 (整数在内存中的存储) – 反码 + 1
1
2
3
4
5
6
7
8
9
10
11
/*
整数二进制共 4 * 8 = 32 位 (整型 4 个字节)
最高位表示符号位 0 表示正数, 1 表示负数

正整数的原码、反码、补码相同(规定)
*/

-1:
10000000000000000000000000000001 (原码)
11111111111111111111111111111110 (反码)
11111111111111111111111111111111 (补码)
  • 算数操作符

    +

    -

    *

    /

    %

  • 移位操作符 (移动二进制位)

    左移运算符: << 低位补 0

    右移运算符: >>

  • 位操作符

    按位与: &

    按位或: |

    按位异或: ^

  • 赋值操作符

    =

    +

    =

    -=

    *=

    /=

    &=

    ^=

    |=

    >>=

    <<=

  • 单目操作符 (只有一个操作数)

    操作符 作用
    ! 逻辑反操作
    - 负值
    + 正值
    & 取地址
    sizeof 操作数的类型长度(以字节为单位)
    ~ 对一个数的二进制按位取反
    前置、后置–
    ++ 前置、后置++
    * 间接访问操作符(解引用操作符)
    (类型) 强制类型转换
  • 关系操作符

    >

    >=

    <

    <=

    !=

    ==

  • 逻辑操作符

    &&

    ||

  • 条件操作符 (三目运算符)

    exp1 ? exp2 : exp3

    如果 exp1 为 true, 表达式的值为 exp2; 为 false, 表达式的值为 exp3

  • 逗号表达式

    exp1, exp2, exp3 ... expN

    从左往右运算, 逗号表达式的值为最后一个表达式 expN 的值

  • 下标引用

    []

  • 函数调用

    ()

  • 结构成员

    .

    ->


1.11 常见关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
auto  自动创建, 自动销毁, 所有局部变量都是auto
break   case  char  const   continue  default  do   double else  enum  
extern 用来生命外部符号
float  for   goto  if   int   long  
register    寄存器关键字 (大量频繁被使用的数据, 放在寄存器中提高效率)
return   short  
signed 有符号的
unsigned   无符号的
sizeof  
static 静态的
struct  switch  typedef
union 联合体 (共用体)
void 空
volatile  while
  • typedef 类型重命名
1
2
3
4
5
6
7
//给基本数据类型起一个别名
typedef int t_int;
int main() {
t_int a = 1;

return 0;
}
  • static 修饰变量与函数
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
/*
* static修饰局部变量
* 改变了变量的生命周期——本质是改变了变量的存储类型
*
* static修饰全局变量
* 使被修饰的全局变量只能在自己所在的源文件内部使用
* 之所以可以在其他文件中使用,是因为全局变量具有外部链接属性
* 被static修饰之后,就变成了内部链接属性,其他源文件无法连接此全局变量
*
* static修饰函数
* 使被修饰的函数只能在自己所在的源文件内部使用
* 之所以可以在其他文件中使用,是因为函数具有外部链接属性
* 被static修饰之后,就变成了内部链接属性,其他源文件无法连接此函数
*/
void test_common() {
int i = 0;
i++;
printf("%d", i);
}
void test_static() {
static int i = 0;
i++;
printf("%d", i);
}
int main() {
int i = 0;
for(i = 0; i < 10; i++) {
test_common();
}
printf("\n\n");
for (i = 0; i < 10; i++) {
test_static();
}

return 0;
}

输出:

1
2
3
1111111111

12345678910

内存中的存储类型


1.12 define 定义常量和宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* define 是一个预定义指令
*/
//define定义标识符常量
#define 一坤年 2.5
//define定义宏
#define Multiply(x, y) ((x)*(y)) //注意格式, 这几层括号是必要的
int main() {
double sum = Multiply(一坤年, 一坤年);
printf("%lf\n", sum);
printf("%lf\n", 10 * sum);

return 0;
}

输出:

1
2
6.250000
62.500000

1.13 指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
int a = 3;
int* indicator_a = &a; // * 说明indicator_a是指针变量, int说明indicator_a所指向的对象的类型

char b = 'a';
char* indicator_b = &b;

*indicator_a = 10; // * 是解引用操作, *indicator_a 就是通过 indicator_a 里的地址找到 a, 对 a 进行操作
printf("%d\n", a);

int num = 10;
int* p;//p为一个整形指针变量
p = &num; //指针变量定义的是一个地址
*p = num; //解引用

return 0;
}
  • 指针变量的大小

    指针需要多达空间, 取决于地址存储需要多达空间

    32位机 32 bit – 4 byte

    64位机 64 bit – 8 byte

1
2
3
4
5
6
7
8
int main() {
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(short*));
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(double*));

return 0;
}

输出:

1
2
3
4
8
8
8
8

1.14 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Garden {
//结构体成员变量
char name[20]; //注意分号
int num;
double money;
}; //注意分号

int main() {

//结构体的创建于初始化
struct Garden flower = { "violet", 100, 33.3};
//结构体变量.成员变量
printf("一般写法: %s, %d, %lf\n", flower.name, flower.num, flower.money);

struct Garden* pf = &flower;
//结构体指针.成员变量
printf("指针.写法: %s, %d, %lf\n", (*pf).name, (*pf).num, (*pf).money);

//结构体指针->成员变量 (常)
printf("指针->写法: %s, %d, %lf\n", pf->name, pf->num, pf->money);

return 0;
}

二、分支和循环

分支语句

  • if..else
  • switch

循环语句

  • while
  • for
  • do...while

转向语句

  • goto 语句
  • break 语句
  • continue 语句
  • return 语句

2.1 分支语句 (选择结构)

  • if...else
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
//判断一个数是否为奇数
int main() {
printf("请输入一个数: ");
int num = 0;
scanf("%d", &num);
if (num % 2 == 0) {
printf("\n这个数不是奇数");
}
else {
printf("\n这个数是奇数");
}

return 0;
}

//输出 1-100 之间的奇数
int main() {
for (int i = 1; i < 101; i++) {
if (i % 2 != 0) {
printf("%d\n", i);
}
}

return 0;
}
  • 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
29
30
31
32
33
int main() {
int day = 0;
scanf("%d", &day);
//switch中加入判断值
switch (day) {
case 1: //case后面只能加 整数常量表达式, 注意冒号
printf("今天是星期1"); //注意分号
break; //不加 break 会全部执行一遍, 一定要加, 这是好习惯!!!
case 2:
printf("今天是星期2");
break;
case 3:
printf("今天是星期3");
break;
case 4:
printf("今天是星期4");
break;
case 5:
printf("今天是星期5");
break;
case 6:
printf("今天是星期6");
break;
case 7:
printf("今天是星期日");
break;
default: //都不匹配直接进行 default, 必须加, 这是好习惯!!!
printf("没有这个星期哦");
break;
}

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
//输入1-5,输出的是“weekday”;
//输入6 - 7,输出“weekend”
int main() {
int day = 0;
scanf("%d", &day);
//switch中加入判断值
switch (day) {
case 1: //case后面只能加 整数常量表达式, 注意冒号
case 2:
case 3:
case 4:
case 5:
printf("今天是weekday");
break;
case 6:
case 7:
printf("今天是weekend");
break;
default: //都不匹配直接进行 default, 必须加, 这是好习惯!!!
printf("没有这个星期哦");
break;
}

return 0;
}

2.2 循环语句

  • while

getchar 详见CSDN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* getchar() - 字符输入函数,没有参数,从输入缓冲区里面读取一个字符 - 「 一次只能读取一个字符 」
* getchar() 返回值类型为 int, 需引用头文件 <stdio.h>
*
* EOF(-1) - End Of File 文件结束标志 - 键盘上用 ctrl + z 实现
*/
int main() {
int input = 0;
while ((input = getchar()) != EOF) {
putchar(input);
}

return 0;
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
12
12
fsdf
fsdf
fadsfa dfsdfsd
fadsfa dfsdfsd
fdsfa dfsdfaf\n
fdsfa dfsdfaf\n
dfdsaafdsf xbnnc gnbnn\n\n\n\n
dfdsaafdsf xbnnc gnbnn\n\n\n\n
^Z //键入 Ctrl + z 停止

1
2
3
4
5
6
7
8
9
10
11
12
//只打印数字字符,跳过其他字符
int main() {
int num = '\0';
while ((num = getchar()) != EOF) {
if (num < '0' || num > '9') {
continue;
}
putchar(num);
}

return 0;
}

输出:

1
2
3
4
5
6
7
8
f
fsdf
45646
4564643
43gfdg
g
23
23
  • for
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
/*
* for (初始化部分; 条件判断部分; 调整部分)
* 如果省略条件判断部分则会产生死循环
* 可以使用多个变量来控制循环
*
* 建议:
* 1. 不可在for 循环体内修改循环变量,防止 for 循环失去控制。
* 2. 建议for语句的循环控制变量的取值采用“前闭后开区间”写法。
*/

int main() {
int i = 0;
int j = 0;
int count = 0;

//省略初始化部分
for (; i < 10; i++) {
count++;
}
printf("%d\n", count); //count = 10

//死循环
for (;;) {
count++;
}
printf("%d\n", count);

//for 循环嵌套
for (i = 0, count = 0; i < 10; i++) { //前开后闭式写法
for (j = 0; j < 10; j++) {
count++;
}
}
printf("%d\n", count); //count = 100

//两个变量控制循环
for (i = 0, j = 0, count = 0; i < 2 && j < 5; i++, j++) {
count++;
}
printf("%d\n", count); //count = 2

//k = 0, 判断为 FALSE, 循环 0 次
int i = 0;
int k = 0;
for(i = 0,k = 0; k = 0; i++, k++) {
k++;
}

return 0;
}
  • do...while 至少执行一次, 不经常使用
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
```



****



## 2.3 练习



### 计算 n 的阶乘



```c
//计算n的阶乘
int main() {
int n = 0;
scanf("%d", &n);
int sum = 1;
while (n > 0) {
sum = sum * n;
n--;
}
printf("%d", sum);

return 0;
}

计算 1!+2!+3!+……+10!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//计算 1!+2!+3!+……+10!
int main() {
int n = 10;
int sum_p = 0;
while (n > 0) {
int sum_m = 1;
for (int i = n; i > 0; i--) {
sum_m = sum_m * i;
}
sum_p = sum_m + sum_p;
n--;
}
printf("%d", sum_p);

return 0;
}

在一个有序数组中查找具体的某个数字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
26
27
28
29
30
31
//在一个有序数组中查找具体的某个数字n (二分查找)
int main() {
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int left = 0;
int right = sizeof(arr)/sizeof(arr[0]) - 1; //数组元素个数
int key = 9;
int mid = 0;

//二分查找关键代码段
while (left <= right) {
mid = (left + right) / 2;
if (arr[mid] > key) {
right = mid - 1;
}
else if (arr[mid] < key) {
left = mid + 1;
}
else {
break;
}
}

if (left <= right) {
printf("yes");
}
else {
printf("no");
}

return 0;
}

编写代码,演示多个字符从两端移动,向中间汇聚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//编写代码,演示多个字符从两端移动,向中间汇聚
int main() {
char arr[] = "This is a string , I will full it with '$'.";
int left = 0;
int right = strlen(arr) - 1;

//关键代码段
while (left <= right) {
arr[left] = '$';
arr[right] = '$';
left++;
right--;
printf("%s\n", arr);
}

return 0;
}

编写代码实现,模拟用户登录情景,并且只能登录三次。(只允许输入三次密码,如果密码正确则提示登录成,如果三次均输入错误,则退出程序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//编写代码实现,模拟用户登录情景,并且只能登录三次。(只允许输入三次密码,如果密码正确则提示登录成,如果三次均输入错误,则退出程序)
int main() {
int password = 123456;
int count = 0;
while (1) {
int input = 0;
printf("请输入密码: ");
scanf("%d", &input);
if (input == password) {
printf("\n登陆成功");
break;
}
count++;
if (count == 3) {
printf("\n登陆失败");
break;
}
}

return 0;
}


三、函数

3.1 函数分类

库函数

库函数网站

  • IO函数
  • 字符串操作函数
  • 字符操作函数
  • 内存操作函数
  • 时间/日期函数
  • 数学函数
  • 其他库函数

自定义函数 (形参&实参, 传值&传址)

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
#include <stdio.h>

//传值调用 实现成函数,但是不能完成任务, 因为没有返回值, 没意义
void Swap1(int x, int y) { //函数定义, 形参
//形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
//形参实例化之后其实相当于实参的一份临时拷贝, 改变形参不能改变实参
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}

//传址调用 正确的版本
void Swap2(int *px, int *py) { //函数定义, 形参
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}

int main() {
int num1 = 1;
int num2 = 2;

//传值调用
Swap1(num1, num2); //函数调用, 实参
printf("Swap1::num1 = %d num2 = %d\n", num1, num2);

//传址调用
Swap2(&num1, &num2); //函数调用, 实参
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);

return 0;
}

3.2 函数练习

判断一个数是不是素数

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
//判断一个数是不是素数
//如果对正整数n,如果用2到sqrt(n)之间的所有整数去除,均无法整除,则n为质数。

//判断一个数是不是素数
#include<math.h> //sqrt()函数所在头文件
void PrimeNumber(int n) {
int whether = 0;
if (n > 2) {
for (int i = 2; i < sqrt(n); i++) { //sqrt(n)表示n的算术平方根
if (n % i == 0) {
printf("\n这个数不是素数");
whether = 1;
break;
}
}
}
else {
printf("\n这个数不是素数");
whether = 1;
}
if (whether == 0) {
printf("\n这个数是素数");
}
}
int main() {
int n = 0;
scanf("%d", &n);
PrimeNumber(n);

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
//判断一年是不是闰年
/* 两种情况
* 1. 能被 4 整除不能被 100 整除 (普通闰年)
* 2. 能被 400 整除 (世纪闰年)
*/
//判断一年是不是闰年
void LeapYear(int year) {
if (year % 4 == 0 && year % 100 != 0) {
printf("\n这一年是闰年");
}
else if (year % 400 == 0) {
printf("\n这一年是闰年");
}
else {
printf("\n这一年不是闰年");
}
}

int main() {
int year = 0;
scanf("%d", &year);
LeapYear(year);

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
//实现一个整形有序数组的二分查找
void Search(int* arr, int key, int left, int right) {
while (left <= right) {
int mid = (right + left) / 2;
if (arr[mid] > key) {
right = mid - 1;
}
else if (arr[mid] < key) {
left = mid + 1;
}
else {
break;
}
}
if (left <= right) {
printf("查找成功");
}
else {
printf("查找失败");
}
}

int main() {
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int key = 9;
int left = 0;
int right = sizeof(arr)/sizeof(arr[0]) - 1;
Search(arr, key, left, right);

return 0;
}

写一个函数,每调用一次这个函数,就会将 num 的值增加1

1
2
3
4
5
int Count() {
count++;

return count;
}

3.3 函数的嵌套调用&链式访问

嵌套调用

函数之中调用另一个函数

链式访问

把一个函数的返回值作为另外一个函数的参数


3.4 函数的声明&定义

  • 函数声明

    1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。

    2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用

    3. 函数的声明一般要放在头文件中的, 其他文件引用时用 include 引用

  • 函数定义

    函数的定义是指函数的具体实现,交待函数的功能实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pargma comment(lib, "add.lib") //导入静态库(目前不知道用在哪, 详情请CSDN)
#include "add.h" //引用
//函数定义
int Add(int x, int y) {
return x + y;
}

int main() {
int a = 1;
int b = 2;

//函数声明, 以下两种皆可
int Add(int x, int y); //先声明后使用, 如果函数定义在main()函数后面, 必须声明, 不然用不了
//int Add(int , int );

int c = Add(a, b);
printf("%d", c);

return 0;
}

3.5 函数递归

程序调用自身的编程技巧称为递归( recursion)。

递归做为一种算法在程序设计语言中广泛应用。

一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,

它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,

递归策略

只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

递归的主要思考方式在于:把大事化小

  • 递归的两个必要条件

    存在限制条件,当满足这个限制条件的时候,递归便不再继续。

    每次递归调用之后越来越接近这个限制条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//接受一个整型值(无符号),按照顺序打印它的每一位

//无限递归容易 栈溢出
#include <stdio.h>
void print(int n) {
if(n>9) {
print(n/10);
}
printf("%d ", n%10);
}

int main() {
int num = 1234;
print(num);

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//编写函数不允许创建临时变量,求字符串的长度

#incude <stdio.h>
int Strlen(const char* str) {
if(*str == '\0')
return 0;
else
   return 1+Strlen(str+1);
}

int main() {
char *p = "abcdef";
int len = Strlen(p);
printf("%d\n", len);

return 0;
}
1
2
3
4
5
6
7
8
//求n的阶乘。(不考虑溢出)

int factorial(int n) {
if(n <= 1)
return 1;
else
return n * factorial(n-1);
}
1
2
3
4
5
6
7
8
//求第n个斐波那契数。(不考虑溢出)

int fib(int n) {
if (n <= 2)        
return 1;
else
   return fib(n - 1) + fib(n - 2);
}
  • 递归的改进

    1. 将递归改写成非递归。

    2. 使用 static 对象替代 nonstatic 局部对象。

      在递归函数设计中,可以使用 static 对象替代 nonstatic 局部对象(即栈对象),

      这不仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,

      而且 static 对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。

1
2
3
4
5
6
7
8
9
10
11
//求n的阶乘 (非递归)

int factorial(int n) {
int result = 1;
   while (n > 1) {
    result *= n ;
       n -= 1;
   }

   return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//求第n个斐波那契数 (非递归)

int fib(int n) {
int result;
   int pre_result;
   int next_older_result;
   result = pre_result = 1;
while (n > 2) {
    n -= 1;
      next_older_result = pre_result;
       pre_result = result;
       result = pre_result + next_older_result;
   }

   return result;
}
  1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。

  2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。

  3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。

经典题目

  • 汉诺塔问题

  • 青蛙跳台阶问题


四、数组

4.1 一维数组

  • 一维数组在内存中是连续存放的
  • 随着下标增长, 地址由低到高变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//一维数组
int main() {
int n = 5;
//int arr[n]; 不可以这样定义, 必须为常量, n是变量
int arr[3] = { 1, 2, 3 }; //完全初始化
int arr_three[3] = { 1, 2 }; //不完全初始化
int arr_t[] = { 1, 2, 3 };

char arr1[4] = { 'f', 'o', 'u', 'r' };
char arr2[] = { 'f', 'o', 'u', 'r' }; // 4 个字符 f o u r
char arr3[] = "four"; // 5 个字符 f o u r \0

//sizeof[arr]表示整个数组大小, 单位字节
//sizeof(arr[0])表示数组第一个数大小, 单位字节
int sz = sizeof(arr) / sizeof(arr[0]);

return 0;
}

一维数组的内存存储


4.2 二维数组

  • 二维数组在内存中也是连续存储的
1
2
3
4
5
6
7
8
9
//二维数组 本质是一个矩阵
int main() {
//数组初始化
int arr1[3][4] = { 1,2,3,4 };
int arr2[3][4] = { {1,2},{4,5} };
int arr3[][4] = { {2,3},{4,5} };//二维数组如果有初始化,行可以省略,列不能省略

return 0;
}

二维数组的内存存储


4.3 数组越界

数组的下标是有范围限制的。

数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。

所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。

数组名是数组首元素地址:

  1. sizeof(数组名) - 数组名表示整个数组, 计算的是整个儿数组大小, 单位: 字节
  2. &数组名 - 数组名表示整个数组, 取出的是整个数组的地址

4.4 数组作为函数参数 (冒泡排序)

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
//冒泡排序
void bubble_sort(int arr[], int sz) {//参数接收数组元素个数
//代码同上面函数
int sz = sizeof(arr)/sizeof(arr[0]);//这样对吗?
   int i = 0;
for(i=0; i<sz-1; i++)
  {
       int j = 0;
       for(j=0; j<sz-i-1; j++)
      {
           if(arr[j] > arr[j+1])
          {
               int tmp = arr[j];
               arr[j] = arr[j+1];
               arr[j+1] = tmp;
          }
      }
  }
}
int main() {
   int arr[] = { 3, 1, 7, 5, 8, 9, 0, 2, 4, 6 };
   int sz = sizeof(arr) / sizeof(arr[0]);
   bubble_sort(arr, sz); //是否可以正常排序?
   for (i = 0; i < sz; i++) {
       printf("%d ", arr[i]);
  }
   return 0;
}

4.5 三子棋

  • logic.c 测试游戏的逻辑
  • game.h 关于游戏函数的声明, 符号声明, 包含的头文件
  • game.c 游戏相关函数实现

4.6 扫雷

  • logic.c 测试游戏的逻辑
  • game.h 关于游戏函数的声明, 符号声明, 包含的头文件
  • game.c 游戏相关函数实现

五、操作符

5.1 操作符分类

算术操作符

移位操作符

位操作符

赋值操作符

单目操作符

关系操作符

逻辑操作符

条件操作符

逗号表达式

下标引用、函数调用和结构成员


5.2 算数操作符

1
+    -   *   /   % 
  1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
  2. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
  3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。

5.3 移位操作符

对于移位运算符,不要移动负数位,这个是标准未定义的 num>>-1; //error

内存中存放的是二进制的补码 —–即操作的是补码

  • << 左移操作符

    左边抛弃, 右边补 0

左移操作符

  • >> 右移操作符 (通常算数右移)

    1. 逻辑移位

      左边用0填充,右边丢弃

    2. 算术移位

      左边用原该值的符号位填充,右边丢弃

右移操作符


5.4 位操作符

& //按位与

​ 两个数位值均为 1 , 得数位值为 1 , 其余均为 0

| //按位或

​ 两个数位值均为 0 , 得数位值为 0 , 其余均为 1

^ //按位异或

​ 两个数位值相同为 0 , 相异必须是整数。

注:他们的操作数必须是整数

注:他们的操作数必须是整数。

1
2
3
4
5
6
7
8
9
10
11
12
13
//交换两个整型变量, 不使用第三个变量
int main() {
int a = 1;
int b = 2;
printf("a = %d, b = %d", a, b);
a^ a; //两个相同的数 异或, 值为 0
a = a ^ b;
b = a ^ b; //相当于 a ^ b ^ b, 两个 b 异或值为 0, 剩下 a
a = a ^ b; //相当于 a ^ b ^ a, 两个 a 异或值为 0, 剩下 b
printf("a = %d, b = %d", a, b);

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
//求一个整数存储在内存中的二进制中1的个数
int main() {
int num = -1;
int i = 0;
int count = 0; //计数
while (num) { //num 为 0 时跳出循环, 不用循环32次
count++;
num = num & (num - 1); //每次减一并按位与, 可以找到到底有多少个 1
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}

5.5 赋值操作符

  • 复合赋值符

    +=

    -=

    *=

    /=

    %=

    >>=

    <<=

    &=

    |=

    ^=


5.6 单目操作符

操作符 作用
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
前置、后置– (写在前面就先 - )
++ 前置、后置++ (写在前面就先 + )
* 间接访问操作符(解引用操作符) — 用在指针
(类型) 强制类型转换

5.7 关系操作符

>

>=

<

<=

!= 用于测试“不相等”

== 用于测试“相等”


5.8 逻辑操作符

&& 逻辑与

|| 逻辑或

1
2
3
4
5
6
7
8
9
//输出: 1 2 3 4
int main() {
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
//i = a++||++b||d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);

return 0;
}

5.9 条件操作符

exp1 ? exp2 : exp3


5.10 逗号表达式

exp1, exp2, exp3, ...expN

逗号表达式,就是用逗号隔开的多个表达式。

逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。


5.11 下标引用、函数调用和结构成员

[] 下标引用操作符

() 函数调用操作符

. 结构体.成员名

-> 结构体指针->成员名


5.12 表达式求值

隐式类型转换

C的整型算术运算总是至少以缺省整型类型的精度来进行的。

为了获得这个精度,表达式中的 字符和短整型 操作数在使用之前被转换为普通整型,这种转换称为整型提升

说白了就是因为 char ( 1 字节)short (2 字节) 太小了, 达不到运算时所需要的 4 字节, 才会隐式转成整型

向精度更高的转换

整型提升的意义

表达式的整型运算要在 CPU 的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度

一般就是int的字节长度,同时也是CPU的通用寄存器的长度。

因此,即使两个 char 类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长

度。

通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令

中可能有这种字节相加指令)。所以,表达式中各种长度可能小于 int 长度的整型值,都必须先转

换为 intunsigned int,然后才能送入CPU去执行运算。

1
2
3
char a,b,c;
...
a = b + c; // b 和 c 的值被提升为普通整型,然后再执行加法运算, 加法运算完成之后,结果将被截断,然后再存储于 a 中

如何进行整体提升?

整形提升是按照变量的数据类型的符号位来提升的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,!!! 高位补充符号位,即为1 !!!
提升之后的结果是:
11111111111111111111111111111111

//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,!!! 高位补充符号位,即为0 !!!
提升之后的结果是:
00000000000000000000000000000001

//无符号整形提升,高位补0

例1

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
int main() {
char a = 3;
//00000000000000000000000000000011 - 有符号, 为 0, 高位补 0
//00000011 - a 为 char 型时的二进制位 (8 bit)
char b = 127;
//00000000000000000000000001111111 - 有符号, 为 0, 高位补 0
//01111111 - b 为 char 型的二进制位 (8 bit)

char c = a + b;
//00000000000000000000000000000011 - a
//00000000000000000000000001111111 - b
//00000000000000000000000010000010 - a + b

//10000010 - c 为 char 型的二进制位 (8 bit) - 此处为截断: char中只能放最低的 8 个 bit 位, 其他位置放不下了, 丢弃
//11111111111111111111111110000010 - 补码 - 有符号, 为 1, 高位补 1
//11111111111111111111111110000001 - 反码
//10000000000000000000000001111110 - 原码
//-126
//a 和 b 都是 char 型的, 都没有达到一个 int 的大小
//这里就回发生整形提升

printf("%d\n", c); // -126

return 0;
}

例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* a,b 要进行整形提升,但是 c 不需要整形提升
* a,b 整形提升之后,变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果是假
* 所程序输出的结果是: c
*/
int main() {
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");

return 0;
}

例3

1
2
3
4
5
6
7
8
9
10
11
12
/*
* c 只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是 4 个字节
* 表达式 -c 也会发生整形提升,所以 sizeof(-c) 是 4 个字节,但是 sizeof(c) ,就是 1 个字节.
*/
int main() {
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));

return 0;
}

算数转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类

型,否则操作就无法进行。下面的层次体系称为寻常算术转换

long double

double

float

unsigned long int

long int

unsigned int

int

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算

从下往上转, 下面精度低, 上面精度高, 从精度小的转换为精度大的

操作符的属性

  1. 操作符的优先级

  2. 操作符的结合性

  3. 是否控制求值顺序。

操作符优先级1

操作符优先级2


六、指针

  • 指针是用来存放地址的,地址是唯一标示一块地址空间的。

  • 指针的大小在 32位平台是4个字节,在64位平台是8个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
int a = 3;
int* indicator_a = &a; // * 说明indicator_a是指针变量, int说明indicator_a所指向的对象的类型

char b = 'a';
char* indicator_b = &b;

*indicator_a = 10; // * 是解引用操作, *indicator_a 就是通过 indicator_a 里的地址找到 a, 对 a 进行操作
printf("%d\n", a);

int num = 10;
int* p;//p为一个整形指针变量
p = &num; //指针变量定义的是一个地址
*p = num; //解引用

return 0;
}

6.1 指针类型

char* 类型的指针是为了存放 char 类型变量的地址。

short* 类型的指针是为了存放 short 类型变量的地址。

int* 类型的指针是为了存放 int 类型变量的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//指针 + - 整数
//指针的类型决定了指针向前或者向后走一步有多大(距离) (步长)
int main() {
int n = 10;
char* pc = (char*) &n;
int* pi = &n;

printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc + 1);
printf("%p\n", pi);
printf("%p\n", pi + 1);

return 0;
}

输出:

1
2
3
4
5
0000005C4BCFF8B4
0000005C4BCFF8B4
0000005C4BCFF8B5
0000005C4BCFF8B4
0000005C4BCFF8B8
1
2
3
4
5
6
7
8
9
10
11
//指针的解引用 (此段代码输出为空, 主要是调试证明)
//指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
//比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
int main() {
int n = 0x11223344;
char* pc = (char*)&n;
int* pi = &n;
*pc = 0; //重点在调试的过程中观察内存的变化。
*pi = 0; //重点在调试的过程中观察内存的变化。
return 0;
}

6.2 字符指针

1
2
3
4
5
6
7
8
9
10
11
12
13
//字符指针
int main() {
//法一
char ch = 'w';
char* pc = &ch;
*pc = 'w';

//法二
const char* pstr = "hello bit."; //本质是把字符串"hello bit."的首字母放入指针变量 pstr 中
printf("%s\n", pstr);

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
/*输出: 
* str1 and str2 are not same
* str3 and str4 are same
*/
int main() {
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");

if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");

return 0;
}
/*
* str3 和 str4 指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,
* 当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。
* 但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。
*/

6.3 野指针

指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//未初始化的野指针
int main() {
int* p;//局部变量指针未初始化,默认为随机值
*p = 20;

return 0;
}

//越界访问的野指针
int main(){
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 11; i++) {
//当指针指向的范围超出数组arr的范围时,p就是野指针
//C语言不会检查数据是否越界
*(p++) = i;
}

return 0;
}

//指针指向的空间释放
//空间释放后, 指针指向位置, 编程野指针 (动态内存开辟后补代码)

规避野指针:

  1. 指针初始化

  2. 小心指针越界

  3. 指针指向空间释放即使置NULL

  4. 避免返回局部变量的地址

  5. 指针使用之前检查有效性


6.4 指针运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//指针关系运算
for (vp = &values[N_VALUES]; vp > &values[0];) {
*--vp = 0;
}
//代码改进
for (vp = &values[N_VALUES - 1]; vp >= &values[0];vp--) {
*vp = 0;
}
/*
* 标准规定:
* 允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,
* 但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
* 定义数组 arr[10] = { 0 }, 可以访问与 arr[10] 比较但是不能与 arr[-1] 比较
*/

6.5 指针与数组

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
//数组名的意义
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
printf("%p == %p", arr, &arr[0]); //数组名表示的是数组首元素的地址

int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int* p = arr;

return 0;
}

//指针与数组关系
// p+i 其实计算的是数组 arr 下标为 i 的地址
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
int* p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++) {
printf("&arr[%d] = %p   <====> p+%d = %p\n", i, &arr[i], i, p + i);
}

return 0;
}

//通过指针访问数组
int main() {
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int* p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++) {
printf("%d ", *(p + i));
}

return 0;
}

6.6 指针数组

1
2
3
//指针数组 -- 存放指针的数组
int* arr3[5];
char* arr4[5];
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
//指针数组
//数组中存放的是指针 (地址)
int main() {
//指针指向变量
int a = 10;
int b = 20;
int c = 30;
int* arr[3] = { &a, &b, &c };
for (int i = 0; i < 3; i++) {
printf("%d ", *(arr[i]));
}

//指针指向数组
int a1[5] = { 1, 2, 3, 4, 5 };
int b1[5] = {};
int c1[5] = {};
int* arr1[3] = { a1, b1, c1 };
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 5; j++) {
printf("%d ", *(arr1[i] + j));
printf("%d ", arr[i][j]); //这种写法用于模拟二维数组
}
}

return 0;
}

6.7 二级指针

1
2
3
4
5
6
7
8
//二级指针
//指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
int b = 20;
int* ppa = &b;//等价于 pa = &b;
int* *ppa = &ppa;
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

6.8 数组指针

1
2
3
4
5
6
7
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int(*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
//但是一般很少这样写代码, 太复杂了没必要

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
//数组指针的使用
void print_arr1(int arr[3][5], int row, int col) {
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int(*arr)[5], int row, int col) {
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main() {
int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址

//也可以数组指针来接收
print_arr2(arr, 3, 5);

return 0;
}
1
2
3
4
5
int arr[5]; //整形数组
int* parr1[10]; //指针数组, 整型数组中存放 10 个指针
int (*parr2)[10]; //数组指针, 该指针指向一个数组, 这个数组 10 个元素, 每个都是整型
//存储 数组指针 的 数组, 可存放 10 个 数组指针, 每个 数组指针 指向一个 数组, 每个 数组 5 个元素, 都是 整型
int (*parr3[10])[5];

6.9 数组参数 & 指针参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//一维数组传参
void test(int arr[]) //ok
{}
void test(int arr[10]) //ok
{}
void test(int* arr) //ok
{}
void test2(int* arr[20]) //ok
{}
void test2(int** arr) //ok
{}
int main() {
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//二维数组传参
void test(int arr[3][5]) //ok
{}
void test(int arr[][]) //不ok
{}
void test(int arr[][5])//ok?
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int* arr) //不ok, 实参是一个数组地址, 形参不能用一个普通指针接收
{}
void test(int* arr[5]) //不ok, 既不是指针, 又不是二维数组
{}
void test(int(*arr)[5]) // ok, 数组指针, 传的值为二维数组第一行的一维数组
{}
void test(int** arr) //不ok, 二级指针与实参不匹配
{}
int main() {
int arr[3][5] = { 0 };
test(arr);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//一级指针传参
void print(int* p, int sz) { //形参 * p 的意思是传过来一个指针变量
int i = 0;
for (i = 0; i < sz; i++) {
printf("%d\n", *(p + i));
}
}
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//二级指针传参
void test(int** ptr) { //形参 ** ptr 的意思是传过来一个二级指针变量
printf("num = %d\n", **ptr);
}
int main() {
int n = 10;
int* p = &n;
int** pp = &p;
test(pp);
test(&p);

return 0;
}

6.10 函数 & 指针

函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//函数指针 - 存放函数地址的指针
//&函数名 - 取到的就是函数的地址
//函数名 与 &函数名一个意思, 函数名 == &函数名
void test(char*) {

}
int Add(int , int) {

}
int main() {
//指针类型跟随函数返回值类型
int (*pf)(int, int) = &Add; //Add == pf
int (*pf)(int, int) = &Add; //两种都可以
void (*pc)(char*) = &test;

//函数指针调用
int ret = (*pf)(3, 5);

return 0;
}

函数指针数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//函数指针数组
//本质是数组, 存储的事一堆函数指针
int test(int , int) {

}
int Add(int , int) {

}
int main() {
//函数指针数组 存放的 函数指针 的 形参 要相同, 不然会报错
int (*pf[10])(int, int) = { Add, test };

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
32
33
34
35
36
37
38
//函数指针数组的用途:转移表
//应用: 计算器
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int mul(int a, int b) {
return a * b;
}
int div(int a, int b) {
return a / b;
}
int main() {
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
while (input) {
printf("*************************\n");
printf(" 1:add           2:sub \n");
printf(" 3:mul           4:div \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1)) {
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else {
printf("输入有误\n");
}
printf("ret = %d\n", ret);
}
return 0;
}

指向函数指针数组的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//指向函数指针数组的指针 (了解)
void test(const char* str) {
printf("%s\n", str);
}
int main() {
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;

return 0;
}

回调函数

回调函数就是一个通过函数指针调用的函数。

如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

回调函数不是由该函数的实现方直接调用,

而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//qosrt快排
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void* p1, const void* p2) {
return (*(int*)p1 - *(int*)p2);
}
int main() {
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;

qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
printf("%d ", arr[i]);
}
printf("\n");

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
32
33
34
35
//回调函数 + 冒泡 模拟快排
int int_cmp(const void* p1, const void* p2) {
return (*(int*)p1 - *(int*)p2);
}
void _swap(void* p1, void* p2, int size) {
int i = 0;
for (i = 0; i < size; i++) {
char tmp = *((char*)p1 + i);
*((char*)p1 + i) = *((char*)p2 + i);
*((char*)p2 + i) = tmp;
}
}
void bubble(void* base, int count, int size, int(*cmp)(const void*, const void*)) {
int i = 0;
int j = 0;
for (i = 0; i < count - 1; i++) {
for (j = 0; j < count - i - 1; j++) {
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0) {
_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
int main() {
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
//char *arr[] = {"aaaa","dddd","cccc","bbbb"};
int i = 0;
bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
printf("%d ", arr[i]);
}
printf("\n");

return 0;
}

6.11 一些笔试题

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/*
* 数组名的意义:
* 1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
* 2. & 数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
* 3. 除此之外所有的数组名都表示首元素的地址。
*/
int main() {
//一维数组
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a)); //16
printf("%d\n", sizeof(a + 0)); //4或8
printf("%d\n", sizeof(*a)); //4
printf("%d\n", sizeof(a + 1)); //4或8
printf("%d\n", sizeof(a[1])); //4

printf("%d\n", sizeof(&a)); //4或8
printf("%d\n", sizeof(*&a)); //16
printf("%d\n", sizeof(&a + 1)); //4或8
printf("%d\n", sizeof(&a[0])); //4或8
printf("%d\n", sizeof(&a[0] + 1)); //4或8

//字符数组
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr)); //6
printf("%d\n", sizeof(arr + 0)); //4或8
printf("%d\n", sizeof(*arr)); //1
printf("%d\n", sizeof(arr[1])); //1
printf("%d\n", sizeof(&arr)); //4或8
printf("%d\n", sizeof(&arr + 1)); //4或8
printf("%d\n", sizeof(&arr[0] + 1)); //4或8

printf("%d\n", strlen(arr)); //随机值
printf("%d\n", strlen(arr + 0)); //随机值
printf("%d\n", strlen(*arr)); //err
printf("%d\n", strlen(arr[1])); //err
printf("%d\n", strlen(&arr)); //随机值
printf("%d\n", strlen(&arr + 1)); //随机值
printf("%d\n", strlen(&arr[0] + 1)); // 随机值

char arr[] = "abcdef";
printf("%d\n", sizeof(arr)); //7
printf("%d\n", sizeof(arr + 0)); //4或8
printf("%d\n", sizeof(*arr)); //1
printf("%d\n", sizeof(arr[1])); //1
printf("%d\n", sizeof(&arr)); //4或8
printf("%d\n", sizeof(&arr + 1)); //4或8
printf("%d\n", sizeof(&arr[0] + 1)); //4或8

printf("%d\n", strlen(arr)); //6
printf("%d\n", strlen(arr + 0)); //6
printf("%d\n", strlen(*arr)); //err
printf("%d\n", strlen(arr[1])); //err
printf("%d\n", strlen(&arr)); //6
printf("%d\n", strlen(&arr + 1)); //随机值
printf("%d\n", strlen(&arr[0] + 1)); //5

char* p = "abcdef";
printf("%d\n", sizeof(p)); //4或8
printf("%d\n", sizeof(p + 1)); //4或8
printf("%d\n", sizeof(*p)); //1
printf("%d\n", sizeof(p[0])); //1
printf("%d\n", sizeof(&p)); //4或8
printf("%d\n", sizeof(&p + 1)); //4或8
printf("%d\n", sizeof(&p[0] + 1)); //4或8

printf("%d\n", strlen(p)); //6
printf("%d\n", strlen(p + 1)); //5
printf("%d\n", strlen(*p)); //err
printf("%d\n", strlen(p[0])); //err
printf("%d\n", strlen(&p)); //随机值
printf("%d\n", strlen(&p + 1)); //随机值
printf("%d\n", strlen(&p[0] + 1)); //5

//二维数组
int a[3][4] = { 0 };
printf("%d\n", sizeof(a)); //48
printf("%d\n", sizeof(a[0][0])); //4
printf("%d\n", sizeof(a[0])); //16
printf("%d\n", sizeof(a[0] + 1)); //
printf("%d\n", sizeof(*(a[0] + 1))); //4
printf("%d\n", sizeof(a + 1)); //4
printf("%d\n", sizeof(*(a + 1))); //16
printf("%d\n", sizeof(&a[0] + 1)); //4
printf("%d\n", sizeof(*(&a[0] + 1))); //16
printf("%d\n", sizeof(*a)); //16
printf("%d\n", sizeof(a[3])); //16
}
1
2
3
4
5
6
7
8
//输出: 2,5
int main() {
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1);
printf("%d,%d", *(a + 1), *(ptr - 1));

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//结构体的大小是20个字节
struct Test {
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main() {
printf("%p\n", p + 0x1); //0000000000000020
printf("%p\n", (unsigned long)p + 0x1); //0000000000000001
printf("%p\n", (unsigned int*)p + 0x1); //0000000000000004

return 0;
}
1
2
3
4
5
6
7
8
9
//输出: 4,2000000
int main() {
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%x,%x", ptr1[-1], *ptr2);

return 0;
}
1
2
3
4
5
6
7
8
9
//输出: 1
int main() {
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int* p;
p = a[0];
printf("%d", p[0]);

return 0;
}
1
2
3
4
5
6
7
8
9
//输出: 00000000,0
int main() {
int a[5][5];
int(*p)[5];
p = a;
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);

return 0;
}
1
2
3
4
5
6
7
8
9
//输出: 10,5
int main() {
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* ptr1 = (int*)(&aa + 1);
int* ptr2 = (int*)(*(aa + 1));
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));

return 0;
}
1
2
3
4
5
6
7
8
9
//感觉现阶段无需学这么深, 考研够用即可
int main() {
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
//感觉现阶段无需学这么深, 考研够用即可
int main() {
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char**cp[] = {c+3,c+2,c+1,c};
char***cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *--*++cpp+3);
printf("%s\n", *cpp[-2]+3);
printf("%s\n", cpp[-1][-1]+1);

return 0;
}

七、结构体

7.1 结构体声明

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 结构体声明
* struct tag {
* member-list; // 局部变量
* }variable-list; //全局变量
*/
struct Student {
//局部变量
char name[100]; //注意分号
int age;
double average;
}student_number; //全局变量, 注意分号

struct Book {
struct Student borrow; //结构体嵌套, 结构体内的类型可以为其他结构体
int year;
} book_name;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//结构体声明
struct Point {
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2

//初始化:定义变量的同时赋初值
struct Point p3 = { x, y };

//结构体初始化
struct Stu { //类型声明
char name[15];//名字
int age; //年龄
};
struct Stu s = { "zhangsan", 20 };//初始化

//结构体嵌套初始化
struct Node {
int data;
struct Point p;
struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化
struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化

7.2 结构体成员访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//结构体成员访问
struct Stu {
char name[20];
int age;
};

void print(struct Stu* ps) {
printf("name = %s   age = %d\n", (*ps).name, (*ps).age);
//使用结构体指针访问指向对象的成员
printf("name = %s   age = %d\n", ps->name, ps->age); //推荐
}
int main() {
struct Stu s = { "zhangsan", 20 };
print(&s); //结构体地址传参, 传址
return 0;
}

7.3 结构体传参

  • 传值 or 传址 ?

首选传址

函数传参的时候,参数是需要压栈的。

如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降

说白了就是,

如果传值的话, 调用函数的时候就要新建一个跟结构体大小相等的变量, 开辟一个跟结构体大小相等的空间, 增大没必要的性能损耗

而且形参的改变无法改变实参, 有些功能函数传值没意义, 也对原结构体无法进行修改

如果传址就好办了, 直接对地址进行操作, 无需开辟另一个空间, 而且也可以直接对实参进行操作, 一句多得

!!!!!不要恐惧指针!!!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//结构体传参
struct S {
int data[1000];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参, 传值 (性能损耗)
void print1(struct S s) {
printf("%d\n", s.num);
}
//结构体地址传参, 传址 (推荐)
void print2(struct S* ps) {
printf("%d\n", ps->num);
}
int main() {
print1(s); //传结构体值
print2(&s); //传结构体地址
return 0;
}

八、数据的存储

8.1 数据类型

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
//整形家族
char
unsigned char
signed char
short
unsigned short[int]
signed short[int]
int
unsigned int
signed int
long
unsigned long[int]
signed long[int]

//浮点数家族
float
double

//构造类型
数组类型
结构体类型 struct
枚举类型 enum
联合类型 union

//指针类型
int* pi;
char* pc;
float* pf;
void* pv;

//空类型
/*
* void 表示空类型(无类型)
* 通常应用于函数的返回类型、函数的参数、指针类型
*/

8.2 整型在内存中的存储

计算机中的整数有三种表示方法,即原码、反码和补码。

三种表示方法均有符号位数值位两部分,符号位都是用0表示“正”,用1表示“负”

1
2
3
4
5
6
7
8
9
10
/*
* 原码
* 直接将二进制按照正负数的形式翻译成二进制就可以。
*
* 反码
* 将原码的符号位不变,其他位依次按位取反就可以得到了
*
* 补码
* 反码+1就得到补码
*/

正数的原、反、补码都相同。

对于整形来说:数据存放内存中其实存放的是补码。

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;

同时,加法和减法也可以统一处理(CPU****只有加法器)此外,补码与原码相互转换,其运算过程

是相同的,不需要额外的硬件电路。

大端小端

大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;

小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。

在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8 bit。但是在C语言中除了 8 bit 的 char 之外,还有 16 bit 的 short 型,32 bit 的 long 型(要看具体的编译器),另外,对于位数大于 8 位的处理器,例如 16 位或者 32 位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。

例如:一个 16 bit 的 short 型 x ,在内存中的地址为 0x0010x 的值为 0x1122 ,那么 0x11 为高字节, 0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的 ARMDSP 都为小端模式。有些 ARM 处理器还可以由硬件来选择是大端模式还是小端模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序
int check_sys() {
int i = 1;
return (*(char*)&i);
}
int main() {
int ret = check_sys();
if (ret == 1) {
printf("小端\n");
}
else {
printf("大端\n");
}
return 0;
}
int check_sys() {
union {
int i;
char c;
}un;
un.i = 1;
return un.c;
}
1
2
3
4
5
6
7
8
9
//输出: a = -1, b = -1, c = 255
int main() {
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("a = %d, b = %d, c = %d", a, b, c);

return 0;
}
1
2
3
4
5
6
7
//输出: 4294967168
int main() {
char a = -128;
printf("%u\n", a);

return 0;
}
1
2
3
4
5
6
7
//输出: 4294967168
int main() {
char a = 128;
printf("%u\n", a);

return 0;
}
1
2
3
4
5
6
7
8
9
//输出: -10
int main() {
int i = -20;
unsigned int j = 10;
printf("%d\n", i + j);
//按照补码的形式进行运算,最后格式化成为有符号整数

return 0;
}
1
2
3
4
5
6
7
8
int main() {
unsigned int i;
for (i = 9; i >= 0; i--) {
printf("%u\n", i);
}

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
4294933570
4294933569
4294933568
4294933567
4294933566
4294933565
4294933564
4294933563
4294933562
4294933561
4294933560
4294933559
4294933558
4294933557
4294933556
4294933555
4294933554
4294933553
4294933552
4294933551
4294933550
4294933549
4294933548
4294933547
4294933546
4294933545
4294933544
4294933543
4294933542
4294933541
4294933540
4294933539
4294933538
4294933537
4294933536
4294933535
4294933534
4294933533
4294933532
4294933531
4294933530
4294933529
4294933528
4294933527
4294933526
4294933525
4294933524
4294933523
4294933522
4294933521
4294933520
4294933519
4294933518
4294933517
4294933516
4294933515
4294933514
4294933513
4294933512
4294933511
4294933510
4294933509
4294933508
4294933507
4294933506
4294933505
4294933504
4294933503
4294933502
4294933501
4294933500
4294933499
4294933498
4294933497
4294933496
4294933495
4294933494
4294933493
4294933492
4294933491
4294933490
4294933489
4294933488
4294933487
4294933486
4294933485
4294933484
4294933483
4294933482
4294933481
4294933480
4294933479
4294933478
4294933477
4294933476
4294933475
4294933474
4294933473
4294933472
4294933471
4294933470
4294933469
4294933468
4294933467
4294933466
4294933465
4294933464
4294933463
4294933462
4294933461
4294933460
4294933459
4294933458
4294933457
4294933456
4294933455
4294933454
4294933453
4294933452
4294933451
4294933450
4294933449
4294933448
4294933447
4294933446
4294933445
4294933444
4294933443
4294933442
4294933441
4294933440
4294933439
4294933438
4294933437
4294933436
4294933435
4294933434
4294933433
4294933432
4294933431
4294933430
4294933429
4294933428
4294933427
4294933426
4294933425
4294933424
4294933423
4294933422
4294933421
4294933420
...
1
2
3
4
5
6
7
8
9
10
11
//输出: 255
int main() {
char a[1000];
int i;
for (i = 0; i < 1000; i++) {
a[i] = -1 - i;
}
printf("%d", strlen(a));

return 0;
}
1
2
3
4
5
6
7
8
9
//输出: 无限循环 hello world
unsigned char i = 0;
int main() {
for (i = 0;i <= 255;i++) {
printf("hello world\n");
}

return 0;
}

8.3 浮点型在内存中的存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//输出:
/* n的值为:9
* *pFloat的值为:0.000000
* num的值为:1091567616
* *pFloat的值为:9.000000
*/
int main() {
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);

return 0;
}

任意一个二进制浮点数 V 可以表示成下面的形式:

  • (-1)^S * M * 2^E

  • (-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。

  • M表示有效数字,大于等于1,小于2。

  • 2^E表示指数位。

十进制的 5.0,写成二进制是 101.0 ,相当于 1.01×2^2

那么,按照上面 V 的格式,可以得出 s=0,M=1.01,E=2

十进制的 -5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么 s=1,M=1.01,E=2

对于 32 位的浮点数,最高的 1 位是符号位 s,接着的 8 位是指数 E,剩下的 23 位为有效数字 M。

float在内存中的存储

对于 64 位的浮点数,最高的 1 位是符号位 S,接着的 11 位是指数 E,剩下的 52 位为有效数字 M。

double在内存中存储

在计算机内部保存 M 时,默认这个数的第一位总是 1,因此可以被舍去,只保存后面的xxxxxx部分。

比如保存 1.01 的时候,只保存 01,等到读取的时候,再把第一位的 1 加上去。这样做的目的,是节省 1 位有效数字。

以 32 位浮点数为例,留给 M 只有 23 位,将第一位的 1 舍去以后,等于可以保存 24 位有效数字。

至于指数 E,情况就比较复杂。

首先,E 为一个无符号整数(unsigned int)

这意味着,如果 E 为 8 位,它的取值范围为 0~255;如果 E 为 11 位,它的取值范围为 0~2047

但是,我们知道,科学计数法中的E是可以出现负数的,

所以 IEEE 754 规定,存入内存时 E 的真实值必须再加上一个中间数,

对于 8 位的 E,这个中间数是 127;对于 11 位的 E,这个中间数是 1023。

比如,2^10 的 E 是 10,所以保存成 32 位浮点数时,必须保存成 10+127=137,即 10001001

然后,指数E从内存中取出还可以再分成三种情况:

  • E 不全为 0 或不全为 1

这时,浮点数就采用下面的规则表示,

即指数 E 的计算值减去 127(或1023),得到真实值,再将有效数字 M 前加上第一位的 1。

比如:

0.5(1/2)的二进制形式为 0.1,由于规定正数部分必须为 1,即将小数点右移 1 位,则为 1.0*2^(-1),其阶码为 -1+127=126

表示为 01111110,而尾数 1.0 去掉整数部分为 0,补齐 0 到 23 位 00000000000000000000000

则其二进制表示形式为:

0 01111110 00000000000000000000000

  • E 全为 0

这时,浮点数的指数E等于 1-127(或者1-1023)即为真实值,

有效数字 M 不再加上第一位的1,而是还原为 0.xxxxxx 的小数。这样做是为了表示 ±0 ,以及接近于 0 的很小的数字。

  • E全为 1

这时,如果有效数字 M 全为 0,表示 ±无穷大(正负取决于符号位 s);

下面,让我们回到一开始的问题:为什么 0x00000009 还原成浮点数,就成了 0.000000

首先,将 0x00000009 拆分,得到第一位符号位 s=0,后面 8 位的指数 E=00000000 ,

最后 23 位的有效数字 M=000 0000 0000 0000 0000 1001

9 -> 0000 0000 0000 0000 0000 0000 0000 1001

由于指数 E 全为 0,所以符合上一节的第二种情况。因此,浮点数 V 就写成:

V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146)

显然,V 是一个很小的接近于 0 的正数,所以用十进制小数表示就是 0.000000

再看例题的第二部分。

请问浮点数 9.0,如何用二进制表示?还原成十进制又是多少?

首先,浮点数 9.0 等于二进制的 1001.0,即 1.001×2^3

9.0 -> 1001.0 ->(-1)^01.0012^3 -> s=0, M=1.001,E=3+127=130

那么,第一位的符号位 s=0,有效数字 M 等于 001 后面再加 20 个 0,凑满 23 位,指数 E 等于 3+127=130,即 10000010

所以,写成二进制形式,应该是 s+E+M,即

0 10000010 001 0000 0000 0000 0000 0000

这个 32 位的二进制数,还原成十进制,正是 1091567616