C语言基础

软考

代码托管网站 github gitee 分支控制器

计算机基础

目录

基础知识

计算机组成五部分组成:运算器、控制器、存储器(内存、外存)、输入设备、输出设备

内存:常见的内存条

外存:常见的硬盘

CPU只于内存打交道

内存与外存两者的区别:

内存的数据是掉电会丢失

外存的数据是掉电不会丢失

计算机之父:冯诺依曼 图灵奖(纪念艾伦·图灵)

冯诺依曼提出的理论:

  1. 提出计算机是由五部分组成

  2. 计算机中的数据采用二进制存储

电子元件具备双稳定性

  1. 计算机中的程序按照顺序组成

1946年 第一台电子计算机

存储器


查看数电第七章半导体存储

计算机存储多字节数据时,涉及大端小端问题

存储器分为内存外存

内存:常见的内存条

外存:常见的硬盘

CPU只于内存打交道

半导体存储器:存储大量的二进制信息的半导体

复制代码
		   从存取功能来看分为只读存储器**ROM**和随机存储器**RAM**

随机存储器:随机读/写存储器

随机是指随时想读哪个地址就读取,想写入哪个地址就写

ROM:掉电不会丢失 属于非易失性存储器

RAM:掉电会丢失 属于易失性存储器

随机存储器又分为:静态随机存储器SRAM 和动态随机存储器DRAM

SRAM:制作工艺复杂,内存小

DRAM:制作简单,容量大,常用作内存条
DRAM:是利用MOS管栅极电容可以存储电荷原理

电容具备充电放电的特性,充电到一定值时表示逻辑1,放电到一定值时表示为逻辑

计算机处理数据的最小单位:bit

计算机处理数据的基本单位:byte

32位系统和64位系统

指的是计算机地址总线的位数,指的是计算机的寻址范围

32位系统

32bit=4GB

32bit也就是32个电容,32个0或1

2\^{32}=4GB

写地址时,32个地址太长了,采用十六进制表示

32个二进制==8个十六进制

存储单元地址:0x0000_0000=0xFFFF_FFFF

每个存储单元大小都是1字节

每个存储单元的地址长度都是4字节

1字节=8bit
4字节=32bit

64位系统

存储单元地址长度为8字节

存储单元的大小不变1字节

只是地址编号变成了

0x00000000_00000000-0xFFFFFFFF_FFFFFFFF

原码补码反码

计算机内部的二进制都是由补码的方式来存储数据

二进制负数的补码等于它的反码加1

反码计算时,保留符号位不变

正数的原码反码补码相同

负数:函数中不特意声明unsigned

就是有符号,在地址最高位,8位数据=1位符号位+7位数据位

最高位0 为正 1为负

127=原码:0 111_1111 反码:0 111_1111 补码:0 111_1111

129=原码:127+1+1+1=0 111_1111+1+1

复制代码
=1 000_0000+1

=1 000_0001

反码:1 111_1110

补码:1 111_1111=-127

出现数据溢出现象

输入的数据超出了存储器申请的空间大小,会导致数据溢出,数据异常

计算机输入输出缓冲区设置原理+刷新缓冲区的方式

计算机的CPU的性能好,所以CPU的执行效率高,但是计算机的输入设备和输出设备的效率低,所以CPU为了提高工作效率,要降低访问输入输出设备的次数。

缓冲区 (Buffer)是内存空间的一部分。也就是说,在内存中预留了一定的存储空间,用来暂时保存输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区输出缓冲区。

缓冲区分为:输入缓冲区和输出缓冲区

缓冲区一般4kb

CPU读取内存的方式: 1、'\n'换行符是信号的一种

复制代码
				2、缓冲区数据已满时,也会通知CPU

				3、程序死掉时,也会通知CPU

根据IO设备的不同,可以把缓冲区分为输入缓冲区和输出缓冲区,同样,根据刷新形式的不同,可以把缓冲区分为三种:全缓冲、行缓冲、无缓冲

*全缓冲:* 指的是当缓冲区被填满 就立即把数据冲刷到文件、或者在关闭文件、读取文件内容以及修改缓冲区类型时也会立即把数据冲刷到文件,一般读写文件的时候会采用

*无缓冲:* 指的是没有缓冲区 ,直接输出,一般linux系统的标准出错stderr就是采用无缓冲, 这样可以把错误信息直接输出。

*行缓冲:* 指的是当缓冲区被填满 (一般缓冲区为4KB ,就是4096字节 )或者缓冲区中**遇到换行符'\n'**时,或者在关闭文件、读取文件内容以及修改缓冲区类型时也会立即把数据冲刷到文件中,一般操作IO设备时会采用,比如printf函数就是采用行缓冲。

思考:如果用户不小心在使用printf函数输出字符串的时候忘了添加结束标志'\n',并且用户没有打算输出太多字符,请问此时要输出的字符串会不会输出?

回答:可以输出,当程序结束时会自动刷新缓冲区,如果输入缓存区中存在数据则被丢弃,如果输出缓存区存在数据,则会输出。

思考:如果用户不小心在使用printf函数输出字符串的时候忘了添加结束标志'\n',并且用户没有打算输出太多字符,并且程序永远不退出,请问此时要输出的字符串会不会输出?

回答:是不会输出,因为不满足刷新条件,注意:用户可以选择手动刷新缓冲区,可以调用标准C库中的fflush()函数接口

第一个f指的文件

flsh刷新

注意:不要利用fflush函数区刷新输入缓冲区(stdin) ,因为一般编译器是不支持该操作!!!

查找:查阅资料,了解输入输出缓存区为什么一般是4KB大小? 提示:涉及到linux的内存分页机制。

C语言

概述

c语言是由ANSI 美国标准协会组织发布,c语言也称为ANSIC

后面由ISO组织国际标准组织制定与发布C语言

C标准一共四套:

知道C89、C99、C11就可以

主流的编译器都支持C99

写程序是给人看的,写程序要避免二义性

写程序是给人看的,机器是看不懂的,所以就得使用编译器 将C语言转换为机器看的懂得二进制指令0和1组成

GCC编译器编译:

c 复制代码
gcc demo.c -o xxx
    

字符是组成语言的基本元素

C语言中的字符都是英文字符,由美国国家标准协会ASO设计,收录128个字符

标准ASCII码是128个字符 2的七次方 七个电容

拓展ASCII码是256个字符 2的八次方 八个电容=8bit 每一个字符用8bit,也是8个电容(连续的电容)

存储器一般以**一组电容(8bit)**为存储单元

只需要记住常见的三个:

字符0:十进制 48

字符A:十进制 65

字符a:十进制 97

大写字母与小写字母之间差32

笔试题:把大写字母与小写字母进行转化

中国也有自己的标准GBK2312 这个是中文简体字符集

后来推出GBK收录了繁体中文

UTF-8字符集是收录了各个国家的字符

最后养成编译习惯

写程序都要有注释,程序是给人看的

写文件、函数、算法、都要注释

要养成一个好的编译习惯

查看The Linux Kernel Archives

程序的入口和结束

查看文档C99第5章第6章

1、程序开始

程序启动时,有一个main的函数被调用

main函数必须要有返回值,可以没有参数

2种main函数

2、程序结束

C 复制代码
int main()   
{
		return 0;   
}

exit函数只能调用一次,可以让程序正常终止

系统自动调用exit函数,并且将main的返回值传递给exit函数作为参数

0正常终止、非零为异常

函数调用

函数调用分为主函数和子函数

主函数就是main

子函数分为库函数用户自定义

库函数:标准库 官方机构发布的

复制代码
  第三方库	其他组织

主要是如何区分哪个是官方的哪个是自己的

< xxx.h > :编译器只去系统指定路径中查找该头文件,如果未找到,则编译器进行错误提示。

" xxx.h " :先去当前路径 中查找头文件,未找到再去系统路径中查找,还未找到则报错提示。

怎么掌握一个函数

关键字和标识符

标识符

存储器单元分为常量和变量常量和变量统称为标识符

"量"是指存储单元

"常和变"指的是数据的状态是否发生变化

c 复制代码
int a=10;
10是常量、a是变量
10是int型占4个字节,int型的a也申请了4个字节
    一共申请了8个字节
    

标识符:只能由字母、数字、下划线、以及美元符号组成

第一个字符只能是字母和下划线

统一域内不能重复定义,不能使用系统已经定义的名字

关键字

C语言标准中常用的关键字****只有32个****

(1) 字符型

C语言标准中提供了一个关键字char,其实是character单词的缩写,表示字符的意思,操作系统char类型的数据宽度定义为1字节,用于存储字符,C语言标准中用单撇号' '表示字符。

C语言标准规定:用户打算存储字符 数据宽度 变量名称 ; 举例: char ch = 'c' ;

C语言标准中关于字符的种类有两种:*普通字符* and *转义字符*,对于ASCII码表中转义字符

*注意:ASCII码中的转义字符需要使用* *'* *\0* *'* *'* *\r* *'* *'* *\n* *'* *进行表示,代表字符具有特殊的意义。*

思考:已知计算机是以二进制存储数据,意味着写入到存储器中的字符都会被编译器转换为而二进制指令,那请问用户能否直接以二进制指令的形式把字符输入到存储器中,编译时是否会报错??

回答:会报错,因为C语言不支持二进制输入,但是C语言支持八进制、十进制、十六进制。

(2) 整数型

C语言标准中使用关键字int表示整数,关键字int的英文单词是integer,对应的中文具有整数的含义,在32系统下关键字int的数据宽度是4字节,也就意味着存储单元所能存储的整数范围比较广泛。

C语言标准规定了用户可以采用不同的进制来表示数据,常用的进制有八进制、十进制以及十六进制,并且为了区分这三种进制,所以规定每种进制都有对应的前导符(前缀),规定八进制的前缀以0表示,比如064,规定十六进制的前缀以0x/0X表示,比如0x64。

(3) 短整型

C语言标准中规定使用关键字short来表示短整型,一般短整型的全称是short int,只不过写程序的时候可以只写short即可,在32位系统下short短整型占2字节。

(4) 长整型

C语言标准中规定使用关键字long来表示长整型,一般长整型的全称是long int,只不过写程序的时候可以只写long即可,在32位系统下long长整型占4字节,在64位系统占8字节。

(5) 长长整型

C语言标准中规定使用关键字long来表示长整型,但是长长整型是使用long long来表示,在32位和64位系统下长长整型占8字节。

思考:C语言中提供了char、int、short、long、long long来表示整型,但是整型数是分正负的,编译器如何来区分整数的正负呢?

回答:C语言标准中提供了两个关键字 signed && unsigned用于修饰整数,unsigned表示无符号,signed表示有符号,C语言中的signed修饰符是隐式声明,也就是用户定义整型变量的时候如果没有特别强调,则整型变量默认是有符号的。

用户如果要存储无符号的整数,则必须定义变量必须显式声明变量是无符号的(unsigned)

思考:计算机可以区分整数的正负,但是程序最终都会被转换为二进制指令,但是二进制是不分正负的,请问编译器是如何转换数据的???

回答:二进制可以分正负,在二进制数的前面添加1bit,作为符号位,并且bit=0,用于表示二进制数为正数,bit=1,用于表示二进制数为负数。

*注意:设计程序时,定义的变量如果是有符号的,则尽量避免写入超过变量范围的数值!!!!!!*

字符型: char 1字节 -- 有符号 -- 数值范围 -128 ~ 127 -- 无符号 -- 数值范围 0 ~ 255
短整型:short 2字节 -- 有符号 -- 数值范围 -32768 ~ 32767 无符号 --数值范围 0 ~ 0 ~ 6553565

(6) 浮点型

数据有整数和小数之分,一般情况下处理的数据也是具有小数的,所以C语言标准中规定使用关键字float 来表示单精度浮点数,单精度 浮点型占4字节 ,另外C语言标准中也提供了另一个关键字double 用来表示双精度 浮点数,double占8字节(64位) ,其实C语言也提供了一种类型long double ,该类型占16字节

C语言中一般表示浮点数有两种方案:十进制形式 or 指数形式,两者的具体区分如下所示

l 十进制形式

十进制形式是采用整数+小数的组合表示浮点数,比如3.14 、5.0 ,基本上也是最常用的方案。

l 指数形式

指数形式指的是采用以10为底的形式表示浮点数,比如 3.14 × 10² ,但是在编写程序的时候采用英文半角输入法进行设计,这种输入法是不支持输入上标或者下标,所以C语言规定采用字符e或者E来表示以10为底的指数,比如3.14E2 。

*注意:C语言标准中规定字符e/E的后面必须是整数,另外字符e/E的前面必须有数字!!!!!!*

思考:用户定义了一个单精度浮点型变量,并把变量命名为a,此时用户不小心把一个整数5存储到了该变量中,请问变量中存储的数是什么? float a; 回答:%f 5.000000

作业:整型数据在计算机中存储的时候是以二进制补码形式进行存储,请问一个浮点型数据在计算机中是如何存储的?

浮点型数据(如 C 语言的floatdouble)的存储逻辑和整型完全不同 ------不采用补码,而是遵循「IEEE 754 标准」(全球通用的浮点存储规范),核心思想是用「科学计数法的二进制形式」存储,拆解为 3 个部分:符号位、指数位、尾数位,通过这三部分协同表示小数和大数。

(7) 字符串

字符串是表示某种含义的一个字符序列,字符串在内存是需要一块连续的内存空间进行存储,C语言中规定字符串使用****双引号**** 表示,并且规定字符串的结束标志是****'**** *\0* *'*,但'\0'不需要用户手动添加,系统会自动在一个字符串的末尾添加'\0'。

思考:既然在内存中字符串需要一块连续的空间来存储,内核肯定会返回字符串的开始地址,请问内核如何判断字符串何时结束? 回答:遇到'\0'自动结束

(8) 布尔型

用户有时候需要在程序进行判断,在C89标准中,用户如果想要判断某种条件是否成立,一般是定义一个整型变量,然后利用数字0和数字1来表示条件是否成立,用户就可以把变量作为一个标志位使用。

在C99标准可以使用布尔型来表示真假两种情况,头文件stdbool.h有相关描述,在头文件中定义了三个宏,分别是bool、true以及false。

用1不一定是真,但是用true一定是1

同理

思考:大家已经知道基本数据类型的宽度是取决于系统环境以及编译器,假设用户此时并不清楚当前使用的机器的位数(32bit/64bit)和编译器类型,那应该如何知道基本数据类型的宽度?

使用sizeof函数

输入输出函数

一、C语言的输入输出

用户如果打算使用标准C库的函数,就必须要包含函数库对应的头文件,比如输入输出函数对应的头文件就叫做stdio.h,stdio指的是standard input output,翻译为标准输入输出。在linux系统下stdio.h的位置在 */usr/include*/stdio.h

格式化输出

一般标准C库中提供了很多关于输出的函数接口,其中最常用的就是printf()函数,关于printf函数的使用规则可以通过linux系统的man手册进行查阅。

用户一般都是使用linux系统,在linux系统中是提供了一个帮助文档,叫做man手册,man手册一共有9个章节,只需要打开linux系统的终端(Ctrl+Alt+T)输入命令:

复制代码
man  man

man man

第一个man查找,第二个man是,man手册

man手册至关重要

如果想要针对性查找某些接口,可以指定man手册章节,终端输入:

复制代码
man  n(1-9)  xxx

stdout 标准输出,默认设备是显示器

字符串用" "表示

**char **一般表示字符串,如果想要表示字符就不加

在printf函数中" "是格 式化字符串

普通字符是" "中没有%的
%转换说明符

(1) 标志说明

**#**把对应进制的前导符进行输出

o 把八进制的前导符输出

x 把十六进制的前导符输出

必须把#放在转换说明符%的后面

**-减号**用于数据对齐,表示左对齐

0表示高位不足时补零

%08x 格式控制符,其中:

  • 0 表示高位不足时补零;
  • 8 表示输出宽度为 8 位;
  • x 表示以小写十六进制输出

默认的是右对齐

c 复制代码
printf("%-10d%d\n",100,200);

(2) 字段宽度

字段宽度是指待输出的字符串需要占用多少字符

c 复制代码
printf("%*d\n",10,20);
*在格式化字符串中可以理解为占位符	表示列宽	此时函数第二个参数必须是整数,提供给*
 在这里10作为第二个参数给*

(3) 转换精度

精度用英文句号.表示

**.**后面跟一个可选的十进制

如果只有.,则精度为0

会进行四舍五入

(4) 长度修饰

h:half(一半)

hh:把整型转换为字符型,只是针对输出内容而言,数据本身没有受到影响

h:把整型转换为短整型 ,只是针对输出内容而言,数据本身没有受到影响

注意:计算机内部存储多字节的数据时会涉及到大端小端,不同的处理器架构采用的模式是不同的,一般X86架构采用小端模式,ARM架构一般采用大端模式(但是并不绝对)!!!!!!

多字节数据

小端存储:数据的低地址存储在内存的低地址

大端存储:数据的高地址存储在内存的低地址

笔试题:设计程序,判断当前机器是大端还是小端

c 复制代码
printf("%hhx\n",0x12345678)

(5) 转换说明

f默认精度为6位(采用四舍五入)

%f 单精度

%lf 双精度

%c 以字符形式输出

%s 以字符串形式输出

%p 可以把存储单元的编号以十六进制形式输出

c 复制代码
printf("%p\n",&a)
    &取地址符,&变量名--->得到变量地址--->得到变量对应的存储单元的编号
a='h';	相当于访问变量a对应的存储单元
*(unsigned int *)0x75ffc448='e';
0x75ffc448合法地址
(unsigned int *)指向无符号整数的指针
第一个*是解引用运算符,作用是 "通过指针读取或修改它指向的内存中的数据"。

普通符号%,需要写%%

两个百分号,表示%

(6) 转义字符

在利用printf函数输出字符串时,用户可能需要输出一些转义字符 '\n '以及'\t'是比较常用的。

注意:有的时候是需要把一些特殊字符当做普通字符输出 ,比较 "" ,可以把 \" 和 \" 输出。

(7) 返回结果

函数调用成功,则返回打印的字符个数,'\0'不算在内

因为遇到'\0'就退出

笔试题:请分析以下程序,根据自己对程序的理解回答出程序的运行效果,不许讨论和抄袭!
printf是标准输出:输出到显示器

printf是返回结果:输出字符的个数

结果为:4321

思考 :程序为什么是下面的运行效果?同一个程序,就加了一个换行,为什么效果不一样 回答:从计算机组成原理的角度 工作效率问题

查看计算机基础的输入输出缓冲区设置原理

第一个程序,是5s后出现helloworld

原理:从第一句函数开始分析

首先、判断有没有满一行(4096字节),而hello是5字节,显然没有满一行,所以不会刷新(显示器看不到)

其次、判断程序有没有结束,程序没有结束,也不会刷新

最后、判断程序有没有'\n',没有'\n'的信号,也不满足刷新条件

hello还是放到缓存区中

延时5s

同理,输入的world也被放到与hello相同的缓存区

最终,程序结束,缓存区中有数据,就会自动刷新,所以显示器会在5s后看到helloworld
第二个程序,是先显示hello,5s后出现world

回答:计算机的CPU的性能好,所以CPU的执行效率高,但是计算机的输入设备和输出设备的效率低,所以CPU为了提高工作效率,要降低访问输入输出设备的次数。

格式化输入

用户会使用**标准输入设备(键盘)**写入数据,所以标准C库提供了一个输入函数scanf(),作用是把数据流写入到内存中,scanf指的是scan format,翻译为格式化扫描,用法类似于printf函数。

注意:如果在scanf的字符串中使用了多个转换说明符,则输入的数据类型必须个转换类型一一对应。

不要在设计程序时调用scanf函数,在scanf内部的字符串中手动添加'\n'

常用的:%d 整型 %f 单精度浮点型

复制代码
  		%s	匹配字符串(遇到'\0')结束		%c	字符
C 复制代码
scanf("%c%d%f",&ch,&a.&b);

在程序运行时,输入时用空格给分开
    例如:
    h 100 3.1415

(1) 匹配字符
需要匹配的字符序列应该由[]括着

如果[^],表示不匹配[]中的字符

-在[]中可以当作连接符

0-9\]匹配0-9,只有输入这10个数字有效的 \[\^0-9\]不匹配0-9,检测到其中一个就结束输入

c 复制代码
scanf("%[^#]s",buf);
[]匹配字符
[^]不匹配对应字符
    
在这个代码中,[^#]时,不匹配#,当遇到#时,就停止输入。但是刷新缓冲区还是要

运算符

11种赋值运算符

运算符指明要进行的运算和操作,操作数是指运算符的操作对象,根据运算符操作数的数目不同,C语言标准把运算符分为三种:单目运算符(一元运算符)、双目运算符(二元运算符)、三目运算符(三元运算符)。

单目就是一个操作对象

双目就是两个操作对象

算数运算符

C语言中提供了7种算术运算符,分别是 **+ - * / % ++ -- ,++和--**都属于单目运算符,使用的时候比较特殊,其他五种都属于双目运算符。

+、-、*、/ 就是四则运算

当+作为正号时,是单目运算符

除法运算符 两端的操作数都是整数 的时候,得到的结果也是整数 ,当两个操作数无法被整数,会舍弃小数,只保留整数,不进行四舍五入

除法运算符 两端的操作数不全是整数,比如其中一个操作数是浮点数 ,则得到的结果也是浮点数,如果不指定精度,则默认是6位精度

C语言中提供**%作为求余运算** 符,也可以称为取模运算符,C语言标准中规定%符号两端的操作数必须是整数

可以知道,C语言中的运算符是有优先级的 ,运算符的优先级指的是多个运算符出现在同一个表达式中,优先执行哪个运算符,可以知道算术运算符*** / %的优先级是 高于算术运算符+和-**。

思考:既然运算符有优先级,那如果一个表达式使用的运算符的优先级都一样,那应该如何进行运算?

C语言中的运算符是有结合性的,运算符的结合性指的是多个优先级相同的运算符出现在同一个表达式中,优先执行哪个运算符。

运算符的结合性分为两种:一种是左结合性 ,遵循先左后右原则,比如 a + b - c,相当于(a+b)-c,另一种是右结合性,遵循先右后左原则,比如双目运算符中的赋值运算符 = ,比如表达式a=b=c,相当于a=(b=c)。

*注意:C语言中的单目运算符和三目运算符都是遵循右结合性,当然也包含双目运算符中的赋值运算符=,其他的运算符都遵循左结合性。*

C语言中提供了两个增量运算符++和--,属于单目运算符,*只能在变量中使用* ,一般可以作为变量的后缀增量或者后缀减量,++和--也被称为自加运算符和自减运算符,注意:C语言标准中规定++和--也可以作为变量的前缀增量前缀减量。 作用是让变量的值自动加1或者减1。

a++ :在表达式中则表示先让变量a参与表达式运算,运算之后才会让变量a的值自加

++a :在表达式中则表示先让变量a的值自加,自加之后再参与表达式运算

核心思想自增 / 自减运算符的作用是修改变量,但它们的返回值是一个临时的、不可修改的数值。 这个返回值只能被使用,不能被赋值。因此,任何试图将 a++++a 的结果作为赋值目标的操作,在 C 语言中都是语法错误。

int a=10;

int b=1;

a = ++a+b;

=运算级最低

单目运算符的优先级比+高

思考:请问该笔试题的结果是什么?请给出简单的推理过程,请独立完成该笔试题的分析。

掌握好前缀和后缀运算规则、结合性

思考:前缀增量和后缀增量都属于单目运算符,如果一个表达式中同时出现两种运算符,那应该如何进行解释? 比如 表达式 ++i++ 如何解释? ++(i++) 遵循右结合性

单目运算符都是右结合性

思考:请问该笔试题的结果是什么?请给出简单的推理过程,请独立完成该笔试题的分析。

++i+:单目运算符的优先级高于+,属于前缀增量 3+1=4 i=4

++i:4+1=5 i=5

sizeof输出数据类型的大小

选C

注意sizeof在C语言中是一个操作符,作用是计算数据类型的大小,结果以字节 为单位,sizeof括号中的表达式是不会实现运算和处理的。

思考:为什么输出都是4,为什么两个char输出为4?

另外,虽然sizeof运算符中的表达式不会被运算,但是如果sizeof表达式中出现多个数据类型的大小计算,这个时候会涉及到C语言基本数据类型的转换,转换的意思指的是将数据(变量、数值、表达式的结果等)从一种类型转换为另一种类型。

一般程序中的数据类型转换分为两种情况:自动类型转换 or 强制类型转换。两者区别如下

  • 自动类型转换

自动类型转换就是编译器默默地、隐式地进行的数据类型转换,这种转换不需要程序员干预,会自动发生。比如将一种类型的数据赋值给另外一种类型的变量时就会发生自动类型转换。

在赋值运算中,赋值号两边的数据类型不同时 ,需要把右边 表达式的类型转换为左边 变量的类型,这可能会导致数据失真,或者精度降低;所以说自动类型转换并不一定是安全的。对于不安全的类型转换,编译器一般会给出警告。

在不同类型的混合运算中,编译器也会自动地转换数据类型,将参与运算的所有数据先转换为同一种类型,然后再进行计算。

转换的规则:转换按数据长度增加的方向进行,以保证数值不失真,或者精度不降低。 例如int 和 long 参与运算时,先把 int 类型的数据转成 long 类型后再进行运算。所有的浮点运算都是以双精度 进行的,即使运算中只有 float 类型,也要先转换为 double 类型,才能进行运算。另外char 和 short 参与运算时,必须先转换成 int 类型。

C 复制代码
int a=0x1234568;
char b=0x01;
char c=0x01;

a=a+b+c;//0x12345678+0x00000001+0x00000001=0x1234567a
先将char转化为int型
  • 强制类型转换

自动类型转换是编译器根据代码的上下文环境自行判断的结果,有时候并不是那么"智能",不能满足所有的需求。如果需要,程序员也可以自己在代码中明确地提出要进行类型转换,这称为强制类型转换。

自动类型转换是编译器默默地、隐式地进行的一种类型转换,不需要在代码中体现出来;强制类型转换是程序员明确提出的、需要通过特定格式的代码来指明的一种类型转换。换句话说,自动类型转换不需要程序员干预,强制类型转换必须有程序员干预。

强制转换规则: (需要转换的数据类型) 变量 ()常量

不会四舍五入

总结:无论是自动类型转换还是、强制类型转换,都只是为了本次运算而进行的****临时性转换**** ,转换的结果也会保存到临时的内存空间(栈空间),不会改变数据本来的类型或者值。

位操作运算符

C语言中提供了6种位操作运算符,分别是 ~ & ^ | << >>,其中~属于单目运算符,其他五种都属于双目运算符。

查看数字电路第二章,掌握符号

~:按位取反,对于二进制数而言,0变为1,1变为0 ~ 1101_1110 = 0010_0001

&:按位与,对于二进制而言,当两个bit同时为1,则结果为1,如果bit存在0,结果为0

|:按位或,对于二进制而言,当两个bit同时为0,则结果为0,如果bit存在1,结果为1

^:按位异或,对于二进制而言,当两个bit相同,则结果为0,两个bit不同,则结果为1

<<:左移运算符,对于二进制而言原则:高位舍弃、低位补0 0111_1010 << 3 -- 1101 0000

>>:右移运算符,对于二进制而言原则:低位舍弃、高位补0 0111_1010 >> 3 -- 0000 1111

优先级可以查看C语言标准手册(优先级从高到低排序)&:按位与,对于二进制而言,当两个bit同时为1,则结果为1,如果bit存在0,结果为0

|:按位或,对于二进制而言,当两个bit同时为0,则结果为0,如果bit存在1,结果为1

^:按位异或,对于二进制而言,当两个bit相同,则结果为0,两个bit不同,则结果为1

<<:左移运算符,对于二进制而言原则:高位舍弃、低位补0 0111_1010 << 3 -- 1101 0000

>>:右移运算符,对于二进制而言原则:低位舍弃、高位补0 0111_1010 >> 3 -- 0000 1111

优先级:可以查看C语言标准手册(优先级从高到低排序)

  1. 单目位运算符~(按位取反),属于单目运算符,优先级较高。

  2. 移位运算符<<(左移)、>>(右移)。

  3. 按位与&

  4. 按位异或^

  5. 按位或|

思考:请问该笔试题的结果是什么?请给出简单的推理过程,请独立完成该笔试题的分析。

先换成二进制做

思考:请问该笔试题的结果是什么?请给出简单的推理过程,请独立完成该笔试题的分析。

笔试题:

关系运算符

C语言中一共提供了6种关系运算符,分别是 < <= > >= == != ,关系运算符都是双目运算符,都遵循左结合性,一般用在条件判断中,如果表达式中使用关系运算符,则表达式也被称为关系表达式,关系表达式的结果只有两种,用户可以使用布尔型进行结果的表示。

通过标准可以知道,关系运算符 < <= > >=的优先级相同的,并且这四种运算符是高于关系运算符 == !=,两者的优先级是相同的。

思考:请问该笔试题的结果是什么?请给出简单的推理过程,请独立完成该笔试题的分析。

逻辑运算符

C语言中提供了3种逻辑运算符,分别是 && || ! ,对于逻辑与&&和逻辑或||都属于双目运算符 ,遵循左结合性而逻辑非!属于单目运算符 ,遵循右结合性。一般表达式中如果使用了逻辑运算符,则表达式被称为逻辑表达式。

潜规则:如果一个操作对象的逻辑为假,则不会继续分析第二个操作对象的逻辑
非零即真,两个操作对象有一个为假,则整个表达式的结果为假

潜规则:如果第一个操作对象的逻辑为真,则不会分析第二个操作对象的逻辑
两个操作对象有一个为真,则整个表达式结果为真

还有一个逻辑运算符是 ! (逻辑非) ,属于一元运算符,只有一个操作对象,遵循右结合性,作用是把操作对象的逻辑取反(真变为假,假变为真)。

优先级 :逻辑 的优先级最高>算数运算符 >关系运算符 >逻辑与 >逻辑或

思考:请问该笔试题的结果是什么?请给出简单的推理过程,请独立完成该笔试题的分析。

条件运算符

C语言中提供了1种条件运算符,符号是 ? : ,条件运算符是唯一的一个三目运算符,需要三个操作数。

赋值运算符

C语言中提供了11种 赋值运算符,如下图所示,都属于双目运算符,但是遵循右结合性!!!

  1. 左值(lvalue) :本质是 "可寻址、可标识的实体"(比如变量名),能出现在赋值运算符的左侧(比如 a = 5 中,a 是左值,因为它有内存地址,能被赋值)。

  2. 右值(rvalue) :本质是 "一个临时的、不可寻址的值"(比如字面量 5、表达式计算结果 a+3),只能出现在赋值运算符的右侧(比如 b = a+3 中,a+3 是右值,它是临时结果,没有独立内存地址,不能被赋值)。
    让我们来分解 a++ 这个操作,假设 a 的初始值是 5

  3. 读取变量 a 的值 :计算机去 a 所在的内存地址,读取到它的值是 5

  4. 返回一个临时值 :计算机创建一个临时的、看不见的空间,把刚刚读取到的 5 放了进去。这个 5 就是 a++ 这个表达式的 结果

  5. 修改变量 a 的值 :计算机回到 a 原来的内存地址,把它的值改成 6

关键结论 :当你写下 (a++) 时,你得到的是那个 临时的 5 ,而不是变量 a 本身。这个临时的 5 是一个没有固定地址的 右值
当你试图执行 (a++) += a 时,它在计算机看来就像是在执行:5 += 6; (假设第二步后 a 变成了 6)

这显然是荒谬的,你不能把 6 加到 5 这个数字上。+= 的左边必须是一个可以被修改的 容器,也就是变量。

逗号运算符

C语言中提供了1种逗号运算符,符号是 , 作用是把多个表达式连在一起 ,构成一个大的表达式,也被称为逗号表达式。注意按照从左向右的流程对每个表达式进行运算,只是逗号表达式最终结果是最后一个表达式的结果。

思考:请问该笔试题的结果是什么?请给出简单的推理过程,请独立完成该笔试题的分析。

B

思考:请问该笔试题的结果是什么?请给出简单的推理过程,请独立完成该笔试题的分析。

7

思考:请问该笔试题的结果是什么?请给出简单的推理过程,请独立完成该笔试题的分析。

做错了

(3/2) ----> 得到1 因为都是整数相除

(int) 1.99*2 ----> 得到2 先进行强制转化(不进行四舍五入),再乘2

思考:请问该笔试题的结果是什么?请给出简单的推理过程,请独立完成该笔试题的分析。

常见的优先级

优先级层级 运算符类型 代表运算符
1 括号(最高) ()
2 算术运算符 +-*/
3 关系运算符 <>==!=
4 逻辑运算符 &&、`
5 赋值运算符(最低) =+=-=

*注意:C语言中的单目运算符和三目运算符都是遵循右结合性,当然也包含双目运算符中的赋值运算符=,其他的运算符都遵循左结合性。*

C语言的语句和块

C语言程序的基本单位:函数 函数是由语句构成 函数都需要配合复合语句使用

C语言标准中一共提供6种语句,注意C语言中的语句要指明执行的操作,并且没有特殊情况,语句是按照顺序执行的。

用户一般会把实现某些功能的语句整合在一起,构成一个语法单元,C语言标准的语法单元也被称为块,也被称为块语句。

复合语句

复合语句可以限制语句的作用范围,但是一般情况复合语句很少单独使用,都是和其他语句一起使用使用。

表达式语句

C语言程序一般使用表达式来实现某种操作,表达式是由一组操作符以及操作数组成,目的是实现某种特定的操作。

选择语句

(1) 针对一种情况

C语言标准中提供了一种 if() 语句,if是C语言的关键字之一,具有"如果"的含义,可以看到if()语句需要一个控制表达式,当表达式为真时,则会执行statement,如果表达式为假,则不执行statement。

(2) 针对两种情况

C语言中提供了if()... else结构的语句,else是C语言关键字之一,具有"否则",当if语句的表达式不成立时,则会执行else对应的语句。

if ( 表达式 )

{

块语句1; //当表达式为真,则执行块语句1

}

else

{

块语句2; //当表达式为假,则执行块语句2

}

(3) 针对多种情况

C语言标准提供了switch语句,switch也是C语言关键字之一,用于表示多分支选择,需要配合标签语句一起用:

switch( 表达式 ) //表达式结果必须是整型

{

case 常量表达式1 : 待执行的语句1

case 常量表达式2 : 待执行的语句2

case 常量表达式3 : 待执行的语句3

case 常量表达式4 : 待执行的语句4

default : 待执行的语句n ---->当所有的case语句都不满足时才会执行!!!

}

标签语句

C语言标准中提供了3种标签语句,其中使用频率较高是case标签和default标签,case和default都是C语言的关键字之一,case具有匹配的含义,default具有默认的含义。

case 常量表达式1 : 待执行的语句1

case 常量表达式2 : 待执行的语句2

case 常量表达式3 : 待执行的语句3

case 常量表达式4 : 待执行的语句4

default : 待执行的语句n ----->当所有的case语句都不满足时才会执行!!!

注意:case标签的常量表达式的结果必须是整型常量,并且case标签必须是互斥的(不能出现重复的情况,会导致二义性)。

注意:case标签语句和default标签语句只能在switch语句中使用,对于普通的标签语句是可以在跳转语句中使用的。

注意:switch语句应该结合break语句,目的是利用break语句终止switch语句,避免多个标签都被执行。

switch( 表达式 ) //表达式结果必须是整型

{

case 常量表达式1 : { 待执行的语句1 ..... } break;

case 常量表达式2 : { 待执行的语句2 ..... } break;

case 常量表达式3 : { 待执行的语句3 ..... } break;

case 常量表达式4 : { 待执行的语句4 ..... } break;

default : 待执行的语句n ---->当所有的case语句都不满足时才会执行!!!

}

笔试题:遇到switch时,看有没有break语句

避免多个标签被执行

笔试题:float比较

c\ 复制代码
float	a=3.14;
if(a==3.04)	{
	printf("==\n");	
}
else{
printf("!=\n");
}

输出:!=
原因一个数是浮点型,一个是整数,精度不一样

跳转语句

C语言标准中提供了四种跳转语句:goto语句、continue语句、break语句、return语句。

continue只能用于循环体中

跳到循环体的末尾,相当于提前结束本次循环(一次的循环)

循环体内continue后面的语句不会被执行

没有终止循环

迭代语句

C语言标准中提供了3种迭代语句给用户完成重复性的工作,迭代也可以理解为循环,可以重复执行某个代码块。

非零即真

可以看到,C语言标准中提供了while()语句、do ...while()语句、for()语句,每种语句都具有控制表达式,当控制表达式的值不等于0则会重复执行循环体,当控制表达式的值等于0时终止循环体的执行。

(1) while()语句

先判断,后执行

(2) do...while()语句

先执行,后判断

(3) for()语句

先判断,后执行

表达式2如果被省略,则会用非零常量0进行替换

**for( ; ; )**是死循环

c 复制代码
for( int i=0 ;i<10 ;++i)
{
    printf("%d\n",i);
}
执行顺序
    表达式1是变量定义,在表达式2之前先进行,
    再进行表达式2进行判断,如果不等于0,则执行循环体的语句
    执行完循环体的语句后,再进行执行表达式3

循环语句内部的循环体一般都需要结合break语句、continue语句,都属于跳转语句!!!!!!

赋值运算符

核心思想自增 / 自减运算符的作用是修改变量,但它们的返回值是一个临时的、不可修改的数值。 这个返回值只能被使用,不能被赋值。因此,任何试图将 a++++a 的结果作为赋值目标的操作,在 C 语言中都是语法错误。

c 复制代码
#include <stdio.h>

int main(int argc, char *argv[])
{
    int a, b, c;
    printf("请输入a、b、c的值:");
    scanf("%d%d%d", &a, &b, &c);
    printf("a: %d\n", a);
    printf("b: %d\n", b);
    printf("c: %d\n", c);

    // 核心逻辑:判断b是否在a和c的闭区间内
    if ((b >= a && b <= c) || (b >= c && b <= a))
    {
        printf("b在a~c之间\n");
    }
    else
    {
        printf("b不在a~c之间\n");
    }
    return 0;
}

C语言的数组

注意段错误

数组的概念

数组就是数据的集合 ,简单的说数组就是由n个数据组合在一起,数组的英文是Array。数组其实就是用户向内核申请的一块空间,只不过内核提供的这块空间的内存地址是连续的,目的就是方便用户存储数据和访问数据。

思考:既然用户可以向内核申请一块连续的空间来存储数据,那用户如何访问这块内存呢?

回答:首先向内核申请内存时如果申请成功则操作系统会把内存的起始地址 提供给用户,用户可以通过存储单元的地址访问,但是程序的可读性以及可维护性变差,所以C语言允许用户像访问变量一样,可以对数组 进行命名 ,这样系统内核就会把数组名和数组的起始地址建立映射关系

思考:既然数组在内存中是一块连续的空间,那如果用户打算存储的数据的类型不一致,计算机如何区分数据的类型?

回答:计算机不用区分,因为只需要让数组中只能存储相同类型的数据,可以避免二义性出现

数组的定义

如果用户打算把多个同一个类型的数据写入数组进行存储,那应该如何向内核申请空间???

回答:用户需要说明 数据数量 * 数据宽度 ,对于数据宽度而言指的是数据类型 ,数据的类型可以是基本数据类型(字符型、整型、短整型、浮点型) or 复杂数据类型(结构体、联合体) or 指针类型

定义数组的格式: 数据类型 数组名称 [数据个数] ; // 内存大小:数据类型 * 数据个数

申请的存储单元为:int 4字节 4*10=40 个存储单元

\]内部是一个表达式,用于指定数组的大小,表达式的值应该是整数 **笔试题**: ![c3692979-40c2-4155-adf7-02e1db9ac773](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163055183-587705488.png) > Segment fault 是段错误 > > 在运行程序的阶段会报错,出现的根本原因是访问内存时出现的 > > 场景:内存越界,溢出,数组申请的容量超出上限8M ### 数组的访问 思考:如果把数据存储到数组中,用户如何访问数组中某个数据? 可以参考C语言标准 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163054540-1045878106.jpg) > 数组的名称其实是指向数组对象的指针,**数组名** 表示数组**第一个存储单元的地址** 数组重点: **E1\[E2\] (\*((E1)+(E2)))** 回答:可以知道C语言标准中想要访问数组中的某个元素,可以使用 **E1\[E2\] 结构** ,**E1是数组对象,其实就是数组名,E2是一个整数,用于作为数组下标,并且E2的值是从0开始**。 所以数组的下标的**范围 E1\[0\] \~ E1\[E2-1\]**,就可以通过这种方式来表示任意一个数组中的元素! ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163053997-235364167.jpg) 思考:既然数组名表示数组首个元素的地址,那用户除了通过数组下标访问数组元素外,是否可以**直接通过内存地址来访问数组中的数据** ?如果可以那如何**直接通过地址访问数组**呢? ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163053495-82590729.jpg) C语言标准中规定可以通过 **下标方式** 访问数组的元素 E1\[E2\] 也可以通过 **内存地址方式** 访问数组中的元素 ( \* ( (E1) + (E2) ) ) ,注意:如果**E1就是数组名,并且E2就是整型常量** ,**则可以把括号省略,变为 ( \* (E1 + E2) )** 。 思考:为什么C语言中规定*( E1 + E2 )可以访问数组元素地址,是什么原理?*有什么作用? 回答:C语言标准中提供多种运算符,\* 可以作为二元运算符, \* 作为**乘法运算符** ,需要两个操作对象,并且遵循"左结合性",但是 \* 也可以作为一元运算符, \* 的含义就是**间接运算符**。 一元运算符:右结合性 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163052924-687297346.jpg) > 当*****表示**一元运算符**时,* *操作对象的类型应该是指针(地址)* *,右结合,又称为* *间接运算符* *,* *输出结果为数值*\* > > **\&** 可以作为一元运算符,需要一个操作对象,右结合,\&操作符**可以计算出操作数的地址** ,又称为**地址运算符** ,**输出结果为地址** ***( E1 + E2 )**的解释:* *E1* *是一个数组对象,E1也就是* *数组名称* *,C语言标准中规定数组名可以* *作为数组中第一个元素的地址* *,所以相当于* *E1是数组中第一个元素的地址* *,而* *E2是一个整数* *,所以 **E1 + E2 相当于从E1这个地址向后偏移E2个单位**(以元素为单位,所以需要考虑元素的类型),所以* *E1+E2的结果还是一个地址* \*, \*( E1 + E2) 相当于间接访问该地址,相当于得到了(E1+E2)这个地址下的值。 总结: \***(E1+E2) == E1\[E2\]** \*地址 ------\> 得到地址下面的值 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163052145-1918445676.jpg) 笔试: ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163051642-670004502.jpg) 运行不报错 可以知道,**如果E1是数组名,E2是整型常量,则E1\[E2\]可以等价于 E2\[E1\]**,这两种方式都可以访问数组中的元素。 思考:如果用户定义了一个整型数组 int buf\[5\]; 那么 (buf+1) 指的是**数组地址 向后偏移一个元素对应的单元大小,也就是地址向后偏移了4字节** ,请问 (**\&buf+1**) 表示什么意思,应该如何解释? 回答:(\&buf + 1)表达式中存在地址运算符,\&可以是一元运算符,也可以是二元运算符,使用规则如下: (1) 二元运算符 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163051138-743259264.jpg) (2) 一元运算符 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163050591-1110885269.jpg) 重要: ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163050178-1744405883.jpg) > \*\&data=30; > > \&data ==\>得到变量的地址 > > \*(\&data) ==\>得到该地址下的值 > > 变量名本身表示地址下的数值 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163049817-231581331.jpg) > 这里的\*和\&都是一元运算符,右结合 > > =是赋值运算符,二元运算符,右结合 > > 一元运算符的优先级高 C语言标准中提到数组名可以用于表示数组的第一个元素的地址,但是此时有两种例外情况。 #### 重点 第一种情况: 当 **数组名 和 \&地址运算符 一起使用时,数组名就不表示数组首元素的地址,而表示数组本身**。 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163049270-789792535.jpg) (\&buf+1) 可以知道 \&取地址符和数组名一起使用时,数组名不表示数组第一个元素的地址,而**表示数组本身的地址** ,所以**+1的动作** 是**向后偏移整个数组的大小**。 > 地址是16进制 > > 而+1的1是10进制 > > \&buf+1的地址比原先地址大0x14,换成10进制就是20 > > 20:4\*5=20 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163048727-999153363.jpg) > \&是取地址运算符,输出的是地址,32位系统中,存储单元编号32bit等于4字节 > > 所以只要运算结果是地址的,都是4字节大小 > 第22行\*\&相互抵消后得到a,a与sizeof单独运算时,数组名a表示数组本身 第二种情况:当 **数组名 和 sizeof()运算符 单独使用的时候** ,**数组名就不表示数组首元素地址,而表示数组本身**。 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163048219-550347506.jpg) ### 数组初始化 思考:用户为数组申请的内存空间是由内核挑选的,那内存地址中是否会存储一些意想不到的值,如果会,那用户如何对数组进行初始化? 回答:C语言标准中规定数组可以进行初始化,注意:只有定义数组的同时进行赋值才叫初始化! 格式:**数据类型 数组名\[数组容量\] = {0}**; 比如 int buf\[5\] = {0,0,0,0,0}; //数组初始化 思考:语句 int buf\[10\] = {0}; 可以把数组的每个元素都设置为0,那 int buf\[10\] = {1}; 是否表示把数组的每个元素都设置为1? ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163047708-264884278.jpg) ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163047005-2015622405.jpg) 思考:如果用户在定义数组时还没想好要存储的数据的个数,那数组\[\]里面是否可以空着不写? 语法上是否符合? 比如 int buf\[\]; //用户没有填写数据元素的个数 回答:语法是符合的,可以在定义数组时不去指定数组的元素个数,但是一般需要在定义数组的同时进行初始化。 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163046367-714056004.jpg) 思考:如果用户定义数组时并未在\[\]中说明数组元素个数,但是在定义数组时已经对数组进行初始化,所以系统会自动计算数组所需要占用的内存大小,请问如何计算出数组的有效长度以及如何计算数组元素个数? 回答:可以利用**sizeof()运算符来计算数组的容量** ,计算出的数组大小是**以字节为单位** ,然后再用**数组容量 / 数组中元素的类型** 就可以**得到数组中元素的个数**。 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163045816-45998959.jpg) 思考:用户定义了一个数组,并且也对数据正确进行了初始化,但是用户后面准备存储新的元素到数组中,想要把之前存储的元素清空,由于定义数组已经做过初始化的,是否意味着只能把数组中的元素一个一个单独清空? 回答:不需要,可以调用库函数 **bzero()** 以及 **memset()** ,可以专门对数组进行处理,尤其是清空数组的处理。**memset() 比 bzero() 更灵活**。 查找:MMU--------内存管理单元 #### 重点 ##### (1) bzero() ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163045297-1156872699.jpg) > void 空 > > void \* 任意类型的指针 > 把'\\0'的ASCII码存入 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163044802-303742953.jpg) ##### (2) memset() ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163044416-1972384177.jpg) ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163043901-506228790.jpg) 思考:用户定义了一个数组,并且也对数据正确进行了初始化,但是用户不小心把超过数组大小的数据赋值给数组,请问编译器是否会报错?以及用户是否可以这么操作? 回答:**编译一般是不会报错** ,甚至于警告都不会出现,但是在**程序运行阶段可能会导致内存错误(段错误)**,现在的现象是数组出现越界访问的情况,如果刚好访问的内存是有访问权限的,则运行也不会报错,但是如果访问的内存是没有访问权限的,就会段错误,所以就需要用户设计程序要谨慎细心。 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163043395-2024670952.jpg) 思考:用户定义一个数组,但是在定义数组之后并没有进行初始化,而是在定义数组之后想要对数组初始化,请问是否可以,如果可以,怎么做? 不可以 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163042770-1736657319.jpg) ### 字符型数组 一般实际开发中,使用数组一般都是为了存储字符序列,C语言中的字符串也属于字符序列,字符串需要使用双引号""进行限制,双引号""表示字符串的首字符的地址,字符串的结束以**'\\0'作为结束**。 思考:用户定义了一个字符数组 char buf\[5\]; 用户想要把一个字符序列abcde这5个字符存储到字符数组中,提供两种方案: **char buf\[5\] = "abcde"; char buf\[5\] ={'a','b','c','d','e'};** 请问两种方案有什么区别? ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163042270-975844373.jpg) > 用字符串的方式赋值时,系统会在末尾自动添加'\\0',作为结束标志 回答:如果数组的容量刚好和字符串常量中的有效字符的数量一致时,就**会导致数组越界** ,因为**字符串常量的末尾有一个转义字符'\\0'** ,也是需要**占用1个字节**的存储单元。 ![img](https://img2024.cnblogs.com/blog/3723495/202512/3723495-20251224163041528-1819115807.jpg) > arr+1 代表向后偏移一个元素 > > \&arr+1 代表向后偏移一个数组的元素 ### 数组型数组 思考:既然数组中可以存储某个类型的数据,那数组本身也是一个类型,那能否在数组中存储数组呢?如果可以,应该怎么做? 回答:对于数组型数组而言,就称为**多维数组** ,但是注意:**维度是针对用户而言** ,**内存是线性的**,是不分行和列的,所以多维数组其实和一维数组的本质是一样的,都是为了申请一块连续的内存空间,并且内存空间存储的数据的类型是一致的,这个只需要把数组作为元素来看待即可。 注意:不管是几维数组,**数组的定义规则: 数组名\[元素数量\] + 元素类型 比如 int buf\[5\]** > 除了 数组名\[元素数量\] 其他的都是元素类型 二维数组定义格式 : 元素类型 数组名称\[元素数量\]\[元素数量\] 比如 int buf\[2\]\[3\] = {0}; > 先找**名称** 加上后面紧跟的一个**\[\]**,这就是一个数组buf\[2

剩下的int [3] 就是元素类型

int [3]也是一个数组,没有名字的数组,称为匿名数组

int buf[2][3];

能容纳2个元素,元素的类型是数组

思考:如果定义的是多维数组,那如何去访问多维数组中的某个元素? 应该如何设计程序?

回答:就可以通过下标的方式或者地址的方式进行访问,下标的方式: int buf[3][4]; 则如果打算访问 buf[1][1] ,就表示访问元素如下图

通过地址的方式访问: int buf[3][4]; 则如果打算访问 *buf[1][1] ==> * ( ( (buf + 1) ) + 1 )

3、a[0] 指的是大数组中第一个小数组地址,把a[0]当作小数组的名字 ,当 sizeof 的操作数是一个数组名 时,它返回的是整个数组的大小

  1. 在表达式 sizeof(a[0]) 中,a[0] 这个 "匿名数组" 的名字,作为 sizeof 的操作数。
  2. sizeof(a[0]) 计算的是类型 int[4] 的大小。
    20、sizeof(a[3]) sizeof只是计算数据的a[3]的宽度,不进行访问内存的操作,也没有申请那块内存,自然不会有段错误。

柔性型数组

思考:用户定义一个数组,但是在定义数组的时候没有想清楚数组的元素数量 ,所以能否使用一个变量来代替数组元素个数 呢?如果可以,那是否意味着用户可以在运行程序的时候通过键盘对变量赋值,从而实现手动控制数组元素个数

回答:柔性数组在C89标准中是不支持的,是C99标准引入的概念,柔性数组也被称为变长数组 ,但是注意:当数组的内存一旦确定,则不会因为变量发生变化导致数组长度变化!

匿名型数组

C99标准中支持匿名数组,但是匿名数组一般都是在函数参数中或者在多维数组中使用,很少单独使用。

比如二维数组 int buf[3][4] ; ---> buf[3] 数组的每个元素的类型是 int [4] ,就是匿名数组。

零长度数组

GNU组织 在C99标准的柔性数组的基础之上拓展了一个新的概念,叫做零长数组,也就是数组的长度可以是0 ,但是由于数组长度是0,所以操作系统是不会提供内存单元给数组的

注意:零长度数组是不会得到内存,但是是可以访问 的,一般都是结合C语言的结构体一起使用 ,可以用于对结构体进行拓展,所以零长度数组也属于柔性数组的一种

C语言的指针

指针的概念

程序是需要载入内存 中运行,内存是有范围的,对于32位系统,内存地址范围是0x0000_0000~0xFFFF_FFFF,也就是内存大小为4GB,内存地址指的是内存中单元的编号,编号是固定的。

所以内存地址(存储单元的编号)本质就是一个整数,对于32位系统而言,地址所对应的编号是4字节的正整数

用户想要访问内存地址下面的数据是比较麻烦的,因为地址比较难记忆,内存地址都是由内核管理,所以C语言规定用户有权利对内存地址进行命名,比如变量名字、数组名字.........,所以标识符就可以和操作系统提供的内存单元建立映射关系。

思考:既然用户可以定义变量来存储数据,那能否把内存地址当成数据存储在一个变量中?

回答:是可以的,因为存储单元的地址本质就是一个整数,如果是在32bit系统 下,则只需要4个字节的存储单元就可以完成存储。

思考:既然内存地址可以当做数据存储在一个变量中,那内核如何区分变量中的数据是作为普通数据还是作为内存地址呢

回答:操作系统不需要区分 ,但是作为用户而言,需要区分该变量下存储的是地址还是普通整数,所以C语言表中规定:用户如果打算定义一个变量来存储一个内存地址,则需要定义变量的时候指明该变量中存储的是一个地址

指针的定义

C语言中把用于存储地址的变量 称为指针变量 ,因为通过变量中的地址可以指向某个存储单元 ! 指针指向的是地址,所以可以把指针理解为地址,也可以把地址当做指针使用,注意:如果*打算获取某个地址下的值,必须使用 * 间接运算符 , 地址 == 地址下的值

*指针变量定义格式:数据类型 变量名; 比如 int *p; or char *p; or int * buf[5];

存储的是地址,地址是4字节内存空间

int *p2; 向存储器申请4字节的内存大小,用来存储地址,地址下存储的数据类型是int型

4字节的原因,是由于操作系统是32位的,32操作系统的存储单元的地址长度都是4字节

思考:既然指针变量可以存储一个内存地址,那请问内核是否会为指针变量分配内存空间

回答:当然会分配,因为定义变量的目的就是为了申请内存单元,32bit系统下需要4个存储单元才能记录一个地址,而记录的地址和变量本身的地址是不一样的。变量的存储单元就相当于是一个容器,记录的地址就相当于数据而已

思考:用户定义了一个指针变量,但是此时并没有打算让该指针变量指向某个内存地址,请问内核分配给指针变量的内存空间中是否会存储一些未知的数据?如果存在,应该如何解决?

回答:是会的,所以为了提高程序的可靠性,为了避免异常出现所以就算不存储有效地址,也应该对定义的指针变量进行初始化,注意:对指针变量进行初始化,则应该把指针变量对应的内存初始化为0 ,但是0只是一个整数,并不是地址,而指针变量就是应该存储地址

用户应该把*普通整数0进行强制转换,转换为一个地址 0x00000000 --> (void )0x00000000

void *任意格式

int *p = (void *)0x00000000; //可读性较差,所以C语言中提供了一个宏定义 NULL 空指针

define NULL ((void *)0)

可以看到,linux系统的内存中有一部分内存是属于保留区 ,保留区地址范围就是0x0000_0000 ~ 0x0804_8000 ,属于用户没有权限访问的内存空间 ,一旦用户访问这块区域,就会导致段错误

int *p = NULL ; //对指针变量进行初始化,目的是防止野指针出现,为了避免段错误的!!!

思考:已经知道内存中有一块保留的空间,程序是没有权限访问的,但是用户能否定义一个指针变量指向这块空间?

堆内存属于匿名内存,只能通过内存间接堆访问

回答:是可以的,但是只能用指针变量记录该地址,但是不能通过指针变量间接访问该地址,如果间接访问,则会导致内存异常,发生段错误

思考:既然指针变量中存储的是一个内存地址内存地址的本质就是一个整数,所以能否对整数进行算术运算呢?

回答:是可以的,只不过普通整数的算术运算和地址的算术运算的理解是不同的,一般对于普通整数可以进行算术运算,则结果也是一个整数。

但是对于指针变量中存储的地址进行算术运算,一般只能进行加法运算和减法运算 ,对地址进行加法运算和减法运算,其实就是对地址进行偏移而已 ,偏移的单位一般是应该以字节为单位

注意:对于指针变量的偏移,要考虑到变量中存储的地址的数据类型 ,所以 地址 + 1 不表示存储单元向后偏移1个字节,应该是向后偏移 (1 * 数据类型)个字节

数组指针

思考:既然可以用指针变量指向另一个变量的地址,请问能否用指针变量指向数组的地址

回答:当然可以,就相当利用指针变量来对数组的地址进行备份提高了访问数组的安全性 ,而利用指针变量来指向数组的地址,被称为数组指针!!!! int buf[5]; int *p = buf;

常用的优先级

运算符的优先级:()>[]>*

考虑优先级,得先让变量名和*先结合在一起在可以,这样才是定义的指针,

然后再写定义数据类型,int [10],

int *a[10],这个不对,这里是[]先与a结合成为数组a[10],数据类型位int *,

其含义为,一个有10个指针的数组
要让*与a先结合,就用()

int (*a)[10],这个才是这个题的答案

数组指针的访问

如果此时用户需要访问数组中的元素 ,可以通过数组下标 and 指针访问,两者的区别如下:

笔试题:请分析以下程序,根据自己对程序的理解回答出程序的运行效果,不许讨论和抄袭!

笔试题:请分析以下程序,根据自己对程序的理解回答出程序的运行效果,不许讨论和抄袭!

思考:用户打算定义一个二维数组,并且用一个指针变量来存储数组的地址,现在想用指针变量来访问二维数组中的元素,请问如何访问?

C 复制代码
int **(*p)[2][3];

(*p)该变量是指针,存储一个地址-->[2]代表存储的地址下的数据是一个数组,可以容纳两个元素
    [3]代表数组中的元素类型是匿名数组,匿名数组内有3个元素-->*表示匿名数组中存储的元素类型是一个指针,存储一个地址-->*表示存储的地址下的数据还是指针,存储一个地址-->int 指的是指针存储的地址下的数据是int型

指针数组

思考:既然数组可以存储同一类型的数据,请问能否在一个数组中存储指针变量的地址???

回答:是可以的,如果在一个数组中,每个元素都是一个指针,则C语言中把这种结构称为指针数组

指针数组的定义格式: 数据类型 *数组名[元素个数]; // 比如 int *buf[5];

注意:**[]后缀运算符 优先级高于 间接运算符* ,所以 buf[5] 作为一个整体,剩余部分就是数组中元素的类型,所以 int *就是数组中元素的类型,其中int是用于修饰指针指向的地址中数据的类型。

二级指针

思考:既然可以使用一个指针变量来存储另一个变量的地址 ,能否定义一个指针变量,然后来存储另一个指针变量的地址?如果可以,那如何可以访问到最终内存地址下的数据?

回答:是可以的,如果一个指针变量中存储的地址是另一个指针变量的地址,则把这种结构称为二级指针。

二级指针定义格式: int data; //整型变量 int *p1 = &data; //指针变量 int **p2 = &p1;

习题

练习:用户现在定义一个int buf[5] = {1,2,3,4,5}; 现在用户定义一个数组指针来存储数组的地址, int *p = buf;

请问 printf("%d\n",*p++);

printf("%d\n",(*p)++); 请问两句话的输出结果?

单目运算符,右结合性,++与*的优先级相同,但是++是后置自增

所以*p++先进行 *p得到buf的地址,再进行++自增

特性是 "先使用原值,再移动指针"。

  1. 先解引用 p,获取 buf[0] 的值 1,作为输出结果;
  2. 指针 p 向后移动一位,指向 buf[1](值为 2)。
  • 因此第一句输出:1

字符串常量,本身表示字符串第一个字符的地址

%s是字符序列,碰到'\0'才会停止

重要:

出错:

没看仔细

c注意()优先级

出错:

*p指针,存储的数据类型是char

p=s[1]="two"中的't'的地址

p+1:由于p的类型是char,p+1得到'w'的地址

出错:

0x67a9 是个整型,想要当成地址,得使用强转换 int *或者unsigned int *

再使用间接运算符,进行赋值

到不了10,

3p对的,可以交换,只要E2确保是整数就能交换

C语言的函数

耦合、内聚

C语言程序的基本单位是函数,C语言是面向过程的一门编程语言,采用"自顶向下"的设计思想,采用的方案是把一个大问题拆解为很多个小问题,每个小问题单独进行解决,每个小问题可能需要多条语句才能解决,为了提高效率,所以就把可以解决问题的多条语句构成一个块语句,C语言中把这种块语句就称为函数。

C语言标准在发布的同时也随之发布了标准C库,标准C库中提供了已经封装好的函数接口,目的也是方便用户提高开发效率,不过预先封装好的函数属于****库函数**** 。库函数根据发布者的不同,又分为****标准库**** 和****第三方库****,比如标准C库。

函数的本质就是一段可以重复使用的代码块,用户不想每次都复制这段代码,就可以把这段可以重复使用的代码封装为一个函数接口。

函数定义

思考:既然函数可以很大程度提高开发效率,应该如何去定义一个函数呢?有没有注意事项?

//函数有参数列表 ,则应该在函数名称的()中写清楚每个参数的类型,以及每个参数的名称

函数类型 函数名称(参数1类型 参数1名称,参数2类型 参数2名称.........)

{

}

//函数的参数是可以可选的,如果没有参数,则需要在函数名称的()中填写void即可

函数类型 函数名称(void)

{

}

注意:void在C语言标准中是一个关键字,含义具有空的意思,所以如果在参数列表中出现,则表示函数没有参数,同样,如果void是函数类型,则表示函数没有返回值

注意:函数的类型其实指的是函数的返回值的类型 ,C语言标准中规定函数类型可以是void或者是对象类型(基本数据类型 int char long float... + 复杂数据类型 struct union.... +指针)

但是函数的返回值类型不允许是数组!!!!

注意:如果函数有返回值类型,则函数内部的需要返回的数据的类型必须要和函数的返回值类型一致,则需要在函数内部调用return语句实现

int 函数名称(void)

{

return 3.14; //不允许,因为实际返回的数据的类型和定义函数的时候声明类型不一致

}

void 函数名称(void)

{

return 10; //不允许,因为void作为函数的类型,表示函数是没有返回值的!!!!!!!!!

}

int [10] 函数名称(void)

{

int buf[10] = {1,2,3,4,5};

return buf; //不允许,因为函数的返回值类型不允许是数组,但是可以选择传递地址

}

**int *** 函数名称(void)

{

int buf[10] = {1,2,3,4,5};

return buf; //允许的,因为函数的返回值类型不允许是数组,但是可以选择传递地址!

}

注意:如果函数的类型是一个指针类型,则表示该函数可以返回一个地址,就把这种函数称为指针函数

耦合内聚

思考:既然C语言程序的基本单位是函数,能否在一个已经存在的函数中定义一个新的函数?

回答:不可以!C语言中函数都是独立的个体,不允许在一个函数内部定义新的函数,但是允许在一个函数内部调用其他的函数!设计函数应该做到****低耦合,高内聚****!

耦合 :两个或者两个以上的模块之间关联的紧密程度叫做耦合,耦合度越低越好

内聚 :一个模块内部的各部分的紧密程度,内聚度越高越好

函数调用

思考:如果用户打算封装一个函数实现某个功能,但是此时用户还没想好函数对应的块语句怎么写,只是把函数的名称和返回值类型以及参数列表写了出来,那能否在一个函数中进行调用?

回答:是可以调用的,但是遵循一个**"先定义,后使用"** 原则,由于C语言中程序都是以函数 为单位,并且程序的入口是主函数main(),所以应该把用户自定义的函数定义在main()函数之前,然后在ma in()函数中进行调用。

但是,有时用户可以在程序设计时是先在main()中调用了某个自定义函数,然后在main()函数后面定义了子函数,此时编译会报错,会提示:子函数未定义,为了避免此类问题,C语言中也是支持"先声明,后定义"。

练习:用户打算设计一个函数,把两个整数传递到函数内部,从而实现计算两个数中较大的数并把较大的整数作为结果输出到终端,请问如何设计程序?

函数参数

思考:既然一个函数可以对数据进行处理,请问如何把要处理的数据传递给函数?应该如何操作?

回答:需要在设计函数的时候说清楚函数需要传递的参数的类型以及参数名称,都是在定义函数的时候通过函数的参数列表传递。

函数的参数列表是在后缀运算符()里面进行填写,()中的参数只是一个函数的助记符, 只是为了描述需要传递给函数的参数,所以函数的参数一般称为形式参数,简称为形参

而****定义函数**** 的时候函数参数列表中的形参是不占内存 的,只是为了提醒用户参数的数量和类型!

用户在调用函数接口 时,需要按照函数的参数列表来向函数提供对应的数据,数据的数量和数据的类型必须和形参一致

注意:当一个函数被调用之后,函数的形参才会得到对应的内存 ,并且函数的形参的内存只会在函数内部生效 ,当函数调用完成后,则函数形参的内存会被系统自动释放

注意:当用户调用一个函数时,如果函数有参数列表 ,则用户需要提供对应的数据给函数 ,而用户提供的数据的类型必须和函数参数类型一致 ,用户实际提供的数据被称为实际参数,简称为实参 ,而实参是必须存在的,实参的形式可以是表达式、常量、变量、地址........

c 复制代码
void func(int a,int b);

int main(int argc,char const *argv[])
{
    int c=10;
    int d=20;
    //10和20是变量,有存储单元
    func(c,d);
    
    //10和20是常量,有存储单元
    func(10,20);
}

//函数的形参是调用函数时才会得到内存,在函数结束时会被释放
//传参时,是把实参的值传递

单向传递

单向传递 :只是把实参的值传递给函数作为参数 ,在函数内部对数值进行修改是不会影响外部实参的

思考:一个函数的参数列表中的参数有对应的类型和名称,那另一个函数在调用该函数的时候,传递给函数的需要处理的数据(实参)的类型和名称是否需要和参数列表(形参)完全一致?

回答:实参的名称和函数形参的名称不需要一致 ,只需要确保实参的类型和函数形参的类型一致 即可,如果类型不一致,则会出现数据精度异常

思考:既然函数数据传递的过程是单向的,请问用户如何获取函数内部对数据的处理结果呢?

回答:可以通过函数的返回值获取函数的处理结果,函数中调用return语句可以把结果返回给被调用的位置

当func()函数的返回结果sum的值给到主函数时,函数的sum释放内存

双向传递

如果不打算调用return语句,则可以选择把实参的地址 作为参数传递给函数内部,这样函数内部对地址中的数据进行修改 ,则函数外部的实参地址下的值也会变化 ,只不过此时函数参数类型应该是指针才可以。

c 复制代码
#include <stdio.h>

int *xxx(void)  // 函数返回int类型指针
{
    int buf[5] = {1, 2, 3, 4, 5};
    printf("%d\n", buf[0]);  // 输出数组第一个元素1
    return buf;  // 注意:此处返回局部数组地址,存在风险
}

int main(int argc, char const *argv[])
{
    int *p = xxx();
    *(p + 0) = 10;  // 尝试修改指针指向的内存(实际为无效操作)
    printf("%d\n", p[0]);  // 输出结果不确定(未定义行为)
    return 0;
}
存在段错误的风险
    因为p存储的是局部变量的地址,但是由于函数使用完就会释放,p只能存储这个地址,但是不能进行访问p存储的内容

生命周期

思考:程序中全局变量和局部变量在使用的时候是否有区分?有哪些使用细节需要注意??

回答:对于生命周期是指变量的生命周期 ,也就是变量从得到内存到释放内存的时间 就是变量的生命周期,程序中变量如果按照存储单元的属性分类 ,可以分为变量和常量 ,也可以按照生命周期进行划分 ,可以分为全局变量和局部变量

(1) 局部变量 :在函数内 部定义的变量或者在某个复合语句中定义的变量都称为局部变量!

c 复制代码
{
    {
        int a;
    }
}
a的作用域是第二个{}
a的生命周期是第一个{}

(2) 全局变量 :在所有的函数外部 (在所有复合语句外部)定义的变量就被称为全局变量!

作用范围

作用范围指的是定义的变量的作用域,也就是变量的有效使用范围 ,对于全局变量 而言,作用域是针对整个程序 ,所以程序中任何一个函数都有访问权限。对于局部变量 而言。作用域只针对当前局部变量的复合语句有效。

注意:当全局变量的名称和局部变量名称相同时 ,则应该遵循****"就近原则"**** ,也就是应该优先使用同一个作用域内的变量,如果该作用域中没有该变量,则可以扩大作用域

数组传递

思考:通过学习已经知道可以把变量的值或者地址 当做参数传递给函数进行处理,但是有时用户需要连续处理多个相同类型的数据 ,但是不想定义多个形参,请问如何解决该问题???

回答:可以选择把多个类型相同的数据构造为一个数组 ,然后把数组作为参数 传递给函数,本质就是把数组的地址传递过去,此时分为两种方案:

思考:如果把数组的地址当做参数传递给函数,那用户如何知道实参数组的长度是多少???

回答:如果打算把数组作为参数传递给函数,则应该连同数组的长度一同作为参数传递给函数 ,而数组长度应该使用sizeof进行计算。

思考:既然可以把一维数组的地址 作为参数传递 ,请问能否把多维数组传递给函数处理???

回答:一维数组和多维数组其实没有区别,因为都是把数组的首地址传递过去,只不过在函数内部访问数组元

c 复制代码
void func2(char buf[2][5],int size);
void func2(char **buf,int size);
void func2(char *buf[2],int size);

思考:如果用户打算在一个函数中定义一个数组 用来存储已经处理好的数据,但是C语言中规定不允许返回一个数组类型,当函数调用完成后函数内部的内存会被内核释放掉,也就意味着处理好的数据都会丢失,请问应该如何处理?

回答:由于C语言不支持函数的返回值类型是一个数组,但是可以选择把数组的地址作为返回值 ,此时函数的返回值类型就应该是指针才对。

思考:程序中数组的类型和函数的返回值类型是一致的,为什么编译程序会报警告,运行时也出错,是什么原因导致的?应该怎么解决?

出现段错误的原因 :因为子函数中的变量buf的生命周期是在函数内部的 ,所以当函数调用完成后,则数组buf的内存会被系统自动释放 ,此时数组buf的地址对应的存储单元就没有访问权限了

虽然得到了数组buf的地址 ,但是由于用户没有该地址的访问权限,所以访问时会出现段错误。

(1) 解决方案:可以选择把函数内部的数组定义为全局变量, 此时程序中任意函数都可以访问,并且数组内存是在程序终止后才会被释放

(2) 解决方案:可以选择把函数内部的局部变量的生命周期延长 ,此时需要使用C语言中的存储类修饰符 ,就是C语言关键字之一的static关键字 ,static具有静态的含义,可以把局部变量的生命周期进行延长。

static

内存角度 分析:如果在函数内部定义 一个局部变量 ,则系统会从内存分区中的****栈空间**** 中分配一块内存给局部变量,栈空间是由系统自动管理 ,所以当函数调用结束时系统会自动释放该局部变量的内存。

如果函数中定义的局部变量使用static关键字 进行修饰,则系统会从****全局数据区**** 分配内存空间给该局部变量,全局数据区的生命周期是跟随程序的,不会因为函数结束而释放。

static 除了可以修饰局部变量外 ,也可以用于修饰函数 ,如果一个函数在定义的时候使用static关键字进行修饰,则可以限制函数的作用域为文件内部有效

同一个函数内,静态局部变量不会重复定义

c 复制代码
void func(void)
{
    static int b=0;
    b++;
    printf("%d\n",b);
}
int main(int argc,char *argv[])
{
    int a=1;
    for(a=1;a<=2;a++)
    {
      func();
    }
    
    return 0;
}

b输出为
1
2

习题

32位系统,数组buffer是一个局部变量,由栈空间提供,栈空间默认8M,但是定义时分配100M,超出了

所以因为越界出现段错误

str虽然存储了函数GetMenory()的地址,但是由于函数GetMenory()调用结束,其函数内的p的内存被释放,没有访问权限

所以访问该地址时出现段错误

内存分布

思考:请问什么是栈空间以及什么是全局数据区?两者之间有什么联系?如何区分变量是在栈空间还是全局数据区?

如果采用的是32bit 的linux系统,则每个运行的程序都会得到4G大小的内存空间 ,只不过每个程序得到的4G大小的内存都是虚拟内存 ,而物理内存才只有4G,物理内存是真实存在的,而虚拟内存是通过映射得到的

虚拟内存是由物理内存映射而来 ,所以都需要计算机中的重要部件:MMU 内存管理单元

查找:MMU

(1) 保留区

保留区也可以称为不可访问区域,用户是没有权限访问的 ,对于Linux系统而言,保留区的地址范围是0x0000_0000 ~ 0x0804_8000 ,所以保留区的大小是128M ,一般用户定义的指针变量在初始化的时候就可以指向这块空间,由于这块空间任何程序都没有权限访问,所以可以确保指针不会被误用,所以可以防止野指针出现 ,宏定义NULL其实就是指向0x0000_0000

野指针是指向不确定或无效内存区域的指针

(2) 代码段

查找:冯诺依曼架构与哈弗架构的区别

程序由数据 以及指令 组成,代码段存储的是编译器对程序编译之后生成的二进制指令,代码段分为两部分 ,分别是**.text段和.init段**。

.text段用于存储用户程序生成的指令 ,.init段用于存储系统初始化的指令 ,这两部分的属性是只读的,在程序运行之后代码段中的数据就不应该再被修改。在程序运行之前代码段的内存空间就已经被内核计算完成。

(3) 数据段

程序由数据 以及指令 组成,根据数据的生命周期和数据类型 的不同,一般把数据存储在两部分,一个部分是栈空间 ,另一个部分是数据段

数据根据数据类型 (变量or常量,全局or局部)以及根据数据是否被初始化 (已初始化or未初始化)把数据存储在三个不同的位置:.rodata段 .bss段 .data段

l .rodata段 :被称为只读常量区 ,程序中的常量(整型常量、字符串常量) 都是存储在该区域,对于该区域的属性是只读的 ,当程序结束后 该区域的内存会被释放 。 ro readonly

int a=10;

10这个常量也是有存储单元,属性是只读

l .data段 :用于存储程序中的已经被初始化的全局变量已经被初始化的静态局部变量 ,另外注意初始化的值不能为0!

初始化

l .bss段 :用于存储程序中未被初始化全局变量 以及未被初始化的静态局部变量 以及初始化为0的全局变量和初始化为0的静态局部变量

未初始化

(4) 堆空间

堆空间属于用户可以随意支配的内存 ,用户想要支配堆空间的内存的前提是需要向内核申请 ,可以通过库函数malloc()、calloc()申请堆内存 ,注意堆空间需要用户手动申请以及手动进行释放 ,通过库函数free()释放堆内存堆内存属于匿名内存,只能通过指针访问!!!

向上递增,用的越多,分配的越大

1、malloc()

利用malloc申请堆内存 ,则申请成功的堆内存是未被初始化的 ,所以用户应该对申请的堆内存进行初始化 ,同样,malloc只需要一个参数 ,该参数指的是需要申请的堆内存的大小,以字节为单位

存储4个整数,4*4=16

malloc(16);

该函数的返回值是申请成功的堆内存的首地址 ,但是该地址的类型是void ,则用户 应该对该地址进行强制转换 ,如果 申请失败,则函数返回NULL ,所以用户应该进行 错误处理*!

perror()函数,可以输出格式化字符串,并且可以输出错误原因

2、calloc()

申请4个整数 4*4=16

calloc(4,4);

calloc函数可以申请堆内存 ,calloc有两个 参数,第一个参数是要申请的内存块的数量 ,第二个参数是内存块的大小 ,所以申请的内存的总大小 = 内存块数量 * 内存块大小,相当于是数组结构。

该函数的返回值 是申请成功的堆内存的首地址 ,但是该地址的类型是void ,则用户应该对该地址进行 强制转换 ,如果 申请失败 ,则函数 返回NULL ,所以用户应该 进行错误处理*!

3、free()

free函数可以释放有参数ptr指向的内存,但是ptr指向的地址必须是通过malloc或者calloc申请的

同一块内存不能释放多次

注意:由于堆空间是由用户进行支配,所以用户申请成功之后,使用完成后需要及时释放堆空间 ,并且必须手动释放,并且必须只能释放一次如果不释放,则会导致内存泄漏

另外,当把申请的堆内存释放之后 ,则应该同样把指向堆内存首地址的指针的地址指向NULL!

realloc()

用于扩展内存,一般不常用

c 复制代码
int main(int argc,char const *argv)
{
    //定义指针变量并初始化,NULL指向保留区
    int *p=NULL;
    //利用calloc申请堆内存,calloc申请的内存是被系统自动初始化为0的
    p=(int *)calloc(5,4);
    if(NULL==p)
    {
        perror("calloc error!);
        return -1;
    }
    //使用完成后,需要释放堆空间
    free(p);
   	//指向重新指向NULL,防止内存泄漏
    p=NULL;
    return 0;
}
c 复制代码
int main()
{
int buf[5];
(int *)calloc(5,4);
}
两者的区别?
    buf是局部变量,函数调用完成后会被释放,利用的栈空间
    calloc是跟随程序的,当程序结束后,才会被释放,利用的堆空间,空间比栈空间大

(5) 栈空间

栈空间主要用于存储程序的命令行参数局部变量 、函数的参数值 、函数的返回地址 ,当函数被调用期间,内核会分配对应大小的栈空间给函数使用,当函数调用完成则栈空间就会内核释放

栈空间的内存存储是随机值 ,所以用户得到栈空间之后,应该把变量进行初始化 ,目的是防止变量中存储的值是不确定的

对于栈空间的地址分配是****向下递增**** ,所以栈空间使用的越多,则分配的内存地址越低 ,栈空间的数据遵循"*先进后出* "原则,一般内核都会提供两个指针,一个指针指向栈顶,一个指针指向栈底,数据进入栈空间的动作就叫做入栈/压栈(PUSH) ,数据从栈空间出去的动作就叫做出栈/弹栈(POP)

注意:Linux系统中栈空间的容量是有限的,如果超过容量,则会发生栈溢出,导致程序出现段错误

一般最多8M,所以运行时才会报段错误

思考:既然linux内存的栈空间大小是有限制的,请问栈空间的大小是多少?能否修改大小?

回答:linux系统的栈空间的大小默认是8M ,是允许修改 的,可以利用linux系统的ulimit命令来查询栈空间的大小以及修改栈空间的大小 。注意:栈空间的大小的修改是临时性,是针对当前终端的,不是永久有效的

以KB为单位

习题:内存分析重点

c 复制代码
//检查这段代码有没有问题
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 分配内存的函数
void GetMemory(char **p, int num) {
    // 问题1:未检查 malloc 是否成功
    *p = (char *)malloc(num);
}

void Test() {
    char *str = NULL;	//定义一个指针str,str存储的是NULL的值,char进行强转
    GetMemory(&str, 10);  // 传入 str 的地址,分配内存
    
    // 若 malloc 失败,str 仍为 NULL,strcpy 会触发段错误
    //没有判断
    strcpy(str, "hello");
    
    // 问题2:printf 直接用 str 作为格式化字符串,不安全
    printf(str);
    
    // 问题3:未释放 malloc 的内存,造成内存泄漏
}

int main() {
    Test();
    return 0;
}
/*
p=&str;p等于str的地址
*p=变量str
malloc(num);从堆上分配num字节内存,返回堆内存的起始地址
强转(char *)类型
存储到str中
*/

![Snipaste_2025-11-30_16-27-12](./../C图库/Snipaste_2025-11-30_16-27-12.png

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// GetMemory函数:尝试分配内存,但传参方式错误(值传递无法修改外部指针)
void GetMemory(char *p) {
     // 为局部变量p分配堆内存(100字节),但p是函数内的副本,与外部str无关
    // 这里p被赋值为malloc返回的堆地址,但外部str完全不受影响
    p = (char *)malloc(100);
}

void Test(void) {
    char *str = NULL; // 定义char*类型指针str,初始化为NULL(存储的是地址值0,隐式转换为char*类型
     // 错误1:传参方式错误------传递的是str的值(NULL),而非str的地址
    GetMemory(str);
     // 错误2:str仍为NULL,对NULL指针解引用(strcpy操作str指向的地址)会触发段错误
    strcpy(str, "hello world");
    // 错误3:printf直接使用字符串作为格式化串,存在安全风险(若字符串含%会导致未定义行为)
    // 且str为NULL时,这里也会触发段错误
    printf(str);
}

int main() {
    Test();
    return 0;
}

GetMemory函数执行后,里面的局部变量p的内容就会被释放,只返回p之前的地址

查找:什么叫时间换空间,空间换时间?

时间复杂度、空间复杂度。

日志

在学校里没有得一个奖,在格力场子里干流水线,给个优秀员工的奖,也不错,给个坚果礼盒

const常量关键字

思考:可以看到C语言标准中对于main函数的第二个参数的约束是 char * argv[],但是代码编辑器在定义main函数的时候为什么是 char const *argv[] ? 为什么多了一个const,这个词表示什么意思,有什么作用?

回答:const是C语言的关键字 之一,其实是英文constant的缩写,具有常量的含义 ,const关键字在C语言标准中是类型限定符 ,一般用于修饰变量 的,可以用于降低变量的访问权限,相当于把变量的属性变为只读变量的存储单元只能读,不能写。

举例: int data = 10; data = 20; //变量是可读可写的 const int data; //只读变量

思考:C语言中明明支持定义常量,为什么还需要定义一个变量,再把变量的权限变为只读?

回答:程序的中的常量都是存储在只读常量区(.rodata段 ),但是由于这个段的属性是只读 的,并且没有办法通过名称来访问常量 ,所以就可以定义变量,通过变量名称来间接访问 在程序运行期间不需要修改的常量的值!

定义格式: const int data; // 不行,因为变量的存储单元已经变为只读 data = 10; 会报错

注意: 如果需要利用变量来存储一个常量,则需要在定义变量的时候利用const关键字修饰,并且一定要完成初始化!

思考:既然const可以修饰普通变量,那是否可以修饰指针变量呢?如果可以修饰指针变量,那请问 int *const p; 和 const int *p; 是否有区别?有什么区别?

回答:const关键字是可以修饰指针变量,但是 int *const p; 和 const int *p;是有很大区别的!

(1) int *const p;

可以看到const离变量名称更近,const是修饰变量p的,而变量p是一个指针变量,变量p用于存储一个地址,但是变量p本身也可以得到存储单元,相当于降低了变量p的存储单元的属性,也就是变量p中存储的地址就不能发生变化,所以这个指针变量就称为****指针常量****。

(2) const int *p;

可以看到const关键字 离指针变量p指向的地址下的数据类型更近 ,所以const是用于修饰指针变量p指向的地址的 ,所以也可以写成 int const *p ;变量p的存储单元是没有受到影响的,而是变量p中存储的地址的权限降低为只读了,可以为变量p是美杜莎之眼,被称为****常量指针****。

注意:只有通过指针变量p来间接访问地址下的值时,才会出现错误,因为变量p中存储的地址被变量p影响了,权限降低为只读了,但是只要不通过变量p来间接访问,则就不会受到变量p的影响。

递归思想的应用

C语言:面向过程,自上而下,顺序执行

查找:斐波那契数列,并且程序实现

遇到数学问题,使用递归方法

思考:C语言程序的基本单位是函数,每个函数都可以解决一个问题,但是如果此时一个程序中有n个相同的问题出现,则就需要调用对应的函数n次,这样会导致程序冗杂,可读性较差,请问是否有更为简单的方案来解决对应的问题呢?

回答:可以使用递归函数解决,当然注意递归函数不是万能的,一般用于解决数学问题,递归函数指的是在一个函数内部反复调用自己的函数,递归函数具有递进和回归的过程,就相当于把一个类似的大问题拆分为很多类似的小问题,再把每个小问题的结果作为上一个问题的答案,一层一层进行解决。

注意:使用递归函数的时候要谨慎,*必须要提前写清楚终止条件*,如果不写终止条件,就会变为死循环,相当于一直调用自己,由于每调用一次函数,内核都会提供一块栈空间,就会栈溢出,从而发生段错误,导致程序崩溃。

笔试题:用户打算设计程序,用户通过键盘输入一个正整数n,然后设计一个递归函数,用来求1 * 2* 3* 4 *....*n的结果,请问如何设计程序。

笔试题:用户设计一个程序,通过scanf函数输入一个字符串,在主函数中申请一块堆内存,把输入的字符串存储到该堆内存中,然后利用递归的方案实现字符串的字符逆序输出,请问如何设计程序? "hello" -> "olleh"

练习:申请一块堆内存,利用scanf函数实现通过键盘输入一个字符串,并存储到堆内存中,要求利用递归的方案实现计算字符串的实际长度,并把计算结果输出到终端,请问如何设计程序?

注意:想要计算字符串的实际长度,可以调用库函数的方式实现:strlen() 计算字符串实际长度

作业01:利用递归思想实现设计一个程序,完成斐波那契数列的函数设计,利用递归实现!

1 1 2 3 5 8 13.....

int func(int n)

{

//终止条件

if( n<=2 )

{

return 1;

}

else

{

return func(n-1)+func(n-2);

}

}

作业02:

C语言的结构体

基本概念

思考:如果一个函数需要处理很多种不同的数据,那就需要传递很多参数,会导致用户传递参数时容易发生混乱,并且程序可读性也不好,请问有没有方案可以实现多种类型的数据传递,并且又不需要传递多个参数呢?如果有,应该如何设计程序?

回答:当然是有的,为了方便用户传递数据,所以C语言标准中提供一种数据类型叫做结构体 ,也被称为用户自定义数据类型 或者复杂数据类型,指的是用户有权利在程序中设计一种新的类型。

C语言标准中提供了一个叫做struct 的关键字,是英文structure的缩写,具有结构的含义,一般在C语言中利用该关键字设计的类型被称为结构体

定义格式

struct 结构体名称

{

数据类型 成员名称;

数据类型 成员名称;

数据类型 成员名称;

....

}; //注意:复合语句后的分号不可以省略

注意:构造的结构体类型是不占内存的,只有使用该类型创建的变量才会得到内存空间!!!

成员访问

思考:用户定义了一个新的struct结构体数据类型,并且结构体类型中包含了很多的成员,请问用户如何访问结构体中的成员呢?

(1) 通过.访问

可以看到,后缀运算符. 有两个操作对象,第一个操作对象是结构体或者联合体的类型,第二个操作对象是成员的名称。

(2) 通过->访问

初始化

思考:用户设计了一个结构体类型,并且在程序中利用该类型创建了一个结构体变量,但是用户打算在定义变量之后就对结构体进行初始化,请问如何实现?

(1) 连续赋值

(2) 单独赋值

优点:可读性好,可维护性高,更可靠 缺点:代码冗杂 ,可以忽略不计

空间大小

思考:用户设计了一个结构体类型,结构体类型中包含很多成员类型,那用户在定义结构体类型的变量时内核会分配对应的内存空间,请问内核会提供给结构体变量多少内存空间?如何计算结构体变量所需要的内存大小?

回答:可以通过C语言标准中提供的sizeof运算符来计算机结构体变量的大小,sizeof运算符的结果是以字节为单位。

可以看到,理论上计算出的结构体类型需要的字节宽度是166字节,但是实际上64bit系统计算出的字节宽度是168字节,所以出现偏差。

不能单纯的做加法运算

原因:计算自定义数据类型 宽度的时候,要考虑CPU的运行效率一般嵌入式系统都采用32bit系统 ,所以CPU的地址总线是32bit ,所以为了提高CPU的工作效率 ,所以寻址都是4字节 为主,也就是当数据宽度不够4字节,则系统默认提供4字节内存方便CPU寻址 ,所以这种方式被称为****字节对齐****。

所以计算结构体类型的大小时考虑计算机字节对齐 是非常典型的以空间换时间的案例!!!!!

验证技巧:计算出的结构体的大小应该是结构体类型中字节宽度最大的成员的整数倍!!!!!

int i; 4字节

char j; 本来是1字节

char * ptr;存储的是地址4字节
j的后面是4字节大小的数据,为了提高计算机的工作效率,将j的存储空间提升到4字节

思考:内核在为结构体变量分配内存单元的时候会进行地址对齐,好处是提高了CPU的寻址效率,但是同时也可能会导致内存空间的浪费,对于某些嵌入式产品而言,内存大小是极其有限的,请问是否有办法可以让内核在分配内存单元时采用"按需分配"的原则

回答:是可以的,只不过需要使用C语言标准的一个预处理指令 #pragma ,当然该预处理指令和编译器相关,C语言标准中的预处理指令 #pragma pack(n)可以用于进行字节对齐以及取消字节对齐。

**#pragma pack(n)**可以用于进行字节对齐以及取消字节对齐,n的值可以是1、2、4、8.......

n是字节对齐的宽度

变量定义

思考:用户定义了结构体类型之后,可以利用该结构体类型定义多个变量,但是在某些情况下用户只打算定义一个结构体类型的变量专门用来存储唯一且重要的数据 ,后期并不打算继续使用该结构体类型定义新的变量,请问是否有方案来满足这种需求?

别名定义

思考:用户在使用结构体类型创建结构体变量的时候,需要写比较繁琐的语句,可能会导致程序可读性不好,请问是否有更加简单的方案来实现相同的效果?如果有,那应该如何设计?

回答:可以使用C语言中提供了的typedef关键字 ,其实是type define的缩写,用于给某个数据类型起一个别名,不会创建新的类型

typedef关键字对数据类型起别名,应该是针对整个程序而言,所以不应该在某个复合语句内进行声明。

使用格式: typedef 数据类型 别名; 举例 : typedef unsigned int uint32_t;

潜规则:如果打算把某个数据类型起一个新的别名,在使用typedef的时候,新的别名应该在命名的时候末尾添加 _t

举例 : typedef unsigned int uint32****_t****; //为了提高可读性,用户可以知道这个是别名!!

思考:既然用户可以通过struct关键字定义一个新的数据类型,那是否可以定义一个结构体数组呢?

回答:是可以的,数组的元素可以是结构体类型,但是必须为相同的结构体类型 ,也就是结构体类型的数据宽度要一致。

函数传参

思考:用户通过struct关键字定义一个新的数据类型,并且打算封装一个函数来处理结构体类型的数据,请问是否可以把结构体类型作为函数的形参?在函数形参中是否可以使用typedef起的别名?

思考:既然可以把结构体作为函数参数 ,请问是否可以把结构体指针作为函数的参数呢,如果可以,那在函数中应该如何访问结构体中的成员?

C语言联合体

某些情况下用户只打算使用一块空间来存储不同类型的数据,而这些数据又是互斥的 ,也就是不会同时出现,为了节约内存空间,所以C语言提供了另一种复杂数据类型,那就是联合体

基本概念

C语言标准中提供了一个叫做union 的关键字,用于构造联合体类型,联合体也被称为共用体,指的是联合体中的各个成员是共用一块内存空间 ,所以联合体的内存空间 就是以成员中数据宽度最大的那个成员为主

定义格式:

union联合体名称

{

数据类型 成员名称;

数据类型 成员名称;

数据类型 成员名称;

....

}; //注意:复合语句后的分号不可以省略

注意:联合体变量中的成员由于是共用一块内存 ,所以每个成员的起始地址都是相同 的,只要修改联合体中任何一个成员的值,都会影响其他成员的值 ,另外,也不应该同时对联合体中的多个成员进行赋值

成员访问

(1) 通过.访问

可以看到,后缀运算符. 有两个操作对象,第一个操作对象是结构体或者联合体的类型,第二个操作对象是成员的名称。

(2) 通过->访问

思考:内核为结构体变量分配内存单元的过程中会遵循内存对齐原则 ,请问内核为联合体变量在分配内存单元的过程中是否同样遵循内存对齐原则呢? 回答:也是遵循字节对齐!!!

64位系统,8字节对齐

注意:联合体很少单独使用,一般都是作为结构体中一个成员而已 ,灵活一些,如果在笔试过程中遇到判断机器大小端的问题,则应该使用联合体解决,才能拿满分!

C语言的枚举

为了提高程序的可读性以及可维护性,方便后期对程序的迭代,所以C语言标准提供了一种数据类型,就是枚举类型,可以很轻松的提高程序可读性。

C语言中规定,枚举类型也属于用户自定义类型 ,用户通过关键字enum可以实现枚举类型的设计,enum是C语言关键字之一,是英文单词enumeration的缩写,中文具有枚举的含义。

constant 常量

枚举列表的每个元素都必须是int型

都是必须是整型常量

如果枚举中的第一个元素的没有赋值,默认为0

如果枚举列表中的元素未被初始化,则当前的元素的值等于前一个元素的值+1

注意:枚举就是把一些没有意义的数字(整数)起一个有意义的名称,利用该名称就相当使用该整数常量,是为了提高程序可读性!!

C语言的宏定义

基本概念

C语言中可以利用宏定义实现文本的快速替换 ,注意:宏定义是单纯的文本替换不检查语法是否合法

C语言标准中提供了很多的预处理指令,比如#include、#pragma......以#开头的都属于预处理指令。

预处理指令指的是在gcc编译套件中的cpp预处理器 对程序进行编译之前所做的一些动作,比如#include预处理指令就是在程序编译之前 由预处理器把包含的头文件中的代码拷贝一份到源文件对应的位置 ,如果包含的文件中还有其他的预处理指令,会递归执行!

C语言标准中还提供了**#define预处理指令** ,define是C语言关键字之一,中文具有定义的含义,所以利用#define预处理指令可以对某些表达式、某些常量、某些函数进行定义 ,其实就是给这些内容起一个可读性较高的名称

定义格式

宏替换其实就是简单的文本替换,宏名称 就是一个用户命名的特定的标识符 ,一****般实际开发中宏名称都采用大写****(潜规则)。 macro 宏

宏名称后面就是用来替换宏名称的替换列表,这个替换列表可以是常量、表达式、if语句以及函数等。

定义格式: #define 宏名称(大写) 替换列表 换行(一般就是用户按下回车)

用户在源文件中某个位置使用了宏,不管使用了多少次,在程序编译之前,预处理器都会把宏用替换列表进行替换,*当然要注意,宏替换就是单纯的文本替换*,预处理器并不会做任何检查,比如替换之后是否符合语法,语法的检查是由编译器在编译阶段进行的。

同一个函数中,静态变量不会重复定义

l 使用规则

注意:宏定义的作用域是针对整个文件有效,所以应该定义在源文件的开头部分,这样才可以在其他的函数中使用宏定义,另外,宏不是语句,所以不需要在末尾添加分号,如果添加分号,则分号也会被一起替换。

具体分类

C语言中宏定义的方案有三种,分别是无参数宏带参数宏无替换列表宏,具体如下所示:

(1) 无参数的宏定义

注意:除了用户自定义的宏之外,系统中也存在一些已经定义好的宏,比如常用的NULL就是一个宏,当然,C99标准中还有一些常用的系统预定义的宏:

(2) 带参数的宏定义

C语言标准中支持定义带参数的宏,带参数的宏的使用在语法上类似于函数调用,**宏的参数由括号()**进行包含,括号中如果有多个参数则需要通过逗号来分隔,另外,带参数的宏在定义的时候宏名称和参数列表之间不能空格,如下所示:

带参数的宏,替换列表应该把参数都用()括起来

可以发现,带参数的宏和函数的形式很像,但是却完全不同,带参数的宏会在程序所有出现的位置进行展开,缺点是浪费了内存空间,但是节约了函数切换的时间。

作业:

素数又称质数。所谓素数是指除了 1 和它本身以外,不能被任何整数整除的数,例如17就是素数,因为它不能被 2~16 的任一整数整除。

字符串

注意:标准C库中其实是提供了一个字符串拷贝函数,名字叫做strcpy(),使用规则如下:

strcpy函数可以把str指向的字符串拷贝到dest指向的内存中,并且是包含'\0'

strncpy可以拷贝字符串的前ng

(3) 无替换列表的宏

C语言中也允许只定义一个宏,这个宏可以没有替换列表,一般实际开发中都是对程序进行****条件编译****的情况下来使用。

条件编译指的是可以选择性的编译程序中的某段代码,也就是预处理器可以根据具体的条件来保留或者删除某段源程序。

可以理解为是类似于C语言的判断语句,只不过是使用C语言中的预处理指令来判断宏的有效性,有效性指的是宏是否为真以及宏是否存在,C语言中提供了多种预处理指令来实现条件编译。

A. #if 用于判断常量表达式是否成立 ,遵循**"非0即真"** 原则,#if预处理指令作为条件编译

一般**#if和#endif是结合一起使用**的,经常用于程序中的调试,可以选择保留或注释代码块!

B. #ifdef 用于判断宏是否被定义 ,如果宏是提前定义好的 ,则该预处理指令是有效 的,也需要和**#endif一起**使用

1q

后面学习FreeRTOS和LVGL时大量使用

C. #if和#elif和#else和#endif 用于条件编译,可以通过常量表达式的多种状态来选择保留或者删除某些代码块

D. *#ifndef和#endif* 用于判断宏是否未定义,如果宏定义,则该代码块会被删除,如果宏未被定义,则该代码块可以保留

作用范围

思考:宏定义一般定义在源文件的开头,所以作用域是针对整个文件,但是有的时候如果只打算让某个宏只对某个函数有效,请问应该如何实现?

回答:可以实现,可以利用C语言标准中提供的预处理指令**#undef** ,可以提前终止某个宏的作用域。

c 复制代码
#define MACRO if()	\
{
					\
                    \
}

\是lian'ji

程序的编译过程

思考:什么叫做预处理阶段?预处理阶段和编译阶段有什么不同?源文件转换为可执行文件一共需要经历几个阶段?

预处理:

对源码进行简单的加工,GCC编译器会调用预处理器cpp对程序进行预处理,其实就是解释源程序中所有的预处理指令,如#include(文件包含)、#define(宏定义)、#if(条件编译)等以#号开头的预处理语句。

这些预处理指令将会在预处理阶段被解释掉,如会把被包含的文件拷贝进来,覆盖掉原来的#include语句,把所有的宏定义展开,所有的条件编译语句被执行,GCC还会把所有的注释删掉,添加必要的调试信息。

预处理指令: gcc -E xxx.c -o xxx.i 会生成预处理文件 xxx.i

编译:

就是对经过预处理之后的.i文件进行进一步翻译,也就是对语法、词法的分析,最终生成对应硬件平台的汇编文件,具体生成什么平台的汇编文件取决于编译器,比如X86平台使用gcc编译器,而ARM平台使用交叉编译工具arm-linux-gcc。

编译指令 : gcc -S xxx.i -o xxx.s 会生成汇编文件 xxx.s

汇编:

GCC编译器会调用汇编器as将汇编文件翻译成可重定位文件,其实就是把.s文件的汇编代码翻译为相应的指令。

编译指令 : gcc -c xxx.s -o xxx.o 会生成目标文件 xxx.o

链接:

经过汇编步骤后生成的.o文件其实是ELF格式的可重定位文件,虽然已经生成了指令流,但是需要重定位函数地址等,所以需要链接系统提供的标准C库和其他的gcc基本库文件等,并且还要把其他的.o文件一起进行链接。-lc -lgcc 是默认的,可以省略

编译指令:gcc hello.o -o hello -lc -lgcc 会生成可执行文件 xxx // l是lib的缩写