查漏补缺|C 语言

本文最后更新于:4 years ago

C 语言的知识点不多,但是比较杂。本文系统地对 C 语言进行补充学习。

001.C语言的简洁

32个关键字、9中控制语句、34个运算符。

shortintfloatdoublecharunsigned
longenumvoidautoforwhile
doifelseswitchcasebreak
continuegotoreturnsizeofconsttypedef
staticstructdefaultunionregistersigned
externvolatile
If-elseforwhile
do-whilecontinuebreak
switchgotoReturn

002.编译过程

1
2
3
4
gcc hello.c     # 默认生成名为 a.out 的可执行文件

gcc hello.c -o hello # 生成名为 hello 的可执行文件
gcc -o hello hello.c # 生成名为 hello 的可执行文件【只要 -o 后接可执行文件名称即可】

1.分步编译的四步

  1. **预处理:**宏定义展开、头文件展开、条件编译、删除注释、不会检查语法错误,生成 *.i 文件
  2. **编译:**词法分析、语法分析、语义分析、优化后生成相应的汇编代码。检查语法错误,将 *.i 文件 编译成汇编文件 *.s 文件
  3. **汇编:**将 *.s 文件 生成 目标文件*.o 文件(二进制文件)。
  4. **链接:**C语言程序需要依赖各种库,编译后需要把库链接到可执行文件*.out 或者 *.exe中。【主要是动态库即DLL文件(Dynamic Link Library)】。

2.编译命令

1
2
3
4
gcc -E hello.c -o hello.i    # 预处理
gcc -S hello.i -o hello.s # 编译
gcc -c hello.s -o hello.o # 汇编
gcc hello.o -o hello # 链接
选项含义
-E只进行预处理
-S只进行预处理、编译
-c只进行预处理、编译、汇编
-o指定输出文件的文件名

PS:助记,选项是ESC,后缀是iso。

3.文件包含处理

include <> 用于包含库函数的头文件。

include "" 用于包含自定义函数的头文件。

4.条件编译

条件编译在预处理阶段展开。

(1)测试存在

1
2
3
4
5
#ifdef 标示符
程序段1
#else 标示符
程序段2
#endif

举个例子:

如果存在变量 a,则打印上面一句,如果不存在变量a,则打印下面一句。

1
2
3
4
5
6
7
8
9
10
int main() {
int a;
#ifdef a
printf("存在 变量a");
#else
printf("不存在 变量a");
#endif
}

//存在 变量a

(2)测试不存在

ifndefifdef 多一个 n。

多用于 防止头文件重复包含。

1
2
3
4
5
#ifndef 标示符
程序段1
#else 标示符
程序段2
#endif

举个例子:

自定义头文件 test.h。为防止头文件重复包含,可以使用以下条件编译:

1
2
3
#ifndef _TEST_H_
#define _TEST_H_
#endif

也可以防止头文件重复包含。

1
2
// 保证头文件只被编译一次。
#pragma once

(3)根据表达式定义

1
2
3
4
5
#if 表达式
程序段1
#else
程序段2
#endif

举个例子:

用来调试代码。

1
2
3
#if 0
// 此处代码不会被执行。
#endif

003.system函数

**用途:**在程序中,执行DOS命令 / Linux命令 /外部程序。

常见的有:

system(“PAUSE”) 暂停屏幕

system(“CLS”):清屏

system("shutdown -s -t 3"):三秒后关机

1
2
3
4
5
6
7
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Command is coming:");
system("ls"); //调用Linux命令
system("./hello") //执行外部可执行文件hello
}

PS:Windows的两个框架:QT / MFC,故可用QT / MFC调用system函数,执行 DOS命令 或者 外部程序。

004.数据类型

1.作用

编译器预算变量分配的内存空间大小。

2.本质

数据类型的本质:固定内存大小的别名

3.补充

  • 变量命名不能以数字开头。
  • char 类型本质上是 1字节大小的整型,存储的是ASCII码。

4.数组类型

数组也是一种数据类型,由 元素个数元素对应类型 决定。

int a[10];,a 的数据类型是 int [10]

5.sizeof

(1)sizeof是用来求数据类型的字节数的。

int a; 那么无论 sizeof(int) 或者是 sizeof(a) 都是等于4,因为 sizeof(a) 其实就是 sizeof(type of a)

  • 对于 char str[] = "ab"; 就有 sizeof(str) == 3, 因为 str 的数据类型是 char [3]
  • 对于 sizeof("ab") == 3,因为 "ab" 的数据类型是 const char [3]
  • 对于char *p = "ab"; 就有 sizeof(p) == 4,因为 p 的数据类型是 char*
  • 对于空字符串 sizeof("") == 1sizeof("\0") == 2 。只要是字符串,默认结尾加 \0

(2)数组名和指针的辨析

1
2
3
4
5
6
7
8
9
10
char *p1 = "ab";   // "ab" 储存在字符常量区,“ab” 表示一个指针,指向 “ab” 的首地址
char *p2;
char s[2] = "ab";
p2 = s; // 数组名是指针,指向首元素的首地址

sizeof(s); // 2 // 返回字符数组的内存大小【传入的是首元素首地址】数组做sizeof的参数不退化成指针。
sizeof(&s); // 8 // 返回地址的内存大小。&s 表示整个字符数组的首地址。
sizeof(p1); // 8 // 返回指针的大小。64位系统下,指针占八个字节。存的是 “ab” 的地址。
sizeof(p2); // 8 // 返回指针的大小。
sizeof("ab"); //2 // 返回"ab"的内存大小。

(3)内存大小和字符串长度的辨析

sizeof 计算内存大小,测量字符串长度,使用 strlen() 函数。

1
2
char a[100] = {'a', 'b', 'c'};     // sizeof(a) == 100;
char b[] = “abc”; // sizeof(b) == 4; (附加结束符 \0)

6.数据类型取值范围

计算机储存数据,都是按照补码来储存的,所以取值范围即若干位补码的取值范围。

(1)字节大小

数据类型占用空间取值范围
char1字节-128~127(-27 ~ 27 -1)
short2字节-32768~32767(-215 ~ 215 -1)
int4字节-2147483648~2147473647(-231 ~ 231 -1)
long4字节-2147483648~2147473647(-231 ~ 231 -1)
unsigned short2字节0~65535(0 ~ 216 -1)
unsigned int4字节0~4294967295(0 ~ 232 -1)
unsigned long4字节0~4294967295(0 ~ 232 -1)
long long8字节
float4字节精度:7位有效数字
double8字节精度:16位有效数字

(2)为何正数范围比负数范围少一个?

以有符号的char为例。

原码范围
正数0000 0000 ~ 0111 111+0 ~ +127
负数1000 0000 ~ 1111111-0 ~ -127
特殊1000 0000 不等于-0,等于-128-128

**原因:**因为-127 + -1 = 1 1000 0000 即-128,舍去符号位1,将 1000 0000 当作 -128。

1
2
3
  1000 0001(-127的补码)
1111 1111(-1的补码)
1 1000 0000(-127 + -1 = -128的补码)

(3)数值越界

化为补码运算后,舍弃高位。

1
2
unsigned char a = 255 + 2;
printf("%u", a); // 1

PS:以下不是数值越界情况,a + 2 是另外一块存储单元。

1
2
unsigned char a = 255;
printf("%u", a + 2); // 257

7.关键字:signed、unsigned

  • signed 表示 有符号,数据类型默认有符号。
1
signed int a = -1;    /*等价于*/    int a = -1;
  • unsigned 表示 无符号。
  • 无符号打印用 %u,打印的是补码,所以不能打印负数!。
1
2
unsigned int a = 1;
print("%u", a);

005.输出

1.格式

类型格式
short%hd
long%ld
unsigned int%u
long%ld
long long%lld
unsigned long long%ull
以16进制打印指针%p
打印%%%
转义字符含义
\r光标切换到句首,【删除本行】
用处:进度条,倒计时
\b退格,【删除一个字符】
\123八进制转义字符
\x23十六进制转义字符
“\0123”字符串中 “\0123” 表示 \n,
1
2
printf("%d", '\123');   //0123 = 10
printf("%d", '\0x23'); //0x23 = 35
  • double 和 float 输出默认六位小数,格式化输出除外。

2.调试

用于打印调试日志。

__FILE____LINE__ 是系统设置好的。⚠️注意:__FILE____LINE__ 是左右各两个下划线连在一起。

1
2
printf("file = %s\n", __FILE__);   // 文件目录打印
printf("line = %d\n", __LINE__); // 该行代码所在行

3.补充:取余和取模的区别

  • 同号取余取模结果一致。
  • 异号结果不一致

举例:

1
2
3
4
5
6
7
// 取余
-7 % 4 = -3
7 % -4 = 3

// 取模
-7 mod 4 = 1
7 mod -4 = -1

**本质:**取余和取模都是以下两步

1
2
商 = a / b;
取余或取模 = a - (商 * b);

取余和取模的区别就在于第一步求商这步:

  • 取余:-7 / 4 = -1.75,取余会靠近 0 舍入,商 = -1。
  • 取模:-7 / 4 = -1.75,取模会靠近 负无穷大 舍入,商 = -2。

第二步相同。

006.各进制的储存方式

十进制DEC直接写%d
八进制OCT以0开头:int a =0123%o【默认以四个字节输出】
十六进制HEX以0x开头:int a = 0x123%x【默认以四个字节输出】
%X(即大写字母十六进制输出)
二进制BINC语言不能直接表示

注意事项:

  • 输入输出十进制,计算机默认输入输出的是 原码。

  • 输入输出八进制 / 十六进制,计算机默认输入输出的是 补码。

  • 正数原码补码一致,所以无影响。

举例:

1
2
3
// 输入为十六进制(补码),输出为十进制(原码)
char a = 0x81; //补码:1000 0001
pritnf("%d", a); //原码:1111 1111 = -127
1
2
3
// 输入为十进制(原码),输出为十六进制(补码)
int a = -1; //原码:1000 0000 0000 0000 0000 0000 0000 0001
printf("%0x", a) //补码:1111 1111 1111 1111 1111 1111 1111 1111 = ffff ffff

PS:%0x 以四个字节输出,(32位)。

007.关键字:define、类型限定符

**类型限定符:**extern、const、volatile、register。

1.define

1
#define MAX 1000   //结尾没有分号
  • 作用:定义一个宏定义标示符MAX,它代表1000。
  • #号开头的语句是预处理语句,(预处理时,MAX将自动替换为1000),无需分号结束。

2.extern

1
extern int b;  //未建立储存空间
  • int a 是既声明,又定义。
  • extern int a 是声明,未定义(即未建立储存空间)。

3.const

constant 的缩写。

(1)修饰只读变量

1
const int a = 10

(2)易错

  • 必须初始化。
1
2
const int a;      // 非法
const int b = 10; // 合法
  • 如何在另一.c源文件中引用const常量。
1
2
extern const int i; //合法
extern const int j=10; //非法,常量不可以被再次赋值

(3)好处

  • 可以避免不必要的内存分配。(define 有若干个拷贝,const 只有一份拷贝,不会浪费内存。)
  • 指针做函数参数,可以有效的提高代码可读性,减少bug。

4.volatile

防止编译器优化代码。

1
2
3
int i = 10;
volatile int a = i;
int b = i;
  • 编译器优化代码:当编译器发现,第2行 和 第3行之间没有代码对 i 的值进行改变,自动把上次读的 i值 放在 b 中。
  • volatile 关键字声明 变量i 之后,告诉编译器 i 易变(受操作系统、硬件或者其它线程的影响), 编译器每次都需要在 i 的地址处读取 i 值。

5.register

register:寄存器

1
register int a = 1
  • 定义寄存器变量,如果CPU有空闲寄存器,将a存入寄存器,提高效率。

008.switch语句

case 数字或者字符

1
2
3
4
5
6
7
8
switch (a) {
case 1 :
pass;
case 2 :
pass;
default :
pass;
}
  • 括号内的a只能是 整型 或者 字符型。

009.随机数

  1. void srand(unsigned int seed); 设置rand()产生随机数时的随机种子seed。
  2. int rand(void) rand()返回一个随机数。
1
2
3
4
#include <stdlib.h>
#include <time.h> //time()函数的头文件,用 time() 的返回值做seed
srand((unsigned int)time(NULL)); //设置随机数种子
random_num = rand(); //产生随机数

010.getchar() 吞掉回车

读入字符的时候,一定要注意!利用getchar() 清空 stdin 的缓冲区。

两种场景

  1. 先读入一个字符串,再读入一个字符
1
2
3
4
5
scanf("%s", str);
...
// 中间有上万行代码,只要没有处理 stdin 的缓冲区。
...
sacnf("%c", ch);
  1. 先读入一个字符,再读入一个字符
1
2
3
4
5
scanf("%c", ch1);
...
// 中间有上万行代码,只要没有处理 stdin 的缓冲区。
...
sacnf("%c", ch2);

原因

键盘输入的字符,会放到缓冲区,scanf 函数从缓冲区中读取。读取字符的时候,可以读 ‘\n’ 。读字符串时,不会读入\n,所以不需要 getchar()

以两个c为例,键盘上敲出,

1
2
c
c

缓冲区中显示如下。

1
c\nc

scanf(“%c”, ch) 会读取一个 \n

011.字符数组和字符串

1.区别

在 C 语言中,字符串实际上是使用 null 字符 ‘\0’ 终止的一维字符数组。

1
2
3
4
5
6
7
8
9
10
11
12
//字符数组
char a[] = {'a', 'b', 'c'}; //sizeof(a) = 3
char a[3] = "abc"; //zizeof(a) = 3 【如果没有内存空间补0,就是字符数组】
//注意 char a[3] = "abc"; c++ b允许这么使用,用 const char[4] 来初始化 char[3]

//字符串, %s输出到0停止,sizeof(a) = 4
char a[] = {'a', 'b', 'c', 0};
char a[] = {'a', 'b', 'c', '\0'}; //'\0' 和 0 一样
char a[10] = {'a', 'b', 'c'}; //限定长度,后7位自动补0,(结束符)

//字符串,结尾自动加'\0',会隐藏,但是占大小
char a[] = "abc"; //sizeof(a) = 4

2.字符串越界

定义字符串时,初始化内容的长度大于定义的字符串,就会越界。

3.字符串初始化原理

"abcdef" 储存在文字常量区,a[] 内存空间在栈区,初始化时,将文字常量区的内容 "abcdef" ,拷贝到栈区 的 a[]

1
char a[] = "abcdef";

012.字符串处理函数

1.gets()

可以读取空格,不推荐使用。

1
2
3
4
// 可以读取空格
char a[100];
gets(a);
//返回值:成功:读入的字符串。失败:NULL

2.fgets()

三个要点:

1.写入会覆盖字符串。

2.stdin 内容 < size,写入时自动增加一个换行符。

3.当 stdin 什么都没有时,由于stdin 内容 < size,写入时自动增加一个换行符。

读取回车、空格,推荐使用。

读取遇到换行符,结束本次读取。

1
2
3
4
5
6
char *fgets(char *s, int size, FILE *stream);
//stream:文件指针,如果读键盘输入的字符串,固定写为 stdin

char a[100] = {"原来的文字将会被覆盖掉"};
// 1.如果输入内容 > sizeof(a)-1,只读取 sizeof(a)-1 个字符
// 2.如果输入内容 < sizeof(a)-1,会读取换行符【重要!会增加一个换行符】

3.fputs()

1
2
3
4
5
char a[] = "abc"
fputs(a, stdout); //stdout是设备文件指针,往屏幕上输出,固定写 stdout

FILE *fp = fopen("1.txt", "w");
fputs("abc", fp); // 将“abc”的指针,输出到 文件指针 fp 所关联的文件1.txt

4.strlen()

计算字符串长度(不算结束符 “\0”)(“\n” 算一个字符)。

1
2
3
#include <string.h>
char str[] = "123456";
int len = strlen(str); //6

注意事项:

strlen():从首元素开始,到结束符为止的长度(不计算结束符)。

sizeof():计算数据类型的长度,不会因为结束符停止。

1
2
3
char a[100] = "abc";
strlen(a); //3
sizeof(a); //100

5.strcpy

拷贝字符串,自动加’\0’。

**重要:**遇到 ‘\0’ 停止拷贝。

1
2
3
char old[] = "abcdef\0gjk";
char new[100];
strcpy(new, old); //拷贝到 结束符'\0',并自动加'\0')

6.strncpy

拷贝指定n个字符,不自动加’\0’。

**重要:**遇到 ‘\0’ 停止拷贝。

1
2
3
4
5
6
7
8
9
char old[] = "abcdef\0gjk";
char new01[100];
char new02[100];
int n = 3;
// old 是指针(首地址)
strncpy(new01, old, n); //指定拷贝3个字符(不自动加'\0')

//1. 若 n>strlen(old),拷贝到‘\0’(包含)
//2. 若 n<strlen(old),拷贝n个字符,不自动加'\0'

7.strcmp

判断字符串是否相同。

比较字符串大小(一个字母字母比较)。
注意:strcmp 传入参数时需保证参数不为 NULL/nullptr,因为 strcmp 实现中会直接取*,若是 NULL/nullptr,会报段错误。

1
2
3
4
5
6
7
8
9
char str1 = "abc";
char str2 = "bac";
if (strcmp(str1, str2) > 0)
printf("str1 > str2");

//返回值:
//1.相等 =0
//2.大于 >0
//3.小于 <0

附:字符串比较问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// char * 类型 [不可以使用 == 比较]
char *str1 = "abc";
char *str2 = "abc";
printf(str1 == str2)   // 打印 1,表示 str1 和 str2 指向的地址相同。
// 这里 == 比较的是 str1 和 str2 存的数据,也就是 “abc” 的地址。“abc” 是在文字常量区。


// char 数组类型 [不可以使用 == 比较]
char str3[] = "abc";
char str4[] = "abc";
printf(str1 == str2)   // 打印 0, 表示 str3 和 str4 指向的地址不同。
// char 数组内存空间在栈区,str3 和 str4 分别指向两块内存空间。


// c++ 中的 string 类型 [可以使用 == 比较]
string str5 = "abc";
string str6 = "abc";
printf(str1 == str2) // 打印 1, string 类重载了 == 操作符

8.strncmp

判断指定n个字母的大小。
注意:与 strcmp 一样,传入参数时需保证参数不为 NULL/nullptr,因为 strncmp 实现中会直接取*,若是 NULL/nullptr,会报段错误。

1
2
3
4
char str1 = "abc";
char str2 = "bac";
if (strncmp(str1, str2,2) == 0)
printf("str1 和 str2的前两个字母 相同");

9.strcat

将 str2 追加在 str1 后面。

1
2
3
char str1 = "abc";
char str2 = "def";
strcat(str1, str2); //str1 = "abcdef"

存疑:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;
int main(){
char a[] = "abcaaaaa11111111111111121sdfhaljashdflhfsdajlshdlfjsdhfljhldfjashfldajsdfhljksdfhljksfdhljksfdahljkfdhljkdfhljkfdhjklshfjklsfhlkjfsadhjklafdhjklfadhjkldfaljhkfadjklfsadjlsfahdjlhfjldahdfjlsaldhfsjalhjkdlfhdsaahlfsjdahsldjsfldhjalhfjdhljfdaahsjfdhljdsdflhklsdfkajhjfsdlahfjsdhfsdjkalhdfksljfdhjkasfhjklhfsajlfsahjkfdhskjafdskhjlfdshkldfhsklhlsfhfsdlshdflkshdfhskdhfhfdkjshsafhdlklasfdhk323342egwyrgyasdhljsdfhlkashfklsdhfkjsdfklhasadlhsdjlfhalshjdsljahkdfaljkdflhjkasfhkjlsafdkljlfsahjkfsahlkfsahlkdafldhjshfladhslfjkadhjfdshjflsfhajldhjlfhfjlhlflhkjlhksaklhsfhdklsdhfjklsaldfhasdjhlfsjldhkfjhkdljkhdfaljkhadjkfdhalkfadshlsdafjklsfdkjlsfkjlsjhkflddfjkhlhjfdklfdajkhlfjkhlhjklsfdhjsfklajksfldajkhlfkhsfhkfshjkfslhfjsdlewgfy83hfuehfjkashfjhsdfhgwieuhfuishflsdf";
char b[] = "1234567890";
strcat(a, b);
cout << a;
}

// 当 a 字符串过长,追加会报错。【待解决】

但是若是定义a 的空间大一些,就不会错。

1
2
3
4
5
6
7
8
9
#include <iostream>
using namespace std;
int main(){
char a[1024] = "abcaaaaa11111111111111121sdfhaljashdflhfsdajlshdlfjsdhfljhldfjashfldajsdfhljksdfhljksfdhljksfdahljkfdhljkdfhljkfdhjklshfjklsfhlkjfsadhjklafdhjklfadhjkldfaljhkfadjklfsadjlsfahdjlhfjldahdfjlsaldhfsjalhjkdlfhdsaahlfsjdahsldjsfldhjalhfjdhljfdaahsjfdhljdsdflhklsdfkajhjfsdlahfjsdhfsdjkalhdfksljfdhjkasfhjklhfsajlfsahjkfdhskjafdskhjlfdshkldfhsklhlsfhfsdlshdflkshdfhskdhfhfdkjshsafhdlklasfdhk323342egwyrgyasdhljsdfhlkashfklsdhfkjsdfklhasadlhsdjlfhalshjdsljahkdfaljkdflhjkasfhkjlsafdkljlfsahjkfsahlkfsahlkdafldhjshfladhslfjkadhjfdshjflsfhajldhjlfhfjlhlflhkjlhksaklhsfhdklsdhfjklsaldfhasdjhlfsjldhkfjhkdljkhdfaljkhadjkfdhalkfadshlsdafjklsfdkjlsfkjlsjhkflddfjkhlhjfdklfdajkhlfjkhlhjklsfdhjsfklajksfldajkhlfkhsfhkfshjkfslhfjsdlewgfy83hfuehfjkashfjhsdfhgwieuhfuishflsdf";
char b[] = "1234567890";
strcat(a, b);
cout << a;
}
// 正确执行

10.strncat

将 指定长度的 str2 追加在 str1 后面。

1
2
3
char str1 = "abc";
char str2 = "def";
strcat(str1, str2, strlen("cd")); //str1 = "abcd"

11.sprintf

格式化一个字符串,并输出到指定数组中。

1
2
3
4
5
int a = 10
char str[] = "abc";
char dst[100];
sprintf(dst, "a = %d, str = %s", a, str);
printf("%s", dst); //a = 10, str = abc

12.sscanf

从字符串数组中按指定格提取内容(便于提取数字)。

(1)提取数字

1
2
3
4
char str[] = "a = 1, b = 2, c = 3";
int a, b, c;
sscanf(str, "a = %d, b = %d, c = %d", &a, &b, &c);
printf("%d %d %d", a, b, c); //1 2 3

(2)提取字符

1
2
3
4
char str[] = "hello world my friend"  //提取字符串,默认以空格分割
char m[10], n[10], k[10];
sscanf(str, "%s %s %s", m, n, k); //不用取地址&,数组名就是首元素地址
pritnf("%s %s %s", m, n, k);

13.strchr

查询字符。

1
2
3
4
5
6
7
8
9
10
char buf[] = "abcdef";
char *p = strchr(buf, 'd');
// 在 buf 中查询字符'd'
// 若找到,返回d所在位置地址,若失败,返回NULL
if (p == NULL) {
printf("查询失败");
} else {
printf("%s", p); //打印 d 开始的字符串
printf("%c", p[0]); //只打印 d
}

14.strstr

(1)查询字符串

1
2
3
4
5
6
7
8
9
char buf[] = "abcdef";
char *p = strstr(buf, "abc");
// 在 buf 中查询字符串 "abc"
// 若找到,返回 a 所在位置地址,若失败,返回NULL
if (p == NULL) {
printf("查询失败");
} else {
printf("%s", p);
}

(2)模型|查找字符串个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 查找匹配字符串的个数
int main(){
char *p = "12abc23dasdabcrweqabc";
char *tmp = NULL;
int n = 0;
while (1) {
//tmp 标记当前查找到的位置,若为NULL,表示没查到
tmp = strstr(p, "abc");
if (tmp == NULL) {
break;
} else {
n ++;
p = tmp + strlen("abc");
}
}
}

15.strtok

字符串分割,会更改原字符串,记得备份。

  1. 第一次调用,参数写原字符串地址。
  2. 第二次起调用,参数写NULL。
  3. 每次调用,返回值为切割的字符串地址。
1
2
3
4
5
6
7
8
9
char buf[] = "abc,edf,mnk,110";
// 第一次调用,第一个参数写原字符串地址,第二个参数写分割符
char *P = strtok(buf, ",");
//若分割成功,p = 字符串地址。若失败(即无内容可分),则 p = NULL
while (p != NULL) {
printf("%s\n", p);
// 第二次起调用,第一个参数写NULL,第二个参数写分割符
p = strtok(NULL, ",");
}

具体样例:

1
2
3
4
5
char buf[] = "abc,edf";
char *p = strtok(buf, ","); // 此时 buf[] = "abc"
printf("%s", p); //abc
p = strtok(NULL, ","); // 第二次参数使用NULL
printf("%s", p); //edf

16.atoi

将字符串转化为整型。

1
2
3
char str[] = "abc123";
int num = atoi(str);
printf("%d", num); //123

17.atof

将字符串转化为浮点型。

1
2
3
char str[] = "0.123";
double num = atof(str);
printf("%lf", num); //0.123

18.atol

将字符串转化为长整型。

1
2
3
char str[] = "abc123";
long num = atoi(str);
printf("%ld", num); //123

013.字符串常用模型

1.两头堵模型

有一个字符串,开头或结尾含有n个空格, (" abcdefgdddd "),欲去掉前后空格,返回一个新字符串。

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

char *trimSpace(char *inbuf, char *outbuf) {
if (inbuf == NULL) {
return NULL;
}
char *begin = inbuf, *end = inbuf + strlen(inbuf) - 1;
while (begin < end) {
if (*begin == ' ') {
begin++;
end--;
} else {
break;
}
}
outbuf = (char *)malloc(end - begin + 2);
strncpy(outbuf, begin, end - begin + 1);
return outbuf;
}

int main() {
char *inbuf = "";
char *outbuf = NULL;
outbuf = trimSpace(inbuf, outbuf);
printf("%s", outbuf);
return 0;
}

2.字符串反转模型

例如:“abcd” 变为 “dcba”。

(1)传统解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
char *inverse(char *str) {
if (str == NULL) {
return NULL;
}
int lens = (int)strlen(str);
char *head = NULL, *end = NULL;
head = str;
end = str + lens - 1;
while (head < end) {
char tmp = *head;
*head = *end;
*end = tmp;
head ++;
end --;
}
return str;
}

(2)递归解法

非常巧妙,strncat(buf, str, 1); 一定要放在 inverse(str+1); 后面,这样才能先写入后面的数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 全局变量
char g_buf[100] = {0};

int inverse(char *str) {
if (str == NULL) {
return -1;
}
if (str[0] == '\0') {
return 0;
}

inverse(str+1);
strncat(g_buf, str, 1);
return 0;
}

014.分文件编程

1.文件功能

  1. main.c

调用自己的头文件。

1
2
3
4
5
#include "my.h"

int main() {
//
}
  1. my.h

放置函数声明,避免每调用一次,都要写无数条函数声明。

1
2
int my_fun01(int a);
int my_fun02(int a);
  1. my_fun.c

写函数的具体实现。

1
2
3
4
5
6
7
int my_fun01(int a) {
//
}

int my_fun02(int a) {
//
}

2.防止头文件重复包含

1
#pragma once

015.指针

1.野指针

野指针:保存非法地址的指针。

非法地址:只有定义后的变量的地址才是合法地址。

1
2
3
int *p;
p = 0x1234;
*p = 1; //操作野指针所指向的内存 将导致段错误

2.空指针

1
2
3
// p1, p2都是空指针
int *p1;
int *p2 = NULL;

3.指针大小

32位编辑器 用32位(4字节)存指针。

64位编辑器 用64位(8字节)存指针。

4.多级指针

1
2
3
4
int a = 10;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;

5.*p 等价于 p[0]

1
2
int a = 10;
int *p = &a;

变量a 大小为4个字节,由四个地址储存。p 是指向 a 的首地址,等价于 p[0]。

**PS:**p[1] 等价于 *(p + 1),即p加上4字节,野指针不能用。

1
2
3
int a[5] = {0};
int *p = a;
// 如果是数组的话,p[1] 等价于 *(p + 1),数值刚好是 a[1];

6.指针步长

p+1,不是 p中所指向的地址 + 1,而是 p中所指向的地址 + sizeof(数据类型)

7.万能指针

void * 定义的指针。

1
2
3
4
5
// void a;  不能定义变量,因为不知道分配多大空间
void *p; //可以定义指针,因为64位编译器用 8字节 储存指针
int a = 10;
p = &a;
*(int *)p = 11; // 为什么要类型转换? 因为*p 不知道要操作多大内存(p指向的是首地址)

8.const修饰指针

(1)const * 表示指针所指向的变量只读。

const 修饰 *

1
2
3
4
int a = 10;
const int *p1 = &a;
int const *p2 = &a; // 两种写法都可以
p = NULL; //不能通过 *p 修改 a的值,但可以修改 p 的值

(2)* const p 表示指针变量只读。

const 修饰 p

1
2
3
4
int a = 10;
int * const p = &a;
*p = 100; // 可以通过 *p 修改a的值,但是不能修改 p 的值
p = NULL //error

(3)双const

指针和指针所指变量的值都不能修改。

1
2
int a = 10;
const int * const p = &a;

9.形参中的数组退化为指针

形参中的 a[] 是指针,写 int *a int a[100] int a[]是一样的。

1
2
3
4
5
6
7
8
9
int fun(int a[]) {
int n = sizeof(a)/sizeof(a[0]) //无法求出数组长度,sizeof(a) = 64位/32位
//a[1] 表示 *(a + 1)
}

int main() {
int a[10] = {0};
fun(a); //传入的是指针(数组首地址)
}

PS:补充见 015.13.(2) 形参中不能用 char **a 代替 char a[][10]

10.字符串常量地址相同

  1. 字符串常量 放在 data 区,相同的常量,地址相同。
  2. 每个字符串都是一个地址】,这个地址是字符串首地址。
1
2
3
4
5
6
7
8
9
10
11
12
int fun() {
printf("%p", "hello");
}

int main() {
printf("%p", "hello"); //0x123
fun(); //0x123

printf("%c", *("hello")); //h //"hello" 表示 字符串的首地址【重要】
}

int *p = "hello"; // 表示 指针p 指向 "hello" 表示的地址

11.数组名

(1)数组名是常量

数组只有定义的时候可以初始化!

1
2
3
4
5
6
7
8
9
int a[10];
//1. 数组名 始终储存着 数组首元素地址,即 a == &a[0]
a = {10}; //error //2. 数组名是常量,不允许修改

---------------------------------------------------

char a[10];
a = "abc"; //error //见 2
strcpy(a, "abc"); //【解决方案】

(2)数组名b&b 的数据类型不一样

1
int b[10];

b 表示首元素的地址。b + 1 表示 首元素的地址加 4。

&b 表示整个数组的首地址。&b + 1 表示 整个数组的首地址加 4 * 10。

(3)二维数组数组名

1
int a[3][4] = {{0}, {1}, {3}};

a&a[0] 表示 第 0 行首地址。步长:sizeof(int [4])

a + i&a[i] 表示 第 i 行首地址。 步长:sizeof(int [4])

*(a + i)a[i] 表示 第 i 行首元素地址。步长:sizeof(int)

*(a + i) + j&a[i][j] 表示 第 i 行第 j 列元素的地址。步长:sizeof(int)

*(*(a + i) + j)a[i][j] 表示 第 i 行第 j 列元素的值。

&a 表示 整个数组的首地址,&a + 1 表示 整个数组的首地址加 整个数组的大小。步长:sizeof(int [4] * 3)

辨析】:

&a:整个数组的首地址

aa + 0:第 0 行首地址,&a[0]&(*a)

*a*(a + 0):第 0 行 第 0 个元素首地址,a[0]

**a*(*(a + 0) + 0) :第 0 行 第 0 个元素的值,*a[0]a[0][0]

(4)二维数组的本质

二维数组的本质:一维数组;也可以看成若干个一维数组的组合。

将二维数组的每一行看作一个数组,有助于理解 数组指针。

另外,由于二维数组其实也是线性存储的,所以二维数组可以当一维数组输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
void fun(int *p, int n) {
for (int i = 0; i < n; i ++) {
printf("%d\n", p[i]);
}
return;
}

int main() {
int a[4][3] = {1, 2, 3, 4, 5, 6, 7};
int a_size = sizeof(a) / sizeof(a[0][0]);
fun((int *)a, a_size);
return 0;
}

(5)二维数组名的本质

二维数组名的本质:数组指针。

通过 + i 来指向若干个数组的首地址,a 表示第 0 行的首地址,a + 1 表示第 1 行的首地址。

(6)二维数组求行数、列数

1
2
3
int a[3][4] = {0}; //第一个元素为0,初始化变量时,未提及的元素自动为0
line = sizeof(a) / sizeof(a[0]); //求行数
row = sizeof(a[0]) / sizeof(a[0][0]); // 求列数

12.指针数组

1
2
char *p[] = {"11111", "22222", "22333"};
// p[0] 指向 文字常量区的 “11111”,sizeof(p[0]) = 8

13.数组指针

(1)储存整个数组首地址

一维数组】数组指针指向 一维数组 整个数组的首地址,&a ,步长为整个数组的大小。

二维数组】每一行是一个数组,数组指针指向 一维数组 整个数组的首地址(即每一行的首地址),a,表示第 0 行数组的首地址,步长为整个第 0 行数组的大小。

注意】数组指针 p 的步长,是数据类型的大小,也就是整个数组的大小。

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
/* 定义------------------------------------------------ */
// 第一种格式,【先定义数组类型,再定义数组指针】
typedef int A[10];
A *p = NULL;

// 第二种格式,【先定义数组指针类型,再定义数组指针】
typedef int(*P)[10];
P p = NULL; // 定义数组指针p

// 第三种,直接定义
int(*p)[10];

/* --------------------------------------------------- */
// 1. 赋值,一维数组
int a[10] = {0};
p = &a;

// 遍历一维数组
for (int i = 0; i < 0; i++) {
printf("%d", (*p)[i]); // *p 表示 *(&a),即 a,
}

/* --------------------------------------------------- */
// 2. 赋值,二维数组
int b[2][10] = {0};
p = b;

// 遍历二维数组
for (int i = 0; i < 2; i++)
for (int j = 0; j < 3; j++) {
printf("%d", p[i][j]);
}

(2)形参中不能用 char **a 代替 char a[][10]

对于一维数组,函数形参中,写 char a[]char a[100]char *a 是一样的,均为 char * 类型的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 下面代码完全正确 */
void fun(char *a, int n) {

for (int i = 0; i < n; i++) {
printf("%c\n", a[i]);
}
printf("%p\n", a);
printf("%p\n", a + 1); // 指针步长为 sizeof(char) == 1
return;
}

int main() {
char a[3] = {'a', 'b', 'c'};
int n = sizeof(a) / sizeof(char);
fun(a, n);
return 0;
}

但是对于,二维数组,函数形参中不能用 char **a 代替 char a[][10]

因为 a[1] 表示 *(a + 1) ,而 a 的步长是 sizeof(char *) == 8 。我们想要的步长是 10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 下面代码错误:不能用 char **a 代替 char a[][10] */
void fun(char **a, int n) {

for (int i = 0; i < n; i++) {
printf("%s\n", a[i]);
}
printf("%p\n", a);
printf("%p\n", a + 1); //步长是 sizeof(char *) == 8
return;
}

int main() {
char a[3][10] = {"aaa", "bbb", "ccc"};
int n = sizeof(a) / sizeof(a[0]);
fun(a, n);
return 0;
}

(3)改进办法1:形参中规定指针步长

void fun(char **a, int n); 改为 void fun(char a[3][10], int n);fun(char a[][10], int n);

数组做形参都会退化成指针,即 a 是一个指针,所以cahr a[3]char a[] 都是 char *a 的意思,指针 a 指向 数组的首地址,(每一行就是一个数组),a + i 表示 第 i 行的首地址,即第 i 行数组的首地址。

(4)改进本法2:形参中用数组指针

void fun(char **a, int n); 改为 void fun(char (*a)[10], int n);

传入的是数组的首地址,(每一行就是一个数组),即传入的是 每一行的首地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void fun(char (*a)[10], int n) {

for (int i = 0; i < n; i++) {
printf("%s\n", a[i]);
}
printf("%p\n", a);
printf("%p\n", a + 1); //步长是 sizeof(char *) == 10
return;
}

int main() {
char a[3][10] = {"aaa", "bbb", "ccc"};
int n = sizeof(a) / sizeof(a[0]);
fun(a, n);
return 0;
}

14.指针函数

返回指针类型的函数。

1
int *fun();

15.函数指针

指向函数的指针。

(1)先定义函数类型,再定义指针(不常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int fun_01(int a) {
//
}

// 【定义】
typedef int FUN(int a); // FUN 是函数类型
FUN *p = NULL; // 函数指针变量,函数返回值和形参 必须和和 FUN 函数类型相同

// 【赋值】
// 下面两种赋值方式完全等价
p = &fun_01; // 函数指针 p 指向 fun_01 函数
p = fun_01;

// 【使用】
fun(5); // 传统调用
p(6); // 函数指针调用
(*p)(6); // 函数指针调用

(2)先定义函数指针类型,根据类型定义指针变量

1
2
3
4
5
6
7
8
// 定义函数指针类型
typedef int (*PFUN)(int a); // a 可省略。typedef int (*PFUN)(int);

// 定义函数指针
PFUN p = fun_01 // 函数指针 p 指向 fun_01 函数

// 使用
p(7);

(3)直接定义函数指针(常用)

1
2
3
4
5
6
7
8
// 第一种
int (*p)(int a) = fun_01; // a 可省略。int (*p)(int) = fun_01;
p(8);

// 第二种
int (*p)(int a);
p = fun_01;
p(9);

16.函数指针数组

函数指针组成的数组。

int (*p_fun[5])() = {add, subtract, multiply, divide, exit}; 大括号内写函数名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void add() {
//
}

void subtract() {
//
}

void multiply(){
//
}

void divide() {
//
}

void exit() {
//
}

没使用函数指针数组之前,制作菜单,根据命令调用函数。

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
int main() {
char command[100];
while (1) {
printf("inputs your command please:");
scanf("%s", command);

if (strcmp(command, "add") == 0) {
add();
}

if (strcmp(command, "subtract") == 0) {
subtract();
}

if (strcmp(command, "multiply") == 0) {
multiply();
}

if (strcmp(command, "divide") == 0) {
divide();
}

if (strcmp(command, "exit") == 0) {
exit();
}
}
}

使用函数指针之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(){
char command[100];
int (*p_fun[5])() = {add, subtract, multiply, divide, exit}; /*【函数指针数组的定义】*/
char *command_str[5] = {"add", "subtract", "multiply", "divide", "exit"};
while (1) {
scanf("%s", command);
for (int i =0; i < 5; i++) {
if (strcmp(command, *command_str[i]) == 0) {
p_fun[i](); // ⚠️注意:函数调用需要有个括号。
break
}
}
}
}

17.回调函数

在函数中调用函数,通过传递不同的函数指针,实现同一个函数框架,调用不同的函数。

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
int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}

int multiply(int a, int b){
return a * b;
}

int divide(int a, int b) {
return a / b;
}

// 框架
// a、b 可以省略。
// int fun(int x, int y, int (*p_fun)(int, int) /*回调函数*/ ) {
int fun(int x, int y, int (*p_fun)(int a, int b) /*回调函数*/ ) {
return p_fun(x, y);
}

// 主函数使用
int main() {
int a = 1, b = 1;
int res = fun(a, b, add);
}

18.指针易错

(1)指针值传递

调用函数修改指针的值,需要传递的参数是 指针的地址,并用二级指针的来做形参,接收指针的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//【错误】:
int a = 1, b = 2; //定义两个全局变量

void fun(int *p) {
p = &b;
}

int main() {
int *p = &a;
fun(p); // 虽然传的参数是指针,但是是值传递。// 调用fun函数,无法改变 p 的值。
}

// 【改正】:
int a = 1, b = 2;

void fun(int **p) { // 由于 p 是指针,所以要用二级指针来存 p 的地址。
*p = &b; // 用 *p 改变 p 所指向地址 所储存的值。
}

int main() {
int *p = &a;
fun(&p); // 要改变 p 的值,需要传递的参数是 p 的地址。
}

(2)二级指针没有空间

一级指针没有指向内存空间,通过操作二级指针给一级指针所指内存拷贝内容。【内存污染】

1
2
3
4
// pp 指向一个堆区空间,存放三个指针 pp[0], pp[1], pp[2] 的地址。
char **pp = (char **)malloc(3 * sizeof(char *));
char ch[] = "abcd";
strcpy(pp[0], ch); // error. //pp[0] 没有指向任何空间,所以没有空间 存储字符串。

016.main()函数参数

1
2
3
int main(int argc, char *argv[])
int main(int argc, char **argv)
// argv[] 是一个指针数组,但形参中用二级指针保存指针数组 argv[] 的首地址

argc 储存的是 argv[] 的 数组大小

argv[] 存储的是命令 + 参数,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main(int argc, char *argv[]) {
printf("argv[0] = %s\n", argv[0]);
printf("argv[1] = %s\n", argv[1]);
}

/*
1. 将此文件编译成二进制可执行文件,名叫 my_main

2. 在终端中执行该二进制文件,并添加参数:
./my_main abc

3. 打印结果显示为
argv[0] = ./my_main
argv[1] = abc
*/

017.内存管理

1.普通局部变量

(1)作用域:括号

(2)离开{},内存自动释放

(3)就近原则

1
2
3
4
5
6
7
int main() {
int a = 10;
{
int a = 11;
printf("%d", a); // a = 11
}
}

2.static局部变量

(1)作用域:括号

(2)离开{},不会释放;程序结束,static局部变量才自动释放【存储在data区,程序不结束,data区数据不释放】。

1
2
3
4
5
6
7
8
9
10
11
int fun() {
static int a;
a ++;
printf("%d", a); //函数结束,a不释放
}

int main() {
fun(); //1
fun(); //2
fun(); //3
}

(3)data区数据,在编译阶段已分配空间,程序还没执行,static 局部变量就存在。

(4)static 局部变量不初始,值默认 = 0。多次调用初始化语句,只会执行一次。

(5)static 局部变量只能用常量初始化。

1
2
int a = 10;
static b = a; //error //程序还没执行(a变量还没定义),static变量已存在

3.普通和static局部变量的区别

(1)普通局部变量只有执行到定义变量的语句才分配空间;static局部变量编译时就分配空间。

(2)普通局部变量离开{},自动释放;static局部变量程序结束,自动释放。

(3)普通局部变量不初始化,值为随机数,static局部变量不初始化,值为0【初始化语句只执行一次】。

4.全局变量

(1)使用变量时,前面没有变量定义,需声明。

1
2
3
4
5
6
7
8
9
int fun() {
puts(a); //使用变量时,前面未定义变量 //在第2行前面,extern int a;
}

int a; //{}外定义全局变量

int main() {
fun();
}

(2)分文件写,在main.c中定义,在头文件中声明。

  • 为什么在头文件中声明? 避免函数需要进行多次声明。【见 014.分文件编程
  • **为什么在main.c中定义?**不能在头文件中定义,否则多次调用头文件时,会出现多次定义全局变量的问题。

5.static全局变量

  • static全局变量,只能在本文件中使用,不能在其他文件使用。
  • 普通全局变量可以在所有文件中使用。

6.内存分区

(1)内存四区

  1. 堆区 heap

  2. 栈区 stack

    • 局部变量
  3. 全局区

    • 未初始化 bss
      • 静态变量
      • 全局变量
    • 初始化 data
      • 静态变量
      • 全局变量
    • 文字常量区
  4. 代码区 code

(2)内存加载顺序

  • 当程序运行时,首先将以下几个确定的内存分区(code,data,bass)先加载:

    • code(代码区):只读,函数。
    • data:初始化的数据,全局变量,static 变量。
    • 文字常量区(只读)。
    • bss:没有初始化的数据,全局变量,static变量。
  • 然后额外加载两个区:

    • stack(栈区):普通局部变量,自动管理内存,先进后出。
    • heap(堆区):手动申请空间(malloc),手动释放(free),整个程序结束,系统自动回收。

(3)栈区堆区扩展方向

栈区:从高地址向低地址扩展。

堆区:从低地址向高地址扩展

7.内存操作函数|memset

void 是万能指针,可以接收任何类型指针【见015.7.万能指针】,size_t 是无符号整型。

特别注意

memset(b, 0, sizeof(b)) 是对 每个字节 都赋值为 0;以64位 int 为例,四个字节分别赋值为 0(最后该数字还是等于0)。

进一步探究

对于 char 而言,可以赋值任何字符,因为其只有一个字节。

对于 int 而言,除了 0 之外,还可赋值为 -1、0x3f3f3f。

  1. 当赋值为 -1,-1 的反码是 1111 1111 1111 1111 1111 1111 1111 1111,存入 1 字节就是 1111 1111,每个字节都是 1111 1111 ,那四个字节就是 1111 1111 1111 1111 1111 1111 1111 1111,所以该 int 数字还是 -1。
  2. 当赋值为 0x3f3f3f,其反码是 0x3f3f3f,存入 1 字节就是 0x3f,每个字节都是 0x3f,那四个字节就是 0x3f3f3f,所以该 int 数字还是 0x3f3f3f。
1
2
3
4
5
6
7
8
9
10
11
12
13
void *memset(void *s, int c, size_t n);
//将s的内存区域的前n个字符,以参数c填入【参数c,是以字符来处理,即ascii码】

int a;
memset(&a, 0, sizeof(a));
printf("%d", a); // 0

memset(&a, 97, sizeof(a));
printf("%c", a) // c

//用处:清空数组
int b[10];
memset(b, 0, sizeof(b));

8.内存操作函数|memcpy

1
2
3
4
5
6
7
8
void *memcpy(void *dest, const void *src, size_t n);
//拷贝src所指内存的前n个字节,到dest所值的内存地址上

char p[] = "hello\0world";
char buf[100];

memcpy(buf, p, sizeof(p)); // buf[100] = "hello\0world"
//memcpy 可以拷所有字符,不会因为”\0“结束【与strcpy的区别】

9.内存操作函数|memmove

**用途:**将 a[0], a[1], a[2] 所存内容移动到 a[2], a[3], a[4].(如果用 memcpy 拷贝,会发生内存重叠)。

1
2
3
4
int a[5] = {1, 2, 3};                    // a[5] = {1, 2, 3, 0, 0}
memmove(&a[2], a, 3 * sizeof(int)); // bfore_a[5] = {1, 2, 1, 2, 3}
// 以上表示的是,将 地址a的数组的 前3个元素,移动到 地址是&a[2]数组的 前3个元素
// 如使用 memcpy,a[5] = {1, 2, 1, 2, 1},内存冲突,会覆盖

10.内存操作函数|memcmp

与 strncmp 相似,**用途:**判断是否相等。

1
2
3
4
5
6
7
int a[10] = {1, 2, 9};
int b[10] = {9, 2, 1};

int flag = memcmp(a, b, 10 * sizeof(int)); //flag < 0 // 比较的是ascii码,一个个比较
// 相等:flag == 0
// a>b:flag > 0
// a<b:flag < 0
  • **补充:**不能使用 memcmp 来判断 strcut 结构体是否相等。

    因为结构体存在内存对齐,内存对齐的那几个字节的值是随机的。

1
2
3
4
5
6
7
struct MyStruct
{
char a;
int b;
};
MyStruct A, B; // A 和 B 均有内存对齐,但是内存对齐的三个字节,内容是随机的。
memcmp(&A, &B, sizeof(MyStruct));
  • 再补充:如果一定要用 memcmp 比较,需要先用 memset 将 A、B 置 0,再赋值,才能比较。因为 memset 将字节对齐的那部分也置于 0 了。

  • 再再补充:结构体比较是否相等,用重载 ==。

11.内存释放

内存释放只能释放一次,释放多次会发生段错误。

内存泄漏: 动态分配空间,不释放空间。

内存污染: 非法使用内存(操作野指针所指向的内存、堆区越界)

1
2
3
4
5
6
int *p = NULL;
p = (int*) malloc(sizeof(int)); // 从堆中申请空间,申请成功返回地址,失败返回NULL
if (p != NULL) {
free(p);
p = NULL// 【很重要】释放完,赋值为NULL
}

补充:

  • 第五行,null 代表着,该指针被释放;未被赋值为 null,代表着指针所指空间未被释放。
  • 未被赋值为 null 的指针,容易造成二次释放。已释放的空间,二次释放,将出现错误。

12.堆区空间越界

malloc 分配的空间 < 操作的空间,会发生内存污染。

1
2
char *p = (char*) malloc(1);
strcpy(p, "123abc"); // 堆区空间越界

13.堆区数组

1
2
3
4
int *p = NULL;
p = (int *)malloc(10 * sizeof(int)); // 连续申请10个 int 大小空间,p指向首地址
p[0] = 1; // 等价于 *(p + 0)
p[1] = 2; // 等价于 *(p + 1)

14.内存污染|返回栈区地址

(1)问题

函数执行完,自动释放栈区内存空间,(可以返回结果,不能返回空间地址)。

错误案例01:

1
2
3
4
5
6
7
8
9
int *fun() {
int a = 10;
return &a;
}

int main() {
int *p = fun(); // 程序执行完,自动释放内存空间,此时p为野指针
*p = 20; //error // 操作野指针
}

错误案例02:

1
2
3
4
5
6
7
8
9
10
char *get_char() {
char str[] = "abcdef"; // 从文字常量区,将 "abcdef" 拷贝到栈区
return str;
}

int main() {
char buf[100] = {0};
strcpy(buf, get_str()); // error //程序执行完,返回的 str 内存空间被自动释放
printf("%s", buf); // 打印出来的将是乱码
}

(2)改进

将函数体内局部变量定义为 static ,储存在 data 区,程序结束前,不会释放。

1
2
3
4
5
6
7
8
9
int *fun() {
static int a = 10;
return &a;
}

int main() {
int *p = fun();
*p = 20; // 合法!
}

15.内存泄漏|值传递

(1)问题

传递的是指针的值(NULL),函数中申请堆区空间,tmp 指向堆区,但是 p 并没有指向堆区。

1
2
3
4
5
6
7
8
9
10
void fun(int *tmp) {
tmp = (int *)malloc(sizeof(int)); // 堆区空间,函数结束没释放,【内存泄漏】
*tmp = 10;
}

int main() {
int *p = NULL:
fun(p); // 传递的是指针p所储存的内容(NULL)
printf("%d", *p); // 此时p,还是空指针,会发生段错误,【内存污染】
}

(2)改进|返回堆区空间

1
2
3
4
5
6
7
8
9
10
11
int *fun(int *tmp) {
tmp = (int *)malloc(sizeof(int));
* tmp = 10;
return tmp; // 把堆区空间地址传回来,赋值给p
}

int main() {
int *p = NULL;
p = fun(p);
printf("%d", *p);
}

16.非法使用内存导致错误

非法使用指针,造成内存污染。例如:

1
2
char *p;
*p = "abc"; // 可能编译通过,随着代码量的增加,会出现断错误

17.如何避免非法使用内存

定义一个指针后,先指向一块内存再使用!

  1. 指向栈区内存(即指向普通变量)
1
2
3
char *p;
char s[] = "ab";
p = s;
  1. 指向堆区(malloc申请空间)

【堆区空间 记得 free()】

1
2
char *p;
p = (char *)malloc(sizeof(char) * 10);

3.指向文字常量区

1
2
char *p;
p = "ab";

018.结构体

复合类型

1.格式

  • 结构体类型定义,右括号有分号!
1
2
3
4
5
6
struct Student {
int age;
char name[64];
}; // 【重要】有分号!

// 结构体、联合体、枚举、do-while、typedef都有分号!
  • 和数组一样:
    • 结构体变量只有定义的时候可以初始化。
    • 初始化使用大括号,而且有分号。struct student s1 = {18, "Tom"};
  • 易错】没初始化,不能操作 字符数组名(line 4)
1
2
3
4
5
6
int main() {
struct Student s1;
s1.age = 18;
s1.name = "tom"; // error【重要】name是数组名(首地址),数组名是常量,不允许修改
strcpy(s1.name, "tom"); // 解决方案1 // 解决方案2:使用指针定义 name
}

2.不常用定义方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Student {
int age;
char name[64];
}s1, s2; // 定义两个结构体变量

-------------------------------------

struct Student {
int age;
char name[64];
}s1 = {18, "Tom"}; // 定义时初始化

-------------------------------------

struct {
int age;
char name[64];
}s3, s4; // 定义两个结构体变量,但此时结构体类型是一次性的,不能用它定义其他结构体变量

3.结构体数组初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Student {
int age;
char name[64];
}; // 【重要】有分号!

struct Student s[5] {
{18, "student_01"},
{19, "student_02"},
{20, "student_03"} // 最后没有逗号
};

struct Student double_s[5] {
18, "student_01",
19, "student_02",
20, "student_03" // 最后没有逗号
};

4.结构体嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 结构体类型定义
struct Score {
int chinese;
int math;
}

struct Student {
int age;
char name[64];
struct Score score; // 结构体嵌套。
}

// 初始化嵌套结构体
struct Student s = {90, 100, 18, "tom"} // 也就是chinese=90, math=100, age=18。
// 修改嵌套结构体数据
s.score.math = 110; // 点到嵌套结构体的最后一层。

5.结构体的值传递和地址传递

值传递,是直接内存拷贝,效率不高;地址传递效率更高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Student {
int age;
char name[64];
}; // 【重要】有分号!

// 利用指针进行地址传递
void fun(struct Student *p /*p指向s1所在内存*/) {
p->age = 10;
memcpy(p->name, "Tom", sizeof("Tom"));
}

int main() {
struct Student s1;
fun(&s1); // 地址传递
}

---------------------------------------------------

// 如果限制只读,可以用 const
void fun(const struct Student *p){}

6.结构体指针套一级指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Student {
int age;
char *name;
}; //有分号!

int main() {
struct Student s1;
s1.age = 18;
s1.name = "Tom"; // 可以这样赋值,“Tom” 在文字常量区,将 ”Tom“ 的首地址,赋值给指针 name
printf("%s", s1.name); // 用指针直接打印字符串


struct Student *p;
p = (struct Student *)malloc(sizeof(struct Student)); // 记得free()
p->age = 18;
strcpy(p->name, "tom"); // error // 只分配了 age大小(int,4字节) + name大小(指针),没有让name 指向内存空间,所以无法将“tom” 拷贝到此空间
p->name = "Tom"; // 解决方案,让name指向常量区
}

7.结构体指针数组套二级指针

用一个指针指向 导师结构体数组,表示有 teacher_num 个导师,一个导师有 student_num 个学生。用二级指针指向学生数组。

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
typedef struct Teacher {
char **student;
}Teacher;

Teacher *creatTeacher(int teacher_num) {
Teacher *p = (Teacher *)malloc(sizeof(Teacher) * teacher_num);
return p;
}

void creatStudent(Teacher **p, int teacher_num, int student_num) {

// 1. 对于每个结构体,申请空间,用来存储一级指针,一级指针指向 学生姓名字符数组。
for (int i = 0; i < teacher_num; i++) {
(*p)[i].student = (char **)malloc(sizeof(char *) * student_num);

// 2. 对于每个一级指针,申请空间,用来存储 学生姓名字符数组。
for (int j = 0; j < student_num; j++) {
(*p)[i].student[j] = (char *)malloc(30);

// 3. 为每个 学生姓名字符数组 赋值。
char tmp_name[30] = {0};
sprintf(tmp_name, "s_name_%d_%d", i, j);
strcpy((*p)[i].student[j], tmp_name);
}
}
return;
}

void printAllStudent(Teacher **p, int teacher_num, int student_num) {
int n = 0;
for (int i = 0; i < teacher_num; i++) {
for (int j = 0; j < student_num; j++) {
printf("第%d名,%s\n", n++ ,(*p)[i].student[j]);
}
}
return;
}

int freeAllSpace(Teacher **p, int teacher_num, int student_num) {
for (int i = 0; i < teacher_num; i++) {
for (int j = 0; j < student_num; j++) {
if ((*p)[i].student[j] != NULL) {
free((*p)[i].student[j]);
(*p)[i].student[j] = NULL;
}
}
if ((*p)[i].student != NULL) {
free((*p)[i].student);
(*p)[i].student = NULL;
}
}
if(*p != NULL) {
free(*p);
*p = NULL;
}
return 0;
}

int main() {
Teacher *p = NULL;
// 创建导师结构体数组
int teacher_num = 3;
p = creatTeacher(teacher_num);

// 为每个导师结构体中的 二级指针所指向的空间 申请空间并赋值
int student_num = 2;
creatStudent(&p, teacher_num, student_num);

// 遍历不同导师的不同学生
printAllStudent(&p, teacher_num, student_num);

// 释放空间
freeAllSpace(&p, teacher_num, student_num);
}

8.结构体的深拷贝与浅拷贝

  • 浅拷贝(Shallow Copy):结构体中嵌套指针,而且动态分配空间,同类型结构体变量赋值,不同结构体成员指针变量指向同一块内存。
  • 深拷贝(Deep Copy):人为申请空间,重新拷贝堆区内容。

浅拷贝

深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main() {
typedef struct Teacher {
char *course;
int score;
}Teacher;

Teacher t1;
t1.course = (char *)malloc(10);
t1.score = 90;
strcpy(t1.course, "math");

// 浅拷贝,对于指针所指向的内存空间不拷贝
Teacher t2 = t1;
}

9.结构体内存

由于结构体存在字节对齐,所以结构体变量的内存大小,大于其成员变量的内存大小之和。

**注意:**当结构体变量更改顺序时,所占内存大小不一样。

1
2
3
4
5
6
7
8
9
10
11
struct tmp1 {   // sizeof(tmp1) = 4+4
int a;
short b;
char c;
};

struct tmp2 { // sizeof(tmp2) = 4+4+4
short b;
int a;
char c;
};

(1)结构体偏移量

结构体类型定义下来,内部的成员变量的内存布局已经确定下来。

(2)结构体对齐规则

  • 为什么要对齐?

    便于查找,提高存取数据的速度,利用空间换时间。

    比如有的平台每次都是从偶地址处读取数据,对于一个int型的变量,若从偶地址单元处存放,则只需1个读取周期(以32位系统为例,CPU读取内存时,一次读取32位,4个字节)即可读取该变量;但是若从奇地址单元处存放,则需要2个读取周期读取该变量。

  • 结构体的对齐参数

    以结构体中最长字节的变量对齐。利用偏移量来对齐,偏移量按照类型大小成倍增加。

  • 举个例子

1
2
3
4
5
6
struct A {
char c; //1byte
double d; //8byte
short s; //2byte
int i; //4byte
};
  1. 系统默认以最长字节的类型的大小来对齐:8 个字节
变量偏移量 sizeof(类型) * 倍数所占位置首地址
char c1 * 0 = 011
double d8 * 1 = 89 - 169
short s2 * 8 = 1617 - 1817
int i4 * 5 = 2021 - 2421
  1. 字节对齐可以程序控制,采用指令。但对齐参数不能大于最长字节的类型大小。
1
2
3
4
5
6
#pragma pack(xx)
#pragma pack(1) //1字节对⻬齐
#pragma pack(2) //2字节对⻬齐
#pragma pack(4) //4字节对⻬齐
#pragma pack(8) //8字节对⻬齐
#pragma pack(16) //16字节对⻬齐
  1. 当系统以 4 个字节对齐

当最长类型大小 > 对齐参数,按照对齐参数计算偏移量。

变量偏移量 sizeof(类型) * 倍数所占位置首地址
char c1 * 0 = 011
double d4 * 1 = 4【最长类型大小 > 对齐参数】5 - 125
short s2 * 7 = 1415 - 1615
int i4 * 4 = 1617 - 2017
  1. 当系统以 1 个字节对齐

(3)特殊情况|当结构体成员为数组时

当结构体成员为数组时,并不是将整个数组当成一个成员来对待,而是 将数组的每个元素当一个成员来分配,其他分配规则不变。比如 int a[5] ,当作 5 个 int 类型来处理。

(4)特殊情况|当有不完整类型时

1
2
3
4
5
6
7
struct A {
int a1:5;
int a2:9;
char c;
int b:4;
short s;
};

【要点】:

  • 对于位段成员,依旧按类型分配空间。
  • 同类型的、相邻的位段成员,可当作一个类型变量来处理。比如 a1:5 a2:9 可以当作一个 int 变量处理。

系统默认以 4 个字节来对齐:

a1:5 表示 占 5bit,a2:9 表示 占 9bit,加起来一共 16bit,未超过 int 的 4 个字节。所以将 a1:5 a2:9 放在一个 int 空间。由于 b:4s 的类型不一样,所以不能放在一起,b:4 依旧占 4 个字节。

10.typedef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1.给 类型struct Stdent 取个别名 Student
typedef struct Student {
int age;
char *name;
}Student;

// 2.给int取别名
typedef int int64;

// 3.定义数组类型
typedef int A[8]; // A 代表一个类型,不是变量
A a; // 等价于 int a[8]

// 4.定义函数类型
typedef int(MY_FUNC)(int, int);

11.比较结构体是否相等

不可使用 memcmp 判断,因为用于内存对齐的几个字节是垃圾值。

使用重载 == 操作符。

019.共用体(联合体)、枚举

1.共用体

1
2
3
4
5
6
7
8
// a(1字节)、b(2字节)、c(4字节)共用一块内存(最大成员的内存)。
union Test {
unsigned char a;
unsigned short b;
unsigned int c;
};

union Test obj; //obj 内存大小为 4,因为成员中 int 最大,4字节。

2.枚举

1
2
3
enum {
red, write, blue // 最后没逗号
}; // 有分号!

020.文件操作

1.缓冲区

ANSI C标准采用“缓冲文件系统”处理数据文件。

(1)为什么要有缓冲区?

提高交互效率

fputs() 把内存的值写入文件:内存 -> 缓冲区 -> 缓冲区(缓冲区满了,或者程序结束) -> 屏幕(标准输出文件)。

fgets() 从文件读取数据:键盘(标准输入文件) -> 缓冲区 -> 缓冲区(缓冲区满了,或者程序结束)-> 内存。

(2)缓冲区写入文件的条件

  • 【标准设备文件】stdin stdout stderr 可以实时读写。

  • 【缓冲区满了】但是缓冲区不同系统大小不一样,缓冲区相当于一个固定大小的字符串。

  • 【程序结束】程序正常关闭,缓冲区的内容,自动写入文件。

  • 【关闭文件】fclose(fp) 文件正常关闭,缓冲区的内容,自动写入文件。

  • 【手动刷新缓冲区】fflush(fp) 仅限 Linux。文件不关闭,程序没结束,实时刷新缓冲区。

(3)刷新缓冲区|fflush()

1
int fflush(FILE *stream);

【linux系统下,刷新缓冲区】:当缓冲区积累的内容没有足够多,或者程序未结束,缓冲区的内容还未写入文件,需要手动刷新一下缓区。

1
2
3
4
5
6
7
8
char buf[] = "this is test";
FILE *fp = fopen("./test.txt", "w+");
fputs(buf, fp); // 此时写入了缓冲区,但是没有写入文件
//----------此时文件中并没有内容--------------
fflush(fp); //刷新缓冲区,只有在linux系统可以。

getchar(); // 程序未结束
fclose(fp); // 不要忘记关闭文件

2.文件指针

1
FILE *fp;
  • fp指针 是结构体指针,结构体内部是,若干个数据成员,保存文件状态等各种信息(不必关心内部)。
  • fp指针,调用 fopen() 时 ,在堆区分配空间,地址返回给 fp,【故 fp指针 不指向文件,而是指向堆区的结构体,该结构体用来储存文件状态等信息】。
  • 通过文件库函数操作 fp指针,来修改该结构体内部数据成员。
1
2
3
4
5
6
7
8
9
10
11
12
// 【文件句柄】了解就可以
typedef struct {
short level; /* 缓冲区"满"或者"空"的程度 */
unsigned flags; /* ⽂文件状态标志 */
char fd; /* ⽂文件描述符 */
unsigned char hold; /* 如⽆无缓冲区不读取字符 */
short bsize; /* 缓冲区的⼤小 */
unsigned char *buffer;/* 数据缓冲区的位置 */
unsigned ar; /* 指针,当前的指向 */
unsigned istemp; /* 临时⽂文件,指⽰示器 */
short token; /* ⽤用于有效性的检查 */
}FILE;

3.文件分类

(1)分类

  • 磁盘文件:硬盘中的文件
    • 文本文件(遇到 -1或特殊字符会结束)
    • 二进制文件
  • 设备文件
    • 键盘
    • 屏幕

(2)win 和 linux 文本文件的区别

  • win 文本文件的换行符是 “\r\n”
  • linux 文本文件的换行符是 “\n”

ps:所以记事本打开会不换行。

win 下读写 会自动转换换行符。

  • 在 win 下,读取文本文件时,系统将 “\r\n” 转化为 “\n”
  • 写入文件时,系统将 “\n” 转化为 “\r\n”

4.文件操作流程

  1. 打开文件fopen()
  2. 读写文件
    • 按 字符 读写 fgetc()、fputc()
    • 按 字符串(行)读取文件 fgets()、fputs()
    • 文件结尾判断 feof() 【end of file】
  3. 关闭文件 fclose()

5.标准设备文件指针

  • stdin:标准输入
  • stdout:标准输出
  • stderr:标准出错,perror 函数输出错误信息

补充:

  • fclose() 关闭文件
  • perror() 打印库函数调用失败原因
    • 失败:Bad file descriptor
    • 成功:Sucess 或者 不显示信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {

// 1.关闭 标准输出
printf("aa\n"); // 可以正常打印 aa
fclose(stdout); // 关闭标准输出文件
printf("bb\n"); // 无法打印出来

// 2.打印 库函数 调用失败原因
perror("my error"/*括号内为自定义的字符串*/); //my error: Bad file descriptor

// 3.关闭 标准输入
int a = 0;
fclose(stdin); // 关闭标准输出文件
scanf("%d", &a); // 无法从键盘读取数据, 此时 a 还是等于0

perror("my new error"); // my new error: Bad file descriptor
}

6.文件打开和关闭

(1)fopen()

fp指针,调用 fopen() 时 ,在堆区分配空间,地址返回给 fp。失败返回 NULL。

<1> 路径
1
2
3
4
5
6
7
8
9
10
11
12
FILE *fp = NULL;

// 仅 windows 可使用的路径
// 直接复制过来,是反斜杠,需要多加一个斜杠进行转义
fp = fopen("my_code\\hello\\hell.txt", "w");

// 推荐的路径格式
fp = fopen("my_code/hello/hell.txt", "w");

//用指针代替路径
char *p = "1.txt";
fp = fopen(p, "w");

**Tip:**如果路径字符串过长,可以用续行符。

1
2
3
char *p = "1234567"\               // 表示是一个完整的字符串。
"asdasfsdgasfdgs";
printf("%s", p);
<2> 权限

w:【写入】如果文件不存在,新建;如果文件存在,清空内容再打开。

r:【只读】如果文件不存在,打开失败。

a:【追加】如果文件不存在,新建;如果文件存在,光标自动放在文件最后末尾。

r+:【读写】允许读写,文件必须存在

w+:【读写】允许读写,文件不存在,则创建,文件存在,则清空

rb:window 平台下,r或者rt 读文本文件,rb 表示二进制文件【linux 写 rb 不影响】。

wb:window 平台下,w或者wt 写文本文件,wb 表示二进制文件【linux 写 wb 不影响】。

<3> 注意
  • 编译的同时运行程序,相对路径不一定相对于源代码路径。
  • 直接运行程序,相对路径是相对于可执行程序。

(2)fclose()

1
fclose(FILE *stream);

7.写文件|fputc()、fputs()

1
int fputc(int ch, FILE *stream);

将 ch 转换为 unsigned char 后,写入 stream 指定文件中。

  • ch:需要写入文件的字符
  • stream:文件指针

返回值:**

  • 成功:返回读到的字符
  • 失败:-1

流程:写入缓冲区 -> 写入硬盘中的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FILE *fp = NULL;
//1.打开文件
fp = fopen("1.txt", "w");

if(fp == NULL) { // 判断是否打开失败
perror(fopen); // 打印出错原因
return -1;
}

//2.写文件
fputc('a', fp);

//3.关闭文件
fclose(fp);

PS:写文件|fputs() 见 012.3 fputs()

8.读文件|fgetc()、fgets()

1
int fgetc(FILE *stream);

从 stream 指定的文件中读取一个字符。

返回值:

  • 成功:返回读到的字符
  • 失败:-1
1
2
3
4
5
6
7
8
9
10
11
// 从键盘上读数据
fgetc(stdin);

FILE *fp = fopen("1.txt", "r"); // 读文件,以只读的方式打开文件。

// 读取一个字符
char ch;
ch = fget(fp);
printf("%c", ch);
ch = fget(fp); // 多次读取字符,光标自动向后移动。
printf("%c", ch);

PS:读文件|fgets() 见 012.2 fgets()

当 fgets() 读到文件结尾,读取失败,保存上一次读取的值(按行读取,也就是上一行的值)。

9.判断文件结尾|feof()

feof() 任何文件都能判断。

文本文件,结尾有一个隐藏的 -1,因为ascii码中没有 -1 ,EOF(end of)的宏定义为 -1。

1
2
3
4
5
6
7
8
9
10
11
FILE *fp = fopen("1.txt", "r");

// 读取文件所有字符
while (1) {
char ch;
ch = fget(fp);
if (ch != -1) { // 当读到隐藏的 -1,就是 文件结尾。
break;
}
printf("%c", ch);
}

二进制文件不能用 -1 判断结尾。

任何文件都能判断是否结尾,如果到文件结尾,feof() 返回真。

1
2
3
4
5
6
7
8
9
10
11
FILE *fp = fopen("1.txt", "r");

// 读取文件所有字符
while (1) {
char ch;
ch = fget(fp);
if (feof(fp)) { // 如果到文件结尾,feof() 返回真。
break;
}
printf("%c", ch);
}

原理: 判断的是,光标前一个字符是否是最后一个字符。

  • 对于空文件,应该先读取,让光标后移,才能判断是否结尾。
  • 对于文本文件,读取到 隐藏字符 -1,feof() 才能判断结尾。
1
2
3
FILE *fp = fopen("1.txt", "r");   // 1.txt 为空文件

feof(fp); // 此时永远返回假

10.格式化|fprintf()

1
2
3
FILE *fp = fopen("1.txt", "w");     // 1.txt为空。
int num = 10;
fprintf(fp, "num = %d\n", &num); // 将 num 内容格式化写入文件。

11.格式化|fscanf()

1
2
3
FILE *fp = fopen("1.txt", "r+");   // 1.txt 中内容为 num = 10。
int num = 0;
fscanf(fp, "num = %d\n", &num); // 按照格式,读出数据 到num。
  • 读取失败时(读到末尾,边界值),自动保存上一次读取的值。
1
2
3
4
5
6
7
8
9
/*  1.txt内容如下
num = 99
num = 98
*/
FILE *fp = fopen("1.txt", "r+");
int num = 0;
fscanf(fp, "num = %d\n", &num); // num = 99
fscanf(fp, "num = %d\n", &num); // num = 98
fscanf(fp, "num = %d\n", &num); // num = 98 // 还是98,读取失败时,保存上一次读取的数据。

12.按块读写文件

(1)fwrite()

1
size_t fwrite(const *ptr, size_t size, size_t number, FILE *stream);

返回值:

  • 成功:返回写入内容的块数目 number,(不是文件总大小)。
  • 失败:返回 0。

参数:

  • ptr:准备写入文件数据的地址

  • size:往文件写入内容的块大小,数据类型的大小

  • number:往文件写入内容的块数目

  • stream:操作的文件指针

总大小:

size * number 是总大小,所以 size 和 number 的参数位置调换,没有影响。

1
2
3
4
char *p  = "Tom";
FILE *fp = fopen("1.txt", "w");
int ret = fwrite(p, sizeof(char), strlen(p), fp); // 写入成功,ret 返回的是 strlen(p) 的大小。
fclose(fp);

(2)fread()

**。返回值:**同 fwrite()

**参数:**同 fwrite()

注意:

  • 文件内容 > 用户指定总大小 size * number ,返回值为用户指定大小的块数目。
  • 文件内容 < 用户指定总大小 size * number ,返回值为实际读取的块数目,或者为 0(比如,number = 0.2时,返回为 0 )。
1
2
3
4
char s[100] = {0};
FILE *fp = fopen("1.txt", "r");
int ret = fread(s, sizeof(char), 50 /* 假如文件中有50个字符 */, fp);
fclose(fp);

(3)返回值与边界问题

对于二进制文件,所读取的字符串,不要用 strlen() 判断长度。

  1. 读文件时,应该用 ret == 0 做是否读到的判断,不应该用 strlen() == 0 做是否读到的判断,因为有些特殊字符会导致出现 ‘/0’
1
2
3
4
5
char a[100] = {0};
int ret = fread(a, sizeof(char), sizof(a), r_fp);
if (ret == 0) {
break;
}
  1. 对读到的字符串进行处理,应该用 i < ret 做循环,不应该用 i < strlen() 做循环,因为有些特殊字符会导致出现 ‘/0’
1
2
3
for (int i =0; i < ret; i++) {
a[i] = a[i] + 1;
}
  1. 写文件时,应该读多少写多少,避免越界。
1
2
3
4
5
int ret = fread(a, sizeof(char), sizof(a), r_fp);
fwrite(a, 1, ret, w_fp);
/*
fwrite(a, 1, sizeof(a), w_fp); // 错误:sizeof(a) 有可能大于 ret,产生越界
*/

13.光标移动|fseek()

**功能:**移动文件光标

1
int fseek(FILE *stream, long offset, int whence);

参数:

  • offset:偏移量
  • whence:
    • SEEK_SET:从文件开头移动 offset 个字节。
    • SEEK_CUR:从当前位置移动 offset 个字节。
    • SEEK_END:从文件末尾移动 offset 个字节。
1
2
3
4
fseek(fp, 0, SEEK_SET);    // 光标回到 开头偏移 0个字节的位置【光标回到开头】
fseek(fp, 100, SEEK_SET); // 光标回到 开头偏移 100个字节的位置
fseek(fp, 0, SEEK_END); // 光标回到 末尾偏移 0个字节的位置【回到末尾】
fseek(fp, -1, SEEK_END); // 光标回到 倒数第二个位置。

14.获取光标位置|ftell()

返回的长整型为,光标距离开头的位置。

1
long ftell(FILE *stream);

15.光标回到开头|rewind()

1
void rewind(FILE *stream);

16.应用:获取文件大小

  1. 读方式打开文件。
  2. 光标移到文件结尾。
  3. 返回文件位置。
  4. 关闭文件 或者 恢复光标
1
2
3
4
5
6
7
8
9
10
11
12
// 1. 读方式打开文件。
fp = fopen("test.txt", "rb+");

// 2. 光标移到文件结尾。
fseek(fp, 0, SEEK_END);

// 3. 返回文件位置。
long size = ftell(fp);

// 4. 关闭文件 或者 恢复光标
rewind(fp);
fclose(fp);

021.链表

1.链表和数组的区别

链表:

  • 不需要一块连续的储存区域
  • 删除和插入元素高效

数组:

  • 随机访问元素效率高

2.分类

(1)动态链表和静态链表

类似于,动态数组和静态数组,储存结构的两种不同表示方式。

1
2
3
4
// 动态数组
int *p = malloc(10 * sizeof(int));
// 静态数组
int a[10];

**静态链表:**所有结点程序定义,不是临时申请的,无需释放。

**动态链表:**一个个临时申请的结点和输出的数据。建立的前后相连的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 静态
struct structure {
int num;
struct structure *p;
}

int main() {
struct structure s1, s2, *head;
head = &s1;
s1.num = 1;
s1.p = &s2;
s2.num = 2;
s2.p = NULL;
}

(2)带头链表和不带头链表

**带头链表:**头结点固定,储存的并不是有效数据,可能结点的个数。有效结点从第二个开始,若需在头部插入新结点,插在头结点和第二个结点之间即可。

**不带头结点:**头结点不固定,若需在头部插入新结点,新结点指针指向头结点即可。

(3)单向链表和双向链表

(4)循环链表

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

typedef struct Node {
int data;
struct Node *next;
}Node;

// 创建一个带头结点的链表
Node *ListCreat() {
Node *head = NULL;
head = (Node *)malloc(sizeof(Node));
if (head == NULL) {
return NULL;
}
head->data = -1;
head->next = NULL;

Node *current = head;
Node *new_ptr = NULL;
int data = 0;

while (1) {
puts("input your data please:");
scanf("%d", &data);
if (data == -1) {
break;
}
new_ptr = (struct Node *)malloc(sizeof(struct Node));
if (new_ptr == NULL) {
continue;
}
current->next = new_ptr;
new_ptr->data = data;
new_ptr->next = NULL;
current = new_ptr;
}

return head;
}

// 打印链表
void ListShow(Node *head) {
if(head == NULL) {
return;
}
Node *current = head->next;
while (current != NULL) {
printf("%d\n",current->data);
current = current->next;
}
return;
}

// 插入数据
// 在第一个 place_data 的前面插入 new_data
int ListInset(Node *head, int place_data, int new_data) {
if (head == NULL) {
return -1;
}
Node *aim_ptr = NULL;
Node *current = head;
while (current->next != NULL) {
if (current->next->data == place_data) {
aim_ptr = current;
break;
}
current = current->next;
}
Node *new_ptr = (Node *)malloc(sizeof(Node));
new_ptr->data = new_data;
if (aim_ptr == NULL) {
new_ptr->next = NULL;
current->next = new_ptr;
return 0;
} else {
new_ptr->next = aim_ptr->next;
aim_ptr->next = new_ptr;
}
return 0;
}

// 删除结点
// 删除第一个 delete_data
int ListDelete(Node *head, int delete_data) {
if (head == NULL) {
return -1;
}
Node *current = head;
while(current->next != NULL) {
if(current->next->data == delete_data) {
Node *tmp = current->next;
current->next = tmp->next;
tmp->next = NULL;
free(tmp);
tmp = NULL;
break;
}
current = current->next;
}
return 0;
}

// 清空链表
Node *ListClean(Node *head) {
Node *tmp = NULL;
while (head != NULL) {
tmp = head->next;
free(head);
head = tmp;
}
return head;
}

int main() {
Node *head = NULL;
// 创建
puts("ListCreat:");
head = ListCreat();
ListShow(head);
// 插入
puts("ListInset:");
ListInset(head, 5, 4);
ListShow(head);
// 删除
puts("ListDelete:");
ListDelete(head, 6);
ListShow(head);
// 清空
puts("ListClean:");
head = ListClean(head);
ListShow(head);
}

4.交换节点

交换节点 = 交换结构体储存内容 + 交换指针指向。

1
2
3
4
5
6
7
8
9
// 1.交换整个结构体内容:数据域和指针域
tmp = *pCur;
*pCur = *pPre;
*pPre = tmp;

// 2.交换指针指向
tmp.next = pCur->next;
pCur->next = pPre->next;
pPre->next = tmp.next;

PS:如果数据域只有一个,直接交换数据域。

1
2
3
tmp = p1->data;
p1->data = p2->data;
p2->data = tmp;

PSS:如果数据域有多个,可以封装成一个结构体 data,在交换 data。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Node {
struct Data{
int id;
char name[64];
int num;
} data;

Node *next;
};

struct Data tmp;

tmp = p1->data;
p1->data = p2->data;
p2->data = tmp;

5.链表反转

  • 先用 p_next 保存第 3 个节点地址。
  • 在将第 2 个节点(p_cur)的指针域指向第 1 个节点(p_pre)。
  • p_pre、p_cur 整体后移
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int ListNodeReverse(Node *head) {
if (head == NULL || head->next == NULL || head->next->next == NULL) {
return -1;
}

Node *p_pre = head->next;
Node *p_cur = p_pre->next;
Node *p_next = NULL;

while (p_cur != NULL) {
p_next = p_cur->next;
p_cur->next = p_pre;
// p_pre、p_cur 整体后移
p_pre = p_cur;
p_cur = p_next;
}
// 更改 head 指向。
head->next->next = NULL;
head->next = p_pre;
return 0;
}

022.常见错误

1. Segmentation Fault

段错误,主要原因是 操纵了非法内存

常见情景有:

  • 数组越界
1
2
int a[10];
a[10] = 10; // error
  • 链表越界
1
2
head = NULL;
head->next = NULL;
  • 指针操纵非法内存
1
2
int *p = NULL;
printf("%d", *p);

查漏补缺|C 语言
https://www.aimtao.net/c/
Posted on
2020-02-22
Licensed under