C语言 总结

本文详细介绍了C语言的基础知识,包括变量、数据类型、运算符、流程控制、数组、字符串、函数的使用。深入讲解了指针的原理和应用,如指针与数组、函数指针、动态内存管理。此外,还涵盖了结构体、枚举、位段、文件操作等高级主题。最后,提到了预处理指令如宏定义、条件编译以及内存管理中的内存对齐和动态内存分配。
摘要由CSDN通过智能技术生成

C语言笔记总结

这是我近期学习C语言的过程中的一些总结与一些理解
该笔记的Gitee地址

初识C语言

VS中的一点儿事

在vs当中,会认为scanf函数不安全,需要用scanf_s才能过编译,也可以使用预处理指令使编译器忽略这个问题

#define _CRT_SECURE_NO_WARNINGS

声明与定义的关系

将建立空间的声明称为”定义“将不需要分配空间的声明称为”声明“

main函数

main函数是C语言程序的入口,代码从main函数第一句开始执行,因此,main函数有且只能有一个

计算机的单位

bit 1/0(计算机只能识别二进制)比特位

byte 1 byte = 8 bit 字节

KB 1 KB = 1024 byte

MB 1 MB = 1024 KB

GB 1 GB = 1024 MB

C语言标准规定

sizeof(long) >= sizeof(int)

变量

局部变量

生命周期:进入作用域生命周期开始。出作用域生命周期结束

作用域:变量所在的局部范围,也就是其所在代码块

全局变量

生命周期:整个程序的生命周期

作用域:整个工程,不同源文件使用,需要用extern来进行声明不初始化默认初始化为0

常量

字面常量

如 12,“hello world”,‘w’…

常变量(关键字:const)

const修饰的变量,称为常变量,即被const修饰的变量具有常属性,但注意,其仍然是一个变量,不是一个常量,因此不能用来声明一个数组标识符常量(关键字:#define) 注意,标识符常量不占内存,在编译过程中直接进行替换枚举常量(关键字:enum)可以用来自定义数据类型

字符串

字符串末尾隐藏着一个\0,这个是字符串结束的标志

转义字符

转义字符实则就是C语言规定的一个字符

## 运算符 (sizeof)

sizeof 是一个运算符,而不是一个函数,用于计算类型创建的变量所占内存空间的大小

原码,反码,补码

规则

0及正数的原码,反码,补码一致,负数则遵循以下规则

原码:其原有的二进制序列

反码:原码按位取反得到反码

补码:反码+1

经典例题

int a = 0;
printf("%d",~a);

输出的结果是:-1

解释:

0的原码:00000000000000000000000000000000

0的补码:00000000000000000000000000000000

对补码按位取反得:11111111111111111111111111111111

按位取反后得到的原码:10000000000000000000000000000001

故所打印结果为-1

extern

在某个源文件访问另一个源文件的全局变量或者函数需要在该源文件中使用extern来进行声明

static

static 修饰局部变量

会改变局部变量的生命周期(即在编译时会直接在全局区开辟空间),但不会改变其作用域,因此不会随着代码块结束而被释放

static 修饰全局变量

一个全局变量被static修饰,使得这个全局变量只能在本源文件中使用,而不能在其他源文件中使用

static 修饰函数

一个函数被static修饰,使得这个函数只能在本源文件中使用,不能在其他源文件中使用
注意:被static修饰的函数及全局变量无法通过extern声明而被外部链接

#define 定义常量和宏

#define 定义标识符常量

#define MAX 1000

在预编译阶段编译器就会将程序中的MAX替换成1000,所以不会占用内存

#define 定义宏

#define ADD ((a)+(b))

要带上括号

指针

内存单元:一个内存单元占用一个字节

32位环境下,是通过32根地址线来表示对应内存的地址,因此32位环境下的指针变量所占内存为4个字节

int a = 10;
int* pa = &a;

&a 的时候,取出的是 a 所占内存中四个字节中首字节的地址

pa 是一个指针,存放的是 a 的地址,指向的是 a 所占的空间,指向的对象类型是 int 类型

指针变量是一种变量,存放的是一个地址

* 解引用操作符,用来访问指针所指向的空间,访问空间的大小由指针类型决定

.h 文件与 .c 文件的联系

在编译过程中,.h文件中的所有内容会被写到包含它的 .c 文件中,而所有的 .c 文件以一个共同的 main 函数作为可执行程序的入口。在 .h 文件中编写函数实现依然可以正常编译执行,相当于所有 .h 的内容最后都被写到了 main.c 文件中。但是为了逻辑性、易于维护性以及一些其他目的,一般在 .h 文件中编写函数的声明,在 .c 文件中编写函数的实现。

结构体

关键字:struct

分支与循环

分支

if 语句

语法结构

// 1
if (/*表达式*/)
{
    //语句1;
}
// 2
if (/*表达式*/)
{
    //语句1;
}
else
{
    //语句2;
}
// 3
if (/*表达式1*/) 
{
    //语句1;
}
else if(/*表达式2*/)
{
    //语句2;
}
else
{
    //语句3;
}

else 语句总是和距离其最近的可以匹配的 if 语句进行匹配

解释一下: 如果表达式的结果为真,则语句执行。

在C语言中如何表示真假?

0表示假,非0表示真

当一个常量和一个变量比较是否相等,建议将常量放在==的左边,以防止手误

if (5 == a)
{
	//语句;
}

switch 语句

语法形式
switch (/*整型表达式*/)
{
	//语句项;
}

语句项是什么呢?

//是一些case语句:
//如下:
case /*整形常量表达式*/:
   //语句;
switch 语句中的 break

break 用来跳出 switch 语句

switch 语句中,无法直接实现分支,搭配 break 使用才能实现真正的分支。

比如:

int main()
{
	int day = 0;
	scanf("%d", &day);
	switch (day)
	{
	case 1:
		printf("星期一\n");
		break;
	case 2:
		printf("星期二\n");
		break;
	}
	return 0;
}

编程好习惯

在最后一个 case 语句的后面加上一条 break 语句。 (之所以这么写是可以避免出现在以前的最后一个 case 语句后面忘了添加 break 语句)

default 子句

switch 中的表达式的值与所有的 case 标签的值都不匹配时,则会进入 default 语句,因此 default 语句只能有一个

当switch语句中没有default语句,编译器也不会报错,结果只是将所有的语句跳过

编程好习惯

在每个 switch 语句中都放一条 default 子句是个好习惯,甚至可以在后边再加一个 break

循环

while 循环

while(/*表达式*/)
{ 
    //循环语句;
}

break :直接终止循环

continue :跳过本次循环 continue 后面的代码,终止本次循环

for 循环

for(int i = 0; i < 3; i++)
{  
    //循环语句;
}

三要素:初始化,判断,调整

break :直接终止循环

continue :跳过本次循环 continue 后面的代码,但仍会进行调整语句

函数

函数的参数

实际参数

真实传递给该函数的参数叫做实参,可以是常数,变量,表达式等,无论实参是何种类型,都需要一个确定的值,以传递给形参

形式参数

只有函数调用的时候会创建,函数结束调用后,其所占的内存会被释放,形参是实参的一份临时拷贝,改变形参是不会影响实参的

传值调用/传址调用

传值调用

传递一个值,函数的形参和实参分别占用不用的内存单元,因此改变形参不会影响实参

传址调用

传递一个函数外部变量的地址,传址调用可以让函数和函数外部的变量建立起真正的联系,也就是说函数内部可以直接操作函数外部的变量,形参可以直接操控实参

printf 函数

printf 格式字符串

printf不仅会将读取到的数据打印在屏幕上,格式字符之间的一切字符也都会打印在屏幕上

printf函数返回值

返回打印的字符数,如果发生错误,则返回负值

printf("%d", printf("%d", printf("%d",43))); 
// 打印结果:4321 

scanf 函数

scanf 格式字符串

scanf函数中格式字符串中是否添加空格,都不会影响输入。

编译器能够正确地识别格式字符「%d」之间使用了什么样的字符来间隔,在两个格式字符「%d」之间如果不添加任何字符或者添加「空白」——空格、制表符、换行等等,编译器会一律将其间隔看作是「空白」,「空白」就是相当于没有,如果格式字符之间使用了「非空白」字符,比如逗号等等,一系列可见的字符,那么在输入数据时,需要显式输入对应的格式字符。

scanf函数返回值

返回成功转换和分配的字段数,返回值不包括已读取但未分配的字段

返回值为0表示未分配任何字段

返回值为EOF,表示错误或在首次尝试读取字符时遇到文件结尾字符或字符串结尾字符

函数的声明与定义

函数在使用之前要先声明,让编译器知道有这么个函数,或者直接在主函数之前声明定义

函数的递归

什么是递归

什么是递归?先了解什么是递归.

递归应存在限制条件,当满足这个限制条件的时候,递归便不再继续,每次递归之后都要逼近那个限制条件

有时函数使用递归会便于计算,但有时使用递归的效率会较低

如何写递归

  1. 寻找一个结束条件。当达到这个结束条件的时,停止调用函数,直接返回结果
  2. 不断缩小范围。结合所写函数寻找出一个比当前参数更逼近结束条件的参数与当前参数的关系,进而不断缩小范围。
  3. 将这个关系写成代码

例如:

结束条件为n == 1,就尝试思考当函数的参数为n时与为n - 1时二者的关系并将这个关系写成代码

在思考如何写递归的时候,将要递归函数的目的带入函数中思考会使思路更清晰

求解斐波那契数列

递归和循环两种方式

//递归计算斐波那契数
int Fib(int n)
{    
    if (n <= 2)   
    {       
        return 1;   
    }    
    return Fib(n - 1) + Fib(n - 2);
}

//循环计算斐波那契数
int main()
{    
    int a = 1;    
    int b = 1;    
    int n = 0;    
    int ret = 1;   
    printf("请输入一个值:");   
    scanf("%d", &n);   
    while (n > 2)    
    {        
        ret = a + b;       
        a = b;       
        b = ret;       
        n--;    
    }   
    printf("斐波那契数为:%d", ret);  
    return 0;
}

递归求解汉诺塔

递归求解汉诺塔

搭配时间戳求随机数

srand((unsigned int)time(NULL)); // 放置随机数种子

C语言其实并没有随机数,只是有许多固定好了的数组,然后每一组数都有一个固定的编号,通过srand设置随机数种子(即得到那个编号),然后用rand函数在这组数中随机选择一个数,这叫做伪随机数。其中,time(NULL)用来获取当前时间,此函数会返回从公元1970年1月1日的UTC时间从0时0分0秒算起到现在所经过的秒数。如果time函数参数并非空指针的话,此函数也会将返回值存到参数指针所指的内存,但如果传进来NULL的话,就不保数组

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

数组在创建的时候,[]中必须放一个常量,不能是一个变量

函数栈帧的创建和销毁

函数栈帧的创建与销毁

数组详解

数组初始化

不初始化的话,数组中各元素存放的随机值

不完全初始化,数组中剩余的元素初始化为0

不规定数组大小进行初始的话,编译器会根据所要存放数组元素的个数来确认数组大小、

下标引用操作符 []

数组是通过下标来访问的,下标是从0开始的

数组元素在内存中的存放

数组随着下标的增长,数组元素在内存中是连续存放的,数组元素的地址是由低到高的

二维数组

二维数组也可以看成是一个一维数组,数组元素是一维数组。因此在初始化二维数组的行可以省略,列不可以省略,因为列的存在才能够进行截断

int arr[3][4]
  • 在通过 sizeof 计算 arr[0] 的大小的时候,仍然是计算数组的第一个元素的大小,而对于一个二维数组而言,数组的元素是一个一维数组,所以这里计算的是第一行数组的大小
  • arr 表示数组首元素(第一个数组)的地址
  • arr[0] 表示二维数组的第一个元素,也是第一行数组中的数组名,也是第一行数组的第一个元素的地址
  • 二维数组所有元素在内存中也是连续存放的

编译器不会给数组越界访问报错,因此在写代码的时候应仔细检查

数组作为函数参数

传参时通常将数组名作为实参传递,在这里,编译器进行优化会将数组名降级为数组首元素的地址,因此传递的是一个地址,而不是一个数组,而形参要用指针来接受

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

但有两个例外(这个时候数组名表示整个数组):sizeof(数组名),当sizeof内部单独放置数组名的时候,这时数组名表示整个数组,得到的是整个数组的内存大小,单位是字节&数组名,数组名表示整个数组,取出的是整个数组的地址其他任何情况下数组名均表示数组首元素的地址,这种情况下,可以将数组名认为是一个常指针

访问数组元素的原理

指向数组元素的指针和数组下表访问符结合用来访问数组元素,通过指针的偏移量来得到对应元素的地址,然后再对这个地址解引用访问到对应的元素

操作符详解

左移操作符 <<

左边丢弃,右边补0

右移操作符 >>

算术右移

大多数编译器(包括VS2019)采用算术右移

右边丢弃,左边补原符号位

逻辑右移

右边丢弃,左边补0

左移与右移操作符的操作数必须是整数并且移动的位数不能是负数

按位与 &

对应二进制位只要有一个是0,得到的结果对应二进制位就是0,只有两个数的对应二进制位都是1时,得到的结果对应二进制位才为0

统计一个数的1的位数

int numberOf1(int n)
{
    int count = 0;
    while(n)
    {
        ++count;
        n = n & (n-1); // 去掉二进制序列中最右边的1
    }
    return count;
}

按位或 |

对应二进制位只要有一个是1,得到的结果对应二进制位就是1,只有两个数的对应二进制位都是0时,得到的结果对应二进制位才为0

按位异或 ^

相同为0,相异为1

异或支持交换律

a ^ b ^ b = a;

异或表示当两个数的二进制表示,进行异或运算时,当前位的两个二进制相同则为0,不同则为1

a ^ a = 0; // 任何数与自身异或都是0
0 ^ a = a; // 0与任何数异或都是这个数

// 不创建临时变量交换两个变量的值
a ^ a ^ b = b;
a ^ b ^ a = b;

赋值运算符 =

左值 --> 空间

右值 --> 要存放到空间的内容

解引用操作符 *

拿到指针所指向的空间

指针的类型不同,拿到的空间也不同,拿到的空间大小是指针指向的变量决定的

拿到空间后,以指针指向的变量类型的视角看向这块空间

sizeof

计算变量或类型创建变量的大小,sizeof不是函数,是一个操作符

sizeof的内部表达式在运行时是不参与运算的

sizeof在计算的时候只看操作数的类型,不会访问对应空间

按位取反 ~

将对应二进制位取反

0 --> 1 1 --> 0

加加/减减 ++/--

后置 ++/--

先使用,后运算

前置 ++/--

先运算,再使用

逻辑与/逻辑或 && ||

注意这里的短路运算

条件操作符(三目操作符)(exp1 ? exp2 : exp3)

若exp1表达式为真,则结果为exp2的值;若为假,则结果为exp3的值

下标引用操作符 []

arr[4] --> *(arr+4)

因为加法支持交换律,所以 *(arr+4) = *(4+arr),所以就有4[arr]

结构成员访问操作符

结构变量 .

结构变量指针 ->

(*结构变量指针) .

初级指针

指针

指针是一个变量,存放的是一个地址

指针的大小

在X86环境(32位环境)中,一个变量的地址是通过32根地址线来表示的,所以为了能够描述地址线的情况,需要用32位二进制位,所以在32位环境中,指针的大小是4个字节

在X64环境(64位环境)中,使用64根地址线来表示,所以使用64位二进制位来表示,所以要用8个字节

指针类型

指针类型决定了解引用操作所访问空间的大小

指针的类型决定了+/-整数的时候跳过几个字节,也就是指针+/-整数的时候的偏移量

野指针

指向的位置不可知的指针就是野指针

野指针的成因

  1. 指针未初始化
  2. 指针越界访问
  3. 指针指向的空间释放

如何避免野指针

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向空间释放,及时置空(NULL)
  4. 指针使用之前检查有效性

在初始化指针的时候,如果不知道应该指向什么变量的时候,应将其置NULL,这样的好处是在后面程序中使用该指针的时候,容易判断该指针是否指向某一个变量,进而避免野指针

指针的计算

指针 +/- 整数

根据指针的类型+/-对应的字节数

指针 + 指针

无意义❎

指针 - 指针

得到的是数字,这个数字的绝对值是指针和指针之间元素的个数

C语言标准规定访问数组元素

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较

指针数组

int* arr[10] = {NULL};

存放指针的数组称为指针数组

初识结构体

结构体类型声明是不开辟内存空间的,定义一个结构体变量的时候才会分配对应的内存空间(可以将其理解为,结构体类型声明是一个图纸,创建结构体变量是盖房子)

结构体变量传参

传值调用

直接复制一个结构体变量,这样的话参数压栈的系统开销会比较大,所以导致性能下降

传址调用

传递结构体变量的地址

结论

建议使用传址调用,这样既可以减小内存消耗,提升性能,也可以直接通过结构体指针访问或修改实参的成员变量

实用调试技巧

Debug 和 Release

Debug

Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序

Release

Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用

编译器会对Release版本做出优化

一些快捷键

最常使用的几个快捷键:

F5

启动调试,经常用来直接调到下一个断点处。

F9

创建断点和取消断点 断点的重要作用,可以在程序的任意位置设置断点。这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。

F10

逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。

F11

逐语句,就是每次都执行一条语句,但是这个快捷键可以使执行逻辑进入函数内部(这是最常用的)。

CTRL + F5

开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。

编程常见的错误

编译型错误

直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。

链接型错误

看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误

运行时错误

借助调试,逐步定位问题。最难搞。

无法解析的外部符号

导致这种错误的原因

  1. 符号未定义

  2. 符号名写错

如何写出好的代码

优秀的代码:

  1. 代码运行正常

  2. bug很少

  3. 效率高

  4. 可读性高

  5. 可维护性高

  6. 注释清晰

  7. 文档齐全

常见的coding技巧

  1. 使用 assert

  2. 尽量使用 const

  3. 养成良好的编码风格

  4. 添加必要的注释

  5. 避免编码的陷阱

栈区的使用习惯

先使用高地址,再使用低地址

编译器在开辟空间时,变量与变量之间是有间隔的,不同的编译器所留下的间隔是不同的,在VS2019环境中,编译器至少留有8个字节的空间

Null/null/NULL

在一些文档中null/Null均表示字符串结束的字符 '\0' ,但NULL表示的是空地址

断言 assert()

括号中表达式为真,程序继续;括号中表达式为假,程序直接终止,利于查看出哪里出现了问题

const 修饰指针

int a = 0;
const int* p = &a;
int* const p = &a;

const 放在 * 左边

修饰的内容是 *p ,指针指向的内容不可以修改,但指针指向的位置(指针中存放的地址)可以修改

int a = 0;
int b = 0;
const int* p = &a;
*p = 1; //err
p = &b; //ok

const放在 * 右边

修饰的内容是 p,指针指向的位置(指针中存放的地址)不可以修改,但指针指向的内容可以修改

int a = 0;
int b = 0;
int* const p = &a;
*p = 1; //ok
p = &b; //err

size_tunsigned int 等效

size_t 是在 <stdio.h> 头文件中对 unsigned int 进行类型重定义的

数据的存储

原码、反码、补码

无符号数

原码、反码、补码相同

有符号数

  • 正整数
    • 原码、反码、补码相同
  • 负整数
    • 原码 - 直接按照数字的正负写出的二进制序列
    • 反码 - 原码的符号位不变,其他位按位取反
    • 补码 - 反码+1

整型在内存中的存储

对于整型数据而言,数据在内存中存放的是补码

浮点数在内存中的存储

浮点数在内存中的存储

static 变量

static 的特性

  • static 变量在程序装载的时候就被初始化,它存在于程序内存空间的静态储存区中而不是堆栈中,这样在下一次被调用的时候它还是保持原来的值
  • 和全局变量不一样,它只在自己的作用范围内可见,但是在可用范围之外它并不会消失(与堆栈和堆中的数据不一样)
  • 它和全局变量最大的不同之处,在于它具有隐藏的功能(全局变量在所有被编译的.c文件中都可见)
  • 静态全局变量、静态局部变量、全局变量都放在内存的静态存储区,局部变量则存在于堆栈区

static 变量的实现原理

在程序被装载进内存时,所有定义为 static 的变量已经完成了初始化,并被放在了程序段的静态段,所以我们无论定义了多少次这个 static 的对象,内存中实际存在的变量还是只有这一个。

因为static修饰的变量在编译阶段就已经被初始化,所以在程序调用中并不会执行创建经static修饰的变量

无论这个static变量在我们看来作用于哪一个范围内,比如一个函数中,函数结束后这个变量依然存在,并不会因为函数结束而被回收。

为什么要存放补码呢?

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理; 同时,加法和减法也可以统一处理( CPU只有加法器,1-1等价为1+(-1) )此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

大/小端字节序存储

大小端字节序存储讨论的是字节存放的顺序,故称为字节序

小端字节存储

把一个数据的低位字节的内容,存放在内存的低地址处;把高位字节的内容,存放在内存的高地址处

大端字节存储

把一个数据的低位字节的内容,存放在内存的高地址处;把高位字节的内容,存放在内存的低地址处

举例

int main()
{
	int a = 0xaaabacad;
	return 0;
} // VS环境下小端存储

image-20211016202144889

判断当前环境存储的字节序

  1. 指针访问空间

    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;
    }
    
  2. 通过联合体

    typedef union Test
    {
    	char a;
    	int b;
    }Test;
    
    int main()
    {
    	Test s;
    	s.b = 1;
    	if (s.a == 1)
    		printf("小端\n");
    	else
    		printf("大端\n");
    	return 0;
    }
    

整型提升

因为CPU的计算结构,表达式中的 char/short 类型变量在使用之前会被转换为普通整形,这种转换称为整形提升只要 char/short 类型变量参与运算就会发生整型提升整型提升的方式:无符号位数:高位均补0有符号位数:高位均补符号位(负数补1,正数补0)

整型截断

将32位bit的数放入 char/short 类型变量中,会根据所要存放变量的大小进行截断

例如:0X11223344

存放到char中: 0x44

存放到short中:0x3344

char a = 10;
char b = 20;
char c = a + b;

上述代码中,a和b进行相加操作时,a和b首先会进行整型提升,然后进行相加,最后将相加的和进行截断存放入c中

算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换

long double
double
float
unsigned long int
long int
unsigned int
int

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
注意:算术转换要合理,要不然会有一些潜在的问题。

float f = 3.14;
int num = f; // 隐式转换,会有精度丢失

总结

我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的

指针的进阶

决定指针+1的步长(偏移量)的是指针的类型

指针数组

int* arr[10]

这是一个数组,数组的元素是指针

数组指针

int (*parr)[10]

这是一个指针,指向的是一个数组,指向的是一个10个元素的数组,每个数组元素的类型是int

因为*操作符的优先级小于[],所以在声明变量类型的时候要加上(),使变量名先和*结合,这样表示所生命的变量类型是一个指针,指向的是一个数组,数组元素使int

数组指针的指针类型

int (*) [10]

解引用一个数组指针

(\parr ) 相当于 parr 指向的数组的数组名

数组指针的应用场景

函数传传参传递一个二维数组的数组名是,形参可以使用数组指针因为对于一个二维数组而言,数组首元素是一个一维数组,所以二维数组的数组名是一个一维数组的地址,因此形参可以使用数组指针来接收

int (*parr[10])[5]

因为[]的优先级高于,所以这里表示了一个数组,数组元素的类型是数组指针,也就是int (*)[5],指向的是有5个int型元素的数组的数组指针

指针传参的场景

数组传参

无论什么数组进行传参,都是在传递数组首元素的地址

一维数组传参

函数的形参的类型应是数组元素类型的指针

二维数组传参

二维数组可以理解成是一个元素为一维数组的一维数组所以在进行传参的时候,传递的是首元素的地址,而二维数组的首元素是数组,所以在形参要设为数组指针

指针作为函数参数

一级指针可以接收变量的地址,数组,一级指针二级指针传参可以接收一级指针的地址,一级指针数组,二级指针

函数指针

函数的入口地址

如果在程序中定义了一个函数,在编译时会把函数的源代码转换为可执行代码并分配一段存储空间。这段内存空间有一个起始地址,也称为函数的入口地址。函数名代表函数的起始地址。调用函数时,从函数名得到函数的起始地址,并执行函数代码。

函数指针中存放的地址就是函数的入口地址,函数名就是函数入口地址

函数指针(); // () 这个是函数调用操作符,函数指针+函数调用操作符,当程序走到这个语句是,程序会跳转到函数指针指向的函数入口地址,并调用函数

以Add函数为例

//举个栗子
int Add(int x, int y)
{  
    return x + y;
}
//Add  &Add

Add 与 &Add 不仅值相同,意义也相同,均表示Add函数的地址

Add函数指针定义
int(pfun)(int,int) = &Add;
//或者
int(\*pfun)(int,int) = Add;
Add函数指针类型
int(*)(int,int)
Add函数指针的使用
pfun(1,2);
(*pfun)(1,2);

对于下面两行代码的理解

//代码1
(*(void (*)())0)();
//代码2
void(*signal(int , void(*)(int)))(int);

函数指针数组

int(*pfArr[5])(int,int)

函数指针数组的用途:转移表

指向函数指针数组的指针

指向函数指针数组的指针是一个 指针 指针指向一个 数组 ,数组的元素都是 函数指针 ;

int Add(int x, int y)
{
    return x + y;
}
int main()
{
    // 函数指针的定义
    int(*pfun)(int,int) = Add;
    // 函数指针数组的定义
    int(*pfunArr[5])(int,int);
    pfunArr[0] = Add;
    // 指向函数指针数组的指针的定义
    int(*(*ppfunArr)[5])(int,int) = &pfunArr;
    return 0;
}

void*

void* 类型指针可以用来接收任意类型的地址,这样编译器不会报警告

void* 类型指针不能进行解引用操作

void* 不能进行 +/- 整数操作

可以通过强制类型转换,来访问内存空间

回调函数

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

指针练习

字符串的一些库函数

求字符串的长度

strlen

size_t strlen ( const char * str );
  • 传递一个字符串,返回 ‘\0’ 前面出现的字符个数(不包含 ‘\0’)
  • 参数指向的字符串必须以 ‘\0’ 结束
  • 返回参数是 size_t类型,是无符号整型

长度不受限制的字符串函数

strcpy

char* strcpy(char * destination, const char * source );
  • 将传递的源字符串内容复制到目标字符串中
  • 源字符串必须以 ‘\0’ 结尾
  • 同时会将 ‘\0’ 拷贝到目标字符串
  • 目标字符串空间应该足够大,且可以被修改

strcat

char * strcat ( char * destination, const char * source );
  • 将源字符串内容追加到目标字符串,目标字符串的结尾位置 (也就是第一个 ‘\0’ 出现的位置)会直接被源字符串的第一个字符覆盖,源字符串的 ‘\0’ 也会被追加到新字符串结尾
  • 源字符串和目标字符串都必须以 ‘\0’ 结尾
  • 目标空间必须足够大,且可以被修改

strcmp

int strcmp ( const char * str1, const char * str2 );
  • This function starts comparing the first character of each string. If they are equal to each other, it

    continues with the following pairs until the characters differ or until a terminating null-character is

    reached.

  • 标准规定:

    • 第一个字符串大于第二个字符串,则返回大于0的数字
    • 第一个字符串等于第二个字符串,则返回0
    • 第一个字符串小于第二个字符串,则返回小于0的数字
    • 这里是比较的是,两个字符串查找停止时的位置字符之间ASCII码的大小

长度受限制的字符串函数

strncpy

char * strncpy ( char * destination, const char * source, size_t num );
  • 拷贝num个字符从源字符串到目标空间
  • 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。

strncat

char * strncat ( char * destination, const char * source, size_t num );
  • 与strcat大致相同
  • 如果源代码中的C字符串长度小于num,则仅复制直到 ‘\0’ 字符的内容。

strncmp

int strncmp ( const char * str1, const char * str2, size_t num );
  • 比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完

字符串查找

strstr

char * strstr ( const char *, const char * );
  • Returns a pointer to the first occurrence of str2 in str1, or a null pointer if str2 is not part of str1.
  • 模拟实现这个函数有两种算法:
    • BF算法 时间复杂度:O(n*m)
    • KMP算法 时间复杂度:O(n+m)

错误信息报告

### strerror

以字符串的形式返回错误码所对应的错误信息

字符分类函数

函数如果参数符合下列条件就返回真
isdigit十进制数字 0~9
isxdigit十六进制数字,包括所有十进制数字,小写字母af,大写字母AF
islower小写字母a~z
isupper大写字母A~Z
isalpha字母a~z或A~Z
isalnum字母或者数字,a~z,A~Z,0~9

内存操作函数

memcpy

void * memcpy ( void * destination, const void * source, size_t num );
  • 从source的位置开始向后复制num个字节的数据到destination的内存位置。
  • 这个函数在遇到 ‘\0’ 的时候并不会停下来。
  • 如果source和destination有任何的重叠,复制的结果都是未定义的。

memmove

void * memmove ( void * destination, const void * source, size_t num );
  • 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
  • 要考虑由于内存拷贝方向问题导致内存覆盖,数据移动异常
  • 模拟实现的时候通过判断两个指针的大小来决定移动内存的方向

memset

int memcmp ( const void * ptr1, const void * ptr2, size_t num );
  • 比较从ptr1ptr2指针开始的num个字节

memcmp

int memcmp ( const void * ptr1, const void * ptr2, size_t num );
  • 比较从ptr1ptr2指针开始的num个字节
  • 比较两块内存空间数据是否一致
  • 返回值与strcmp返回值规则一样

自定义类型

结构体

结构体声明方式

// 结构体通常的声明
struct Stu
{
	char name[20]; //名字
	int age; //年龄
	char sex[5]; //性别
 	char id[20]; //学号
}; //分号不能丢

// 匿名结构体类型
struct
{
 	int a;
	char b;
 	float c; 
}x;

// 匿名结构体很鸡肋 但可以用在类型重定义
typedef struct
{
 	int a;
	char b;
 	float c; 
}stu;

结构体访问成员

  • 结构体变量

    • 变量通过 . 操作符访问
  • 结构体指针

    • 指针通过 -> 操作符访问

    结构体访问成员变量是通过偏移量进行访问

结构体内存对齐

这个非常重要!!

规则:
  • 结构体的第一个成员永远放在结构体起始位置偏移量为0的位置
  • 结构体成员从第二个成员开始,总是放在偏移量是对齐数最小整数倍数的位置
  • 对齐数 = 编译器默认的对齐数和变量自身大小的较小值

Linux - 没有默认对齐数
VS - 默认对齐数是8

  • 结构体总大小是最大对齐数的整数倍
为什么存在结构对齐?
  1. **平台原因(移植原因):**不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地地址取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

总体来说:就是用空间时间

修改默认对齐数

#pragma 预处理指令,可以改变默认对齐数。

#pragma pack(6) // 将默认对齐数改为6
#pragma pack()  // 取消设置的默认对齐数,还原为默认

offset 宏的实现

#define m_offset(st_type, mem_name) (int)&(((st_type*)0)->mem_name)

这里只是看向了在st_type相对于 0 地址 mem_name 所在的内存空间,并取得了地址,并没有进行访问,所以这句代码不涉及内存空间非法访问

位段

位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”

位段的声明和结构是类似的,有两个不同:

  1. 位段的成员必须是 intunsigned intunsigned charchar (整型家族)。
  2. 位段的成员名后边有一个冒号和一个数字。
struct A
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

A中的成员就是位段,冒号后面的值是该变量所占内存空间比特位的数量,例如,成员变量a占用3个比特位

image-20211016194944315

总结:

  • 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

  • 利用位段能够用较少的位数存储数据。

  • 在一个字节中,先使用地址高的比特位,再使用地址低的比特位。

  • 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

枚举

枚举顾名思义就是一一列举,将可能取的值列举出来

// 枚举的定义
enum Color //颜色
{
	RED,
	GREEN,
	BLUE
};
enum Color clr = RED;

在上述定义中,enum Color 是枚举类型,{}中的内容是枚举类型的可能取值,叫做枚举常量,通过枚举类型声明的变量是该枚举类型的枚举变量,占用四个字节

例如,在上述的枚举定义中,REDenum Color 枚举类型的枚举常量,clrenum Color 枚举类型的枚举变量

这些可能取值都是有值的,默认从0开始,依次递增1,当然在定义的时候也可以赋初值, 例如:

enum Color//颜色
{ 
 	RED = 1, 
	GREEN = 2, 
 	BLUE = 4 
};

使用枚举类型的优点:

防止了命名污染(封装)

增加代码可读性的可维护性(例如:使用枚举常量作为 switch 语句中 case 分支的标志位)

使用方便,一次可以定义多个常量

#define 定义的标识符相比枚举有类型检查,更加严谨

便于调试

联合(共用体)

定义

联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合也叫共用体)

//联合类型的声明
union Un 
{ 
 	char c; 
 	int i; 
}; 
//联合变量的定义
union Un un; 
//计算连个变量的大小
printf("%d\n", sizeof(un)); // 输出结果:4

上述联合体变量占用内存图

image-20211016211243892

联合体变量大小的计算

  • 联合的大小至少是最大变量的的大小
  • 当最大成员不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍

例如:

union Test
{
	char a[5];
	short b;
}; // 该共用体类型所占空间大小为 6

动态内存开辟

动态开辟内存的库函数

malloc

堆区上申请一段连续的空间,申请成功则会返回动态内存的地址,不会对申请到的空间初始化。

void* malloc (size_t size);
  • malloc不会初始化开辟的内存空间

  • 如果开辟成功,则返回一个指向开辟好空间的指针。

  • 如果开辟失败,则返回一个 NULL 指针,因此在使用 malloc 函数时,要对函数返回值做一个判断,以防止对 NULL 指针解引用操作

  • 返回值的类型是 void* ,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。

  • 如果参数 size 为0,malloc 的行为是标准是未定义的,取决于编译器

free

用来做动态内存的释放和回收,该函数只能用于释放或回收动态开辟的内存

void free (void* ptr);
  • 释放空间就是将在堆区中申请的到的空间还给操作系统,其存储的数据会被删除,这段空间也就不会归程序所有
  • 不能释放动态开辟的内存中的一部分,不能对同一块内存重复释放
  • 给free函数传递空指针,free 函数什么操作都不会做
  • free函数传递的指针指向的空间不是动态开辟的,则 free 函数的动作是未定义的
  • 在每次释放空间之后,应及时将指针置空,避免野指针

malloc free搭配使用

  • 在使用 malloc函数开辟空间以后,应判断返回的指针是不是 NULL ,来判断是否成功开辟空间

  • 在每次释放空间之后,应及时将指针置空,避免野指针

    int main()
    {
    	int num = 10;
    	int* ptr = NULL;
    	ptr = (int*)malloc(num * sizeof(int));
    	if (NULL != ptr) // 判断ptr指针是否为空
    	{
    		// TODO
    		// 使用开辟的空间
    	}
    	free(ptr); // 释放ptr所指向的动态内存
    	ptr = NULL; // 将指向动态内存的指针置空,避免野指针
    	return 0;
    }
    

calloc

void* calloc (size_t num, size_t size);
  • 函数的功能是开辟 num 个大小为 size 的元素大小的空间,并且把空间的每个字节初始化为0。
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
  • 在堆区上申请一段连续的内存空间,申请成功则会返回动态内存的地址,并将申请到的的内存空间全部赋值为0,申请失败返回 NULL
  • 如果对于申请的空间需要进行初始化操作,使用 calloc

realloc

void* realloc (void* ptr, size_t size);
  • 在已经申请到的空间上继续追加申请内存空间,并返回这个空间的地址,但如果这段空间不足以追加,会新开辟一个内存空间,然后将之前的数据复制到新开辟的内存,并将旧的空间 free 掉,然后返回新的内存空间的起始地址
  • 注意,为了避免开辟失败,是先创建一个变量来接受 realloc 函数的返回值,然后经过判断之后,再将新开辟的内存空间地址赋值给之前维护的指针,避免因返回 NULL 造成内存空间泄露
int main()
{
	int* ptr = (int*)malloc(10 * sizeof(int));
	// 增容
	int* temp = (int*)realloc(ptr, 20 * sizeof(int));
	// 判断是否开辟成功
	if (temp != NULL)
	{
		ptr = temp; // 将ptr指向新的内存空间,以维护
		// TODO
	}

	// 如果是这样呢?
	int* ptr_1 = (int*)malloc(10 * sizeof(int));
	ptr_1 = (int*)realloc(ptr_1, 20 * sizeof(int));
	// 如果开辟失败返回NULL,将NULL赋给ptr_1,继而导致原始内存泄露
	
	return 0;
}

动态内存开辟忘记释放

在堆区上开辟的空间有两种回收的方式:

  1. 主动free
  2. 程序退出时,堆区开辟的空间会自动回收

但如果不主动释放内存空间,程序一旦一直不停止就会导致内存泄漏

  • 因此每次使用完成动态开辟的空间之后,要及时正确释放开辟的空间
  • 函数中尽量不直接返回栈空间的地址,因为栈区的空间是由操作系统进行释放,不好把握

C/C++ 内存分区

image-20211017102742663

  • 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  • 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
  • 数据段(静态区 static):存放全局变量、静态数据。程序结束后由系统释放。
  • 代码段:存放可执行代码,只读常量的二进制代码。

这样,也就可以解释为什么static修饰的局部变量生命周期是整个程序了

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。

但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长。

柔性数组

typedef struct st_type
{
 int i;
 int a[0]; // 柔性数组成员
}type_a;
// 有些编译器会对上述代码报错,可以改成下面的
typedef struct st_type
{
 	int i;
 	int a[]; // 柔性数组成员
}type_a;
  • 结构体中最后一个成员支持是未知大小的数组

  • 柔性数组成员前必须还有一个成员变量

  • sizeof 返回的这种结构体大小不包含柔性数组的内存,柔性数组成员前面的成员变量同样会考虑内存对齐,会为后续预留出柔性数组成员因内存对齐的空间

  • 包含柔性数组成员的结构用 malloc 函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应

    柔性数组的预期大小

typedef struct st_type
{
	char a;
	int arr[]; // 柔性数组成员
}st_type; // sizeof 计算出来的结果是 4

int main()
{
	st_type* st = (st_type*)malloc(sizeof(st_type) + 20 * sizeof(int)); // 开辟内存
	if (st != NULL)
	{
		// TODO
	}
	st_type* temp = (st_type*)realloc(st, sizeof(st_type) + 40 * sizeof(int)); // 增容操作
	if (temp != NULL)
	{
		st = temp;
		// TODO
	}
	return 0;
}

柔性数组成员的好处

typedef struct st_type
{
 	int i;
 	int* p_a;
}type_a;
type_a* p = malloc(sizeof(type_a));
// 这样是不是也可以呢?
  • 如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要 free ,这样无意间就会造成内存泄露,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free 就可以把所有的内存也给释放掉。
  • 利于提高内存访问速度,连续的内存有益于提高访问速度,也有益于减少内存碎片。

文件操作

“流”即是流动的意思,是物质从一处向另一处流动的过程。在计算机中,是对一种有序连续且具有方向性的数据( 其单位可以是bit、byte、packet )的抽象描述。数据从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程,这种输入输出的过程被形象的比喻为“流”。

文件缓冲区

缓冲区就是一块内存区, 它用在输入输出设备和CPU之间,用来缓存数据 。它使得低速的输入输出设备和高速的CPU能够协调工作 ,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。对于文件缓冲区,可以认为他是一个水池,当池子满了以后,在会将存储的数据向外输出,全部输出以后,才会继续接收输入的数据

例如:从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。

缓冲区的大小根据C编译系统决定的,在 stdio.h 中有定义缓冲区的大小

缓冲区在即将刷新之前,才会将缓冲区内部的信息写入流中

因为有缓冲区的存在,写入数据的时候要及时刷新缓冲区或关闭文件

文件指针

缓冲文件系统中,关键的概念是文件类型指针,简称文件指针

每个被使用的文件在打开之后都会在内存中开辟一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的,该结构体类型是有系统声明的,取名FILE,文件信息区由文件指针来维护,这样就建立了内存与磁盘文件的联系,一般通过FILE* 来维护文件信息区,因此认为这个指针是一个流

文件的打开与关闭

在标准库中,官方定义了fopenfclose函数来打开和关闭文件

FILE * fopen ( const char * filename, const char * mode ); // 返回打开文件的文件信息区的指针
int fclose ( FILE * stream );

fopen打开文件成功则返回文件信息区的头地址,否则返回空,若直接给文件名,则是一个相对路径,是在当前工程文件夹搜索这个文件,搜索不到则打开失败,若要打开其他位置的文件,应给到文件的绝对路径

文件使用方式含义如果指定文件不存在
"r"(只读)为了输入数据,打开一个文本文件出错
"W"(只写)为了输出数据,打开一个文本文件创建一个新文件
"a"(追加)向文本文件尾添加数据出错
"rb"(只读)为了输入数据,打开一个二进制文件出错
"wb"(只写)为了输出数据,打开一个二进制文件创建一个新文件
"ab"(追加)向一个二进制文本文件尾添加数据出错
"r+"(读写)为了读和写,打开一个文本文件出错
"w+"(读写)为了读和写,创建一个文本文件建立一个新的文件
"a+"(读写)打开一个文本文件,在文件尾部读写建立一个新的文件
"rb+"(读写)为了读和写,打开一个二进制文本文件出错
"wb+"(读写)为了读和写,建立一个二进制文本文件建立一个新的文件
"ab+"(读写)打开一个二进制文本文件,在文件尾部读写建立一个新的文件
int main()
{
	FILE* ptr = fopen("C:\\Users\\14775\\OneDrive\\桌面\\nihaolala.txt", "r"); //这里就是文件的绝对路径 
	//FILE* ptr = fopen("nihaolala.txt", "r"); 
    // 这里则是文件的相对路径,只会在当前项目所在文件夹中寻找
    if (ptr != NULL)
	{
		printf("Open Successfully\n");
	}
	else
	{
		printf("Open Falied\n");
		exit(-1);
	}
	fclose(ptr);
}

读写文件的输入,输出相都是相对于内存而言,输出则为内存像内存外部输出数据,输入则是内存外部像内存中输入数据,输入输出的方向都是相对于内存而言

文件的顺序读写

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

fgetc

  • 从一个流中读取一个字符

fputc

  • 向一个流中输入一个字符

fputs

  • 向一个流中输出一个字符串

fgets

  • 从一个流中读取一个字符串
  • 读取n-1个字符,第n个字符会赋值为’\0’

scanf / fscanf / sscanf

scanf

int scanf( const char* format [,argument]... );

从标准输入流中读取数据,根据 format 参数中的类型说明符,得知所读取的数据类型,然后将所要输入的数据写入指定的空间

只适用于 stdin 标准输入流

fscanf

int fscanf( FILE* stream, const char* format [, argument ]... );

从给定的流中读取数据,根据 format 参数中的类型说明符,得知所读取的数据类型,然后将所要输入的数据写入指定的空间

适用于所有输入流

scanffscanf 操作相同,只是 scanf 只能从 stdin读取数据,fscanf 可以从所有输入流中读取数据

sscanf

int sscanf( const char *buffer, const char *format [, argument ] ... );

从给定的字符串中读取数据,根据 format 参数中的类型说明符,得知所读取的数据类型,然后将所要输入的数据写入指定的空间

只适用于从字符串中读取信息,不适用于流

printf / fprintf / sprintf

printf

int printf( const char *format [, argument]... );

根据 format 参数中的类型说明符,得知所输出的数据类型,然后将所要输出的数据写入到 stdout

只适用于 stdout 标准输出流

fprintf

int fprintf( FILE *stream, const char *format [, argument ]...);

根据 format 参数中的类型说明符,得知所输出的数据类型,然后将所要输出的数据写入到给定输入流中

适用于所有输出流

printffprintf的行为相同,只是printf只能将所要输出的数据写入stdout中,而 fprintf 可以是所有输出流

sprintf

int sprintf( char *buffer, const char *format [, argument] ... );

根据 format 参数中的类型说明符,得知所输出的数据类型,然后将所要输出的数据写入到给定字符串空间中

只适用于字符串,不适用于流

文件的随机读写

C语言给定了几个标识符常量,来确认常用的文件指针的位置:

  • SEEK_CUR
    • 当前文件指针位置
  • SEEK_END
    • 文件结尾的位置
  • SEEK_SET
    • 文件开头的位置

fseek

int fseek ( FILE * stream, long int offset, int origin )

根据给定的位置和偏移量来定位文件指针

	if (ptr != NULL)
	{
		char a[40] = { 0 };
		fprintf(ptr, "%s", "Hello Wrold HH\n");
		fseek(ptr, 0, SEEK_SET); // 将文件指针定位到与文件开头位置的偏移量为0的位置
		fscanf(ptr, "%s", a);
		fputs(a, stdout);
	}

ftell

返回文件指针相对于起始位置的偏移量

long int ftell ( FILE * stream );
if (pFile != NULL)
{
    fseek(pFile, 0, SEEK_END);   // non-portable
    int size = ftell(pFile);
    fclose(pFile);
    printf("Size of myfile.txt: %ld bytes.\n", size);
}

rewind

void rewind ( FILE * stream );

将文件指针返回到文件起始位置

文本文件和二进制文件

文本文件存储的是字符的信息,是以字符的ASCII码的二进制格式存储

数据在内存中以二进制的形式存储,如果不加转换直接输入到外存,就是二进制文件

文件结束的标志 EOF

程序的编译

一个源程序变成可执行程序(.exe文件)要经过四个步骤

预编译 -> 编译 -> 汇编 -> 链接

组成一个程序的每个源文件都会先依次经历以下三个步骤:

  • 预编译 执行预处理操作
  • 编译 将代码转换为汇编指令
  • 汇编 将汇编指令转换为机器可读的二进制编码

链接

  • 将每个源文件链接到一起,成为一段单一完整的可执行程序
  • 链接期间,合并段表,符号表合并和重定位,使得代码可以多文件交互 符号 (全局变量,函数名))
  • 链接期间在符号表合并和重定义时,因为在main函数所在的源文件中,只会检索到main函数之前的符号,因此在main函数之后定义的函数应在main函数之前声明以下,这样在链接期间main函数中使用的这个函数才能定位到个函数本体

image-20211022154705442

预编译操作

#define

#define定义标识符

#define MAX 1000
#define reg register          //为 register这个关键字,创建一个简短的名字
// 如果定义的标识符过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
                          __FILE__,__LINE__ ,  \
                          __DATE__,__TIME__ )

#define定义的标识符,在预处理进行会直接将其定义的内容进行替换,这里的替换非常粗暴,就是简单的替换

例如下面这句代码,在预处理时,分号也会被替换到程序中

#define MAX 1000;

因此,建议不要添加分号,以避免出现后续的语法问题

#define定义宏

\#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏

在这里,参数同样也是简单粗暴的进行替换,如果参数是表达式,替换时并不会计算,而是直接将这个表达式作为宏的参数进行替换

#define SQUARE(x) x * x 
// 如果这样传参 
SQUARE(a + 1); // 替换后:a + 1 * a + 1; 这样计算出的结果就不符合预期

因此,在定义宏的时候应注意加上括号,保证运算优先级,定义宏的时候加上括号是一个好的习惯

#define SQUARE(x) (x) * (x)

宏和函数的对比

宏通常被用于只想一些简单的计算,那为什么在这里不选择使用函数呢?

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹
  2. 更为重要的是函数的参数必须声明为特定的类型,这要求函数传递参数必须找到正确的接口,所以函数只能在类型合适的表达式上使用。反之宏可以适用于整形、长整型、浮点型等可以用来比较的类型。宏是类型无关的

命名约定

因为宏的使用与函数的使用非常相似,语言不能直接帮助区分二者,于是有了一个编程习惯

宏名全部大写,函数名不全部大写

条件编译

#if 后面常跟一个常量表达式,以#endif结束

#ifdef后面跟上一个标识符,以#endif结束。用于执行定义过该标识符,应执行的操作

#ifndef后面跟上一个标识符,以#endif结束。用于执行未定义过该标识符,应执行的操作

条件编译内的语句,如果条件判定为真,则下方语句就会被编译,否则下方语句不会进行编译

头文件包含的方式

#include <stdio.h> #include "nihao.h"

库文件的包含

< >引入一个头文件,查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

本地文件的包含

" "引入一个头文件,先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。

引入库文件同样可以通过" "进行引入,只不过这样查找的效率会低,并且不容易区分引入的是库文件还是头文件

嵌套引入头文件的处理方式

同一个头文件可能会在多个地方被引入,这样会严重影响编译的效率,因此要进行避免多次引入

在每个头文件开头这样写

#ifndef __TEST_H__ 
#define __TEST_H__ 
//头文件的内容
#endif //__TEST_H__

或者这样写

#pragma once

其他预处理指令

#pragma pack() //在结构体部分中有介绍,用于修改默认对齐数

就先写这一个吧,以后遇到会更新补充

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值