嵌入式c语言小结

引言

大一学的C语言,本科写过一些简单的C代码,后来读研期间,python,matlab,c++写的多一些,加入工作后,正式进入了嵌入式软件的行业,又重新开始写C代码了。这篇文章小结了一些C语言基础,基本用的是大白话,浅显易懂,用以记录,也欢迎各位路过的大神批评指正。

指针

介绍几个容易混淆的概念:函数指针,指针函数,数组指针,指针数组,结构体指针。在介绍之前,先理解下什么是指针,这个理解清楚,上面这几个就很容易理解了。经常听人说指针就是地址,这句话怎么理解呢?举个例子,定义一个整型指针变量p,指向整型变量a(这句话如果听的还比较迷糊的话,后面会解释清楚为什么这么说)。

int a = 1;
int *p = &a;

在内存中(32位处理器),它是这样子的:

在这里插入图片描述

从图中就很容易理解,p和a很类似,p是一个变量,存放的是整型变量a的地址,p自身也是有一个地址,中间这个箭头是人为的加上去的,变量p存放的是变量a的地址,就好像变量p指向了变量a,这样的变量命名为指针变量p,指针的英文为pointer。知道了一个变量的地址,还不能完全表示这个变量,因为你仅仅知道这个变量的地址,并不清楚这个变量多长,这个类型就是表示变量的长度。上图中p指向了a,同时int *p目的是告诉编译器这个指针变量指向的是个整型变量,也就是4个字节。因此,在使用指针的时候,我们应该关注这两点:

  1. 指针变量p指向哪里?(或者说指针变量p的值是什么?)
  2. 指针变量p指向的变量类型是什么?(或者说指针变量p指向的地址,数据占多长?)

以此类推,定义一个char指针,float指针都是一个意思,都是指指针变量p指向一个char变量,float变量。其实懂了这个,指针也就全懂了,后面什么函数指针,数组指针,结构体指针,指针的指针都是一个意思,下面稍微展开讲讲。每个后面都有例子及其解释。

函数指针与指针函数

函数指针

函数指针本质是个指针,该指针指向一个函数。定义如下:

int (* func)(int, int);

(* func)先结合,显然这是一个指针变量,变量名为func,上面代码的意思是指针变量func,指向一个函数,该函数输出两个整型参数,输出是一个整型。本质上是一个地址,一个指针,只不过指向的是一个函数而已。我们知道函数名表示一个函数的首地址,只要将func指向一个函数即可(某个函数名赋给func即可)。

指针函数

指针函数本质是一个函数,只不过该函数的返回值是一个指针。定义如下:

int * func(int a, int b){
	return &(a+b);
}

func与()先结合,编译器把它翻译为一个函数,前面int *很容易理解了,表示函数的返回值是一个int指针(就是个地址,这个地址存放的数据是整型,占4个字节)。

例子

为了模拟真实的项目代码,有兴趣的话可以建立一个像我这样的目录结构,如下图所示:
在这里插入图片描述
在func.h文件里声明如下,其中声明了一个func1函数(值传递),func2函数(指针传递),func2还是个指针函数,即返回值为一个指针,func3是一个函数指针,指向一个入参是两个整型变量,返回值是一个整型的函数。

#ifndef _FUNC_H
#define _FUNC_H
int func1(int a,int b);
int* func2(int *a, int *b);
int (*func3)(int, int);
#endif

在func.c文件中定义函数,如下,其中func1返回a,b的积,func2将输入的两个指针交换,返回第一个指针的值。

#include <stdio.h>
#include "../include/func.h"
int func1(int a, int b)
{
	return a*b;
}

int* func2(int *a, int *b)
{	
	int *tmp;
	tmp = a;
	a = b;
	b = tmp;
	return a;
}

在主函数中,我们进行简单的调用如下:

int b = 3;
int c = 4;
int *p2;
printf("func1的结果为%d\n",func1(b,c));
p2 = func2(&b,&c);
printf("func2的结果为%d\n",*p2);
func3 = func1;//将函数指针func3指向函数func1
printf("func3的结果为%d\n",func3(2,4));

结果返回如下:
在这里插入图片描述
func3 = func1就是将函数指针func3指向了func1,从内存上解释的话,与前文中定义一个int指针一样,指针变量func3指向函数func1的首地址,数据的类型只不过是个函数体。

数组指针与指针数组

数组指针

数组指针本质是个指针,指向一个数组。定义如下:

int a[] = {2,3,4,6};
int (* p)[4] = &a;

从前文可以知道()的优先级高于*,同时()放在某一标识符后,编译器会将该变量翻译为一个函数,同样的,[]的优先级也高于*,同时[]放在某一标识符后,编译器会将该变量翻译成一个数组,因此上述定义应解释为*与p在括号里,先结合,p肯定是个指针,再结合[4],这样就表示指向的是一个含有4个整型的数组,从我们关注指针的两个点出发,指针p指向数组a,类型是包含4个int的数组。
刚学C的时候,也会看到这样的定义。

int a[] = {2,3,4,6};
int *p = a;

这里p是一个int指针,指向一个整型,从我们关注指针的两个点出发,指针p指向数组a的首元素地址,类型是1个int类型。a,&a,&a[0]这三个值都是一样,都表示数组的首地址,但意义不一样,a与&a[0]一样,都表示数组的首个元素地址,指向的是一个int类型,但是&a指向的是整个数组长度,在该例子中就是4个int,这也就是我们关注指针两点中的第二个点不一样。下面看代码:

int a[] = {2,3,4,6};
int *p = a;
printf("数组a的第一个元素是%d\n",*p);
printf("数组a的第二个元素是%d\n",*(p+1));
int (*p1)[4] = &a;
printf("p1=%p\n", p1);
printf("*p1=%p\n", *p1);
printf("*p1+1=%p\n", *p1+1);
printf("*(p1+1)=%p\n", *(p1+1));
printf("数组a的第一个元素是%d\n", **p1);
printf("数组a的第二个元素是%d\n",*(*p1+1));
printf("%p\n",a);
printf("%p\n",&a);
printf("%p\n",&a[0]);

printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(*&a));
printf("%d\n",sizeof(*&a[0]));

输出结果为在这里插入图片描述

针对这个例子,内存(上图结果运行在64位系统,画图用的是32位)的结果如图
在这里插入图片描述
数组指针p1指向的是数组a的首地址,类型是4个int,int指针p指向数组a的首元素地址,类型是单个int,因此,p+1就是p偏移一个int的地址,p1+1就是偏移4个int的地址了,我们知道p1=&a的,对它解引用后,*p1=a了,a又是个地址,只是类型变成了一个int,其实这里*p1就和p是一模一样了。对于一些复杂的指针,心中只要有张内存的图,其实就不难了。

指针数组

指针数组本质是个数组,数组中每个元素是个指针。先来个简单的定义:

int a1 = 3;
int b1 = 5;
int * p2[3];
p2[0] = &a1;
p2[1] = &b1;
p2[2] = p;
printf("p2第一个元素为%p\n", *p2);//或者p2[0]
printf("p2第二个元素为%p\n", *(p2+1));//或者p2[1]
printf("p2第三个元素为%p\n", *(p2+2));//或者p2[2]

输出结果为
在这里插入图片描述
p2先与[]结合,说明这是个数组,再与int *结合,说明数组元素为int指针。假设指针数组里面存的都是函数指针,则就变成了函数指针数组,定义如下:

int a1(int a, int b);
int a2(int a, int b);
int (*task[2])(int,int);
task[0] = a1;
task[1] = a2;

如果外面再来一个指针,指向这个数组,这个该如何定义呢?答案就很容易了int (*(*p)[2])(int,int)看网上有个这个定义char * (*(*pf)[3])(char * p),看懂了上文应该就不难了,首先这是个指针,变量名pf,指向了一个3个元素的数组,元素类型又是个指针,指向的是一个函数,函数的入参是一个char指针,返回值是一个char指针。

结构体指针

看到这,结构体指针就可以顾名思义了,就是一个指向结构体变量的指针。其定义形式上和常规的int指针一样。先定义两个结构:

typedef struct A1{
	int a;
	float b;
	char *p;	
} mystruct;

typedef struct A2{
	int a;
	float b;
	char d;
	mystruct c;
} mystruct2;

在主函数进行调用如下:

int a[10] = {0,2,3,4};
mystruct s1 = {100,100.0,(char *)a};
mystruct *ms = &s1;
printf("\n结构体s1首成员地址%p\n", &s1.a);
printf("结构体s1首地址%p\n", ms);
printf("结构体s1第二成员地址%p\n", &s1.b);
printf("结构体s1第二成员地址%p\n", (int *)ms+1);
printf("结构体s1第二成员地址%p\n", (char *)ms+4);
printf("结构体s1第三成员地址%p\n", &s1.p);
printf("结构体s1第三成员是%p\n", s1.p);
printf("结构体s1第三成员是%p\n", ms->p);
printf("数组a第二成员是%d\n", *((int*)((s1.p)+4)));
printf("结构体s1长度%d\n", sizeof(s1));
printf("指针长度%d\n", sizeof(s1.p));

mystruct2 s2 = {10, 10.1, 'S', s1};
printf("结构体s2长度%d\n", sizeof(s2));
printf("结构体s2首成员地址%p\n", &s2.a);
printf("结构体s2第二成员地址%p\n", &s2.b);
printf("结构体s2第三成员地址%p\n", &s2.d);
printf("结构体s2第4个成员地址%p\n", &s2.c);

输出结果如下
在这里插入图片描述
从结果中可以看出这么几点

  1. 结构体指针的定义,调用,跟常规int指针,char指针一样,该例子使用了typedef关键字将结构体struct A1换了个名字,叫mystruct,后续使用方法与int,char一样。
  2. 输出结果的第二,三,四行还验证了指针的运算,说到底就是改变了前文所述的关注指针的两个点的第二点,ms指向的地址是一样,改变其指向的数据类型(运用强制类型转换),来改变指针指向的变量的不同,从这个地方可以看出,指针很灵活,很轻便。(很多黑客喜欢c语言指针,因为如果获取到了某个关键数据或者函数的内存地址,只要修改某个指针指向该地址,便可以获取或篡改数据。)
  3. 结构体的长度需要进行对齐。结构体s2中,第三个成员进行了字节对齐,使得最终的结构体长度为32个字节。

宏定义#define

宏定义使用关键字#define,开发过程中一些端口,或者参数可以提前定义好,一些简单的函数操作亦可进行宏定义,宏定义在预处理的时候进行,预处理程序会将代码中用到的宏名直接使用宏定义的内容替换,直接看个简单例子。

#include <stdio.h>
int func1(int a, int b);
#define PI 3.1415926
#define Add(x,y) func1(x,y)
int main(){
	float r = 3;
	printf("半径为%f的圆的面积为%f\n",r,PI*r*r);
	Add(2,4);
	return 0;
}

使用gcc -E进行预处理,得到预处理文件,打开与处理文件,可以看到如下:
在这里插入图片描述
显然,就是直接替换。

条件编译

条件编译与条件语句差不多,条件编译语句由预处理程序运行,将不符合条件的代码直接去除,符合条件的代码生成预处理文件,然后交给后面的编译器进行编译。常见的有:

#if 常量表达式
	...
#elif 常量表达式
	...
#endif
#ifdef 名称
	...
#endif
#ifndef 名称
	...
#endif

条件编译可以选择性代码进行编译,还可以处理重复引用头文件的问题。假设这里有个这样的引用结构:
在这里插入图片描述
main函数使用了func1和func2,func1,func2都引用了func3,直接编译出现重复引用头文件,因此在func3.h中定义一个唯一性的标识,一般可以这么写:

#idndef _FUNC3_H_
#define _FUNC3_H_
	...
#endif

_FUNC3_H_就是该头文件的唯一标识,实际项目头文件很多,每个头文件中基本都是这样子写,防止出现重复引用。

static,inline,extern修饰

这几个关键词项目代码中也经常遇到,简单叙述下他们的作用。

static

static常用于修饰全局变量,局部变量,函数,静态变量在编译后已经生成在静态区,并不是运行的时候生成

  1. 修饰全局变量时,变成了静态全局变量,静态全局变量的作用域发生了改变,全局变量的作用域是整个源程序,一个源程序里面可能包含了很多个源文件,静态全局变量作用域只能在定义该变量的源文件中使用,其他源文件无法使用该源文件的静态全局变量,即使是同名,也无法使用,各用各的,互不影响,起到了隔离作用
  2. 修饰局部变量时,变成静态局部变量,静态局部变量存储位置位于静态区,不存放在栈区,因此在函数调用返回后,静态局部变量不会销毁,继续保留当时值,等下次再被调用,继续使用上次保留下来的值
  3. 修饰函数时,静态函数只能被该源文件的其他函数调用,不能被其他源文件调用,与静态全局变量类似,相当于该源文件的私有函数了。

inline

inline放在函数定义的前面,使该函数成为内联函数。当某个函数操作小但是需要频繁调用的时候,可以将该函数使用inline修饰,使之成为内联函数,在编译阶段,需要调用这个函数时,编译器直接替换函数的定义,这就不需要在运行的时候再来调用,提高了代码运行效率,说到底就是编译器提前干了程序运行时调用函数的一些事情,比如说调用函数的压栈操作等等。
inline函数有点像#define宏定义,inline发生在编译阶段,#define发生在预处理,inline是将函数真正放在了目标文件中,#define仅仅是文本替换。

extern

extern如果放在函数定义前,表明该函数为外部函数,可供其他源文件调用,注意,如果定义前没有,C默认该函数为外部函数。其他源文件在调用某个外部定义的函数时,需要先声明,声明前面需要用extern进行修饰用来告诉编译器该函数为外部函数。extern的作用与static作用正好相反,前者是放大作用域,后者是减小作用域。

小结

  1. 指针重点要理解指针的两个关注点,即指针的指向(地址),指向的数据类型(多长)
  2. 优先级()>[]>*,复杂指针借助内存结构分析。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值