C语言本质

计算机中数的表示

  • 浮点数:符号位+指数部分(2的多少次方)+尾数部分(小数点后的数字)
  • 用偏移的指数(Biased Exponent)表示负指数
  • 正规化(Normalize):尾数部分最高位必须是1,故不保存1,节省一位提高精度
  • 浮点数标准:IEEE 754

数据类型详解

  • 计算机存储的最小单位是字节(Byte),一字节等于8个bit
  • char型占一个字节空间,取值范围0~255(无符号整数),-128~-127(有符号整数)
  • x86平台的gcc规定char是有符号的(C优先考虑效率而不是可移植性)
  • C中的未明确定义
    • Implementation-defined:C不规定,编译器规定,char是有符号还是无符号
    • Unspecified:C不规定,编译器自己决定但不写在文档中,一个函数调用各个实参表达式的求值顺序
    • Undefined:完全不确定,数组访问越界
  • 缺省平台x86/Linux/gcc,遵循ILP32,char有符号
  • 对于浮点型,x86遵循IEEE 745,float是32位,double是64位
  • 类型转换
    • Integer Promotion:一个表达式中,凡是可以用int或unsigned int做右值的地方都可以用有符号或无符号的char、short型,如果原始类型的取值范围都能用int表示,则其值被提升为int型,若表示不了就提升为unsigned int型,具体情况有:

      • 一个函数的参数类型未知
      • 算数运算中,两个操作数类型不同,编译器自动做类型转换
      • 单目、移位运算符
    • Usual Arithmetic Conversion:具体情况有:
      • 一边为long double,则把另一边也转为long double
      • 否则,一边为double,则把另一边也转为double
      • 否则,一边为float,则把另一边也转为float
      • 否则,两边都应为整型,做Integer Promotion,若类型仍不同,继续转换
    • 赋值产生的类型转换:赋值或初始化时等号两边的类型不同,编译器会把右边的类型转成左边的类型再赋值,特别要注意函数传参和返回值也适用
    • 强制类型转换:以上三种转换称为隐式类型转换(Implicit Conversion),是编译器根据自己的规则执行的,除此之外,程序员可以通过类型转换运算符(Cast Operator)自己规定转换规则,称为显式转换(Explicit Conversion)或强制类型转换(Type Cast),如(double)3+i,先把3转换为3.0,再和整型变量i相加(根据第二条规则,会将i转为double)

运算符详解

  • 移位运算符(Bitwise Shift):包括左移<<和右移>>,将一个整数的各二进制位全部左移或右移若干位
  • 移动的位数必须小于左操作数的总位数,否则结果是Undefined的
  • 在一定取值范围内,将一个整数左移1位相当于乘以2
  • 计算机做移位比乘法快得多,编译器会自动优化,如将i*8编译成移位指令而不是乘法指令
  • 建议只对无符号数做位运算
  • 利用掩码(Mask)对一个整数中的某些位进行操作,如0x0000ff00表示对一个32位整数的8~15位进行操作
  • 异或^(二进制位运算,相同得0,不同得1(找不同信息))
    • 满足结合律和交换律
    • 一个数和自己做异或的结果是0
    • 一个数和全0做异或值不变,和全1做异或得到原值的相反值(可结合Mask实现位翻转)
    • 若a1^a2^a3^a4...^an的结果是1,则表示a1~an中1的个数为奇数个,否则为偶数个,可用于奇偶校验(Parity Check)
    • x^y^x=y,可实现原地交换
  • 其他运算符
    • 复合赋值运算符(Compound Assignment Operator):在赋值的同时做一个运算
    • 条件运算符(Conditional Operator):C语言中唯一的三目运算符(Ternary Operator)
    • 逗号运算符(Comma Operator):从左至右依次求值,最后一个表达式的值为整个表达式的值
    • sizeof运算符:sizeof 表达式 和sizeof(类型名),sizeof 中的表达式不求值,而是把表达式类型所占的字节数作为整个表达式的值
    • typedef类型声明:给一个类型起一个新名字

计算机体系结构

  • 内存中的每个存储单元有一个地址(Address),CPU通过地址找到相应的存储单元,取其中的指令,或读写其中的数据
  • 一个地址所对应的存储单元只能存一个字节
  • 对于int、float等多字节数据,把起始地址当作数据的地址
  • 内存地址是从0开始编号的整数,最大编到多少取决于CPU的地址空间(Address Space)有多大,如32位x86平台,地址是32位,从0x0000 0000到0xffff ffff
  • CPU
    • 寄存器(Register):CPU内部的高速存储器,比内存快得多
    • 程序计数器(PC,Program Counter):保存CPU取指令的地址,每次CPU读出程序计数器中保存的地址,然后按这个地址去内存里取指令,程序计数器保存的地址加上该指令的长度指向下一条指令
    • 指令解码器(Instruction Decoder):解释CPU取上来的指令的含义,然后调动相应的执行单元去执行它
    • 算数逻辑单元(ALU,Arithmetic and Logic Unit):若指令解码器将一条指令解释为运算指令,就调用算数逻辑单元去做运算,并将结果保存在寄存器或内存中
    • 地址和数据总线(Bus):CPU和内存之间用地址总线、数据总线和控制总线连接,32位处理器有32条地址线和32条数据线,每条线上有1和0两种状态,32条线的状态可表示一个32位的数
  • 设备
    • 设备中可供读写访问的单元称为设备寄存器
    • 在x86平台上,硬盘是ATA、SATA或SCSI总线上的设备,操作系统执行程序时先把程序从硬盘拷到内存,然后CPU取指令执行,这个过程称为加载(Load)
    • 程序加载到内存后成为操作系统调度执行的一个任务,称为进程(Process),一个程序可以被多次加载到内存,成为同时运行的多个进程
    • 内存总是被动地等待被读或被写,而设备会自己产生数据,并通过中断(Interrupu)通知CPU处理,每个设备都有一条中断线,通过中断控制器连接到CPU
    • Linux内核源代码中绝大部分是设备驱动程序,设备驱动程序通常是操作系统内核里的一组函数,通过对设备寄存器的读写实现对设备的初始化、读、写等操作
  • MMU  
    • 虚拟内存机制:VMM(Virtual Memory Management),由内存管理单元MMU(Memory Management Unit)支持实现
    • 物理地址 PA(Physical Address):内存芯片的地址
    • 虚拟地址 VA(Virtual Address):从CPU到MMU的地址,MMU将VA翻译成另一个地址发到CPU芯片的外部地址引脚上,即将虚拟地址映射成物理地址。虚拟地址既可大于也可小于物理地址空间
    • MMU将虚拟地址映射到物理地址是以页(Page)为单位的,对于32位CPU通常一页为4KB
    • 操作系统可以设置MMU的映射项,将相同的虚拟地址映射到不同的物理地址上,避免进程冲突
    • 32位CPU指寄存器是32位,数据总线是32位,虚拟地址空间是32位,物理地址空间不确定
    • MMU除了做地址转换外,还提供内存保护机制,管理不同页面的权限(可读、可写、可执行)
    • CPU访问一个VA时,MMU检查CPU当前出于用户模式还是特权模式,访问的目的是读数据、写数据还是取指令,如果和操作系统设置的页面权限相符就允许访问,转换成PA,否则不允许访问,抛出异常
    • 中断的产生与指令执行是异步(Asynchronous)的,异常的产生与指令执行是同步(Synchronous)的
    • 操作系统把虚拟空间划分为用户空间和内核空间
    • 所有内核代码的执行都是从中断或异常服务程序开始的,整个内核就是由各种中断处理和异常处理程序组成
    • 段错误的产生:用户程序访问一个VA,经MMU检查无权访问;MMU产生一个异常,CPU从用户模式切换到特权模式,跳转到内核代码执行异常服务程序;内核把这个异常解释为段错误,把引发异常的进程终止掉
  • Memory Hierarchy
    • 由于硬件技术限制,可以制造出容量很小但很快的存储器,或很大很慢的存储器,但不能造出又快又大的
    • 按离CPU由近到远的顺序:CPU寄存器->Cache->内存->硬盘
    • 寄存器:一种数据电路,由一组触发器(Flip-flop)组成
    • Cache:Cache和内存都是RAM(Random Access Memory)组成,可根据地址随机访问,断电后数据丢失
    • 内存:通过地址访问,启用MMU情况下,程序中地址是VA,访问内存用的是PA
    • 硬盘:由磁性介质和磁头组成,访问速度受限,但断电后数据不丢失
    • 大多数程序的行为具有局部性(Locality),可利用缓存,通过预读提高执行速度

x86汇编程序基础

  • 寻址方式(Addressing Mode)

    • 直接寻址(Direct Addressing Mode):
    • 变址寻址(Indexed Addressing Mode):访问数组成员
    • 间接寻址(Indirect Addressing Mode):
    • 基址寻址(Base Pointer Addressing Mode):访问结构体成员
    • 立即数寻址(Immediate Mode):
    • 寄存器寻址(Register Addressing Mode):
  • ELF文件
    • 各种UNIX系统的可执行文件

汇编与C的关系

  • 函数调用

    • 参数从右向左依次压栈传递,ebp指向栈帧的栈底,返回值通过eax寄存器传递
  • main函数和启动例程
    • 整个程序的入口点是.o中的_start,它首先做一些初始化工作(启动例程,Startup Routine),然后调用C代码中的main函数

  • 变量的存储布局

    • const和字符串的内存地址位于.rodata段,程序加载运行时合并到一个Segment中,系统做只读保护防止意外改写
    • 栈从高地址向低地址增长,数组从低地址向高地址排列
    • 全局变量、函数中的静态变量存在符号表中,函数中的普通变量存在栈中
    • 作用域(Scope)的概念适用于所有标识符,而不仅仅是变量(如main属于文件作用域)
    • 属于同一命名空间(Name Space)内的重名标识符,内层作用域的标识符将覆盖外层作用域的标识符(如局部变量名在它的函数中将覆盖重名的全局变量)
    • 标识符的链接属性(Linkage)有三种:
      • 外部链接(External Linkage):最终的可执行文件由多个程序文件链接而成,一个标识符在任意程序文件中即使声明多次也都代表同一个变量或函数。通过extern声明
      • 内部链接(Internal Linkage):一个标识符在某个程序文件中即使声明多次也都代表同一个变量或函数。通过static声明
      • 无链接(No Linkage):除以上情况之外的标识符
    • 存储类修饰符(Storage Class Specifier)
      • static:存储空间静态分配,具有Internal Linkage
      • auto:自动在栈上分配存储空间,函数返回时自动释放,是默认的修饰符
      • register:尽可能分配一个专门的寄存器存储
      • extern:用于多次声明同一个标识符
      • typedef:定义一个类型名
    • 变量的生存期(Lifetime)
      • 静态生存期:具有外部或内部链接属性,或被static修饰的变量
      • 自动生存期:链接属性为无链接且未被staci修饰的变量,进入块作用域时在站上或寄存器中分配,退出块作用域时释放
      • 动态分配期:malloc-free
  • 结构体和联合体

    • 结构体:在内存中从低地址向高地址排列,成员之间有空隙,称为填充(Padding)
    • 联合体:一个联合体的各个成员占用相同的内存空间,联合体的长度等于其中最长成员的长度
  • C内联汇编

链接详解

  • 多目标文件链接

    • 链接的过程由链接脚本(Linker Script)控制
  • 定义和声明
    • 凡是被多次声明的变量或函数,必须有且只有一个声明是定义,否则无法链接
    • 函数声明的extern可写可不写,而变量不写的话就代表定义局部变量
    • 提供给外界使用的函数或变量声明为extern(External Linkage)
    • 阻止外界访问模块内变量,声明为static(Internal Linkage),声明后即使外界用extern声明也无法访问
    • 通过头文件避免重复的声明
    • include用<>的,gcc先查找-l选项指定的目录,然后查找系统的头文件目录;用“”的,gcc先查找包含头文件的.c文件所在的目录,然后查找-l选项指定的目录,然后查找系统头文件目录
    • #ifndef STACK_H和 #endif:如果STACK_H没有定义过,则#ifndef和#endif之间的代码就包含在预处理的输出结果中,否则就不出现在预处理结果中(Header Guard)
    • 头文件中的变量和函数声明不能使定义,否则被多个文件包含后无法链接在一起
  • 静态库
    • 静态库以.a作为后缀,表示Archive
    • 链接静态库时,链接器会把静态库中的目标文件取出来和可执行文件真正链接在一起
  • 共享库
    • 链接共享库时只是指定了动态链接器和该程序所需要的库文件,并没有真的做链接,需要运行时动态链接
    • 指令中的地址没有使用绝对地址,故称为位置无关代码
    • 将绝对地址保存在一个地址表中,而指令通过地址表做间接寻址,避免了硬编码
  • 虚拟内存管理
    • 堆空间向高地址增长,堆空间地址上限称为Break,向高地址增长就要抬高Break,映射新的VA到PA
    • 栈空间的高地址部分保存着进程的环境变量和命令行参数,低地址部分保存函数栈帧,栈空间向低地址增长,栈空间比堆空间更可能用尽
    • 虚拟内存管理的作用:
      • 控制物理内存的访问权限:利用CPU模式和MMU的内存保护机制,实现不同页面具有不同访问权限
      • 让每个进程有独立的地址空间:不同进程中同一个VA被MMU映射到不同的PA,保证系统稳定性
      • VA到PA的映射给分配和释放内存带来方便:物理地址不连续的几块内存可以映射成虚拟地址连续的一块内存
      • 可以通过交换设备(Swap Device)扩充物理内存:系统可分配的内存总量=物理内存大小+交换设备大小

预处理

  • 宏定义

    • 变量式宏定义(Object-like Macro):
    • 函数式宏定义(Function-like Macro)
      • 参数没有类型,预处理只负责做形式上的替换,而不做参数类型检查
      • 编译生成的目标文件较大,代码执行效率较低
      • 省去了分配和释放栈帧、传参、返回值等一系列工作
    • 内联函数:告诉编译器这个函数的调用要尽可能快,可以当普通的函数调用实现,也可以用宏展开的办法实现
  • 条件预处理指示:为多种平台编写程序

Makefile

  • 程序由多个源文件编译而成,通过Makefile文件管理源文件的处理步骤

    • 避免只改一个源文件,就要重新全部编译
    • make命令自动读取当前目录下的Makefile文件,完成编译步骤
    • Makefile由一系列规则(Rule)组成,格式为:目标 ... :条件 ...
    • 目标和条件的关系:欲更新目标,必须先更新它的所有条件;所有条件中只要有一个被更新了,目标也必须随之被更新
    • make会自动选择受影响的源文件重新编译,不受影响的源文件不重新编译
    • make处理Makefile分为两个阶段:
      • 从前到后读取所有规则,建立起一个完整的依赖关系图
      • 从缺省目标或命令行指定的目标开始,根据依赖关系图选择适当的规则执行
  • 隐含规则和模式规则
  • 变量
  • 自动处理头文件的依赖关系
  • 常用的make命令行选项

指针

  • 指针基本操作

    • 避免定义一个指针类型的局部变量而没有初始化,在堆栈上分配的变量初始值不确定,指向不确定地址的指针称为“野指针”(Unbound Pointer),可能导致运行时错误,应定义明确初值,或初始化为NULL
    • NULL在C标准库的头文件定中为空指针,即地址0的指针类型,任何对地址0的访问会导致段错误
    • void* 为通用指针,可以转换为任意其他类型的指针,常用于函数接口,必须先转换成别的指针类型再做Dereference
  • 指针类型和参数返回值
  • 指针与数组

    • a[2]等价于*(a+2),表示取数组的第二个元素
    • 指针相减表示两个指针间元素相差的个数,只有指向同一个数组元素的指针之间相减才有意义
    • 数组名做右值时转换成指向首元素的指针,做左值仍表示整个数组的存储单元,而不是首元素的存储单元
    • 函数原型中的[]表示指针而不表示数组
  • 指针与const限定符
    • const给读代码的人传递有用信息,如一个函数的参数是const char*,在调用这个函数时就可以放心地传给它char*或const char*指针,而不必担心指针指向的内存单元被改写
    • 尽可能多地使用const限定符,这样可依靠编译器检查程序中的Bug,防止意外改写数据
    • const对编译器优化做有用提示,编译器可能把const优化成常量
  • 指针与结构体
    • 通过指向结构体的指针访问结构体成员,可写成p->c或(*p).c
  • 指向指针的指针与指针数组
    • int *a[10]表示一个数组,该数组有10个int*指针
    • main函数的标准原型是 int main(int argc, char *argv[]);,argc表示命令行参数的个数,argv是指向指针的指针,等价于char **argv,argv指向一个指针数组的首元素,数组中每个元素都是char*指针,指向一个命令行参数字符串
  • 指向数组的指针与多维指针
    • int (*a)[10]表示一个指向数组的指针,该数组有10个int元素
  • 函数类型和函数指针类型
    • 函数指针可以指向参数和返回值与之对应的函数
    • 通过函数指针调用函数和直接调用函数相比,可以更高地复用现有代码,实现低耦合、高内聚
  • 不完全类型和复杂声明
    • C语言类型分为函数类型、对象类型和不完全类型

      • 对象型分为标量类型和非标量类型,指针类型属于标量类型,可以做逻辑运算和if、for、while的控制表达式
      • 不完全类型变量是暂时没有完全定义好的类型,编译器不知道占几字节空间
      • 不完全结构体可用于构成多种数据结构,如链表、二叉树等
      • 借助typedef把复杂声明分解
        • T *p;,p是指向T类型的指针
        • T a[];,a是由T类型的元素组成的数组
        • T1 f(T2, T3...);,f是一个函数,参数类型是T2、T3,返回值类型是T1

函数接口

  • malloc-free

    • 进程有一个堆空间,C标准库函数malloc可以在堆空间动态分配内存,使用完后通过free释放
    • 分配后不释放会导致内存泄露(Memory Leak)
  • 传入参数与传出参数
    • 传入参数:把指针指向的数据传给函数使用
    • 传出参数:由函数填充指针所指的内存空间,传回给调用者使用
    • strcpy(char *dest, const char *src); src是传入参数(只能读不能改写),dest是传出参数(要改写)
    • Value-result参数:既是传入参数又是传出参数
  • 两层指针的参数
    • 两层指针做传出参数

      • 传出的指针指向静态内存,或已分配的动态内存
      • 在函数中动态分配内存,然后传出的指针指向这块内存空间,使用者应在使用内存后调用释放内存的函数
  • 返回值是指针的情况
    • 返回值是传出而不是传入的
    • 返回值传出的是指针,两种情况
  • 回调函数(Callback Function)

    • 参数是一个函数指针,调用者可以传递一个函数的地址给实现者,让实现者去调用它
    • 应用:C++泛型算法(Genereics Algorithm)、GUI编程中的异步回调函数
    • 把函数也当做一种数据来操作,操作函数的函数称为高阶函数(High-order Function)
  • 可变参数
    • 处理可变参数要用到C标准库的va_list类型和va_start、var_arg、var_end宏,这些宏定义在stdarg.h中

参考

http://docs.linuxtone.org/ebooks/C&CPP/c/

[Linux] Linux C编程一站式学习 Part.2的更多相关文章

  1. Linux网络编程一站式学习

    提要 学过非常多遍计算机网络,依旧不会网络编程. 看完这篇文章之后就不会是这样了. 环境:Ubuntu14.04 64bit 何为Socket 是基于TCP/IP的网络应用编程中使用的有关数据通信的概 ...

  2. 有关于《Linux C编程一站式学习》(备份)

    Linux C编程一站式学习 -- PDF版本,共37章: Linux C编程一站式学习 -- 在线版,来自灰狐: Linux C编程一站式学习 -- 在线版,来自亚嵌教育: Linux C一站式学习 ...

  3. gdb笔记 ---《Linux.C编程一站式学习》

    gdb笔记 ---<Linux.C编程一站式学习> 单步执行和跟踪函数调用 函数调试实例 #include <stdio.h> int add_range(int low, i ...

  4. [Linux] Linux C编程一站式学习 Part.3

    Linux系统编程 文件与I/O C标准I/O库函数与Unbuffered I/O函数 C标准I/O库函数printf().putchar().fputs(),会在用户空间开辟I/O缓冲区 系统函数o ...

  5. Liunx+C编程一站式学习

    Liunx+C编程一站式学习这本书有什么特点?面向什么样的读者?这本书最初是为某培训班的嵌入式系统Linux工程师就业班课程量身定做的教材之一.该课程是为期四个月的全日制职业培训,要求学员毕业时具备非 ...

  6. [Linux] Linux C编程一站式学习 Part.1

    C语言入门 程序基本概念 程序和编程语言 C语言--(编译器)--汇编语言--(汇编器)--机器语言(目标代码 / 可执行代码) 可移植 / 平台无关:平台指计算机体系结构或操作系统,或二者的组合.不 ...

  7. Linux C 编程一站式学习

    个人认为这是一个挺不错的从C语言到Linux系统开发的教程,这本是两个网上的文档. 其中 一本是<How To Think Like A Computer Scientist: Learning ...

  8. Linux C编程一站式学习读书笔记——socket编程

    前言 研一的时候写过socket网络编程,研二这一年已经在用php写api都快把之前的基础知识忘干净了,这里回顾一下,主要也是项目里用到了,最近博客好杂乱啊,不过确实是到了关键时刻,各种复习加巩固准备 ...

  9. Linux C编程一站式学习

    http://docs.linuxtone.org/ebooks/C&CPP/c/ 很全面的介绍

随机推荐

  1. 总结traefik 在k8s 环境中的配置文件

    总结traefik 在k8s 环境中的配置文件 traefik.toml配置文件引用 [www@localhost traefik-ingress]$ more * :::::::::::::: co ...

  2. 第26 章 : 理解 CNI 和 CNI 插件

    理解 CNI 和 CNI 插件 本文将主要分享以下几方面的内容: CNI 是什么? Kubernetes 中如何使用 CNI? 哪个 CNI 插件适合我? 如何开发自己的 CNI 插件? CNI 是什 ...

  3. lms微服务框架主机介绍

    目录 概念 .net的通用主机 .net的web主机 lms的业务主机类型 用于托管业务应用的普通主机 1. 创建一个应用台程序 2. 安装Silky.Lms.NormHost包 3. 注册LMS服务 ...

  4. 又一起.NET程序挂死, 用 Windbg 抽丝剥茧式的真实案例分析

    一:背景 1. 讲故事 前天有位粉丝朋友在后台留言让我帮忙看看他的 Winform程序 UI无响应 + 410线程 到底是啥情况,如下图: 说实话,能看到这些真实案例我是特别喜欢的 ,就像医生看病,光 ...

  5. (一)LDAP 简介

    一  LDAP  简介 LDAP是一种通讯协议,LDAP支持TCP/IP.协议就是标准,并且是抽象的.在这套标准下,AD(Active Directory)是微软出的一套实现.    AD 暂且把它理 ...

  6. 次小生成树 详解及模板 (仅kruskal)

    思路 关于次小生成树,首先求出最小生成树,然后枚举每条不在最小生成树上的边(在原本的节点上添加一个vis属性进行判断即可),并把这条边放到最小生成树上面,然后就一定会形成环,那么我们在这条环路中取出一 ...

  7. 案例分析–Note-taking Management Softwares

    项目 内容 这个作业属于那个课程 2021春季学期软件工程(罗杰.任健) 这个作业的要求在哪里 案例分析 我在这个课程的目标是 团队协作,利用软件工程的思维和方法开发出一款具有实用价值的软件 这个作业 ...

  8. 1057 Stack

    Stack is one of the most fundamental data structures, which is based on the principle of Last In Fir ...

  9. 2sat建边总结

    2sat的基础建边 AND = 1 : ~x -> x ,~y -> y   (两个数必须全为1) AND = 0 : y -> ~x ,x -> ~y  (两个数至少有一个为 ...

  10. Windows API初练手 -- 疯狂写文件代码

    警告:恶作剧软件,慎用!仅供初学者研究代码所用!!! 提示:默认文件创建目录在"D:\test",如果需要使用的话请自行更改目录. 1. Windows API 版本 (调用系统函 ...