(一级标题、三级...;以此类推
什么时候用?
怎么用?
为什么要这样设计?
理解C语言本质 > 语法
GCC的使用及其常用选项介绍
GCC 编译过程 P3
(1). 预处理 (2).编译 (3).汇编 (4).连接
自我理解:(1)将 .c 文件 变成 .i (2,3)将 .i 再变成 .o 最后(4)链接成可直接执行应用
(1)预处理也可以被称为替换,此时将宏和include已经展开替换
(替换,能把定义的宏替换 #define ABC 10 int a =ABC => int a = 10 。此时define ,include已经被处理完了)
define include 不是关键字
预处理错误: p4
include < > 系统库
include " " 自定义的头文件
预处理找不到头文件(not find),可以使用 gcc -I 指定头文件目录
(2)
编译错误:
语法错误 漏写 ; { }
(3)链接也就是将许多.o文件融合成一个可执行程序
链接错误:
.o文件不足
声明但未定义函数
函数名重复
一般链接错,使用 gcc -c 把预处理、编译、汇编都做了,但是不链接,来纠错
预处理的使用 p5-p7
预定义宏:
gcc -D : (p6)
gcc -DABC == #define ABC
在预处理前定义一个宏,根据宏ABC判断代码运行时是调试还是发行版本。
C语言常用关键字及运算符操作
关键字
关键字 sizeof、return p9
sizeof:编译器给我们查看内存空间的一个工具(不是函数的实现,在任何环境下(裸机等)都能使用。因为printf其实是标准c库函数,裸机(不带操作系统)没有这个函数(printf))
return:返回值
数据类型与关键字介绍以及char类型 p10
C操作对象:资源/内存{内存类型的资源,LCD缓存、LED灯}
C语言如何描述这些资源的属性那?
资源属性[大小]
限制内存(土地)的大小,关键字
char
硬件芯片操作的最小单位:
bit(比特,位) 1 0 (高地电平)
软件操作的最小单位: 8bit == 1B(字节)
正常宽带4M=4Mbit 使用转化 Kbit/s => KB/s
char a; 1字节
应用场景:
硬件处理的最小单位 char buff[xx]; int [xx]; char是一个字节,软件接受的最小单元
ASCII码表 8bit(用8bit表示键盘的所有键位)
盒子:1 2 3 4 5 6 7 8 9 10
状态:2 4 8 16 32 64 128 256 512 1024
数据类型与关键字介绍以及int、long、short类型 p10
8bit表示的最大状态值(最大位数) == 256
char a = 300; 当你a++,a!= 301,因为char 1bit 你令其为300已经溢出了
int
大小:
根据编译器来决定
编译器最优的处理大小:
系统一个周期,所能接受的最大处理单位,int
32bit 4B int
16bit 2B int(单片机)
(数字的数据类型)int a; (表示状态)char a;
==============================
整型常量
int
进制表示
十进制 八进制 十六进制 二进制
3bit 8进制
111 0x7
1000 0x8 int a= 010;//0开头系统默认8进制,十进制8
4bit 16进制 int a=0x10 //0x开头默认十六进制,16
long、short
int类型的扩充,short短整型2B,long长整型4B
数据类型之符号数、浮点类型 p12
unsigned、signed
无符号:数据
有符号:数字
内存空间的最高字节 是符号位 还是数据
unsigned int a;
char a;
float、double
大小
float 4B
double 8B
内存存在形式
0x10 16
0001 0000 16
浮点型常量
1.0 1.1 double
1.0f float
自定义数据类型struct.、union、typedef P13
struct
不同(相同)类型元素之间的和
struct myabc{
unsigned int a;
unsigned int b;
unsigned int c;
unsigned int d;
};
int i;
struct myabc mybuf;
元素顺序有要求的
union
共用起始地址的一-段内存
技巧型代码
union myabc{
char a;
int b;
};
union myabc abc;//int a;
enum
被命名的整型常数的集合
#define MON 0
#define TUE 1
#define WED 2
enum abc{ MOD = 0,TUE,WED }//等同于上述写法
enum 枚举名称 {常量列表};
enum week{
Monday= 0 ,Tuesday =1 ,Wednesday= 2,
Thursday,Friday,
Saturday,Sunday
};
typedef
数据类型的别名
len t
time_ t
int a=170;
int b = 3600;
len_t a= 170;
time_ t b= 3600;
int a; a是一个int类型的变量
typedef int a_t; a_t是一个int类型的外号
a_t mysize;
xxx_ t: typedef 习惯用 _
len_ t
逻辑结构关键字 p16
逻辑结构
CPU顺序执行程序
PC
分支-》选择
循环
if、else
条件
if(条件表达式)
xxx;
else
yyy;
switch、case、 default
多分支
swtich(整形数字)//注意:不能用浮点数
float a
switch(a)
{
case 1.0:
break:
case2.0:
}
do、while、for
for: 次数
while : 条件
do: 先执行do再while之类的
continue, break、 goto
注意goto要在同一个函数中使用
类型修饰符 p17-p19
对内存资源存放位置的限定
资源属性中位置的限定
一、register
auto
默认情况------->分配的内存可读可写的区域
auto int a;
auto long b:
区域如果在{ },栈空间
register
auto int a;
register int a;
限制变量定义在寄存器上的修饰符
定义一些快速访问的变量
编译器会尽量的安排CPU的寄存器去存放这个“register int a”的 a ,如果寄存器不足时,a还是放在存储器中。
&这个符号对register不起作用
内存(存储器) 寄存器
(地址即可)0x100 (特殊编号)RO, R2
内存(存储器) 寄存器
CPU在一个区域,内存在另一个区域;而寄存器就像CPU这个大区域中很多小块,CPU直接访问寄存器(也可以理解为缓存)的数据速度很快,而访问内存的数据比较慢。
二、_ static_ const
static
静态
应用场景:
修饰3种数据:
1)、函数内部的变量
int fun()
int a; ===> static int a;
2)、函数外部的变量
int a;
====> static int a;
int fun()
3)、函数的修饰符
int fun() i = ==> static int fun():
const
常量的定义
只读的变量
const int a= 100;//a的值不可修改
但是还是有办法进行赋值 a= 200;
const修饰的局部变量依然在栈区
在STM32中,使用const定义的变量,是存储在Flash里面的
三、Volatile
告知编译器编译方法的关键字,不优化编译
修饰变量的值的修改,不仅仅可以通过软件,也可以通过其他方式(硬件外部的用户)
例如:
int a= 100;
while( a==100 );
mylcd();
=======================================
[a] : a的地址
f1: LDR RO, [a]
f2: CMP R0, #100
f3: JMPeq f1 ----》 JMPEQ f2 因为系统认为a=100,赋值不会再改变,会优化步骤跳转到f2,而不是重新回到f1
f4: mylcd();
总结:汇编层面有1、2、3、4个步骤,1赋值,2比较,3是条件判断,4结果。当3不满足正常会跳转到1,但是系统认为1的赋值不会再改变,会优化步骤直接跳转到2,但有时候变量会通过其他方式(硬件外部的用户)改变,这时候就达不到我们想要的结果4。
运算符 p20-p25
一、常用运算符
+、-
A + B 保证相加减数据类型相同
* 、 / 、 %
inta= b*10; CPU可能多个周期,甚至要利用软件的模拟方法去实现乘法,不直接支持乘法
inta= b+10; CPU 一个周期可以处理
%:(取余,或求模)
0 %3=0 ,1%3= 1, 2%3=2 3%3=0 ,4%3=1 ,5%3=2,6%3=0... ...
n % m=res;//res的取值范围 [0 ~ m-1]
应用场景:
取一个范围的数:
eg.给一个任意的数字,得到一一个1到100以内的数字?
(n% 100)+1 ===> res;//直接101求模会得到0-100,因此需要100求模+1
得到M进制的一个个位数;
循环数据结构的下标;//类似循环数组就用到取余
二、逻辑运算
逻辑运算
真 假
返回结果就是1 0
int a = -1;
if(a)
|| 、 &&(或、与;并联,串联;)
AIIB != B || A 两者并不等价,因为编译器在判断“AIIB”中优先判断A是否为真,为真后并不会判断B(不会执行B)
A&&B != B&&A 同上A若为假则不会判断B
>=、<、<= 无需多言,正常理解
! 逻辑中取反
对比位运算中取反
int a = 0x0;
!a if(!a) { } !a为真
~a 0xffff 位取反,变为全1(0xffff)
? : 类似if else
三、位运算符 p22-p25
①移位运算符
<<、>>
<<左移(乘法 ‘*’):
乘法*2二进制下的移位
m << 1; 左移1位,类似m * 2
4: 0 1 0 0
8: 1 0 0 0
m << n; 左移n位,类似m * 2^n
eg: int a = b*32; 类似b<<5
[数据、数字] 数据:存放的内容;数字:包括符号,正负
例如:
-1 * 2 = -2;
“-1” 的8bit表示:
原码:(符号位加上真值的绝对值)
1 0 0 0 0 0 0 1 (最高位1表示负数)
反码:(符号位不变其余取反)
1 1 1 1 1 1 1 0
补码:(补+1, 也就是在计算机中表示的方式)
1 1 1 1 1 1 1 1 计算机中这么存储,也就是我们显示中看到的“-1”
很多开发中把-1,当成标志位,因为-1原码都是高电平“1”
1. 原码
原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。
2. 反码
反码的表示方法是:
正数的反码是其本身;
负数的反码是在其原码的基础上,符号位不变,其余各个位取反。
3. 补码
补码的表示方法是:
正数的补码就是其本身;
负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1。(也即在反码的基础上+1)
-1 * 2 = -2; “-2”三种表示方式如下:
1 0 0 0 0 0 1 0
1 1 1 1 1 1 0 1
1 1 1 1 1 1 1 0 对比“-1”,整体左移了一位
如图:
>>右移(除法 ‘/’)
如图:但是涉及一个最高位0、1的问题所以要特别注意
和符号变量有关
右移是严格遵循符号操作位的运算符
int a; a>>n
unsigned int a; a>>n
右移,移动空出来的位置,正数填0负数填1
例如:如果‘a’没有区分符号,则若‘a’为负数‘a=-10’右移,最高位永远为‘1’,会出现死循环
②与或非运算符&、|、^ (串联、并联、取反) p23
&、|
A & 0 ---> 0
&:屏蔽
int a = 0x1234;
a & 0xff00;屏蔽低8bit, 取出高8bit
A & 1 ---> A
&:取出
&(与0能改变A清零):清零器 clr
I :
A | 0 === A
保留
A | 1 === 1
|:(或1能改变A置一)设置为高电平的方法,设置set
设置一个资源的bit5为高电平,其他位不变
int a;
a=( a | 1 0 0 0 0 )
更便捷的方法:a = ( a | (0x1<<5) ); =====> a = ( a | (0x1<<n) ) 第n位
清除第五位
int a;
a = ( a & ~(0x1<<4) ); =======> a = ( a & ~(0x1<<n) );
假如:
int a;
a = a & 0 1 1 1 1 1(6bit) 十进制31,a&31
31是32bit存储,转化为二进制00....0 1 1 1 1 1,第五位之后的高位部为零,不同机器会错。
③~、^取反异或运算符
^:(相同为0,不同为1)
1 ^ 1 = 0 0 ^ 0 = 0
1 ^ 0 = 1
----------------------------
交换两个数:
1)
int a = 10;
int b = 20;
int temp = 0;
temp = a;
a = b;
b = a;
2)
int a = 10;
int b = 20;
a = a ^ b;
b = a ^ b;
a = a ^ b;
10:0 1 0 1 0
20:1 0 1 0 0
^ : 1 1 1 1 0 ==> = a
^ : 0 1 0 1 0 ==>10 = b
^ : 1 0 0 1 0 ==>20 = a
两个数a,b异或出现一个的数‘x’,‘x’与任意一位异或都会得到两个数的另一位。
a^b = x; x^a=b; x^b=a;
补码方式存储与计算,逻辑运算是用原码还是补码?
计算机在存储和计算整数时通常使用补码表示法。补码是一种用于表示有符号整数的编码方式,在补码表示中,最高位为符号位(0表示正数,1表示负数),其余位表示数值部分。
计算机在进行逻辑与(&)、逻辑或(|)、逻辑非(~)等位运算时,使用的是原码而不是补码。逻辑位运算是直接对二进制位进行操作,不涉及符号位,因此无需进行补码转换。在逻辑位运算中,使用的是数据的原码表示。
~:
32位常量 0xf0 ~ 0xffff ff0f
456bit设置为101
a = (a | (0x5 << 3) ) | (a & ~(0x1 << 4));
注意:在二进制数 "1100 0111" 中,456bit 指的是第 4、第 5 和第 6 位(由左向右计数)。这三位的数据是 "000"。
四、内存访问符 p25
赋值运算
a l (0x1<<5)
~a
a=a+b
a+=b a= a+b; a+=b
a |= (0x1<<5)
a &= ~(0x1<<5)
内存访问符号
():强调优先级,或者函数fun();
[]:内存访问的ID符号 a[1] a[2] 把数组当做内存(地址,指针)理解就好
{}:函数体的限制符 struct abc{xxx}X
->、.:‘->’地址访问,指针指向;‘.’正常成员(变量)访问
&、*
&p:取地址
a&0x1:位运算
a*10:乘法
*p: 解引用
int* p:定义int*的指针变量p(定义指向int类型的数据的指针p)
指针与数组 p26-p43
另一个笔记记有就不再赘述(可参考),只记不同之处或者指针扩展:
C语言之指针的详解与应用_qq_42241352的博客-优快云博客
指针
指针概述
内存类型资源地址、门门牌号的代名词
指针
指针变量:存放指针这个概念的盒子
int a;
*p;
C语言编译器对指针这个特殊的概念,有2个疑问?
1、分配一个盒子,盒子要多大?
在32bit系统中,指针就4个字节
2、盒子里存放的地址所指向内存的读取方法(int、char...)是什么?
*&
指针指向内存空间,一定要保证合法性
用char类型指针,可以读取int数据类型一个字节的内容。上图也就是小端存储的0x78
指针+修饰符
const(只读):
参考C语言常见问题与笔记_qq_42241352的博客-优快云博客
目录9.
volatile:
防止优化指向内存地址
常用:
volatile char *p ;
typedef:
别名
char *name_ t :
name_ t是一个指针,指向了一个char类型的内存
typedef char *name_t; name_ t是一个指针类型的名称,指向了一个char类型的内存
name t abc;
指针 + 运算符
++、--、+、-
int a= 100;
a+1
int*p=xx [0x12]
p+1 [0x12 + 1*(sizef*p)]
指针的加法、减法运算,实际上加的是一个单位,单位的大小可以使用sizeof(p[0])
int *p p+1
char *p p+1
p++ p-- :更新地址
[]
变量名[n]
n:D标签
地址内容的标签访问方式
取出标签里的内存值
指针+-1
指针越界用法:
出栈是先出低地址的局部变量,然后再出高地址的局部变量。栈是一种后进先出(Last-In-First-Out,LIFO)的数据结构,因此在函数调用结束时,会先释放最后声明的局部变量,然后逐步释放先前声明的局部变量。这与局部变量的分配顺序相反。
将局部变量的地址空间分配想象成一个倒立的水杯(栈),然后套进声明的变量中,如上:const int a 先进去在栈顶(高地址),int b 后进在栈底(低地址)。出栈就是先进后出,底部的‘b’先出。
uint8_t aa = 0x66;
uint8_t bb = 0x77;
int main(void)
{
}
全局变量就是正常地址空间分配,其中aa地址是:2000 0000 bb地址:2000 0001
在C语言中,将一个变量声明为 `const` 意味着该变量的值不能被修改。然而,C语言中的 `const` 并不是绝对的,因为编译器可能会在一些情况下允许绕过 `const` 限制,例如使用指针操作或位运算。这并不是一种良好的编程实践,因为它可能会导致不可预测的行为。
关于您提到的局部变量和全局变量在使用 `const` 修饰后的行为差异,这可能与编译器的优化和存储分配策略有关。
在局部变量的情况下,如果您在 `main` 函数中将一个局部变量 `int a` 用 `const` 修饰,然后尝试使用位运算修改它,这是违反了 `const` 的规则。编译器应该会发出警告,但是具体行为可能因编译器而异。
在全局变量的情况下,使用 `const` 修饰的全局变量可能会被优化到常量区。这是因为全局变量的值在程序运行时不会改变,因此编译器可能会将其放入只读的常量区,以便在运行时避免重复存储。这种优化可能会导致在尝试修改这些变量时出现错误或未定义的行为。
总之,无论是局部变量还是全局变量,都应该遵守 `const` 的规则,避免修改被声明为 `const` 的变量。避免使用不良的编程实践,以确保程序的可预测性和稳定性。
指针逻辑运算符操作
==、!=
1、跟一个特殊值进行比较
0x0 :地址的无效值,结束标志
if(p== 0x0) 类似NULL
2、指针必须是同类型的比较才有意义
char *
int *
多级指针
二级指针:
多级指针
int **p;
存放地址的地址空间
char **p:
如下:二级指针存放不同关系空间的连续关系(线性关系)。
(多级指针就是一串连续的空间,保存了其他空间的地址)
char *p = "hello world!!!\n"
p[0] == "hello world" p[1] == "!!!\n" "hello world"与"!!!\n"并不连续
p[0] p[1] ... p[n]
p[m] == NULL--->结束了
多级指针枚举案例
二级指针就是那个圈
二维指针就是指针的线性表
或者:
数组
数组的定义
定义一个空间(数据结构):
1、大小
2、读取内存方式(int、char...)
数据类型数组名[m]m的作用域是在申请的时候
数组名是一个常量符号,一定不要放到 = 的左边
越界
数组空间的初始化1、2、3
1
空间的赋值
按照标签逐一处理
int a[10];
[0-9]
a[0]=xx;
a[1]= yy;
程序员这样赋值,工作量比较大,能不能让编译器进行一些自动处理,帮助程序员写如上的程序---》空间定义时,就告知编译器的初始化情况,空间的第一次赋值,初始化操作
int a[10]=空间;
C语言本身,CPU内部本身一般不支持空间和空间的拷贝
int a[10]= {10.20.30};
====> a[0]= 10: a[1]= 20: a[2]= 30 a[3]=0;
=================================================
数组空间的初始化 和 变量的初始化本质不同,尤其在嵌入式的裸机开发中,空间的初始化往往需要库函数的辅助
char
char buf[10]= {'a','b','c};
buf当成普通内存来看,没有问题
buf当成一个字符串来看,最后加上一个'\0' 0
字符串的重要属性,结尾一定有个'\0'
char buf[10]= {"abc"};
char buf[10] = "abc"; buf有空间并且将‘abc’拷贝进去,可以改变buf的值但是不能改‘abc’;常量拷贝到变量
不同于:
char *p= "abc"; 将p的指向‘abc’’;指针指向常量区 完整:const char *p= "abc";
buf[2] = 'e' 可行,因为是该变量的值
p[2] = 'e' 不行,因为常量区不允许修改
一段内存(内存地址)
2
char buf[] = "abcd"; //5字节
char buf[10]= "abc";
buf = "hello world"; 错误写法,将数组名(常量标签)修改
第二次内存初始化,赋值?
逐一处理
buf[0l = 'h' buf[1] = 'e' buf[n]= 'd', buf[n+1]= 0;
strcpy, strncpy
一块空间,当成字符空间,提供了一套字符拷贝函数
字符拷贝函数的原则:
内存空间和内存空间的逐一赋值的功能的一个封装体
一旦空间中出现了0这个特殊值,函数就即将结束。
strcpy();
char buf[10]= "abc";
buf = "hello world"; 错误写法
strcpy(buf,"hello world"); 正确写法,但是‘strcpy’容易内存泄露。
例如:原来我们只想拷贝‘hello world’,十个字节,但是多写了几个,‘strcpy’的继续拷贝可能会覆盖一些重要数据。如下:
3
非字符串空间
字符空间
ASCII码编码来解码的空间,给人看
%s 人看:abc 内存中存放(特殊的char类型): 'a' 'b' 'c'
\0作为结束标志
非字符空间
非字符空间
数据采集:0x00- 0xff
8bit
开辟一个存储这些数据盒子
char buf[10]; ---> string
unsigned char buf[101]----> data
buf= "abc";
unsigned char *p = sensor base;
只管逐一拷贝,结束在哪里?只能定义个数
拷贝三要素:
1、src 源地址
2、dest 目标地址
3、个数
memcpy
int buf[10];
int sensor_ buf[100];
memcpy(buf, sensor_ buf, 10*sizeof(int)); 注意:n指的是字节个数
unsigned char buf1[10];
unsigned char sensor_ _buf[100];// 0000 00 23 45 78
strncpy(buf,sensor_ buf,10) 无法采集到数据,因为遇到0就终止了
memcpy(buf,sensor_ buf, 10*sizeof(unsigned char));
先确定操作的对象是什么:有无符号?
再进行相应操作
指针与数组的概述(指针数组)
二级指针可以理解为指针数组
指针数组
int a[100]
char * a[100];
sizeof(a)= 100 * 4; 一个标签(常量)
int b;
sizeof(b) 一个变量
char **a;
数组名的指针保存(多维指针)
二维数组和二维指针没有任何关系
二维指针是描述地址的线性关系的表示形式(连续); 地址存储器(地址中地址存储器)
定义一个指针,指向int a[10]的首地址
定义一个指针,指向int b[5][6]的首地址 错误写法:int *p[5];
int (*p)[5]:加括号后,优先读取(*p),就是一个地址。然后就剩下int [6];
类比int b[5][6]或者
c语言编译器就是字符串解释器: int *p[5];从右到左读,[5] 数组,p数组名,存储‘ * ’指针,读取方式int
例如:
int b[2][3][4];
int (*p)[3][4];
结构体
结构体字节对齐(常问)p44
解释: 一般32位系统4字节读取效率比较高(总线啥的全部用上)。例如:我们定义一个‘char’和‘int’,按照正常理解结构体应该是大小‘1+4’字节,但是这样不方便读取(框图--中),因此字节‘4+4’对齐保证读取效率(框图--右)。用空间换效率。
与内部最大类型对齐最终结构体的大小一定是4的倍数。
结构体里成员变量的顺序不一致,也会影响到他的大小。例如:
分析:
buf: char:要占用4个字节,空出3个 short:占用2个,正好在字节低两位占用
因此,4+4=8
buf1: char:用4,空3,不够int int:占4个 short:用4,占2
因此,4+4+4=12
内存分布思想概述 p45-p48
内存分布图
c的核心就是如何操作内存!
内存的属性:
1、大小
2、在哪里
int a;默认方式
预处理---》编译---》汇编---》链接
*.。 build(可执行程序)
栈:系统自动分配的空间,只要不特殊声明,就定义在栈区,函数的区域也在栈上。栈是向下增长的。(const 局部变量在栈里)
堆:使用动态内存分配的方式可以申请堆空间,用完要手动释放。
全局区:全局变量、静态变量(static)
常量区:代码中的数字,字符等常量,例如’a’,—1.2等
代码区:存放可执行代码,避免频繁的读硬盘。
三者的关系
在计算机内存中,有两个主要的部分:RAM(随机存取内存)和ROM(只读存储器)。
RAM(随机存取内存)是计算机用于存储正在运行程序和数据的地方。它包括了:
- **栈区:** 用于存储局部变量和函数调用的上下文信息。这些数据在函数调用时被分配,函数结束时自动释放。
- **堆区:** 用于动态分配内存,例如通过 `malloc()` 或 `new` 分配的内存。需要手动释放。
- **全局区(静态存储区):** 存储全局变量、静态变量和常量,这些在整个程序运行期间都是存在的。
ROM(只读存储器)存储了程序的只读部分,包括代码和常量数据。这些数据在程序运行时不能被修改。
.data、.bss 和 .text 则是编译器和链接器在可执行文件中定义的节(sections),与上述内存区域有关:
- **.data:** 存储已初始化的全局变量和静态变量。
- **.bss:** 存储未初始化的全局变量和静态变量,在程序加载时会清零。
- **.text:** 存储程序的机器代码,即可执行的指令。
总之,RAM 是实际的内存,用于存储正在运行的程序和数据,而 ROM 存储了程序的只读部分。而 .data、.bss 和 .text 则是可执行文件中不同节的命名,与程序的内存分配有关。
对比
内核空间 应用程序不许访问
============================== 3G
栈空间 局部变量 RW
==============================
堆空间 malloc
==============================
全局的数据空间(初始化的,未初始化的) static(访问是局部的,但是在全局段中) RW
只读数据段 "hello world"字符串常量(数字常量) R
代码段 code R (.text)
==============================
0x0 :
" ":
双引号表示,开辟一段常量空间。
0x0804
一般是代码段地址
内存分布之 栈空间
运行时,函数内部使用的变量,函数一且返回, 就释放,生存周期是函数内
内存分布之 堆空间
运行时,自我管理分配与释放的空间,生存周期又程序员决定
malloc(),一旦成功,返回分配好的地址给我们,只需要接受,对于这个新地址的读法,由程序员灵活把握,输入参数指定分配的大小,单位就是B(字节)。
char *p;
p = (char *)malloc(1);
int a[5]; malloc(5*sizeof(int));
因为是申请,不一定成功,因此得写:
if (p == NULL)
{
error...
}
注意:内存泄露
void fun()
{
char *p;
p = (char *)malloc(100);
return ;
}
函数结束,p消失,但是申请的(char *)malloc(100); 并没有消失,造成了内存泄露
free(p);
释放:
free(p)
内存分布之 只读空间
静态空间,整个程序结束时释放内存,生存周期最长
C语言函数的使用 p49
概述
一堆代码的集合,用一个标签(函数名)去描述它
复用化
函数名 --- 标签(地址)
函数 != 数组,函数具备3要素:
int *p; *p说明是一个指针变量,然后‘int’内存读取数据类型
int a[100]; 100个‘int’数据类型的空间
1、函数名 (地址)
2、输入参数
3、返回值
在定义函数时,必须将3要素告知编译器。
int fun(int,int,char)
{XXX}
如何用指针保存函数(用指针调用函数)?
定义一个数组 char a[10];
定义一个指针 char *p:
定义一个10字节的char数组 char(*p)[10] ; char a[][10];
定义一个指针数组(二维(级)指针) char *a[10]; 数组10个*a,读取内存方式char
定义一个二维数组 char(*p)[5]; char a[4][5];
int fun(int,int,char)
{XXX}
指针保存函数: int (*p)(int, int, char);
如果这么写: int *p(int, int, char); p就被()修饰,当成一个函数p(),而不是一个指针
定义函数,调用函数
int fun (int a, char b)
{
XXXX
}
int main()
{
fun(10,a) ;
}
数组的定义与调用
char buf [100] ;
buf [10] = ‘a’ ;
函数名举例 p50
函数名:
函数名的地址:
输入参数
函数形参与实参概念 p51
输入参数
承上启下的功能
调用:
函数名(要传递的数据) //实参
被调:
函数的具体实现
函数的返回值函数名(接收的数据) //形参
{
xxx xxx
}
实参传递给形参
传递的形式:
拷贝
函数实参形参拷贝举例 p52
看视频案例即可,C语言函数调用传参,本质就是拷贝
函数值传递概述 p53
void fun(int a)
{
a= xX;
a= sha md5 yy
}
int main()
}
int a = 20;
fun(a);
printf a ==?
}
上层调用者(main) 保护自己空间值不被修改
函数地址传递概述 p54
上层,调用者(main)让下层(子函数)修改自己空间值的方式
类似结构体(数组)这样的连续空间,函数与函数之间调用关系 -->连续空间的传递
int a= 10;
fun(&a);
a ===? 10
int a;
scanf("%d" ,a);不改变a的实际值 scanf("%d",&a);改变a的实际值
连续空间传递概述
1、数组
数组名(标签(地址))
实参:
int abc[10];
fun(abc)
形参:
void fun(int *p)
void fun(int p[10]) 这种写法其实也是把p当成一个地址(写成p[10]、p[] 都一样)
2、结构体
结构体变量
struct abc{int a;int b;int c;}; 定义一个新类型
struct abc buf; 定义一个变量,内存读取方式是这个新类型
实参:
fun(buf); fun(&buf)
形参:
void fun(struct abc a1) void fun(struct abc *a2)
建议:
使用地址传递,防止逐一拷贝造成的资源浪费
连续空间只读性 p56
void fun(char *p);
const char*p: 只读空间,为了空间看看
char *p : 该空间可能修改
void fun(char *p); 该空间可能修改
void fun(const char *p)
{
// p[n]='1"; 只读空间,不可修改
}
例如:
strcpy:其声明如下
目标‘dest’可以修改 源‘src’只读
sprintf():声明如下
char buf[100];
sprintf(buf,"hello world");
int a= 12;
char buf[10];
printf("%d",a);
sprintf(buf,"%d",a); 更便捷将‘a’打印,并且将‘buf’初始化为‘12’
字符空间操作介绍 p57
地址传递作用
1、修改 int* char* ...
2、空间传递
2.1子函数看看空间里的情况 const * ; void fun(const int *p);
2.2子函数反向修改上层空间里的内容 char *;字符空间(hello) void * 非字符空间(0011)
内存(空间)修改统一用 void * ; 单个值修改用int值用 int * 、short * 、long *
空间:
空间首地址、结束标志。
结束标志:
字符空间:内存里面存放了0x00 (1B)
非字符空间:0x00不能当成结束标志 数据采集0x00可能只是低电平
字符空间
错误访问:
void fun(char *p)
{
p[1000]='1;
char buf;
buf = p[100];
}
正确访问(字符串空间的操作,从头找到尾):
void fun(char *p)
{
int i = 0;
while(p[i])
{
p[i]操作 p[i]=x;赋值 a = p[i];获取 +-*/运算
i++;
}
}
字符空间操作举例 p58
实现strlen:
int strlen(const char *p)
{
int i = 0;
/*错误处理,判读输入参数是否合法*/
if(p == NULL)
{
// return ...
}
/*内存处理,从头到尾逐一处理*/
while(p[i])
{
//具体实现+++++++++++
i++;
}
}
实现strcpy:
void strcpy(char *dest,const char *src);
" " --> 初始化const char *
char buf[10] --> 初始化char *
非字符空间 p59
unsigned char *p; 只代表‘1’与‘0’
结束标志:数量(B(字节))
int *p unsigned char *p
short*p struct abc *p
void *
void * :数据空间(非字符空间)的标识符
错误写法:(结束标志没指出)
void fun(unsigned char *p)
{
p[100]= XX
p[1000] = yy;
}
正确写法(非字符空间操作模板):
void fun(unsigned char *p, int len)
{
int i;
for(i=0;i < len; i++)
{
p[i]=; a= p[i] //+ - * /
}
}
memcpy:声明
都是‘ void * ’ ===== 内存(非零字符空间),且后尾跟上结束标志 n
空间最重要的两要素: 空间的标签(首地址 ) 结束标志位
非字符空间操作举例 p60
举例:
void * :非零字符空间
char buf[100]; 定义字符空间
recv(n,buf,m); 传入字符空间 假如数据是 00 00 h e l l o
printf("%s", buf); 遇到0无法输出00 00 h e l l o
void * 如何用
int fun(void *buf,int len)
{
unsigned char *tmp = (unsigned char *)buf; 转化为具体类型
tmp[i] , i++ , len
}
函数地址传递总结 p61
1、修改 int * 、short * 、long *
2、空间传递
2.1子函数看看空间里的情况 const * ; void fun(const int *p);
2.2子函数反向修改上层空间里的内容 char *;字符空间(hello) void * 非字符空间(0011)
函数声明:
1、单个值修改用int值用 int * 、short * 、long *
void fun(int *p);
int a = 10;
fun(&a);
a != 10
2、内存(空间)修改统一用 void *
void fun(void *p);
int buf[10];
fun(buf);
函数返回值
基本语法 p62
基本语法:
返回类型 函数名称 (输入列表)
{
return
}
调用者
a = fun(); 定义一个变量接收
被调者:
int fun()
{
return num;
}
返回值思路:拷贝
返回类型
基本数据
指针类型(空间)
不能返回:数组
返回基本数据类型 p63
int *fun1(void);
int main()
{
int *p;
p = fun1();
}
或者写成
void fun2(int **p) 更新地址空间
int main()
{
int *p;
fun2(&p); 类比 int *p fun2(p)值传递; 得 int *p fun2(&p);
}
返回连续空间类型 p64
指针作为空间返回的唯一数据类型
地址 关键:1.指向谁 2.内存读取的方式
空间(内存) 关键:1.首地址(标签) 2.结束标志
int *fun(); 指针函数,返回一个地址
地址:指向的合法性
例如:
hello world " "字符串常量 保存了,但是buf是局部变量(指针变量),return的时候已经没有了
指向空间不合法!
作为函数的设计者,必须保证函数返回的地址所指向的空间是合法。[不是局部变量]
改成:
可以用static,但不能用const ,const修饰的局部变量依旧存放在栈区
使用者:
int *fun();
intl*p = fun();
内部实现概述 p65
基本数据类型 fun(void)
{
基本数据类型 ret;
XXXx
ret = xxxx;
return ret;
}
1.静态区:
返回地址:static
2.只读区
字符串常量
不太常用
3.堆区
malloc free
解读:
FILE *fopen (const char*path, const char*mode) ;
char*:参数字符串空间
const:只读
FILE *fopen:返回值是连续空间,内存读取方式是FILE
常见面试题 p66-p69
具体参考想成为嵌入式程序员应知道的0x10个基本问题 - zhengmeifu - 博客园 (cnblogs.com)
宏定义
1 . 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
我在这想看到几件事情:
1) #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)
2)懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
3) 意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。
4) 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。
数据声明
5. 用变量a给出下面的定义
a) 一个整型数
a) int a;
b)一个指向整型数的指针
b) int *a;
c)一个指向指针的的指针,它指向的指针是指向一个整型数
c) int **a;
d)一个有10个整型数的数组
d) int a[10];
e) 一个有10个指针的数组,该指针是指向一个整型数的。
e) int * a[10];
先看a[10],[]修饰a,长度为10的数组;再看 * 修饰 a[10],10个指针数组;int 修饰 * 读取方式整型
f) 一个指向有10个整型数数组的指针
f) int [10] *a; ====> int (*a)[10];
注意和上面一个区别开,()先确定优先级,a是一个指针,而不是一个数组
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数
g) int fun(int) *a ======> int (*a)(int); 不能写成 int * a(int);
右边优先级最高,a(int)会被当成一个函数
h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数
h) int (*a[10])(int);
先看a[10],a是一个10个数的数组,* a[10]读取方式是指针;指针指向函数先(* a[10])防止混乱,再对比上一题 g) 即可
读函数或者写函数核心:
变量名为中间节点,右边优先级最高,看到 [ ] 升级为数组(连续空间),看到 () 升级为函数名;右边没了看左边,看到 * 升级为指针,没 * 看什么读取方式(类型),如果有 * 需要括号“()”,将语义和 * 括号“()”起来再看右左
修饰符
Static
6. 关键字static的作用是什么?
这个简单的问题很少有人能回答完全。在C语言中,关键字static有三个明显的作用:
1、修饰局部变量
局部变量:在栈空间,生存周期短。
函数体内被调用过程中维持其不变。存放到静态空间,周期长。
2、修饰全局变量
在模块内(但在函数体外),被模块内所用函数访问,但不能被模块外其它函数访问。
防止重命名,限制变量名只在本文件使用。
3、修饰全局函数
只可被这一模块内的其它函数调用。
防止重命名,限制函数名只在本文件使用。
1)在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
2)在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
3)在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处和重要性。
Const
7.关键字const有什么含意?
C:只读,建议性不具备强制性 ! =常量
const int a = 100; 可以用溢出修改
c++:常量
我只要一听到被面试者说:"const意味着常数",我就知道我正在和一个业余者打交道。去年Dan
Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems
Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.如果你从没有读到那篇文章,只要能说出const意味着"只读"就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks的文章吧。)
如果应试者能正确回答这个问题,我将问他一个附加的问题:下面的声明都是什么意思?
const int a;
int const a; 正常变量名,其本身就是读取方式(存放的值);不像指针
const int *a;
int * const a;
int const * a const;
/******/
前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 ,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:
1) 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)
2) 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
3) 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。
Volatile
8. 关键字volatile有什么含意?并给出三个不同的例子。
防止c语言编译器的优化。
他修饰的变量,该变量的修改可能通过第三方来修改
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1) 并行设备的硬件寄存器(如:状态寄存器)
2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3) 多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。 假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
1)一个参数既可以是const还可以是volatile吗?解释为什么。
2); 一个指针可以是volatile 吗?解释为什么。
3); 下面的函数有什么错误:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
下面是答案:
1)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2); 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3) 这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
位操作
9. 嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。
{
a |= (0x1 << 3);
}
{
a &= ~(0x1 << 3);
}
自己的错误写法(用了逻辑操作,并且是第几位,不是bit):
{
a = a | (0x4)
}
{
a = a & ~(0x4)
}
这个写法使用了逻辑操作来设置和清除特定的位。具体来说:
1. `a | (0x4)` 表示将变量 `a` 的第3位设置为1,其他位保持不变。这是通过将 `0x4` 的二进制表示(`0000 0100`)与变量 `a` 进行按位或操作实现的。这会将 `a` 的第3位变为1,其他位保持不变。
2. `a & ~(0x4)` 表示将变量 `a` 的第3位清除(设置为0),其他位保持不变。这是通过将 `0x4` 的二进制表示取反(`1111 1011`),然后与变量 `a` 进行按位与操作实现的。这会将 `a` 的第3位变为0,其他位保持不变。
这些逻辑操作允许您精确地操作变量的特定位,同时保持其他位不变。
访问固定的内存位置
10. 嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。
在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换(typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同而不同。典型的类似代码如下:
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;