c语言技巧以及注意事项

  1. 开发步骤以及注意事项&常用开发技巧集锦
    实时查man手册:
    man 1 xx查linux shell命令,man 2 xxx查API, man 3 xxx查库函数
    输入型参数和输出型参数:
    会使用errno和perror
    如if(a= =11) 最好写为if(11==a)
    避免野指针,判断指针是否野指针时,写成if (NULL != p)
    注意:输入型参数与输出型参数
    用宏定义来实现DEBUG和release
    //学习方法:man手册、头文件、百度、博客、总结
    写程序尽量避免使用全局变量,尤其是非static类型的全局变量。
    全局变量应该定义在c文件中并且在头文件中声明,而不要定义在头文件中

  2. 基本的*****************************************************
    变量占用的空间由数据类型决定,不同平台占用内存不同。存和取数据类型时必须相同
    void类型代表的是任意类型,它的类型是未知的,没有指定的。
    隐式转换:默认向精度更高的方向转换,转换后的变量是临时变量,赋值时会隐式转换为左值类型。
    c语言原生类型无bool类型,不是关键字可用int代替

  3. 内存*******************************************************
    程序运行的目的:计算机程序 = 代码 + 数据(经过运行后) = 结果
    冯诺依曼结构是:数据和代码放在一起。
    哈佛结构是:数据和代码分开存在。
    什么是代码:函数 什么是数据:全局变量、局部变量
    DRAM是动态内存,SRAM是静态内存。
    位(位永远都是1bit) 字节(字节永远都是8bit) 半字(一般是16bit) 字(一般是32bit)
    硬件内存的实现本身是有宽度的(内存位宽在逻辑上是任意的),有些内存条就是8位的,而有些就是16位的。。。
    内存编址是以字节为单位(8bit)
    内存和数据类型的关系:
    C语言中的基本数据类型有:char short int long float double
    int 整形和CPU本身的数据位宽是一样的(效率高)
    C语言中,函数就是一段代码的封装。函数名的实质就是这一段代码的首地址,本质也是一个内存地址。
    指针来间接访问内存:类型只是对后面数字或者符号(即内存地址)所表征的内存的一种长度和解析方法
    结构体内嵌指针实现面向对象:C语言是面向过程的,但是C语言写出的linux系统是面向对象的
    struct s //使用这样的结构体就可以实现面向对象。
    {int age; // 普通变量
    void (*pFunc)(void);}; // 函数指针,指向 void func(void)这类的函数
    栈 是一种数据结构(小内存、自动化),C语言中使用栈来保存局部变量。先进后出:栈;先进先出:队列
    栈是有大小的,太小怕溢出,太大怕浪费内存。栈的溢出危害很大,局部变量不能定义太多或者太大,减少递归调用
    堆 大块内存、手工分配。申请及释放都需要申请malloc及释放free。申请内存使用后未释放,这段内存就丢失了
    数据结构:链表、哈希表、二叉树、图等都是数据结构。

  4. 位操作、优先级***********************************************
    位与时两个操作数是按照二进制位彼次对应位相与的,逻辑与是两个操作数作为整体来相与的
    位或时两个操作数是按照二进制位彼次对应位相与的,逻辑或是两个操作数作为整体来相或的。
    按位取反是将操作数的二进制位逐个按位取反;而逻辑取反是真(非0)变成假(0)
    任何非0的数被按逻辑取反再取反就会得到1; 任何非0的数倍按位取反再取反就会得到他自己;
    位异或^ 与1位异或会取反,与0位异或无变化(两个位相同为0,不同为1)
    左移位<< 与右移位>>:
    对于无符号数,左移时右侧补0;右移时左侧补0
    对于有符号数,左移时右侧补0;右移时左侧补符号位(正数就补0,负数就补1
    嵌入式中研究的移位,以及使用的移位都是无符号数
    操作寄存器时读-改-写三部曲。
    特定位清零用&:如 a &= ~(1<<2);//将a第二位清0
    特定位置1用|:如 a |= (1<<2);//将a第二位置1
    特定位取反用^:如a ^= (1<<2);//将a第二位取反
    用宏来置位、复位(最右边为第1位)。

    • #define SET_NTH_BIT(x, n) (x | ((1U)<<(n-1)))
    • #define CLEAR_NTH_BIT(x, n) (x & ~((1U)<<(n-1)))
      截取变量的部分连续位#define GETBITS(x, n, m) ((x & ((0U)<<(m-n+1))<<(n-1)) >> (n-1))
      复杂宏怎么分析:
      ((x & ((0U)<<(m-n+1))<<(n-1)) >> (n-1))
      第一步,先分清楚这个复杂宏分为几部分:然后逐步拆解
      (x & ((0U)<<(m-n+1))<<(n-1)) >> (n-1)
  5. 指针相关*********************************************
    指针完整的名字应该叫指针变量,它跟普通变量没有任何本质区别
    为什么需要:指针的出现是为了实现间接访问,而高级语言并不是没有指针,而是被封装了
    定义指针变量、关联指针变量、解引用,如:int a;int * p; p=&a; *p = 32;
    指针定义时,结合前面的类型用于表明要定义的指针的类型;第二种功能是指针解引用,解引用时p表示p指向的变量的内容
    取地址符&使用时直接加在一个变量的前面,表示这个变量的地址。
    野指针(危害极大):指针指向的位置是不可知的,可能触发运行时段错误(Sgmentation fault)
    避免野指针:1.定义指针时,同时初始化为NULL
    2.在指针解引用之前,先去判断这个指针是不是NULL
    3.指针使用完之后,将其赋值为NULL
    4.在指针使用之前,将其赋值绑定给一个可用地址空间
    c语言的NULL是什么, #ifdef _cplusplus // 定义这个符号就表示当前是C++环境
    #define NULL 0 // 在C++中NULL就是0
    #else
    #define NULL (void *)0 // 在C中NULL是强制类型转换为void 的0
    #endif
    判断指针是否野指针时,都写成if (NULL != p)
    const关键字与指针: const int p;//p是指针,它指向一个只读的int型变量
    int * const p; //p是不可被改变的指针,指向可以改变内容的int型变量
    (gcc环境下)const修饰的变量其实是可以改的,gcc把const类型的常量放在了data段,只读是通过编译器校验实现。
    数组变量也是变量,和普通变量和指针变量并没有本质不同。
    数组中如数组a[];a做右值表示数组首元素的首地址(a不能做右值)
    &a不能做左值(数组的地址是常量),做右值时表示整个数组的地址。
    &a[0]字面意思就是数组第0个元素的首地址
    指针方式来访问数组元素:不能整体访问,只能单个访问。
    数组方式:数组名[下标]; (注意下标从0开始)
    指针格式:
    (指针+偏移量);//如果指针是数组首元素地址(a或者&a[0]),那么偏移量就是下标
    指针参与运算的特点是,指针变量+1,并不是真的加1,而是加1
    sizeof(指针类型);
    sizeof是C语言的一个运算符,sizeof的作用是用来返回()里面的变量或者数据类型占用的内存字节数。
    strlen是一个C库函数,用来返回一个字符串的长度(注意不计算字符串末尾的’\0’的)。
    注意strlen接收的参数必须是一个字符串(字符串的特征是以’\0’结尾)
    sizeof(数组名)实际返回的是整个数组所占用内存空间(以字节为单位的)
    函数传参,形参是可以用数组的,实际传递是不是整个数组,而是数组的首元素首地址。
    实际相当于传递的是指针(没有元素个数的信息)。
    宏和typedef #define dpChar char * //单纯的替换
    typedef char *tpChar; //定义了tpChar类型
    结构体变量作为函数形参:和普通变量传参时表现是一模一样的。
    结构体因为自身太大,所以传参应该用指针来传。(传结构体变量过去C语言也是允许)
    输入型参数与输出型参数:
    输入型参数:传参中使用const指针(如const int *p;),声明在函数内部不会改变这个指针所指向的内容
    函数传参如果传的是普通变量(不是指针)肯定是输入型参数;
    输出型参数:函数可以向外部返回多个值(利用输出型参数)
    linux风格函数中,返回值是不用来返回结果的,用来表示程序执行是成功还是失败

  6. 复杂表达式*************************************************
    指针数组与数组指针:
    指针数组的实质是一个数组,存储的内容是指针变量:int *p[5];
    数组指针的实质是一个指针,指针指向的是一个数组:int (*p)[5];
    [] . ->这几个优先级比较高
    函数的实质是一段代码,函数名表示第一句代码的地址
    假设有函数是:void func(void); 对应的函数指针:void (p)(void); 类型是:void ()(void);
    char *strcpy(char *dest, const char *src);),对应的函数指针是:char *(*pFunc)(char dest, const char src);
    linux中命令行默认是行缓冲的,只要没有遇到\n(或者程序终止,或者缓冲区满)都不会输出而会不断缓冲
    结构体内嵌函数指针实现分层:
    完成一个计算器,我们设计了2个层次:上层是framework.c,实现应用程序框架;下层是cal.c,实现计算器。
    实际工作时cal.c是直接完成工作的,但是cal.c中的关键部分是调用的framework.c中的函数来完成的。
    先写framework.c,由一个人来完成。这个人在framework.c中需要完成计算器的业务逻辑,并且把相应的接口写在对应的头文件中发出来,将来别的层次的人用这个头文件来协同工作。
    另一个人来完成cal.c,实现具体的计算器;这个人需要framework层的工作人员提供头文件来工作(但是不需要framework.c)
    上层注重业务逻辑,与我们最终的目标相直接关联,而没有具体干活的函数。
    下层注重实际干活的函数,注重为上层填充变量,并且将变量传递给上层中的函数(其实就是调用上层提供的接口函数)来完成任务。
    C语言的2种类型:内建类型ADT、自定义类型UDT。typedef定义(或者叫重命名)类型而不是变量
    二重指针和一重指针的本质都是指针变量。二重指针就是:指针数组指针
    二维数组的两种访问方式:以int a[2][5]为例,(合适类型的)p = a;
    a[0][0]等同于
    (
    (p+0)+0); a[i][j]等同于 ((p+i)+j)

  7. 数组&字符串&结构体&共用体&枚举*****************************
    内存来源:栈(stack)、堆(heap)、数据区(.data)
    栈:运行时自动分配&自动回收,反复使用,临时性的,有大小限制会溢出
    堆:操作系统堆管理器管理,内存空间大,程序手动申请&释放,脏内存,临时性
    malloc():malloc(0)C语言并没有明确规定malloc(0)时的表现
    malloc(4)gcc中的malloc默认最小是以16B为分配单位的。
    代码段:代码段就是程序中的可执行部分,直观理解代码段就是函数堆叠组成的。
    数据段(也被称为数据区、静态数据区、静态区):数据段就是程序中的数据,直观理解就是C语言程序中的全局变量。(注意:全局变量才算是程序的数据,局部变量不算程序的数据,只能算是函数的数据)
    bss段(又叫ZI(zero initial)段):bss段的特点就是被初始化为0,bss段本质上也是属于数据段,bss段就是被初始化为0的数据段。
    都可以给程序提供可用内存,都可以用来定义变量给程序用。
    栈内存对应C中的普通局部变量;堆内存完全是独立于我们的程序存在和管理的,程序需要内存时可以去手工申请malloc,使用完成后必须尽快free释放。;数据段对于程序来说对应C程序中的全局变量和静态局部变量。
    C语言的字符串类型:没有原生字符串类型,没有String类型,通过字符指针来间接实现的。
    C语言中字符串的本质:指针指向头、固定尾部的地址相连的一段内存
    字符串和字符数组:字符数组和字符串有本质差别。字符数组本身是数组,自带内存空间,可用来存东西;字符串本身是指针只占4字节,只能把字符串地址存在p中。
    结构体:结构体使用时先定义结构体类型再用类型定义变量。访问方式:表面上有2种方式(数组下标方式和指针方式);实质上都是指针方式访问。
    结构体的对齐访问:32位编译器,一般编译器默认对齐方式是4字节对齐。
    结构体整体本身必须安置在4字节对齐处,结构体对齐后的大小必须4的倍数
    gcc支持但不推荐的对齐指令:#pragma pack()//设置编译器1字节对齐(不对齐)
    #pragma pack(n) (n=1/2/4/8字节)
    gcc推荐的对齐指令
    attribute((packed))使用时放在要进行内存对齐的类型定义的后面,范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。
    attribute((aligned(n)))使用时放在要进行内存对齐的类型定义的后面,范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐
    (注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐)
    offsetof宏与container_of宏***详细
    offsetof宏的作用是:用宏来计算结构体中某个元素和结构体首地址的偏移量
    container_of宏作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。
    共用体union(联合):不存在内存对齐的问题。sizeof测到的大小实际是union中各个元素里面占用内存最大的那个元素的大小。
    大小端模式:大端模式,是指数据的高字节保存在内存的低地址中。
    小端模式,是指数据的高字节保存在内存的高地址中。(可以想象高对高差异小,高对低差异大)
    在通信协议中,大小端是非常重要的
    枚举在C语言中其实是一些符号常量集。枚举是将多个有关联的符号封装在一个枚举中,而宏定义是完全散的。
    当我们要定义的常量是一个有限集合时,最适合用枚举。(定义的常量符号之间无关联,或者无限的)用宏定义。

  8. 宏、函数和函数库、预处理***********************************
    由源码到可执行程序的过程:
    源码.c->(预处理)->.i源文件->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序
    处理这些过程的工具集合叫编译工具链
    常见的预处理:
    #include(#include <>专门用来包含系统提供的头文件,只会到系统指定目录寻找,编译器还可用-I来附加指定其他的包含路径)去寻找这个头文件
    #include ""首先在用户自定义目录下找,然后再去系统、标准库下找)
    #if #elif #else #endif #ifdef #ifndef
    gcc中只预处理不编译:-o xx可以指定可执行程序的名称.-E参数可以实现只预处理不编译。
    typedef是由编译器来处理而不是预处理器处理的
    定义带参宏时,每一个参数在宏体中引用时都必须加括号,最后整体再加括号,括号缺一不可。
    如:#define MAX(a, b) (((a)>(b)) ? (a) : (b)) #define SEC_PER_YEAR (3652460*60UL)
    宏定义不会检查参数的类型,返回值也不会附带类型;而函数有明确的参数类型和返回值类型。当我们调用函数时编译器会帮我们做参数的静态类型检查,如果编译器发现我们实际传参和参数声明不同时会报警告或错误。
    内联函数和inline关键字,内联函数是编译器负责处理的,编译器可以帮我们做参数的静态类型检查,不用调用开销,而是原地展开
    当我们的函数内函数体很短(譬如只有一两句话)的时候,我们又希望利用编译器的参数类型检查来排错,我还希望没有调用开销时,最适合使用内联函数。
    宏定义来实现条件编译(#define #undef #ifdef)
    函数的目的就是实现模块化编程:函数的返回类型、函数名、参数列表等
    一个函数只做一件事情。传参不宜过多.
    最好用传参、返回值来和外部交换数据,不要用全局变量。
    函数三要素:定义、声明、调用。
    函数原型的作用:让编译器静态类型检查。函数声明的主要作用是告诉编译器函数的原型
    递归函数:调用了自己本身这个函数的函数。典型就是:求阶乘、求斐波那契数列
    使用递归函数的原则:
    递归函数必须有一个终止递归的条件。收敛性
    递归是占用栈内存的,在栈内存耗尽之前递归收敛(终止),否则就会栈溢出。
    函数库:写好的函数的集合。函数是模块化的,可以被复用。
    现在的标准的函数库.譬如说glibc(开源方式、源码方式)
    库(主要有2种:静态库和动态库)的形式来提供。
    静态链接库:将自己函数库源码只编译不链接形成.o文件,用ar工具归档为.a归档文件(静态链接文件)
    (商业公司发布.a和.h文件,使用时链接器链接.a文件,形成可执行程序)
    动态链接库(效率更高):(.so文件)动态链接库本身不将库函数的代码段链接入可执行程序,只是做个标记。
    然后当应用程序在内存中执行时,运行时环境发现它调用了一个动态库中的库函数时,会去加载这个动态库到内存中,
    然后以后不管有多少个应用程序去调用这个库中的函数都会跳转到第一次加载的地方去执行(不会重复加载)。
    gcc默认使用动态库,要用静态库需要用-static强制静态链接。
    函数的使用需要注意:包含相应的头文件;调用库函数时注意函数原型
    有些库函数链接时需要额外用-lxxx来指定链接;如果是动态库,要注意-L指定动态库的地址。
    字符串函数:指定了开头(字符串的指针)和结尾(结尾固定为字符’\0’),而没有指定长度(长度由开头地址和结尾地址相减得到)
    字符串处理的需求是客观,面试笔试时,常用字符串处理函数
    //学习方法:man手册、头文件、百度、博客、总结
    数学库函数定义在:/usr/include/i386-linux-gnu/bits/mathcalls.h
    使用时只需要包含math.h即可。数学库链接时需加-lm。高版本的gcc中,可能没有加也可以链接成功
    注意区分编译时警告/错误,和链接时的错误:
    编译时警告/错误:
    4.6.10.math.c:9:13: warning: incompatible implicit declaration of built-in function ‘sqrt’ [enabled by default]
    double b = sqrt(a);
    链接时错误:
    4.6.10.math.c:(.text+0x1b): undefined reference to `sqrt’
    collect2: error: ld returned 1 exit status
    制作静态链接库并使用:
    制作:使用【gcc 库源文件.c -o 目标文件名.o -c】参数只编译不连接,生成.o文件;然后使用ar工具进行打包成.a归档文件
    库名不能随便乱起,一般是lib+库名称,后缀名是.a表示是一个归档文件。发布时需要发布.a文件和.h文件
    链接:ar工具: ar -rc lib+库名.a 库所需的.o文件
    nm工具:nm 库名.a 可查看.a文件中有哪些.o文件,有哪些函数
    使用:把.a和.h都放在需引用的文件夹下,然后在.c文件中包含库的.h,然后直接使用库函数。
    编译时:gcc 要使用库的文件.c -o 可执行程序名 -l库名 -L库路径(不加只在默认位置找)

    制作动态链接库并使用:后缀.so(windows下的.dll)
    制作:【gcc 库源文件.c -o 目标文件名.o -c -fPIC】只编译不连接,生成.o文件 -fPIC生成位置无关代码
    链接:【gcc -o lib库名.so 库使用的目标文件.o -shared】-shared表示用共享库方式链接
    使用:与静态链接相同:编译时:gcc 要使用库的文件.c -o 可执行程序名 -l库名 -L库路径(不加只在默认位置找)
    这样还会报错,动态链接库运行时需要被加载,编译器会去固定目录尝试加载
    (不推荐)可以将动态库放到固定目录下,一般是/usr/lib
    使用环境变量LD_LIBRARY_PATH:【export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:绝对路径】
    linux的ldd命令可以查看可执行程序用到的库位置,查看库能否找到并解析

  9. 存储类、作用域、生命周期、链接属性*************************
    存储类就是存储类型,也就是描述C语言变量在何种地方存储。描述这个变量存储在何种内存段中。
    作用域:描述这个变量起作用的代码范围
    生命周期:描述这个变量什么时候诞生及什么时候死亡
    链接属性:C语言中的符号有三种链接属性:外连接属性、内链接属性、无连接属性。
    符号就是编程中的变量名、函数名等。运行时变量名、函数名能够和相应的内存对应起来,靠符号来做链接的。
    .o的目标文件链接生成最终可执行程序的时候,其实就是把符号和相对应的段给链接起来。
    linux下C程序的内存映像:
    代码段又叫文本段(.text):对应着程序中的代码(函数)
    只读数据段:const修饰的常量有可能是存在只读数据段的(但是不一定,const常量的实现方法在不同平台是不一样的)
    数据段:显式初始化为非0的全局变量;显式初始化为非0的static局部变量。在main函数运行之前就已经被初始化了,是重定位期间完成的初始化。
    bss段(ZI段,零初始化段):显式初始化为0或者未显式初始化的全局变量;
    显式初始化为0或未显式初始化的static局部变量。
    堆:C语言不会自动向堆中存放东西。程序员使用堆内存时,自己申请、使用、释放
    文件映射区:进程打开了文件后,将这个文件的内容从硬盘读到进程的文件映射区,
    以后就直接在内存中操作这个文件,读写完了后在保存时再将内存中的文件写到硬盘中去。
    栈:局部变量分配在栈上;函数调用传参过程也会用到栈
    内核映射区:将操作系统内核程序映射到这个区域了。每一个进程都活在自己独立的进程空间中,0-3G的空间每一个进程是不同的(因为用了虚拟地址技术),但是内核是唯一的。
    OS下和裸机下C程序加载执行的差异:操作系统中运行程序时程序员自己不用操心,会自动完成重定位和清bss。裸机中需要手动
    存储类相关关键字:
    auto:作用,修饰局部变量。表示这个局部变量是自动局部变量,自动局部变量分配在栈上。
    static:作用1:用来修饰局部变量形成静态局部变量。非静态局部变量分配在栈上,而静态局部变量分配在数据段/bss段上。
    作用2:用来修饰全局变量,形成静态全局变量。与非静态全局变量在链接属性上不同
    静态局部变量和全局变量的区别是:作用域、连接属性。静态局部变量作用域是代码块作用域(和普通局部变量是一样的)、链接属性是无连接;全局变量作用域是文件作用域(和函数是一样的)、链接属性方面是外连接。
    register:(慎用)作用:register修饰的变量。编译器会尽量将它分配在寄存器中。寄存器数量有限,不保证一定放在寄存器中
    extern:主要用来声明全局变量,声明的目的主要是在本文件使用其他文件的全局变量
    volatile:用来修饰一个变量,表示这个变量可以被编译器之外的东西改变。没有这个会被编译器优化。
    (中断isr中引用的变量,多线程中共用的变量,硬件会更改的变量)正确区分,该加的时候加不该加的时候不加,如果不能确定该不该加为了保险起见就加上。
    restrict:c99中才支持的。只用于限定指针;该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于(base on)该指针的,即不存在其它进行修改操作的途径;
    这样的后果是帮助编译器进行更好的代码优化,生成更有效率的汇编代码。
    typedef:C语言关键字归类上属于存储类关键字,但是实际上和存储类没关系。
    作用域:局部变量的代码块作用域,可以被访问和使用的范围仅限于定义这个局部变量的代码块中定义式之后的部分。
    函数名和全局变量的文件作用域,整个.c文件中都可以访问这些东西。
    准确的说:全局变量/函数的作用域都是自己所在的文件,但是定义式之前的部分因为缺少声明所以没法用,解决方案是:1、把它定义到前面去;2、定义到后面但是在前面加声明;
    局部变量因为没法声明,所以只能定义在前面去。
    同名变量的掩蔽规则:如果两个同名变量作用域有交叠,C语言规定在作用域交叠范围内,作用域小的一个变量会掩蔽掉作用域大的那个
    生命周期:
    栈变量的生命周期:局部变量(栈变量)存储在栈上,生命周期是临时的。
    堆变量的生命周期:从malloc申请时诞生,然后使用,直到free时消亡。
    数据段、bss段变量的生命周期:全局变量的生命周期是永久的,在程序被执行时诞生,在程序终止时消亡。不能被程序自己释放
    代码段、只读段的生命周期:程序执行的代码,其实就是函数,它的生命周期是永久的。(const类型的常量、字符串常量有时候放在rodata段,有时候放在代码段,取决于平台)
    链接属性:C语言程序的组织架构:多个C文件+多个h文件
    编译以文件为单位、链接以工程为单位
    编译器工作时是将所有源文件依次读进来,单个为单位进行编译的
    )链接的时候实际上是把第一步编译生成个单个的.o文件整体的输入,然后处理链接成一个可执行程序。
    三种链接属性:外连接、内链接、无链接
    外连接:外部链接属性,也就是说可以在整个程序范围内(可以跨文件)进行链接,譬如普通的函数和全局变量属于外连接。
    内链接:(c文件内部)内部链接属性,不能在当前c文件外面的其他c文件中进行访问、链接。static修饰的函数/全局变量属于内链接。
    无连接:这个符号本身不参与链接,它跟链接没关系。所有的局部变量(auto的、static的)都是无连接的
    函数和全局变量的同名冲突:一个程序中的所有c文件中不能出现同名的函数/同名的全局变量。
    static的第二种用法:修饰全局变量和函数
    普通的(非静态)的函数/全局变量,默认的链接属性是外部的
    static(静态)的函数/全局变量,链接属性是内部链接。
    总结:
    普通(auto)局部变量:分配在栈上;作用域为代码块;生命周期在代码块内;链接属性,无连接
    静态局部变量:分配在数据段/BSS段;作用域为代码块;生命周期为程序运行的整个周期;链接属性为无连接
    静态全局变量、静态函数和普通全局变量/普通函数:
    区别在于static使全局变量/函数的链接属性由外部链接(整个程序所有文件范围)转为内部链接(当前c文件内)。
    存储类决定生命周期,作用域决定链接属性
    宏和inline函数的链接属性为无连接。

  10. 其他零碎***************************************************
    操作系统:
    裸机程序:代码量小,功能简单、所有代码都和直接目的有关,没有服务性代码
    操作系统:本身不产生价值,主要是管理所有资源,为应用程序提供服务。
    操作系统调用通道:API函数
    c库函数和API关系:单纯的API只是提供了极简单没有任何封装的服务函数,。应用程序为了好用,就对这个API进行了二次封装,于是就成了C库函数。
    有时完成一个功能,有相应的库函数可以完成,也有API可以完成
    不同平台(windows、linux、裸机)下库函数的差异
    不同操作系统API是不同的,但是都能完成所有的任务,只是完成一个任务所调用的API不同。
    库函数在不同操作系统下也不同,但是相似性要更高一些。封装API成库函数的时候,尽量使用了同一套接口,所以封装出来的库函数挺像的。但是还是有差异
    跨操作系统可移植平台,譬如QT、譬如Java语言。
    main函数
    标准c语言 int main(void); int main(int argc,char **argv); int main(int argc,char*argv[])
    main函数的返回值应当是int,void不正确只是一些单片机里这么写。
    main函数是特殊的,c语言规定main函数是整个程序的入口。
    linux一个程序的执行
    可在linux命令行下被调用,可通过shell脚本调用,可在程序中调用另一个程序(fork exec)。
    本质:一个进程的创建、加载、运行、消亡;执行程序就是创建新进程中执行这个程序直到结束;
    一个进程被它的父进程开启调用。main函数的返回值返回给这个程序(进程)的父进程。
    main函数返回值给父进程一个答复。一般0表示成功,负数表示失败。
    linux shell中用$?这个符号来存储和表示上一个程序执行结果。
    main函数的参数:
    调用main函数所在的程序的它的父进程给main函数传参,并且接收main的返回值。
    main传参通过argc和argv这两个C语言预订的参数来实现,argv是一个字符串数组,这个数组用来存储多个字符串,每个字符串就是我们给main函数传的一个参数。调用程序本身如./a.out 本身是第一个参数
    程序调用本质上都是父进程fork一个子进程,然后字进程和一个程序绑定起来去执行(exec函数族),我们在exec的时候可以给他同时传参。
    程序调用时可以被传参(也就是main的传参)是操作系统层面的支持完成的
    各个参数之间是通过空格来间隔的,在程序内部如果要使用argv,那么一定要先检验argc。
    void类型的本质
    C语言属强类型语言,所有的变量都有明确的类型。C语言中的一个变量都要对应内存中的一段内存,编译器需要这个变量的类型来确定这个变量占用内存的字节数和这一段内存的解析方法
    void类型的正确的含义是:不知道类型,不确定类型,还没确定类型。
    void *类型的指针指向的内存是尚未确定类型的,因此我们后续可以使用强制类型转换强行将其转为各种类型。
    NULL:不是关键字,是宏定义出来的
    #ifdef _cplusplus // 条件编译
    #define NULL 0
    #else
    #define NULL (void *)0 // 这里对应C语言的情况
    #endif
    C++的编译环境中,编译器预先定义了一个宏_cplusplus,程序中可以用条件编译来判断当前的编译环境是C++的还是C的。
    NULL的本质解析:NULL的本质是0,但是这个0不是当一个数字解析,而是当一个内存地址来解析的
    几乎所有CPU中,内存的0地址处是被操作系统严格管理的,应用程序不能随便访问。
    定义一个标准的指针流程:
    1.定义指针 赋值为 NULL
    2.给指针赋值
    3.检查是否为NULL
    {4.使用指针}
    5.使用后再赋值为NULL
    注意不要混用’\0’ 和 ‘0’ 和 0 和 NULL
    '\0’用法是C语言字符串的结尾标志,一般用来比较字符串中的字符以判断字符串有没有到头
    '0’是字符0,对应0这个字符的ASCII编码,一般用来获取0的ASCII码值
    0是数字,一般用来比较一个数字是否等于0;
    NULL是一个表达式,一般用来比较指针是否是一个野指针。
    运算中的临时匿名变量
    C语言叫高级语言,汇编语言叫低级语言。高级语言(C语言)它对低级语言进行了封装(C语言的编译器来完成),给程序员提供了一个靠近人类思维的一些语法特征
    更高级的语言如java、C#等只是进一步强化了C语言提供的人性化的操作界面语法,在易用性上、安全性上进行了提升。
    高级语言中有一些元素是机器中没有的
    高级语言在运算中允许我们大跨度的运算。低级语言中需要好几步才能完成的一个运算,在高级语言中只要一步即可完成。
    顺序结构
    顺序结构说明CPU的工作状态,就是以时间轴来顺序执行所有的代码语句直到停机。
    选择和循环结构内部还是按照顺序结构来执行的。
    每个c文件编译的时候,编译器是按照从前到后的顺序逐行进行编译的。
    链接过程链接器实际上是在链接脚本指导下完成的。所以链接时的.o文件的顺序是由链接脚本指定的。如果链接脚本中没有指定具体的顺序则链接器会自动的排布。

  •   程序调试的debug宏
      程序调试的常见方案:单步调试、裸机LED调试、打印信息、log文件
      单步调试:直观,缺点是限制性大、速度慢。用于代码量小,不能printf的时候
      利用硬件调试:LED、蜂鸣器等,适合代码量小的裸机程序
    
  •   printf函数打印调试,作为程序员必须学会使用打印信息调试,具有普遍性
      log文件(日志文件):适合于系统级或者大型程序的调试。
      	打印信息不能太多也不能太少,太少会不够信息找到问题所在,太多会有大量的无用的信息淹没有用信息
    
    调试(DEBUG)版本和发行(RELEASE)版本
    DEBUG版本就是包含了调试信息输出的版本,在程序测试过程中会发布debug版本,程序运行时会打印出来调试信息/log文件
    输出调试信息占用了系统资源,拖慢了系统运行速度。因此DEBUG版本的性能低于RELEASE版本。
    RELEASE版本就是最终的发布版本,去掉了所有的调试信息,程序的运行效率要更高。
    debug宏的实现原理
  •   #ifdef DEBUG
      #define dbg()		printf()
      #else
      #define dbg()	
      #endif
      如果我们要输出DEBUG版本则在条件编译语句前加上#define  DEBUG
      	调试语句dbg()就会被替换成printf从而输出
    
    c语言预定义宏:
    DATE 当前前源文件的编泽口期,用 “Mmm dd yyy”形式的字符串常量表示
    FILE 当前源文件的名称,用字符串常量表示
    LINE 当前源义件中的行号,用十进制整数常量表示,它可以随#line指令改变
    TIME 当前源文件的最新编译吋间,用“hh:mm:ss”形式的宁符串常量表示
    STDC 如果今前编泽器符合ISO标准,那么该宏的值为1,否则未定义
    STDC_VERSION 如果当前编译器符合C89,那么它被定义为199409L;如果符合C99,那么它被定义为199901L:在其他情况下,该宏为宋定义
    STDC_HOSTED (C99)如果当前是宿主系统,则该宏的值为1;如果当前是独立系统,则该宏的值为0
    _STDC_IEC_559 (C99)如果浮点数的实现符合IEC 60559标准时,则该宏的值为1,否则为未定义
    STDC_IEC_559_COMPLEX (C99)如果复数运算实现符合IEC60559标准时,则该宏的伉为1,否则为未定义
    STDC_ISO_10646 (C99 )定义为长整型常量,yyyymmL表示wchai_t值遵循ISO 10646标准及其指定年月的修订补充,否则该宏为未定义
    除标准 C 语言提供的标准宏之外,各种编译器也都提供了自己的自定义预定义宏。
    c语言可变参数:C语言编程中有时会遇到一些参数个数可变的函数
    如int printf( const char* format, …);
    除了有一个参数format固定以外,后面跟的参数的个数和类型是可变的(用三个点“…”做参数占位符)
    定义一个函数,最后一个参数为省略号,省略号前面可以设置自定义参数。
    在函数定义中创建一个 va_list 类型变量,该类型是在 stdarg.h 头文件中定义的。
    使用 int 参数和 va_start 宏来初始化 va_list 变量为一个参数列表。宏 va_start 是在 stdarg.h 头文件中定义的。
    使用 va_arg 宏和 va_list 变量来访问参数列表中的每个项。
    使用宏 va_end 来清理赋予 va_list 变量的内存。
  •   #include 
      #include 
      double average(int num,...)
      {
      	va_list valist;
      	double sum = 0.0;
      	int i;
      	/* 为 num 个参数初始化 valist */
      	va_start(valist, num);
      	/* 访问所有赋给 valist 的参数 */
      	for (i = 0; i < num; i++)
      	{
      	   sum += va_arg(valist, int);
      	}/* 清理为 valist 保留的内存 */
      	va_end(valist); 
      	return sum/num;
      }
      int main()
      {
         printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
         printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
      }
    
  1. 链表、状态机、多线程***************************************
    链表:
    数组缺陷:数组元素类型必须一致,数组元素个数需指定并且不能更改
    数组第一个缺陷由结构体解决,链表是为了解决第二个问题
    链表可以看作一个元素个数可变大变小的数组。
    链表是由若干个节点组成的(链表的各个节点结构是完全类似的),节点是由有效数据和指针组成的。有效数据区域用来存储信息完成任务的,指针区域用于指向链表的下一个节点从而构成链表。
    链表就是用来解决数组的大小不能动态扩展的问题,所以链表其实就是当数组用的。链表用来存数据相对于数组来说优点就是灵活性,需要多少个动态分配多少个,不占用额外的内存。
    链表的操作:遍历、插入、删除、逆序
    单链表:链表由节点构成,节点包括有效数据和指针
    定义一个struct node ;和一个头指针,用结构体创建一个节点,用头指针指向这个节点的地址,以后依次类推。
    1、申请堆内存,大小为一个节点的大小(检查申请结果是否正确);2、清理申请到的堆内存;3、把申请到的堆内存当作一个新节点;4、填充新节点的有效数据和指针区域。
    头指针是一个普通指针,只占4字节。头指针的类型是struct node *类型的
    单链表局限性:指针只能单向移动,算法有局限性
    linux内核链表:
    实际项目中的链表,节点中存储的数据其实是一个结构体,这个结构体中包含若干的成员,这些成员加起来构成了我们的节点数据区域。
    内核链表中自己实现了一个纯链表(纯链表就是没有数据区域,只有前后向指针)的封装,以及纯链表的各种操作函数(节点创建、插入、删除、遍历······)。这个纯链表本身自己没有任何用处,它的用法是给我们具体链表作为核心来调用。
    内核中核心纯链表的实现在include/linux/list.h文件中
    list.h中就是一个纯链表的完整封装,包含节点定义和各种链表操作方法。
    内核链表只有纯链表,设计的使用方法是将内核链表作为将来整个数据结构的结构体的一个成员内嵌进去。利用container_of宏
    状态机:
    常说的状态机是有限状态机FSM。FSM指的是有有限个状态
    关键点:当前状态、外部输入、下一个状态
    两种状态机:Moore型和Mealy型
    Moore型状态机特点是:输出只与当前状态有关(与输入信号无关)。相对简单,考虑状态机的下一个状态时只需要考虑它的当前状态就行了。
    Mealy型状态机的特点是:输出不只和当前状态有关,还与输入信号有关。状态机接收到一个输入信号需要跳转到下一个状态时,状态机综合考虑2个条件(当前状态、输入值)后才决定跳转到哪个状态。
    状态机的主要用途:电路设计、FPGA程序设计、软件设计(框架类型的设计,譬如操作系统的GUI系统、消息机制)

多线程:
操作系统下的并行执行机制:并行分微观上的并行和宏观上的并行。
宏观上的并行就是从长时间段(相对于人来说)来看,多个任务是同时进行的;微观上的并行就是真的在并行执行。
操作系统要求实现宏观上的并行。宏观上的并行有2种情况:第一种是微观上的串行,第二种是微观上的并行。
单核CPU本身只有一个核心,同时只能执行一条指令,这种CPU只能实现宏观上的并行,微观上一定是串行的。微观上的并行要求多核心CPU。多核CPU中的多个核心可以同时微观上执行多个指令,因此可以达到微观上的并行,从而提升宏观上的并行度。
进程和线程是操作系统的两种不同软件技术,目的是实现宏观上的并行(通俗一点就是让多个程序同时在一个机器上运行,达到宏观上看起来并行执行的效果)。
进程和线程在实现并行效果的原理上不同。而且这个差异和操作系统有关。(linux中线程就是轻量级的进程)。
最终目标都是实现并行执行。
现代操作系统设计时考虑到了多核心CPU的优化问题,保证了:多线程程序在运行的时候,操作系统会优先将多个线程放在多个核心中分别单独运行。所以说多核心CPU给多线程程序提供了完美的运行环境。所以在多核心CPU上使用多线程程序有极大的好处。
线程同步和锁
多线程程序运行时要注意线程之间的同步。

你可能感兴趣的:(c语言技术,编程语言)