Lazy loaded image
📘C语言进阶笔记
字数 25857阅读时长 65 分钟
2024-7-24
2025-8-23
😀
介绍
这些是我在学习C语言的过程中自己练习的一些题目以及个人笔记

一、深度剖析数据在内存中的存储

本章重点
  1. 数据类型详细介绍
  1. 整形在内存中的存储:原码、反码、补码
  1. 大小端字节序介绍及判断
  1. 浮点型在内存中的存储解析

1. 数据类型的介绍

  • char 1字节
  • short 2字节
  • int 4字节
  • long 在64位中占8字节 在32位系统重占4字节
  • long long C99中引入 8字节
  • float 4字节
  • double 8字节
类型的意义:
  1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)
  1. 类型决定如何看待内存空间的视角

1.1 类型的基本归类

整形家族
  • char
  • unsigned char
  • signed char
  • short
  • unsigned short [int]
  • signed short [int]
  • int
  • unsigned int
  • signed int
  • long
  • unsigned long [int]
  • signed long [int]
  • long long double
  • 他们在头文件limits.h的头文件中
其中cahr 字符的本质是ASCII值,是整形 unsigned 为无符号 signed 为有符号 默认为有符号的。
在使用unsigned时,32为bit都可以用来做数值位置。 原来的符号位不用了。符号位为1=2^31
浮点型家族
  • float
  • double
  • long double
  • 他们在float.h的头文件中
其中float精度低。存储数值范围小 double精度高。存储的数值范围更大
构造类型
  • 数组类型
  • 结构体类型 struct
  • 枚举类型 enum
  • 联合类型 union
指针类型
  • int* pi
  • cahr* pi
  • float* pi
  • void* pi
等等都是指针类型
空类型
void 表示空类型(无类型) 通常应用于函数的返回类型、函数的参数、指针类型。
  1. 函数的在定义式用void。表示函数不会返回值
  1. 函数的参数为void。表示函数不需要 传参,你别给我传!(强行传会警告)

2.整形在内存中的存储

2.1原码、反码、补码

计算机中的整数有三种2进制表示方法,即原码、反码和补码。 三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正“,用1表示“负”,而数值位正数的原、反、补码都相同。 负整数的三种表示方法各不相同。 原码 直接将数值按照正负数的形式翻译成二进制就可以得到原码。 反码 将原码的符号位不变,其他位依次按位取反就可以得到反码。 补码 反码+1就得到补码。
为什么要用补码来存储呢? 在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理; 同时,加法和减法也可以统一处理(CPU只有加法器)(-1和1补码相加=0000..0000) 此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。 (补码也可以取反+1得到原码。他们的计算过程是相同的)

2.2大小端介绍

我们可以看到整形存储在内存中的补码。但是我们发现内存上顺序有点不对劲。是从右往左的 这是又为什么?
什么大端小端: 大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中; 小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
左边为低地址,右边为高地址 。 字节序是以字节为单位进行排序。 比如a= 0x 11 22 33 44
但是存储在内存中的是11 22 33 44 就叫做大端字节序存储 (在码中左边的为高位) VS中存储的为小端的存储模式、 他会倒着存进去
大端【字节序】存储: 把一个数据的高位字节序的内容存放在低地址处,把低位字节序的内容放在高地址处,就是大端字节序存储。 小端【字节序】存储: 把一个数据的高位字节序的内容存放在高地址处,把低位字节序的内容放在低地址处,就是小端字节序存储。

为什么有大小端?

为什么会有大小端模式之分呢?因为在许算机系统举,我是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。 例如:一个16bit的 short型x,在内存中的地址为0x0010,x的值为 0x1122,那么 0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即 0x0011中。小端模式,刚好相反。我们常用的x86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
和硬件有关。

写一个判断大小端的程序

这里考察对申请内存和指针操作权限的理解

整形存储 练习

一、数据截断、整型提升规则
char:通常是一个 8 位的有符号整数,范围是 -128 到 127 。
unsigned char:8 位的无符号整数,范围是 0 到 255 。
short:通常是 16 位的有符号整数,范围是 -32768 到 32767 。
unsigned short:16 位的无符号整数,范围是 0 到 65535 。
int:在大多数系统中是 32 位的有符号整数,范围是 -2147483648 到 2147483647 。
unsigned int:32 位的无符号整数,范围是 0 到 4294967295 。
long:在 32 位系统中通常与 int 范围相同;在 64 位系统中,通常是 64 位的有符号整数,范围更大。
unsigned long:对应的无符号类型。
long long:64 位的有符号整数,范围是 -9223372036854775808 到 9223372036854775807 。
unsigned long long:64 位的无符号整数,范围是 0 到 18446744073709551615 。
二、在进行整形提升的时候, 要根据数据的类型来判断补0 还是补 1
三、结果一样。
四、注意类型转换。强制提升并不会影响最后以% 啥的形式打印
五、-1在无符号打印为很大的正数
六、字符串求长度 和char 赋值
七、unsigned char的取值范围为0-255
八、库函数strlen 返回值为无符号数

3.浮点型在内存中的存储

常见的浮点数
3.14 1E10 1.0*10^10

3.1浮点数存储的一个例子

整形和浮点型在存储和获取值时他们的方法是不一样的

3.2 浮点数存储规则

根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式
  • V = -(1)^2 ×M×2^E
  • (-1)^S表示符号位,所以当S=0,V为正数; 当S=1,V为负数。
  • M表示有效数字,大于等于1,小于2。
  • 2^E表示指数位。
比如V = 5.0 f
在二进制中表示的形式是101.0
他等价于1.01 * 2^2
= (-1)^0 × 1.01 × 2^2
再举例子 V = 9.5f 那么他的二进制是1001.1 (小数点后的1 位 2的 -1 次方)
=(-1)^0 × 1.0011 × 2^3
所以S = 0 M=1.0011 E=3
我们也由此可以了解浮点数的精度问题
例如V = 1.6 那么他的二进制数为1.1XXX 后面的XX是因为后面的2的n次方。但是float只能存四位字节。即使是double也才能存8个字节。 没办法精确的等于0.1 这个值。所以精度会丢失
IEEE 754规定: 对于32位的浮点数, 最高的1位是符号位S,紧接着的8位是指数E,剩下的23位为有效数字M
对于64位的浮点数,
最高的1位是符号位S,紧接着的11位是指数E,剩下的23位为有效数字M
补零补后边
IEEE 754对有效数字M和指数E,还有一些特别规定。
  1. 对于数字M
    1. 前面说过,1≤M<2,也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分
      IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位 将第一位的1舍去以后,等于可以保存24位有效数字。 这样存储数据的精度就会更高
  1. 对于数字E,情况就比较复杂(E有可能是负数)
    1. 首先,E为一个无符号整数(unsignedint)这意味着,如果E为8位I它的取值范围为0255;如果E为11位,它的取值范围为02047。但是,我们知道,科学计数法中的E是可以出现负数的,
      所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。
      真实值(E)+中间值 ( 172 / 1023 )
      比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001.
然后,指数E从内存中取出还可以再分成三种情况:
  1. E不全为0或不全为1
    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位00000000000000,则其二进制表示形式为:0 01111110 00000000000000000000000
  1. E全为0
    1. 这时,浮点数的指数E等于1-127(或者1-1023)即为真实值有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示+0,以及接近于0的很小的数字。
  1. E为全1
    1. 这时,如果有效数字M全为0,表示+无穷大(正负取决于符号位s)

二、指针的进阶

本章重点
  1. 字符指针
  1. 数组指针
  1. 指针数组
  1. 数组传参和指针传参
  1. 函数指针
  1. 函数指针数组
  1. 指向函数指针数组的指针
  1. 回调函数
  1. 指针和数组面试题的解析
指针的主题,我们在初级阶段的《指针》章节已经接触过了,我们知道了指针的概念 1.擂针就是个变量,用来存放地址,地址唯一标识一块内存空间。 2.指针的大小是固定的4/8个字节(32位平台/64位平台)。 3.指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。 4.指针的运算。

字符指针

在指针的类型中有一个char*
正常使用不说了
他也可以这样使用
这里注意,hello并不是存到了指针变量p中。而是p保存了首字母h的地址。
这里使用const是为了保护字符串。 如果const不用其实也改不了。 不过编译器会报错
一道面试题

指针数组

是用来存放指针的数组
指针数组来关联三个数组的代码示例

数组指针

数组指针是指针。举例
p1 为指针数组, 数组里面有是个指针 int*类型的 有[10]个元素
p2 为数组指针, 指针指向的是一个有十个元素数组
数组指针是存放数组的地址,使用方法是 **int(*p2)[10] = &arr 他的类型是int* (*) [10]**
再次讨论数组名
int arr [10] = { 0 };
数组名arr通常表示的都是数组首元素的地址
除了两个例外
  1. sizeof()内 他计算的是整个数组的大小。
  1. 使用&arr 他取出的是整个数组的地址。 (如果你对他+1 则会跳过整个数组

利用数组指针来输出一个二维数组的内容

学习了这些知识。理解一下下面的代码

数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
4.1 一维数组传参!
以上都可以。在数组传参的时候。可以以数组【】接收。也可以用指针来接收
4.2 二维数组传参!
4.3 一级指针传参
传指针。用指针接收
形参部分是 整形指针的时候 可以传整形变量的地址、整形指针、整形数组名、 只要传过去的本质是一个整形指针
4.4二级指针传参
传二级指针。用二级指针接收
形参部分是 整形的二级指针的时候。 可以传二级指针变量的地址 。也可以取一级指针变量的地址传过去、 指针数组也可以传过去。它相当于指针数组的首地址。
函数指针

函数指针

什么是函数指针

函数指针的本质是 指向函数的指针
函数的名字就是他的地址。和数组一样
用指针这样指向函数。和数组指针类似。

两段代码加深函数指针的理解

第一个代码 void(*)()是一个函数指针类型。他把0强制转换为void函数指针 然后0的地址的函数进行调用。
第二个代码
首先,从最内层开始分析。
  1. 因为signal左边的 没有被括起来。所以他signal是和右边绑定的。 所以signal ( int, void ( * ) ( int ) ) 是一个小块儿。 其中signal 是一个函数名:int 表示这个函数的第一个参数是一个整数类型。 void ( * ) ( int ) 表示这个函数的第二个参数是一个指向函数的指针变量,该函数接受一个整形参数并且没有返回值。(就像我们接受一个整形变量一样 int *
  1. 可以看到。函数signal ( int, void ( * ) ( int ) ) 的外边又被一层:void ( * ) ( int )所包住。 而位于的括号内是用来存放函数指针变量的。所以 signal 函数的返回值是一个函数指针变量。比如返回的函数指针变量是p 那么 外边的一层就等于void ( *p ) ( int ) 就可以表示p所指向的函数是一个接受int型变量,没有返回的函数。
  1. 所以整个 void ( * signal ( int, void ( * ) ( int ) ) ) ( int ); 表示 signal 函数的返回值是一个函数指针变量,返回的函数指针变量所指向的函数是一个接受一个整数参数并且没有返回值。
  1. 最后 void (*signal(int, void (*)(int)))(int);表示整个signal函数的返回类型是一个函数指针,该指针指向一个函数,这个函数接受一个整型参数并返回void
简化声明使用typedef
为了提高代码的可读性,我们可以使用typedef来简化signal函数的声明:
这里,acc是一个新的类型名,代表了一个函数指针类型,指向一个接受整型参数且返回void的函数。使用typedef使得signal函数的声明更加清晰,提高了代码的可读性。

使用回调函数,在函数指针在函数内部调用其他函数来实现+ - * 计算器

函数中利用函数指针调用其他函数叫做函数回调。

函数指针数组

有套娃了 233
类比一下吧,
指针数组是 int* arr1[5]; cahr* arr2[6];
函数指针是 int (*pf) (int, int);
所以函数指针数组是把指针变量pf变成指针数组arr[] 也就是int (*arr[4]) (int, int);
那上小节的计算器举例。可以更改成这样子
可以看到,利用函数指针数组极大地增大了简洁性。 以及可拓展性
这个又叫做转移表 因为他利用函数指针数组的下标来在不同的函数之间跳转

指向函数指针数组的指针

指向函数指针数组的指针是一个指针
指针指向一个数组,数组的元素都是函数
这是一个函数指针数组int (*pfArr[5])(int, int) = { 0, add,sub,mul,div };
我们要定义一个指向这个指针数组的指针。
那么我们的指针的类型 就是 数组+函数指针
数组则是[5] 函数指针类型则是int (*) (int ,int) 所以就定义一个指针变量 ppArr 他得是数组所以加[5]ppArr[]
pparr[] 又要指向函数指针类型 int (*) (int ,int)
所以他最终为int (*(*ppArr[5])) (int ,int)
其中ppArr* 表示 ppArr 是一个数组,数组中的每个元素都是一个指针。

回调函数

回调函数是通过函数指针的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数。我们就说这个叫回调函数。 回调函数不是有该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或者条件进行相应。

利用qsort来实现int数组的升序/降序排列

qsort 头文件是 stdlib.h
  1. void指针:
      • 明白void指针可以接受任意类型的数据。
      • 清楚void指针在使用前需要进行强制类型转换才能进行解引用和算术运算。
  1. qsort函数的参数,包括要排序数据的起始位置、元素个数、元素大小以及自定义的比较函数指针。 int(*cmp)(const void* e1, const void* e2)

自己写一个qsort,然后实现结构体内数字、字符串、和int型排序

这段 C 语言代码主要实现了自定义的排序算法和对不同数据类型(整型数组、结构体数组)的排序操作。
1. 比较函数 cmp_int: - 这个函数用于比较两个整数。它通过将两个指针转换为 int 类型,然后计算差值来确定大小关系。
2. test1 函数: - 定义了一个整数数组 arr,并通过 qsort 函数对其进行升序排序。 - qsort 函数需要四个参数:待排序的数组、数组元素个数、每个元素的大小以及比较函数的指针。
3. 结构体 Stu 的定义: - 包含一个字符数组 name 用于存储姓名和一个整数 age 表示年龄。
4. Swap 函数: - 用于交换两个元素,通过逐个字节交换来实现不同类型元素的交换。
5. bubble_sort 函数: - 这是一个冒泡排序函数,它接受一个通用的指针 base、元素个数 sz、每个元素的宽度 width 和比较函数的指针 cmp。 - 在函数内部,通过循环比较相邻元素并根据比较结果进行交换。
6. 比较函数 cmp_stu_by_namecmp_stu_by_age: - cmp_stu_by_name 用于按照结构体中 name 字段的字符串进行比较。 - cmp_stu_by_age 用于按照结构体中 age 字段的整数值进行比较。
7. test2 函数: - 定义了一个结构体数组 s,并使用 qsort 函数根据年龄进行排序。
8. test3 函数: - 对整数数组使用自定义的冒泡排序函数 bubble_sort 进行排序。
9. test4 函数: - 对结构体数组使用自定义的冒泡排序函数 bubble_sort 根据姓名进行排序。

数组名的理解

总结
sizeof中只有数组名单独出现才是代表整个数组,有别的都不行其他都是首元素地址 sizeof只关注类型。不关注数值
1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。 2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。 3.除此之外所有的数组名都表示首元素的地址。

指针笔试题

三、字符函数和字符串函数

本章重点 重点介绍处理字符和字符串的库函数的使用和注意事项
求字符串长度 strlen
长度不受限制的字符串函数 strcpy strcat strcmp
长度受限制的字符串函数介绍 strncpy strncat strncmp
字符串查找 strstr strtok
错误信息报告 strierror字符操作
内存操作函数 memcpy memmove memset memcmp

函数介绍

头文件,<string.h>

strlen

用来求字符串的长度。计算\0之前的字符数量。
他的反回蚀sze_t。是无符号的 所以不要去进行相减的操作, 对于无符号数字 3-5 的值是大于0的
char arr[] = {‘a’,‘b’,‘c’} 是没有\0char arr[] = “abc” 是有\0

strcpy

char* strcpy(char *destination, const char *source)
意思是把源头的字符串拷贝到目标字符串 返回目标空间的起始地址 传入的是字符串的指针。cahr数组 数组名字就是地址~ 要注意在传入的时候源数据里必须要有\0 并且目标数组要放得下原数组内的东西 并且目标空间要能够被修改。
  • 源字符串必须以’\0’结束。
  • 会将源字符串中的’\0’拷贝到目标空间。
  • 目标空间必须足够大,以确保能存放源字符串。
  • 目标空间必须可变。
  • 学会模拟实现。

strcat

char* strcpy(char *destination, const char *source) 两个参数目标地址和要追加的字符的地址。 返回目标空间的起始地址
字符串追加 函数
使用的注意事项
  • 要追加的目标要有足够大的空间来存放追加的字符串
  • 追加的字符串和被追加的字符串都要有\0
  • 被追加的空间要能够被修改
模拟一下

strcmp

标准规定: 第一个字符串大于第二个字符串,则返回大于0的数字第一个字符串等于第二个字符串,则返回0 第一个字符串小于第二个字符串,则返回小于0的数字
这里的比较不是比较字符串长度,而是按位比较每位字节的大小(ascii码)
就比如abcq 比abdcef 大!
模拟实现strcmp

strncpy、strncat、strncmp

strcpy、strcat、strcmp是不受限制的。如果目标承受不了会报错的

所以我们可以使用长度受限制的函数: strncpy、strncat、strncmp 、他们在传参的时候回多一个无符号整形 num 。 意为cpy几个字符。追加几个字符。比较几个字符

strstr

查找子串的一个函数
给一个字符串,在这个字符串里查找另一个字符串
返回这个子串的第一个字符的地址 char*的指针。
找不到则返回空指针NULL
模拟实现。
(但这个不是效率最高的。)
效率高的有KMP算法 这个算法也是用来实现在一个字符串中查找子字符串的 效率高,但是实现难度大 B站搜索:比特大博哥,可以找到KMP算法的实现

strtok

char * strtok(char* *str,const char** sep);
  • 这个函数是用来 切割字符串用的。 返回分隔符号前的字符串的第一个字符的地址
  • 但是需要注意这个函数是会改原字符串的,他会在你定义的分隔符的地方补\0 所以在使用的时候要临时拷贝一份字符串,来对拷贝的字符串进行操作。不影响原字符串。
  • 这个函数。传入的第一个参数是待查找的字符串、第二个参数是可用作分隔符的一个字符集合 字符集合这样写的 “@.” 这里面俩都是可以用作字符集合的。可以自己添加。
  • 第一个参数传参的时候。传入的第一个变量有两种情况。
    • 第一种情况是:传入的变量是一个字符串地址,那么他会从字符串第一个字符开始查找,一直到找到分隔符号才停止。然后把这个分隔符变为\0 然后返回\0 左边的这个字符串的第一个元素的地址。首地址。并且保存这次的分隔符号的地址。(是的,他是一个有记忆的函数。)
    • 第二种情况是:传入的变量是一个空指针。那么他会从他上次记忆的分隔符号的地址的下一个地址开始找分隔符。直到找到一个分隔符,记住他的地址。然后替换为\0. 并返回这个这次开始查找到的字符串的首元素地址。
    • 我们可以使用for循环来用这个函数。不用写的一列一列的。

strerror

设置错误码函数
C语言的库函数,在执行失败的时候,都会设置错误码函数。0 1 2 3 4 5 6 7 8 都是错误码
函数会自动设置。
使用示例,

字符分类函数 <ctype.h>

该函数检查所传的字符是否是字母和数字。
该函数检查所传的字符是否是字母。
该函数检查所传的字符是否是控制字符。
该函数检查所传的字符是否是十进制数字。
该函数检查所传的字符是否有图形表示法。
该函数检查所传的字符是否是小写字母。
该函数检查所传的字符是否是可打印的。
该函数检查所传的字符是否是标点符号字符。
该函数检查所传的字符是否是空白字符。
该函数检查所传的字符是否是大写字母。
该函数检查所传的字符是否是十六进制数字。

标准库还包含了两个转换函数,它们接受并返回一个 “int”
该函数把大写字母转换为小写字母。
该函数把小写字母转换为大写字母。

memcpy <string.h>

=memory + copy
他可以直接把内存中的数据拷贝。 而不是像“strcpy和strncpy”一样只能拷贝字符串
void * memepy (void * destination, const vo1d source, size_t num ); 目标内存地址,源空间内存地址。拷贝的字节数量
返回目标空间的起始地址
他是用来操作两个独立的内存空间的。把一个复制到另一个
他在使用时最好不用对对重叠空间进行拷贝。有memmove来进行拷贝。 但是你要是真的拿他来对他进行重叠空间的操作,也是可以的
自己写一个

memmove

void * memepy (void * destination, const vo1d source, size_t num );
参数都是和上面的memmove一样的。但是她是可以对重叠空间进行移动的。、
模拟实现一下。

memcom

void * memepy (const void * ptr1, const vo1d ptr2, size_t num );
比较ptr1和ptr2开始 的 向后的num个字节的内容进行比较
如果大于则返回>0的数字。 = 返回0 小于则返回<0 的数字

memset

void *memset( void *ptr, int value, size t num );
第一个参数:传入一个地址 第二个参数:要设置的值 第三个参数:设置的长度
用法。只能在char字符串中,五个字符改成x 不能来改int 。 他会改每个字节为x(的ascii码)

一些作业题

把奇数变1 把偶数变 0
1 2 3 = 1 0 1
22222 = 0
实现一个函数。可以左旋字符串中k个字符
杨氏矩阵找数字
先输入矩阵的列、在输入行、然后按照行再列的方式已打印

四、自定义类型详解(结构体、枚举、联合)

本章重点
  • 结构体
    • 结构体类型的声明
    • 结构的自引用
    • 结构体变量的定义和初始化
    • 结构体内存对齐
    • 结构体传参
    • 结构体实现位段(位段的填充&可移植性)
  • 枚举
    • 枚举类型的定义
    • 枚举的优点
    • 枚举的使用
  • 联合
    • 联合类型的定义
    • 联合的特点
    • 联合大小的计算

结构体struct

1 结构体的声明

结构体的基础知识: 结构是一些值的集合,这些值被称为成员变量,结构的成员可以是不同类型的值 结构体的变量是全局变量还是局部变量。取决于变量放的位置。

2 匿名结构体类型

没有名字的结构体叫匿名结构体类型。他只能用一次

3 结构体的自引用

数据结构 数据在内存中的存储结构
  • 线形
    • 顺序表
      • 在内存中连续存放。1 2 3 4 5
    • 链表
      • 让1能找到2 ,让2 能找到3. 3能找到4 4能找到5 这样叫链表 放下一个节点的地址。 这个结构体的一个部分存数据—-指针域 另一个存放另一个节点的地址——-指针域
  • 树形
    • 二叉树

4 结构体的初始化

不记了。跟二维数组那种差不多,嵌套的时候也是一样的。{ {} , x , }

5 结构体内存对齐

计算结构体的大小
结构体的成员相同,但是顺序不同 。他们所占用的字节大小也是不同的。
结构体的对齐就是探讨这方面的知识的。
结构体的一些成员。必须存储在某一些对齐的位置以上才可以
首先得掌握结构体的对齐规则:
  1. 第一个成员在与结构体变量偏移量为0的地址处。
  1. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=编译器默认的一个对齐数 与 该成员大小的较小值。
    1. VS中默认的值为8
  1. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  1. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
使用offsetof可以查看结构体成员在结构中的偏移量
  • 为什么存在内存对齐?
大部分的参考资料都是如是说的:
  1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  1. 性能原因
    1. 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起
#pragma pack(8) //设置默认对齐为8

6 结构体传参

两种方法:传址和传值调用
传值调用时用 .
这个会对系统内存有较大开销
传址调用是用 ->
优选传址调用传址调用,并且在后边加上const来保证参数不被修改

7 位段

什么是位段
位段的声明:
位段成员必须是整形家族
位段的成员后面有一个冒号和数字
:3 意思是给他分配 3 个 比特位 (例如存储真、假)
位段是一种节省空间的做法。在开辟时。他是看到是啥类型的就先开辟啥类型,同类型就往里边赛。如果不够就再开辟一个整形。
一般情况下位段是同一类型的东西放到一块儿~
位段是不跨平台的。 跨平台性不好。 1. int位段被当成无符号还是有符号,未被定义 2. 从左向右还是从右向左未被定义 3. 最大位数不确定。16位、32位、64位、最大数不一样 4. 对于无法容纳的成员,是舍弃还是利用。是重新分配 还是再次开辟不确定

静态通讯录的实现、自定义通讯录大小。名称年龄上限等…

Contacts.c
Contacts.h
游戏测试文件test.c

枚举menu

有些值是可以一一列举出来的。这时就能用枚举了。
和结构体类似,他的后面是 , 默认%d打印从 0 开始。他是一个图纸。不占用空间
只有当用这个图纸创建一个枚举变量的时候他才会像内存申请空间。
在 , 的后面加 数字 可以自定义他的数字 不是从 0 开始的, .
枚举的好处
  1. 增加代码的可读性和可维护性
  1. 和#define定义的标识符比较,枚举有类型检查,更加严禁
  1. 防止了命名污染
  1. 便于调试,用枚举的时候,不会替换
  1. 使用方便,一次可以定义多个常量

联合(共用体)union

联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,
特征是这些成员 共用同一块空间 (所以联合也叫共用体)
联合体的特点:
,联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。 并且他也存在对齐。 vs 默认的对齐是8。用的时候要除以一下最大的对齐数。 联合体在计算大小的时候也存在内存对齐。
他的大小是最大的那个成员。在用的时候只能同时一个人用。用完就会覆盖。
他们每个成员的地址是相同的。
用联合体的思想写一个判断大小端的函数

五、动态内存管理

本章重点:
  • 为什么存在动态内存管理
  • 动态内存管理的介绍
    • malloc
    • colloc
    • realloc
    • free
    • 他们申请的空间是在堆区里的
  • 常见的动态内存错误
  • 柔性数组

为什么存在动态内存管理

空间开辟大小是固定的。是不能改变的 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

动态内存管理的介绍

1、malloc <stdlib.h> 不会初始化

void* malloc(size_t size)
申请zize个字节的内存。 返回一个void*的指针,指向这个新开辟的内存的起始位置的地址。
如果开辟失败,就返回空指针。
如果size为0,这个是未被定义的。取决于编译器。
malloc的使用
使用完之后没有还,没有用free。 但是在程序关闭的时候,分配的空间也是会还回去的, 但倘若程序在一直走,并且没有还空间。他还不用。 那么这个空间 就叫做内存溢出了, 别人也用不了。
在C语言初阶笔记中,有这么一句话 堆区:它是由操作系统管理的, 但分配和释放操作由程序员通过代码显式完成, 通常使用malloc()、calloc()分配内存,使用free()释放内存。 堆内存地址由低到高增长,适合存储大数据或生命周期长的对象, 但需注意内存泄漏问题。 释放空间用free(p);

2、free <stdlib.h>

C语言提供的用来动态内存的释放和回事用的函数
void free(void* ptr); free函数用来释放动态开辟的内存。 如果参数 ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。 如果参数 ptr 是NULL指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h头文件中。
free 只能释放动态开辟出来的空间!!

3、calloc = malloc + memset 他会会初始化为0.

void* calloc(sizet num, size_t size);
num:要开辟几个元素
size:每个元素的大小
void* 返回开辟好的空间的起始地址
他在返回地址之前,会初始化
使用案例

4、realloc

realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,
那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。
那rea11oc 函数就可以做到对动态开辟内存大小的调整。 函数原型如下:
void* realloc(void* ptr, size_t size)
第一个参数:要修改的空间的起始地址。
第二个参数:希望把它调整为多大的新的空间。
返回的类型是新的地址,或者原地址不变。理由如下:
realloc在追加字节的时候有两种情况。 一种情况是在追加时。后面能用的是不够的。因为后面有正在使用的内存空间。如果使用就覆盖掉了
  • 这种情况。他会去找一片新的空间,然后把旧的空间的数据拷贝过来。然后返回新的地址。并且帮你把旧的空间释放掉。
    • 在接收新的地址的时候,不能用原来的指针变量来接收。因为当realloc申请的空间过大。会申请失败,realloc会返回空指针!!!这样就把原来的地址也忘了!
    • 要用新的指针来接收新的地址。在判断不是空指针后,在把新的地址覆盖到原来的地址上,能保证不忘记原来的地址!!
另一种该情况是空间是够得。后面没有正在使用的内存空间,直接扩容。地址不变。
realloc的使用

常见的动态内存错误

  1. 对NULL进行解引用操作 这里的p可能为空指针, 应该先判断再赋值
    1. 对动态开辟空间的越界访问 这里开辟了10个字节,但访问了11个字节
    1. 对非动态开辟内存使用free释放 这里的p不是动态开辟的内存。会报错
    1. 使用free释放一块动态开辟内存的一部分 这里的p在程序运行中移动了。不再指向起始位置,会报错
    1. 对同一块的动态内存多次释放 释放再次释放就会报错!所以在释放空间后,应该把p赋值为空指针, 如果不弄成空指针,p就是野指针!
    1. 动态内存开辟后忘记释放造成内存泄漏

    柔性数组

    柔性数组(flexible array) C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员
    有些编译器会报错,那么可以改成
    柔性数组的特点
    • 结构中的柔性数组成员前面必须至少一个其他成员
    • sizeof 返回的这种结构大小不包括柔性数组的内存。sizeof不会算柔性数组的内存进去
    • 包含柔性数组成员的结构应该用malloc()函数进行内存的动态分配, 并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
    柔性数组的优势
    第一个好处是:方便内存释放 如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。 第二个好处是:这样有利于访问速度 连续的内存有益于提高访问速度,有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

    六、语言文件操作

    本章重点
    1. 为什么使用文件
    1. 什么是文件
    1. 文件的打开和关闭
    1. 文件的顺序读写
    1. 文件的随机读写
    1. 文本文件和二进制文件
    1. 文件读取结束的判定
    1. 文件缓冲区

    1. 为什么使用文件

    使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。

    2.什么是文件

    但是在程序设计中,我们一般谈的文件有两种:程序文件和数据文件 (从文件功能的角度来分类的)
    程序文件比如,.c .exe等等..
    数据文件是程序在运行时读写(输入)的程序, 或者用来输出(写)内容的文件

    3.文件的打开和关闭

    操作文件的步骤

    1. 打卡文件
    1. 对文件进行读或者写
    1. 关闭文件

    文件指针

    缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”
    每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信
    (如文件的名字,文件状态及文件当前的位置等)。
    这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
    每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量, 并填充其中的信息 使用者不必关心细节。
    一般都是通过一个FLE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
    定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,
    通过文件指针变量能够找到与它关联的文件。
    也就是说,当你用FILE*的指针变量来指向一个文件的时候。实际上他指向的 是存在内存中的文件信息区。文件信息区会自动的 关联到你想指向的文件,然后来操作文件
    任何一个c程序,只要运行起来就会默认打开3个流:
    FILEstdin -标准输入流(键盘) FILE stdout -标准输出流(屏幕) FILE* stderr -标准错误流(屏幕)

    文件的打开和关闭

    文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。 在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。 ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。
    也就是说,当你用FILE*的指针变量来指向一个文件的时候。实际上他指向的 是存在内存中的文件信息区。文件信息区会自动的 关联到你想指向的文件,然后来操作文件
    打开 fopen
    FILE * fopen ( const char* *filename, const char** mode );
    filename为文件名
    mode 为打开模式(读或写)
    打开失败会返回空指针
    mode的参数可以为如下。记得带“”
    文件使用
    含义
    如果指定文件不存在
    “r”
    为了输入数据,打开已存在的文本文件
    出错
    “w”
    为了输出数据,打开一个文本文件
    建立一个新的文件
    “a”
    向文本文件尾添加数据
    建立一个新的文件
    “rb”
    为了输入数据,打开一个二进制文件
    出错
    “wb”
    为了输出数据,打开一个二进制文件
    建立一个新的文件
    “ab”
    向一个二进制文件尾添加数据
    出错
    “r+”
    为了读和写,打开一个文本文件
    出错
    “w+”
    为了读和写,建议一个新的文件
    建立一个新的文件
    “a+”
    打开一个文件,在文件尾进行读写
    建立一个新的文件
    “rb+”
    为了读和写打开一个二进制文件
    出错
    “wb+”
    为了读和写,新建一个新的二进制文件
    建立一个新的文件
    “ab+”
    打开一个二进制文件,在文件末尾读和写
    建立一个新的文件

    4.文件的顺序读写

    功能
    字符输入函数
    字符输出函数
    文本行输入函数
    文本行输出函数
    格式化输入函数
    格式化输出函数
    二进制输入
    二进制输出
    函数名 fgetc
    fputc
    fgets
    fputs
    fscanf
    fprintf
    fread
    fwrite
    适用于
    所有输入流
    所有输出流
    所有输入流
    所有输出流
    所有输入流
    所有输出流
    文件
    文件

    fputc 写一个数据

    int fputc ( int character, FIlE* stream);
    第一个参数是int、第二个参数是FILE类型的文件指针 成功则返回写入的字符
    在同一段程序中多次使用,则在同一行后追加

    fgetc 读一个数据

    int fgetc(FILE * stream ).
    参数是文件指针。 成功则返回读到的字符类型为int 如果成功读取,他的内部文件指针会推进到下一个字符 也就是说下次读写会读写这次读写的下一个字符
    读取失败则返回EOF
    那么我们就可以写一个while循环,直到他读取到EOF才停下

    fputs 写一行数据

    int fputs ( const char * str, FILE * stream );
    第一个参数是写一个字符串,第二个参数是关联到那个文件(文件指针)
    成功返回一个非负值、失败返回EOF、
    字符串中的\0不会被写入进去
    在同一段程序中多次使用,则在同一行后追加
    • *可以自己使用*

    fgets 读一行数据

    char * fgets ( char * str, int num, FILE * stream );
    第一个参数是把读到的东西放到str所指向的空间当中
    第二个参数是最多读这么多的数字减一.因为要留一个来放\0
    成功则返回存放字符串的地址str的地址。
    失败则返回空指针

    fprintf 格式化输出函数

    int fprintf ( FILE * stream, const char * format, ... );
    第一个参数是要写入的文件,后面的和printf使用方法一样。
    返回值是字符总数。

    fscanf 格式化输入函数

    int fscanf ( FILE * stream, const char * format, ... );
    第一个参数是要读取的文件,后面的和scanf使用方法一样。

    fwrite 二进制输出

    size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
    第一个参数是要写入的数据的起始位置
    第二个参数是每个元素有多大
    第三个参数是有多少个元素要输出
    第四个参数是写入的文件流指针。

    fread 二进制输入

    size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
    第一个参数是要输入的起始地址
    第二个参数是每个元素有多大
    第三个参数是有多少个元素要输入
    第四个参数是写入的文件流指针。

    sscanf 针对字符串,不针对文件和流

    int sscanf ( const char * s, const char * format, ...);
    从一个字符串中转换出一个格式化的数据
    从字符串中获取一个格式化的数据到另一个地方。

    sprintf 针对字符串,不针对文件和流

    int sprintf ( char * str, const char * format, ... );
    写格式化的数据到一个字符串里去
    把一个格式话的数据写到字符串中
    使用举例

    5.文件的随机读写

    fseek
    根据文件指针的位置和偏移量来定位文件和指针 int fseek ( FILE * stream, long int offset, int origin );
    第一个参数,操作那个文件流?
    第二个参数,偏移量是多少
    第三个参数,
    常数
    参考位置
    SEEK_SET
    文件的开始
    SEEK_CUR
    文件指针的当前位置
    SEEK_END
    文件的末尾
    ftell
    返回文件指针相对于文件起始位置的偏移量 long int ftell ( FILE * stream );
    rewind
    让文件指针回到文件的起始位置 void rewind ( FILE * stream );

    6.文本文件和二进制文件

    根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
    数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
    如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件
    一个数据在内存中是怎么存储的呢?
    字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
    如有整数10000,如果以ASCI码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。
    10000在内存中为00000000 00000000 00100111 00010000
    二进制形式储存是00000000 00000000 00100111 00010000
    ASCII形式存储是00110000 00110000 00110000 00110001 00110000 是把每个数字当成一个字符存储。存储的是ASCII码

    7.文件读取结束的判定

    7.1被错误使用的feof

    牢记:在文件读取的过程中,不能用feof函数的返回值来判断文件是否结束
    而是应用于文件读取结束的时候,用它来判断是读取失败结束,还是遇到文件末尾。
    1. 文本文件读取是否结束,判断返回值是否为E0F(fgetc), 或者 NULL(fgets )
      1. 例如:
        fgetc判断是否为 EOF ofgets 判断返回值是否为 NULL.
    1. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
      1. 例如:
        fread判断返回值是否小于实际要读的个数。

    8.文件缓冲区

    ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程)中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小据C编译系统决定的。

    七、程序环境和预处理

    1 编译和链接

    每个源文件都会单独经过编译器编译成目标文件,后缀为.obj (linux为 . o)
    最后目标文件会经过链接器,加上链接库 链接成可执行程序 .exe文件
    其中编译分为三个阶段
    1. 预编译/预处理 头文件的包含、这一步会删除#define 和注释//
    1. 编译 把C语言代码编译为汇编代码
    1. 汇编 把汇编代码转换成二进制的机器指令
    链接是吧多个目标文件和链接库链接输出成可执行文件 .exe
    它分为两个阶段
    1. 合并段表(每个函数都有一个地址,如果地址对不上啥的就会报错)
    1. 符号表的合并和重定位

    2 #define

    #define 定义的后面最好不要加;容易出现问题

    3 #define 定义宏

    #define 机制包括了一个规定: 允许把参数替换到文本中, 这种实现通常称为宏(macro)或定义宏(define macro)
    申明方式如下
    #define name( parament-list )stuff 注意宏名和左括号要临近,不能加空格
    分别是宏名 宏体 和参数
    比如一个x * x 的一个宏
    #define SQUARR(X) ( (x)*(x)) 注意在参数部分加括号!!在参数部分不能吝啬括号,不然会因为优先级的问题出问题.外圈和内圈都要加括号
    使用是这样的
    int r = SQUARR(5);
    结果r为25

    4 # 和

    #可以在宏中把参数插入到字符串中。例如#n它相当于“n”
    举个栗子
    他的输出的是
    ##可以把两边的符号合并成一个符号

    5 宏和函数的命名约定

    宏的名字全大写
    函数的名字不全大写

    6 undef 移除宏定义

    undef + 宏名
    消除宏

    7.条件编译

    根据宏定义的条件输出

    这里的输出为你好。
    也可以用 #if 和 #endif

    在单片机中有一段是这里用到的!!

    头文件<>和“”的区别

    他们的查找策越不一样
    <>是去库里查找
    “”是先去代码所在的路径下查找,如果找不到,再去库目录下查找。
    完结撒花。去学32 了
    上一篇
    龙芯2k0300初探(从点灯到图像识别)
    下一篇
    C语言初阶笔记

    评论
    Loading...
    目录