嵌入式开发C语言基础

嵌入式C语言高级

1. GCC的使用及其常用选项介绍

1.1 GCC概述

  1. GCC的最初全名是GNU C Compiler,是GNU操作系统专门编写的一款编译器,即C-->机器语言,随着GCC支持的语言越来越多,它的名称变为GNU Compiler Collection

  2. 一般Linux系统会默认安装GCC,输入gcc -v,查看是否安装GCC,没有使用apt-get install gcc安装

  3. 编译命令gcc -o 输出文件名 输入文件名,其中输入的文件名需要带后缀来自动调用相应类型的编译器,如c语言.c或者c++语言.cpp;输出文件不需要后缀。输入文件名也可以写在-o前面。生成的编译文件可以直接使用./编译文件名运行编译后的文件。

    image-20220530193932232
  4. 编译命令gcc -v -o 输出文件名 输入文件名,可以输出gcc工作的详细过程

1.2 C语言的编译过程

  1. 编译过程

    预处理->编译->汇编->链接

    预处理:define和include均在预处理阶段处理,define和include不是关键字

    编译:主要翻译的是关键字和运算符 高级语言->汇编语言

    汇编:汇编语言->二进制机器语言

    gcc详细执行过程参考(189条消息) gcc简介和命令行参数说明_KuoGavin的博客-CSDN博客_gcc 机器码

    链接工作过程参考(189条消息) 链接器、链接过程及相关概念解析_KuoGavin的博客-CSDN博客_链接解析

1.3 编译常见错误

  1. 预处理错误

    #include "name"自定义库,在当前目录中寻找name,和#include <name>系统库,在系统目录中寻找name。当在库中找不到name报错not find

    使用gcc -I+头文件路径名 -o可以从指定路径中寻找头文件。如:gcc -I./inc -o build helloWord.c,从当前目录中的Inc目录下寻找头文件

  2. 编译错误

    • 语法错误,如漏掉;{}
  3. 链接错误

    • 原材料不够错误。如仅定义了fun函数,却没有实现报错undefined reference to 'fun' 。解决方法:寻找标签是否实现,连接时是否对多个文件一起加入链接

    • 原材料多了。如多次实现了fun标签,报错multiple definition of 'fun'

      解决方法:只保留一个标签实现即可

    • 当存在多个文件时,建议先使用gcc -c -o将单个文件分别编译生成单独的.o文件,再使用gcc -o 将两个.o文件一起连接。如

    image-20220530212949521

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; }

    执行结果:

    image-20220530221052347

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宏名

    image-20220531132438503

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;

    (189条消息) C语言中的struct用法_14skyang的博客-CSDN博客_struct用法

    (189条消息) (C语言)union关键字_wangzi9505的博客-CSDN博客_union关键字

  • 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; };

    枚举类型enum详解——C语言 - 蓝海人 - 博客园 (cnblogs.com)

  • 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 类型修饰符

(189条消息) c语言类型修饰符_sdpeppawutz的博客-CSDN博客_c语言修饰符

(189条消息) C语言修饰符_追梦的魂影的博客-CSDN博客_c语言修饰符

  • 对于内存资源属性存放位置的限定
  • 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; }

​ 运行结果:

image-20220601161441514
  • 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 位运算

  • <<(左移)、>>(右移)、&、|、^、~
image-20220601201145274
  • 移位运算
image-20220601203636529

​ 1)不发生溢出情况下,左移一位相当于数值扩大2倍,适用于有符号数和无符号数

​ 如:m<<1=====>相当于m2,如00100 左移一位变为 01000

​ m<<n=====>相当于m2n,如int a=b*32 可以变为b<<5

对于负数,左移仍然是每移动一位数值扩大两倍

​ 如12=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); } }
  • 与或运算

位运算(&、|、^、~、>>、 | 菜鸟教程 (runoob.com)

​ 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 内存访问符号

  • ()、[]、{}、->、. 、&、*

(190条消息) C语言学习笔记 内存访问符号_义薄云天us的博客-CSDN博客

3. C语言内存空间的使用

3.1指针

(190条消息) 指针基本认识_任我驰骋.的博客-CSDN博客_指针是什么

C 指针 | 菜鸟教程 (runoob.com)

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:修饰指针

详细使用说明(190条消息) volatile与指针_turkeyzhou的博客-CSDN博客_volatile指针

(190条消息) 一文彻底搞懂volatile用法_木头linux的博客-CSDN博客_volatile使用

  • 指针+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

(190条消息) argc,argv是什么_DeRoy的博客-CSDN博客_argc

# 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; }

运行结果:

image-20220605102403535

不使用输入参数个数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; }

运行结果:

image-20220605103043976

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 指针与数组

(190条消息) 【C语言】指针数组和二级指针_Jacky_Feng的博客-CSDN博客_指针数组和二级指针

  • 指针数组

char *a[100]:数组中存放的是100个数据的内存地址,每个地址的大小仍为4字节,因此数组的大小为100*4个字节。指针指向的内存为char型

指针数组与二级指针的区别:

指针数组是一个数组,那么指针数组的数组名是一个地址,它指向数组中第一个元素。

指针数组的数组名实质是一个指向数组的二级指针

  • 数组名的保存

C语言指向多维数组的指针_C语言中文网 (biancheng.net)

1)定义一个指针,指向int a[10]的首地址

int *p1=a

2)定义一个指针,指向int b[5][6]的首地址

int (*p2)[6]=b,它表示p2是一个指针变量,它指向包含6个元素的一维数组,即一维数组b[0]的首地址

3.3 结构体字节对齐

(190条消息) C语言结构体_字节对齐_C4cke的博客-CSDN博客_c语言结构体字节对齐

  • 由于我们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++内存图

C语言内存分布 - 言何午 - 博客园 (cnblogs.com)

链接: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字节

image-20220606115013423

​ 修改后,增加3字节

image-20220606115252804

第6行和第10行" "中字符串为共用的,若是修改其中一行,则会重新生成新的字符串空间。

因此在嵌入式裸板开发中,输出语句都应在调试版本中,在发行版本中尽量避免输出语句。

使用strings可以查看代码中有的字符串(不仅仅是自己写的字符串)

image-20220606135848406

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查看静态空间中数据 标签与地址

image-20220606145942332

使用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

(191条消息) 函数指针和指针函数用法和区别_luoyayun361的博客-CSDN博客_函数指针

//例子:使用函数指针 指向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 连续空间的传递

  1. 连续空间传递概述
  • 数组

​ 数组的 实参==>形参 只能使用地址传递 数组名==标签==地址

实参: 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){}
  1. 连续空间的只读性
  • 在使用地址传递(尤其是连续空间只能使用地址传递)时,我们有时不希望改变原空间的地址,怎么实现?

​ 在形参中使用const修饰地址指针

​ 如void fun(const char*p):即表明为只读空间,不能修改p指向的内存

void fun(char *p):意味着该空间可以修改

​ 为了避免出现错误,我们在定义函数时要明确指出该空间是否为只读

  • 库函数中的只读的例子

​ 如strcpy()函数 char *strcpy(char *dest, const char *src);

  1. 字符空间
  • 地址传递的作用

    1. 对于基本的数据类型进行修改 如int *char\*

    2. 空间传递

      • 子函数看看空间里的情况 使用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()的实现
  1. 非字符空间
  • 对于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 返回值

  1. 函数返回值的基本语法
返回类型 函数名称(输入列表) { 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; }
  • 返回类型

    基本数据

    指针类型(空间)

  1. 返回基本数据类型
  • 返回的数据类型除了int、char、short……等基本数据类型外,还能返回struct结构体的数据类型,但是由于返回的实质是拷贝,为了节约空间不建议使用struct返回,而是使用指针返回
  1. 返回连续空间类型
  • 指针作为空间返回的唯一数据类型
  • 设计函数时要考虑指向地址的合法性

​ 必须保证函数返回的地址所指向的空间时合法的。比如说局部变量不合法,在函数结束后就会自动销毁,再返回地址就没用。数据段,堆中的数据均合法。

//不合法的例子 #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; }
image-20220608112808244
  1. 函数返回的内部实现
  • 对于基本数据类型返回 内部实现的框架如下
基本数据类型 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. 常用面试题目

  • 宏定义
  1. 用预处理命令#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
    1. 修饰局部变量

      默认局部变量在 栈 空间存在,生存周期比较短

      局部静态化后,局部变量在静态数据段中保存,生存周期非常长

    2. 修饰全局变量

      防止重命名,限制变量名只在本文件内起作用

    3. 修饰全局函数

      防止重命名,限制该函数只在本文件内起作用

    • 关键字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__

本文作者Ray963
本文链接https://www.cnblogs.com/ray93/p/16355717.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   ray963  阅读(247)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
点击右上角即可分享
微信分享提示