嵌入式C语言——学习笔记

嵌入式C语言——学习笔记

    • 计算机程序语言的学习思路?
    • GCC的使用及其常用选项介绍
      • gcc概述
      • C语言编译过程
      • C语言常见的错误
      • 预处理的使用
      • 宏展开下的 #、##
    • C语言常用关键字及运算符操作
      • 关键字
        • sizeof、return
      • 数据类型
        • char
        • int
        • unsigned、signed
        • float、double
      • 自定义数据类型
        • struct
        • union
        • enum
        • typedef
      • 逻辑结构
        • if、else
        • switch、case、default
        • do、while、for
      • 类型修饰符
      • 运算符
        • 算数运算
        • 逻辑运算
        • 位运算
        • 赋值运算
        • 内存访问符号
    • 内存空间
      • 指针概述
      • 指针+修饰符
      • 指针+运算符
        • ++、--、+、-
        • [ ]
        • 逻辑操作符
      • 多级指针
    • 数组
      • 数组的定义及初始化
      • 空间的赋值
      • strcpy , strncpy
        • 非字符空间
      • 指针数组
      • 多维数组
    • 结构体
      • 字节对齐
    • 内存分布
    • 函数
      • 函数概述
      • 输入参数
      • 返回值
      • 常见面试题

计算机程序语言的学习思路?

基本程序设计思想 + 语言工具的特性

基本程序设计思想:

  1. 数据类型、运算符、条件分支、循环设计
  2. 面向对象的设计

C语言工具的特性:

  1. 比如操作底层,尤其是内存地址的寻址及操作,指针的使用
  2. 掌握C语言的设计思路,比普通的语法要重要的多
  3. 万变不离其宗,掌握C语言的核心规律。

学习过程主要就是弄清楚三个问题:

  1. 什么时候用?(在什么情况下会去使用)
  2. 怎么用?
  3. 为什么要这样设计?(通过这个语法所要传达的意义)

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

掌握C语言如何变成机器指令的过程
gcc工具的几个常用选项的意义
如果你对gcc越熟悉或者理解更深入,对后面学驱动和arm体系架构都有一定的帮助

gcc概述

GCC(GNU Compiler Collection,GNU编译器套件)是由GNU开发的编程语言编译器。其主要的目的就是将我们能理解的一套语言翻译成一个机器所能理解的语言,相当于翻译官。

查看gcc版本

gcc -v

gcc编译

gcc -o 输出文件名 输入文件名
注:“-o 输出文件名” 相当于一个整体,中间不能有其他东西

运行输出文件

./输出文件名

C语言编译过程

查看编译过程

gcc -v -o 输出文件名 输入文件名

在这里插入图片描述

  1. 预处理(将 .c 文件转换为 .i 文件)

gcc -E -o a.i test.c
注:define include不是关键值,它们经过预处理就没有了,替换到相对应的位置。

  1. 编译(将 .i 文件转换为 .s 文件)

.s结尾文件:
小写的 s文件,在后期阶段不会再进行预处理操作了,所以我们不能在其内写上预处理语句。
一般是 .c 文件经过汇编器处理后的输出【即 gcc -S 包括 “ 预处理 + 编译 ” 】。 如 GCC 编译器就可以指定 -S 选项进行输出,且是经过预处理器处理后的了。
例如:gcc -S -o a.s test.c-----生成.s结尾的文件,打开为汇编代码

  1. 汇编(将 .s 文件转换为 .o 文件)

.o结尾文件:
只编译不链接形成.o文件。里面包含了对各个函数的入口标记,描述,当程序要执行时还需要链接(link).链接就是把多个.o文件链成一个可执行文件。如 GCC 编译器就可以指定 -c选项进行输出。打开是乱码(二进制文件)。
例如:gcc -c -o a.o a.s-------结果生成.o文件,打开为能被cpu直接识别的二进制代码

  1. 链接

gcc -o xxx test.c

C语言常见的错误

预处理错误:

#include “name” //自定义的头文件
#include //系统库的头文件
not find 没有找到文件
使用【gcc -I对应头文件的目录 -o build test.c】指定头文件的目录

编译错误:

语法错误,如:缺少 ;{ }

链接错误:

  1. 原材料不够:undefined reference to ‘xxx’

寻址标签是否实现,链接时是否加入一起链接
如果有多个.c文件可以先将他们编译成.o文件,在一起进行链接

gcc -c -o a.o 001.c
gcc -c -o b.o 002.c
gcc -o build a.o b.o

  1. 原材料多了:multiple definition of ‘xxx’

多次实现了标签,只保留一个标签实现

预处理的使用

  1. #include 包含头文件
  2. #define 宏 主要用于替换,不进行语法检查

普通宏:#define 宏名 宏体

如:#define ABC (5+3)
尽量加括号,防止产生歧义,如printf(“the %d\n”, ABC*5);

函数宏:#define 函数宏(x) 函数宏体

如:#define ABC (x) (5+(x))
同样也是要加上括号

  1. #ifdef #else #endif 用来进行调试版本和发行版本的切换
#include 

int main()
{
#ifdef ABC
	printf("======%s======\n",__FILE__);
#endif
	printf("hello world!\n");
	return 0;
}

想运行 printf (“======%s======\n”, __FILE__);
方法一:在main函数前添加 " #define ABC " (但不太建议使用这种方法,以为还要修改原文件)
方法二:在编译时使用 【gcc -D】通过编译器认为的添加宏名,,使用 gcc -DABC 相当于 #define ABC (如:gcc -DABC -o build test.c)

  1. 预定义宏
    系统已经定义好的,我们都可以直接使用,在调试中用得比较多
/*
__FUNCTION__ :函数名
__LINE__ :行号
__FILE__ :文件名
*/

#include 
int fun()
{
	printf("the %s, %s, %d", __FUNCTION__, __FILE__, __LINE__);
	return 0;
}
int main()
{
	fun();
	return 0;
}
//返回结果:fun, test.c, 4

宏展开下的 #、##

#: 字符串化
##:连接符号

#include 

#define	  ABC(x)  #x		//这比较简单,就是将 x 转换为字符串型的
#define	  DAY(x)  myday##x

int main()
{
	int myday1 = 10;
	int myday2 = 20;
	
	printf(ABC(ab\n));
	printf("the day is %d\n", DAY(1));
	return 0;
}

//输出结果:
ab
the day is 10

常见的使用:
嵌入式C语言——学习笔记_第1张图片

C语言常用关键字及运算符操作

掌握C语言 的常用关键字及其应用场景,使用技巧
掌握位运算的典型操作
掌握常用的逻辑操作

when to do? how to do? why to do?

关键字

关键字:编译器 预先定义了一定意义的 字符串(共32 个字符串)

auto :声明自动变量
break:跳出当前循环
case:开关语句分支
char :声明字符型变量或函数返回值类型
const :声明只读变量
continue:结束当前循环,开始下一轮循环
default:开关语句中的“默认”分支
do :循环语句的循环体
double :声明双精度浮点型变量或函数返回值类型
else :条件语句否定分支(与 if 连用)
enum :声明枚举类型
extern:声明变量或函数是在其它文件或本文件的其他位置定义
float:声明浮点型变量或函数返回值类型
for:一种循环语句
goto:无条件跳转语句
if:条件语句
int: 声明整型变量或函数
long :声明长整型变量或函数返回值类型
register:声明寄存器变量
return :子程序返回语句(可以带参数,也可不带参数)
short :声明短整型变量或函数
signed:声明有符号类型变量或函数
sizeof:计算数据类型或变量长度(即所占字节数)
static :声明静态变量
struct:声明结构体类型
switch :用于开关语句
typedef:用以给数据类型取别名
unsigned:声明无符号类型变量或函数
union:声明共用体类型
void :声明函数无返回值或无参数,声明无类型指针
volatile:说明变量在程序执行中可被隐含地改变
while :循环语句的循环条件

sizeof、return

sizeof:编译器给我们查看内存空间容量的一个工具,用来计算数据类型或变量长度(即所占字节数)
return:返回的概念

#include 

int main()
{
	int a;
	printf("the a is %lu\n", sizeof(a));
}

sizeof:大小一般由编译器来决定

数据类型

C语言操作对象:资源/内存(内存类型的资源,LCD缓存、LED灯)
C语言如何描述这些资源的属性?
大小:使用数据类型限制内存的大小
嵌入式C语言——学习笔记_第2张图片
long在linux64环境下所占用字节位8,也就间接说明了long在macOS下的字节长度也是8。(这是因为MacOS系统和Linux都是类Unix系统,只不过基于不同的内核)

char

硬件芯片操作的最小单位:bit
软件操作的最小单位:8bit == 1B (即char)

int

大小:int 没有固定的大小,根据编译器来决定
编辑器最优的处理大小:系统一个周期,所能接收的最大处理单位,int

32bit系统 4B int
16bit系统 2B int

进制问题:

int a = 10; 表示为10进制
int a = 010; 表示为8进制
int a = 0x10; 表示16进制

unsigned、signed

区别:内存空间的最高字节 是符号位, 还是数据
无符号:数据
有符号:数字

float、double
float   4B
double  8B
1.0   1.1double类型
1.0f  1.1ffloat类型

自定义数据类型

C编译器默认定义的内存分配不符合实际资源的形式
自定义 = 基本元素的组合

struct

struct 相当于累加的过程

struct myabc{
	unsigned int a;
	unsigned char b;
	unsigned int c;
};//此处还未申请内存空间

struct myabc mybuf;//申请空间

顺序有要求的:顺序的不同也会影响到最后大小的不同

union

union 相当于叠加的过程
共用体 就是 大家共用一个起始地址来申请空间

共用体 data 中,成员 f 占用的内存最多,为 8 个字节,所以 data 类型的变量(也就是 a、b、c)也占用 8 个字节的内存,请看下面的演示:

#include 
union data{
    int n;
    char ch;
    short m;
};

int main(){
    union data a;
    printf("%d, %d\n", sizeof(a), sizeof(union data) );
    a.n = 0x40;
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
    a.ch = '9';
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
    a.m = 0x2059;
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
    a.n = 0x3E25AD54;
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
   
    return 0;
}

运行结果:
4, 4
40, @, 40
39, 9, 39
2059, Y, 2059
3E25AD54, T, AD54

这段代码不但验证了共用体的长度,还说明共用体成员之间会相互影响,修改一个成员的值会影响其他成员。

enum

枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。

例如,列出一个星期有几天:

enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun };

可以看到,我们仅仅给出了名字,却没有给出名字对应的值,这是因为枚举值默认从 0 开始,往后逐个加 1(递增);也就是说,week 中的 Mon、Tues … Sun 对应的值分别为 0、1 … 6。

我们也可以给每个名字都指定一个值:

enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };

更为简单的方法是只给第一个名字指定值:

enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };

这样枚举值就从 1 开始递增,跟上面的写法是等效的。

typedef

typedef:数据类型的别名
来替代系统默认的基本类型名称、数组类型名称、指针类型名称与用户自定义的结构型名称、共用型名称、枚举型名称等。

一般看到 xxx_t:就是typdef定义过的

  1. 为基本数据类型定义新的类型名
typedef unsigned int uint32_t;
  1. 为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称
typedef struct tagPoint
{
    double x;
    double y;
    double z;
} Point;
  1. 为数组定义简洁的类型名称
typedef int INT_ARRAY_100[100];
INT_ARRAY_100 arr;
  1. 为指针定义简洁的名称
typedef char* PCHAR;
PCHAR pa;

逻辑结构

if、else

条件

if(条件表达式)
	xxx;
else
	yyy;
switch、case、default

多分支

switch(整型数字){
	case 1:
		aaa;
		break;
	case 2:
		bbb;
		break;
	default:
		ccc;
}
do、while、for

循环
for:次数
while:条件

continue、break、goto
continue: 语句的作用是跳过循环体中剩余的语句而强制进入下一次循环。continue语句只用在 while、for 循环中,常与 if 条件语句一起使用,判断条件是否成立。

break:用于 while、for 循环时,会终止循环而执行整个循环语句后面的代码。break 关键字通常和 if 语句一起使用,即满足条件时便跳出循环。

类型修饰符

auto、register、static、extern、const、volatile
对内存资源存放位置的限定

auto:为默认情况,分配的内存可读可写的区域,区域如果在{ }中,则为站空间

register:限定变量定义在寄存器是的修饰符,它可以定义一些要快速访问的变量。编译器会尽量的安排CPU的寄存器去存放这个变量,如果寄存器不足时,变量还是存放在存储器中。(寄存器里的变量是没有地址的,所以register修饰的变量使用 & 去取地址是不起作用的)

static:
隐藏与隔离的作用
全局变量虽然属于静态存储方式,但并不是静态变量。全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,全局变量在各个源文件中都是有效的。
如果我们希望全局变量仅限于在本源文件中使用,在其他源文件中不能引用,也就是说限制其作用域只在定义该变量的源文件内有效,而在同一源程序的其他源文件中不能使用。这时,就可以通过在全局变量之前加上关键字 static 来实现,使全局变量被定义成为一个静态全局变量。这样就可以避免在其他源文件中引起的错误。也就起到了对其他源文件进行隐藏与隔离错误的作用,有利于模块化程序设计。

保持变量内容的持久性
有时候,我们希望函数中局部变量的值在函数调用结束之后不会消失,而仍然保留其原值。即它所占用的存储单元不释放,在下一次调用该函数时,其局部变量的值仍然存在,也就是上一次函数调用结束时的值。这时候,我们就应该将该局部变量用关键字 static 声明为“静态局部变量”。

static 是用来修饰变量和函数的:

  1. 修饰局部变量-称为静态局部变量
  2. 修饰全局变量-称为静态全局变量
  3. 修饰函数-称为静态函数

全局变量具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用 extern 关键字再次声明这个全局变量。

静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。

局部变量也只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。

静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。

extern:如果一个工程由多个源文件组成,在一个源文件中想引用另一个源文件中已定义的 外部变量,就需要在引用变量的源文件中用extern关键字声明变量。

extern和static关系:
extern和static两者之间是有一种相克的关系:用了extern的不能用static,用了static的不能用extern,extern是声明让别的文件中能够使用,static修饰之后的函数或全局变量不能被其他文件使用。

const:

(1)const是一个C语言的关键字,它的作用是限定一个变量不允许被改变。
(2)const是给系统看,让系统不要改变我的值。
(3)const也是给程序员看,让程序员看这里为什么要用const,让程序员不要轻易修改,其实可以无视const,用指针调用指针来把const的作用给无视掉。

(1)修饰局部变量

const int n=5;
int const n=5;

这两种写法是一样的,都是表示变量n的值不能被改变了,需要注意的是,用const修饰变量时,一定要给变脸初始化,否则之后就不能再进行赋值了。

接下来看看const用于修饰常量静态字符串,例如:

const char* str="fdsafdsa";

如果没有const的修饰,我们可能会在后面有意无意的写str[4]=’x’这样的语句,这样会导致对只读内存区域的赋值,然后程序会立刻异常终止。有了const,这个错误就能在程序被编译的时候就立即检查出来,这就是const的好处。让逻辑错误在编译期被发现。

(2)常量指针与指针常量

常量指针是指针指向的内容是常量,可以有一下两种定义方式。

const int * n;
int const * n;

需要注意的是一下两点:

1、常量指针说的是不能通过这个指针改变变量的值,但是还是可以通过其他的引用来改变变量的值的。

int a=5;
const int* n=&a;
a=6;

2、常量指针指向的值不能改变,但是这并不是意味着指针本身不能改变,常量指针可以指向其他的地址。

int a=5;
int b=6;
const int* n=&a;
n=&b;

指针常量是指指针本身是个常量,不能在指向其他的地址,写法如下:

int *const n;

需要注意的是,指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向改地址的指针来修改。

int a=5;
int b=6;
int* const p=&a;
*p=8;
p = &b; //错误,指向的地址不能改变

区分常量指针和指针常量的关键就在于星号的位置,我们以星号为分界线,如果const在星号的左边,则为常量指针,如果const在星号的右边则为指针常量。如果我们将星号读作‘指针’,将const读作‘常量’的话,内容正好符合。int const * n;是常量指针,int *const n;是指针常量。

指向常量的常指针

是以上两种的结合,指针指向的位置不能改变并且也不能通过这个指针改变变量的值,但是依然可以通过其他的普通指针改变变量的值。

const int* const p;

volatile:告知编译器编译方法的关键字,不进行优化编译。由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。(如 for 循环的 i++ 可能会在寄存器里进行,不会将 i++ 的结果写入到RAM中后,CPU再进行读取)

运算符

算数运算

【 +、-、*、/、% 】

*、/ :CPU一般不支持乘、除,CPU可能要多个周期,甚至要利用软件的模拟方法去实现乘法

% :求模
用法:
(1) 取一个范围的数
n % m = res,可以获得 0 ~ (m - 1) 范围的数
(2) 得到 M 进制的一个个位数
(3) 得到循环数据结构的下标

逻辑运算

【 || 、&&、 >、>=、 <、 <=、!、? : 】
返回结果就是:1 、0 (非0即为真)

|| 、&&:
A || B 不等价为 B || A (一个为真即为真)
A && B (全部为真才为真)

位运算

【 <<、>>、&、|、^、~ 】

<<、>>:
左移:乘法 2 二进制下的移位(m< m2^n)
[数据、数字(符号)]
右移:
与符号变量有关

&、|、^ :
&:屏蔽(A & 0 ----> 0)、取出(A & 1 ----> A),清零器

int a = 0x1234;
a & 0xFF00;		//屏蔽第8位,取出高8位

|:保留( A | 0 ----> A)、设置高电平( A | 1 ----> 1),设置器
应用:

//设置一个资源的bit5(从bit0开始)为高电平,其他位不变
int a;
a = a | (0x1<<5);		// ====> a|(0x1<

//清除bit5
a = a & 0x011111		//这是错误的,如果在32位的单片机中,0前面还有26个'0',相与之后高26位也被清除
a = a & ~(0x1<<5);		// ====> a & ~(0x1<

^ :异或(相同为假,不同为真)
主要用于加密算法中:如,AES、SHA1…

//交换两个数(不引入第三方变量)
int fun()
{
	int a = 20;
	int b = 30;
	a = a ^ b;
	b = a ^ b;
	a = a ^ b;
}

~:每一位都要取反

赋值运算

【 =、+=、-=、&=、…… 】

内存访问符号

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

():1.限制符、2.函数访问

内存空间

指针概述

指针变量:存放指针的盒子
C语言编译器对指针这个特殊的概念,要解决2个问题:

1、分配一个盒子,盒子的大小是多少?
在32bit系统中,指针就是4个字节

#include 
int main()
{
	int *p1;
	char *p2;
	printf("the p1 is %u, the p2 is %u\n",sizeof(p1),sizeof(p2));
}

//输出结果:
//the p1 is 4, the p2 is 4

2、盒子里存放的地址所指向内存的读取方法是什么?
char *p; 表示一次读取一个字节
int *p; 表示一次读取4个字节

#include 
int main()
{
	int a = 0x12345678;
	char *p;
	p = &a;
	printf("the p is %x\n",*p1);
}

//输出结果:
//the p is 78

指针+修饰符

const:

const char *p;
char const *p;
常量指针,指针的指向可以变,但指针指向的内容不能变。===> 字符串 “hello world”

char * const p;
char *p const;
指针常量,指正的指向不能变,但指针指向的内容能变。===> 硬件资源,如,LCD、显卡缓存

const char* const p;
希望存放在 ROM 空间中

#include 
int main()
{
	char *p1 = "hello world!\n";
	printf("the one is %x\n",*p1);
	*p1 = 'a';
	printf("the %s\n",p1);
}

//输出结果:
//the one is 68
//Segmentation fault(段错误)

" "修饰的就是字符串,即为const char*,只读常量。

#include 
int main()
{
	char buf[] = {"hello world!\n"};
	char *p2 = buf;
	printf("the one is %x\n",*p2);
	*p2 = 'a';
	printf("the %s\n",p2);
}

//输出结果:
//the one is 68
//the aello world!

char*和char[]区别

本质上来说,char *s定义了一个char型的指针,它只知道所指向的内存单元,并不知道这个内存单元有多大,所以:

当char *s = “hello”;后,不能使用s[0]=‘a’;语句进行赋值。这是将提示内存只能读,不能为写。

当用char s[]=“hello”;后,完全可以使用s[0]=‘a’;进行赋值,这是常规的数组操作。

#include 
 
int main(int argc, char* argv[]) {
	char* buf1 = "this is a test";
	char buf2[] = "this is a test";
	printf("size of buf1: %d\n", sizeof(buf1));
	printf("size of buf2: %d\n", sizeof(buf2));
	return 0;
}

//输出结果:
//size of buf1: 4
//size of buf2: 15

(1)buf1 指向的 “this is a test” 位于字符串常量区。正是指向常量区的原因buf1[0]= ‘a’;这样的操作会报错,常量区内容不可修改。
如果定义
char* a = “123”;
char* b = “123”;
其实 a 和 b 都指向同一块地址,如果可以通过a=‘4’ 去改变"123",那么b所指向的内容也跟着改变,所以a='4’肯定会报错。
(2)buf2 则是开辟一块空间,依次存放 ‘t’ ‘h’ ‘i’ ‘s’ ’ ’ ‘i’ ‘s’……。buf2指向数组的第零个元素,不可修改其指向,例如buf2++是会报错的。

volatile:

告知编译器编译方法的关键字,不进行优化编译。由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。(如 for 循环的 i++ 可能会在寄存器里进行,不会将 i++ 的结果写入到RAM中后,CPU再进行读取)

使用场景

1.程序使用RTOS,多线程中都会读写的全局变量需要使用volatile定义
2.中断和主函数中都要读写的全局变量,需要使用volatile定义
3.单片机的寄存器定义,当然这些变量已经有芯片厂商在库函数中完成定义。

typedef:

给变量类型起别名

char *name_t;			//name_t是一个指针,指向了一个char类型的内存

typedef char *name_t;	//name_t是一个指针类型的名称,指向了一个char类型的内存
name_t abc;

指针+运算符

++、–、+、-

指针的加减法运算,实际上加的是一个单位,单位的大小可以使用sizeof(p[0])

int *p = xxx;  [0x12]
p+1  ————>  [0x12 + 1*(sizeof(*p))]

int *p 和 char *p 的 p+1 是不一样的

p++、p-- :在查看的过程中还跟新了地址
[ ]

变量名[n]
理解为是地址内容的标签访问方式,取出标签里的内存值

#include 

int main(void)
{
	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);
	printf("the p2+1 is %x\n",p2[1]);
}

//输出结果
//the p1+1 is 12345678,12345678,9999119a
//the p2+1 is 11

指针越界访问
(被 const 修饰的变量还是有可能改变的)

#include 

int main()
{
	const int a = 0x12345678;
	int b = 0x11223344;
	
	int *p = &b;
	p[1] = 0x100;
	printf("the a is %x/n",a);
}

//输出结果
//the a is 100
逻辑操作符

主要还是使用 == 和 !=

  1. 跟一个特殊值比较
    NULL / 0x00 :地址的无效值,结束标志 if ( p== 0x00 )
  2. 指针必须是同类型的比较才有意义

多级指针

**int p
存储地址的地址空间
嵌入式C语言——学习笔记_第3张图片
char **p;
将没有关系的内容变得有顺序
嵌入式C语言——学习笔记_第4张图片
当 p[m] == NULL ——> 结束了

#include 

int main(int argc, char **argv)
{
	int i;
	for (i = 0; i < argc; i++)
	{
		printf("the argv[%d] is %s\n",i,argv[i]);
	}
	return 0;
}
#include 

int main(int argc, char **argv)
{
	int i = 0;
	//出现二级指针常见的模板
	while (argv[i] != NULL)
	{
		printf("the argv is %s\n",argv[i]);
		i++;
	}
	return 0;
}

数组

数组的定义及初始化

定义一个空间:

  1. 名字
  2. 大小
  3. 读取方式

数据类型 数组名[m] :
m的作用域是在申请的时候
在使用时[ ] 里的数可以任意,但是会发生越界访问
数组名是一个常量符号,一定不要放到 = 的左边

空间的赋值

按照标签逐一处理
int a[10] ; [ 0 - 9 ]
a[0] = xx;
a[1] = yy;

程序员这样赋值,工作量比较大,能不能让编译器进行一些自动处理,帮助程序员写如上的程序
——》空间定义时,就告知编译器的初始化情况,空间的第一次赋值,初始化操作

数组空间的初始化 和 变量的初始化 本质不同,尤其在嵌入式的裸机开发中,空间的初始化往往需要库函数的辅助

char buf[10] ={'a','b','c'};
buf当成普通内存来看,没有问题
buf当成一个字符串来看,最后加上一个‘\0’或‘0’ (字符转的重要属性,结尾一定有个‘\0’)

char buf[10] = "abc";
buf[2] = 'e';
char *p = "abc";
p[2] = 'e';
//p指向的是一个常量,不能修改,这样会发生段错误

strcpy , strncpy

一块空间,当成字符空间,提供了一套字符拷贝函数
字符拷贝函数的原则:

  1. 内存空间和内存空间的逐一赋值的功能的一个封装体
  2. 一旦空间中出现了“0”这个特殊值,函数就即将结束

头文件:#include
strcpy
strcpy() 函数用来复制字符串,其原型为:
*char *strcpy(char *dest, const char src);
【参数】dest 为目标字符串指针,src 为源字符串指针。
注意:src 和 dest 所指的内存区域不能重叠,且dest 必须有足够的空间放置 src 所包含的字符串(包含结束符NULL)。
【返回值】成功执行后返回目标数组指针 dest。
strcpy() 把src所指的由NULL结束的字符串复制到dest 所指的数组中,返回指向 dest 字符串的起始地址。
注意:strcpy 是依据 \0 作为结束判断的,如果参数 dest 所指的内存空间不够大,可能会造成缓冲溢出(buffer Overflow)的错误情况,在编写程序时请特别留意,或者用strncpy()来取代。

strncpy
strncpy()用来复制字符串的前n个字符,其原型为:
char * strncpy(char *dest, const char *src, size_t n);
【参数说明】dest 为目标字符串指针,src 为源字符串指针。
strncpy()会将字符串src前n个字符拷贝到字符串dest。
如果src的前n个字节不含NULL字符,则结果不会以NULL字符结束。
如果src的长度小于n个字节,则以NULL填充dest直到复制完n个字节。
src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。
注意:src 和 dest 所指的内存区域不能重叠,且dest 必须有足够的空间放置n个字符。
【返回值】返回指向dest的指针(该指向dest的最后一个元素)
注意:但 strncpy 其行为是很诡异的(不符合我们的通常习惯)。标准规定 n 并不是 sizeof(s1),而是要复制的 char 的个数。一个最常见的问题,就是 strncpy 并不帮你保证 \0
另外,如果 s2 的内容比较少,而 n 又比较大的话,strncpy 将会把之间的空间都用 \0 填充。这又出现了一个效率上的问题,如下:

char buf[80];
strncpy( buf, "abcdefgh", 79 );

上面的 strncpy 会填写 79 个 char,而不仅仅是 “abcdefgh” 本身,还包含71 (79-8)个 ‘\0’ 。

strncpy 的标准用法为:(手工写上 \0)

strncpy(path, src, sizeof(path) - 1);
path[sizeof(path) - 1] = '\0';
len = strlen(path);
非字符空间

字符空间:
ASCII码编码来解码的空间 ——> 给人看 %s,\0 作为结束标志
char buf[10]; ——> string

非字符空间:
数据采集 0x00 - 0xFF 8bit,开辟一个存储这些数据的盒子
unsigned char buf[10]; ——> data
没有\0作为结束标志,所以在逐一拷贝时,结束标志只能定义个数

拷贝三要数:
1、src
2、dest
3、个数

memcpy

unsigned char buf[10];
unsigned char sensor_buf[100];//00 00 00 23 45 22 ....

strncpy(buf,sensor_buf,10); //带str是以 \0 或 0 为结束标志,如果像上面的数据一样,数据中有0的数据就会结束。

memcpy(buf,sensor_buf,10*sizeof(unsigned char));

strcpy和memcpy主要有以下3方面的区别。

1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy

指针数组

指针数组,就是说首先是一个数组,而数组的元素是指针
嵌入式C语言——学习笔记_第5张图片

#include 
int main(){
    char *str0 = "aaaaa";
    char *str1 = "bbbbb";
    char *str2 = "ccccc";
    char *str[3] = {str0, str1, str2};
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}
char *a[100];
//a的大小sizeof(a) = 100*4

多维数组

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

int main(void)
{
	int a[10];
	int b[5][6];

	int *p1 = a;
	int (*p2)[6] = b;  //*p2代表地址,是按“6个int”方式去读的
}

结构体

字节对齐

cpu一次能读取多少内存要看数据总线是多少位,如果是16位,则一次只能读取2个字节,如果是32位,则可以读取4个字节,并且cpu不能跨内存区间访问。

#include 

struct abc{
	char a;
	short b;
	int c;
};

struct my{
	char a;
	int b;
	short c;
}
int main()
{
	struct abc buf1;
	struct my buf2;
	printd("the buf is %d:%d",sizeof(buf1),sizeof(buf2));
}

//输出结果
//the buf is 8:12

内存分布

内核空间 —— 应用程序不许访问
———————————————— 3G
栈空间 —— 局部变量———— RW
——————
运行时的堆空间 —— malloc
——————
全局的数据空间(初始化的,未初始化) static ————RW data bss
只读数据段 —— “hello world” 字符串常量———— R TEXT
代码段 —— code ———— R TEXT
———————————————— 0x00

1、只读空间:静态空间,整个程序结束时释放内存,生存周期最长
2、栈空间:运行时,函数内部使用的变量,函数一旦返回,就释放,生存周期是函数内
3、堆空间:运行时,可以自由分配和释放的空间
分配:malloc(),一旦成功,返回分配好的地址给我们,只需要接受,对于这个新地址的读法,由程序员灵活把握,输入参数指定分配的大小,单位是B。

char *p;
p = (char *)malloc(100);
if (p == NULL){
	error;
}

int a[5]; =====> malloc(5*sizeof(int))

释放:free§;

函数

函数概述

一堆代码的集合,用一个标签去描述它,实现代码的复用化。(标签—— 函数名)
函数具备的三要素:
1、函数名(地址)
2、输入参数
3、返回值

指针保存函数:int (*p) (int, int, char);

#include 
int main()
{
	int (*myshow)(const char *,...);
	printf("hello world");
	
	myshow = printf;
	myshow("===========\n");
	return 0;
}

//输出结果
hello world
===========
// 如果已知printf的地址是0x8048320
#include 
int main()
{
	int (*myshow)(const char *,...);
	myshow = (int (*)(const char *,...))0x8048320
	myshow("===========\n");
	return 0;
}

//输出结果
===========

输入参数

承上启下的功能

调用者:
函数名(要传递的数据)

被调者:
函数的返回类型 函数名(接收的数据)
{
函数体;
}

连续空间的传递
1、数组
数组名 —— 标签

int abc[10];

void fun(int *p);
fun(abc);

2、结构体
结构体变量

struct abc{
	int a;
	int b;
	int c;
}
struct abc buf;

//值传递:
void fun(struct abc a1);
fun(buf);

//地址传递:
void fun(struct abc *a2);
fun(&buf)

//通常使用地址传递,值传递每次调用形参都要开辟一个结构体大小的空间,这样会浪费空间

连续空间只读

1、下面函数的形参为(char *p)程序设计者会认为空间的内容是可以改变的,而当调用者传入的是字符常量时,就会出现段错误(Segmentation fault)

#include 
void fun(char *p)
{
	p[1] = '2';
}

int main(void)
{
	fun("hello");
	return 0;
}

2、将程序编写为下面的代码可以解决段错误,但还是容易混淆。

#include 
void fun(char *p)
{
	p[1] = '2';
}

int main(void)
{
	char buf[] = "hello";
	fun(buf);
	return 0;
}

3、将函数的形参写为(const char *p)表示为只读 空间,即只能看空间内容不能修改。

void 指针的使用

  1. void 指针可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针对 void 指针赋值
  2. 如果要将 void 指针 p 赋给其他类型的指针,则需要强制类型转换

典型的如内存操作函数 memcpy 和 memset 的函数原型分别为:

void * memcpy(void *dest, const void *src, size_t len);
void * memset ( void * buffer, int c, size_t num );

这样,任何类型的指针都可以传入 memcpy 和 memset 中,这也真实地体现了内存操作函数的意义,因为它操作的对象仅仅是一片内存,而不论这片内存是什么类型。

地址传递的作用

1、修改
int *、short *、long *
2、空间传递
(1) 子函数看看空间里的情况 const *
(2) 子函数反向修改上层空间里的内容 char *、void *

返回值

返回基本数据类型

基本数据类型 fun(void)
{
	基本数据类型 ret;
		xxx;
		ret = xxx;
	return ret;
}

返回连续空间类型

指针作为空间返回的唯一数据类型
int *fun();
地址:指针的合法性,作为函数的设计者,必须保证函数返回的地址所指向的空间是合法的。【即不是局部变量】

函数内部实现
1、静态区(static)
2、只读区(字符常量…)
3、堆区(malloc、free)

#include 
#include 
#include 

cher *fun(void)
{
	//静态区方法
	//static char buf[] = “hello wrold”;
	//堆区方法		
	char *buf = (char *)malloc(100);
	strcpy(buf,"hello wrold");
	
	return buf;
}

int main(void)
{
	char *p;
	p = fun();
	printf("the p is %s\n",p);

	free(p);
	return 0;
}

//输出结果
//the p is hello wrold

常见面试题

嵌入式工程师必备0x10道题目

你可能感兴趣的:(c语言,开发语言)