C语言学习

冯诺依曼结构和哈佛结构

冯诺依曼结构是:数据和代码放在一起。

哈佛结构是:数据和代码分开存在。

什么是代码:函数

什么是数据:全局变量、局部变量

在S5PV210中运行的linux系统上,运行应用程序时:这时候所有的应用程序的代码和数据都在DRAM,所以这种结构就是冯诺依曼结构;在单片机中,我们把程序代码烧写到Flash(NorFlash)中,然后程序在Flash中原地运行,程序中所涉及到的数据(全局变量、局部变量)不能放在Flash中,必须放在RAM(SRAM)中。这种就叫哈佛结构。

单片机并不用把程序加载到RAM中,ARM采用哈弗结构,SRAM取数据,FLASH取指令,两者同时进行.

DRAM是动态内存,SRAM是静态内存。
所谓的“静态”,是指这种存储器只要保持通电,里面储存的数据就可以恒常保持。相对之下,动态随机存取存储器(DRAM)里面所储存的数据就需要周期性地更新。
SRAM与DRAM的区别只在于一个是静态一个是动态。由于SRAM不需要刷新电路就能够保存数据,所以具有静止存取数据的作用。而DRAM则需要不停地刷新电路,否则内部的数据将会消失。而且不停刷新电路的功耗是很高的,在我们的PC待机时消耗的电量有很大一部分都来自于对内存的刷新。SRAM存储一位需要花6个晶体管,而DRAM只需要花一个电容和一个晶体管。cache追求的是速度所以选择SRAM,而内存则追求容量所以选择能够在相同空间中存放更多内容并且造价相对低廉的DRAM。
DRAM的数据实际上是存在电容里的。而电容放久了,内部的电荷就会越来越少,对外就形成不了电位的变化。而且当对DRAM进行读操作的时候需要将电容与外界形成回路,通过检查是否有电荷流进或流出来判断该bit是1还是0。所以无论怎样,在读操作中我们都破坏了原来的数据。所以在读操作结束后需要将数据写回DRAM中。在整个读或者写操作的周期中,计算机都会进行DRAM的刷新,通常是刷新的周期是4ms-64ms。
  关于SRAM和DRAM的寻址方式也有所不同。虽然通常我们都认为内存像一个长长的数组呈一维排列,但实际上内存是以一个二维数组的形式排列的,每个单元都有其行地址和列地址,当然cache也一样。而这两者的不同在于对于容量较小的SRAM,我们可以将行地址和列地址一次性传入到SRAM中,而如果我们对DRAM也这样做的话,则需要很多很多根地址线(容量越大,地址越长,地址位数越多)。所以我们选择分别传送行地址和列地址到DRAM中。先选中一整行,然后将整行数据存到一个锁存器中,等待列地址的传送然后选中所需要的数据。这也是为什么SRAM比DRAM快的原因之一。

  1. 静态内存
    静态内存是指在程序开始运行时由编译器分配的内存,它的分配是在程序开始编译时完成的,不占用CPU资源。
    程序中的各种变量,在编译时系统已经为其分配了所需的内存空间,当该变量在作用域内使用完毕时,系统会自动释放所占用的内存空间。
    变量的分配与释放,都无须程序员自行考虑。
    eg:
    基本类型,数组

  2. 动态内存
    用户无法确定空间大小,或者空间太大,栈上无法分配时,会采用动态内存分配。

  3. 区别
    a) 静态内存分配在编译时完成,不占用CPU资源; 动态内存分配在运行时,分配与释放都占用CPU资源。
    b) 静态内存在栈(stack)上分配; 动态内存在堆(heap)上分配。
    c) 动态内存分配需要指针和引用类型支持,静态不需要。
    d) 静态内存分配是按计划分配,由编译器负责; 动态内存分配是按需分配,由程序员负责。

    再从语言角度来讲:不同的语言提供了不同的操作内存的接口。

    譬如汇编:根本没有任何内存管理,内存管理全靠程序员自己,汇编中操作内存时直接使用内存地址(譬如0xd0020010),非常麻烦;

    譬如C语言:C语言中编译器帮我们管理直接内存地址,我们都是通过编译器提供的变量名等来访问内存的,操作系统下如果需要大块内存,可以通过API(malloc free)来访问系统内存。裸机程序中需要大块的内存需要自己来定义数组等来解决。

    譬如C++语言:C++语言对内存的使用进一步封装。我们可以用new来创建对象(其实就是为对象分配内存),然后使用完了用delete来删除对象(其实就是释放内存)。所以C++语言对内存的管理比C要高级一些,容易一些。但是C++中内存的管理还是靠程序员自己来做。如果程序员new了一个对象,但是用完了忘记delete就会造成这个对象占用的内存不能释放,这就是内存泄漏。

    Java/C#等语言:这些语言不直接操作内存,而是通过虚拟机来操作内存。这样虚拟机作为我们程序员的代理,来帮我们处理内存的释放工作。如果我的程序申请了内存,使用完成后忘记释放,则虚拟机会帮我释放掉这些内存。听起来似乎C# java等语言比C/C++有优势,但是其实他这个虚拟机回收内存是需要付出一定代价的,所以说语言没有好坏,只有适应不适应。当我们程序对程序运行效率非常在乎的时候(譬如操作系统内核)就会用C/C++语言;当我们对开发程序的效率非常在乎的时候,就会用Java/C#等语言。

C语言中,函数就是一段代码的封装。函数名的实质就是这一段代码的首地址。所以说函数名的本质也是一个内存地址。

struct s

{

	int age;					// 普通变量

	void (*pFunc)(void);		// 函数指针,指向 void func(void)这类的函数

};

使用这样的结构体就可以实现面向对象。

这样包含了函数指针的结构体就类似于面向对象中的class,结构体中的变量类似于class中的成员变量,结构体中的函数指针类似于class中的成员方法。

C语言操作堆内存的接口(malloc free)

堆内存释放时最简单,直接调用free释放即可。 void free(void *ptr);

堆内存申请时,有3个可选择的类似功能的函数:malloc, calloc, realloc

void *malloc(size_t size);

void *calloc(size_t nmemb, size_t size);	// nmemb个单元,每个单元size字节

void *realloc(void *ptr, size_t size);		// 改变原来申请的空间的大小的

譬如要申请10个int元素的内存:

malloc(40);			malloc(10*sizeof(int));

calloc(10, 4);		calloc(10, sizeof(int));

数组定义时必须同时给出数组元素个数(数组大小),而且一旦定义再无法更改。在Java等高级语言中,有一些语法技巧可以更改数组大小,但其实这只是一种障眼法。它的工作原理是:先重新创建一个新的数组大小为要更改后的数组,然后将原数组的所有元素复制进新的数组,然后释放掉原数组,最后返回新的数组给用户;

堆内存申请时必须给定大小,然后一旦申请完成大小不变,如果要变只能通过realloc接口。realloc的实现原理类似于上面说的Java中的可变大小的数组的方式。

位操作时,特定位清零用&,特定位置1用|,特定位取反用^
获取bit3~bit7为1,同时bit23~bit25为1,其余位为0的数:((0x1f<<3) | (7<<23))
获取bit4~bit10为0,其余位全部为1的数: ~(0x7f<<4)

位与、位或结合特定二进制数即可完成寄存器位操作需求

(1)如果你要的这个数比较少位为1,大部分位为0,则可以通过连续很多个1左移n位得到。

(2)如果你想要的数是比较少位为0,大部分位为1,则可以通过先构建其位反数,然后再位取反来得到。

(3)如果你想要的数中连续1(连续0)的部分不止1个,那么可以通过多段分别构造,然后再彼此位或即可。这时候因为参与位或运算的各个数为1的位是不重复的,所以这时候的位或其实相当于几个数的叠加。

用宏定义来完成位运算
直接用宏来置位、复位(最右边为第1位)。

#define SET_NTH_BIT(x, n)  (x | ((1U)<<(n-1)))

#define CLEAR_NTH_BIT(x, n) (x & ~((1U)<<(n-1)))

截取变量的部分连续位。例如:变量0x88, 也就是10001000b,若截取第2~4位,则值为:100b = 4

#define GETBITS(x, n, m) ((x & ~(~(0U)<<(m-n+1))<<(n-1)) >> (n-1)) 

(1)指针的出现是为了实现间接访问。在汇编中都有间接访问,其实就是CPU的寻址方式中的间接寻址。

(2)间接访问(CPU的间接寻址)是CPU设计时决定的,这个决定了汇编语言必须能够实现间接寻址,又决定了汇编之上的C语言也必须实现简介寻址。

(3)高级语言如Java、C#等没有指针,那他们怎么实现间接访问?答案是语言本身帮我们封装了。

const修饰指针有4种形式:

第一种:const int *p;

第二种:int const *p;

第三种:int * const p;

第四种:const int * const p;

关于指针变量的理解,主要涉及到2个变量:第一个是指针变量p本身,第二个是p指向的那个变量(*p)。一个const关键字只能修饰一个变量,所以弄清楚这4个表达式的关键就是搞清楚const放在某个位置是修饰谁的const变量可以被修改,是因为gcc把const类型的常量也放在了data段,其实和普通的全局变量放在data段是一样实现的,只是通过编译器认定这个变量是const的,运行时并没有标记const标志,所以只要骗过编译器就可以修改了。

对数组名进行&操作,并不是取其地址,而是得到了指向整个数组的指针。也就是说,arr与&arr指向的是同一个地址,但是他们的类型不一样。arr相当于&arr[0],类型是int ,而&arr是指向整个数组的指针,类型是int ()[5]。

int、char、short等属于整形,他们的存储方式(数转换成二进制往内存中放的方式)是相同的,只是内存格子大小不同(所以这几种整形就彼此叫二进制兼容格式);而float和double的存储方式彼此不同,和整形更不同

sizeof是C语言的一个运算符

sizeof(数组名)实际返回的是整个数组所占用内存空间(以字节为单位的)

void fun(int b[100])

{

	sizeof(b)}			

(1)函数传参,形参是可以用数组的

(2)函数形参是数组时,实际传递是不是整个数组,而是数组的首元素首地址(?我觉得是传递了数组名,它代表数组地址,只是值刚好与首元素首地址相同)。也就是说函数传参用数组来传,实际相当于传递的是指针(指针指向数组的首元素首地址)。

	#define dpChar char *

typedef char *tpChar;



    dpChar p1,  p2;         // 展开:char *p1, p2; 相当于char *p1, char p2;
    tpChar p3,  p4;         // 等价于:char *p3, char *p4;  
    printf("sizeof(p1) = %d.\n", sizeof(p1));       // 4
    printf("sizeof(p2) = %d.\n", sizeof(p2));       // 1
    printf("sizeof(p3) = %d.\n", sizeof(p3));       // 4
    printf("sizeof(p4) = %d.\n", sizeof(p4));       // 4

#include <stdio.h>
int func(int a[10])
{
    printf("func \
            sizeof(a) = %d, \
            $a + 1 = 0x%x, \
            $a = 0x%x, \
            a = 0x%x\n",
            sizeof(a),
            &a + 1, 
            &a, 
            a);
}
int main(void)
{
    int a[8] = {0};
    printf("main \
            sizeof(a) = %d, \
            $a + 1 = 0x%x, \
            $a = 0x%x, \
            a = 0x%x\n", 
            sizeof(a),
            &a + 1, 
            &a, 
            a);
    func(a);
}

运行结果

main             sizeof(a) = 32,             $a + 1 = 0x5487e4e0,             $a = 0x5487e4c0,             a = 0x5487e4c0

func             sizeof(a) = 8,             $a + 1 = 0x5487e4b0,             $a = 0x5487e4a8,             a = 0x5487e4c0

上面说明:
数组作为形参时,传进去的就是一个指向数组首地址的指针,不包含任何数组长度信息。

结构体变量作为函数形参的时候,实际上和普通变量(类似于int之类的)传参时表现是一模一样的,也是传了一份值的拷贝。

函数名是一个符号,表示整个函数代码段的首地址,实质是一个指针常量,所以在程序中使用到函数名时都是当地址用的,用来调用这个函数的。

int *p[5];
核心是p,p是一个数组,数组有5个元素大,数组中的元素都是指针,指针指向的元素类型是int类型的;整个符号是一个指针数组。

int (*p)[5];

核心是p,p是一个指针,指针指向一个数组,数组有5个元素,数组中存的元素是int类型;整个符号的意义就是数组指针。

// 这句重命名了一种类型,这个新类型名字叫pType,类型是:char* (*)(char *, const char *);
typedef char* (*pType)(char *, const char *);
// 函数指针数组
typedef char* (*pType[5])(char *, const char *);
// 函数指针数组指针
typedef char* (*(*pType)[5])(char *, const char *);

使用typedef一次定义2个类型,分别是结构体变量类型,和结构体变量指针类型。

typedef struct teacher
{
    char name[20];
    int age;
    int mager;
}teacher, *pTeacher;

(1)typedef int *PINT; const PINT p2; 相当于是int *const p2;

(2)typedef int *PINT; PINT const p2; 相当于是int *const p2;

(3)如果确实想得到const int *p;这种效果,只能typedef const int *CPINT; CPINT p1;

二维数组的下标式访问和指针式访问

(1)回顾:一维数组的两种访问方式。以int b[10]为例, int *p = b;。

b[0] 等同于 *(p+0); b[9] 等同于 *(p+9); b[i] 等同于 *(p+i)

(2)二维数组的两种访问方式:以int a[2][5]为例,(合适类型的)p = a;

a[0][0]等同于*(*(p+0)+0); a[i][j]等同于 ((p+i)+j)

在一个C语言程序中,能够获取的内存就是三种情况:栈(stack)、堆(heap)、数据段(.data)

malloc(0)

malloc申请0字节内存本身就是一件无厘头事情,一般不会碰到这个需要。

如果真的malloc(0)返回的是NULL还是一个有效指针?答案是:实际分配了16Byte的一段内存并且返回了这段内存的地址。这个答案不是确定的,因为C语言并没有明确规定malloc(0)时的表现,由各malloc函数库的实现者来定义。

malloc(4)

gcc中的malloc默认最小是以16B为分配单位的。如果malloc小于16B的大小时都会返回一个16字节的大小的内存。malloc实现时没有实现任意自己的分配而是允许一些大小的块内存的分配。

bss段(又叫ZI(zero initial)段):bss段的特点就是被初始化为0,bss段本质上也是属于数据段,bss段就是被初始化为0的数据段。

注意区分:数据段(.data)和bss段的区别和联系:二者本来没有本质区别,都是用来存放C程序中的全局变量的。区别在于把显示初始化为非零的全局变量存在.data段中,而把显式初始化为0或者并未显式初始化(C语言规定未显式初始化的全局变量值默认为0)的全局变量存在bss段。

有些特殊数据会被放到代码段

(1)C语言中使用char *p = “linux”;定义字符串时,字符串"linux"实际被分配在代码段,也就是说这个"linux"字符串实际上是一个常量字符串而不是变量字符串。

(2)const型常量:C语言中const关键字用来定义常量,常量就是不能被改变的量。const的实现方法至少有2种:第一种就是编译将const修饰的变量放在代码段去以实现不能修改(普遍见于各种单片机的编译器);第二种就是由编译器来检查以确保const型的常量不会被修改,实际上const型的常量还是和普通变量一样放在数据段的(gcc中就是这样实现的)。

显式初始化为非零的全局变量和静态局部变量放在数据段

(1)放在.data段的变量有2种:第一种是显式初始化为非零的全局变量。第二种是静态局部变量,也就是static修饰的局部变量。(普通局部变量分配在栈上,静态局部变量分配在.data段)

未初始化或显式初始化为0的全局变量放在bss段

(1)bss段和.data段并没有本质区别,几乎可以不用明确去区分这两种。

字符数组与字符串的本质差异(内存分配角度)

(1)字符数组char a[] = “linux”;来说,定义了一个数组a,数组a占6字节,右值"linux"本身只存在于编译器中,编译器将它用来初始化字符数组a后丢弃掉(也就是说内存中是没有"linux"这个字符串的);这句就相当于是:char a[] = {‘l’, ‘i’, ‘n’, ‘u’, ‘x’, ‘\0’};

(2)字符串char *p = “linux”;定义了一个字符指针p,p占4字节,分配在栈上;同时还定义了一个字符串"linux",分配在代码段;然后把代码段中的字符串(一共占6字节)的首地址(也就是’l’的地址)赋值给p。

总结对比:字符数组和字符串有本质差别。字符数组本身是数组,数组自身自带内存空间,可以用来存东西(所以数组类似于容器);而字符串本身是指针,本身永远只占4字节,而且这4个字节还不能用来存有效数据,所以只能把有效数据存到别的地方,然后把地址存在p中。

也就是说字符数组自己存那些字符;字符串一定需要额外的内存来存那些字符,字符串本身只存真正的那些字符所在的内存空间的首地址。

gcc支持但不推荐的对齐指令:#pragma pack() #pragma pack(n) (n=1/2/4/8)

(1)#pragma是用来指挥编译器,或者说设置编译器的对齐方式的。编译器的默认对齐方式是4,但是有时候我不希望对齐方式是4,而希望是别的(譬如希望1字节对齐,也可能希望是8,甚至可能希望128字节对齐)。

(2)常用的设置编译器编译器对齐命令有2种:第一种是#pragma pack(),这种就是设置编译器1字节对齐(有些人喜欢讲:设置编译器不对齐访问,还有些讲:取消编译器对齐访问);第二种是#pragma pack(4),这个括号中的数字就表示我们希望多少字节对齐。

(3)我们需要#prgama pack(n)开头,以#pragma pack()结尾,定义一个区间,这个区间内的对齐参数就是n。

(4)#prgma pack的方式在很多C环境下都是支持的,但是gcc虽然也可以不过不建议使用。

gcc推荐的对齐指令__attribute__((packed)) attribute((aligned(n)))

(1)attribute((packed))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。

(2)attribute((aligned(n)))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐)

// TYPE是结构体类型,MEMBER是结构体中一个元素的元素名
// 这个宏返回的是member元素相对于整个结构体变量的首地址的偏移量,类型是int
#define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)

offsetof宏:

(1)offsetof宏的作用是:用宏来计算结构体中某个元素和结构体首地址的偏移量(其实质是通过编译器来帮我们计算)。

(2)offsetof宏的原理:我们虚拟一个type类型结构体变量,然后用type.member的方式来访问那个member元素,继而得到member相对于整个变量首地址的偏移量。

// ptr是指向结构体元素member的指针,type是结构体类型,member是结构体中一个元素的元素名
// 这个宏返回的就是指向整个结构体变量的指针,类型是(type *)
#define container_of(ptr, type, member) ({          \
    const typeof(((type *)0)->member) * __mptr = (ptr); \
    (type *)((char *)__mptr - offsetof(type, member)); })

container_of宏:

(1)作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。有了container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针。

(2)typeof关键字的作用是:typeof(a)时由变量a得到a的类型,typeof就是由变量名得到变量数据类型的。

(3)这个宏的工作原理:先用typeof得到member元素的类型定义成一个指针,然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到的),减去之后得到的就是整个结构体变量的首地址了,再把这个地址强制类型转换为type *即可。

#include <stdio.h>
// 共用体中很重要的一点:a和b都是从u1的低地址开始的。
// 假设u1所在的4字节地址分别是:0、1、2、3的话,那么a自然就是0、1、2、3;
// b所在的地址是0而不是3.
union myunion
{
    int a;
    char b;
};
// 如果是小端模式则返回1,小端模式则返回0
int is_little_endian(void)
{
    union myunion u1;
    u1.a = 1;               // 地址0的那个字节内是1(小端)或者0(大端)
    return u1.b;
}
int is_little_endian2(void)
{
    int a = 1;
    char b = *((char *)(&a));       // 指针方式其实就是共用体的本质
    
    return b;
}
int main(void)
{
    int i = is_little_endian2();
    if (i == 1)
    {
        printf("小端模式\n");
    }
    else
    {
        printf("大端模式\n");
    }
    
    return 0;
}

看似可行实则不行的测试大小端方式:位与、移位、强制类型转化

(1)位与运算。

结论:位与的方式无法测试机器的大小端模式。(表现就是大端机器和小端机器的&运算后的值相同的)

理论分析:位与运算是编译器提供的运算,这个运算是高于内存层次的(或者说&运算在二进制层次具有可移植性,也就是说&的时候一定是高字节&高字节,低字节&低字节,和二进制存储无关)。

(2)移位

结论:移位的方式也不能测试机器大小端。

理论分析:原因和&运算符不能测试一样,因为C语言对运算符的级别是高于二进制层次的。右移运算永远是将低字节移除,而和二进制存储时这个低字节在高位还是低位无关的。

(3)强制类型转换

同上

源码.c->(预处理)->预处理过的.i源文件->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序

编程中常见的预处理

(1)#include(#include <>和#include ""的区别)

(2)注释

(3)#if #elif #endif #ifdef

(4)宏定义

gcc中只预处理不编译的方法

(1)gcc编译时可以给一些参数来做一些设置,譬如gcc xx.c -o xx可以指定可执行程序的名称;譬如gcc xx.c -c -o xx.o可以指定只编译不连接,也可以生成.o的目标文件。

(2)gcc -E xx.c -o xx.i可以实现只预处理不编译。一般情况下没必要只预处理不编译,但有时候这种技巧可以用来帮助我们研究预处理过程,帮助debug程序。

总结:宏定义被预处理时的现象有:第一,宏定义语句本身不见了(可见编译器根本就不认识#define,编译器根本不知道还有个宏定义);第二,typedef重命名语言还在,说明它和宏定义是有本质区别的(说明typedef是由编译器来处理而不是预处理器处理的);

宏定义示例1:MAX宏,求2个数中较大的一个

#define MAX(a, b) (((a)>(b)) ? (a) : (b))

关键:

第一点:要想到使用三目运算符来完成。

第二点:注意括号的使用

宏定义示例2:SEC_PER_YEAR,用宏定义表示一年中有多少秒

#define SEC_PER_YEAR (3652460*60UL)

关键:

第一点:当一个数字直接出现在程序中时,它的是类型默认是int

第二点:一年有多少秒,这个数字刚好超过了int类型存储的范围

宏定义和函数的差别:
宏定义是原地展开,因此没有调用开销;而函数是跳转执行再返回,因此函数有比较大的调用开销。
宏定义不会检查参数的类型,返回值也不会附带类型;而函数有明确的参数类型和返回值类型。

(1)内联函数通过在函数定义前加inline关键字实现。

(2)内联函数本质上是函数,所以有函数的优点(内联函数是编译器负责处理的,编译器可以帮我们做参数的静态类型检查);但是他同时也有带参宏的优点(不用调用开销,而是原地展开)。所以几乎可以这样认为:内联函数就是带了参数静态类型检查的宏。

(3)当我们的函数内函数体很短(譬如只有一两句话)的时候,我们又希望利用编译器的参数类型检查来排错,我还希望没有调用开销时,最适合使用内联函数。

实际上递归函数是在栈内存上递归执行的,每次递归执行一次就需要耗费一些栈内存。

栈内存的大小是限制递归深度的重要因素。

自己制作静态链接库并使用

(1)第一步:自己制作静态链接库

首先使用gcc -c只编译不连接,生成.o文件;然后使用ar工具进行打包成.a归档文件

库名不能随便乱起,一般是lib+库名称,后缀名是.a表示是一个归档文件

注意:制作出来了静态库之后,发布时需要发布.a文件和.h文件。

(2)第二步:使用静态链接库

把.a和.h都放在我引用的文件夹下,然后在.c文件中包含库的.h,然后直接使用库函数。

all:
gcc aston.c -o aston.o -c
ar -rc libaston.a aston.o

库函数的使用需要注意3点:第一,包含相应的头文件;第二,调用库函数时注意函数原型;第三,有些库函数链接时需要额外用-lxxx来指定链接;第四,如果是动态库,要注意-L指定动态库的地址。
编译方法:gcc test.c -o test -laston -L.

除了ar命令外,还有个nm命令也很有用,它可以用来查看一个.a文件中都有哪些符号
nm libaston.a

自己制作动态链接库并使用

(1)动态链接库的后缀名是.so(对应windows系统中的dll),静态库的扩展名是.a

(2)第一步:创建一个动态链接库。

gcc aston.c -o aston.o -c -fPIC

gcc -o libaston.so aston.o -shared 

	-fPIC是位置无关码,-shared是按照共享库的方式来链接。

注意:做库的人给用库的人发布库时,发布libxxx.so和xxx.h即可。
编译方法:gcc test.c -o test -laston -L.

但是运行出错,报错信息:

error while loading shared libraries: libaston.so: cannot open shared object file: No such file or directory

错误原因:动态链接库运行时需要被加载(运行时环境在执行test程序的时候发现他动态链接了libaston.so,于是乎会去固定目录尝试加载libaston.so,如果加载失败则会打印以上错误信息。)

从这里能看出静态库和动态库的区别
静态库在编译时,就把库里有用到的函数编译进可执行程序了,动态库则是需要在运行时加载。

解决方法一:

将libaston.so放到固定目录下就可以了,这个固定目录一般是/usr/lib目录。

cp libaston.so /usr/lib即可

解决方法二:使用环境变量LD_LIBRARY_PATH。操作系统在加载固定目录/usr/lib之前,会先去LD_LIBRARY_PATH这个环境变量所指定的目录下去寻找,如果找到就不用去/usr/lib下面找了,如果没找到再去/usr/lib下面找。所以解决方案就是将libaston.so所在的目录导出到环境变量LD_LIBRARY_PATH中即可。

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/hgfs/Winshare/s5pv210/AdvancedC/4.6.PreprocessFunction/4.6.12.sharedobject.c/sotest

在ubuntu中还有个解决方案三,用ldconfig

(4)ldd命令:作用是可以在一个使用了共享库的程序执行之前解析出这个程序使用了哪些共享库,并且查看这些共享库是否能被找到,能被解析(决定这个程序是否能正确执行)。
ldd test

register修饰的变量。编译器会尽量将它分配在寄存器中。(平时分配的一般的变量都是在内存中的)。分配在寄存器中一样的用,但是读写效率会高很多。所以register修饰的变量用在那种变量被反复高频率的使用,通过改善这个变量的访问效率可以极大的提升程序运行效率时。所以register是一种极致提升程序运行效率的手段。

中断isr中引用的变量,多线程中共用的变量,硬件会更改的变量,都是编译器在编译时无法预知的更改,此时应用使用volatile告诉编译器这个变量属于这种(可变的、易变的)情况。编译器在遇到volatile修饰的变量时就不会对改变量的访问进行优化,就不会出现错误。

关键字restrict只用于限定指针;该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于(base on)该指针的,即不存在其它进行修改操作的途径;这样的后果是帮助编译器进行更好的代码优化,生成更有效率的汇编代码

(1)普通(自动)局部变量分配在栈上,作用域为代码块作用域,生命周期是临时,连接属性为无连接。定义时如果未显式初始化则其值随机,变量地址由运行时在栈上分配得到,多次执行时地址不一定相同,函数不能返回该类变量的地址(指针)作为返回值。

(2)静态局部变量分配在数据段/bss段(显式初始化为非0则在数据段,显式初始化为0或未显示初始化则在bss段),作用域为代码块作用域(人为规定的),生命周期为永久(天然的),链接属性为无连接(天然的)。定义时如果未显式初始化则其值为0(天然的),变量地址由运行时环境在加载程序时确定,整个程序运行过程中唯一不变;静态局部变量其实就是作用域为代码块作用域(同时链接属性为无连接)的全局变量。静态局部变量可以改为用全局变量实现(程序中尽量避免用全局变量,因为会破坏结构性)。

(3)静态全局变量/静态函数和普通全局变量/普通函数的唯一差别是:static使全局变量/函数的链接属性由外部链接(整个程序所有文件范围)转为内部链接(当前c文件内)。这是为了解决全局变量/函数的重名问题(C语言没有命名空间namespace的概念,因此在程序中文件变多之后全局变量/函数的重名问题非常严重,将不必要被其他文件引用的全局变量/函数声明为static可以很大程度上改善重名问题,但是仍未彻底解决)。

(4)写程序尽量避免使用全局变量,尤其是非static类型的全局变量。能确定不会被其他文件引用的全局变量一定要static修饰。

(5)注意区分全局变量的定义和声明。一般规律如下:如果定义的同时有初始化则一定会被认为是定义;如果只是定义而没有初始化则有可能被编译器认为是定义,也可能被认为是声明,要具体分析;如果使用extern则肯定会被认为是声明(实际上使用extern也可以有定义,实际上加extern就是明确声明这个变量为外部链接属性)。

(6)全局变量应该定义在c文件中并且在头文件中声明,而不要定义在头文件中(因为如果定义在头文件中,则该头文件被多个c文件包含时该全局变量会重复定义)。

(7)在b.c中引用a.c中定义的全局变量/函数有2种方法:一是在a.h中声明该函数/全局变量,然后在b.c中#include <a.h>;二是在b.c中使用extern显式声明要引用的函数/全局变量。其中第一种方法比较正式。

(8)存储类决定生命周期,作用域决定链接属性

(9)宏和inline函数的链接属性为无连接。

C++的编译环境中,编译器预先定义了一个宏_cplusplus,程序中可以用条件编译来判断当前的编译环境是C++的还是C的。

链表还有另外一种用法,就是把头指针指向的第一个节点作为头节点使用。头节点的特点是:第一,它紧跟在头指针后面。第二,头节点的数据部分是空的(有时候不是空的,而是存储整个链表的节点数),指针部分指向下一个节点,也就是第一个节点。

linux内核链表中实现了一个纯链表(纯链表就是没有数据区域,只有前后向指针)的封装,以及纯链表的各种操作函数(节点创建、插入、删除、遍历······)。这个纯链表本身自己没有任何用处,它的用法是给我们具体链表作为核心来调用。

list.h文件简介

(1)内核中核心纯链表的实现在include/linux/list.h文件中

(2)list.h中就是一个纯链表的完整封装,包含节点定义和各种链表操作方法。
设计的使用方法是将内核链表作为将来整个数据结构的结构体的一个成员内嵌进去。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值