嵌入式开发C语言基础
1. GCC的使用及其常用选项介绍
1.1 GCC概述
-
GCC的最初全名是GNU C Compiler,是GNU操作系统专门编写的一款编译器,即C-->机器语言,随着GCC支持的语言越来越多,它的名称变为GNU Compiler Collection
-
一般Linux系统会默认安装GCC,输入
gcc -v
,查看是否安装GCC,没有使用apt-get install gcc
安装 -
编译命令
gcc -o 输出文件名 输入文件名
,其中输入的文件名需要带后缀来自动调用相应类型的编译器,如c语言.c或者c++语言.cpp;输出文件不需要后缀。输入文件名也可以写在-o
前面。生成的编译文件可以直接使用./编译文件名
运行编译后的文件。 -
编译命令
gcc -v -o 输出文件名 输入文件名
,可以输出gcc工作的详细过程
1.2 C语言的编译过程
-
编译过程
预处理->编译->汇编->链接
预处理:define和include均在预处理阶段处理,define和include不是关键字
编译:主要翻译的是关键字和运算符 高级语言->汇编语言
汇编:汇编语言->二进制机器语言
gcc详细执行过程参考(189条消息) gcc简介和命令行参数说明_KuoGavin的博客-CSDN博客_gcc 机器码
1.3 编译常见错误
-
预处理错误
#include "name"
自定义库,在当前目录中寻找name,和#include <name>
系统库,在系统目录中寻找name。当在库中找不到name报错not find使用
gcc -I+头文件路径名 -o
可以从指定路径中寻找头文件。如:gcc -I./inc -o build helloWord.c
,从当前目录中的Inc目录下寻找头文件 -
编译错误
- 语法错误,如漏掉;{}
-
链接错误
-
原材料不够错误。如仅定义了fun函数,却没有实现报错undefined reference to 'fun' 。解决方法:寻找标签是否实现,连接时是否对多个文件一起加入链接
-
原材料多了。如多次实现了fun标签,报错multiple definition of 'fun'
解决方法:只保留一个标签实现即可
-
当存在多个文件时,建议先使用
gcc -c -o
将单个文件分别编译生成单独的.o
文件,再使用gcc -o
将两个.o
文件一起连接。如
-
1.4 C语言预处理使用
1.4.1 预处理介绍
-
#include 包含头文件:在预处理阶段将头文件内容展开,相当于通过gcc将包含的文件也进行编译
-
**#define 宏 ** 可以理解为替换,在预处理阶段不进行语法检查,只简单替换
#define 宏名(大写字母) 宏体
如定义 #define ABC 5+3
printf("the %d\n",ABC*5); 此时执行的为5+3*5
为了避免问题,一般都在宏体处加(),如定义#define ABC (5+3)
- 宏函数#define ABC(x) (5+(x)) 宏定义中含有变量x
-
条件编译#ifdef #else #endif
-
预定义宏
_FUNCTION_:代表函数名
_LINE_:代表行号
_FILE_:代表文件名
使用以上预定义宏可以在调试过程中方便查看问题出现的位置
执行结果:
1.4.2 条件预处理的应用
- 区分
- 调试版本
- 发行版本
调试版本便于开发人员开发、调试及二次开发,而在发行版本中通过编译预处理阶段隐藏调试的代码,保护信息,不执行调试代码。
-
在代码中不定义宏,通过
gcc -D宏名
控制版本切换的开关如:去掉代码中宏定义
define ABC
,再使用gcc -D宏名
1.4.3 宏展开下#、##的使用
#表示字符串化 #define ABC(x) #x 作用是将x转化为字符串"x"
##表示连接符号 #define ABC(x) ABC##x
2. C语言常用关键字及运算符操作
2.1 关键字
- C语言中总共有32个特殊意义的关键字
2.1.1 杂项
- sizeof:编译器给我们查看内存空间容量的一个工具,在任何编译环境下都可以使用
- return:在函数最后返回
2.1.2 数据类型
-
C操作的对象为资源(或称之为内存),内存不光包括内存条,还应包括其他资源如LCD缓存、LED灯、显存灯。
-
数据类型描述了资源的大小属性,通过sizeof(a),可以获得a的大小。注意数据类型的大小是由编译器决定的,一般来说int为4字节或2字节,char为1字节,long为4字节或8字节,short为2字节。
-
char类型
-
硬件芯片操作的最小单位为bit 即1或0;
软件操作的最小单位为byte 1B=8bit
因此char是描述硬件所能操作的在软件能体现出来的最小单位。
-
应用场景:
硬件处理的最小单位 char数组char buff[xx]和int数组int buff[xx]在数据上的不同;
ASCII码表 8bit的数据 能代表键盘的所有键位状态
-
-
int类型
int大小大小属性会根据编译器来决定
-
编译器最优的处理大小:
int是系统一个周期(受总线宽度限制),所能接收的最大处理单位
32位系统:32bit==4B==int
单片机系统:16bit==2B==int 最大表示数为2^16-1=65535
-
进制
十进制:便于人使用
二进制:计算机操作
八进制:用3bit描述一个八进制数 如八进制数12 ==> 转换为二进制001 010
八进制数开头为0:如int a=010; 代表的为八进制数,转为十进制为8
十六进制:用4bit描述一个十六进制数
十六进制数开头为0x:如int a=0x10; 代表的为八进制数,转为十进制为16
-
-
long和short
-
long和short是为了作为int和short的补充
-
short是特殊长度的限制符,除非是在32位中要求空间长度必须是16bit时才用short,否则都使用int
-
long是C语言可扩的一种数据类型,如long long类型表示64位
-
-
无符号数unsigned、有符号signed
- 对于数据类型不声明默认为有符号数,如int a,若要使用无符号数要声明unsigned int a;
- 使用场景:无符号数用于数据(如摄像头采集数据),有符号数用于数字计算
-
浮点数float,double
- float占4B,double占8B
- 浮点型常量 如1.0 2.0
-
void类型
- 主要用于占位标志,用于声明一些东西,更多的是一种语义含义
2.1.3 自定义数据类型
-
struct结构体和union共同体
-
struct结构体表示元素之间的和
struct中的顺序有要求
-
union共用体
允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值
不同的数据类型共用起始地址
-
enum枚举类型
enum可以理解为被命名的整型常数的集合,可以用来代替宏定义,宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。enum的好处在于更好描述一组数据,语义更加清晰。
使用格式:
enum typeName{valueName1,valueName2,valueName3......};
typeName是枚举类型的名字,valueName是枚举成员,注意其值只能为常量,不能为变量。
在没有显示说明的情况下,枚举常量(也就是花括号中的常量名)默认第一个枚举常量的值为0,往后每个枚举常量依次递增1。
如:
-
typedef
为其他数据类型起别名,更好理解变量的使用场景
如:
2.1.4 逻辑结构
- CPU顺序执行程序
- 分支->选择
- if...else
- switch...case...default
- 循环
- do
- while
- for
- 循环控制符continue、break、goto
2.1.5 类型修饰符
- 对于内存资源属性存放位置的限定
- auto:默认值(可以不写)-->分配内存为可读可写的区域
auto int a
auto long b
如果使用{}
包含的数据位于栈空间
{auto char a}
-
register:限制变量定义在寄存器(CPU内部)上的修饰符
register int a
定义一些快速访问的变量时使用
编译器会尽量安排CPU的寄存器去存放这个变量,但如果寄存器不足时,a还是放在存储器中
对于内存(寄存器),其地址一般表述为十六进制数字,如0x100。而对于寄存器,一般由芯片 决定,如ARM的R0、R2寄存器。因此使用&符号获取变量地址,对于register修饰的变量不起作 用。如:
运行结果:
- static:静态
static的3种应用场景
- extern:外部声明
- const:常量的定义,是只读的变量。如
const int a=100
- volatile:告知编译器编译方法的关键字。不优化编译,一般用在嵌入式开发中
修饰变量的值的修改不仅仅可以通过软件,也可以通过其他方式(硬件的外部用户)
2.2 运算符
2.2.1 算数操作运算+、-、*、/、%
-
注意在大部分硬件开发中,CPU都只支持+、-,而不支持*、/运算的
对于
int a = b+10
,CPU一个周期就可以处理而对于
int a = b*10
,CPU可能需要多个周期处理,甚至需要利用软件模拟的方法去实现乘法(对于裸机一般不具有软件) -
%运算的使用
0 % 3 = 0 1%3=1 2%3=2 3%3=0 4%3=1... ...
===>n%m=res res的范围为[0,m-1]
利用%运算的特点可以得到几种使用方式
1)取一个范围的数 如给任意一个数字,得到一个1到100以内的数
(m%100)+1===>[1,100]
2)得到一个M进制数的一个个位数
如16进制数个位为[0,15],8进制为[0,7],M进制数个位为[0,M-1]
3)循环数据结构下标
2.2.2 逻辑运算
-
||、&&、>、>=、<、<=、!、? :
-
注意
A||B
运算,只要A为真,C语言编译器即可判定A||B
为真,不会执行B
同样A&&B
运算,只要A为假,就判断A&&B
为假
- 注意逻辑取反
!A
和按位取反~A
2.2.3 位运算
- <<(左移)、>>(右移)、&、|、^、~
- 移位运算
1)不发生溢出情况下,左移一位相当于数值扩大2倍,适用于有符号数和无符号数
如:m<<1=====>相当于,如00100 左移一位变为 01000
m<<n=====>相当于,如int a=b*32 可以变为b<<5
对于负数,左移仍然是每移动一位数值扩大两倍
如,也可以变为-1向左移动一位
采用8bit数: -1 原码1000 0001 -2 原码1000 0010
反码1111 1110 反码1111 1101
计算机中存储 补码1111 1111==左移1位==>补码1111 1110
2)正数的右移相当于除法,右移几位就除以2的几次方,如100>>4 等效 100/2^4(只包括商)
负数的右移不等于除法,即负数右移不能按除以2的n次方计算
右移:与符号变量有关 即正数右移高位补0;负数右移高位补1
- 与或运算
1)&: 注意是按补码运算 对于一个资源A A & 0 ---> 0
A & 1------>A
作用
1)屏蔽某些位 如 int a=0x1234
屏蔽低八位 a & 0xff00
2)取出某些位
3)在硬件中&一般设置为低电平,因此常用作清零器(clr)
2)|: A|0===A
A|1=====1
作用
1)设置为高电平的方法,设置器(set)
3)清零器(clr)和设置器(set)使用例子:
1)如让一个资源的bit5(规定计数都是从0位开始的)为高电平,其他位不变(使用set)
int a
a=(a|(0x1<<5))
=======>让bit n为高电平a|(0x1<<n)
2)让一个资源的bit5清除
a=a&(~(0x1<<5))
======>让bit n为低电平a&(~(0x1<<n))
- ^异或操作 相同为假,相异为真
工程上异或使用较少,主要用于实现某些算法
^异或的使用,置换两个变量
- ~按位取反
2.2.4 赋值运算
- =、+=、-=、&=、|=、... ...
a+=b
====>a=a+b
a &= ~(0x1<<5)
====>a = a & ~(0x1<<5)
2.2.5 内存访问符号
- ()、[]、{}、->、. 、&、*
3. C语言内存空间的使用
3.1指针
3.1.1 指针概述
-
指针:内存资源的地址
内存资源操作的最小单位是1字节,指针指向的是内存资源首个字节的地址
-
指针变量:存放指针这个概念的变量
-
指针中两个重要的概念
-
指针变量中存放的地址应当有多少位
在32位系统中,指针变量为4字节,即32bit
-
指针所指向的地址的读取方法是什么
char *p
代表每次读取1字节int *p
代表每次读取4字节或2字节即
*p
前面的数据类型值得是内存的读取方法 每次读取多少个字节数 一般指针变量的数据类型应当和指针指向地址的数据类型相同,不同时c语言也可以执行,但是会警告。指针指向的内存空间,一定要保证合法性 如直接
int *p=0x1122
即为不合法,不能保证0x1122这个地址是否可用。
-
-
使用指针读取浮点数
3.1.2 指针+修饰符(const 、voliatile、typedef )
- 指针+const
几种不同 指针+const 写法
1)修饰指针指向的属性:即只读操作,指向的地址可以更改,但地址中的内容不能更改。
一般用在字符串 如"hello world"
const char *p
推荐使用格式
char const *p
2)修饰指针地址,指针地址不可更改,地址指向的内容可以更改。一般用在硬件资源,如LCD 灯,显卡资源
char *const p
推荐使用格式
char *p const
3)指针地址不可更改,内容也不可更改 一般用在硬件的ROM存储器中
const char *const p
printf("%c",*p);
:输出的是p指向的字节
printf("%s",p);
:输出的是指针p指向的字符串
printf("%p",p);
:输出的是指针p的地址
linux下可以输入man printf
查看printf的帮助文档
printf("%s",p)详解C语言 printf("%s",p) - 简书 (jianshu.com)
- 指针+volatile
voliatile char *p
:修饰指针指向的内容
char * voliatile p
:修饰指针
- 指针+typedef
1)未使用typedef,char *p
:p是一个指针,指向了一个char类型的内存
2)使用typedef,typedef char *name_t
:name_t就是一个表示char型内存的指针类型名称
继续使用name_t p
定义指针,p仍指向了一个char类型的内存
3.1.3 指针+运算符
- 指针的++、--、+、-
指针的+、-运算,实际上操作的是一个单位,单位的大小为sizeof(p[0])
如假设
p+:未更新p
p++:更新了p,即p=p+1
- 指针与[ ]
变量名[n]:其中n被称为标签ID,这是一种地址内容的标签访问方式,并获得标签里的内存值
即p[n]相当于p+n,并取出p+n位置的值
- 指针越界访问
内存空间被切成不同标签的空间,通过指针可以访问不同的标签值,如上例中p1[10]
,访问不是 自身维护的数据时,即发生越界。因此在使用指针时,还要定义属性:访问范围的大小。 访问超 出范围,将发生内存泄露 (段错误)
内存越界的实例:
const
修饰的变量,在C中并不是不能变化,在C中const
只是建议性字符,编译器一般不会进行 改变,但通过指针越界可以修改const
修饰的变量
- 指针与逻辑运算符 ==、!=
1)指针跟一个特殊值进行比较 一般为0x0:地址的无效值,结束标志
如if(p==0x0)
,一般我们使用if(p==NULL)
,NULL是编译器定义的宏,值为0x0
2)指针必须是同种类型的比较菜更有意义
如char *
与int *
比较无意义,在编译阶段就会报错或警告
3.1.4 多级指针
- 存放地址的地址空间 如
int**p
,特别注意char **p
- 其中p[0]、p[1]....p[n]表示的为存放的地址,当其中某个地址p[m]==NULL时,表明地址结束
- 例子,通过命令行传递参数
int main(int argc,char **argv)
argc是传递给应用程序的参数个数,argv是传递给应用程序的参数,且第一个参数为程序名
[(190条消息) c语言中argc和argv ]的作用及用法_Black_黑色的博客-CSDN博客_c语言argv
运行结果:
不使用输入参数个数argc,利用地址p[m]==NULL
,同样可以输出argv
运行结果:
3.2 数组
3.2.1 数组空间的初始化
- 空间的赋值
1)按照标签逐一处理
int a[10]; a[0]=xx; a[1]=yy......
2)在空间定义时就告知编译器初始化情况,由编译器代替程序员进行赋值处理,只能用于空间的第 一次赋值。
int a[10]=空间;
注意C语言本身,CPU内部一般不支持空间和空间的拷贝
int a[10]={10,20,30};
内部实际上还是执行了a[0]=10;a[1]=20;a[2]=30
,对于为赋值的空间 初始值可能为0,也可能为任意随机数
数组空间的初始化和变量的初始化本质不同,尤其在嵌入式裸机开发中,空间的初始化往往需要库 函数的辅助,或者由程序员定义
- char[]字符数组
char[]数组解析 (190条消息) char 数组 解析_贵在坚持,不忘初心的博客-CSDN博客_char数组
char * 与char []的区别 [(190条消息) char与char ]的区别_Solieaor的博客-CSDN博客_char[]和char*
char buf[10]={'a','b','c'}
,buf可以当成普通内存,但buf当为一个字符串来看时,最后要加 一个'\0'或0
char buf[10]={"abc"}
,使用“ ”
结尾中的默认增加了个'\0'
也可以简写为char buf[10]="abc";
进一步简写char buf[]="abc";
注意此时空间的大小为4个字节,还包含一个'\0'
注意字符数组是将字符串复制到buf中,所以可以修改字符。指针指向的字符串是不可修改的。如
cahr buf[10]="abc"
buf[2]='e'
- 字符数组的二次赋值
1)对于字符数组char buf[10]="abc"
不能直接使用buf="Hello world"
进行二次赋值,而应该使用以下方法进行逐一赋值
2)使用函数strcpy,strncpy进行字符串拷贝
字符串拷贝函数的原则:
内存空间和内存空间的逐一赋值的功能的一个封装体
一旦空间中出现了0 ('\0') 这个特殊值,函数就结束工作
但是在工程中一般不使用strcpy(),该函数容易造成内存的泄露
使用更安全的strncpy(),该函数需要指定拷贝的字符个数
- 非字符串空间
1)字符空间:可以使用ASCII码来编码和解码的空间,用'\0'作为结束标志
非字符空间:如采集到的数据,开辟一个存储空间存储这些数据
char buf[];----->string
unsigned char buf[];----->data
2)非字符串空间拷贝使用memcpy()函数,必须指定拷贝的个数
3.2.1 指针与数组
- 指针数组
char *a[100]
:数组中存放的是100个数据的内存地址,每个地址的大小仍为4字节,因此数组的大小为100*4个字节。指针指向的内存为char型
指针数组与二级指针的区别:
指针数组是一个数组,那么指针数组的数组名是一个地址,它指向数组中第一个元素。
指针数组的数组名实质是一个指向数组的二级指针
- 数组名的保存
1)定义一个指针,指向int a[10]
的首地址
int *p1=a
2)定义一个指针,指向int b[5][6]
的首地址
int (*p2)[6]=b
,它表示p2是一个指针变量,它指向包含6个元素的一维数组,即一维数组b[0]
的首地址
3.3 结构体字节对齐
-
由于我们32位的计算机在处理数据时最喜欢4个字节4个字节的处理,这样效率最高,所以在堆栈中,是4个字节一排,方便计算机每次处理4个字节。
-
字节对齐
的本质就是:作为计算机本身,是想要空间还是要效率?显然在对于处理结构体这样特定的数据时,就会选择效率 -
结构体的一个成员先占领一个堆栈空间,如果在这一排中剩下字节如果不能满足下一个成员的大小,那么下一个成员就会在下一排堆栈空间存放数据。
如果能满足下一个成员的大小的情况下,两者大小不到四的话,小的一方补齐字节,平均分配这 一排堆栈4个字节。
最终结构体的大小一定是4的倍数
结构体里成员变量的顺序不一致,也会影响到它的大小
3.4 内存分布图
(190条消息) C/C++:内存分配,详解内存分布(P:图解及代码示例)_AngelDg的博客-CSDN博客_c++内存图
链接:https://www.nowcoder.com/questionTerminal/d1622983cfdb47e98908f648f65576df?source=relative
-
bss段:
bss段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。
bss是英文Block Started by Symbol的简称。
bss段属于静态内存分配。
-
data段:
数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。
数据段属于静态内存分配。
-
text段:
代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。
这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可 写,即允许修改程序。
在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
-
堆(heap):
堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);
当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。
-
栈(stack):
栈又称堆栈,是用户存放程序临时创建的局部变量,
也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变 量)。
除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。
由于栈的先进先出(FIFO)特点,所以栈特别方便用来保存/恢复调用现场。
从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区
内核空间 应用程序不允许访问
栈空间 局部变量 函数{ }执行完即释放空间
运行时的堆空间
全局的数据空间 已初始化的全局变量属于data段,未初始化的全局变量属于bss段(静态段)
只读数据段 如字符 属于text段
代码段 属于text段(静态段)
0x0
在32位操作系统下,4G的内存用户能操作的内存空间大致有3G
3.4.1 只读空间
- (text段)代码段和数据段 只读
代码段
数据段
注意:printf中的" "
中字符串也用数据段的空间,如将上例子中7行中改为printf("111the address of p is %p\n",p);
,同样会增加3个1所占的3个字节;
使用size
可以查看段大小
未修改前:text段大小为1678字节
修改后,增加3字节
第6行和第10行" "
中字符串为共用的,若是修改其中一行,则会重新生成新的字符串空间。
因此在嵌入式裸板开发中,输出语句都应在调试版本中,在发行版本中尽量避免输出语句。
使用strings
可以查看代码中有的字符串(不仅仅是自己写的字符串)
3.4.2 数据段
- 未初始化的全局变量放在bss段,默认值为0
- 初始化的全局变量放在data段
- static修饰的局部变量也会放在全局数据段(bss或data),不会随着函数的返回消失。但是静态局部变量只在相应的函数{ }中有效,出了函数即失效
代码1和代码2分别使用size
查看内存变化 使用nm
查看静态空间中数据 标签与地址
使用size
查看段大小
-
全局的数据空间 可读可写
-
全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量。全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。 这两者的区别在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而 静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。 由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。
-
const修饰的局部变量仍然位于栈空间,通过指针可以修改数值;const修饰的全局变量才位于全局数据空间中
-
3.4.3 堆空间
-
段对比
- 静态空间,整个程序结束时释放内存,生命周期最长
- 栈空间,运行时函数内部使用变量,函数一旦返回就释放,生命周期是函数内
- 堆空间,运行时可以自由分配和释放的空间,生命周期由程序员决定
-
内存分配使用malloc()函数
malloc()函数一旦执行成功,将返回分配好的地址,我们只需要接受这个地址。对于这个新地址采用何种读取方式,有程序员灵活把握,如char型,int型。函数中需要指定分配空间的大小,单位为字节
注意malloc()函数也可能执行失败
函数结束后,指针p会自动释放,但是生成的内存不会自动释放
-
内存释放使用free()
free(p)
4. C语言函数的使用
4.1 函数概述
- 函数,简单来说就是一堆代码的集合,用一个标签去描述它 代码复用化
函数也是一段连续的空间,函数应具备3个元素
1)函数名 (地址)
2)输入参数
3)返回值
-
函数的定义与调用
-
如何使用指针描述函数(函数指针)
对于函数int fun(int,int,char){}
可以定义函数指针int (*p)(int,int,char)
,再使用p=fun
指向函数fun
进阶:函数指针数组,把函数的地址存到一个数组中,那这个数组就叫函数指针数组。作用将若干个函数放在一起管理。定义方式如下
4.2 输入参数
4.2.1 函数的实参与形参
- 函数起承上启下的功能
调用者:
函数名(要传递的数据) //实参
调用者:
函数的具体实现
函数的返回值 函数名(接收的数据) //形参
{
xx xxx
}
实参 传递给 形参
- 传递的形式:拷贝
C语言中实参和形参是逐位拷贝的,不管实参和形参的长度和类型是否相同,都可以拷贝
4.2.2 值传递与地址传递
- 值传递
上层调用者 保护自己空间值不被修改的能力
- 地址传递
上层调用者 允许下层子函数 修改自己空间值的方式
连续空间的传递一般都使用地址传递(节约内存空间)
4.2.3 连续空间的传递
- 连续空间传递概述
- 数组
数组的 实参==>形参 只能使用地址传递 数组名==标签==地址
- 结构体(结构体变量)
结构体变量跟普通变量一样可以使用 值传递和地址传递
但是为了节约内存,一般不建议使用值传递
- 连续空间的只读性
- 在使用地址传递(尤其是连续空间只能使用地址传递)时,我们有时不希望改变原空间的地址,怎么实现?
在形参中使用const修饰地址指针
如void fun(const char*p)
:即表明为只读空间,不能修改p指向的内存
void fun(char *p)
:意味着该空间可以修改
为了避免出现错误,我们在定义函数时要明确指出该空间是否为只读
- 库函数中的只读的例子
如strcpy()函数 char *strcpy(char *dest, const char *src);
- 字符空间
-
地址传递的作用
-
对于基本的数据类型进行修改 如
int *
、char\*
-
空间传递
-
子函数看看空间里的情况 使用
const
修饰形参 -
子函数反向修改上层空间的内容 (字符空间怎么修改、非字符空间怎么修改)
对于连续字符空间使用
char*
,结束标志为0x00
。对于连续非字符空间使用
void *
,需给出空间长度。为了区别单值传递,不建议使用int *,unsigned *
等形参定义方式,统一使用void *
。
-
-
-
对于空间地址要明确空间的首地址、结束标志
结束标志:
字符空间——内存里面存放了0x00(1B)
非字符空间不能使用0x00当成结束标志
-
对于字符空间操作的框架
- 函数strlen() 和strcpy()的实现
- 非字符空间
- 对于
int *p、unsigned char *p、short *p、struct abc *p
类型的参数,一般都是非字符空间的参数传递
非字符空间的结束标志:需要在传递参数时,指定传递参数的数量(单位是B)
void*
:数据空间的标识符,用在描述形参,可以避免函数实参类型的多种多样
总结:对于非字符空间,形参要是用void *
,并且要给出数据的长度,对于接收到的形参数据在使用前要转为具体的数据类型
4.3 返回值
- 函数返回值的基本语法
- 函数的返回值传递仍然是拷贝
-
返回类型
基本数据
指针类型(空间)
- 返回基本数据类型
- 返回的数据类型除了int、char、short……等基本数据类型外,还能返回struct结构体的数据类型,但是由于返回的实质是拷贝,为了节约空间不建议使用struct返回,而是使用指针返回
- 返回连续空间类型
- 指针作为空间返回的唯一数据类型
- 设计函数时要考虑指向地址的合法性
必须保证函数返回的地址所指向的空间时合法的。比如说局部变量不合法,在函数结束后就会自动销毁,再返回地址就没用。数据段,堆中的数据均合法。
- 函数返回的内部实现
- 对于基本数据类型返回 内部实现的框架如下
-
地址返回 内部实现框架
-
函数返回的数据地址主要有三种类型:
- 只读区 如字符串,但是这样的返回是没有意义的,因为只读区的数据声明周期和作用域都是在编译后即存在,不需要使用函数
- 静态区 static修饰的局部变量 生命周期是到运行结束
静态局部变量的声明周期和作用域(192条消息) 静态变量,静态局部变量的生存周期_xiaoheibaqi的博客-CSDN博客_局部静态变量生命周期
- 堆区 注意使用malloc()开辟空间,使用free()释放空间
5. 常用面试题目
- 宏定义
- 用预处理命令#define声明一个常数,用以表示1年中有多少秒(忽略闰年)
知识点:宏定义:#define 宏名 宏体
宏名:要使用大写字母
#define SECOND_OF_YEAR (365*24*3600)UL
注意:1)365*24*3600
是一个常量,在编译阶段就被计算出来了,因此不会在执行时重写计算
2)UL中U代表无符号数,L代表long型,指定UL是为了防止溢出
在不同的系统中,int可能为2字节或4字节,但是char是1字节,long是4字节
8bit范围为0~255
16bit范围为0~65535
为了防止溢出,凡是超出65535的数都用long定义,不用int防止开发板不同的歧义
-
数据声明
-
类型修饰符
- 关键字static
-
修饰局部变量
默认局部变量在 栈 空间存在,生存周期比较短
局部静态化后,局部变量在静态数据段中保存,生存周期非常长
-
修饰全局变量
防止重命名,限制变量名只在本文件内起作用
-
修饰全局函数
防止重命名,限制该函数只在本文件内起作用
-
关键字const
C中:const为只读,但具有建议性作用,而不具备强制性。不能将const理解为常量
C++中:可以理解为常量,不能修改
-
关键字volatile
防止C语言编译器的优化
修饰的变量可能通过第三方来修改
-
位操作
设置变量a的bit3
unsigned int a;
a|=(0x1<<3);
清除变量a的bit3
a&~(0x1<<3);
- 访问固定内存位置
在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66.
方法一:
int *p=(int *)0x67a9;
p[0]=0xaa66;
或者*p=0xaa66;
方法二:
*((int *)0x67a9)=0xaa66
__EOF__

本文链接:https://www.cnblogs.com/ray93/p/16355717.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
本文来自博客园,作者:{Ray963},转载请注明原文链接:{https://www.cnblogs.com/ray93/}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!