1.写程序思路:
1).头文件重要性:
a.编译器没有看到func原型,默认返回类型为int
b.GCC编译报警告很“圆滑”(-Wall)——警告或者错误都报警告,因此除非很明确知道原因且可以忽略,否则警告都要解决。
由a、b引出的问题:
eg.1
#include
//#include //语句0
int main()
{
int* p =NULL;
p = malloc(sizeof(int)); //语句1
//p = (int*)malloc(sizeof(int)); //语句2
return 0;
}
分析:当未添加头文件语句0时,编译器看不到malloc的函数返回类型,就默认为malloc返回int而非void*,此时就有类型不匹配的问题,即使调整为语句2可能老版本的gcc编译器能编过,新版依然不行。
eg.2
#include
#include
#include
//#include //语句0
int main()
{
FILE* fp = NULL;
fp = fopen("tmp", "r");
if(fp == NULL)
{
fprintf(stderr, "fopen():%s\n", strerror(errno));
exit(1);
}
puts("ok");
exit(0);
}
分析:同样的,由于无语句0看不到strerror返回类型,fprintf()将int变为%输出。导致段错误core dumped。
c.由上可知,一定要检查头文件,一般上来先加上stdio、stdlib、string、errno等标准库头文件。
2)以函数为单位编程
3)声明部分+实现部分
4)return 0;语句
return 0;表结束当前函数,返回给父进程,比如在main中可能给shell(shell调用./xxx.c),可以用echo $?(打印当前执行结束的状态)来打印返回值;exit语句结束当前进程,返回给OS。在main中二者没有区别。
如果main中只有一条printf("hello world!\n");没有return或者exit,则父进程返回的子进程的值为13(printf是有返回值的,成功返回字符串字符个数,失败为负数)
5)多用空格和空行
6)注释:
a.学习内核源码
b.#预处理阶段注释掉了,不参加编译
#if 0
#endif
2.算法:解决问题的方法。可用流程图、NS图、有限状态机FSM表示
3.程序:用某种语言实现算法
4.进程:在32位机器上,一进程占得虚拟空间大小为4G。
5.防止写越界,防止内存泄漏;谁打开谁关闭,谁申请谁释放
6.查看函数定义的方式:Linux 下man命令,man 2/3 函数名(查看2系统调用,3是标准库的函数),在vim中光标置于函数名处shift+k查看函数定义
7.编译后用strace ./可执行文件名,跟踪该可执行文件,每一行对应一个系统调用
8.ANSI C标准:美国国家标准协会 C标准:只规定的了语法和头文件(函数原型、变量、类型声明和宏定义),并没有函数实现。GNU C是对标准C(ANSI C)进行了拓展,可以支持标准C不支持的功能和操作,比如可以定义元素个数为0和变量长度的数组等等。
由于标准C和GNU C没有具体实现,函数实现并扩展了标准中没有规定的功能(比如对线程操作)的叫做运行时库。
libc就是Linux下根据标准C/ANSI C实现的C运行时库,glibc是Linux下根据GNU C实现的运行时库。此外还有为嵌入式系统设计的uclibc。当前Linux中用的最多的就是glibc。
9.glibc的发布版本主要由两部分组成,一部分是头文件,比如stdio.h、stdlib.h等,它们往往位于/usr/include。平时.c.h开头所用的标准库头文件是在/usr/include中。但自己定义的头文件是放在.c文件同一目录下的,并用#define “xxx.h”而非<>
另外一部分则是库的二进制文件部分。二进制部分主要的就是C语言标准库,它有静态和动态两个版本。动态的标准库位于/lib/libc.so.6;而静态标准库位于/usr/lib/libc.a。
glibc除了C标准库之外,还有几个辅助程序运行的运行库,这几个文件可以称得上是真正的“运行库”。它们就是/usr/lib/crt1.o、/usr/lib/crti.o和/usr/lib/crtn.o。
note:为何要用二进制?动态和静态区别?
10.库函数和系统调用函数。
库函数是给应用程序的编程人员用的,库函数是在用户态,可能会用到系统函数。
系统调用函数是在内核态,涉及对系统硬件和操作系统资源的操作,库函数通过软中断0x80调用系统调用函数。
库函数调用通常比行内展开的代码慢,因为它需要付出函数调用的开销。但系统调用比库函数调用还要慢很多,因为它需要把上下文环境切换到内核模式
11.预处理——编译——汇编——链接
1)预处理:gcc -E xx.c -o xx.i 对头文件、宏、条件编译替换、去掉注释,得到扩展.c文件
编译:gcc -S xx.i -o xx.s 检查语法、语义,得到汇编码
汇编:gcc -c xx.s -o xx.o 得到二进制机器指令
链接:gcc xx.o -o xx.out 或者 gcc xx.o -o xx(不需要-选项)
2)除了链接,都是单个文件单独进行,只有链接时才会将不同的文件有机结合起来。
3)可跳跃进行,只需-o指定生成的文件名和类型即可,gcc 会调用相应命令自动补齐中间过程
4)单个.c文件也需要链接这一步,因为需要启动代码,这是操作系统给的,裸机需要自己写
12.linux查看机器字长:getconf LONG_BIT
13.数据类型所占字节和机器字长和编译器有关:
1)32位:long 4B,char* 4B
2)64位:long 8B, char* 8B, void字长 1B, void* 8B
3)64位机器所有的指针所占字节都是8B:
sizeof(void*), sizeof(long*), sizeof(short*), sizeof(float*), sizeof(int*), sizeof(long long*), sizeof(double*), sizeof(char*), sizeof(unsigned char*),sizeof(struct xx*)
note:
1)memset(&(struct xx), 0 , sizeof(struct xx*));是错的,指针所占字节数永远是4/8,应该改为sizeof(struct xx)
2)注意数据类型指针所占字节和该数据类型指针变量所占字节是一致的,和该数据类型指针所指对象的值不一致:
void print_arr(int* p)
{
printf("sizeof(int*)=%lu, sizeof(p)=%lu,izeof(*p)=%lu,\n",
sizeof(int*), sizeof(p), sizeof(*p));
}
int main(int argc, char* argv[])
{
int a[5] = {1,3,5,7,9};
printf("[%s]:%lld\n", __FUNCTION__, sizeof(a));
print_arr(a);
}
运行结果:
sizeof(int*)=8, sizeof(p)=8,izeof(*p)=4
14.结构体
1)定义:struct xx { 成员};//struct xx本身就可视为变量类型
2)定义该结构体变量 :struct xx stru_xx;//类型名+变量名
3)由于结构体一般是模块协作使用,故多放于函数体外,用typedef定义比较方便:
类似typedef int _size_t方式,
typedef struct xx{成员} yy; 则yy就等同于类型名 struct xx, 当定义该结构体变量时即可用
yy zz;或者 yy* zz;方式。
15.内存字节对齐(64位机器)
0)在缺省情况下,c编译器为每一个变量或数据单元按其自然对界条件分配空间,也叫边界对齐,有一下三个规则:
a).每个成员起始偏移地址都是其数据类型长度的整数倍;
b).如果含有嵌套构造体,该构造体起始偏移地址是其最大成员数据类型长度的整数倍;
c).整个构造体的字节大小是其最大成员数据类型长度的整数倍。
Note:都是要求为最大成员数据类型的整数倍。
1)一般地可以通过下面的两种方法来改变缺省的对界条件:
方法一:
使用#pragma pack(n),指定c编译器按照n个字节对齐;
使用#pragma pack(),取消自定义字节对齐方式。
Note:(成对使用,否则该文件所有都按该方式对齐)
eg.1
#pragma pack(N)
typedef struct A
{
long l; //8B
short s; //2B
float f; //4B
}stru_A;
N | 1 | 2 | 4 | 8 | 16 | ||||
字节数 | 14 | 14 | 16 | 16 | 16 |
eg.2
#pragma pack(1)
typedef struct student
37 {
38 char a; //设置1个字节对齐,char 是1个字节,以1字节对齐,按1个字节处理
39 short b; //设置1个字节对齐,short 是2个字节,以1字节对齐,按2个字节处理
40 int c; //设置1个字节对齐,int 是4个字节,以1字节对齐,按4个字节处理
41 float d; //设置1个字节对齐,float 是4个字节,以1字节对齐,按4个字节处理
42 double e; //设置1个字节对齐,double 是8个字节,以1字节对齐,按8个字节处理
43 long f; //设置1个字节对齐,long 是4个字节,以1字节对齐,按4个字节处理
44 unsigned char g; //设置1个字节对齐,unsigned char 是1个字节,以1字节对齐,按1个字节处理
45 unsigned short h; //设置1个字节对齐,unsigned short 是2个字节,以1字节对齐,按2个字节处理
46 unsigned int i; //设置1个字节对齐,unsigned int 是4个字节,以1字节对齐,按4个字节处理
47 }student;
#pragma pack()
结果:
N | 1 | 2 | 4 | 8 | 16 |
内存大小 | 34 | 36 | 36 | 40 | 40 |
Note:
a).N=1时就是不考虑任何补齐操作,数据一个挨着一个紧密排放,内存占用大小等于各数据类型大小之和;
b).连续几个成员的数据类型大小之和≤N,有可能放在内存中一个连续N字节空间中,而非每个成员都在内存中独占一个N字节连续空间。能否放在同一个连续N字节空间中取决于是否满足“每个成员起始地址偏移是其数据类型大小的整数倍”这个条件,满足时也是按照该条件分布的。如eg2当N=4时,ab就放在内存中同一个4字节空间中,且分布是:1Byte的char+1Byte空+2Byte的short(因为short2字节,其起始地址应该是2的倍数,因此char后空1Byte放short)
c)最后不足N个Byte的也要补齐凑成最大成员的整数倍或者N的整数倍(两者中较小的那个),而不是规则3整个构造体内存大小是最大成员的整数倍。如成员是{short,int,char},N=2, 内存分布是2+4+(1+1【N=2,最大成员是int=4,取较小值2的整数倍,所以char后补1】)=8,char后补一个字节;N=4,由于short为2小于4,但int要从4的倍数处开始,因此short后空2Byte,(2+2)+4+(1+3【N=4,int=4,所以char后补3】)=12;N=8,虽然各数据类型之和为7小于8有可能都放在内存同一个连续的8Byte空间中,由于int需要放在起始偏移地址为4倍数处,char不是占用另外8个Byte,char后补三个字节是int 4Byte的倍数,因此是(2+2)+4+(1+3【N=8,int=4,char补3】)=12。
d)中间补齐的情况是取决于N的情况还有下一个成员数据类型整数倍,二者较小值。
eg1. {char longlong char},N=4,则
(1+3【凑齐N=4补3,而非由于longlong从8开始需要补齐7】)+8+(1+3)=16
eg.2 { char int char}, N=8, (1+3【不是补齐7到N=8,而是int整数倍起始地址】)+4+(1+3)=12
e).当N不是自然边界对齐时,不用满足规则3即:构造体所占的内存大小是最大成员数据类型大小的整数倍,如eg1中N=2,eg2中N=2和N=4。
f). 三个量:N,下一个成员数据类型整数倍,最大成员数据类型
3)综上,非边界自然对齐也要遵循15.0)的前两条规则。计算N对齐的字节数方法:
先用N从第一个成员去套,如果规则1和2都满足情况下看最多能放进去几个成员,如果放不下下一个成员,但N放这几个成员又有空余,则空余空着。如果下一个成员类型大小Y≥N,直接在当前的计算得到的内存空间数上加Y即可,直到遇到数据类型大小<N的成员,再用N字节套接下来的下几个成员,重复以上步骤即可。
4)不同的N的区别:
N不同,套进来的成员数目和顺序不同,补齐情况不同
5)自然边界对齐和方法一对齐计算区别:
自然边界对齐只考虑当下成员起始偏移地址是否为该数据类型大小的整数倍+总大小是否为最大成员变量的整数倍进行偏移补齐;方法一计算是考虑N能在满足规则1和2时能套住几个并补齐,区分成员数据类型大小是否≥N,是的话直接加上,直到最后并按最大成员数据类型补齐;二者计算方法不同,但理论上自然边界对齐也是N=X的情况,和#pragma pack(N) #pragma pack()结果应该一致的?
方法二:
__attribute(aligned(n)),让所作用的数据成员对齐在n字节的自然边界上;如果结构中有成员的长度大于n,则按照最大成员的长度来对齐;
__attribute((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐
当N=1表示编译器愿意让程序为了节省空间或者为了照顾协议(如串口通讯数据连续数据好处理),而牺牲执行效率,此时结构体在内存是连续存放的,不会为了三条规则而补齐。
struct student
{
char name[7];
uint32_t id;
char subject[5];
} __attribute__ ((packed));
16.Makefile
0) 基本语句就是:
target:denpendency
(tab键)cmd
1)发不出来的一般是大写M的Makefile,程序员自己调试用的是小写makefile
2)$()dollor符号是取值后加括号
3)$^取本层依赖,$@取本层target
4)%通配符,可用于将.c得到.o的语句里,如:%.o:%.c
5) RM 是个宏,等于rm -f
6)CC=编译器类型;OBJS=.o的target;CFLAGS+=编译选项,表明在当前已有的编译选项追加新的编译选项
7)赋值也是用=号,多个值赋给变量之间用空格分隔,命令后没有标点符号
OBJS=main.o tool1.o tool2.o #等号赋值,空格隔开
CC=gcc
CFLAGS+=-c -g -Wall
mytool:$(OBJS)
$(CC) $^ -o $@
#生成最后的可执行文件时的编译命令没有编译选项,原命令就是 gcc xx.o -o executeFileName
%.o:%.c
$(CC) $(CFLAGS) $^ -o $@
clean:
$(RM) %.o %.out -r
17.动态库静态库
1)动态库也叫共享库(公共资源),只能在固定的路径访问
ldd ./可执行文件 (查看该可执行文件用到的动态共享库的依赖 xxx.so.yy,so表动态库,yy表其版本号修订版本号)
格式: libxx.so lib是前缀,点so是后缀,so可以理解为shared obj文件,共享库文件
创建:gcc -shared -fpic -o libxx.so yy.c (将yy.c变为动态共享库,-fpic位置无关)
发布:把xx.o对应的头文件xx.h拷贝到/usr/local/include/下,把xx动态库拷贝到/usr/local/lib
添加路径:vim /etc/ld.so.conf 添加动态库的路径/usr/local/lib,用/sbin/ldconfig重读/etc/ld.so.conf
使用:gcc -I/usr/local/include -L/usr/local/bin -o xx.c -lxx(-I/usr/local/include -L/usr/local/bin可省略)
非root用户发布:cp xx.so ~/lib; export LD_LIBRARY_PATH=~/lib
*NOTE:
i)静态动态共享库重名时,内核优先选择动态库。
ii)当更新动态共享库时,要用/sbin/ldconfig重读/etc/ld.so.conf,比如再封装了libllist.so的基础上又封装了libstack.so,要重读
iii) 当把自己做的库放入标准库中,才能省略-lxx选项,但不建议这么做
2)静态库:格式是libxx.a,lib和点a不可省,xx叫做静态库库名,点a的a可以助记为从static中来。
路径没有那么严格,是编译时装载进来,占用编译时间不影响运行和调度时间;但是代码量膨胀。
创建静态库命令: ar -cr libxx.a xx.o 将.o文件变为静态库xx
发布到: 把xx.o对应的头文件xx.h拷贝到/usr/local/include/下,把xx静态库拷贝到/usr/local/lib
使用: gcc -L/usr/local/lib -o main main.o -lxx 或者 gcc -L/usr/local/lib -o main main.c -lxx 或者gcc -o main main.o -lxx (因为已经将libllist.a放入了默认lib路径/usr/local/lib,所以-L/usr/local/lib这个选项也可以省略)
*NOTE:
a)-lxx必须在最后,xx即是静态库的名字,且当有多个库时被依赖的放在后面,如用链表实现栈的时候,把llist.c stack.c都封装为动态库时,由于stack.c依赖llist.c所以-lllist放在后面:
gcc -o main main.c -lstack -lllist;
b)对于此前的练习中双向环链的llist.c llist.h llist.o main main.c main.o Makefile,当把llist.c做成静态库,llist.h放入/usr/local/include中后有以下变化:
i)llist.c llist.h Makefile都不需要了
ii)main中的include “llist.h”可改为include
3)用的较多的是动态库,自己写自己用的多做静态库。可以这么理解,静态库是在运行前就编译好的了而不是使用时才去找,因此得名静态,静态static作用域是局部的,因此自己调用自己的库多作为静态库。