文件来源于http://blog.51cto.com/9291927/category11.html (天山老妖S) 的博文
第一课 嵌入式linux C语言—位运算的使用
ARM是内存与IO统一编址,SoC中有很多控制寄存器,通过对这些寄存器进行位运算对这些控制寄存器进行设置,进而控制外设功能。在修改寄存器某些位的过程中不能修改其他的位。
一、位运算基础
C语言基本的位操作符有与、或、异或、取反、左移、右移六种位运算符。如下表所示:
符号 | 描述 | 运算规则 |
& | 与 | 两个位都为1时,结果才为1 |
| | 或 | 两个位都为0时,结果才为0 |
^ | 异或 | 两个位相同为0,相异为1 |
~ | 取反 | 0变1,1变0 |
<< | 左移 | 各二进位全部左移若干位,高位丢弃,低位补0 |
>> | 右移 | 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移) |
位运算使用说明:
1、六种位运算只能用于整型数据,对float和double类型进行位操作会被编译器报错。
2、逻辑运算与位运算的区别:逻辑运算是将参与运算的两个表达式整体的结果进行逻辑运算,而位运算是将参与运算的两个数据,按对应的二进制数逐位进行逻辑运算。逻辑运算符有逻辑与&&、逻辑或||、逻辑非!,位运算则有六种运算符,位与&、位或|、位异或^、位取反~、位左移<<、位右移>>。
3、如果左移位数>=类型长度,在GCC环境下,GCC编译器会报警告,但实际左移位数为左移位数%(8 * sizeof(int))。例如:
int i = 1, j = 0x80000000; //设int为32位
i = i << 33; // 33 % 32 = 1 左移1位,i变成2
j = j << 33; // 33 % 32 = 1 左移1位,j变成0,最高位被丢弃
4、在C语言中,左移是逻辑/算术左移(两者完全相同),右移是算术右移,会保持符号位不变。左移时总是移位和补零。右移时无符号数是移位和补零,此时称为逻辑右移;而有符号数大多数情况下是移位和补最左边的位(也就是补最高有效位),移几位就补几位,此时称为算术右移。 算术移位是相对于逻辑移位,它们在左移操作中都一样,低位补0即可,但在右移中逻辑移位的高位补0而算术移位的高位是补符号位。
右移对符号位的处理和左移不同,对于有符号整数来说,比如int类型,右移会保持符号位不变。符号位向右移动后,正数的话补0,负数补1,也就是汇编语言中的算术右移。当移动的位数超过类型的长度时,会取余数,然后移动余数个位。
int i = 0x80000000;
i = i >> 1; //i的值不会变成0x40000000,而会变成0xc0000000
5、位操作符的运算优先级比较低,因为尽量使用括号来确保运算顺序,否则很可能会得到莫明其妙 的结果。比如要得到像1,3,5,9这些2^i+1的数字。写成int a = 1 << i + 1;是不对的,程序会先执行i + 1,再执行左移操作。应该写成int a = (1 << i) + 1。
二、位运算的使用
1、位与运算&
位与运算的实质是将参与运算的两个数据,按对应的二进制数逐位进行逻辑与运算。典型应用如下:
A、特定数据段清零
快速对某一段数据单元的数据清零
unsigned int a = 0x00FF1278;
a &= 0xFFFF0FFF;//对a的bit12--bit15位进行清零,a=0x00FF0278
//a &= ~(0xF<<12) ;//对a的bit12--bit15位进行清零,a=0x00FF0278
B、保留数据区的特定位
unsigned int a = 0x00FF1278;
a &= (0xF<<12);//保留a的bit12--bit15位,其他清零,a=0x00001000
C、判断奇偶数
只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if ((a & 1) == 0)代替if (a % 2 == 0)来判断a是不是偶数
2、位或运算符|
位或运算的实质是将参与运算的两个数据,按对应的二进制数逐位进行逻辑或运算。典型应用如下:
A、对数据位置1
unsigned int a = 0x00FF0278;
a |= 0x0000F000;//对a的第12-15位置1,a=0x00FFF278
//a |= (0xF<<12);//对a的第12-15位置1,a=0x00FFF278
3、位异或^
位异或运算的实质是将参与运算的两个数据,按对应的二进制数逐位进行逻辑异或运算。只有当对应位的二进制数互斥的时候,对应位的结果才为真。典型应用如下:
A、特定位取反
设定一个数据的指定位,将1换为0,0换为1。例如整型数a=321,,将其低八位数据进行翻位的操作为a=a^0XFF。
B、数值交换
a=a^b;
b=b^a;
a=a^b;
不使用第三方变量可以用位操作来实现交换两数
4、位非~
位非运算的实质是将参与运算的两个数据,按对应的二进制数逐位进行逻辑非运算。
A、变换符号
变换符号只需要取反后加1
5、位左移<<
左移运算的实质是将对应的数据的二进制值逐位左移若干位,并在空出的位置上填0,最高位溢出并舍弃。
6、位右移>>
位右移运算的实质是将对应的数据的二进制值逐位右移若干位,并舍弃出界的数字。如果当前的数为无符号数,高位补零。
如果当前的数据为有符号数,在进行右移的时候,根据符号位决定左边补0还是补1。如果符号位为0,则左边补0;但是如果符号位为1,则根据不同的计算机系统,可能有不同的处理方式。可以看出位右移运算,可以实现对除数为2的整除运算。
提示 将所有对2的整除运算转换为位移运算,可提高程序的运行效率。
A、求绝对值
int i = a >> 31;
return i == 0 ? a : (~a + 1);
或
int i = a >> 31;
return ((a ^ i) - i);
7、嵌入式开发中常用位操作
A、将寄存器指定位(第n位)置为1
GPXX |= (1<<n)
GPXX |= (1<< 7) | (1<< 4 ) | (1<< 0);//第0、4、7位置1,其他保留
B、将寄存器指定位(第n位)置为0
GPXX &= ~(1<<n )
将寄存器的第n位清0,而又不影响其它位的现有状态。
GPXX &= ~(1<<4 )
C、嵌入式开发位操作实例
unsigned int i = 0x00ff1234;
//i |= (0x1<<13);//bit13置1
//i |= (0xF<<4);//bit4-bit7置1
//i &= ~(1<<17);//清除bit17
//i &= ~(0x1f<<12);//清除bit12开始的5位
//取出bit3-bit8
//i &= (0x3F<<3);//保留bit3-bit8,其他位清零
//i >>= 3;//右移3位
//给寄存器的bit7-bit17赋值937
//i &= ~(0x7FF<<7);//bit7-bit17清零
//i |= (937<<7);//bit7-bit17赋值
//将寄存器bit7-bit17的值加17
// unsigned int a = i;//将a作为i的副本,避免i的其他位被修改
// a &= (0x7FF<<7);//取出bit7-bit17
//a >>= 7;//
// a += 17;//加17
// i &= ~(0x7FF<<7);//将i的bit7-bit17清零
// i |= (a<<7);//将+17后的数写入bit7-bit17,其他位不变
//给一个寄存器的bit7-bit17赋值937,同时给bit21-bit25赋值17
i &= ~((0x7FF<<7) | (0x1F<<21));//bit7-bit17、bit21-bit25清零
i |= ((937<<7) | (17<<21));//bit7-bit17、bit21-bit25赋值
三、位操作的宏定义
//用宏定义将32位数x的第n位(bit0为第1位)置位
#define SET_BIT_N(x,n) (x | (1U<<(n-1)))
//用宏定义将32位数x的第n位(bit0为第1位)清零
#define CLEAR_BIT_N(x,n) (x & (~(1U<<(n-1))))
//用宏定义将32位数x的第n位到第m位(bit0为第1位)置位
#define SET_BITS_N_M(x,n,m) (x | (((~0U)>>(32-(m-n+1)))<<(n-1)))
//用宏定义将32位数x的第n位到第m位(bit0为第1位)清零
#define CLEAR_BITS_N_M(x,n,m) (x & (~(((~0U)>>(32-(m-n+1)))<<(n-1))))
//用宏定义获取32位数x的第n位到第m位(bit0为第1位)的部分
#define GET_BITS_N_M(x,n,m) ((x & ~(~(0U)<<(m-n+1))<<(n-1))>>(n-1))
第二课 嵌入式Linux C语言——指针
指针是C语言中广泛使用的一种数据类型,是C语言的灵魂。指针提供了动态操控内存的机制,强化了对数据结构的支持,而且实现了访问硬件的功能。学习指针是学习C语言中最重要的一环,能否正确理解和使用指针是我们是否掌握C语言的一个标志。
一、指针的概念
在计算机中,所有的数据都是存放在内存中的,一般把内存中的一个字节称为一个内存单元,不同的数据类型所占用的内存单元数不一样,如int占用4个字 节,char占用1个字节。为了正确地访问内存单元,必须为每个内存单元编上号。每个内存单元的编号是唯一的,根据编号可以准确地找到该内存单元。内存单元的编号叫做地址(Address),也称为指针(Pointer)。因此理解指针的关键在于理解C程序如何管理内存。
内存单元的指针和内存单元的内容是两个不同的概念。 可以用一个通俗的例子来说明它们之间的关系。我们每个人都有一个编号唯一的身份证账户,身份证存储了我们每个人的身份信息,身份证号码就是账户的指针, 身份信息是账户的内容。对于一个内存单元来说,单元的地址(编号)即为指针,其中存放的数据才是该单元的内容。
在C语言中,存放指针的变量称为指针变量。一个指针变量的值就是某个内存单元的地址或称为某内存单元的指针。
设有字符变量c,其内容为 'K'(ASCII码为十进制数 75),c占用了0X11A号内存单元(地址通常用十六进数表示)。设有指针变量p,内容为 0X11A,这种情况我们称为p指向变量c,或说p是指向变量c的指针。
二、C语言指针类型分析
1、C语言常见指针类型分析
int p; 这是一个普通的整型变量
int *p;
首先从P处开始,先与*结合,所以说明P是一个指针,然后再与int结合,说明指针所指向的内容的类型为int 型.所以 P是一个返回整型数据的指针
int p[3];
首先从P处开始,先与[]结合,说明P 是一个数组,然后与int结合,说明数组里的元素是整型的,所以 P是一个由整型数据组成的数组
int *p[3];
首先从P处开始,先与[]结合,因为其优先级比*高,所以P是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与 int结合,说明指针所指向的内容的类型是整型的,所以是一个由返回整型数据的指针所组成的数组
int (*p)[3];
首先从P处开始,先与*结合,说明P是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以P是一个指向由整型数据组成的数组的指针
int **p;
首先从 P开始,先与*结合,说明P是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与 int结合,说明该指针所指向的元素是整型数据. 所以P是一个返回指向整型数据的指针的指针
int p(int);
从P处起,先与()结合,说明P是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数然后再与外面的int 结合,说明函数的返回值是一个整型数据.所以P是一个有整型参数且返回类型为整型的函数
int (*p)(int);
从P处开始,先与指针结合,说明P是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P是一个指向有一个整型参数且返回类型为整型的函数的指针
int *(*p(int))[3];
从 P开始,先与()结合,说明P是一个函数,然后进入()里面,与int结合,说明函数有一个整型变量参数,然后再与外面的*结合,说明函数返回的是一个指针,,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与*结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P是一个参数为一个整数且返回一个指向由整型指针变量组成的数组的指针变量的函数。
2、指针分析
指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。 要搞清一个指针需要搞清指针的四方面的内容:指针的类型、指针所指向的类型、指针的值或者叫指针所指向的内存区、指针本身所占据的内存区。
A、指针的类型
把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型
B、指针所指向的类型
把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型(在指针的算术运算中,指针所指向的类型有很大的作用)
C、指针所指向的内存区
指针变量定义之后必须赋值才能使用,指针所指向的内存区从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。一个变量指针指向了某块内存区域,就相当于说该指针变量的值是这块内存区域的首地址。如果指针变量指向的内存区未初始化,则该内存区保存的是垃圾数据。如同变量定义之后不赋值就会是垃圾值一样,指针定义后也必须赋值指定合法内存地址,否则解引用将会出错(段错误)
D、指针本身所占据的内存区
用函数sizeof(指针的类型)可以测出指针本身所占据的内存区(在 32位平台里,指针本身占据了 4个字节的长度) 。
3、指针的强制类型转换
当我们初始化一个指针或给一个指针赋值时,赋值号的左边是一个指针,赋值号的右边是一个指针表达式,这就要求两边的类型一致,所指向的类型也一致,如果不一致的话,需要进行强制类型转换。语法格式是:(TYPE *)p。
这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE *,它指向的类型是TYPE,它指向的地址就是原指针指向的地址。要注意的是,原来的指针p的一切属性都没有被修改。
另外,一个函数如果使用了指针作为形参, 那么在函数调用语句的实参和形参的结合过程中,也必须保证类型一致 ,否则需要强制转换。
4、const与指针
const int *p;指针p为变量,指针指向的内存空间存储的是常量
int const *p;指针p为变量,指针指向的内存空间存储的是常量
int * cosnt p;指针是常量,指针指向的内存空间存储的是变量
const int * const p;指针是常量,指针指向的内存空间存储的是常量
5、野指针
野指针是指向的地址不可预知的指针。野指针容易触发运行时段错误。一般来说,指针定义后如果没有赋值就使用就是一个野指针,指向的内存地址空间是不确定的。避免野指针的一般做法:
A、定义指针时初始化为NULL
B、解引用指针前将指针赋值
C、解引用指针前检查指针是否是NULL
D、指针使用完毕后赋值NULL
int a;
int *p = NULL;
p = &a;
if(NULL != p)
{
*p = 3;
}
p = NULL;
三、指针变量的赋值
指针变量同普通变量一样,使用之前不仅要定义说明, 而且必须赋予具体的值。未经赋值的指针变量不能使用, 否则将造成系统混乱,甚至死机。指针变量的赋值只能赋予地址,决不能赋予任何其它数据,否则将引起错误。在C语言中, 变量的地址是由编译系统分配的,对用户完全透明,用户不知道变量的具体地址。 C语言中提供了地址运算符&来表示变量的地址。其一般形式为: & 变量名; 如&a变示变量a的地址,&b表示变量b的地址。 变量本身必须预先说明。指针变量的赋值方式有两种:
1、指针变量初始化的方法
int a;
int *p = &a;
2、赋值语句的方法
int a;
int *p;
p = &a;
不允许把一个数赋予指针变量,故下面的赋值是错误的: int *p;p=1000; 被赋值的指针变量前不能再加“*”说明符,如写为*p=&a 也是错误的
四、指针变量的运算
指针变量可以进行某些运算,但其运算的种类是有限的。 它只能进行赋值运算和部分算术运算及关系运算。
1、指针运算符
A、取地址运算符&
取地址运算符&是单目运算符,其结合性为自右至左,其功能是取变量的地址。&a的运算结果是一个指针,指针的类型是a的类型加个*,指针所指向的类型是a的类型,指针所指向的地址嘛,那就是a的地址。
B、取内容运算符*
取内容运算符*是单目运算符,其结合性为自右至左,用来表示指针变量所指的变量。在*运算符之后跟的变量必须是指针变量。需要注意的是指针运算符*和指针变量说明中的指针说明符* 不是一回事。在指针变量说明中,“*”是类型说明符,表示其后的变量是指针类型。而表达式中出现的“*”则是一个运算符,用以表示指针变量所指的变量。
2、指针变量的运算
A、赋值运算
指针变量的赋值运算有以下几种形式:
指针变量初始化赋值
把一个变量的地址赋予指向相同数据类型的指针变量
int a,*pa;
pa=&a; /*把整型变量a的地址赋予整型指针变量pa*/
把一个指针变量的值赋予指向相同类型变量的另一个指针变量
int a,*pa=&a,*pb;
pb=pa; /*把a的地址赋予指针变量pb*/
把数组的首地址赋予指向数组的指针变量
int a[5],*pa;
pa=a; (数组名表示数组的首地址,故可赋予指向数组的指针变量pa)
把字符串的首地址赋予指向字符类型的指针变量。
char *pc;pc="c language";
char *pc="C Language";
把函数的入口地址赋予指向函数的指针变量
int (*pf)();
pf=f; /*f为函数名*/
B、加减算术运算
指针变量加或减一个整数n的意义是把指针指向的当前位置(指向某数组元素)向前或向后移动n个位置。应该注意,数组指针变量向前或向后移动一个位置和地址 加1或减1 在概念上是不同的。因为数组可以有不同的类型, 各种类型的数组元素所占的字节长度是不同的。如指针变量加1,即向后移动1 个位置表示指针变量指向下一个数据元素的首地址。而不是在原地址基础上加1。
int a[5],*pa;
pa=a; /*pa指向数组a,也是指向a[0]*/
pa=pa+2; /*pa指向a[2],即pa的值为&pa[2]*/ 指针变量的加减运算只能对数组指针变量进行, 对指向其它类型变量的指针变量作加减运算是毫无意义的。
C、指针变量之间的运算
两个指针变量之间的运算只有指向同一数组的两个指针变量之间才能进行运算, 否则运算毫无意义。
两指针变量相减
两指针变量相减所得之差是两个指针所指数组元素之间相差的元素个数。实际上是两个指针值(地址) 相减之差再除以该数组元素的长度(字节数)。例如pf1和pf2 是指向同一浮点数组的两个指针变量,设pf1的值为2010H,pf2的值为2000H,而浮点数组每个元素占4个字节,所以pf1-pf2的结果为 (2000H-2010H)/4=4,表示pf1和 pf2之间相差4个元素。两个指针变量不能进行加法运算。 例如, pf1+pf2是什么意思呢?毫无实际意义。
两指针变量进行关系运算
指向同一数组的两指针变量进行关系运算可表示它们所指数组元素之间的关系。例如:
pf1==pf2表示pf1和pf2指向同一数组元素
pf1>pf2表示pf1处于高地址位置
pf1<pf2表示pf2处于低地址位置
指针变量还可以与0比较。设p为指针变量,则p==0表明p是空指针,它不指向任何变量;p!=0表示p不是空指针。空指针是由对指针变量赋予0值而得到的。例如: #define NULL 0int *p=NULL; 对指针变量赋0值和不赋值是不同的。指针变量未赋值时,可以是任意值,是不能使用的。否则将造成意外错误。而指针变量赋0值后,则可以使用,只是它不指向 具体的变量而已。
本文是学习网络博文并经自己思考总结整理而来,博文来源有:
C语言中文网、C语言指针总结(开源中国-宏哥)、深入理解C指针(Richard Reese)、C语言指针详解(CSDN ad_ad_ad)。
由于网络博文繁杂,无法一一查明原出处,所列来源为本人学习时所查阅资料。
第三课 嵌入式Linux C语言—指针与函数
指针对函数的功能有巨大的贡献,指针能够将数据传递给函数,并且允许函数对数据进行修改。指针对于函数的作用主要有两方面:将指针传递给函数和声明函数指针。
一、程序的栈和堆
程序的栈和堆是C语言程序运行的运行时元素。
1、程序栈
程序栈是支持函数执行的内存区域,通常和堆共享一块内存区域,通常程序栈占据内存区域的下部,堆用内存区域的上部。程序栈存放栈帧,栈帧存放函数参数和局部变量。调用函数时,函数的栈帧被推倒栈上,栈向上长出一个栈帧,当函数终止时,函数的栈帧从程序栈上弹出,栈帧所使用的内存不会被清理,但可能会被推倒程序栈上的另一个函数的栈帧所覆盖。动态分配的内存来自于堆,堆向下生长,随着内存的分配和释放,堆中会布满碎片。尽管堆是向下生长的,但只是一个大体方向,实际内存可能分配在堆上的任意位置。
2、栈帧
栈帧的组成:
A、返回地址
函数完成后要返回的程序内部地址
B、局部数据存储
为局部变量分配的内存
C、参数存储
为函数参数分配的内存
D、栈指针和基指针
运行时系统用来管理栈的指针
栈指针通常指向栈的顶部,基指针(帧指针)通常存在并指向栈帧内部的地址
ARM的栈帧布局如下:
main stack frame为调用函数的栈帧,func1 stack frame为当前函数(被调用者)的栈帧,栈底在高地址,栈向下增长。图中FP就是栈基址,指向函数的栈帧起始地址;SP则是函数的栈指针,指向栈顶的位置。ARM压栈的顺序依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。
二、通过指针传递和返回数据
1、用指针来传递数据
函数中用指针变量作为参数来传递数据可以在函数中修改数据,如果不需要修改数据,则将指针变量限制为const类型。典型的函数应用如下:
char *strcpy(char *dest, const char *src);
如果函数用值传递数据,则在函数中将无法修改数据。
2、返回指针
返回指针需要返回的类型是某种数据类型的指针。
从函数返回指针存在的问题:
A、返回未初始化的指针
B、返回指向无效地址的指针。
C、返回局部变量的指针
D、返回指针但是没有释放内存
从函数返回动态分配的内存,在使用完内存后必须释放,否则会造成内存泄漏。
函数返回局部数据的指针或局部变量是错误的,函数返回后,局部数据所在的栈帧将会被弹出程序栈,保存在栈帧上的数据极易被后续调用函数的栈帧覆盖。通过将局部变量声明为static类型,可以将局部数据、变量的作用域限定在函数内部,但分配在栈外(data数据段),可以避免局部数据、变量被其他函数的栈帧覆盖。
3、传递指针的指针
将指针传递给函数时,传递的是指针变量的值,如果需要修改原指针而不是指针变量的副本,就需要传递指针的指针。
4、函数参数的求值顺序
函数参数的求值顺序依赖于编译器的实现。
GCC编译器
#include <stdio.h>
void fun(int i, int k)
{
printf("%d %d\n", i, k);
}
int main(int argc, char *argv[])
{
int k = 1;
int i = 1;
int j = 1;
int l = 1;
j = j++ + j++;//j = 4
l = ++l + ++l;//l = 6
fun(++k,++k);//3,3
fun(i++,i++);//2,1
return 0;
}
函数参数的入栈顺序
函数调用发生时,参数会被传递给被调用的函数,返回值则返回给函数调用者。
函数调用约定描述了函数参数如何传递到栈以及栈的维护方式。
调用约定用于库调用和库开发的时候。
从右到左依次入栈:__stdcall, __cdecl, __thiscall
从左到右依次入栈:__pascal, __fastcall
当C语言调用其他语言如pascal语言的库函数时需要显示声明调用约定。
5、可变参数的函数
C语言中可以定义参数可变的函数,参数可变函数的实现依赖于stdarg.h头文件。
va_list:参数集合
va_arg:取具体参数值
va_start:标识参数的开始
va_end:标识参数的结束
可变参数必须从头到尾按照顺序逐个访问
参数列表中至少要存在一个确定的命名参数
可变参数函数无法确定实际存在的参数数量
可变参数函数无法确定参数的实际类型,va_arg如果指定了错误的参数类型结构将是不确定的。
#include <stdio.h>
#include <stdarg.h>
float average(int n, ...)
{
va_list args;
int i = 0;
float sum = 0;
va_start(args, n);
for(i=0; i<n; i++)
{
sum += va_arg(args, int);
}
va_end(args);
return sum / n;
}
int main()
{
printf("%f\n", average(5, 1, 2, 3, 4, 5));
printf("%f\n", average(4, 1, 2, 3, 4));
return 0;
}
三、函数指针
函数指针是持有函数地址的指针变量,指向函数地址的指针变量。
1、函数指针的声明
函数指针变量的声明一般格式为;
数据类型 (*指针变量名)();
Void * fun(void);//返回void*类型指针的函数
void (*pFun)(void);//函数指针
void *(*pFun)(void *, void *);//函数指针
typedef void(*Fun)(void);//定义一个函数指针类型Fun
Fun pfun = fun;//将函数fun的地址赋值给函数指针pfun
Fun pfun = &fun;//将函数fun的地址赋值给函数指针pfun
void *(*pf[5])(void);//函数指针数组
2、函数指针的使用
调用函数指针的一般流程:
A、定义函数指针变量
B、被调函数的入口地址(函数名)赋予该函数指针变量
C、用函数指针变量形式调用函数
D、函数指针变量形式调用函数的一般形式为:(*指针变量名) (实参列表)
函数调用和函数指针变量形式调用函数方式如下:
fun();//函数调用
(*fun)();//函数指针变量形式调用函数
(**fun)();//函数指针变量形式调用函数
pfun();//函数指针形式调用函数
(*pfun)();//函数指针形式调用函数
(**pfun)();//函数指针形式调用函数
函数指针变量不能进行算术运算,函数指针的偏移是毫无意义的。函数调用中(*指针变量名)的两边的括号不可少,其中的*不应该理解为求值运算,*只是一种表示符号。
3、传递函数指针
将函数指针变量作为函数的参数进行传递,使程序代码变得更加灵活。
int add(int num1, int num2)
{
return num1 + num2;
}
int sub(int num1, int num2)
{
return num1 - num2;
}
typedef int (*pFun)(int, int);
int compute(pFun operation, int num1, int num2)
{
return operation(num1, num2);
}
compute(add, 5, 6);
compute(sub, 10,2);
add、sub函数的地址被传递给compute函数作为参数,compute使用add、sub函数的地址调用对应的操作。
4、返回函数指针
返回函数指针需要把函数的返回类型声明为函数指针。
int add(int num1, int num2)
{
return num1 + num2;
}
int sub(int num1, int num2)
{
return num1 - num2;
}
typedef int (*pFun)(int, int);
pFun select(char opcode)
{
switch(opcode)
{
case ‘+’:
return add;
case ‘-’:
return sub;
}
}
int evaluate(char opcode, int num1, num2)
{
pFun operation = select(opcode);
return operation(num1, num2);
}
evaluate(‘+’, 5, 6);
evaluate(‘-’, 10, 6);
通过输入一个字符和两个操作数就可以进行相应的计算
5、函数指针数组使用
函数指针数组可以基于某些条件选择要执行的函数,函数指针数组声明如下:
第一种声明:
typedef int (*operation)(int, int);
operation operations[128] = {NULL};
第二种声明:
int (*operations[128]) (int, int) = {NULL};
函数指针数组声明后可以将某一类操作函数赋值给数组。
operations[‘+’] = add;
operations[‘-’] = sub;
int evaluate_array(char opcode, int num1, num2)
{
pFun operation = operations[opcode];
return operation(num1, num2);
}
evaluate_array(‘+’, 6 ,9);
6、函数指针转换
将指向函数的指针变量转换为其它类型的指针变量。无法保证函数指针和数据指针相互转换后正常工作。
四、函数与宏
宏是由预处理器直接展开的,编译器不知道宏的存在,函数是由编译器直接编译的实体,调用行为由编译器决定。多次使用宏会导致可执行程序体积变大,函数是跳转执行的,内存中只有一份函数体存在。宏的效率较高,没有调用开销;函数调用时会记录活动记录,有调用开销。
宏的效率比函数高,但宏是文本替换,参数无法进行类型检查,因此可以使用函数完成的功能绝对不能使用宏,并且宏的定义中不能使用递归定义。
实例代码:
#include <stdio.h>
#include <malloc.h>
#define MALLOC(type, x) (type*)malloc(sizeof(type)*x)
#define FREE(p) (free(p), p=NULL)
#define LOG_INT(i) printf("%s = %d\n", #i, i)
#define LOG_CHAR(c) printf("%s = %c\n", #c, c)
#define LOG_FLOAT(f) printf("%s = %f\n", #f, f)
#define LOG_POINTER(p) printf("%s = %p\n", #p, p)
#define LOG_STRING(s) printf("%s = %s\n", #s, s)
#define FOREACH(i, n) while(1) { int i = 0, l = n; for(i=0; i < l; i++)
#define BEGIN {
#define END } break; }
int main()
{
int* pi = MALLOC(int, 5);
char* str = "D.T.Software";
LOG_STRING(str);
LOG_POINTER(pi);
FOREACH(k, 5)
BEGIN
pi[k] = k + 1;
END
FOREACH(n, 5)
BEGIN
int value = pi[n];
LOG_INT(value);
END
FREE(pi);
LOG_POINTER(pi);
return 0;
}
五、递归函数
递归是数学上一种分而自治的思想。递归需要有边界,当边界条件不满足时,递归继续进行;当边界条件满足时,递归终止。
递归函数是函数体内自我调用的函数,是递归数学思想在程序设计上的应用。递归函数必须有递归出口,函数没有递归出口将导致无限递归,造成程序栈溢出而崩溃。
递归模型的一般表示方法:
1、递归实现求字符串长度函数
int strlen_r(const char* s)
{
if( *s )
{
return 1 + strlen_r(s+1);
}
else
{
return 0;
}
}
2、斐波那契数列解法
斐波那契数列表示如下:1,1,2,3,5,8,13,21,...
int fac(int n)
{
if( n == 1 )
{
return 1;
}
else if( n == 2 )
{
return 1;
}
else
{
return fac(n-1) + fac(n-2);
}
return -1;
}
3、递归实现汉诺塔
汉诺塔问题
将木块借助B柱由A柱移到C柱
每次只能移动一个木块
小木块只能放在大木块之上
解决方法:
将n-1个木块借助C柱由A柱移动到B柱
将最底层的木块直接移动到C柱
将B柱上的n-1歌木块借助A柱由B柱移动到C柱
#include <stdio.h>
void han_move(int n, char a, char b, char c)
{
if( n == 1 )
{
printf("%c --> %c\n", a, c);
}
else
{
han_move(n-1, a, c, b);
han_move(1, a, b, c);
han_move(n-1, b, a, c);
}
}
int main()
{
han_move(3, 'A', 'B', 'C');
return 0;
}
六、函数设计原则
函数设计的一般原则:
A、函数是一个独立功能的模块
B、函数名要在一定程度上反映函数的功能
C、函数参数名要能够体现参数的意义
D、尽量避免在函数中使用全局变量
E、当函数参数不应该在函数内存被修改时,参数应该使用const声明
F、如果参数是指针,且仅用作输入参数,参数应该使用const声明
G、不能省略函数返回值,无返回值应声明位void
H、对参数进行有效性检查,对于指针参数的有效性检查很重要
I、不要返回指向栈内存的指针,栈内存在函数结束时将自动释放
J、函数规模要小,尽量控制在80行内
K、函数避免有过多参数,控制在4个采纳数之内
第四课 嵌入式Linux C语言——指针与数组
数组是C语言内建的数据结构,彻底理解数组及其用法是开发高效应用程序的基础。数组和指针紧密关联,但又不是完全可以互换。
一、数组简介
数组是能用索引访问的同种类型元素的连续集合。数组的元素在内存中是相邻的,中间不存在空隙,数组的元素是相同类型的。
1、数组的解读
数组的定义:int a[10] = {0,1,2,3,4,5};
a[0]:数组的第一个元素,首元素(做左值时表示第0个元素的内存空间)
&a:数组的地址,是常量,不能做左值,类型等同int (*)[10](数组指针)。
&a[0]:数组第0个元素的地址,与数组名a等价
a:a是数组名,不能做左值,做右值时表示数组首元素的地址,与&a[0]相同。
数组的地址与数组首元素的地址不是一个概念。
数组名可以看作const指针,但数组名作为sizeof操作符的参数和&运算符的参数除外,数组名不是指针。
数组本质是一块连续的内存空间。
2、一维数组
一维数组是线性结构,用一个索引访问成员。
int vector[5] = {1,2,3,4,5};
数组的内部表示不包含其元数数量的信息,数组名字只是引用了一块内存。
3、二维数组
二维数组使用行和列来标识数组元素,二维数组可以看作是数组的数组。
int vetor[2][3] = {{1,2,3},{4,5,6}};
二维数组的数组名等价于数组指针int (*vector)[3];
二、数组和指针表示法
1、数组元数的访问方式
A、数组下标访问
数组名[索引下标];
a[i] <==>i[a]
B、指针方式访问
*(指针+偏移量);
*(a + 2);//等价于a[2]
数组中的元数地址是连续的。
C、数组名的指针运算
a + n ==> p + n*sizeof(*a)
&a + 1 ==> &a + sizeof(a)
C语言没有强制规定数组的边界,因此用无效的索引访问数组将会造成数组访问越界,造成不可预期的行为。
2、数组与指针的区别
int vector[5] = {1,2,3,4,5};
int *p = vector;
vector[i]访问数组元素的方式表示从地址vector开始,移动i个位置取出内容
*(vetcor+i)访问数组元素方式表示从vector开始,在地址上增加i,取出地址中的内容。
sizeof(vector)得到的是数组分配的字节数
sizeof(p)得到的是指针变量p的长度
三、数组作为函数参数
1、传递一维数组
将一维数组作为参数传递给函数实际上是通过值来传递数组的地址,不需要传递整个数组,不用再栈上分配空间,通常也需要传递数组长度,确保对数组的访问不会越界。
在函数声明中声明数组的方法有两种:
A、数组表示法
void display(int a[], int size);
通过使用sizeof(a)计算数组的元数数量是错误的,sizeof(a)/sizeof(int)是正确的。
B、指针表示法
void display(int *a, int size);
数组作为函数参数时,数组会退化为指针。
2、传递多维数组
传递多维数组时,需要在函数原型声明中确定用数组表示法还是指针表示法,以及数组的维数和每一维的大小。要想在函数内部使用数组表示法,必须指定数组的形态,否则编译器将无法使用下标。
void display(int a[][5], int rows);
void display(int (*a)[5], int rows);
void display(int *a[5], int rows);//错误声明,语法没错,但是编译器会认为函数传入的数组有5个整型指针。
传递二维以上数组时,除了第一维以外,需要指定其他维度的长度。
二维数组作为函数参数时,二维数组退化为数组指针。
指针数组作为函数参数时,指针数组退化为二维指针。
四、数组指针
1、数组类型
C语言中数组有自己特定的类型,数组的类型由数组元数和数组大小决定。
int array[n]的数组类型为int[n];
C语言中可以使使用typedef对数组类型重命名。
typedef type(name)[size];name为数组类型
代码实例如下:
#include <stdio.h>
typedef int (array)[5];
int main(int argc, char *argv[])
{
array a;
int i;
for(i = 0; i < sizeof(a) / sizeof(int); i++)
{
a[i] = i;
printf("%d\n", a[i]);
}
return 0;
}
2、数组指针
数组指针是指向数组的指针。
int (*p)[5];//数组指针
数组指针是指向数组的指针变量,数组指针持有的是数组的地址,相当于一个二级指针。
int a[5];
p = &a;//&a是数组的地址,与数组指针类型相同
数组指针可以通过数组类型定义,也可以直接定义。
数组类型的指针定义:
typedef int (array)[5];
array *a;
数组指针的直接定义:
type(*array)[n];
数组指针类型的定义:
typedef type(*ArrayPointer)[n];
数组指针的运算:
array + 1 ===> array + sizeof(*array) ==>array + sizeof(int[5])
代码实例:
#include <stdio.h>
typedef int (*ArrayPointer)[5];
int main(int argc, char *argv[])
{
ArrayPointer pa;
int (*p)[5];
int a[5] = {1,2,3,4,5};
pa = &a;
p = &a;
int i;
for(i = 0; i < sizeof(a) / sizeof(int); i++)
{
printf("%d\n", a[i]);
(*pa)[i] = 10;
printf("%d\n", a[i]);
(*p)[i] = i+1;
printf("%d\n", a[i]);
}
return 0;
}
3、指针数组
指针数组是元素类型为指针的数组,指针数组的定义如下:
type * array[n];
数组array中有n个元素,每个元素存储type *指针。
int *vector[5];//指针数组,数组中元素的是指针变量
代码实例:
#include <stdio.h>
int main(int argc, char *argv[])
{
const char *keyword[] =
{
"do",
"for",
"return",
"while",
NULL
};
unsigned int i = 0;
while(keyword[i] != NULL)
{
printf("%s\n", keyword[i++]);
}
return 0;
}
指针数组与数组指针的内存分配如下图:
第五课 嵌入式 Linux C语言——指针与字符串
一、字符串简介
1、字符串声明
声明字符串的方式有三种:字面量、字符数组、字符指针。
字符串字面量是用双引号引起来的字符序列,常用来进行初始化,位于字符串字面量池中,字符字面量是用单引号引起来的字符。
字符串字面量池是程序分配的一块内存区域,用来保存组成字符串的字符序列。多次用到一个字符串字面量时,字符串字面量池中通常只保存一份副本,一般来说字符串字面量分配在只读内存中,是不可变的,但是当把编译器有关字面量池的选项关闭时,字符串字面量可能生成多个副本,每个副本拥有自己的地址。
GCC编译器中字符串字面量是可以改变的,为了将字符串指针变量声明为常量可以用const修饰。
字符串是以ASCII字符NUL结尾的字符序列。字符串通常存储在数组或者从堆上分配的内存中。并非所有的字符数组都是字符串,字符数组可能没有NUL字符。
字符串的长度是字符串中除了NUL字符之外的字符数。
字符常量是单引号引起来的字符序列,通常由一个字符组成。字符的长度是1个字节,字符字面量的长度是4个字节。sizeof(char)= 1,sizeof(‘a’) = 4。
字符数组是一个数组,每个元素的值都可以改变。而字符串指针指向的是一个常量字符串,它被存放在程序的静态数据区,一旦定义就不能改变。这是最重要的区别。
2、字符串初始化
初始化字符串采用的方法取决于变量是被声明位字符数组还是字符指针,为字符串分配的内存要么是数组要么是指针指向的一块内存。
初始化字符数组:
char buffer[] = “hello world”;//字符串的长度为11,字面量需要12个字节
char buffer[12];
strcpy(buffer, “hello world”);
初始化字符指针:
char *buffer = (char *)malloc(strlen(“hello world”) + 1);
strcpy(buffer, “hello world”);
二、传递字符串
函数中经常将参数声明为字符指针。
1、传递简单字符串
直接传递字符串字面量:
char buffer[12];
strcpy(buffer, “hello world”);
传递字符数组:
char src[] = “hello world”;//字符串的长度为11,字面量需要12个字节
char dest[12];
strcpy(dest, src);
为了避免传入的字符串被修改,可以将传递的形参声明为const。
const char src[] = “hello world”;//字符串的长度为11,字面量需要12个字节
char dest[12];
strcpy(dest, src);//char *strcpy(char *dest, const char *src);
2、传递需要初始化的字符串
函数返回需要函数初始化的字符串,因此需要向函数传递缓冲区。
A、必须传递缓冲区的地址和长度
B、调用者负责释放缓冲区
C、函数通常返回缓冲区的指针
char pfun(char *buffer, int size)
{
xxxxx;
return buffer;
}
3、给应用程序传递参数
int main(int argc, char **argv)
{}
通过应用程序的入口函数main可以像应用程序传递参数
三、返回字符串
函数返回字符串时返回的实际是字符串的地址,返回的地址必须是合法的。
1、返回字面量的地址
Char *returnstring(int code)
{
xxx;
return “hello world”;
}
2、返回动态分配内存的地址
在函数内从堆上动态分配字符串的内存,返回地址。
char *blanks(int num)
{
char *buffer = (char *)malloc(num + 1);
strcpy(buffer, “hello world”);
xxx;
retrurn buffer;
}
使用完成后,函数调用者必须释放内存,否则会造成内存泄漏。
3、返回局部字符串的地址
返回局部字符串的地址是不可取的,局部字符串所在的内存会被其他栈帧覆盖。
char *blanks(void)
{
char buffer[] = "hello world";
return buffer;
}
第六课 嵌入式Linux C语言——内存字节对齐
一、内存字节对齐简介
1、内存字节对齐
计算机中内存空间都是按照字节划分的,从理论上讲对任何类型的变量的访问可以从任何地址开始,但是在程序实际编译过程中,编译器会对数据类型在编译过程中进行优化对齐,编译器会将各种类型数据按照一定的规则在空间上排列,而不是顺序的排放,这就是内存字节对齐。
2、内存字节对齐原因
不同硬件平台对存储空间的处理是不同的。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如某些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据,这样数据读取效率就会很差。
二、内存字节对齐的规则
1、数据类型自然边界对齐
各种数据类型的自然边界对齐值如下:
数据类型 | 对齐值 |
char | 1 |
short | 2 |
int | 4 |
long | 4(8,64位) |
float | 4 |
double | 8 |
void * | 4(8,64位) |
2、结构体、类的自身对齐
为结构体分配内存时,分配的内存大小至少是各个字段的长度和。通常,分配的结构体的长度会大于结构体各个字段的长度和,因为结构体需要对齐,即结构体各字段之间需要填充。
缺省情况下,编译器为结构体的每个成员按其自然边界对齐方式分配空间,按照每个成员被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同。结构体整体的默认字节对齐值是结构体中的所有成员中对齐参数最大的一个的值,结构体长度的计算必须取所用过的所有对齐参数的整数倍。
结构体中成员的对齐方式取决于指定对齐方式对齐值和成员自身自然边界对齐值两者中较小的值。
结构体整体对齐方式取决于结构体中所有成员的自然边界对齐值的最大值和指定对齐值两者中最小的值。
3、编译器指定对齐
内存字节对齐是GCC编译器对C语言进行的扩展。在缺省情况下,C编译器为每一个变量或是数据单元按其自然边界对齐条件分配空间。同时GCC编译器规定了两种内存字节对齐的方法:
A、伪指令方式
#pragma pack(n) //n的取值可以为1、2、4、8,在编译过程中按照n个字节对齐
#pragma pack() //取消指定对齐,按照编译器的优化对齐方式对齐
GCC编译器不建议使用#pragma进行内存字节对齐,而且#pragma最多只能支持8字节的对齐。如果n大于8则编译器会报警告,编译器将会按8字节对齐。
warning: alignment must be a small power of two, not 9 [-Wpragmas]
B、属性设置方式
__attribute__ ((packed)); //取消结构在编译过程中的优化对齐。
__attribute__ ((aligned (n))); //让所作用的结构体、类的成员对齐在n字节自然边界上,如果结构中有成员的长度大于n,则按照机器字长来对齐。n=1,2,4,8,16…
GCC推荐使用方式
C、两种对齐方式的区别
#pragma pack(n) 对齐方式告诉编译器结构体或类内部的成员变量相对于第一个变量的地址的偏移量的对齐方式,缺省情况下,编译器按照自然边界对齐,当变量所需的自然对齐边界比n大时,按照n对齐,当变量所需的自然对齐边界比n小时,按照自然边界对齐。n最大只能为8。
__attribute__((aligned(m)))对齐方式告诉编译器一个结构体或者类或者联合或者类型的变量(对象)分配地址空间时的地址对齐方式。如果__attribute__((aligned(m)))作用于一个类型,那么该类型的变量在分配地址空间时,其存放的地址一定按照m字节对齐(m必须是2的幂次方);如果类型中的成员的自然边界对齐值大于m,则按照机器字长对齐。类型占用的空间,即大小,是m的整数倍,以保证在申请连续存储空间的时候,每一个元素的地址也是按照m字节对齐。
实例对比如下:
struct test{
char a;
int b;
short c;
char *p;
double d;
}__attribute__((aligned(4)));//实际有效指定对齐值为8字节(机器字长)
sizeof(struct test);//32字节
#pragma pack(4)
struct test{
char a;
int b;
short c;
char *p;
double d;
};//指定对齐值小于结构体中成员的自然边界对齐值8字节,有效对齐值为4字节
#pragma pack()
sizeof(struct test);//28字节
三、内存字节对齐实例
环境:Ubuntu x64,GCC 4.6.3
代码示例:
1字节对齐:
#pragma pack(1)
struct test{
char a;
int b;
short c;
};
#pragma pack()
sizeof(struct test);//7字节
结构体成员中数据类型对齐的最大值为4字节,指定对齐值为1字节,结构体整体对齐值为1字节
2字节对齐:
#pragma pack(2)
struct test{
char a;
int b;
short c;
};
#pragma pack()
sizeof(struct test);//8字节
结构体成员中数据类型对齐的最大值为4字节,指定对齐值为2字节,结构体整体对齐值为2字节
4字节对齐:
#pragma pack(4)
struct test{
char a;
int b;
short c;
};
#pragma pack()
sizeof(struct test);//12字节
结构体成员中数据类型对齐的最大值为4字节,指定对齐值为4字节,结构体整体对齐值为4字节
8字节对齐:
#pragma pack(8)
struct test{
char a;
int b;
short c;
};
#pragma pack()
sizeof(struct test);//12字节
结构体成员中数据类型对齐的最大值为4字节,指定对齐值为8字节,结构体整体对齐值为4字节
#pragma pack(12)
struct test{
char a;
int b;
short c;
double d;
};
sizeof(struct test);//24字节
结构体成员中数据类型对齐的最大值为8字节,指定对齐值为8字节,结构体整体对齐值为8字节
struct test{
char a;
int b;
short c;
double d;
}__attribute__((aligned(2)));
sizeof(struct test);//24字节
结构体成员中数据类型对齐的最大值为8字节,有效指定对齐值为8字节(指定对齐值为2字节,小于结构体中成员的最大自然边界对齐值8字节,对齐值为机器字长8字节),结构体整体对齐值为8字节
struct test{
char a;
int b;
short c;
char *p;
double d;
}__attribute__((aligned(2)));
sizeof(struct test);//32字节
结构体成员中数据类型对齐的最大值为8字节,有效指定对齐值为8字节(指定对齐值为2字节,小于结构体中成员的最大自然边界对齐值8字节,对齐值为机器字长8字节),结构体整体对齐值为8字节
struct test{
char a;
int b;
short c;
char vector[10];
char *p;
double d;
}__attribute__((aligned(16)));
sizeof(struct test);//48字节
四、内存字节对齐的应用场合
在设计不同CPU下的通信协议时,或者编写硬件驱动程序时寄存器的结构都需要按一字节对齐。
五、ARM平台的内存字节对齐
在ARM中,有ARM和Thumb两种指令。
ARM指令:每执行一条指令,PC的值加4个字节(32bits)。一次访问4字节内容,该字节的起始地址必须是4字节对齐的位置上,即地址的低两位为bits[0b00],也就是说地址必须是4的倍数。
Thumb指令:每执行一条指令,PC的值加2个字节(16bits)。一次访问2字节内容,该字节的起始地址必须是2字节对齐的位置上,即地址的低两位为bits[0b0],也就是说地址必须是2的倍数。
ARM汇编程序设计中的字节对齐
.align n
.align的作用在于对指令或者数据的存放地址进行对齐,有些CPU架构要求固定的指令长度并且存放地址相对于2的幂指数圆整,否则程序无法正常运行
ARM汇编器并不直接使用.align提供的参数作为对齐目标,而是使用2^n的值,比如这里的参数为4,那么圆整对象为2^4 = 16。缺省为4。
参考博文:
内存中的字节对齐(CSDN susan19890313)
第七课 嵌入式 Linux C语言——结构体
一、结构体简介
1、结构体定义
结构体定义一般有两种方法较为常用:
第一种方法:
struct person{
char *name;
unisgned int age;
};
第二种方法:
typedef struct person{
char *name;
unsigned int age;
}Person;
person实例声明如下:
Person person;//声明一个person对象
Person *ptrPerson = (Person*)malloc(sizeof(Person));//声明一个person对象,并分配内存
2、结构体的初始化
使用结构体声明的结构体对象的初始化:
Person person;
person.name = (char *)malloc(strlen(“socprio”) + 1);
strcpy(person.name, “scorpio”);
person.age = 30;
使用结构体指针声明的结构体的初始化:
Person *ptrPerson;
prtPerson = (Person *)malloc(sizeof(Person));
ptrPerson->name = (char *)malloc(strlen(“scorpio”) + 1);
strcpy(ptrPerson->name, “scorpio”);
prtPerson->age = 30;
3、结构体释放的问题
定义结构体时会为结构体分配内存,但运行时系统不会自动为结构体内部的指针分配内存,结构体销毁时,运行时系统也不会自动释放结构体内部的指针指向的内存。结构体内部的指针指向内存一般为动态分配的内存,使用完成后需要释放,否则将会造成内存泄漏。
4、动态分配内存开销的避免
重复分配然后释放结构体会产生开销,可能会导致巨大的性能瓶颈。解决这个问题的移植方法是为分配的结构体单独维护一个表。当用户不需要某个结构体实例时,将其返回结构体池中。当需要某个结构体实例时,从结构体池中获取一个对象。如果结构体池中没有可用的结构体,就会自动动态分配一个结构体实例。
二、结构体宏
1、offsetof宏
offsetof根据结构体成员的类型和成员名来计算该成员距结构体首地址的偏移量。宏定义如下:
#define offsetof(type, member) ((size_t)(&((type *)0)->member))
( (TYPE *)0 ): 0地址强制 "转换" 为 TYPE结构类型的指针
((TYPE *)0)->MEMBER :访问TYPE结构中的MEMBER数据成员
&( ( (TYPE *)0 )->MEMBER):取出TYPE结构中的数据成员MEMBER的地址
(size_t)(&(((TYPE*)0)->MEMBER)):结果转换为size_t类型
offsetof宏首先将0转换为结构体指针类型,然后引用成员变量并取其地址。由于结构体首地址为0,所以成员变量的地址即为成员距结构体首地址的偏移量。
现代编译器中常用
#define offsetof(type, member) __builtin_offsetof(type, member)
2、container_of宏
container_of根据成员地址、结构体类型和成员名来计算结构体的首地址,其定义如下:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
创建一个结构体,用offsetof和container_of获取结构体的地址和内部成员相对于结构体地址的偏移量。
代码实例:
#include <stdio.h>
#define offsetof(type, member) ((size_t)&((type *)0)->member)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
struct test{
char a;
int b;
short c;
char *p;
double d;
}__attribute__((aligned(4)));
int main(int argc, char **argv)
{
struct test s;
printf("offset a=%lu\n",offsetof(struct test,a));
printf("offset b=%lu\n",offsetof(struct test,b));
printf("offset c=%lu\n",offsetof(struct test,c));
printf("offset p=%lu\n",offsetof(struct test,p));
printf("offset d=%lu\n",offsetof(struct test,d));
printf("s=%p\n",container_of(&s.a,struct test,a));
printf("s=%p\n",container_of(&s.p,struct test,p));
return 0;
}
运行结果:
offset a=0
offset b=4
offset c=8
offset p=16
offset d=24
s=0x7fffc8590cf0
s=0x7fffc8590cf0
第八课 嵌入式 Linux C语言——存储类型、作用域、生命周期、链接属性
一、存储类型
C语言中,每个变量和函数都有两个属性:数据类型和数据的存储类型。
变量的存储类型是指存储变量值的内存类型。变量的存储类型决定变量何时创建、何时销毁以及它的值将保持多久。计算机中有三个地方可以用于存储变量:普通内存,运行时堆和栈,硬件寄存器。变量的存储类型取决于声明变量的位置。
C语言存储类别说明符:
说明符 | 用 法 |
auto | 只在代码块内变量声明中被允许, 表示变量具有本地生存期 |
extern | 出现在顶层或块的外部,函数与变量声明中,表示声明的对象具有静态生存期, 链接器知道其名字 |
static | 可以放在函数与变量声明中,在函数定义时,只用于指定函数名,而不将函数导出到链接器,在函数声明中,表示其后边会有定义声明的函数。在数据声明中,总是表示定义的声明不导出到链接器 |
register | 要使变量尽量存储于寄存器,只能声明自动变量 |
1、静态变量
在代码块之外声明的变量存储于静态内存中,不属于堆和栈的内存,这类变量称为静态(static)变量。静态变量在程序运行之前创建,是在将可执行文件加载到内存的时候创建,其在程序的整个执行期间始终存在。
extern:用于声明全局变量、函数,主要用于在一个文件中定义全局变量、函数,而在另一个文件中引用全局变量、函数。一般来说,函数声明一般放在一个头文件中,供其他文件引用。
static:两种用法含义截然不同
A、修饰局部变量,静态局部变量。静态局部变量和非静态局部变量区别在于存储类不同。非静态局部变量存储在栈上,静态局部变量分配在数据段或bss段静态内存中。静态局部变量的生命周期和全局变量相同,但作用域和链接属性不同,静态局部变量的作用域为代码块作用域,链接属性为无链接;全局变量的作用域为文件作用域,链接属性为外链接。
B、修饰全局变量,静态全局变量。静态全局变量和非静态全局变量的区别在于链接属性不同,静态全局变量为内链接,非静态全局变量为外链接。
2、自动变量
在代码块内部声明的变量的缺省存储类型是自动的 (automatic),存储于栈中,称为自动变量。关键字auto就是用于修饰这种存储类型的,代码块中的变量缺省情况下就是自动变量。在程序执行到声明自动变量的代码块时,自动变量才被创建,当程序的执行流离开代码块时,代码块内创建的自动变量便自行销毁。在代码块内部声明的变量,如果给它加上static,可以使它的存储类型从自动变为静态变量。但是,修改变量的存储类型并不表示修改改变量的作用域,变量的作用域仍然是代码块内部。函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数。
函数的形式参数不能声明为静态,因为函数的参数传递是通过堆栈进行的,用于支持递归。
auto:
修饰局部变量表示自动局部变量,自动局部变量分配在栈上,默认定义的普通局部变量。
3、寄存器存储
要使变量存储于寄存器声明自动变量时需要关键字:register。
register:
register修饰的变量编译器会尽量分配在寄存器上,不保证一定会分配在寄存器中,一般的变量分配在内存中。register修饰的变量读写效率较高,用于高频次访问的变量。
C语言程序运行时有一定要求,C语言程序无法直接在内存中运行,需要外部一定的协助,协助的代码叫加载运行代码,主要作用是给全局变量赋值、清除bss段。裸机程序开发中需要。
二、作用域
作用域是指允许对标识符进行访问的位置范围。
C99规定,C语言的作用域共有 4 种类型:文件作用域、代码块作用域、函数作用域、函数原型作用域。编译器通过变量声明的位置来确定作用域。
类型 | 位置 | 说明 |
文件作用域 (file) | 在所有代码块和参数列表之外 | 整个文件内都可以访问 |
代码块作用域 ( block) | 在“代码块”或者“函数的参数列表”内部 | 只有所在的代码块内可以访问 |
函数作用域 (function) | 函数体内 | 具有此作用域的只有一种语句:只有goto语句要使用的“语句标签”。简化为一条规则:一个函数中的语句标签(label)必须唯一。 |
函数原型作用域 (function prototype) | 声明的函数原型的参数列表中(注意与“函数定义”不同) | 由于函数原型的参数名称可以省略,即使不省略,也不要求和“函数定义”中的形参列表中名称相同。 |
1、文件作用域
代码块之外声明的标识符具有文件作用域,文件作用域的范围是从标识符声明处一直到文件的结束。如果声明在头文件中,并且头文件被其他文件用#include 所包含,标识符的作用域也会相应的扩大到包含文件的结束。
2、代码块作用域
一对花括号之间的所有语句称为代码块作用域。在代码块开始位置声明的标识符具有代码块作用域。在一个代码块作用域开始定义的变量可以被该代码块内的所有语句使用。
如果代码块之间有嵌套,那么内层代码块的标识符就会把外层代码块的同名标识符掩藏,对内层代码块标识符的修改不会影响外层代码块的同名标识符;内层代码块可以使用外层代码块的标识符。
对于非嵌套的两个代码块,两个代码块之间没有交集,那么一个代码块内的语句不能使用另一个代码块内的变量。
函数定义中的参数是代码块作用域。
3、函数作用域
只适用于goto语句的语句标签。函数作用域只适用于语句标签,语句标签用于goto语句。一个函数作用域内的语句标签必须唯一
4、函数原型作用域
在函数原型中声明的参数名具有函数原型作用域。
5、标识符的命名空间
命名空间是为了解决在相同作用域内如何区分相同的标识符。
A、只有在相同作用域的情况下才能使用到命名空间去区分标识符,在嵌套的作用域、不同的作用域区分标识符都不会用到命名空间的概念。
B、在相同的作用域内,如果命名空间不同,标识符可以使用相同的名称。否则,即如果命名空间不同,编译器会报错,提示重复定义。
C99规定C语言命名空间可以分为四种:
A、所有的标签(label)都属于同一个命名空间。
在同一个函数内,标签不能相同。
在同一个函数内,标签可以和其他变量名称相同。因为它们所属的命名空间不同。
B、struct、enum和union的名称属于同一个命名空间
C99中将struct、enum和union的名称称之为tag,所有的tag属于同一个命名空间。 也就是说,如果你已经声明struct A { int a }; 就不能在声明 union A{ int a };
C、struct和union的成员各自属于一个命名空间,而且是相互独立的
例如:如果你已经声明struct A { int a }; 其成员的名称为a,你仍然可以声明 struct B{ int a };或者union B{ int a };
struct和union的成员各自成为一个命名空间,是因为它们的成员访问时,需要通过 "."或"->"运算符,而不会单独使用,所以编译器可以将它们与其他的标识符区分开。由于枚举类型enum的成员可以单独使用,所以枚举类型的成员不在这一名称空间内。
D、其他所有的标识符,属于同一个名称空间。
包括变量名、函数名、函数参数,宏定义、typedef的类型名、enum的成员 等等。
6、重名标识符的处理
如果标识符出现重名的情况,宏定义覆盖所有其它标识符,这是因为它在预处理阶段而不是编译阶段处理。除了宏定义之外其它类别的标识符,处理规则是:内层作用域会隐藏掉外层作用域的标识符。
同名变量的掩蔽规则:
两个同名变量的作用域没有重叠,则两个同名变量互不影响。
两个重名变量的作用域有重叠,则作用域小的变量掩蔽掉作用域大的变量,遵循就近原则。
C89标准的编译器中,所有的局部变量必须先定义在函数的前面,在C99标准的编译器中可以允许在代码块内任意位置定义局部变量,但也必须先定义再使用。
三、生命周期
变量的生命周期是指程序运行期间,变量从分配到地址到地址被释放的过程。根据变量的存储类型可以将变量的生命周期分为:静态生存期、自动生存期、动态分配生存期。
1、静态生存期
属于文件作用域(即external或internal链接属性)、以及被static修饰的变量,具有static静态生存期。静态生存期的变量存储在静态内存中。静态存储的变量,在程序运行之前就已经创建,在程序整个执行期间一直存在,如果声明时没有被显式的初始化,就会被自动初始化为0。
静态变量当然是属于静态存储方式,但是属于静态存储方式的变量不一定就是静态变量,例如外部变量虽属于静态存储方式,但不一定是静态变量,必须由static加以定义后才能成为静态变量。
const常量、字符串常量存储在代码段或只读数据段,取决于平台。
2、自动生存期
链接属性为none,并且没有static修饰的变量,具有automatic自动生存期。
自动生存期的变量存储于栈或寄存器中。其中在代码块内部声明的变量,在C语言书籍中也被称为“自动变量”,使用auto修饰符,默认可以省略。对于自动存储的变量当程序执行到含有自动变量的代码段时,自动变量才被创建,并且不会被自动初始化,代码段执行结束,自动变量就自动销毁,释放掉内存。如果代码段被反复执行,那么自动变量就会反复被创建和销毁。注意这一点和静态变量不同,静态变量只创建一次,到程序结束才销毁。
3、动态分配生存期
使用malloc函数,在进程的堆空间分配内存的变量。
动态分配生存期的变量存储于堆中,也不会被自动初始化,使用free函数释放内存。
四、链接属性
链接属性是为了说明在不同文件中出现的相同标识符应该如何处理。
完整、大型、商业的C语言工程由多个c文件和h文件组成的。编译时将文件编译成.o的二进制文件,通过链接将多个.o文件链接成一个可执行程序。编译时以文件为单位,链接时以工程为单位。
各个源文件被编译后,所有的目标文件和从函数库中引用的函数(一般是归档库文件,*.a类型)经过链接器链接,形成一定格式的可执行程序,如elf格式。如果相同的标示符出现在几个不同个源文件中,他们是否表示同一个实体。这由标示符的链接属(linkage)决定。标示符的作用域和链接属性相关,作用域是由链接属性决定的,但二者并不相同。比如,内部属性的静态全局变量,在代码块内有同样名称的标示符,这时静态变量无效,起作用的是代码块内的局部变量。虽然静态全局变量具有内部链接属性,但当名字冲突时,起作用的是局部变量。
C99规定C语言的链接属性分为三种:external(外部链接), internal(内部链接), none(无链接)。
类型 | 说明 | 默认(即不使用extern和static) |
外部链接external | 同一个标识符,即使在不同的文件中,也表示同一个实体。 | ①具有文件作用域的变量和函数。 |
内部链接internal | 同一个标识符,仅仅在同一个文件中才表示同一个实体。 | 如果不使用static,那么默认没有内部链接属性的标识符。只有被static修饰的具有文件作用域的标识符,才具有internal链接属性 |
无链接none | 表示不同的实体 | 所有其他的标识符。如:函数的参数、代码块作用域的变量、标签等 |
1、外部链接
表示位于不同的源文件的相同标识符表示同一个实体。
链接属性为external的标识符不论声明多少次,位于几个原文件内均表示同一实体。函数和全局变量属于外链接。
2、内部链接
表示只有位于相同源文件的相同标识符表示同一个实体,不同源文件的相同标识符表示不同的实体。
链接属性为internal的标识符在同一个源文件内的所有声明均指向同一个实体,但位于不同源文件的多个声明则分属不同的实体。在单个文件内部进行链接,static修饰的全局变量和函数属于内链接。
3、无链接
链接属性为none的标识符不论在那个文件内都是独立的个体。
没有链接的标示符,总是被当做单独的个体,也就是说该标示符的多个声明被当做不同的实体。所有局部变量属于无链接,宏和inline函数链接属性为无链接。
在文件作用域内声明的变量或是函数,在缺省的条件下链接属性为external,其余的为none。
五、相互关系
标识符的存储类型、作用域、生命周期、链接属性是相互关联的,存储类决定生命周期,链接属性决定作用域。
作用域 | 声明位置 | 链接属性 | 存储类型 | 默认初始化值 | 使用static修饰 |
文件作用域 | 在所有代码块和参数列表之外 | external | 静态存储 | 0 | Internal 静态存储 |
代码块作用域 | 在代码块或函数的参数列表内部 | none | 栈存储 | 形式参数调用时被初始化;代码块内部不自动初始化 | None 静态存储 |
函数作用域 | 函数体内 | --------- | -------- | 标签,不需要初始化 | --------- |
函数原型作用域 | 声明的函数原型的参数列表中(注意与“函数定义”不同) | --------- | -------- | 不需要初始化 |
1、extern关键字
extern用来为一个标识符指定external链接属性。如果使用extern关键字声明某一个变量,说明变量是在别处定义的,可能位于别的文件也可能位于当前文件。
如果一个标识符在声明为extern前已经进行了声明,则链接属性由第一个声明决定(第一个声明的链接属性为external或internal时)。
2、static关键字
对于在代码块内部声明的变量,存储类型为自动变量,作用域为代码块作用域,链接属性为无链接,生命周期为自动生存周期。当static用于代码块内部的变量声明时,会将变量的存储类型从自动变量修改为静态变量,作用域仍然为代码块作用域,生命周期从自动生存期修改为静态生存周期,链接属性仍然为无链接。
对于在代码块外声明的全局变量,存储类型为静态变量,作用域为文件作用域,链接属性为外链接,生命周期为静态生存期。当static用于代码块外部的全局变量声明时,会将变量的链接属性从外链接修改为内链接,存储类型仍然为静态变量,生命周期仍然为静态生存周期,作用域仍然为文件作用域,但作用域将限定为本文件,不能被其它源文件使用。
对于在代码块外的函数定义,作用域为文件作用域,链接属性为外链接。当static用于代码块外的函数声明时,会将函数的链接属性从外链接修改为内链接,作用域仍然为文件作用域,但作用域将限定为本文件,不能被其它源文件使用。
对于在代码块内或代码块外部都有可能出现的情况,例如函数:函数声明的标识符为静态存储的,但是对于其形参是存储于堆栈中的,形参声明的变量作用域为原型作用域。对于存储于堆栈中的变量,即自动变量可以用register关键字使变量存储于机器硬件寄存器中。在代码块内部声明的自动变量可以通过static关键字修改为静态变量。
3、属性修改的一般原则
生命周期、存数类型都是针对变量, 因为在程序运行期间,只有变量才需要分配内存和释放内存,其他的诸如函数等都不需要。
修改变量的存储类型(如用static将自动变量变为静态变量),并不会修改变量的作用域,变量的作用域仍然有其声明的位置决定,
函数的形式参数,如果使用修饰符,只能使用register修饰,表示运行时参数存储在寄存器上。注意:形式参数是不能用auto修饰的。
六、C语言工程中标识符的使用原则
1、函数的定义与声明
在.h文件中声明的函数,如果在其对应的.c文件中有定义,那么我们在声明这个函数时,不使用extern修饰符, 如果反之,则必须显示使用extern修饰符。显示声明表示是引用外部函数,隐式声明是自己声明并定义的函数。
2、全局变量的定义与声明
全局变量应该定义c文件中并且在h文件中声明,不要定义在头文件中。
C语言的所有文件之中,只能有一个定义声明。顶层声明中,存在初始化语句是表示这个声明是定义声明,其他声明是引用声明。
所有引用声明要显示用存储类型关键字extern声明,而每个外部变量的唯一定义声明中省略存储类说明符。
所有全局变量全部以g_开头,并且尽可能声明成static类型。
尽量杜绝跨文件访问全局变量。如果的确需要在多个文件内访问同一变量,应由该变量定义所在文件内提供GET/PUT函数实现.
全局变量必须要有一个初始值,全局变量尽量放在一个专门的函数内初始化.
如调用的函数少于三个,请考虑改为局部变量实现。
数组的引用声明:extern int G_glob[100];extern int G_glob[];
3、模块化编程
为了实现编程的模块化,一般会按功能模块对函数进行封装,封装成为.h和.c文件。
.h文件是头文件,内含函数声明、宏定义、结构体定义等内容。
.cpp文件是程序文件,内含函数实现,变量定义等内容。
为了防止对头文件的重复包含,一般采用宏定义:
#ifndef XXX
#define XXX
函数声明
#endif
参考博文:
C语言中标识符的作用域、命名空间、链接属性、生命周期、存储类型(上) (CSDN daheiantian)
C语言中标识符的作用域、命名空间、链接属性、生命周期、存储类型(下) (CSDN daheiantian)
C_作用域、链接属性和储存类型 (CSDN Gummary)
C语言提高之——C语言中的作用域、链接属性和存储类型 (CSDN 任长江)
作用域+链接属性+存储类型(博客园 graylocus)
第九课 嵌入式 Linux C语言—C语言的安全问题和指针陷阱
C语言是灵活度和自由度较大的编程语言,作为C语言核心的指针更是让C语言程序员可以越过安全的栅栏,对某些内存区域进行破坏性访问,引发安全风险。很多安全问题都能追根溯源到指针的误用。本文将从指针的角度解读C语言常见的安全问题和指针陷阱。
一、指针的声明和初始化
1、不恰当的指针声明
int* ptr1, ptr2;//声明ptr1为int指针,ptr2为整型
int *ptr1, *ptr2;//ptr1,ptr2都声明为指针
#define PINT int *
PINT ptr1, ptr2;//等价于int* ptr1, ptr2;
推荐方式:
typedef int * PINT
PINT ptr1, ptr2;//等价于int *ptr1, *ptr2;
2、使用指针前未初始化
初始化指针前使用指针会导致运行时错误,这种指针称为野指针。
int *p;
printf(“%d\n”, *p);
指针变量p未被赋值指针(地址),此时*p将会导致不可预知的情况。
3、处理未初始化指针
有三种方法可以用来处理未初始化的指针:
A、用NULL初始化指针
int *p = NULL;
if(NULL == p)
{
}
B、用assert函数
assert(NULL != p);
C、用第三方工具
二、误用指针
很多安全问题聚焦于缓冲区溢出,覆写对象边界以外的内存就会导致缓冲区溢出,这块内存可能是本程序的地址空间,也可能是其他进程的,如果是程序地址空间以外的内存,大部分操作系统会发出一段错误然后中止程序。如果缓冲区溢出发生在应用程序的地址空间内,就会导致对数据的未授权访问和控制转移到其他代码段,导致系统被攻陷。下列情况可能导致缓冲区溢出:
A、访问数组元数时没有检查索引项
B、对数组指针做指针算数运算时不够小心
C、用gets这样的函数从标准输入读取字符串
D、误用strcpy和strcat这样的函数
如果缓冲区溢出发生在栈帧的元数上,就可能把栈帧的返回地址部分覆写为对同一时间创建的恶意代码的调用。
1、测试NULL
对于动态分配内存函数一定要检查返回值,否则如果内存分配失败程序可能会非正常终止。
char *vetcor = (char *)malloc(128*sizeof(char));
if(NULL == vector)
{
//Malloc failure.
}
2、错误使用解引操作
声明和初始化指针的常用方法如下:
int num;
int *p = #
但是以下是错误的
int num;
int *p ;
*p = #//正确为p = &num
3、迷途指针
释放指针后却仍然在引用原来的内存,就会产生迷途指针。如果在释放指针后仍然在试图操作原来的内存,读操作可能会返回无效数据,写操作可能会破坏这块内存的值,导致其他正在使用这块内存的程序出现异常。
4、数组访问越界
C语言中数组并没有提供防止访问数组越界的机制,因此必须由程序员保证对数组的访问不越界。尤其是用指针方式访问数组元素时一定要保证不能越过数组边界。
5、错误计算数组长度
将数组传递给函数时,一定要同时传递数组长度。数组长度参数可以避免缓冲区溢出。strcpy函数就是一个允许缓冲区溢出的函数,因此尽量避免使用,使用具有缓冲区保护机制的strncpy函数。
三、释放问题
1、重复释放
重复释放是指将同一块内存释放两次,如:
char *name = (char *)malloc(....);
...................
free(name);
....................
free(name);
为了避免对同一块内存的重复释放,一般释放指针后将指针置为NULL。
char *name = (char *)malloc(....);
...................
free(name);
name = NULL;
2、清除敏感信息
当应用程序终止后,大部分操作系统都不会把用到的内存清零或执行别的操作,系统可能会将之前用过的内存分配给其他程序使用,如果这些内存原来保存的是身份信息、密码信息等敏感数据,这样做就是不安全的,因为其他人可以使用这部分内存,可以通过覆写将内存中敏感数据清空。
四、指针类型转换
指针类型的转换可以实现一些特殊的功能,如:
访问有特殊目的的地址
判断机器的字节序
1、访问特殊用途的地址
在嵌入式系统开发中,很多特殊功能寄存器是统一编址的,通过访问这些特殊功能寄存器可以控制相应的外设的功能。
#define WTCON ( *((unsigned long *)0xE2700000))
WTCON |= 0X3<<8;
通过指针类型的转换,可以WTCON寄存器的某些位设置为1,进而控制外设。
2、判断机器的字节序
字节序是指数据在内存单元中字节的存储顺序。字节序一般分为小字节序和大字节序,也称小端模式和大端模式。小端模式表示整数的4字节的中的低地址存储整数数据的低位。通过将整数的地址从指针转换为char,打印出每个字节的内存就可以知道机器的字节序。
#include <stdio.h>
int main(int argc, char**argv)
{
int num = 0x12345678;
char *p = (char *)#
int i;
for(i = 0; i < 4; i++)
{
printf("%p:%2x\n", p,(unsigned char)*p++);
}
return 0;
}
运行结果如下:
0x7fffe8f13cb9:78
0x7fffe8f13cba:56
0x7fffe8f13cbb:34
0x7fffe8f13cbc:12
结论:低位存储低字节,小端模式。
第十课 嵌入式 Linux C语言——静态库函数和动态库函数
一、静态链接库
静态链接库是obj文件的一个集合,通常静态链接库以".a"为后缀,名字格式一般为libxxx.a,由程序ar生成。静态链接库是在程序编译过程中链接的,已经将调用的相关函数拷贝到程序内部,程序运行时和静态链接库已经没有任何关系。
1、静态链接库的创建
A、编写源码库文件
源码库文件一般包含.c和.h文件,
hello.c文件:
#include <stdio.h>
void display(void)
{
printf("hello world\n");
}
hello.h文件:
#ifndef __HELLO_H
#define __HELLO_H
void display(void);
#endif
B、编译源码库文件
gcc -o hello.o -c hello.c
生成hello.o目标文件
C、将目标文件归档生成静态链接库文件
ar -cr libhello.a hello.o
D、发布静态链接库
一般来说,静态链接库需要发布libxxx.a和.h文件,.h文件可以让第三方开发者了解静态链接库中的各函数的功能和函数声明,libxxx.a文件是第三方开发者在调用静态链接库中的函数后在编译链接阶段链接的库。
2、静态链接库的使用
A、查阅静态链接库的.h文件
获取发布的静态链接库后,查看.h文件,看静态链接库的各个函数功能和函数声明。
B、使用静态链接库的某个函数
使用静态链接库时需要声明静态链接库的.h文件
#include "hello.h"
int main(int argc, char**argv)
{
display();
return 0;
}
C、编译工程文件
编译工程文件时,需要在编译链接时添加相关选项:
-Lpath:表示在path目录中搜索库文件,如-L.则表示在当前目录。
-lxxx:表示要链接的静态链接库为libxxx.a
-static:表示将所有链接的库静态加载
gcc -o main main.c -L. -lhello
二、动态链接库
动态链接库是程序运行时加载的库,当动态链接库正确安装后,所有的程序都可以使用动态库来运行程序。动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊方式形成的。库中函数和变量的地址是相对地址,不是绝对地址,其真实地址在调用动态库的程序加载时形成。
1、动态链接库的创建
A、编写源码库文件
源码库文件一般包含.c和.h文件,
hello.c文件:
#include <stdio.h>
void display(void)
{
printf("hello world\n");
}
hello.h文件:
#ifndef __HELLO_H
#define __HELLO_H
void display(void);
#endif
B、编译源码库文件
gcc -fPIC -c hello.c -o hello.o
生成目标文件hello.o
-fPIC选项的作用是使得gcc生成的代码是位置无关的
C、生成动态链接库
gcc -shared -o libhello.so hello.o
生成动态链接库libhello.so文件
-shared选项告诉编译器生成一个动态链接库
2、动态链接库的使用
A、查阅动态链接库的.h文件
获取发布的动态链接库后,查看.h文件,看动态链接库的各个函数功能和函数声明。
B、使用动态链接库的某个函数
使用动态链接库时需要声明动态链接库的.h文件
#include "hello.h"
int main(int argc, char**argv)
{
display();
return 0;
}
C、编译工程文件
编译工程文件时,需要在编译链接时添加相关选项:
-Lpath:表示在path目录中搜索库文件,如-L.则表示在当前目录。
-lxxx:表示要链接的动态链接库为libxxx.so
gcc -o main main.c -L. -lhello
D、将动态链接库文件注册到系统环境变量中的库加载路径
方法一:将动态链接库文件拷贝到系统环境变量中的库加载路径中的某个目录
cp libhello.so /usr/lib
方法二:将当前目录添加为统环境变量中的库加载路径
把当前工作目录加入动态链接库的搜索路径配置文件/etc/ld.so.conf中。
如果没有以上操作,运行时程序将会报错:
error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory
程序运行时将会到相应目录下加载动态链接库中的函数执行。
E、程序运行时的库依赖
ldd命令可以查询程序运行时需要的依赖库
ldd main
linux-vdso.so.1 => (0x00007fff265d8000)
libhello.so => /usr/lib/libhello.so (0x00007f15d8af1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f15d8733000)
/lib64/ld-linux-x86-64.so.2 (0x00007f15d8d05000)
第十一课 嵌入式 Linux C语言——C语言模块化编程
一、C语言模块化编程
所谓模块化编程,就是指一个程序包含多个源文件(.c 文件和 .h 文件),每个模块即是一个.c文件和一个.h文件的结合,头文件(.h)中是对于该模块接口的声明。C语言模块化编程中对.c、.h文件的潜规则:
1、.c 文件主要负责实现,也就是定义函数;.h 文件主要负责声明,比如函数声明、宏定义等,结构的定义、自定义数据类型一般也放在头文件中,不能在.h文件中定义变量。将一个功能模块的代码单独编写成一个.c文件,然后把该模块的接口函数放在.h文件中。
定义变量和声明变量的区别在于定义会产生内存分配的操作,是汇编阶段的概念;而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。
2、引入编译器自带的头文件(包括标准头文件)用尖括号,引入自定义头文件用双引号,例如:
#include <stdio.h>
#include "myFile.h"
头文件应当是幂等的。也就是说,多次包括头文件的效果和仅包括一次的效果完全相同。
#ifndef _STDIO_H
#define _STDIO_H
/* 声明部分 */
#endif
3、模块提供给其它模块调用的外部函数及数据需在.h中文件中冠以extern关键字声明;模块内的函数和全局变量需在.c文件开头冠以static关键字声明。
二、嵌入式系统开发中的模块化编程
嵌入式系统通常包括两类模块:
(1)硬件驱动模块,一种特定硬件对应一个模块。
(2)软件功能模块,其模块的划分应满足低偶合、高内聚的要求。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差;模块内各元素(语名之间、程序段之间)联系的越紧密,内聚性就越高。
1、单任务系统
所谓"单任务系统"是指该系统不能支持多任务并发操作,宏观串行地执行一个任务。而多任务系统则可以宏观并行(微观上可能串行)地"同时"执行多个任务。
单任务程序典型架构
(1)从CPU复位时的指定地址开始执行;
(2)跳转至汇编代码startup处执行;
(3)跳转至用户主程序main执行,在main中完成:
a.初试化各硬件设备;
b.初始化各软件模块;
c.进入死循环(无限循环),调用各模块的处理函数
用户主程序和各模块的处理函数都以C语言完成。用户主程序最后都进入了一个死循环,其首选方案是:
while(1)
{
}
2、多任务系统
多任务的并发执行通常依赖于一个多任务操作系统(OS),多任务OS的核心是系统调度器,它使用任务控制块(TCB)来管理任务调度功能。TCB包括任 务的当前状态、优先级、要等待的事件或资源、任务程序码的起始地址、初始堆栈指针等信息。调度器在任务被激活时,要用到这些信息。此外,TCB还被用来存 放任务的"上下文"(context)。任务的上下文就是当一个执行中的任务被停止时,所要保存的所有信息。通常,上下文就是计算机当前的状态,也即各个寄存器的内容。当发生任务切换时,当前运行的任务的上下文被存入TCB,并将要被执行的任务的上下文从它的TCB中取出,放入各个寄存器中。
究竟选择多任务还是单任务方式,依赖于软件的体系是否庞大。例如,绝大多数手机程序都是多任务的,但也有一些小灵通的协议栈是单任务的,没有操作系统,它们的主程序轮流调用各个软件模块的处理程序,模拟多任务环境。
3、中断服务程序
中断是嵌入式系统中重要的组成部分,但是在标准C中不包含中断。许多编译开发商在标准C上增加了对中断的支持,提供新的关键字用于标示中断服务程序(ISR),类似于__interrupt、#program interrupt等。当一个函数被定义为ISR的时候,编译器会自动为该函数增加中断服务程序所需要的中断现场入栈和出栈代码。
中断服务程序需要满足如下要求:
(1)不能返回值;
(2)不能向ISR传递参数;
(3) ISR应该尽可能的短小精悍;
(4) printf(char * lpFormatString,…)函数会带来重入和性能问题,不能在ISR中采用。
4、硬件驱动模块
一个硬件驱动模块通常应包括如下函数:
(1)中断服务程序ISR
(2)硬件初始化
A、修改寄存器,设置硬件参数(如UART应设置其波特率,AD/DA设备应设置其采样速率等);
B、将中断服务程序入口地址写入中断向量表:
/* 设置中断向量表 */
m_myPtr = make_far_pointer(0l); /* 返回void far型指针void far * */
m_myPtr += ITYPE_UART; /* ITYPE_UART: uart中断服务程序 */
/* 相对于中断向量表首地址的偏移 */
*m_myPtr = &UART _Isr; /* UART _Isr:UART的中断服务程序 */
(3)设置CPU针对该硬件的控制线
A、如果控制线可作PIO(可编程I/O)和控制信号用,则设置CPU内部对应寄存器使其作为控制信号;
B、设置CPU内部的针对该设备的中断屏蔽位,设置中断方式(电平触发还是边缘触发)。
(4)提供一系列针对该设备的操作接口函数。例如,对于LCD,其驱动模块应提供绘制像素、画线、绘制矩阵、显示字符点阵等函数;而对于实时钟,其驱动模块则需提供获取时间、设置时间等函数。
本文介绍了C语言模块编程的主要思想,嵌入式系统开发中模块划分、多任务还是单任务选取、单任务程序典型架构、中断服务程序、硬件驱动模块设计等。
本文是学习网络博文并经自己思考总结整理而来,博文来源有:
C语言中文网、C语言模块化程序设计(EDN电子技术设计 wnhb)、嵌入式C语言之---模块化编程 (CSDN zhzht19861011)。
由于网络博文繁杂,无法一一查明原出处,所列来源为本人学习时所查阅资料。
第十二课 嵌入式 Linux C语言—单链表
一、单链表简介
1、单链表的结构
单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。
链表中的数据是以节点来表示的,每个节点由两部分构成:一个是数据域,存储数据值,另一个是指针域,存储指向下一个节点的指针。
2、单链表的节点
单链表节点的数据结构如下:
typedef struct data
{
unsigned int id;//学生身份ID
char name[LENGTH];//学生姓名
char subject[LENGTH];//科目
unsigned int score;//分数
}DATA,*PDATA;
typedef struct node
{
unsigned int num;//节点序号,头节点则保存链表的节点数量
DATA *data;//数据域
struct node *next;//指针域
}SNode,*PNode;
3、头指针与头结点的区别
头指针与头结点的区别:
二、单链表的操作
1、单链表的创建
头结点的创建:
//初始化单链表,创建头结点
SList slist_init(void)
{
SList head = (Node *)malloc(sizeof(Node));
if(NULL == head)
{
fprintf(stderr, "slist_init function malloc failure.\n");
return NULL;
}
head->next = NULL;
return head;
}
采用尾部插入节点的方式创建单链表:
//根据创建的头结点创建有num个节点的单链表(头结点不算)
int slist_create(SList head, unsigned int num)
{
SList p,r;
r = head;
int i;
for(i = 0; i < num; i++)
{
p = (Node *)malloc(sizeof(Node));//创建新节点
if(NULL == p)
{
fprintf(stderr, "slist_create function p malloc failed.\n");
return ERROR;
}
p->num = i+1;//将各个节点的序号写入节点成员num
p->data.id = 100;
strcpy(p->data.name, "scorpio");
strcpy(p->data.subject, "English");
p->data.score = 100;
r->next = p;
r = p;
}
r->next = NULL;
head->num = slist_size(head);//将创建链表的大小写入头结点num成员
return OK;
}
2、单链表的销毁
//销毁单链表
void slist_destroy(SList list)
{
SList p;
while(list)
{
p = list->next;
free(list);
list = p;
}
}
3、单链表的节点插入操作
void slist_insert(SList list, unsigned int i,PDATA data)
{
if(slist_size(list) < i)
{
fprintf(stdout, "slist_insert function failed.The list size is too samll.There is no element %u.\n", i);
return ;
}
//创建一个节点
SList p = (Node *)malloc(sizeof(Node));
//利用传入的data结构体的数据对节点的数据域进行初始化
p->data.id = data->id;
strcpy(p->data.name, data->name);
strcpy(p->data.subject, data->subject);
p->data.score = data->score;
//找到节点i
SList r = list->next;
if(NULL == r->next)//链表为空时,直接插入
{
//插入节点到链表
p->next = r->next;
r->next = p;
}
else//链表不为空时,先查找插入的位置
{
while(i != r->num)
{
r = r->next;
}
//插入节点到链表
p->next = r->next;
r->next = p;
}
//顺延链表节点i后的节点的序号
int n = i + 1;
r = r->next;
while(r->next)
{
r->num = n++;
r = r->next;
}
r->num = n;//尾节点的序号
list->num = slist_size(list);//将创建链表的大小写入头结点num成员
}
4、单链表的节点删除操作
void slist_delete(SList list, unsigned int i)
{
if(slist_size(list) < i)
{
fprintf(stdout, "slist_insert function failed.The list size is too samll.There is no element %u.\n", i);
return ;
}
SList r = list->next;
if(NULL == r->next)//链表为空时,不能删除
{
fprintf(stderr, "The list is empty.There is no element %u.\n", i);
return ;
}
else//链表不为空时,先查找插入的位置
{
while(i != r->num)
{
r = r->next;
}
//从链表中删除节点
r->next = r->next->next;
}
//顺延链表节点i后的节点的序号
int n = i;
while(r->next)
{
r->num = n++;
r = r->next;
}
r->num = n;//尾节点的序号
list->num = slist_size(list);//将创建链表的大小写入头结点num成员
}
5、获取单链表的长度
//计算单链表的长度,头节点不计算在长度之内
unsigned int slist_size(SList list)
{
unsigned int count;
SList p = list->next;
while(p)
{
p = p->next;
count++;
}
return count;
}
6、判断单链表是否为空
//判断单链表是否为空
int slist_is_empty(SList list)
{
if(list)
{
fprintf(stdout, "list is no empty.\n");
return 0;
}
else
{
fprintf(stdout, "list is empty.\n");
return 1;
}
}
7、遍历单链表
//遍历单链表
void slist_traverse(SList list)
{
fprintf(stdout, "list size is %u\n", list->num);
SList p = list->next;//跳过头结点
while(p)
{
fprintf(stdout, "id = %u, name = %s, subject = %s, score = %u\n", p->data.id, p->data.name, p->data.subject, p->data.score);
p = p->next;
}
}
8、单链表的翻转(倒序、逆序)
(1)定义当前节点 current,初始值为第一个节点,current =list->next;
(2)定义当前结点的后继节点pnext, pnext = current->next;
(3)将当前第一个节点与后继节点断开,作为尾节点,current->next = NULL;
(4)只要 pnext 存在,表明至少有两个节点,进行逆序,执行以下循环:
A、定义新节点prev,prev是 pnext的后继节点,prev = pnext->next;
B、把pnext的后继指向current, pnext->next = current;
C、此时,pnext 实际上已经到了current 前一位成为新的current,所以这个时候 current 结点实际上成为新的 pnext,current = pnext;
D、此时,新的 current 就是 pnext,current = pnext;
E、而新的 pnext 就是 prev,pnext = prev;
(5)最后将头结点与current重新连上即可,list->next = current;
void slist_reverse(SList list)
{
if(slist_is_empty(list))
{
fprintf(stdout, "The list is empty.No reverse.\n");
return ;
}
SList prev, current, pnext;
current = list->next;//跳过头节点,当前节点为第一个节点
pnext = current->next;//当前节点的下一个节点
current->next = NULL;//当前节点断开后续连接,作为尾节点
while(pnext)
{
prev = pnext->next;//将当前节点的下一个节点作为当前节点的前一个节点
pnext->next = current;//当前节点的前一个节点的指针域指向当前节点
current = pnext;//将下一个节点作为当前节点
pnext = prev;//前一个节点作为下一个节点
}
list->next = current;//头结点指针域指向当前节点,完成逆序
//重新给逆序后的各节点赋值新的序号
int i = 1;
while(current)
{
current->num = i++;
current = current->next;
}
}
参考博文:
小猪的数据结构辅助教程——2.2 线性表中的单链表 (CSDN coder-pig)
第十三课 嵌入式 Linux C语言—双链表
一、双链表简介
1、双链表的结构
双链表是链表的一种,由节点组成,每个数据结点中都有两个指针,分别指向直接后继和直接前驱。
2、双链表的节点
typedef struct data
{
unsigned int id;//学生身份ID
char name[LENGTH];//学生姓名
char subject[LENGTH];//科目
unsigned int score;//分数
}DATA,*PDATA;
typedef struct node
{
struct node *prev;
unsigned int num;//节点序号,头节点则保存链表的节点数量
DATA data;//数据
struct node *next;
}Node,*DList;
二、双链表的操作
1、双链表的创建
双链表头结点的创建:
DList dlist_init(void)
{
DList head = (Node *)malloc(sizeof(Node));
if(NULL == head)
{
fprintf(stderr, "dlist_init function malloc failure.\n");
return NULL;
}
head->next = NULL;
head->prev = NULL;
return head;
}
采用尾部插入节点的方式创建双链表
int dlist_create(DList head, unsigned int num)
{
DList p,r;
r = head;
int i;
for(i = 0; i < num; i++)
{
p = (Node *)malloc(sizeof(Node));//创建新节点
if(NULL == p)
{
fprintf(stderr, "dlist_create function p malloc failed.\n");
return ERROR;
}
p->num = i+1;//将各个节点的序号写入节点成员num
p->data.id = 1000 + i;
strcpy(p->data.name, "scorpio");
strcpy(p->data.subject, "English");
p->data.score = 100;
r->next = p;
p->prev = r;
r = p;
}
r->next = NULL;
head->num = dlist_size(head);//将创建链表的大小写入头结点num成员
return OK;
}
2、双链表的销毁
void dlist_destroy(DList list)
{
DList p;
while(list)
{
p = list->next;
free(list);
list = p;
}
}
3、双链表的节点插入操作
void dlist_insert(DList list, unsigned int i, PDATA data)
{
if(dlist_size(list) < i)
{
fprintf(stdout, "dlist_insert function failed.The list size is too samll.There is no element %u.\n", i);
return ;
}
//创建一个节点
DList p = (Node *)malloc(sizeof(Node));
//利用传入的data结构体的数据对节点的数据域进行初始化
p->data.id = data->id;
strcpy(p->data.name, data->name);
strcpy(p->data.subject, data->subject);
p->data.score = data->score;
//找到节点i
DList r = list->next;
if(NULL == r->next)//链表为空时,直接插入
{
//插入节点到链表
p->next = r->next;
r->next = p;
p->prev = r;
}
else//链表不为空时,先查找插入的位置
{
while(i != r->num)
{
r = r->next;
}
//插入节点到链表
p->next = r->next;
r->next = p;
p->prev = r;
}
//顺延链表节点i后的节点的序号
int n = i + 1;
r = r->next;
while(r->next)
{
r->num = n++;
r = r->next;
}
r->num = n;//尾节点的序号
list->num = dlist_size(list);//将链表的大小写入头结点num成员
}
4、双链表的节点的删除操作
void dlist_delete(DList list, unsigned int i)
{
if(dlist_size(list) < i)
{
fprintf(stdout, "dlist_insert function failed.The list size is too samll.There is no element %u.\n", i);
return ;
}
DList r = list->next;
//查找要删除节点的位置
while(i != r->num)
{
r = r->next;
}
DList p = r->next;
//从链表中删除节点
r->next->next->prev = r;
r->next = r->next->next;
free(p);
//顺延链表节点i后的节点的序号
int n = i;
while(r->next)
{
r->num = n++;
r = r->next;
}
r->num = n;//尾节点的序号
list->num = dlist_size(list);//将创建链表的大小写入头结点num成员
}
5、获取双链表的长度
unsigned int dlist_size(DList list)
{
unsigned int count = 0;
DList p = list->next;
while(p)
{
p = p->next;
count++;
}
return count;
}
6、判断双链表是否为空
int dlist_is_empty(DList list)
{
if(list)
{
fprintf(stdout, "list is no empty.\n");
return 0;
}
else
{
fprintf(stdout, "list is empty.\n");
return 1;
}
}
7、遍历双链表
void dlist_traverse(DList list)
{
fprintf(stdout, "list size is %u\n", list->num);
DList p = list->next;//跳过头结点
while(p)
{
fprintf(stdout, "num = %u, id = %u, name = %s, subject = %s, score = %u\n", p->num, p->data.id, p->data.name, p->data.subject, p->data.score);
p = p->next;
}
}
8、双链表的翻转(逆序、倒序)
void dlist_reverse(DList list)
{
if(dlist_is_empty(list))
{
fprintf(stdout, "The list is empty.No reverse.\n");
return ;
}
DList prev, current, pnext;
current = list->next;//跳过头节点,当前节点为第一个节点
pnext = current->next;//当前节点的下一个节点
current->next = NULL;//当前节点断开后续连接,作为尾节点
while(pnext)
{
prev = pnext->next;//将当前节点的下一个节点作为当前节点的前一个节点
pnext->next = current;//当前节点的前一个节点的指针域指向当前节点
current = pnext;//将下一个节点作为当前节点
pnext = prev;//前一个节点作为下一个节点
}
list->next = current;//头结点指针域指向当前节点,完成逆序
//重新给逆序后的各节点赋值新的序号
int i = 1;
while(current)
{
current->num = i++;
current = current->next;
}
}
代码见附件。
第十四课 嵌入式 Linux C语言——C语言基础总结
一、数据类型
1、基本数据类型
数据类型是创建变量的模型。变量名是连续存储空间的别名,程序中使用变量命名存储空间,通过变量可以使用存储空间。变量所占的内存大小取决于创建变量的数据类型。
2、有符号和无符号
有符号数中数据类型的最高位用于标识数据的符号,最高位为1表示为负数,最高位为0表示为正数。
计算机中有符号数通常使用补码表示,正数的补码为正数本身,负数的补码为负数的绝对值的各位取反后加1。
计算机中无符号数通常使用原码表示,无符号数默认为正数,没有符号位。对于无符号数来说,MAX_VALUE + 1等于MIN_VALUE,MIN_VALUE - 1等于MAX_VALUE。
无符号数与有符号数进行混合运算时,会将有符号数转换为无符号数后再进行计算,结果为无符号数。
3、浮点数的实现
float与double类型的数据在计算机中的表示方法相同,由于所占存储空间不同,分贝能够表示的数值范围和精度不同。
浮点数在计算机内部存储方式分为三段,符号位、指数、尾数
浮点数的转换需要先将浮点数转换为二进制,将得到的二进制浮点数使用科学计数法表示,根据数据类型计算偏移后的指数。指数的偏移量和数据类型有关,float类型加127,double类型加1023。
8.25的float表示如下:
8.25的二进制表示:1000.01==>1.00001(2^3)
符号位:0
指数:127+3 = 130 = b10000010
小数:00001
8.25的float表示为:0 10000010 00001 000000000000000000 = 0x41040000
由于float和int类型都占四个字节,float能表示的具体数字的个数与int相同,但是float表示的数值时不连续的,不能作为精确数使用,因此float类型的表示范围比int类型表示的范围大。Double类型与float类型在计算机中的表示方法相同,但是double类型占用的存储空间大,因此所能表示的精度更高。
4、类型转换
C语言中的数据类型可以进行转换,包括显示类型转换和隐式类型转换。
强制类型转换时,如果目标类型能够容纳目标值,则转换后结果不变;如果目标类型不能容纳目标值,则结果将产生数据截断。
隐式类型转换时编译器进行的类型转换,低类型到高类型的隐式转换是安全的,结果不会产生数据截断;高类型到低类型的隐式转换是不安全的,结果产生类型截断,结果可能是不正确的。
隐式类型转换发生的时机:
A、算术运算中,低类型转换为高类型
B、赋值表达式中,表达式的值转换为左值的类型
C、函数调用时,实参转换为形参的类型
D、函数返回时,return表达式转换为返回值类型
5、void类型
void修饰函数返回值和参数
void不能用于定义变量,标准C语言编译器中sizeof(void)会报错,GCC编译器中因为进行了扩展,不会报错,结果为1
二、程序结构
1、分支语句
if...else
if语句根据条件选择执行语句,else不能独立存在,但是总是和离它最近的if匹配。
float变量不能直接和0进行比较,需要确定变量在某个较小的区间内。
if(-0.0000001 < a && a < 0.0000001)
switch
switch语句对应单个条件多个值的情况,一般情况下case语句需要有break,否则会导致分支重叠。Default分支需要加上,用于处理特殊情况。
case语句中的值只能是整型或char类型。
2、循环语句
do语句先执行后判断,循环体至少执行一次
do
{
}while();
while语句先判断后执行,循环体可能不执行
while()
{
}
for语句先判断后执行
for(; ; )
break表示终止循环的执行
continue表示中止本次循环,进入下一次循环
三、const和volatile
1、const
const修饰的变量告诉编译期变量是只读的,但还是变量,向编译器指明变量不能做左值
const修饰的局部变量在栈上分配空间
const修饰的全局变量在全局数据区分配空间
const只在编译期有效,在运行期无效
#include <stdio.h>
const int g = 10;
int main(int argc, char **argv)
{
const int c = 1;
int *p = (int *)&c;
*p = 100;
printf("%d\n", c);
p = (int *)&g;
*p = 1000;
printf("%d\n", c);
return 0;
}
编译正常,运行时对使用指针可以对const局部变量进行修改,但使用指针对const全局变量修改时发生段错误。
标准C语言编译器不会将const修饰的全局变量存储于只读存储区,而是存储在可以修改的全局数据区,其值可以通过指针修改。但是现代编译器如GCC将const全局变量存储在只读存储区,通过指针修改其值将发生段错误。
const修饰函数参数表示在函数体内部不希望修改参数的值
const修饰函数返回值表示返回值不可改变
2、volatile
volatile指示编译器不能优化,必须每次到内存中取变量的值,主要修饰被多个线程访问的变量、或是被未知原因更改的变量
const volatile int i = 10;
作为局部变量,i不可以做左值,但可以使用指针修改值,编译器不对变量i进行优化,每次都到内存取值。
四、struct和union
1、struct
柔性数组是数组大小待定的数组
结构体中最后的成员可以是柔性数组,柔性数组只是一个标识符,不占存储空间。
2、union
union只分配最大成员的空间,所有成员共享这个空间。
union的使用会受到计算机大小端的影响
使用union测试计算机大小端的代码如下:
#include <stdio.h>
void big_little(void);
int main(int argc, char *argv[])
{
big_little();
return 0;
}
void big_little(void)
{
union Mode
{
char c;
int i;
};
union Mode m;
m.i = 1;
if(m.c)
{
printf("little\n");
}
else
{
printf("big\n");
}
}
五、enum、sizeof、typedef
1、enum
enum是C语言中的一种自定义类型,enum的值是自定义的整型值,第一个定义的enum值默认为0,默认情况下enum的值是前一个定义的值加1,也可以指定。enum中定义的值是C语言中真正的常量
2、sizeof
sizeof是编译器的内置指示符,用于计算类型或变量所占内存大小,sizeof的值在编译期就已经确定。
int var = 0;
int size = sizeof(var++);
var并不会执行var++,而是在编译时就将sizeof(var++0替换为值4
3、typedef
typedef用于给一个已经存在的类型进行重命名,本质上不产生新类型
typedef重命名的源类型可以在typedef语句后定义,typedef不能使用unsigned、signed修饰。
六、接续符、单引号、双引号
1、接续符
C语言中接续符(\)可以指示编译器,编译器会将接续符删除,跟在接续符后面的字符自动接续到前一行。在接续单词时,接续符后面不能有空格,接续符的下一行之前也不能有空格。通常接续符使用在定义宏代码块时使用。
2、单引号和双引号
C语言中单引号用来表示字符字面量,双引号用来表示字符串字面量
‘a’表示字符字面量,占一个字节大小,’a’+1表示’b’,”a”表示字符串字面量,”a”+1表示指针运算,结果为指向”a”字符串的结束符’\0’。
char c = “hello”;
字符串字面量“hello”的地址赋值给字符变量c,由于地址占用四个字节空间,赋值给字符类型后会发生类型截断。
七、++、--、三目运算符
1、++、--
++、--参与混合运算的结果是不确定的。
2、三目运算符
三目运算符返回变量的值,而不是变量本身。根据隐式类型转换规则确定返回值类型。
int a = 1;
int b = 2;
int c = 0;
c = a < b ? a : b;
(a < b ? a : b) = 3;//error
*(a < b ? &a : &b) = 3;//ok
八、宏
1、宏定义
#define由预处理器处理,直接进行文本替换,不会进行语法检查,#define定义的宏可以出现在程序的任意位置
#define定义的宏常量本质为字面量
宏由预处理器处理,编译器不知道宏的存在。
宏表达式没有任何的调用开销,不能出现递归定义
编译器内置的宏:
__FILE__:被编译的文件名
__LINE__:当前行号
__DATE__:编译时的日期
__TIME__:编译时的时间
__STDC__:编译器是否遵循标准C规范
#defineLOG(s) printf("[%s] File:%s, Line:%d %s \n", __DATE__, __FILE__, __LINE__, s)
2、条件编译
条件编译是预编译指示命令,用于控制是否编译某段代码。预编译器根据条件编译指令有选择的删除代码,编译器不知道代码分支的存在。
条件编译可以解决头文件重复包含的编译错误
#ifndef _FILE_H_
#define _FILE_H_
//source code
#endif
条件编译通过不同的条件编译不同的代码,生成不同条件的目标,实际工程中可以使用条件编译将同一份工程代码生成不同的产品线或是区分产品的调试和发布版。
3、#error
#error用于生成一个编译错误消息
语法:#error message
message不需要双引号
#ifndef __cplusplus
#error This file should be processed with C++ compiler.
#endif
编译过程中产生错误信息意味着编译将终止,无法生成最终的可执行程序
4、#line
#line用于强制指定新的行号和编译文件名,并对源程序的代码进行重新编号
#line number filename
#line本质上是对__LINE__和__FILE__宏的重定义
5、#progma
#progma用于指示编译器完成某些特定的动作,所定义的很多关键字和编译器有关,在不同编译器间是不可以移植的。预处理器将忽略不认识的#progma指令,不同的编译器可能会对#progma指令的解释不同。
#progma message
编译时输出消息到编译器输窗口,用于提示信息
#if defined(ANDROID20)
#pragma message("Compile Android SDK 2.0...")
#define VERSION "Android 2.0"
#elif defined(ANDROID23)
#pragma message("Compile Android SDK 2.3...")
#define VERSION "Android 2.3"
#elif defined(ANDROID40)
#pragma message("Compile Android SDK 4.0...")
#define VERSION "Android 4.0"
#else
#error Compile Version is not provided!
#endif
与#error不同,#progma massage仅代表一条编译消息,不代表编译出错
#progma once
#progma once用于保证头文件只被编译一次,和编译器相关,编译器不一定支持。
工程代码使用如下:
#ifndef_FILE_H_
#define _FILE_H_
#progma once
#endif
#progma pack
#progma pack用于指定内存对齐方式,一般成对使用
#progma pack(n)
//source code
#progma pack()
6、#运算符
#运算符用于在预处理期将宏参数转换为字符串,只能在宏定义中有效
#define STRING(x) #x
printf("%s\n", STRING(Hello world!));
#define CALL(f, p) (printf("Call function %s\n", #f), f(p))
7、##运算符
##运算符用于在预处理期粘连两个标识符,只在宏定义中有效
#define NAME(n) name##n
int main()
{
int NAME(1);
int NAME(2);
NAME(1) = 1;
NAME(2) = 2;
printf("%d\n", NAME(1));
printf("%d\n", NAME(2));
return 0;
}