admin管理员组文章数量:1599277
第0章 前言与相关知识
C语言与底层机器开发关联很大,是一种面向过程编程语言,什么是面向过程?简单点来说就是把一个事情拆分成几个步骤,逐一从上到下编译执行。
C语言(计算机编程语言)_百度百科 (baidu)
这个网址是有关C语言的简述,大家自行查阅
ASCII码
大家可以理解成计算机二进制储存与字符的映射
这是一张ASCII表,不需要特殊记忆,需要用的时候查表即可。
这里推荐大家记忆几组关键的就好
***A~Z对应着65~90
***a~z对应着97~122
***对应的大小写字符(a和A)的ASCII码值的差值是32
***数字字符0~9的ASCII码值从48~57
***在这些字符中ASCII码值从0~31 这32个字符是不可打印字符,无法打印在屏幕上观察
***换行符 \n 对应ASCII码为10
看段代码,打印出可打印的字符
int main()
{
int character = 0;
for (size_t i = 32; i <= 127; i++)
{
printf("%c ", i);
if (i % 16 == 15)
printf("\n");
}
return 0;
}
转义字符——转变原来的意思的字符
相关知识这些就足够了,接下来我们进入正题
第1章 内置数据类型与变量
什么是数据类型?数据的表达形式,比如说整数、小数(浮点型),计算机会接收不同的类型来进行操作,何为内置?就是C语言自带的类型
类型分类
整型大类:
短整型 short [int] [signed] short [int] unsigned short [int] 整型 int [signed] int unsigned int 长整型 long [int] [signed] long [int] unsigned long [int] 更长的整型 C99中引入 long long [int] [signed] long long [int] unsigned long long [int]
//其实这些书写起来有点麻烦,C语言给我们提供了一个stdint的头文件,不需要写这么麻烦,后续会演示
字符大类:
[signed] char //有符号的 unsigned char //无符号的
浮点型大类:
float double long double
变量
我们可以把变量理解为一个盒子,用来装数据的
创建一个类型——把数据放进这个盒子
画张图理解一下
变量分为全局变量和局部变量
全局变量是定义在大括号外面的变量,作用域广、生命周期长,可以全局操作
局部变量是定义在大括号内部的变量,作用域窄、生命周期短,只能在所在大括号内操作
当局部变量和全局变量重名时,局部变量优先使用!
整型——integer
int input; //变量的声明
int input = 0; //变量的初始化
intput = 12; //重新给变量赋值
int dogs, cats, pigs = 1; //这种写法可读性很差,需要避免这种情况
关于类型的取值范围问题
现在想象一下有两盏灯,亮表示1,灭表示0
不计算0,因为0没有正负之分
一共可以表示为01 10 11 这三个数
所以2bit可以表示的数有2^2 - 1
同理推广到32bit
第二种理解:
后续都可推广
理解整型溢出
#include <stdio.h>
#include <stdint.h>
int main() {
uint8_t u8_max = UINT8_MAX + 1;
int8_t i8_max = INT8_MAX + 1;
printf("u8_max + 1 = %u\ni8_max + 1 = %d\n", u8_max, i8_max);
return 0;
}
用段代码来理解,在无符号中如果溢出,则从最小值零开始,就像是汽车里程表,到达最大值,重新从零开始
在有符号中如果溢出,则从最小值开始-128开始逐渐增加
UINT8_MAX,INT8_MAX这两个常量是定义在这个头文件中
浮点型:整数部分和小数部分分开储存
IEEE754规定,任意一个二进制浮点数V都可以表示成如下形式:
V = (-1)^S * M * 2^E
(-1)^S :表示符号位,S取0或1
M:表示有效数字,>= 1 && <= 2
2^E:表示数位
存储过程:
M默认为1.xxxx的数,所以在保存时可以把这个1舍去,只保存xxxx小数部分,读取时再把第一位的1加上。这样做可以节省一位有效数字,此时M可以存24位
E在存储时需要加上一个中间数,对于float八位E,中间数为127,对于double11位E,中间数为1023
读取过程:
E不全为0或1
E减去127(1023)得到真实值,再将有效数字M前加上第一位1
E全为0
E=1 - 127(1 - 1023)得到真实值,有效数字M不再加上第一位1,还原为0.xxxx的小数,这种情况趋于0
E全为1
表示无穷大
了解了存储原理,我们可以知道浮点数在内存中可能无法精确保存
所以,两个浮点数比较大小,直接用“==”可能会存在一定的问题
这时,我们需要确定一个精度来限制范围
//f是个浮点数和5.6作比较,给个精度0.000001
if(abs(f - 5.6) <= 0.000001)判断条件
最近偶数舍入/银行家舍入
int main() {
float num1 = 3.24;
float num2 = 3.25;
float num3 = 3.26;
float num4 = 3.14;
float num5 = 3.15;
float num6 = 3.16;
printf("%.1f \n%.1f \n%.1f\n", num1, num2, num3);
printf("\n");
printf("%.1f \n%.1f \n%.1f\n", num4, num5, num6);
return 0;
}
得出结论:
尾数的整数部分是奇数,向上舍入,使其变为偶数
尾数的整数部分是偶数,保持不变,它已经是偶数
输出方式
int main() {
float num = 3.1415f;
//利用%l输出
printf("%f\n", num);
//利用科学计数法输出
printf("%e\n", num);
printf("%E\n", num);
//利用十六进制浮点数 p计数法输出
printf("%a\n", num);
printf("%A\n", num);
return 0;
}
bool类型:判断真假
需要引入stdbool头文件
int main() {
bool is_final = true;
//_Bool is_final = true;
_Bool is_open = false;
printf("%d\n%d", is_final, is_open);
return 0;
}
类型的大小
引入sizeof()操作符
sizeof()专门用来计算sizeof操作符数的类型长度即所占空间大小,单位是字节,其操作数可以是类型,也可以是变量或者表达式
表达式不计算
int main() {
//int num1 = 1;
//long num2 = 1;
//printf("%zd", sizeof(num2 + num1));
printf("%zd\n", sizeof(int));
printf("%zd\n", sizeof(char));
printf("%zd\n", sizeof(unsigned int));
printf("%zd\n", sizeof(double));
printf("%zd\n", sizeof(float));
printf("%zd\n", sizeof(long));
printf("%zd\n", sizeof(long long));
printf("%zd\n", sizeof(unsigned long));
return EXIT_SUCCESS;
}
宏定义#define与常量const
#include <stdio.h>
#define MAX 99
int main(){
const int num = 1;
//num = 2; //已经是常量,无法修改,这行代码会报错
printf("%d",MAX);
return 0;
}
初识输入与输出
printf:将内容格式化输出到屏幕上
int printf(
const char *format [,
argument]... );
例如:打印十六进制和八进制数
scanf:格式化输入数据在屏幕上
int scanf(
const char *format [,
argument]... );
vs认为scanf()不安全,需要检测返回值,它提供了一种更安全的scanf_s()来操作字符串
或者可以在文件开头宏定义一个他要求的#define _CRT_SECURE_NO_WARNINGS
来避免报错,相当于带了头盔,更安全了
具体的用法需要自己去摸索,讲不出来什么
超级重要:数据在内存中的存储
原码 反码 补码
原码:直接将数值按照正负数转化成二进制
反码:符号位不变,其它位按位取反
补码:反码+1 内存中存储的都是补码
是用补码,可以将符号位和数值域统一处理,CPU只有加法器,补码和原码相互转化,运算过程相同,不需要额外的硬件设计
补码转换成原码:补码取反+1 后文还会有介绍
大小端字节序
超过一个字节的数据在内存中存储时,会有存储顺序的问题
大端存储模式:数据的低位字节存储到内存的高地址处,数据的高位字节存储到内存的低地址处
小端存储模式:数据的高位字节存储到内存的高地址处,数据的低位字节存储到内存的低地址处
举个例子:
那如何判断大小端呢?
我们可以找第一个字节,通过char* 取到一字节的地址
int judge()
{
int i = 1;
/*取出i的地址,强制转换成char*解引用,只取出i的第一个字节
* 如果为1,则机器为小端; 如果为0,则为大端
*/
return (*(char*)&i);
}
int main()
{
int ret = judge();
if (ret == 1) printf("机器为小端字节序存储\n");
else printf("机器为大端字节序存储\n");
return 0;
}
数据存储的轮回规律
所有类型都可推广
第2章 运算符:操作控制数据
算数运算符: + - * / %
+ - * 比较简单,直接略过
看下除法
除号两端都是整数,执行的是整数除法,得到的结果是整数
如果想得到小数,两个运算数中至少有一个是浮点数
看下取模%,即两个整数相除的余数。只能用于整数,不能用于浮点数
负数求模的规则是:结果的正负号只有第一个运算数的正负号决定
补充:数的进制
把每一个数位理解成权重
二进制->八进制,从二进制序列右边低位开始向左每三个二进制位组成一个八进制位数字,例如:
同理,二进制->十六进制,从二进制序列右边低位开始向左每三个二进制位组成一个八进制位数字,例如:
原码:按+-形式转化成的二进制数
反码:原码符号位不变,其他位按位取反
补码:反码+1
补码->原码:补码取反+1 理由:二进制数先-1后取反与先去反后+1结果一样
注:整数的三种码完全一样
赋值运算符=
这个不是等号,是将右值丢给左边已知变量
复合赋值符
对于自增自减的操作需要用到
+= -= *= /= %=
>>= <<= ^= |= ^=
相等运算符 ==
==才是C语言中的相等运算符,与“=”一定要区分开
!=不等运算符
>= 大于等于 <=小于等于
以上常用于循环中的条件判断
自增++自减--
分为前置和后置
前置:先加(减),后使用
后置:先使用,后加(减)
int a = 10;
int b = a++;
printf("a=%d b=%d\n",a,b);// 11 10
位操作符
~按位取反:正常每一位按位取反,0变1,1变0
>>按位右移
逻辑右移:不考虑符号位
将运算对象的值每一位向右移动指定位数,左侧用0补齐
算术右移:考虑符号位
将运算对象的值每一位向右移动指定位数,左侧用符号位补齐
<<按位左移
将运算对象的值每一位向左移动指定位数,左侧用0补齐
对于无符号整型
右移n位相当于除以2的n次幂
左移n位相当于乘以2的n次幂
&按位与:同时为1才为1,同时为true才为true
作用:
将某数特定位置数清零
检查某数特定位置是否为1
|按位或:有1就为1,二者有一个为true就为1
作用:
设置特定位 //让特定位置开关打开
^按位异或:0和1的组合才为1
作用:
翻转特定位 //关闭开的位,打开关的位
案例总结:用掩码控制灯位、不创建新的变量交换两个变量的值
void print_bin(uint8_t num);
int main() {
uint8_t starting = 0b00001100;
printf("初始状态:0b");
print_bin(starting);
printf("\n");
printf("关闭低电量灯:0b");
uint8_t closing_low = starting & 0b11111000;
print_bin(closing_low);
printf("\n");
printf("正常工作:0b");
uint8_t final = closing_low ^ 0b00001011;
print_bin(final);
printf("\n");
return EXIT_SUCCESS;
}
void print_bin(uint8_t num) {
for(int i = 7; i >= 0 ; i--)
{
printf("%d", (num >> i) & 1);
}
}
//不创建第三个变量交换两个变量值
int main() {
int a = 3;
int b = 6;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("a = %d\nb = %d", a, b);
return 0;
}
条件表达式:(? :
)
xxxx ? xxxx : xxxx
a b c
a为真,执行b
a为假,执行c
逻辑运算符
&& 且
||或
短路运算的原理:当有多个表达式时,左边的表达式值可以确定结果时,就不再继续运算右边的表达式的值
表达式1 && 表达式2 //若表达式1为假,则没有必要计算表达式2了,整个体系都为假
表达式1 || 表达式2 //若表达式1为真,则没有必要计算表达式2了,整个体系都为真
操作符优先级
第3章 分支与循环:决策与控制
C语言顺序结构、分支结构、循环结构这三种结构
朴素点理解:
顺序:每天早上起床后,会按照一定的顺序进行日常活动。起床洗漱,穿衣服,吃饭,赶早八,一条路径走到头。
选择:洗漱完,吃啥?面包?包子?这时候路径就会有多种选择了。
循环:吃完饭后,上早八,苦逼大学生日复一日地循环着。
分支
if-else语句
if
(
expression
)
statement;
if
(
expression
)
statement
;
else
statement;
if(expression)
statement;
else if(expression)
statement;
else if
//....
else
statement;
括号内表达式如果为真,则执行statement,若为假,则按顺序往下执行
悬空else问题
如果有多个if和else,else总会和最近的if相匹配
int main()
{
int a = 0;
int b = 2;
if(a == 1)
if(b == 2)
printf("hehe\n");
else
printf("haha\n");
return 0;
}
为什么啥都不输出?这个是排版问题,微软会自动匹配组合好,else和第二个if匹配,第一个if里嵌套了一个if-else语句,第一个if为假,直接跳到return 0
所以大括号{ }很重要!!!!!!
防御性编程——条件判断中与常量做比较,将常量放在左侧
//...
if(3 == x){
//......
}
//....
为什么这样写?避免将“==”写成“=”引起难以调试出的bug,大家多写写就懂了
switch-case语句
switch ( expression )
{
// declarations
// . . .
case constant_expression:
// statements executed if the expression equals the
// value of this constant_expression
break;
default:
// statements executed if expression does not equal
// any case constant_expression
}这里的default可以在switch的“{}”内任意位置,只不过习惯放到最后
使用switch-case语句时一个case结束后不要忘了break!!!!!
如果没有break语句,将会逐一执行!!!!!!
修改后:
循环
while循环与do-while循环
while(expression)
statement;如果expression为真则重复执行statement,否则不执行
比较简单没有什么需要注意的
do
{
statement;
}
while(expression);
先执行do里的语句,执行完之后判断expression是否为真,如果为真,则继续循环,否则停止跳出循环
值得注意的是,while()循环是先判断条件后执行,而do-while循环则是先执行后判断,至少会执行一次语句
for循环
for(初始的循环变量 ;循环变量满足的条件 ;调整循环变量 )
{
statement;
}
先明确初始的循环变量,之后判断条件,条件为真,执行statement,之后进行循环变量调整再判断条件,往复执行,指导不满足循环条件跳出
continue和break
continue即继续,跳过continue之后的语句,重新进行循环
break即打破,直接跳出整个循环
int i = 1;
while(i<=10)
{
if(i == 5)
break;//当i等于5后,就执⾏break,循环就终⽌了
printf("%d ", i);
i = i+1;
}
//1 2 3 4
int i = 1;
while(i<=10)
{
if(i == 5)
continue;//当i等于5后,跳过之后的语句,继续执行循环,此时跳过了i = i + 1,i一直等于5,陷入死循环
printf("%d ", i);
i = i+1;
}
int i = 1;
for(i=1; i<=10; i++)
{
if(i == 5)
continue;//这⾥continue跳过了后边的打印,来到了i++的调整部分
printf("%d ", i);
}
//1 2 3 4 6 7 8 9 10
循环相关练习
//任意输入一个正整数N,统计1~N之间奇数的个数和偶数的个数,并输出。
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d",&n);
int o = 0;
int j = 0;//o为偶数,j为奇数
for(int i = 1; i <= n; i++)
{
if(i % 2 == 0) o++;
else j++;
}
printf("%d %d",j,o);
return 0;
}
//所有三位整数中,有多少个质数。
#include <stdio.h>
#include <math.h>
int main()
{
int i = 1;
int num = 0;
for(i = 101; i <= 999; i+=2)
{
int temp = 1;
for(int j = 2; j <= sqrt(i); j++)
{
if(i % j == 0)
{
temp = 0;
break;
}
}
if(temp == 1) num++;
}
printf("%d",num);
return 0;
}
//打印99乘法口诀表
#include <stdio.h>
int main() {
for(int i = 1; i <= 9; i++)
{
for(int j = 1; j <= i; j++)
{
printf("%d*%d=%2d ",j,i,i*j);
}
printf("\n");
}
return 0;
}
/*有一个数字魔法,给你一个正整数n,如果n为偶数,就将他变为n/2, 如果n为奇数,就将他变为乘3加1
不断重复这样的运算,经过有限步之后,一定可以得到1
牛牛为了验证这个魔法,决定用一个整数来计算几步能变成1*/
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d",&n);
int step = 0;
while(n != 1)
{
if(n % 2 ==0)
n /= 2;
else
n = 3*n + 1;
step++;
}
printf("%d",step);
return 0;
}
//一行,一个整数,表示1~2019中共有多少个数包含数字9。
#include <stdio.h>
int main()
{
int num = 0;
int tmp = 0;
for(int i = 1; i <= 2019; i++)
{
int m = i;
while(m)
{
if(m % 10 == 9)
{
num++;
break;
}
else
m /= 10;
}
}
printf("%d",num);
return 0;
}
/*输入数据有多组,每组占一行,包括两个整数m和n(100 ≤ m ≤ n ≤ 999)
对于每个测试实例,要求输出所有在给定范围内的水仙花数,就是说,输出的水仙花数必须大于等于m,并且小于等于n,如果有多个,则要求从小到大排列在一行内输出,之间用一个空格隔开; 如果给定的范围内不存在水仙花数,则输出no; 每个测试实例的输出占一行。
*/
#include <stdio.h>
#include <stdio.h>
int main()
{
int a,b;
int tmp = 0;
while((scanf("%d %d",&a,&b)) != EOF)
{
for(int i = a; i <= b; i++)
{
if((pow(i%10,3)+pow(i/10%10,3)+pow(i/100,3)) == i)
{
tmp = 1;
printf("%d ",i);
}
}
if(tmp == 0)
{
printf("no\n");
}
else
{
printf("\n");
}
}
return 0;
}
/*变种水仙花数 - Lily Number:把任意的数字,从中间拆分成两个数字,比如1461 可以拆分成(1和461),(14和61),(146和1),如果所有拆分后的乘积之和等于自身,则是一个Lily Number。
例如:
655 = 6 * 55 + 65 * 5
1461 = 1*461 + 14*61 + 146*1
求出 5位数中的所有 Lily Number。*/
#include <stdio.h>
int main()
{
for(int i = 10000; i <= 99999; i++)
{
if((i/10000)*(i%10000) + (i/1000)*(i%1000) + (i/100)*(i%100) +(i/10)*(i%10) == i)
{
printf("%d ",i);
}
}
return 0;
}
/*公务员面试现场打分。有7位考官,从键盘输入若干组成绩,每组7个分数(百分制),去掉一个最高分和一个最低分,输出每组的平均成绩。
(注:本题有多组输入)
输入描述:
每一行,输入7个整数(0~100),代表7个成绩,用空格分隔。
输出描述:
每一行,输出去掉最高分和最低分的平均成绩,小数点后保留2位,每行输出后换行。*/
//注意,这题有点坑,这题多组输入,很多朋友可能会用for循环遍历一个数组,这样只有一组数据
#include <stdio.h>
int main() {
int score = 0;
int max = 0;
int min = 100;
double sum = 0;
int cnt = 0; //用于判断是否输入了7个数
while ((scanf("%d ", &score)) != EOF) {
if (score > max) max = score;
if (score < min) min = score;
sum += score;
cnt++;
if (cnt == 7) {
printf("%.2lf\n", (sum - max - min) / 5.0);//别忘了换行符,一组一行
cnt = 0;
max = 0;
min = 100;
sum = 0;
//一组数据处理完成,数据初始化,等待多组数据的输入和处理
}
}
return 0;
}
//输出1到n之间的回文数
#include <stdio.h>
int main() {
int n;
scanf("%d",&n);
for (int i = 1; i < n; i++) {
int temp = i;
int sum = 0;
while(temp){
sum = sum * 10 + temp % 10;
temp /= 10;
}
if(sum == i) printf("%d\n",i);
}
return 0;
}
第4章 数组
数组的概念
数组是存放一组相同类型元素的集合
两个重点:一个或多个数据 数据类型相同
大家学过线性代数就会有更好的理解,可以将数组理解成矩阵
一维数组
数组创建
type arr_name[size];
类型 数组名 数组大小(常量值)、
例如:
int arr[10];
double height[5];
//此时就创建好数组
数组初始化
//完全初始化
int arr1[5] = {0,1,2,3,4};
//不完全初始化
int arr2[6] = {1}; //第一个元素初始化为1,剩下的元素默认为0
//错误初始化
int arr3[3] = {1,2,3,4};//数据溢出,装不下
数组类型
数组算是⼀种⾃定义类型,去掉数组名留下的就是数组的类型
int arr[10];//类型:int [10]
double height[5];//类型:double[5]
数组的使用
数组用来存储数据,存储数据是为了使用数据
数组下标/索引——从0开始
比如这个数组
int age[5] = {7,23,22,18,17};
[ ]叫下标引⽤操作符,有了这个就可以找到想要的数据了
想要第三个元素,则需要age[2]
#include <stdio.h>
int main(){
int age[5] = {7,23,22,18,17};
printf("%d\n",age[2]);//第三个元素22
printf("%d\n",age[4]);//第五个元素17
return 0;
}
想要打印整个数组或者给数组每个元素赋值,可以通过循环语句实现,遍历整个数组
数组在内存中的存储
sizeof()计算数组大小
这里切记 sizeof()是关键字,而不是函数!
如果是数组名,则是整个的数组大小
如果是一个具体的数组元素,则是这个数组类型的大小
两者相除即可得到数组元素个数
二维数组
概念
把一维数组作为元素,即可得到二维数组。二维数组作为元素,即可得到三维数组
二维数组创建
type arr_name[num1] [num2];
例如:
int arr[3][4];//三行四列 每行有四个元素
double day[12][31]; //十二行三十一列 每行有三十一个元素
初始化
//不完全初始化
int arr1[3][4] = {1,2};
int arr2[3][4] = {0};
先给第一行初始化,第一行装满之后再装第二行,逐次填充
//完全初始化
int arr3[3][4] = {1,2,3,4, 2,3,4,5, 3,4,5,6};
//按行初始化
int arr4[3][4] = { {1,2}, {3,4}, {5,6} };
//初始化时可省略行,但列坚决不能省略
//省略了列数,不知道一行能装多少
int arr5[ ] [3] = {1,2}:
int arr6[ ] [5] = {1,2,3,4,5,6,7,8}:
int arr7[ ] [4] = { {1,2}, {2,3}, {3,4} };
这个初始化原则同上,先将行装满,再逐次填充
二维数组的使用
索引/下标同理也是从零开始
对于遍历二维数组,两次循环就可以搞定,这里省略
二维数组在内存中的存储
数组的应用
从两端向中间汇聚,改变字符
int main() {
char arr1[] = "***************";
char arr2[] = "H E L L O C ! !";
int left = 0;
int right = sizeof(arr2) - 2;
while (left <= right) {
printf("%s\n",arr1);
Sleep(100);//睡眠100毫秒,即每间隔100毫秒再执行
system("cls");//清理屏幕
arr1[left] = arr2[left];
arr1[right] = arr2[right];
left++;
right--;
}
printf("%s\n", arr1);
}
二维数组模拟农场农作物成熟
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROWS 10 //行数
#define COLS 10 //列数
#define EMPTY 0 //未种植
#define PLANTED 1 //已种植作物
#define MATURED 3 //已经成熟作物
void print_farm(int farm[][COLS]);
int main() {
int farm[ROWS][COLS]; //用二维数组定义农场
srand((unsigned int)time(NULL)); //生成随机种子
for (int i = 0; i < ROWS; i++){ //生成农场的行
for (int j = 0; j < COLS; j++){ //生成农场的列
farm[i][j] = (rand() % 2 == 0) ? EMPTY : PLANTED;
//农场一块土地是空的还是已种植作物各自有百分之五十的概率
}
}
print_farm(farm);
Sleep(1000);
//模拟作物成长过程,并不断更新农场地图
for (int time = 0; time < 10; time++){
system("cls");
for (int i = 0; i < ROWS; i++){
for (int j = 0; j < COLS; j++){
if (farm[i][j] == PLANTED){
if (rand() % 10 < 3){ //假设每个时间段农作物成熟的机率为30%
farm[i][j] = MATURED;
}
}
}
}
print_farm(farm);
Sleep(1000);
}
return 0;
}
//封装打印农场模拟作物成熟的函数
void print_farm(int farm[][COLS]){
for (int i = 0; i < ROWS; i++){
for (int j = 0; j < COLS; j++){
switch (farm[i][j]){
case EMPTY :
printf(". "); //.代表空地
break;
case PLANTED :
printf("* "); //*代表已种植作物
break;
case MATURED :
printf("# "); //#代表已成熟作物
break;
}
}
printf("\n");
}
}
二分查找
二分查找使用前提是一个有序的数组
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,10};
int target = 0;
int flag = 0;
scanf_s("%d", &target);
int left = 0;
int right = sizeof(arr) / sizeof(int) - 1;
int mid = 0;
while (left <= right) {
mid = (left + right) / 2;
if (arr[mid] > target) right = mid - 1;
else if (arr[mid] < target) left = mid + 1;
else {
flag = 1;
break;
}
}
if (1 == flag) printf("找到了,下标为%d\n", mid);
else printf("未找到!\n");
return 0;
}
第5章 函数——功能块
在国外教材中,英文为function,其实函数就是一个实现某种特定功能的工厂
我们忽略库函数,直接开始讲自定义函数。
自定义函数
ret_type fun_name(形式参数)
{
}
//返回类型 函数名 {}括起来的部分是函数体
举个例子
#include <stdio.h>
int add(int a, int b);
int main() {
int x = 1, y = 4;
int re = add(x, y);
printf("%d\n", re);
return 0;
}
int add(int a, int b) {
return a + b;
}
形参和实参
形参是实参的一份临时拷贝
数组做函数的参数
例如:
我们这里需要注意几个细节:
函数的形参要和实参个数匹配
形参如果是一维数组,可以省略行
形参如果是二维数组,行可省略,列不可省略
数组传参,形参不会创建新的数组
形参和实参操作的数组是同一个数组
嵌套调用
例如:计算某月有多少天
#include <stdbool.h>
int return_day(int year, int month);
bool is_leap_year(int year);
int main() {
int year, month;
scanf_s("%d %d", &year, &month);
int day = return_day(year, month);
printf("%d年%d月有%d天\n", year, month, day);
return 0;
}
int return_day(int year, int month) {
int days[12] = { 31,(is_leap_year(year) ? 29 : 28),31,30,31,30,31,31,30,31,30,31 };
return days[month - 1];
}
bool is_leap_year(int year) {
return ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0));
}
main函数调用了scanf_s return_day printf
return_day调用了is_leap_year
就是一层一层套娃,说到这个名词解释要有点印象
链式访问
将一个函数的返回值作为另一个函数的参数,如同链条一样串起来
printf("%d", printf("%d", printf("%d", printf("%d", 12))));
这段代码结果是什么?
int printf( const char *format [, argument]... );
//返回类型是返回输出的字符数
extern和static
作用域:一段代码中所用到的名字的有效范围
生命周期:一个变量从变量创建(申请内存)到变量销毁(收回内存)的时间段
局部变量作用域是变量所在的局部范围{ }括起来的范围,进作用域创建创建变量,生命周期开始,出作用域生命周期结束
全局变量的作用域即是整个工程,生命周期是整个程序
同理 static修饰函数同全局变量一样,有相同性质
第6章 指针
地址与取地址
计算机中,地址是内存的编号,是内存中储存数据的唯一标识
我们为什么需要地址呢?为了方便快速地查找数据并操作数据
举个简单的例子,你去朋友的宿舍,如果没有房间号(地址编号),你是不是要一层楼一层楼挨个门去敲(逐一访问)直到找到你的朋友,这样很容易被揍的,如果给你个确切的房间号(地址编号)你就会很快到达。
在计算机中也是如此,为了快速找到目标文件或者目标数据,如果没有地址,想象一下会有多么困难。
取地址&这个操作符我们在scanf()函数中使用过
我们现在用段代码来理解
#include <stdio.h>
int main()
{
int place[5] = { 101, 102, 103, 104, 105 };
//我们现在要找到103房间,怎么操作?
int tagert_place = place[2]; //目标103房间
printf("寻找中 %d 中。。。。\n", tagert_place);
for (int i = 0; i < 5; i++)
{
printf("住户 %d 的地址为: %p\n", place[i], &place[i]);
if (place[i] == tagert_place)
{
printf("找到目标住户 %d 的地址为: %p\n", tagert_place, &place[i]);
break;
}
else
printf("未找到!\n");
}
return 0;
}
%p 是地址的占位符,而取地址&操作符就是找到目标地址
我们看下运行结果
这里我们还是逐一查找,为了避免“被揍”的风险,该如何操作?
我们就需要引入指针了
指针变量
在现实生活中我们的具体地址是不会随意泄露的,而计算机中的地址也是,这时就需要一个工具来查找具体地址了,这便是指针。
指针是一种特殊的变量,指针不存在具体的数值,指针用于储存另一个变量的地址
这就好比外卖小哥只知道你的门牌号(地址)和手机号(手机号也有隐私保密)而你的其他相关信息他是不知道的
这是指针变量的定义与初始化
注意看这两种写法的不同:*的位置不同
第一种写法是微软的风格,强调这个ptr_place_103变量是个int*(整型指针)
而第二种写法更强调ptr_place_102这个变量是个指针
二者含义相同,只是风格习惯不同
请大家牢记:
指针指向一个变量,储存这个变量的地址
*用于访问这个变量地址上的值,即变量的值
在同一环境下,指针变量的大小一致,不论是什么类型的指针变量
在x86的环境下,地址表示的是32位的0或1组成的二进制数位,所以指针变量是4个字节
在x64的环境下,地址表示的是64位的0或1组成的二进制数位,所以指针变量8个字节。
那指针变量类型有什么意义呢?
指针变量类型决定解引用时指针对内存空间的访问权限大小
即指针向前或向后走一步的步长
const修饰指针
直接上结论:
const在*左边,限制*ptr,*ptr无法被修改,但ptr可修改
const在*右边,限制ptr,ptr不可被修改,但*ptr可修改
指针运算
指针+-访问数组
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int* ptr = arr;//&arr[0]
for (size_t i = 0; i < 10; i++)
{
printf("%d ", *(ptr + i));
}
ptr = &arr[5];//取第六位地址
printf("\n%d\n", *ptr);
ptr -= 2;//退回到第四位
printf("%d\n", *ptr);
return 0;
}
指针-指针模拟strlen函数
//模拟strlen函数
size_t my_strlen(char* s);
int main()
{
char str[1024];
size_t size = my_strlen(gets(str));
printf("%d\n", size);
return 0;
}
size_t my_strlen(char* s)
{
char* start = s;
//统计'\0'前出现的字符串长度
while (*s != '\0') s++;
return s - start;//指针 - 指针 地址 - 地址
}
野指针
指向的位置不可知(随机、不正确、无明确界限)
指针真正的意义是外部服务操作,我们熟悉的快捷方式就是指针
为什么会有野指针?
指针未初始化,默认值就是随机值,很危险
指针越界访问,比如越界访问数组
指针指向的空间已经释放
怎么规避野指针?
初始化指针,没有明确作用就置为空指针
避免越界访问
指针不再使用时,即时置为空指针,在使用前检查指针有效性
避免返回局部变量的地址
assert断言
assert( expression );
//必须包含头文件<assert.h>
//expression为真,程序正常运行
//expression为假,在stderr中报错,显示未通过的表达式及包含表达式的行号和文件名
在确定程序没问题后,可以在头文件之上添加宏#define NDEBUG关闭assert的功能
需要注意,此宏只能在debug环境下使用
传址调用和传值调用
传值调用:实参传递给形参时,形参会单独创建一份临时空间接收实参,对形参的修改并不影响实参
//传值调用
void Switch(int a, int b);
int main()
{
int x, y;
scanf_s("%d %d", &x, &y);
printf("before switching:x=%d y=%d\n", x, y);
Switch(x, y);
printf("after switching:x=%d y=%d\n", x, y);
return 0;
}
void Switch(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
传址调用:让函数和主调函数之间真正产生联系,在函数内部可以修改主调函数中的变量
//传址调用
void Switch(int* a, int* b);
int main()
{
int x, y;
scanf_s("%d %d", &x, &y);
printf("before switching:x=%d y=%d\n", x, y);
Switch(&x, &y);
printf("after switching:x=%d y=%d\n", x, y);
return 0;
}
void Switch(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
数组名新理解
sizeof(数组名)和 &数组名这两种情况表示整个数组,其余情况下,数组名就是首地址
指针访问数组
int arr[5];
int* ptr = arr;
//......
//for循环里
printf("%d ",*(ptr + i) );
//......
*(p + i)
分析:数组名是首元素地址,赋值给了ptr,则ptr等价于arr
则arr[i]等价于ptr[i]
*(arr + i) *(ptr + i) 他们上下对应等价
交换律:
*(i + arr) *(i + ptr)
则i[arr] i[ptr] 同样上下等价!
一维数组传参的本质:传递数组首元素的地址,本质还是指针
//一维数组传参本质
//void test(int arr[], size_t size);
//
//int main()
//{
// int arr[5] = { 1,2,3,4,5 };
// size_t sz = sizeof(arr) / sizeof(int);
// test(arr,sz);
// return 0;
//}
//void test(int arr[], size_t size)
//{
// for (size_t i = 0; i < size; i++)
// {
// printf("%d ", arr[i]);
// }
//}
//其实是一样的
void test(int* arr, size_t size);
int main()
{
int arr[5] = { 1,2,3,4,5 };
size_t sz = sizeof(arr) / sizeof(int);
test(arr, sz);
return 0;
}
void test(int* arr, size_t size)
{
for (size_t i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
}
冒泡排序:两两相邻元素进行比较
//冒泡排序
#include <stdbool.h>
void bubble_sort(int* arr, size_t size);
int main()
{
int arr_test[10] = { 8,3,2,1,0,9,6,7,5,4 };
size_t sz = sizeof(arr_test) / sizeof(int);
bubble_sort(&arr_test, sz);
for (size_t i = 0; i < sz; i++)
{
printf("%d ", arr_test[i]);
}
return 0;
}
void bubble_sort(int* arr, size_t size)
{
for (size_t i = 0; i < size - 1; i++)//两两排序,有size个元素,则有size - 1趟
{
bool flag = 1;//假设这一趟已经有序
for (size_t j = 0; j < size - 1 - i; j++)//排序完成的元素无需作比较,逐次往后比较
{
if (arr[j] > arr[j + 1])//顺序,如果降序则改为<
{
flag = 0;//发生了交换,说明无序
int tmp = arr[j];//创建第三个变量方便排序
arr[j] = arr[j + 1];
arr[j + 1] = tmp;//升序完成
}
}
if (flag) break;//未交换说明有序,直接跳出循环!
}
}
选择排序:从首元素开始与剩下元素逐一比较
//选择排序
#include <stdbool.h>
void select_sort(int* arr, size_t size);
int main()
{
int arr_test[10] = { 8,3,2,1,0,9,6,7,5,4 };
size_t sz = sizeof(arr_test) / sizeof(int);
select_sort(&arr_test, sz);
for (size_t i = 0; i < sz; i++)
{
printf("%d ", arr_test[i]);
}
return 0;
return 0;
}
void select_sort(int* arr, size_t size)
{
for (size_t i = 0; i < size - 1; i++)//有size个元素,则只少有size - 1趟
{
bool flag = 1;//假设这一趟已经有序
for (size_t j = i + 1; j < size; j++)//每一轮前面的元素都要和后面所有元素比较,比到最后一个
{
if (arr[i] > arr[j])
{
flag = 0;//发生交换
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
if (flag) break;
}
}
多级指针
//多级指针——套娃
int main()
{
int a = 10;
int* ptr_a = &a;//一级指针
int** ptr_a_a = &ptr_a;//二级指针
int*** ptr_a_a_a = &ptr_a_a;//三级指针
***ptr_a_a_a = 40;
//ptr_a_a_a存着ptr_a_a的地址,ptr_a_a存着ptr_a的地址,ptr_a存着a的地址,一层层指向
//*ptr_a_a_a等价于ptr_a_a **ptr_a_a_a等价于ptr_a ***ptr_a_a_a等价于*ptr_a 即a
printf("%d\n", ***ptr_a_a_a);
printf("%p\n", **ptr_a_a_a);
printf("%p\n", *ptr_a_a_a);
}
大家可以通过这两张网上的图来加深一下印象
指针数组——存放指针的数组
指针数组的每个元素是地址,又可以指向一块空间
int* arr[3];
//这便是一个指针数组
去掉数组名就是类型: int*[3]
我们利用指针数组模拟一下二维数组
//模拟二维数组
int main()
{
int arr1[] = { 0,1,2,3,4 };
int arr2[] = { 1,2,3,4,5 };
int arr3[] = { 2,3,4,5,6 };
//数组名是数组首元素的地址,类型是int*,就可以存放在ptr_arr数组中
int* ptr_arr[3] = { arr1, arr2, arr3 };
for (size_t i = 0; i < 3; i++)
{
for (size_t j = 0; j < 5; j++)
{
printf("%d ", ptr_arr[i][j]);//*(*(ptr_arr + i) + j)
}
printf("\n");
}
return 0;
}
ptr_arr[i]访问pt_arr数组的元素,ptr_arr[i]找到的数组元素指向整型一维数组,
而ptr_arr[i][j]就是整形一维数组元素,并非二维数组,因为每一行并不连续
字符指针——存放字符地址的指针
以前定义一个字符: char ch = 'w';
char* pc = &ch;//pc就是字符指针
现在: const char* p = "abcdef";
printf("%c\n","abcdef"[3]);
printf("%c\n",p[3]);
//可以把字符串想象成字符数组,但是这个数组不能修改
//字符常量字符串出现在表达式中,它的值是首字符的地址
int main()
{
char str1[] = "hello CS.";
char str2[] = "hello CS.";
char* str3 = "hello CS.";
char* str4 = "hello CS.";
if (str1 == str2) printf("same\n");
else printf("not same\n");
puts("**********");
if (str3 == str4) printf("same\n");
else printf("not same\n");
return 0;
}
str1[ ]和str2[ ] 是两个不同的数组,地址不同
str3和str4指向同一个字符串常量,地址相同
数组指针——存放数组地址的指针 指针指向数组
辨析:
int* ptr1[10]; //存放10个int*指针的指针数组
int (*ptr2)[10]; //ptr2是指针变量,指向大小为10的整型数组,数组指针
( )和[ ]优先级相同,从左向右结合
而( ) [ ]的优先级都高于*
如何初始化?
int arr[10] = { 0 };
int (*p)[ 10 ] = &arr;
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
//指针访问数组
int (*ptr)[10] = &arr;//ptr指向10个整形元素的数组
printf("%p\n", arr);//数组首地址
printf("%p\n", arr + 1);//首地址+1
printf("%p\n", ptr);//首地址
printf("%p\n", ptr + 1);//首地址+40
return 0;
}
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
指针访问数组
int (*ptr)[10] = &arr;//ptr指向10个整形元素的数组
for (size_t i = 0; i < 10; i++)
{
printf("%d ", (*ptr)[i]);//*(*ptr + i)
/*
ptr == &arr
(*ptr) == *(&arr) == arr
*/
}
return 0;
}
二维数组传参本质——传递第一行一维数组的地址
二维数组的元素是一维数组,首元素是第一行,
则首元素的地址是第一行元素的地址 类型:int ( * ) [num]
//二维数组传参本质——传递第一行一维数组的地址
void Print(int (*arr)[5], int r, int c);
int main()
{
int arr[3][5] = { {0,1,2,3,4}, {1,2,3,4,5}, {2,3,4,5,6} };
Print(arr, 3, 5);
return 0;
}
void Print(int (*arr)[5], int r, int c)
{
for (size_t i = 0; i < r; i++)
{
for (size_t j = 0; j < c; j++)
{
printf("%d ", *(*(arr + i) + j));
}
printf("\n");
}
}
函数指针——存放函数地址
//函数也存在地址 int test(int a, int b); int main() { int x = 3; int y = 9; printf("%p\n",test); printf("%p\n", &test); return 0; } int test(int a, int b) { return a + b; }
函数名就是函数的地址
将函数的地址存放起来,就得创建函数指针变量
int (*ptr_test) (int a, int b);
//通过函数指针调⽤指针指向的函数 int Add(int a, int b) { return a + b; } int main() { int (*ptr_Add)(int a, int b) = Add; printf("%d\n", ptr_Add(5, 8)); printf("%d\n", (*ptr_Add)(5, 8)); return 0; }
(*(void (*)())0)();
void (*signal(int , void(*)(int)))(int);
第6章 结构体、枚举与联合
对齐原则
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对齐到对齐数的整数倍的地址处
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值
VS默认为8
Linux中没有默认对齐数 gcc默认对齐数就是成员自身大小
3. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍
4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
结构体定义与访问
我们利用结构体就可以定义我们自己想定义的类型
比如说一个学生相关信息:id,年龄,性别……
结构体最好定义在main函数外部
struct Stduent{
int id;
int age;
char gender[10];
float height;
//.....
} Student;
Student就是我们定义的结构体类型。其中包含id,年龄,性别,身高这几个成员
struct {
int id;
int age;
char gender[10];
float height;
//.....
} Student;
注意看上下区别,下面的student并未写入,下面的结构体称为匿名结构体
****结构体初始化****
struct Studuent{
int id;
int age;
char gender[16];
float height;
//.....
} Student;
Student kunkun = {101, 18, "man", 1.8};
这便是结构体的初始化,
需要注意:1、用花括号括起来数据,与数组的方括号区分开来
2、需要按照自己定义的顺序来初始化
如果不想按照顺序,需要以下操作:
struct student kunkun = {.age = 18, .id = 101, .height = 1.8, .gender = "man"};
.是用来访问结构体成员的符号
这里引入一个关键字typedef
typedef struct Studuent{
int id;
int age;
char gender[16];
float height;
//.....
} Student;
//初始化
student kunkun = //...........
有了这个关键字就无需在初始化时加上struct
****结构体成员访问****
typedef struct Date {
int year;
int month;
int day;
}Date;
int main() {
Date today = { 2024, 4, 18 };
printf("利用.访问:\ndate : %d-%d-%d\n", today.year, today.month, today.day);
Date* date_ptr = &today;
printf("利用指针访问:\ndate : %d-%d-%d\n", date_ptr->year, date_ptr->month, date_ptr->day);
}
结构体作为函数参数
不废话,直接上案例
//录入一个学生信息,并修改其成绩
typedef struct Student {
int id;
char name[64];
float score;
}Student;
void print_student(Student stu);
float update_by_value(Student stu, float new_score);
void update_by_ptr(Student* stu, float new_score);
int main()
{
Student stu = { 01, "kunkun",2.5 };
puts("未修改之前的信息:");
print_student(stu);
puts("\n修改之后的信息,通过值传递:");
update_by_value(stu, 66);
print_student(stu);
puts("\n修改之后的信息,通过指针传递:");
update_by_ptr(&stu, 66);
print_student(stu);
}
void print_student(Student stu) {
printf("id:%d\n", stu.id);
printf("name:%s\n", stu.name);
printf("score:%.2f", stu.score);
}
float update_by_value(Student stu, float new_score) {
stu.score = new_score;
return new_score;
}
void update_by_ptr(Student* stu, float new_score) {
stu->score = new_score;
}
结构体作为函数的返回值
typedef struct Position {
int x;
int y;
}Position;
position get_position(viod);
int main() {
Position my_position = get_position();
Position your_position = get_position();
printf("Position: (%d,%d)\n", my_position.x, my_position.y);
printf("Position: (%d,%d)\n", your_position.x, your_position.y);
return 0;
}
Position get_position(viod) {
Position p = { 10 , 9 };
return p; //返回一个结构体副本
}
这种写法有什么好处?安全性高!!!
结构体初始化没有在main函数中,放在了自己定义的一个函数中,只要这个函数执行完就立马销毁,位置难以寻找!!!!!
结构体数组
typedef struct Point {
int x;
int y;
int z;
}Point;
int main() {
Point p[3] = {
{1,2,0},
{3,4,9},
{11,12,66}
};
for (size_t i = 0; i < 3; i++)
{
printf("p[%zd] = (%d,%d,%d)\n", i, p[i].x, p[i].y, p[i].z);
}
return 0;
}
嵌套结构体
typedef struct Address {
char country[64];
char city[32];
}Address;
typedef struct Person {
char name[16];
int age;
Address address;
}person;
int main() {
Person kunkun = {
"kunkun",
18,
{"China","BeiJing"}
};
puts("通过.访问");
printf("name:%s\nage:%d\ncountry:%s\ncity:%s\n", kunkun.name, kunkun.age, kunkun.address.country, kunkun.address.city);
puts("通过指针访问:");
Person* ptr = &kunkun;
printf("name:%s\nage:%d\ncountry:%s\ncity:%s\n", ptr->name,ptr->age,ptr->address.country,ptr->address.city);
return 0;
}
typedef struct Address {
char country[64];
char city[32];
}Address;typedef struct person {
char name[16];
int age;
Address address;
}person;
这两个部分顺序不能错!!!
C语言是从上到下依次执行,如果未先定义需要嵌套的结构体,后果很严重!!!!
这里修改个小错误,命名结构体类型的时候首字母大写,有区分度,这里的图片我就不修改了
枚举
顾名思义,一一列举,语法也很简单
为什么需要枚举?
*避免过多的宏定义 *有检测错误机制,好调试 *一次性可定义多个常量联合/共用体
它允许在相同内存位置储存不同的数据类型
联合体所有成员共享一块内存空间大小,大小一般情况下等于其最大成员的大小
这意味着在任意时刻,联合体只能存储一个成员的值,在给定的时刻,只使用成员里的唯一一个类型,即需要用到哪个成员就用哪个成员,这样可以节省内存
//定义一个联合体
//联合体里的成员公用一块内存,一般内存占用是内存最大的那个成员
typedef union {
int int_value;
float float_value;
char* strings_value;
}Data;
//定义一个枚举类型
typedef enum {
INT,
FLOAT,
STRINGS
}DataType;
//定义一个包含枚举和联合体的结构体,我们可以对结构体成员自由操作
typedef struct {
DataType type; //枚举
Data data; //联合
}TypeData;
void print_data(TypeData* ptr);
int main() {
TypeData data1 = {INT, { .int_value = 66 }};
TypeData data2 = { FLOAT, {.float_value = 66.666 } };
TypeData data3 = { STRINGS, {.strings_value = "hello! union!"}};
print_data(&data1);
print_data(&data2);
print_data(&data3);
}
void print_data(TypeData* ptr) {
switch (ptr -> type)
{
case INT:printf("intger: %d\n", ptr->data.int_value);
break;
case FLOAT:printf("float: %f\n", ptr->data.float_value);
break;
case STRINGS:printf("strings: %s\n", ptr->data.strings_value);
break;
}
}
第7章 文件操作
文件相关知识
计算机有内存和磁盘两个存储方式
程序的数据存在内存中,程序退出,内存回收,数据就会丢失
文件保存在磁盘中,程序退出,数据仍会保存,除非删除文件或者磁盘损坏,数据会丢失
文件分为数据文件和程序文件
程序文件:包括源文件程序(.c) 目标文件(.o .obj) 可执行程序(.exe)
数据文件:程序运行时读写的数据
二进制文件:数据在内存中以二进制形式存储的文件
文本文件:数据在内存中以ASCII字符形式存储的文件
流与标准流
对于一个文件,我们可以对其读或写
输入流——input stream 输出流——output stream
输入流的数据被暂存到缓存区(buffer)
流的分类:
文件流:在磁盘上,用于读取与写入在磁盘上的文件
标准I/O流:
标准输入流stdin:默认连接到键盘,用于程序的输入
标准输出流stdout:默认连接到控制台或者屏幕上,用于程序的输出
标准错误流stderr:默认连接到控制台或者屏幕上,专门输出错误信息与警告
管道流:用于进程之间的通信,与许一个进程的输出成为另一个进程的输入
内存流:允许用户将流与内存缓冲区关联,使用户可以向内存中读写数据,就像操作文件一样
网络流:套接字
设备流:特殊文件或打印机
利用FILE* stream(变量名) 指针来进行对流的操作
打开文件 关闭文件
打开文件
fopen:
FILE *fopen( const char *filename, const char *mode );
文件名 访问类型
这里的访问类型指的是你要对文件进行的操作
有如下访问类型:
关闭文件
关闭单个文件fclose:
int fclose( FILE *stream );
关闭所有文件_fcloseall
int _fcloseall( void );
文件读取与输出相关函数
读取:r模式
fgets: 从流中获取字符串
char *fgets(
char *str, //位置
int numChars, //最大字符数
FILE *stream ); //流名称
fgetc
:
从流中读取字符int fgetc( FILE *stream );
fscanf: 从流中读取带格式的数据
int fscanf( FILE *stream, const char *format [, argument ]... );
写入:w模式
fputs: 将字符串写入流
int fputs( const char *str, FILE *stream );
fputc: 将字符写入流
int fputc(
int c, //要写入的字符
FILE *stream );
fprintf: 将格式化的数据打印到流
int fprintf( FILE *stream, const char *format [, argument_list ] );
ftell: 获取文件指针的当前位置,返回相较于起始位置的偏移量
long ftell( FILE *stream );
fseek: 将文件指针移动到指定位置
int fseek(
FILE *stream,
long offset, //来自源的字节数
int origin ); //初始位置
rewind: 将文件指针重新定位到文件开头的位置
void rewind( FILE *stream );
fgets、fgetc与r模式
int main()
{
FILE* pFILE = fopen("myfile.txt", "r");
char buffer[24];
if (pFILE == NULL)
{
perror("pFILE");
return EXIT_FAILURE;
}
while (fgets(buffer, sizeof(buffer), pFILE) != NULL)
{
printf("%s", buffer);
}
rewind(pFILE);//将文件指针移到文件开头
puts("********fgetc*********");
int ch;
while ( (ch = fgetc(pFILE)) != EOF)
{
putchar(ch);
}
memset(buffer, 0, sizeof(buffer));//清理缓存区
fclose(pFILE);
pFILE = NULL;
return 0;
}
fputs、fputc与w模式
int main()
{
FILE* pfile = fopen("myfile.txt", "w");
if (pfile == NULL)
{
perror("pfile");
return EXIT_FAILURE;
}
fputc('H', pfile);
fputc('i', pfile);
fputc('!', pfile);
fputs("This is myfile.",pfile);
fprintf(pfile,"\nnum:%d height:%.2f ", 24, 24.8);
puts("success!");
fclose(pfile);
pfile == NULL;
return EXIT_SUCCESS;
}
此时这个文件已经成功被修改,这里可以看出“w”模式,是把原来文件中的所有内容删除干净之后再写入
所以,w模式可以做一个操作:清空文档,但不写入的操作
typedef struct Student
{
int age;
char name[20];
double score;
}Stu;
int main()
{
Stu kunkun = { .score = 2.5f, .name = "xiaoheizi", .age = 25 };
char arr[81];
sprintf(arr, "%d %s %.2f", kunkun.age, kunkun.name, kunkun.score);
printf("%s\n", arr);
Stu tmp = { 0 };
int ret = sscanf(arr, "%d %s %lf", &(tmp.age), tmp.name, &(tmp.score));
if(ret) printf("%s %d %f\n", tmp.name, tmp.age, tmp.score);
return 0;
}
sprintf和sscanf
int sprintf( char *buffer, const char *format [, argument] ... );
将设置格式的数据写入字符串
int sscanf( const char *buffer, const char *format [, argument ] ... );
从字符串中读取格式化数据
注意对比这两组函数
sscanf:从字符串中读取格式化数据 fscanf:从文件中读取格式化数据 fprintf:将格式化数据写入文件 sprintf:将格式化数据写入字符串
int main() { FILE* pfile = fopen("myfile.txt", "w+"); if (pfile == NULL) { perror("pfile"); return EXIT_FAILURE; } float fp; char s[81]; int i; fprintf(pfile,"%d %s %.2f ", 24,"hi" ,24.8); fseek(pfile, 0L, SEEK_SET); fscanf(pfile, "%d", &i); fscanf(pfile, "%s", &s); fscanf(pfile, "%f", &fp); printf("%d\n%s\n%f\n", i, s, fp); fclose(pfile); pfile == NULL; return EXIT_SUCCESS; }
ftell、fseek、rewind
int main()
{
FILE* ptr_file = fopen("myfile.txt", "r+");
if (ptr_file == NULL)
{
perror("ptr_file");
return 1;
}
long ptr_start = ftell(ptr_file);//记录指针初始位置
printf("%ld\n", ptr_start);
fputs("hello ", ptr_file);
long now_ptr = ftell(ptr_file);//记录指针当前位置
printf("%ld\n", now_ptr);
rewind(ptr_file);//指针回到文件开头
fseek(ptr_file, 4, SEEK_SET);
fputs("CS! ", ptr_file);
rewind(ptr_file);//指针回到文件开头
fclose(ptr_file);
ptr_file = NULL;
return 0;
}
ferror、feof、clearerr
clearerr:重置流的错误指示器,清除错误
void clearerr( FILE *stream );
feof:测试流上的文件末尾/是否到达文件末尾
int feof( FILE *stream );
ferror:测试流上的错误
int ferror( FILE *stream );
int main()
{
char buffer[64];
FILE* stream = fopen("myfile.txt", "r");
//这个错误检测是在打开文件的时候
if (stream == NULL)
{
perror("error opening the file!");
return EXIT_FAILURE;
}
while (fgets(buffer, sizeof(buffer), stream) != NULL)
{
printf("%s", buffer);
}
if (ferror(stream))
{
perror("error!");
clearerr(stream);
}
//这个错误检测是在文件已经打开,在读取的时候
//判断是否因为遇到文件结尾而结束
if (feof(stream))
{
printf("\nSuccessfully reached the end of file!\n");
}
else
{
printf("\nfailuring reached the end of file!\n");
}
fclose(stream);
stream = NULL;
return 0;
}
if (stream == NULL)
{
perror("error opening the file!");
return EXIT_FAILURE;
}
这段代码是检测文件是否能成功打开if (ferror(stream))
{
perror("error!");
clearerr(stream);
}这段代码是在读取文件时,检测是否出错
复制文件
fread: 从流中读取数据
size_t fread(
void *buffer,
size_t size, //项目大小,字节为单位
size_t count, //要读取的最大项目数
FILE *stream );
fwrite: 将数据写入流
size_t fwrite(
const void *buffer,
size_t size,
size_t count, //要写入的最大项目数
FILE *stream );
要注意这两个函数是二进制形式输入输出
int main()
{
char buffer[1024];
size_t bytes_read;
FILE* source_ptr = fopen("C:\\Users\\17601\\Desktop\\节日.jpg", "rb");
if (source_ptr == NULL)
{
printf("error opening!");
return EXIT_FAILURE;
}
FILE* tagert_ptr = fopen("C:\\Users\\17601\\Desktop\\节日复制.jpg", "wb");
if (tagert_ptr == NULL)
{
printf("error opening!");
return EXIT_FAILURE;
}
while ((bytes_read = fread(buffer, 1, sizeof(buffer), source_ptr)) > 0)
{
fwrite(buffer, 1, sizeof(buffer), tagert_ptr);
}
_fcloseall();
puts("文件复制完成!");
return 0;
}
第8章 动态内存管理
******计算机内存管理机制******
内存分为栈内存和堆内存
栈内存:
int input ; //固定死了4字节
int num[4]; //固定死了这个数组只有四个长度
我们在定义之后,整个变量的大小就固定死了
给用户的反馈就是,就这么大,我不管浪费还是不够,你爱用不用吧
a.自动管理的分配机制机制:在函数调用的时候,局部变量被分配在栈区,当函数返回时,局部变量全部销毁并释放
b.访问速度快:栈内存的分配与访问速度通常要比堆内存快,它是一种线性的数据结构(比如说数组的每个格子都是相邻紧挨着,下标连续)
c.大小有限制:栈的大小,在程序启动时就意味着已经确定了,就无法改动了,栈的内存被耗尽,就意味着崩溃,栈溢出。
d.栈区保存着函数的局部变量,函数参数,函数调用的返回地址
堆内存:
a.动态管理:malloc、calloc、realloc、free
b.速度相较于栈有些慢。它需要在内存中寻找足够大的连续空间块
c.大小十分灵活。堆的大小通常受到可用系统内存的限制,而并非栈本身的限制
内存函数
memcpy
在缓冲区之间复制字节
void *memcpy(
void *dest, //新缓存区
const void *src, //源缓存区
size_t count ); //复制大小
需要注意:memcpy将src中的count字节的数据复制到dest中
要确保dest足够大
如果src和dest区域有重合,复制的结果是未知的
此函数遇到'\0'不会停止
int main() { int dest[6] = { 1,2,3,4 }; int src[] = { 'h','i','\0','i','\0' }; memcpy(dest, src, sizeof(src)); for (size_t i = 0; i < 6; i++) { printf("%d ", dest[i]); } return 0; }
模拟实现memcpy
void* my_memcpy(void* dest, const void* src, size_t count)
{
void* ret = dest;//接收新缓存区
assert(dest);
assert(src);
while (count--)
{
*(char*)dest = *(char*)src;//按照一字节一字节复制 没有遗漏
(char*)dest = (char*)dest + 1;//dest指针后移
(char*)src = (char*)src + 1;//src指针后移
}
return ret;
}
memmove
将一个缓冲区移到另一个缓冲区
void *memmove( void *dest, const void *src, size_t count );
和memcpy的差别就是memmove函数处理的源内存块和⽬标内存块是可以重叠的
int main() { int dest[6] = { 1,2,3,4 }; int src[] = { 'h','i','\0','i','\0' }; memmove(dest + 1, src + 2, sizeof(src)); for (size_t i = 0; i < 6; i++) { printf("%d ", dest[i]); } return 0; }
memset
将缓冲区设置为指定的字符
void *memset( void *dest, int c, size_t count );
将内存中的值以字节为单位设置成想要的内容
int main(void) { char buffer[] = "This is a test of the memset function"; printf("Before: %s\n", buffer); memset(buffer + 4, '*', 4); printf("After: %s\n", buffer); }
还有个功能:清空缓存区
int main(void) { char buffer[] = "This is a test of the memset function"; printf("Before: %s\n", buffer); /*memset(buffer + 4, '*', 4);*/ //清空缓存区 memset(buffer, '\0', sizeof(buffer)); printf("After: %s\n", buffer); }
memcmp
比较两个缓冲区中的字符
int memcmp( const void *buffer1, const void *buffer2, size_t count );
从buffer1和buffer2指针指向的位置开始,比较向后的count个字节
malloc:分配内存空间
void *malloc(
size_t size );
//size是分配内存块的大小,单位字节
//
malloc
会返回指向已分配空间的 void 指针,如果可用内存不足,则返回NULL
。 若要返回指向类型而非void
的指针,需要在返回值上使用类型转换
int main()
{
//创建固定数组 分配在栈区
int arr1[5] = { 0,1,2,3,4 };
for (size_t i = 0; i < sizeof(arr1) / sizeof(int); i++)
{
printf("%d ", arr1[i]);
}
//定义一个可变数组 分配在堆区
int* arr2 = (int*)malloc(5 * sizeof(int));
//一定要进行检查!!!!!!!
if (arr2 == NULL)
{
perror("error managing !");
return 1;
}
printf("\n");
for (size_t i = 0; i < 5; i++)
{
arr2[i] = i + 10;//初始化
printf("%zd ", *(arr2) + (i*10));
}
//别忘了手动释放和将指针置为NULL
free(arr2);
arr2 = NULL;
return 0;
}
realloc:重新分配内存空间
void *realloc(
void *memblock, //指向先前的已经分配了内存的指针
size_t size ); //新内存大小,单位字节
特别注意:
realloc
函数更改已分配内存块的大小。memblock
参数指向内存块的开头。 如果memblock
为NULL
,则realloc
与malloc
的行为相同,并分配一个size
字节的新块。 如果memblock
不为NULL
,则它应是指向以前调用calloc
、malloc
或realloc
所返回的指针。
//模拟三部门预算增加至五部门
void Print(double* arr, size_t size);
int main()
{
size_t size = 3;//三部门
double* budget = (double*)malloc(size * sizeof(double));//创建三部门
if (budget == NULL)//检查错误
{
perror("error managing old_budget!");
return 1;
}
puts("before:");
budget[0] = 111;
budget[1] = 222;
budget[2] = 333;//输入三部门的数据情况
Print(budget, size);//输出三部门数据
size_t new_size = 5;//部门数更新
//创建新的缓存区增加部门数
double* new_budget = (double*)realloc(budget, new_size * sizeof(double));
if (new_budget == NULL)
{
perror("error managing new_budget!");
free(budget);//新内存没创建成功 释放旧的即可
return 1;
}
puts("\nafter:");
budget = new_budget;//更新指针
budget[3] = 444;
budget[4] = 555;
Print(budget, new_size);//更新数据并输出
free(budget);//更新指针时新旧内存已经一样了 释放一个即可
budget = NULL;
new_budget = NULL;
return 0;
}
void Print(double* arr, size_t size)
{
for (size_t i = 0; i < size; i++)
{
printf("%.2f ", arr[i]);
}
}
//这个是我copy微软文档的案例
// crt_realloc.c
// This program allocates a block of memory for
// buffer and then uses _msize to display the size of that
// block. Next, it uses realloc to expand the amount of
// memory used by buffer and then calls _msize again to
// display the new amount of memory allocated to buffer.
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
int main( void )
{
long *buffer, *oldbuffer;
size_t size;
if( (buffer = (long *)malloc( 1000 * sizeof( long ) )) == NULL )
exit( 1 );
size = _msize( buffer );
printf_s( "Size of block after malloc of 1000 longs: %u\n", size );
// Reallocate and show new size:
oldbuffer = buffer; // save pointer in case realloc fails
if( (buffer = realloc( buffer, size + (1000 * sizeof( long )) ))
== NULL )
{
free( oldbuffer ); // free original block
exit( 1 );
}
size = _msize( buffer );
printf_s( "Size of block after realloc of 1000 more longs: %u\n",
size );
free( buffer );
exit( 0 );
}
calloc :使用初始化为 0 的元素分配内存中的数组
void *calloc(
size_t number, //元素数量
size_t size );
int main()
{
int* arr = (int*)calloc(10, sizeof(int));
if (arr == NULL)
{
perror("error!");
free(arr);
return 1;
}
for (size_t i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
free(arr);
arr = NULL;
return 0;
}
几道练习题:总结常见内存开辟错误
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
char *GetMemory(void)
{
char p[] = "hello world"; return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str) ;
}
//不够完美 没有内存检查 也没有内存释放 会有内存泄露的风险
综上,我们在使用堆内存要注意:
1.不要对空指针解引用
2.注意释放的内存是堆内存还是栈内存(栈内存由操作系统释放 不需要人为释放)
3.注意指针返回地址,避免野指针
4.不要越界访问内存
5.要确保内存释放完整性 避免释放一部分内存
6.避免对同一块空间多次释放内存
柔性数组
结构体中的最后一个元素允许是未知大小的数组,这就叫作柔性数组
特点
结构中的柔性数组成员前面必须至少一个其他成员
sizeof返回的这种结构大小不包括柔性数组的内存
包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
使用
代码1
typedef struct St
{
char c;
double d;
int arr[];
}St;
int main()
{
St* ps = (St *)malloc(sizeof(St) + 10 * sizeof(int));
if (ps == NULL)
{
perror("error managing ps!");
return 1;
}
//业务处理
ps->c = 'a';
ps->d = 3.14;
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i ;
}
//扩大数组
St* ptr = (St*)realloc(ps, sizeof(St) + 15 * sizeof(int));
if (ptr == NULL)
{
perror("error managing ptr!");
return 1;
}
else
ps = ptr;
for (int i = 10; i < 15; i++)
{
ps->arr[i] = i ;
}
for (int i = 0; i < 15; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n%c\n%.2f",ps -> c,ps -> d);
free(ps);
ps = NULL;
return 0;
}
代码2
typedef struct St
{
char c;
double d;
int* arr;
}St;
int main()
{
St* ps = (St*)malloc(sizeof(St));
if (ps == NULL)
{
perror("malloc");
free(ps);
ps = NULL;
return 1;
}
ps->c = 'a';
ps->d = 3.14;
ps->arr = (int*)malloc(10 * sizeof(int));
if (ps->arr == NULL)
{
perror("malloc-2");
free(ps -> arr);
ps->arr = NULL;
}
for (size_t i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//扩大数组
int* ptr = (int*)realloc(ps->arr, 15 * sizeof(int));
if (ptr == NULL)
{
perror("realloc");
free(ptr);
ptr = NULL;
return 1;
}
else
ps->arr = ptr;
for (size_t i = 10; i < 15; i++)
{
ps->arr[i] = i;
}
for (size_t i = 0; i < 15; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n%c\n%.2f", ps->c, ps->d);
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
对比
代码1使用柔性数组的方法
方便内存释放
我们把结构体的内存以及其成员要的内存一次性分配好了,并返回一个结构体指针,我们做一次free就可以把所有的内存给释放掉
代码2使用指针操作数组
开辟了连续的内存空间,便于快速访问
但是结构体内部有成员二次分配了内存,并没有一次性分配好内存,需要自己观察,手动释放每一块新开辟的内存
第9章 编译和链接
翻译环境和运行环境
翻译环境下,源代码被转化成机器指令(二进制指令)
运行环境下,进行代码的实际运行
我们着重讲一下翻译环境
翻译环境:预编译+编译+汇编+链接
将编译细化拆分成预处理(预编译) 编译 汇编三个过程
预处理
在预处理阶段,源文件和头文件会被处理成为.i为后缀的文件,主要处理预编译指令
将所有的 #define 删除,并展开所有的宏定义
处理所有的条件编译指令
处理#include预编译指令,将包含的头文件的内容插入到该预编译指令的位置
删除所有的注释
添加行号和文件名标识,方便后续编译器生成调试信息
保留所有的#pragma的编译器指令,编译器后续会使用
详细过程
预定义符号
C语言设置了⼀些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的
__FILE__ //源文件
__LINE__ //当前行号
__DATE__ //编译时的日期
__TIME__ //编译时的时间
__STDC__ // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义
#define定义常量
#define name stuff //基本语法
举例:
#define MAX 999 //定义MAX为999
#define DO_FOREVER for(;;) //定义一个死循环
//如果一行写不完,可以给每一行(除了最后一行)加上续行符 \
#define DEBUG_PRINT printf("%s\n%s\n%d\n",\
__FILE__,\
__DATE__,\
__LINE__\
)//要注意续行符不能和前面的字符有空格 不然会让编译器误解
#define定义宏
#define允许把参数替换到文本中,这种实现通常称为宏
#define name( parament-list ) stuff
//parament-list 是由逗号隔开的符号表,它们可能出现在stuff中
//参数列表的左括号必须与name紧邻,如果两者之间有任何空,参数列表就会被解释为stuff的一部分
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!宏定义只做替换,不计算!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#define SQUARE( x ) x * x int main() { size_t a = 5; printf("%zd\n", SQUARE(a)); printf("%zd\n", SQUARE(a+1)); return 0; }
#define DOUBLE(x) (x) + (x) int main() { size_t num = 3; printf("%zd\n", DOUBLE(num)); printf("%zd\n", 10*DOUBLE(num)); return 0; }
修正:
#define SQUARE(x) (x) * (x)
#define DOUBLE( x) ( ( x ) + ( x ) )
所以,为了避免上述错误:
用于对数值表达式进行求值的宏定义都应该加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用
//写一个宏,求2个整数的较大值 #define MAX(x,y) ((x)>(y)?(x):(y)) int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b);// 3 5 int m = MAX(a++, b++); //int m = ((a++)>(b++)?(a++):(b++)); printf("m = %d\n", m); printf("a = %d\n", a); printf("b = %d\n", b); return 0; }
宏替换的规则
1.在调用宏时,先对参数检查,看看是否包含由#define定义的符号,如果是,它们先被替换
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换
3.最后,再次对结果文件扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程
注意:
宏参数和#define定义中可以出现其他#define定义的符号。但是宏不能出现递归(只替换,不计算)
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
例如:
#define MY_MACRO 100
const char* myString = "Value is MY_MACRO";
在这个例子中,'myString'的内容将会是'"Value is MY_MACRO"',而不是'"Value is 100"'。因为预处理器只会在代码中查找'MY_MACRO'这个名称,而字符串常量的内容不会被解析
#undef用于移除一个宏定义
#undef NAME
// 如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除
条件编译:选择性编译
#if 指令:如果后面的常量表达式为真(非零),则编译其后的代码
#ifdef 指令:如果指定的宏已定义,则编译其后的代码
#ifndef 指令:如果指定的宏未定义,则编译其后的代码
#else 指令:与#if、#ifdef或#ifndef结合使用,用于指定条件不满足时的代码块
#elif 指令:可作为#else和#if的组合,用于指定多个条件中的其他条件
#endif 指令:标志着一个条件编译块的结束
#if /*常量表达式*/ //... #endif
#if /*常量表达式*/ //... #elif /*常量表达式*/ //... #else //... #endif
//判断是否被定义 #if defined(symbol) #ifdef symbol #if !defined(symbol) #ifndef symbol
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
#include <stdio.h> #define __DEBUG__ int main() { int i = 0; int arr[10] = { 0 }; for (i = 0; i < 10; i++) { arr[i] = i; #ifndef __DEBUG__ printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 #endif //__DEBUG__ } return 0; }
头文件的引用
本地文件包含
#include "filename"
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误
库文件包含
#include <filename>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
编译
编译过程涉及词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件
词法分析:源代码程序被输入扫描器,扫描器把代码中的字符分割成一系列的记号(关键字、标识符、字面量、特殊字符等)
语法分析:产生语法树,这些语法树是以表达式为节点的树
语义分析:语义分析器对表达式的语法层面进行静态分析,静态语义分析通常包括声明和类型的匹配,类型的转换等,这个阶段会报告错误的语法信息
汇编
汇编器是将汇编代码转转变成机器可执行的指令,每一条汇编语句几乎都对应一条机器指令
根据汇编指令和机器指令的对照表一一进行翻译,不做指令优化
链接
链接是⼀个复杂的过程,链接的时候需要把一堆文件链接在一起才生成可执行程序
链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤
链接解决的是一个项目中多文件、多模块之间互相调用的问题
写在最后
因为笔者自身水平有限,目前阶段还不会使用汇编指令来进行调试编译,编译和链接这一章节讲的有些许理论化,也很无聊,其他章节也多多少少会有些疏漏的地方,希望大家海涵。
五月开的坑终于在八月填上,欢迎大家批评指正,我们一起交流一起学习!
愿我们都能永葆那份初心,静下心来,持续不断地学习,永远保持空杯心态,去面对生活学习工作中的任何事情。
最后,完结撒花!愿这份友谊长存!
版权声明:本文标题:拒绝废话,一文帮你快速打通C语言的任督二脉(已完结) 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/dianzi/1728313383a1153301.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论