【笔记】嵌入式C语言随堂笔记

这篇博客是宕机酱的嵌入式C语言随堂笔记,涵盖了冯诺依曼架构、哈佛结构、内存管理、C++内存封装、语言对比、数据结构、位操作、指针详解、函数指针、typedef应用、内存分层、数组、字符串、结构体等内容,深入浅出地介绍了C语言在嵌入式领域的应用和理解。笔记特别强调了指针的重要性,以及如何避免野指针和内存泄漏。此外,还讨论了栈、堆的特性,内存对齐,以及预处理、函数的本质、递归函数、函数库、静态链接库和动态链接库的使用。
摘要由CSDN通过智能技术生成

一、有关内存的话题

1.1
冯诺依曼架构:数据和代码放在一起
哈佛结构:数据和代码不放在一起
代码:eg:函数
数据:eg:局部变量、全局变量

1.2
操作系统中需要大块内存,使用API,如malloc和free
裸机中使用大块内存,只能使用原始的数组。

1.3
C++对内存的 进一步封装。C++可以用new来创建对象。
C++对内存管理比较容易一些。但new了一定要delete,就像C中malloc必须要free,不然就会内存泄漏

1.4语言没有好坏。
java不直接操作内存,而是用[虚拟机]操作内存。虚拟机来代理,帮助程序员管理内存。如果忘记,虚拟机会帮忙释放,不用考虑内存的问题。
听起来java、c#挺好,但是这些功能是需要付出一定代价的

============================================================

2.1何为内存
硬件角度:内存是电脑的配件。内存芯片 = 内存颗粒。如果每个芯片512M,8个芯片连接起来构成一个4GB的内存。
根据不同的实现原理,还可以把内存分成SRAM/DRAM。DRAM还分几种(SDRAM DDR1 DDR2 LPDDR…)
SRAM不需要初始化可以直接使用,DRAM需要先初始化才可使用。
逻辑角度:内存可以随机访问,即只要给一个地址,就可访问这个内存,并且可以读取和写入。(逻辑上可以限制读写)
内存是用来存放变量的。变量的特性就是要随时访问并且是可变的。正是因为有了内存,C语言才能定义变量。

逻辑上说,内存可以无限大,实际上是有限制的。比如32位的系统(32位系统指的是32位数据线,但一般地址线也是32位)
地址线决定了内存地址也是32位二进制,逻辑大小为32位二进制,即4GB,可用内存要少于4GB。

从硬件角度讲,内存位宽本身也是有宽度的。有些内存16,有些内存8。内存是可以并联的,8位的内存芯片也是可以构成32位的
比如S5PV210就是2个16位的内存并联得到32位的


2.4 结构体

数组的元素类型必须一致,而且在定义的时候必须给它明确大小,这个大小还不能改
结构体的各个元素类型可以不一,定义时不需给出大小。弥补了数组灵活性差的缺陷。这就是个复杂版的数组
C语言是面向过程的,但是写出来的 Linux系统是面向对象的
非面向对象的语言不一定写出面向对象的代码。只能说面向对象的语言写面向对象的程序更加无脑一些
C实现面向对象是一件很麻烦的事情,这就是为什么很多人学过C却看不懂linux内核代码。
实现的方法:结构体内嵌指针

struct s
{
	int age; //普通变量 类似class中成员变量
	void (*pfunc)(void);//函数指针,指向void func(void)类似class中成员方法
};

函数不能写到结构体里,但是结构体里有函数指针就等于包含了方法。相当于结构体充当了class
linux内核和Uboot会关联到。

============================================================

2.5栈

C中定义局部变量的方式就是使用栈。栈指针移动来分配内存空间,然后和局部变量名绑定。
栈空间是反复使用的,也即是说栈是“脏的”,在上次使用时不清零。如果不初始化,那么值就是随机的
不存在“堆栈”这种东西。常说的堆栈即是栈

2.6堆
栈的空间比较小,需要大段(4000字节以上时)就应该使用堆来分配内存

2.7复杂数据结构

链表 嵌入式中用处广,
哈希表 类似下标为335 7 190 -132这样的数组,用key指定。需要会使用
二叉树

============================================================

位操作、位运算

异或^ 记忆方法:如异则或
1 其他位保持不变,bit3 - bit5清0
a &= (~(0x7 <<3));

2 其他位保持不变,bit3 - bit5置位
a |= (0x7<<3);

3 读取bit3 - bit5的值
步骤1,除bit3-bit5以外,其他位清零
b = a;
b &= (0x7<<3)
步骤2,得到的数向右移动3位
b>>=3;

4 其他位保持不变,替换bit3-bit5的数为十进制5(0b101)
步骤一,先将3-5之间的数清零
a &= (~(0x7 <<3));
步骤二,往里面写
a |= (5<<3);

5 其他位保持不变,将bit3-bit5的数算数+2
步骤一,先将3-5之间的数读出
步骤1,除bit3-bit5以外,其他位清零
b = a;
b &= (0x7<<3)
步骤2,得到的数向右移动3位
b>>=3;
步骤二,算数加法
b+=2;
步骤三,先清a的bit3-bit5,或运算写入
a |= (b<<3);

============================================================

3.指针才是C的精髓

3.1为什么需要指针:间接访问,间接寻址。 指针的需要,不是因为我们想用它更好,而是没有他不行。java使用指针的时候封装了
指针使用 分三步:定义指针变量、给指针变量赋值(绑定指针)、解引用
(1)定义指针变量
int *p;
(2)绑定指针
p = &a; //绑定a
p = (int *)4; //绑定0x4这个地址的内存空间
(3)解引用
*p = 2333;
注意:解引用时若未绑定有效地址,那么解引用的结果是完全未知的

3.2 符号理解
c语言的编译器在看我们写的代码的时候,把首要元素的前后最先看
int p5 , p6; //定义了p5 和 int p6;
int* p5 , p6; //定义了*p5 和 int p6;两者没有不同,需要注意

(*p)的两不同用法,两者结合方式不同:
(1) int p; //【首先不与结合,而是int *先结合 ,然后int *与 p结合 】
(2) *p = 4; //*与p结合
这样也容易理解了指针初始化:
int *p = &a; //首先是int * , 然后是 p = &a;

============================================================

3.2野指针

野指针的意思是说指针的指向位置是不可知的,很可能触发运行时候的段错误
其出现的主要原因是没有初始化,那么指针的 值也是随机的。如果解引用他那么结果也是不可知的
解引用野指针出现的可能结果:
(1)访问了内核空间等系统敏感区,出现段错我
(2)解引用了未使用的可用空间
(3)解引用了已经使用的内存空间,程序出现离奇的错误,一般会导致程序崩溃
避免野指针出现:就是确保指针指向一个绝对可用的空间
常规做法:
定义指针时候,初始化为NULL
解引用之前,先去判断这个指针是不是NULL
指针在使用完之前,将值赋值为NULL
int *p = NULL;
//绑定指针
if (p != NULL)
{ *p = 4;}
p = NULL;

NULL代表何意?
C++中代表0 else 代表 (void *)0

p = NULL; 意思就是把p指向一个特殊的0地址,表明他是一个野指针

============================================================

4.复杂表达式和指针高级应用

4.1 指针数组和数组指针

指针数组和数组指针
两者本身是不一样的。
指针数组是数组,存储的全都是指针。a[] = *1 , *2 , *3
数组指针是指针,指针指向的是一个数组

int *p[5]; int (*p)[5]; int (p[5]);
一般规律:int p; //p是一个指针。编译器看到,认为这是一个指针(p是核心,与
结合,所以是指针)
int p[5]; //p是一个数组。编译器看到[],认为这是一个数组 (p是核心,与[]结合,所以数组)
总结:1、找核心:我们在定义一个符号的时候,关键在要搞清楚我定义的符号是谁
2、找结合:看谁跟核心最近谁与核心结合
3、继续向外结合,直到整个扩散结合完

用一般规律来看 int *p[5];
1、找核心: // p
2、找结合:看谁先结合,要看优先级。// []先,所以是p[5]数组
3、继续向外结合 // 紧随其后, p[5]指针数组,元素都是指针
4、直到整个扩散结合完 //int *p[5]指针数组,指针指向的元素类型是int

用一般规律来看 int (p)[5];
1、找核心: // p
2、找结合:看谁先结合,要看优先级。// ()先,所以是
p指针
3、继续向外结合 // []紧随其后,(*p) [5]数组指针,
4、直到整个扩散结合完 //int (*p)[5]数组指针,数组的元素类型是int
总结:像蛀虫一样,从里往外剥离
int *(p[5]) ()没有起到作用,同 int *p[5];
注: () 属于强制优先级
[] . -> (函数变量名)属于第一优先级
++ (强制转换) sizeof ~ 属于第二优先级
= 赋值属于第14级

基础理论和原则是关键,没有无缘无故的规则!
困难的问题不会,是因为简单的东西没学好。
原来根本看都看不明白的东西,30分钟不仅了如指掌还能应用。说明方法对了。
高考的时候一再强调:把会做 的作对就行了,很简单,只是99%的人做不到。

============================================================

4.2 函数指针和typedef

函数指针也是指针。函数指针和其他指针都是4字节,之间并没有本质区别。
函数的代码是一段代码,所以需要第一句代码的地址,也就是函数的首地址,简称函数地址,c语言中用函数名来表示
结合函数的实质,函数指针其实就是一个普通变量,值就是函数的地址
void (*p)(int x,int y)

C语言是强类型指针,所谓强类型就是说每一个变量都需要定义类型如int,char
弱类型语言如脚本、makefile。强类型可以帮我们做严格的类型检查

int a[5];
&a代表的意义则是:指向数组a的一个指针 int (*)[5]

那么
int (*p1)[5] ; //定义变量定义在前面!这个例子很特殊
p1 = &a; //编译通过

主函数中,使用函数指针调用函数

void func1(void)
{
	printf("test function pointer");
}
int main()
{
	void (*pfunc1)(void)
	pfunc1 = func1;//左边是函数指针,右边是函数名。
	pfunc1(); //解引用以调用函数,注意没有*
	return 0;
}

Typedef是用户自定义类型。函数指针,指针数组都属于用户自定义的类型。原生类型:int,char
如果要定义三个函数指针,可能要写很多行,很麻烦。
简化的方法就是typedef。typedef不能制作类型,他只是给类型重命名
注意typedef加工出来的是类型而不是变量
typedef的用法是把核心改成类型

typedef int (pType)(int, int); //定义了一个pFunc
这句话就重命名了一种类型pType,类型是int (
)(int, int)
后面定义就可以直接写
pType p3;
pType p4; //等效于int (*p4)(int, int);

============================================================

4.3 指针强化

int main()
{
	pFunc p1 = NULL;
	char c= 0;

	printf("+-x \");
	scanf("%d",&c);

	switch (c)
	case '+':p1 = add; break;
	case '-':p1 = sub; break;
	case '*':p1 = mul; break;
	default: p1 = NULL;break;

	result = p1(a,b);
}

用一个指针指向不同的函数,实际结果不一样。模拟的是面向对象中的一大特征:多态
引用指向了不同的对象,引用的结果是不一样的,具体看虚拟机 、

============================================================

分层

写程序分层 的原因是复杂的程序一个人搞不定,需要多个人协同工作。【要分工,先分层】
不同的层次由不同的人完成,彼此调用组合,共同工作。

计算器 的 程序设计了两个层次。上层是framework,实现应用框架。下次是cal.c,实现计算器
实际工作时候cal.c是直接完成工作的,重要函数通过framework调用。

(1)先写framework framework应该写实际业务关联的代码(加减乘除的函数,干货。)并且把相应的接口写入头文件

cal.h

#ifndef __CAL_H_
#define __CAL_H_ //避免重复调用
typedef int (*pType)(int, int);
struct cal_t
{
	计算的元素
	函数指针
	int a;
	int b;
	pType p1;
}

(2)编写cal.c的人

int main(void)
{
	struct cal_t mycal;
	mycal.a = 
	mycal.b = 
	mycal.p =
	calculator(&mycal) ;
}

/*……………………………………………………………………………………………………………………………………………………/

*标题:自测_

*作者:宕机酱

*完成时间:2016.6.21

*注释:cal.c

/…………………………………………………………………………………………………………………………………………………………*/

#include <stdio.h>
#include "021_cal.h"
int p1_plus(int a,int b)
{
        return a+b;
}


int main()
{
        struct cal_yuansu mycal;
        mycal.a = 14;
        mycal.b = 3;
        mycal.p = p1_plus;


        calculate(&mycal);
        printf("Code Access~\n");
        return 0;
}

============================================================

4.4关于typedef 以及 . 和 ->

正确的使用姿势是
(对象).(属性)
(对象地址)->(属性)

4.5 二重指针
二重指针必须指向一重指针的地址,
char **p1;
char *p2;
p1 = &p2;

二重指针指向指针数组
int *p[2];
int *p1;
p2 = p1; //error : 指针 = 指针数组

正确的方法:
int **p3;
p3 = p1; //二重指针 = 指针数组;是可以的

4.6多维数组
从内存角度上看,一维数组和二维数组没有任何区别,但两者组织方式有区别
三维数组以上除了一些特殊数学运算有关的之外基本用不到
后来三维数组用于运算四轴飞行器 角度,姿态时候会用到

int a[2][5];
0 0 0 0 0
0 0 0 0 0

============================================================

插一句话:
奖学金申请理由:
学生的学习成绩稍有退步,却依然获得了奖学金资格。这与我强大的底蕴和爆棚的人品是分不开的。感谢学校的培养和期望。最后,我好可爱

============================================================

五、数组&字符串&结构体&共用体&枚举视频课程
5.1 程序中内容

操作系统定义了多种机制来使应用程序来使用内存,这些机制彼此不同。
有点像酒店开房:获取(临时使用权限)、使用、释放(归还权限)

【栈】:
(局部变量)
运行时自动申请、自动释放
反复使用
脏内存
临时性
定义一个变量,用的时候就会给栈上面分配一个空间,用起来非常方便程序员不需要手动干预

int a = 3 | 3 | 0x20004000
int b = 3 | 3 | 0x20004004
int c = 6 | 6 | 0x20004008
int d = 3 | 3 | 0x2000400c

使用的时候先入后出,依次记录
脏内存: 栈没有机制为他清零。最开始是干净的,但第一个程序调用运行完了之后,不会打扫。
下一个程序使用 的时候就是脏的

不设计成使用完清理的原因是:OS不知道是不是最后一次使用,有可能清零之后就没有下一个程序使用了,这打扫了就白打扫了

所以我们就定义了局部变量int a = 0;
养成一个好习惯,使用前先打扫。如果不打扫,使用的就是上个程序剩下的值

【堆】:
手动申请和释放:malloc和free
大块内存
脏内存
临时性

比如有50个进程,500M内存。分配成450+50;450共同管理50个进程的共有内存,这个大的内存集合就叫做堆(公用地)
如果都像栈那样分配就不够灵活。进程有大有小:有的200M,有的只有2M。那么用一样大小的内存那么就非常不灵活。
每个进程也保留了一定大小的 栈(自留地),用于保留自己的局部变量等。
注意:堆内存只在malloc和free中间的代码属于自己的进程。不能在之外访问(有点像大括号)

总纲领就是“按需分配。

//需要一个1000个元素的int数组
int *p = (int *)malloc(1000*sizeof(int)); //malloc返回的是堆管理器分配的 首地址

//这个 首地址 在free之前绝对不能丢不能改。(内存泄漏)
if (NULL == p) //在使用前一定先检验。有时内存不足。很少内存也分配失败
{
	输出错误,return -1;
}

第一步:申请 第二步:检验 第三步:使用 第四步:释放 free§

注意: malloc(20)之后访问第1000个字节也是可以的,修改的值也是正确的。这是堆管理器决定的
但加到一定程度,太大的数就开始出现段错误了。
但我们千万不要越界访问。我们这次是对的,但下一次错误可能就是致命的

全局变量和malloc+free的方法的使用起来是一样的,唯一不同的是生命周期。
使用数据段(全局变量)的做法像是买房,malloc的方法像是租。慢慢会发现,租的方式更为普遍,应用也更加广泛。

============================================================

5.6 strlen & sizeof
strlen是不包括字符串结尾的’\0’的,strlen是测字符串长度的
char a[5] = “Kimo”
strlen 4
sizeof 5
char a[5]
strlen 5
sizeof 5
要注意的是,strlen传入一个 “不是字符串”的字符数组指针,是没有意义的
他也可能确实返回一些东西,但像a[5] = {2,3}这样的数组,是测不出字符串长度的。
没有找到’\0’就会一直向后查找,直到数组结束为止

sizeof典型例子

char* pStr1 = "1234567890";
//结果为 4 :指针型变量大小都是定值,大小为4;

char Str2[] = "1234567890";
//结果是 11 ,除了0-9还有隐藏的\0,占用大小为1 

char Str3[100] = "1234567890";
//结果为100;

int Str4[100];
//结果为400,int每个占用4个

char Str5[] = "abc\n";
//结果为5,除了abc还有\N \0 分别占用1 

char* pStr6 = "cde\n";
//结果为 4,指针

关于字符串和字符数组
字符数组 char a[] = “linux” 这个linux只存在于编译器上,初始化数组上之后就被丢掉
char a[] = {‘l’,‘i’,‘n’…,‘\0’};
字符串 char *p = “linux”; 在栈上会有4个字节 的指针,然后"linux"会保存在代码段

字符串比字符数组要灵活,因为字符串是可以放在很多地方的

char a[] char *p = 局部变量//字符串在栈上
char *p = 全局变量//字符串在数据段
char *p = (char *)malloc(5);//字符串在堆空间

============================================================

5.7 字节对齐

struct {
short sVal_b1;
long lVal_b2;
}B;

class D {
double D1;
float D2;
int D3;
char D4;
};

cout << sizeof(B) << endl; 
//对齐单位为 long 型,但是 4 + 2 = 6 不等于4的整数倍,对齐参数为8,这个时候就要补空字节

cout << sizeof(D) << endl;  
//double 8  int 4  float 4  char 1  数据对齐为24

总结:编译器设置为n字节对齐,那么最终是n的倍数
每个元素本身必须对齐存放,每个元素都有自己的对齐规则。

int a;
char b;
short c;

a a a a
b x c c //8字节

char a;
int b;
short c;

a x x x
b b b b
c c x x //12字节


5.10
gcc支持但不推荐的对齐指令,是用来指挥编译器的

#pragma pack(n) //n = 1,2,4,8...

有时候希望1字节对齐,或者是8字节对齐(甚至128字节对齐,比如视频编解码)
有各种各样的需求,编译器给我们提供了这个

#pragma pack( )

设置编译器1字节对齐,或者说“不对齐访问、取消编译器对齐访问【用在结尾】

#pragma pack(4) 

希望四字节对齐【用在开头】

gcc推荐的 写法:
attribute(packed); attribute(aligned(n));写在结构体后面

attribute(packed); 是每个类型的元素都保持对齐
attribute(aligned(n)); 结构体整个对齐。

============================================================

5.11 offsetof 宏 与container 【这节没听完,回头重听】

【重要】

struct  str1{
	char a; // 0
	int b; // 4
	short c; // 8
}


int main()
{
	struct str1 ;
	s1.b = 1; //第一种写法

	int *p = (int *)((char *)&s1 + 4);// 第二种写法,效果相同
	//有必要讲一下:这里(char *)强制转换是为了保证s1的地址不叠加,char就是1字节
	//然后+4是因为字节对齐,这样才能指向结构体的 属性
	//
return 0;
}

这里的两个值是相等的,问题是我们自己去算 4字节对齐这种方法是比较麻烦的
想让编译器帮我们计算,
所以我们有了offsetof宏

#define  offsetof(TYPE , MEMBER) ((size_t) &(TYPE *)0 -> MEMBER)

原理:虚拟一个type类型的结构体变量,然后用type.member的方式来访问member元素,继而得到他的偏移量。

int offsetofa = offsetof(struct str1 , a); 
printf(a); //打印结果应该是 0,相对于首地址的偏移量

int offsetofa = offsetof(struct str1 ,b); 
printf(b); //打印结果应该是 4,相对于首地址的偏移量

int offsetofa = offsetof(struct str1 ,c); 
printf(c); //打印结果应该是 8,相对于首地址的偏移量

offsetof宏组成原理:
(TYPE *)0 强制类型转换,把0地址转换成一个指针,指向了一个TYPE类型的结构体变量
实际上可能不存在这个结构体变量,不解引用就可以
(TYPE *)0 -> MEMBER
(TYPE *)0是个type类型的结构体变量指针,通过指针来访问结构体的member
&((TYPE *)0 -> MEMBER)
用上面得到的指针,去访问其中一个成员。得到的是一个member的地址
也就是相对于首地址的偏移量

container_of 宏
container_of(TYPE , MEMBER);
知道一个结构体中某个元素的指针,反推结构体变量的指针。

pS = conteiner_of (p, struct str1 ,c); //返回的 是 结构体的 地址

typeof(a)关键字:由变量名得到变量的数据类型。

关于这部分的面试要求:看懂,能理解其意思。如果能写出来那么水平已经爆炸了


5.13大小端模式
起源是从小说开始,吃鸡蛋是从大端吃起还是小端吃。《格列夫游记》
问题:串行通信中,一次只能发1个字节、那么我是按照byte0123还是byte3210顺序发送?
要求:发送方和接收方必须要用相同的收发顺序,否则通信错误

0x 1234 5678
高位 低位

【大端模式】(big endian)
高位→低地址 : 78 56 34 12cpu:C51
【小端模式】(little endian)
高位→高地址 : 12 34 56 78cpu:intel、ARM等大多数
于是我们写代码时,我们不知道是大端还是小端模式的时候,就要用代码检测当前系统大小端

经典C语言面试题:检测当前大小端
解法:

union  myunion
{
	int a ;char b;
}

int is_little_endian(void )
{
	union myunion u1;
	u1.a = 1;
	return u1.b;
}

int main(void)
{
	int i = is_little_endian();
	if (i == 1)
		{小端模式}
	else
		{大端模式}
	return 0;
}

图解:
a=1
高位 低位
00 00 00 01 小端模式
01 00 00 00 大端模式

共用体中非常重要一点。a和b都是从u1的低地址开始访问的。
如果是大端下的u1.a=1 。那么char b的值应该是0
【错误的测试方法集合】
位与:1与0xff位与运算不能测试出大小端模式,因为二进制运算高于内存层次,具有移植性
也就是说,&的时候一定是 高字节&高字节,低字节&低字节
移位:
int a,b;
a=1;
b = a>>1; //如果b是全零,那么就认为是小端模式
但大端小端模式下都是0。。。所以这种方法也是行不通的,原因同上

通讯中的大小端:
比如说要串口发送一个0x12345678,但因为串口限制只能以字节发送,分4次接受
0x12 0x34…这需要通信双方要有一个默契

============================================================

6.1 预处理

预处理 编译 汇编 链接
预处理帮编译器做一些零散的杂碎事物
1、#include
2、注释
3、条件编译#ifndef #undef
4、宏定义

gcc只预处理不编译的方法: gcc -E xx.c -o xx.i
gcc生成.o的方法: gcc xx.c -c -o xx.o

#include<>
#include"" 区别是""是自己的
编译器允许 -I来附加指定其他的包含路径去寻找头文件,这在裸机课程里面有用到
#include<>的真正含义就是在预处理中,将这句原地展开。原地,故而#include写后面将会出错!

注释:编译之前就把注释全部拿掉

#define NUM
int a=0;
#ifdef NUM //如果前面有定义NUM这个符号,成立
a=111;
#elif
a=222;
#endif

打印a的值
[输出结果:] #ifdef NUM a=111;

~#ifdef和#if的区别
~#ifdef是判断这个符号在本语句之前有没有被定义,只要定义了就是成立的
~#if(条件表达式) 判断表达式ture or false,有点像c语言的if

使用宏和函数的差别:
宏的功能是原地展开,函数的功能是跳转执行,两者有本质区别
除此之外,宏的开销小;而函数有比较大的调用开销

短小函数的缺陷在于:其调用开销 和 函数本身的开销相比,调用开销比较大。
就好比借书30分钟,却只看了5分钟,这是一种亏损。但如果看1个月的话30分钟就可以忽略了
据此我们引入了内联函数inline
inline不用调用开销,而是原地展开。(内来拿函数与是带了函数静态类型检查的宏)

当函数体很短,只有一两句话而且又希望不调用开销,多次调用的时候,最时候使用内联函数
Linux内核中,大量使用了inline函数

============================================================

6.5函数的本质

函数的出现是为了便于分工,是程序员的需要而非机器的需要。是程序的集合
函数书写的一般规则:
1、一定格式,包括返回值,函数名,参数列表
2、一个函数只做一件事
3、传参不要太多 【注意:如果参数过多,那么最好传递一个结构体地址进去】
4、少用全局变量 【最好用传参、返回值来和外部交换数据】

============================================================

6.7递归函数

递归就是一层一层按照相同的裸机运行下去
递归函数就是调用了自己的 函数
举例:递归函数实现阶乘

int jiecheng(int n)
{
	if(n==1)
	{return 1;}
else 
	return (n * jiecheng(n-1));
}

int main()
{
	int ret=0;
	ret = jiecheng(5);
}

============================================================

6.8函数库

静态链接库,商业公司将自己的函数库源代码 只编译不链接,形成.o的目标文件,然后用ar文件工具将.o文件归档成.a(.a的归档文件,又称为静态链接库文件)
商业公司通过发布.a 和.h文件提供静态库给客户使用
客户拿到.a 和.h后,通过.h得知库函数的原型,然后在自己的.c直接调用
在链接的时候,链接器会去.a文件中拿出被调用的.o二进制代码链接成最终的elf。这样就让你能用我的代码,又不能直接看到我的源代码,对版权进行了保护

动态链接库比静态链接库出现的晚一些,效率也高一些,是改进型的。我们现在一般用动态库

打印“helloworld”的程序,使用printf
gcc xxx.c 编译生成 的 a.out 大小为7000+字节
gcc xxx.c -static 编译生成 的 a.out 大小为700000+字节!!
原因:
静态链接库(.a):
我的程序只有一点点,5%
printf代码段比较大,95%
动态链接库(.so):
我的程序只有一点点,5%
发现调用了printf,但 [不把他链接过来] 0%
//自己什么都没有,但会在运行的时候由操作系统帮我们加载到内存中来,源程序里面没有
使用静态链接库在每个程序的代码段中都有一段库函数。如果A/B/C三个程序都用到了同样的函数,那么就白白浪费了内存。使用动态库不管有多少个程序调用,都会跳转到第一次加载的地方去执行。

============================================================

6.9字符串函数(需要下一点功夫)

字符串处理函数 是 需要,面试里面也是经常考到的

c库中字符串处理函数包含在string.h中,在Ubuntu中在 /usr/include/string.h中
memcpy
memmove
memset
memcmp
memchr

strcpy
strncpy
strcat
strncat
strcmp
strdup
strchr
strstr
strtok

============================================================

6.10 Math库函数

math库中有一些库函数和子函数,因为库函数很多,搜索起来比较久,ld默认只查找几个最常用的库,如果是一些不常用的库函数被调用,需要程序员在链接时明确给出要扩展查找库的名字。
gcc xxx.c -lm //在libm下查找库函数
【当使用sqrt()这样的函数时候是需要的】

Linux :ldd命令
ldd a.out 查看链接库。

============================================================

6.11静态链接库(.a)

【文件KimonoYan.c】 //函数/过程是不可见的,属于商业机密所以不发布

void func1(void)
{
	printf("这是func1函数");
}

【文件KimonoYan.h】 //函数原型声明

void func1(void );

【Makefile】

all:
	gcc KimonoYan.c -o KimonoYan.o -c// -c只编译不链接,因为要做库文件
	ar -rc libKimonoYan.a KimonoYan.o//ar叫归档器,创建静态链接库

	// -rc就是创建,KimonoYan.o是原材料
	//注意这个静态链接库名字是不能随便乱起的。lib+库名称.a

【release.c】

#include "KimonoYan.h"
int main()
{
	func1();
} 

查找过程:
1、把.a .h都放在引用的文件夹下,然后在.c 文件中包含.h
2、gcc release.c //报错(.text+0xa)系链接时报错,不是默认链接库的范围,需要指定
/tmp/ccdwCeMW.o:在函数‘main’中:
release.c:(.text+0x7):对‘func’未定义的引用
collect2: error: ld returned 1 exit status
3、gcc release.c -lKimonoYan //告诉链接器去找KimonoYan 但还是报错
/usr/bin/ld: cannot find -lKimo
collect2: error: ld returned 1 exit status
4、gcc release.c -lKimonoYan -L. //告诉链接器去当前目录下去找(.)
OK

完整步骤
1、touch Kimotse.c,这个文件将是保密的
gcc KimonoYan.c -o KimonoYan.o -c
ar -rc libKimonoYan.a KimonoYan.o
生成libKimonoYan.a文件

2、gcc release.c -o test -lKimonoYan -L. //编译生成a.out

============================================================

6.12动态链接库
动态链接库(.so),反映在windows中,就是.dll
【makefile】

gcc Kimo.c -o Kimo.o -c -fPIC //编译成位置无关码,被OS加载到什么地方是不一定的
gcc -o libKimo.so Kimo.o -shared //用共享库的方式来连接,即生成libKimo.so

gcc release.c -o test -lKimo -L. //编译无错,执行报错
//原因:静态链接库相当于在.a文件中在执行前复制了代码函数到执行文件中
但是动态链接库执行release的时候会先把.so整个加载到内存中,然后通过标签去调用
运行时需要被加载,加载目录还是系统特定的。
【解决方法1】:将libKimo放到固定目录,一般是/usr/lib
cp libKimo.so /usr/lib
【解决方法2】:使用环境变量LD_LIBRARY_PATH,操作系统在加载固定目录之前,会先去这个环境变量所指定的目录下寻找,如果找到就不用去usr/lib下找了
export LD_LIBRARY_PATH = $LD_LIBRARY_PATH : (.so当前目录)
查看:echo $LD_LIBRARY_PATH
【解决方法3】:使用ldconfig,不过不是很建议用

ldd命令
在一个使用了共享库的程序执行之前,查看哪些共享库被引用了

KimonoYan@KimonoYan-virtual-machine:~/vcdir/vc_review/035_静态链接库$ ldd
a.out linux-gate.so.1 => (0xb7768000) libc.so.6 =>
/lib/i386-linux-gnu/libc.so.6 (0xb75a1000) /lib/ld-linux.so.2
(0xb776a000)

帮助我们解析动态链接库

============================================================

7.1 存储类 作用域 生命周期 链接属性
是描述一个变量的关键概念。
存储类:即存储类型。内存有多种存储方法:堆、栈、数据段、代码段、bss段
作用域:在哪里有用,哪里没用的问题
for(i=0;?;?)
{
int a;
}
a++; .//报错:a未定义
同名的局部变量、全局变量的作用域优先级问题。
链接属性:外链接属性 内链接属性 无链接属性

============================================================

7.2Linux下的C程序内存映像

Linux C的内存模型
–地址:c000 0000
内核虚拟存储器 用户不可见
栈 ESP指向栈顶
文件映射区
运行时堆 通过brk/sbrk系统调用扩大堆,向上增长
数据段、bss段
只读段、代码段
–地址:0804 8000

文件映射区:进程打开了文件后,将这个文件的内容从硬盘打开到进程的区域。以后就直接在内存中操作这个文件,读写完之后在保存时再写回到硬盘中去。
//这个映射 和 裸机里面那个 内存映射是一样的

内核映射区:就是将我们操作系统的内核程序映射到这个区域,对于LINUX中的每个进程来说,他以为只有他自己和内核。他以为0xc000 0000以下都是他自己的活动空间,以上都是自己的活动空间。


| Linux Kernal |…
| | | |…
进程1 进程2 进程3 …


每一个进程都活在自己的虚拟空间,实际不同,但内核是唯一的(在应用编程中详细讲!)

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值