嵌入式开发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_:代表文件名
使用以上预定义宏可以在调试过程中方便查看问题出现的位置
#include <stdio.h> int fun() { printf("函数名%s,文件名%s,行号%d\n",__FUNCTION__,__FILE__,__LINE__); return 0; } int main() { fun(); return 0; }执行结果:

1.4.2 条件预处理的应用
- 区分
- 调试版本
- 发行版本
调试版本便于开发人员开发、调试及二次开发,而在发行版本中通过编译预处理阶段隐藏调试的代码,保护信息,不执行调试代码。
#include <stdio.h>
#define ABC
int main()
{
//当定义了宏ABC时,所有条件编译#ifdef ABC 代码 #endif中的代码才会执行
//若没有定义ABC,则条件编译中代码不执行
#ifdef ABC
printf("======%s======\n",__FILE__);
#endif
printf("helloWorld!\n");
return 0;
}
-
在代码中不定义宏,通过
gcc -D宏名控制版本切换的开关如:去掉代码中宏定义
define ABC,再使用gcc -D宏名
1.4.3 宏展开下#、##的使用
#表示字符串化 #define ABC(x) #x 作用是将x转化为字符串"x"
#include <stdio.h>
#define ABC(x) #x//字符串化
int main()
{
printf(ABC(helloWorld!\n));
return 0;
}
//执行的结果为hellloWorld!
//ABC(helloWorld!\n)作用等同于"helloWorld!\n"
##表示连接符号 #define ABC(x) ABC##x
#include <stdio.h>
#define ABC(x) myday##x//连接符号
int main()
{
int myday1=10;
int myday2=20;
printf("the day is %d\n",ABC(1));
return 0;
}
//执行结果为the day is 10
//ABC(1)作用相当于连接字符串myday+1
2. C语言常用关键字及运算符操作
2.1 关键字
- C语言中总共有32个特殊意义的关键字
2.1.1 杂项
- sizeof:编译器给我们查看内存空间容量的一个工具,在任何编译环境下都可以使用
#include <stdio.h>
int main()
{
int a;
printf("the a is %lu\n",sizeof a);//%lu(long unsigned)无符号长整数或无符号长浮点数
return 0;
}
//结果4字节
- 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中的顺序有要求
//定义结构体mystruct struct mystruct{ unsigned int a; unsigned int b; }; //使用结构体 struct mystruct abc; -
union共用体
允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值
不同的数据类型共用起始地址
//声明共用体 union myunion{ char a; int b; }; //使用共用体 union myunion abc; -
enum枚举类型
enum可以理解为被命名的整型常数的集合,可以用来代替宏定义,宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。enum的好处在于更好描述一组数据,语义更加清晰。
使用格式:
enum typeName{valueName1,valueName2,valueName3......};typeName是枚举类型的名字,valueName是枚举成员,注意其值只能为常量,不能为变量。
在没有显示说明的情况下,枚举常量(也就是花括号中的常量名)默认第一个枚举常量的值为0,往后每个枚举常量依次递增1。
如:
//使用宏定义常量 #define MON 0; #define TUE 1; #define WED 2;#include <stdio.h> //转换为enum定义 enum week{ Monday=0,Tuesday,Wendesday }; int main() { printf("the %d\n",Monday); return 0; }; -
typedef
为其他数据类型起别名,更好理解变量的使用场景
如:
int a;//表示a是int类型的变量
typedef int a;//表示a是一个int类型的外号
//使用
//int a=100;
typedef int len_t;//起别名时规范用法为xxx_t
len_t a=100;//更容易理解a的物理意义为表示len长度
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修饰的变量不起作 用。如:
#include <stdio.h>
int main()
{
register int a;
a=0x10;
printf("the address of a is %d\n",&a);
return 0;
}
运行结果:

- static:静态
static的3种应用场景
//1)、修饰函数内部的变量
int fun()
{
static int a;
};
//2)、修饰函数外部的变量
static int a;
int fun(){};
//3)、修饰函数
static int fun(){};
- 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=====>相当于\(m*2\),如00100 左移一位变为 01000
m<<n=====>相当于\(m*2^n\),如int a=b*32 可以变为b<<5
对于负数,左移仍然是每移动一位数值扩大两倍
如\(-1*2=-2\),也可以变为-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
#include <stdio.h>
int main()
{
int i=1;
//int a=10;//二进制数为01010 每次右移高位补0 经过4次位移10/2^4=0,循环即停止
int a=-10;//二进制数为11010 每次右移高位补1 循环不能停止
while(a)
{
a=a>>1;
i++;
printf("循环了%d次\n",i);
}
}
- 与或运算
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))
- ^异或操作 相同为假,相异为真
工程上异或使用较少,主要用于实现某些算法
^异或的使用,置换两个变量
int fun()
{
int a=20;//a=0001 0100
int b=30;//b=0001 1110
a = a^b;//a=0000 1010
b = a^b;//b=0001 0100
a = a^b;//a=0001 1110
}
- ~按位取反
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这个地址是否可用。
-
-
使用指针读取浮点数
#include <stdio.h>
int main()
{
float a=1.2;
//定义指针读取方式为float,这时候读出来的为1.2
float *p;
p=&a;
printf("the a is %f\n",*p);//需要以浮点数形式输出
}
//the a is 1.200000
#include <stdio.h>
int main()
{
float a=1.2;
//定义指针读取方式为int,这时候读出来的为1.2在内存中的存储方式
float *p;
p=&a;
printf("the a is %x\n",*p);//以十六进制输出
}
//the a is 3f99999a
#include <stdio.h>
int main()
{
float a=1.2;
//定义指针读取方式为char,这时候读出来的为1.2在内存中的存储的第一个字节
unsigned char *p;//需要定义为unsigned char
p=&a;
printf("the a is %x\n",*p);//以十六进制输出
}
//the a is 9a
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
//例子1:字符串常量不可以被修改
#include <stdio.h>
int main()
{
char *p="hello world!\n";//默认隐含了const char *p,即为字符串常量,字符的数据不能修改
*p='a';//尝试修改字符串常量的第一个字符
printf("the one is %s\n",p);//注意这里打印的是整个字符
}
//结果
//段错误 (核心已转储)
//例子2:在数组中字符串为变量可以被修改
#include <stdio.h>
int main()
{
char buf[]={"hello world!\n"};//字符串为变量,可以被修改
char *p=buf;
*p='a';
printf("the one is %s\n",p);
}
//执行结果
//the one is aello world!
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])
如假设
int a=100;
假设&a=0x12;
让指针p指向a,int *p=&a;
执行p+1,此时相当于[0x12+sizeof(*p)],即0x12加了4
p+:未更新p
p++:更新了p,即p=p+1
- 指针与[ ]
变量名[n]:其中n被称为标签ID,这是一种地址内容的标签访问方式,并获得标签里的内存值
即p[n]相当于p+n,并取出p+n位置的值
//指针的+和[]例子
#include <stdio.h>
int main()
{
int a=0x12345678;
int b=0x99991199;//注意后定义的变量在内存中存放在低位置
int *p1=&b;
char *p2=(char *)&b;
printf("the p1+1 is %x,%x,%x\n",*(p1+1),p1[1],*p1+1);//*(p1+1)和p1[1]表示p1+sizeof(int) 即4字节 值为a;*p1+1表示b+1
printf("the p2+1 is %x\n",p2[1]);//p2[1]只增加了一个字节,即b中的11
}
//运行结果
//the p1+1 is 12345678,12345678,9999119a
//the p2+1 is 11
- 指针越界访问
内存空间被切成不同标签的空间,通过指针可以访问不同的标签值,如上例中p1[10],访问不是 自身维护的数据时,即发生越界。因此在使用指针时,还要定义属性:访问范围的大小。 访问超 出范围,将发生内存泄露 (段错误)
内存越界的实例:
const修饰的变量,在C中并不是不能变化,在C中const只是建议性字符,编译器一般不会进行 改变,但通过指针越界可以修改const修饰的变量
#include <stdio.h>
int main()
{
const int a=0x12345678;
int b=0x11223344;
//直接修改 不被允许
//a=100;//错误error: assignment of read-only variable ‘a’
//通过地址修改 允许
//int *p=&a;
//a=0x100;//警告warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
//通过指针越界
int *p=&b;
p[1]=0x100;
printf("the a is %x\n",a);
}
- 指针与逻辑运算符 ==、!=
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
# include <stdio.h>
int main(int argc,char **argv)
{
int i;
for(i=0;i<argc;i++){
printf("the argv[%d] is %s\n",i,argv[i]);//argv[i]是参数argv的地址,使用%s输出argv
}
return 0;
}
运行结果:

不使用输入参数个数argc,利用地址p[m]==NULL,同样可以输出argv
# include <stdio.h>
int main(int argc,char **argv)
{
int i=0;
while(argv[i]!=NULL){
printf("the argv[%d] is %s\n",i,argv[i]);
i++;
}
return 0;
}
运行结果:

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"进行二次赋值,而应该使用以下方法进行逐一赋值
buf[0]='H';
buf[1]='e';
.....
buf[n+1]=0;
2)使用函数strcpy,strncpy进行字符串拷贝
字符串拷贝函数的原则:
内存空间和内存空间的逐一赋值的功能的一个封装体
一旦空间中出现了0 ('\0') 这个特殊值,函数就结束工作
//使用strcpy()将hello world拷贝到buf中
char buf[10]="abc";
strpoy(buf,"hello world");//实现了拷贝,拷贝过去的字符带有结尾符0
但是在工程中一般不使用strcpy(),该函数容易造成内存的泄露
使用更安全的strncpy(),该函数需要指定拷贝的字符个数
- 非字符串空间
1)字符空间:可以使用ASCII码来编码和解码的空间,用'\0'作为结束标志
非字符空间:如采集到的数据,开辟一个存储空间存储这些数据
char buf[];----->string
unsigned char buf[];----->data
2)非字符串空间拷贝使用memcpy()函数,必须指定拷贝的个数
//注意memcpy中的拷贝个数指的是字节数
int buf[10];
int sensor_buf[100];
memcpy(buf,sensor_buf,10*sizeof(int));//使用10*sizeof(int),为了规范在unsigned char buf[]拷贝时,也尽量使用10*sizeof(unsigned char)
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个字节。
#include <stdio.h>
struct abc{
char a;//字节对齐 补一个字节
short b;//字节对齐 两个字节对齐
int c;
};
int main()
{
struct abc buf;
printf("the buf is %lu\n",sizeof(buf));
buf.a='a';
buf.b=2;
buf.c=100;
printf("the address of a is %p\n",&buf.a);
printf("the address of b is %p\n",&buf.b);
printf("the address of c is %p\n",&buf.c);
return 0;
}
//运行结果 char与short共用了4字节,由char补齐字节
//the buf is 8
//the address of a is 62ea7140
//the address of b is 62ea7142
//the address of c is 62ea7144
最终结构体的大小一定是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段)代码段和数据段 只读
代码段
//代码段的数据为只读
#include <stdio.h>
int main()
{
unsigned char*p;
p=(unsigned char*)main;//让p指向main标签,main标签位于代码段
printf("the p[0] is %x\n",p[0]);//读取代码段数据
printf("=================\n");
p[0]=0x12;//尝试修改代码段的值 会出现段错误
printf("the p[0] is %x\n",p[0]);
}
//运行结果
the p[0] is 55
=================
段错误 (核心已转储)
数据段
//数据段的数据为只读
#include <stdio.h>
int main()
{
char *p="hello world";
printf("the p is %s\n",p);
printf("the address of p is %p\n",p);
printf("the address of string is %p\n","hello world");
p[0]='a';//不能修改
printf("the p is %s\n",p);
}
//运行结果
the p is hello world
the address of p is 0x55b9dfec6754
the address of string is 0x55b9dfec6754//字符空间只有一份
段错误 (核心已转储)//尝试修改时即发生段错误
注意: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 未在fun()中定义静态局部变量a
//代码2 在fun()中定义静态局部变量a
#include <stdio.h>
int fun()
{
//static int a=100;
//return a++;
}
int main()
{
static int a;
a=0x10;
return 0;
}
代码1和代码2分别使用size查看内存变化 使用nm查看静态空间中数据 标签与地址

使用size查看段大小
-
全局的数据空间 可读可写
-
全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量。全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。 这两者的区别在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而 静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。 由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。
-
const修饰的局部变量仍然位于栈空间,通过指针可以修改数值;const修饰的全局变量才位于全局数据空间中
-
3.4.3 堆空间
-
段对比
- 静态空间,整个程序结束时释放内存,生命周期最长
- 栈空间,运行时函数内部使用变量,函数一旦返回就释放,生命周期是函数内
- 堆空间,运行时可以自由分配和释放的空间,生命周期由程序员决定
-
内存分配使用malloc()函数
malloc()函数一旦执行成功,将返回分配好的地址,我们只需要接受这个地址。对于这个新地址采用何种读取方式,有程序员灵活把握,如char型,int型。函数中需要指定分配空间的大小,单位为字节
char *p; p=(char *)malloc(100);//分配了100字节的内存,内存操作方式为char注意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
//例子:使用函数指针 指向printf的输出功能
#include <stdio.h>
int main()
{
int (*myshow)(const char *,...);//定义函数指针
printf("hello world!\n");
myshow=printf;//函数指针指向printf标签地址 若是知道printf标签的地址,甚至在有些系统上可以直接指向该地址 如:(int(*)(const char *,...))0x7fa72f535e40;
myshow("===========\n");
return 0;
}
//运行结果
hello world!
===========
进阶:函数指针数组,把函数的地址存到一个数组中,那这个数组就叫函数指针数组。作用将若干个函数放在一起管理。定义方式如下
int (*p[10])(int,int);//先定义个函数指针数组
p[0]=fun1;
p[1]=fun2;
......
4.2 输入参数
4.2.1 函数的实参与形参
- 函数起承上启下的功能
调用者:
函数名(要传递的数据) //实参
调用者:
函数的具体实现
函数的返回值 函数名(接收的数据) //形参
{
xx xxx
}
实参 传递给 形参
- 传递的形式:拷贝
C语言中实参和形参是逐位拷贝的,不管实参和形参的长度和类型是否相同,都可以拷贝
//例子1 形参和实参长度不一致
#include <stdio.h>
void myswap(char buf)//形参8bit
{
printf("the buf is %x\n",buf);
}
int main()
{
int a=10;
myswap(0x1234);//实参16bit
return 0;
}
//运行结果
the buf is 34
//例子2 形参和实参的类型不一致
#include <stdio.h>
void myswap(int buf)//形参是int型
{
printf("the buf is %x\n",buf);
}
int main()
{
char *p="helllo world";
printf("the p is %x\n",p);
myswap(p);//实参是一个指针地址
return 0;
}
//运行结果
the p is cb7a3743
the buf is cb7a3743
4.2.2 值传递与地址传递
- 值传递
上层调用者 保护自己空间值不被修改的能力
#include <stdio.h>
void swap(int a,int b)
{
int c;
c=a;
a=b;
b=c;
}
int main()
{
int a=20;
int b=10;
printf("the a is %d, the b is %d\n",a,b);
swap(a,b);
printf("after swap the a is %d, the b is %d\n",a,b);//值传递后不会改变原来的变量值
}
//运行结果
the a is 20, the b is 10
after swap the a is 20, the b is 10
- 地址传递
上层调用者 允许下层子函数 修改自己空间值的方式
连续空间的传递一般都使用地址传递(节约内存空间)
#include <stdio.h>
void swap(int *a,int *b)//用指针接收地址
{
int c;
c=*a;
*a=*b;
*b=c;
}
int main()
{
int a=20;
int b=10;
printf("the a is %d, the b is %d\n",a,b);
swap(&a,&b);//传递地址
printf("after swap the a is %d, the b is %d\n",a,b);//地址传递后会改变原来的变量值
}
//运行结果
the a is 20, the b is 10
after swap the a is 10, the b is 20
4.2.3 连续空间的传递
- 连续空间传递概述
- 数组
数组的 实参==>形参 只能使用地址传递 数组名==标签==地址
实参:
int abc[10];//长度为10的数组
fun(abc);//实参传递的为数组abc的地址
形参:
void fun(int *p){}//形参接收时也需要使用一个指针接收
或者使用
void fun(int p[10]){}//此时定义的int p[10]实质上也是一个指针,只是便于了解空间长度为10(不建议使用)
- 结构体(结构体变量)
结构体变量跟普通变量一样可以使用 值传递和地址传递
但是为了节约内存,一般不建议使用值传递
struct abc{int a;int b;int c};
struct abc buf;
值传递
实参: fun(buf);
形参: void fun(struct abc a1){}
地址传递
实参: fun(&buf);
形参: void fun(struct abc *a2){}
- 连续空间的只读性
- 在使用地址传递(尤其是连续空间只能使用地址传递)时,我们有时不希望改变原空间的地址,怎么实现?
在形参中使用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 *。
-
-
//为什么连续非数据空间要使用void *
//场景一
void fun(int *p);
int buf[10];//定义一个int型的连续空间
fun(buf);
//场景二
void fun(int *p);
int a=10;
fun(&a);
//可以看出定义为int *p既可以作为连续空间的形参,又可以作为单值的传递。
//为了更好的区分连续空间和单值,对于连续空间直接使用void *定义
-
对于空间地址要明确空间的首地址、结束标志
结束标志:
字符空间——内存里面存放了0x00(1B)
非字符空间不能使用0x00当成结束标志
-
对于字符空间操作的框架
void fun (char *p)
{
int i=0;
while(p[i]!=0){//字符空间结束标志 也可以直接写成while(p[i])
对p[i]的操作;
i++;//指针位移
}
}
- 函数strlen() 和strcpy()的实现
- 非字符空间
- 对于
int *p、unsigned char *p、short *p、struct abc *p类型的参数,一般都是非字符空间的参数传递
非字符空间的结束标志:需要在传递参数时,指定传递参数的数量(单位是B)
void*:数据空间的标识符,用在描述形参,可以避免函数实参类型的多种多样
总结:对于非字符空间,形参要是用void *,并且要给出数据的长度,对于接收到的形参数据在使用前要转为具体的数据类型
void fun(void*p,int len)//形参也可以使用struct sensor_data*p,但是推荐使用void*p描述形参可以统一参数传递
{
unsigned char *tmp=(unsigned char*)buf//在使用buf数据时一定先要将数据转为具体的数据类型
int i;
for(i=0;i<len;i++){
//对tmp[i]的操作
}
}
int main()
{
struct sensor_data buf;
fun(&buf,sizeof(buf)*1);//形参传递buf长度
}
4.3 返回值
- 函数返回值的基本语法
返回类型 函数名称(输入列表)
{
return
}
- 函数的返回值传递仍然是拷贝
#include <stdio.h>
int fun(void)
{
int a=0x100;
//return a;//既可以返回基本数据类型
int *p=&a;
return p;//也可以返回地址
}
int main()
{
int ret;
ret=fun();
printf("the ret is %x\n",ret);
return 0;
}
-
返回类型
基本数据
指针类型(空间)
- 返回基本数据类型
- 返回的数据类型除了int、char、short……等基本数据类型外,还能返回struct结构体的数据类型,但是由于返回的实质是拷贝,为了节约空间不建议使用struct返回,而是使用指针返回
- 返回连续空间类型
- 指针作为空间返回的唯一数据类型
- 设计函数时要考虑指向地址的合法性
必须保证函数返回的地址所指向的空间时合法的。比如说局部变量不合法,在函数结束后就会自动销毁,再返回地址就没用。数据段,堆中的数据均合法。
//不合法的例子
#include <stdio.h>
char *fun(void)
{
char buf[]="hello world!";//字符数组存在于栈中,函数结束即销毁
return buf;
}
int main()
{
char *p;
p=fun();
printf("the p is %s\n",p);
return 0;
}

- 函数返回的内部实现
- 对于基本数据类型返回 内部实现的框架如下
基本数据类型 fun(void)
{
基本数据类型 ret;
各种操作过程
ret=xxx;
return ret;
}
如
int fun()
{
int ret=0;
count++;
ret=xxx;
return ret;
}
-
地址返回 内部实现框架
-
函数返回的数据地址主要有三种类型:
- 只读区 如字符串,但是这样的返回是没有意义的,因为只读区的数据声明周期和作用域都是在编译后即存在,不需要使用函数
- 静态区 static修饰的局部变量 生命周期是到运行结束
静态局部变量的声明周期和作用域(192条消息) 静态变量,静态局部变量的生存周期_xiaoheibaqi的博客-CSDN博客_局部静态变量生命周期
#include <stdio.h> char *fun(void) { static char buf[]="hello world";//使用static修饰数组将其放在静态区 return buf; } int main() { char *p; p=fun(); printf("the p is %s\n",p); return 0; } //运行结果 the p is hello world- 堆区 注意使用malloc()开辟空间,使用free()释放空间
#include <stdio.h> #include <stdlib.h> #include <string.h> char *fun(void) { char *s=(char *)malloc(100);//使用malloc()函数在堆区开辟空间 strcpy(s,"hello world"); return s; } int main() { char *p; p=fun(); printf("the p is %s\n",p); free(p);//最后一定要释放空间 return 0; } //运行结果 the p is hello world
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
本文来自博客园,作者:{Ray963},转载请注明原文链接:{https://www.cnblogs.com/ray93/}

浙公网安备 33010602011771号