文章是我前几天读了朱有鹏,张先凤老师的《嵌入式Linux与物联网软件开发:C语言内核深度解析》写的,拜读之后,虽没有醍醐灌顶,至少解开了我之前的一些疑惑。
《嵌入式Linux与物联网软件开发:C语言内核深度解析》目录
以前学C语言,就是在IDE上编一下代码,编译器会有错误有警告提示,很少思考过变量、指针、结构体、函数之间,及所编代码和当前所运行系统的关系(系统内存有多大,运行速度怎样,怎样优化算法)。等我真正学了嵌入式,才开始思考上面的问题,才开始去了解软件与硬件的关系。
本文章是边复习边记笔记,跟书籍目录不一样,以后可能会补充及修改。
有纰漏请指出,转载请说明。
学习交流请发邮件 1280253714@qq.com
第五章 组合数据类型(结构体、共用体、枚举)
结构体概述
结构体属于聚合数据类型,结构体变量在被定义后,编译器在编译时会为所有成员分配空间
结构体变量名代表的是整个结构体变量,不像数组名代表地址
#include <stdio.h>
typedef struct {
char id[8];
char name[8];
int age;
}Student;
int main(void){
Student stu={"2019","ckj",18};
printf("%p\n",&stu);
printf("%p\n",&stu.id);
printf("%p\n",&stu.id[0]);
printf("%p\n",&stu.id[1]);
printf("%p\n",&stu.age);
return 0;
}
![](https://i-blog.csdnimg.cn/blog_migrate/fc6266bc06779c0326b698707a74646f.png)
向函数传递结构体
向函数传递结构体时,实际上是传递结构体成员的值,但是如果定义的结构体是多成员结构或者有数组的结构时,把整个结构体变量进行压栈,运行性能会恶化。解决办法是向函数传递结构体指针,压栈的仅仅是结构体变量的地址
#include <stdio.h>
typedef struct {
char id[8];
char name[8];
int age;
}Student;
/*
函数addAge1被调用时,在函数栈中开辟stu1的变量空间
可看做复制stu给stu1,实际上操作的是stu1与main函数的stu无关
*/
Student addAge1(Student stu1){
stu1.age++;
return stu1;
}
/*
指针stu2指向stu
即在addAge2中直接操作变量stu
*/
void addAge2(Student* stu2){
(stu2->age)++;
}
int main(void){
Student stu={"2019","ckj",18};
addAge1(stu);
printf("%d\n",stu.age); //18
addAge2(&stu);
printf("%d\n",stu.age); //19
printf("%d\n",addAge1(stu).age); //20 仅仅把返回值输出而已,实际上stu.age还是19
printf("%d\n",stu.age); //19
return 0;
}
结构体对齐访问
结构体元素的偏移量要考虑元素的对齐访问,结构体实际占用的字节数与所有成员占用的字节数的总和不一定相等。
32位系统的内存,每次按照4个字节对齐访问,效率是最高的。对齐访问牺牲了内存空间,换取速度性能。
//编译器默认4字节对齐
#include <stdio.h>
/*
#pragma pack(1) //手动1字节对齐
*/
typedef struct
{ //1字节对齐 4字节对齐
int a; // 4 4
char b; // 1 2
short c; // 2 2
}MyStruct;
int main(void){
MyStruct s1;
printf("%d \n",sizeof(s1)); //8
return 0;
}
共用体(同一个内存空间的多种解析方式)
union类型声明、变量定义和使用
#include <stdio.h>
/*
i、c、和f指向同一块内存,地址一样,只是对这块内存的不同解析方式
*/
typedef union
{
int i;
char c;
float f;
}MyUnion;
int main(void){
MyUnion u1;
u1.i=321;
printf("%d \n",u1.i); //321
printf("%c \n",u1.c); //A 'A'的ASCII码为65,321-256=65
printf("%f \n",u1.f); //0.000000
printf("%p \n",&u1.i); //012FFA1C
printf("%p \n",&u1.c); //012FFA1C
printf("%p \n",&u1.f); //012FFA1C
return 0;
}
用指针和强制类型转换可以替代共用体完成同样的功能,但是共用体更好理解
大小端模式
51单片机用大端,ARM大部分是小端
大端:高字节对应低地址
小端:高字节对应高地址
用union测试机器的大小端模式
#include <stdio.h>
typedef union
{
int i;
char c;
}MyUnion;
int is_little_endian1(MyUnion u2)
{
u2.i=1;
return u2.c;
}
int is_little_endian2(MyUnion* u2)
{
u2->i=1;
return u2->c;
}
int main(void){
MyUnion u1;
if(1==is_little_endian2(&u1)){
//if (1==is_little_endian1(u1)){
printf("小端模式 \n");
}else{
printf("大端模式 \n");
}
return 0;
}
#include <stdio.h>
typedef union
{
int i;
char c;
}MyUnion;
int is_little_endian1(MyUnion u2)
{
u2.i=0x12345678;
return u2.c;
}
int main(void){
MyUnion u1;
printf("%x \n",is_little_endian1(u1)); //78
return 0;
}
![](https://i-blog.csdnimg.cn/blog_migrate/d13633ef219edec1ef2a7e7f0c54df86.png)
用指针测试机器的大小端模式
#include <stdio.h>
int is_little_endian1(void)
{
int a=1;
char b=*(char *)&a;
return b;
}
int main(void){
if(1==is_little_endian1()){
printf("小端模式 \n");
}else{
printf("大端模式 \n");
}
return 0;
}
#include <stdio.h>
int main(void){
int a=0x12345678;
char b=*(char *)&a;
printf("%x \n",b); //78
return 0;
}
#include <stdio.h>
int is_little_endian1(char *p){
return *p;
}
int main(void){
int a=0x12345678;
printf("%x \n",is_little_endian1((char *)&a)); //78
return 0;
}
![](https://i-blog.csdnimg.cn/blog_migrate/40a0e5576a0133bf3598245b4b27ea01.png)
练习(写出三次打印输出的结果)
#include <stdio.h>
#include <stdlib.h>
void *MEN_ADDR;
int main(void){
int *pint_addr=NULL;
short *pshort_addr=NULL;
char *pchar_addr=NULL;
MEN_ADDR=(void *)malloc(sizeof(int));
pint_addr=(int *)MEN_ADDR;
pshort_addr=(short *)MEN_ADDR;
pchar_addr=(char *)MEN_ADDR;
*pint_addr=0x12345678;
printf("0x%x \t\t 0x%x \n",*pshort_addr,*pchar_addr);
pshort_addr++;
*pshort_addr=0x5555;
printf("0x%x \t 0x%x \n",*pint_addr,*pchar_addr);
pchar_addr++;
*pshort_addr=0xAA;
printf("0x%x \t 0x%x \n",*pint_addr,*pchar_addr);
free(MEN_ADDR);
return 0;
}
/*
0x5678 0x78
0x55555678 0x78
0xaa5678 0x56
*/
![](https://i-blog.csdnimg.cn/blog_migrate/a2d3854f486ea373b3f40e902577a5fe.png)
![](https://i-blog.csdnimg.cn/blog_migrate/917ad48f45c0cde3735d636ca43f3b9f.png)
![](https://i-blog.csdnimg.cn/blog_migrate/367f206a1cc71c1831fae62432f30589.png)
枚举enum
为什么需要枚举
枚举就是对0、1这些数字进行符号化编码,符号的意义显而易见,而数字需要查看文档或注释
枚举的规则
枚举是int类型的常量集,自动从0开始
![](https://i-blog.csdnimg.cn/blog_migrate/c47004c76584dd11b83b4e819a2635f2.png)
用户自定义了一个值,则从定义的那个值开始往后依次增加
![](https://i-blog.csdnimg.cn/blog_migrate/9e7877234731b7a639d12a485d57d6e0.png)
![](https://i-blog.csdnimg.cn/blog_migrate/167d6ebeb52315baf9afe35f40d292f8.png)
课后题
2 计算file占用内存大小
#include <stdio.h>
int main(void){
char *file[]= {"f1","f2","f3","f4"};
printf("%d",sizeof(file)); //16
return 0;
}
7 对字符串进行逆序操作
//指针操作
#include <stdio.h>
#include <string.h>
void reverseStr(char *ps){
int i=0;
char tmp;
for (i = 0; i < strlen(ps)/2; i++){
tmp=*(ps+(strlen(ps)-i-1));
*(ps+(strlen(ps)-i-1))=*(ps+i);
*(ps+i)=tmp;
}
}
int main(){
char str[]="hello";
printf("%s \n",str);
reverseStr(str);
printf("%s \n",str);
return 0;
}
//直接对数组操作
#include <string.h>
void swap(char *a,char *b){
char tmp;
tmp=*b;
*b=*a;
*a=tmp;
}
int main(void){
int i=0;
char str[]="part";
printf("%s \n",str);
for (i = 0; i < strlen(str)/2; i++){
swap(&str[i],&str[strlen(str)-i-1]);
}
printf("%s \n",str);
return 0;
}
11 统计字符串有多少个单词(单词之间用空格或制表符隔开)
/*制表符进了csdn的代码编辑区会变成空格o(╥﹏╥)o*/
#include <stdio.h>
int calNunOfWord(char *ps){
int i=0;
int n=0;
printf("%s \n",ps);
while(*(ps+i) != '\0'){
if((*(ps+i)==9)||(*(ps+i)==32)){ //ascii 制表符9 空格32
n++;
}
i++;
};
return n;
}
int main(void){
char str[]=" If I were you, I will give up!";
printf("%d \n",calNunOfWord(str));
return 0;
}
第六章 C语言的预处理、函数和函数库
编译链接流程
hello.c 文件
#include <stdio.h>
#define PI 3.14
int main(void){
printf("%f \n",2*PI*3);
return 0;
}
过程一 预处理(cpp)
>> gcc -E hello.c -o hello.i
生成 hello.i 文件
预处理涉及内容
1 文件包含
2 宏定义
3 条件编译
4 预处理关键字
5 去掉程序中的注释
预处理指令如
#include #ifdef #define宏
![](https://i-blog.csdnimg.cn/blog_migrate/20f8c1de66a9f5540d0f3dec1a9943fc.png)
在预处理过程中,将PI替换成3.14
过程二 编译(cc)
>> gcc -S hello.i -o hello.s
生成 hello.s 文件
.file "hello.c"
.text
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC1:
.ascii "%f \12\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB13:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
call ___main
fldl LC0
fstpl 4(%esp)
movl $LC1, (%esp)
call _printf
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE13:
.section .rdata,"dr"
.align 8
LC0:
.long 1030792151
.long 1077073674
.ident "GCC: (MinGW.org GCC Build-2) 9.2.0"
.def _printf; .scl 2; .type 32; .endef
过程三 汇编(as)
>> gcc -c hello.s -o hello.o
生成 hello.o 文件
过程四 链接(ld)
>> gcc hello.o -o hello.out
生成 hello.out文件
GCC常见拓展名
拓展名 | 含义 |
.c | 是C语言源代码文件 |
.a | 由目标文件构成的档案库文件(静态库文件) |
.C、.cc、.cpp、.C++、.cp、.cxx | C++源码文件 |
.h | 程序包含的头文件(适用于C,C++,Objective-C,Objective-C++) |
.i | 已经预处理过的C源代码文件 |
.ii | 已经预处理过的C++源代码文件 |
.m | Objective-C源代码文件(Note that:必须和“libobjs”库相链接,才能保证Objective-C能正常工作) |
.o | 编译后的目标文件 |
.s | 汇编语言源代码文件 |
.S | 经过预编译的汇编语言源代码文件 |
生成可执行文件
>> gcc hello.c -o hello
生成 hello.exe 文件
库函数
库函数直接调用就好,我们只关心输入参数和输出参数
C语言标准库函数大全(ctype、time 、stdio、stdlib、math、string)
man手册
什么是man手册
man手册就是linux下提供给用户查看linux语法的一本书,当用户遇到一些不懂的命令/函数的时候,不用刻意地去记住命令/函数是什么,只需要记住怎么在man手册中查询即可。
遇到不懂的命令 ---> 立即查询man手册 --> 得知: 命令功能、命令参数、命令使用方法。
遇到不懂的函数 ---> 立即查询man手册 --> 得知: 函数功能、函数头文件、函数原型、函数参数、函数返回值。
>> man man
![](https://i-blog.csdnimg.cn/blog_migrate/9fb659c78f601f57738804d2ac3a41a2.png)
>> man 3 printf
![](https://i-blog.csdnimg.cn/blog_migrate/03329347376d7302c585bd17fbbed930.png)
>> man chmod
![](https://i-blog.csdnimg.cn/blog_migrate/6f1df547c71b0232bc7e92afba05b007.png)