Linux c编程一站式学习笔记:C语言本质

本文为"linux c编程一站式学习"一书的笔记

计算机中数的表示

二进制数表示法

第一位为符号位,在做加法运算时,需要这样:

  1. 如果符号位相同,则符号位不变,其他位相加,在不溢出的情况下得到结果。
  2. 如果符号位不同,则先比较非符号位谁大,然后用大数减小数,最后符号位

和大数的相同,这样就保证了结果的正确性。 !!这种方法,效率低,而且0的表示不唯一,既可以表示为1000 0000也可以表示 为0000 0000

一句话概括:负数用1的补码(1's complement,也叫反码)表示,减法转换成加法,计算 结果的最高位如果有进位则要加回到最低位上去。

0000 1000 - 0000 0100 = 0000 1000 + (-0000 0100) =

0000 1000 + 1111 1011 = 0000 0100

!!这种方法,效率高了,正数和负数一样算,但是还是有2个0

为什么叫2的补码呢?因为对一个只有一位的二进制数b取补码就是1 - b + 1 = 1

1111 1111 - 1000 0001 + 0000 0001 = 0111 1110 + 0000 0001 = 0111 1111

!!这种方法,减法转换成加法,计算结果最高位的进位直接忽略,不需要加到低位

不同则溢出。

浮点数


符号位 指数部分 尾数部分


规定一个偏移值,比如127,实际的指数要加上这个偏移值再填到指数部分,这样比 127大的就表示正指数,比127小的就表示负指数。

9 = (0.01001) * 2 ^ 5,为了解决这个问题,我们需要制定一个标准,规定最高位 必须为1,也就是尾数必须是以0.1开头,这叫正规化(normalize)。由于尾数部分第一 位必须是1,所以这个1就不用保存了,可以节省一位来提高精度。

数据类型详解

type ILP32 LP64


char 8 8 short 16 16 int 32 32 long 32 64 long long 64 64 pointer 32 64

其中ILP32表示int,long,pointer是32位的。

有的没有,用整数来模拟浮点运算,称为软浮点(soft-float)。

gcc实现的是96位,还有的编译器实现的和double的一样,也有更高精度的,比如IBM的powerpc 上的为128位。

promotion,另外,float要提升为double,这叫default argument promotion,例如:

char c = 'a';
printf("%c", c);

c要被提升为int型。

怕溢出。

  1. if 有long double,则要转化成long double
  2. else if 有double,则要转化成double
  3. else if 有float,则要转化成float
  4. else if 有int,则要转化成int
  5. 对于有符号数和无符号数,则要转化成存储位数大的那个操作数的类型。

运算符详解

implementation-defined的,不同编译器有不同的实现。对于x86的gcc, 补1。

C标准中规定某些点是序列点,当执行到这个点时,在此之前的副作用必须全部 作用完毕,在此之后的副作用必须都没发生。

  1. 调用一个函数时,在所有准备工作做完之后,函数调用开始之前是序列点。

比如调用一个函数

sort(arr, get_len(arr), is_sorted(arr));

get_len和is_sorted哪个先执行是不确定的,但是必须它们都完成之后才调用sort函数。

  1. ? :和,和&&和||的第一个操作数求值之后是序列点
  2. 一个完整的定义末尾是序列点。如:
int name,age;

name后面是序列点,age后面也是。

计算机体系结构基础

内存

的地址为它所占内存单元的起始地址。

相应操作。因为CPU获取到的指令由很多个字节组成,有的位表示内存地址,有的位表示 寄存器编号,有的表示做什么操作。

设备

称为设备寄存器,操作设备的过程就是读写这些设备寄存器的过程,比如向串口发送寄存器 里写数据,串口设备就会把数据发送出去,读串口接收寄存器的值,就可以读取串口设备 接收到的数据。

内存映射I/O。但是x86比较特殊,它对于设备有独立的端口地址空间,需要引出额外的地址 线来连接CPU里面的设备,访问设备寄存器的时候使用特殊的in/out指令,这种称为端口I/O

函数,通过读写设备寄存器实现对设备的初始化,读和写等操作,有的设备还要提供一个 中断处理函数。

MMU

产生的,中断产生的原因和CPU当前执行的指令无关,而异常的产生就是因为CPU当前 执行的指令出了问题,例如除0操作。

存储设备

可以推测,DRAM电器比SRAM简单,容量大,但是速度慢。

汇编基础

最简单的汇编程序

.section .data
.section .text
.globl _start
_start:
        movl $1, %eax
        movl $4, %ebx

        int $0x80
as simple.s -o simple.o

再用链接器ld把simple.o链接成可执行文件simple

ld simple.o -o simple

可以看到执行结果:

[monkey@itlodge asm]$ ./simple 
[monkey@itlodge asm]$ echo $?
4

是空的。.text是代码段,后面的代码都是这个段里面的。

就相当于main函数一样,是一个程序的入口,

这个参数表示系统调用。

系统都使用AT&T语法。

寄存器

但是有的指令规定某个寄存器只能用于某种用途,比如除法指令idivl要求被除数必须在 eax中,edx必须为0,而除法可以任意选用,商必须保存在eax中,余数保存在edx中。

计算过程中产生的标志位,ebp和esp用于维护函数调用的栈帧。

代码biggest.s

# find the maximum number

.section .data
data_items:
        .long 2, 7, 1, -5, 9, 0

.section .text
.globl _start
_start:
        movl $0, %edi
        movl data_items(, %edi, 4), %eax
        movl %eax, %ebx

loop:
        cmpl $0, %eax
        je exit
        incl %edi
        movl data_items(, %edi, 4), %eax
        cmpl %ebx, %eax
        jle loop

        movl %eax, %ebx
        jmp loop

exit:
        movl $1, %eax
        int $0x80

编译运行结果:

[monkey@itlodge asm]$ as biggest.s -o biggest.o
[monkey@itlodge asm]$ ld biggest.o -o biggest
[monkey@itlodge asm]$ ./biggest 
[monkey@itlodge asm]$ echo $?
9

状态的是最大值。

寻址方式

address_or_offset(%base_or_offset, %index, multiplier)

这种格式表示的地址是这样计算的:

address = address_or_offset + base_or_offset + multiplier * index

其中address_or_offset, multiplier必须是常数,base_or_offset,index必须是寄存器。 省略这些项,表示它们是0

寻址,如movl 4(%eax), %ebx

ELF文件

汇编与C的关系

常识

比如下面的一个C程序:

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("yuanhang\n");

    return 0;
}

通过编译之后,产生的汇编文件如下:

    .file   "t.c"
    .section    .rodata
.LC0:
    .string "yuanhang"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    %edi, -4(%rbp)
    movq    %rsi, -16(%rbp)
    movl    $.LC0, %edi
    call    puts
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (GNU) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

刚刚那个程序:

[monkey@itlodge test]$ gcc -g t.c
[monkey@itlodge test]$ objdump -dS a.out > t.txt
...
00000000004004fc <main>:
#include <stdio.h>

int main(int argc, char *argv[])
{
  4004fc:   55                      push   %rbp
  4004fd:   48 89 e5                mov    %rsp,%rbp
  400500:   48 83 ec 10             sub    $0x10,%rsp
  400504:   89 7d fc                mov    %edi,-0x4(%rbp)
  400507:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
    printf("yuanhang\n");
  40050b:   bf c4 05 40 00          mov    $0x4005c4,%edi
  400510:   e8 cb fe ff ff          callq  4003e0 <puts@plt>

    return 0;
  400515:   b8 00 00 00 00          mov    $0x0,%eax
}
  40051a:   c9                      leaveq 
  40051b:   c3                      retq   
  40051c:   0f 1f 40 00             nopl   0x0(%rax)
...

只复制了部分结果

gcc t.c -o t

可以分解为下面3条:

gcc -S t.c
gcc -c t.s
gcc t.o -o t
#include <stdio.h>

typedef unsigned int uint;

union demo {
    struct {
        uint one:1;
        uint two:3;
        uint three:10;
        uint four:5;
        uint :2;
        uint five:8;
        uint six:8;
    } bitfield;
    char byte[8];
};

int main(int argc, char *argv[])
{
    union demo d = { { 1, 5, 513, 17, 129, 0x18 } };
    int i;

    printf("size of demo:%u\n", sizeof(union demo));

    printf("values:%u %u %u %u %u %u\n",
           d.bitfield.one, d.bitfield.two, d.bitfield.three,
           d.bitfield.four, d.bitfield.five, d.bitfield.six);
    for (i = 0; i < 8; i++) {
        printf("%x ", d.byte[i]);
    }
    printf("\n");

    return 0;
}

运行结果为:

[monkey@itlodge test]$ ./t
size of demo:8
values:1 5 513 17 129 24
1b 60 24 10 18 0 0 0 

我们知道,byte数组和bitfield是共用存储空间的,所以可见bit-field的内存布局为: 0000 0001 1000 0001 0000 0010 0100 0110 0000 0001 1011 高地址 6组全0 低地址

有了前面的例子的启发,我写了下面程序来验证:

#include <stdio.h>

union test {
    int num;
    char byte[2];
};

int main(int argc, char *argv[])
{
    union test t = { 0x1001 };

    if (t.byte[0] == 0x01 && t.byte[1] == 0x10) {
        printf("little endian\n");
    } else if (t.byte[0] == 0x10 && t.byte[1] == 0x01) {
        printf("big endian\n");
    } else {
        printf("error!\n");
    }

    return 0;
}

C内联汇编

有的相关平台的指令必须要手写汇编,因为C语言中没有等价的概念,比如x86的端口I/O 使用__asm__("assembly code");来插入汇编语句,多条语句用\n\t分开 完整的内联汇编格式为:

__asm__(assembly template
        : output operands
        : input operands
        : list of clobbered registers
);

除了assembly template,其它的都是可选的 看下面的例子:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a, b = 10;

    __asm__("movl %1, %%eax\n\t"
            "movl %%eax, %0\n\t"
            :"=r"(a)
            :"r"(b)
            :"%eax"
        );
    printf("a:%d\n", a);

    return 0;
}

其中,为了区分%1这种占位符,eax前面要加两个%号。 这里%0代表的是变量a,%1代表的是变量b,这两条movl指令其实就是把变量b的值 先放到一个寄存器eax里面,然后再把eax里面的值放到变量a里面。 "r"(b)表示分配一个寄存器保存变量b的值,而"=r"(a)表示把寄存器的值输出到 变量a中,最后一个"%eax"告诉编译器在这期间eax要被改写,不要让eax寄存器 保存其它值

volatile限定符

其中-00表示不优化,这是默认的选项,后面的1-3一个比一个优化得多,编译 时间更长,-0s是为了缩小目标文件的尺寸而优化的。

链接详解

链接库

gcc file1.c file2.c -o file

也可以这样:

gcc -c file1.c
gcc -c file2.c
gcc file1.o file2.o -o file

gcc链接的作用其实是把一些段合并到同一个segment中,还要插入一些符号到生成的 脚本中,如果不使用-T选项指定脚本,默认使用ld作为链接脚本。

我现在有3个文件, t.h

#ifndef _T_H_
#define _T_H_

void hello(void);

#endif /* _T_H_ */

t.c

#include <stdio.h>
#include "t.h"

void hello(void)
{
    printf("hello\n");
}

main.c

#include "t.h"

int main(int argc, char *argv[])
{
    hello();

    return 0;
}

先编译成目标文件:

gcc -c t.c

这时会生成和t.o 现在打包成一个静态库libt.a

ar rs libt.a t.o

这时会生成libt.a Note:r表示将后面的文件列表添加到文件包,s用于为静态库建立索引(ranlib有同样功能) 现在把libt.a和main.c编译

gcc main.c -L. -lt -I. -o main

这里,-L.表示在当前目录(.)找库文件,-lt表示使用libt文件,-I.表示在当前目录找头 文件。

共享库

链接,而静态库是真正链接起来的。

一种,表示生成位置无关代码(position independent code),生成的共享库,各段的 加载地址并不是绝对地址,可以加载到任意位置。

  1. 首先在环境变量LD_LIBRARY_PATH中查找
  2. 从缓存文件/etc/ld.so.cache中查找,这个文件是由ldconfig工具读取

/etc/ld.so.conf后生成的。

  1. 从默认的系统路径中找(/usr/lib,/lib)

再使用ldconfig工具更新缓存文件,这时就能使用了。

每个共享库有3个文件名,real name, soname, linker name real name包含完整的版本号,如libzip.so.2.1.0 soname是一个符号链接,包含了主版本号,如libzip.so.2。主版本号一致即可保证库函数 的接口一致。 linker name只在编译链接的时候使用,有的linker name是库文件的符号链接,有的是 一个链接脚本。

预处理

#define VERSION 2
#if defined x || y || VERSION < 3

先把VERSION定义为2,再看defined x,x没有定义,所以替换为0(如果有定义则为1), 变成了: #if 0 || y || VERSION < 3 然后把VERSION展开,变成了: #if 0 || y || 2 < 3 再把没有定义的变成0,于是: #if 0 || 0 || 2 < 3 最后变成了: #if 1

  1. _FILE_展开为当前文件的名字
  2. _LINE_展开为当前代码行的行号
  3. _func_展开为当前函数名

Makefile基础

基本规则

target ... : prerequisites ... command1 command2 ...

注意,每条命令必须以一个tab开头,不能是空格 每条命令,都会创建一个shell进程来执行 看下面的例子:

main : main.o stack.o
        gcc main.o stack.o -o main
main.o : main.c main.h stack.h
        gcc -c main.c
stack.o : stack.c stack.h
        gcc -c stack.c

Makefile先要执行目标main,发现main.o stack.o需要更新,于是去执行main.o, 于是执行目录main.o,再执行目标stack.o,最后执行main

@echo "cleaning project"

要加-号,因为rm删除的文件或目录可能不存在,mkdir要创建的目录可能已经存在。

时候就要添加一条特殊规则,把clean声明为一个伪目录:

clean:
        @echo "clean"
        -rm *.o
.PHONY: clean

隐含规则和模式规则

main.o main.h stack.h
main.o main.c
        gcc -c main.c

注意,其中只有一条规则允许有命令列表,其它规则不能有命令列表,否则会报警并采用 最后一条规则的命令列表

CC = cc

cc在/usr/bin中,可以看到它是符号链接,指向gcc

main: main.o stack.o
        gcc main.o stack.o -o main
main.o: main.h stack.h
stack.o: stack.h

上面的例子没有给出main.o的命令列表,这时它会查找隐含规则,发现隐含规则中有 %.o: %.c这样一条模式规则可用,于是就加上了:

main.o: main.c
        cc -c -o main.o main.c

同理,stack.o也是一样。

变量

a = $(b)
b = ccc

all:
        @echo $(a)
b := ccc
a := $(b)
all:
        @echo $(a)
a ?= $(b)

如果a没有定义过,那么?=相当于=,如果定义过,则什么也不做

objs = main.o
objs += stack.o

则objs最后变成了main.o stack.o,它会自动在前面加一个空格 如果变量还没定义过就用+=,则它相当于=

如:

main: main.o stack.o maze.o
      gcc $^ -o $@

这样即使修改了目标的名字或者添加了条件,下面的命令也不需要修改 对于$?变量,看下面的例子:

libstack.a: main.o stack.o
        ar rs libstack.a $?

这样,只对更新过的条件进行操作,没更新过的目标文件已经打包好了

name description default value


AR static library archives ar ARFLAGS static library options rv AS assembly compiler as ASFLAGS assembly compiler options null CC c compiler cc CFLAGS c compiler options null CXX c++ compiler g++ CXXFLAGS c++ compiler options null CPP c preprocessor $(CC) -E CPPFLAGS c preprocessor optios null LD linker ld LDFLAGS linker options null TARGET_ARCH target dependent options null OUTPUT_OPTION output -o $@ LINK.o link all .o files $(CC) $(LDFLAGS) $(TARGET_ARCH) LINK.c link all .c files $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH) LINK.cc link all c++ files $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH) COMPILER.c compile .c files $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c COMPILER.cc compile cpp files $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) RM remove rm -f

自动处理头文件的依赖关系

all: main
main: main.o stack.o
        gcc $^ -o $@
clean:
        -rm main *.o
.PHONY: clean

sources = main.c stack.c
include $(sources:.c=.d)
%.d %.c
        set -e; rm -f $@; \
        $(CC) -MM $(CPPFLAGS) {{content}}lt; > $@.$$; \
        sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$ > $@; \
        rm -f $@.$$

部分解释一下,有的东西太复杂,没搞懂 $(sources:.c=.d)是一种变量替换,把sources里面所有的.c文件替换成.d文件, include类型C语言中的#include,用于包含文件 4个$读进来变成了2个$,2个$表示进程的ID

它不是顺序执行的

make CFLAGS=-g

如果Makefile中也定义了CFLAGS,则命令行的值会覆盖掉Makefile中的值

指针

强制转换(),后来引进了void *,这时候可以隐式转换,不用强制转换了。

指针与const

  1. 读代码的人可以放心地使用,不用但是内存单元会修改
  2. 可以依靠编译器来检查出来bug
  3. 可以让编译器优化成常量

指针与数组与函数

为了告诉读代码的人,它不是指向指针而是指向一个指针数组的首元素

int (*arr)[10];

指针arr指向数组arr,这个数组有10个int类型的元素。

void hello(const char *str)
{
    printf("hello %s\n", str);
}

int main(int argc, char *argv[])
{
    void (*f)(const char *str) = hello;
    f("me");

    return 0;
}

定义了一个指向像hello这种函数类型(返回值是void,1个参数,而且第一个参数类型是 const char *)的指针,然后就可以用f来操作函数了。

类型分类

C标准库

字符串

都填充为c的值。

从src拷贝n个字节到dest,如果内存重叠,不能正确拷贝

其实也是从src拷贝n个字节到dest,但是如果内存重叠,也可以正确拷贝

比较内存中的字节,如果前n个字节一样则返回0,如果遇到不一样的字节,返回s1

忽略大小写,但是不是标准C函数,而是POSIX中定义的。

从左往右查找字符c,找到第一次出现的位置,返回这个位置

从右往左查找,r可以理解为reverse

haystack意为干草堆,needle是针,即大海捞针,表明是从haystack指向的字符串中查找 needle指向的字符串,返回第一次出现的位置。

这是一个奇怪的函数,根据界定符delim把字符串str分隔开来,每次分隔一个token,返回 这个token的首地址。第一次调用传入str字符串,以后调用都传入NULL,函数内部实现中 使用了静态指针变量,它会记住上次处理到了哪里,但是这样是不好的做法,这叫不可重入的。

这个函数,调用者自己维护一个saveptr来记住当前字符串处理到哪里,这种是可重入的,r 代表reentrant,这个函数不属于C标准库,而是POSIX标准库中的。

线程安全的。

I/O库函数

文件基本概念

其它含义,有的表示指令,有的表示地址。

打开关闭文件

调用者不必知道FILE里面有哪些成员,调用者只需要把这个文件指针在库函数接口中传 来传去就行了,操作由里面的函数来维护,这种思想叫封装!这种指针叫不透明指针,也 叫句柄(handle),它就像一个把手,抓住它就可以打开门或抽屉。

mode是由rwatb+六个字符组合而成的,r是read,w是write,a是append,t是text, b是binary,对于+号,有下面的约定: r+ 必须先允许读,再允许写 w+ 必须先允许写(不存在则创建,已存在则覆盖),再允许读 a+ 必须允许追加(不存在则创建),再允许读

errno和ferror

数字,如果使用perror函数,就可以打印出来字符串(错误的原因)了。

传进去错误码,返回错误原因,有的错误码并不存在errno中,这个时候这个函数就非常 有用了,而不是只依赖于perror函数。

以字节为单位的I/O函数

如果stream是stdin,则和int getchar(void);是等价的,出错或者读到文件结尾 返回EOF

分清是EOF还是0xff,如果为int,则EOF是0xffffffff,而0xff为0x000000ff

操作位置的函数

成功返回0,出错返回-1并设置errno whence可以选择下面几个:

  1. SEEK_SET,从文件开头
  2. SEEK_CUR,从当前位置
  3. SEEK_END,从文件结尾

返回当前读写的位置

把读写位置移到到文件开头

以字符串为单位的函数

指定长度,如果不指定长度,stream又是stdin,则和gets功能一样,gets是不推荐使用 的函数,因为用户提供一个缓冲区,却不能指定缓冲区的大小。

成功返回一个非负整数,出错返回EOF

以记录为单位的函数

size是记录的长度,nmemb是要读多少条记录。成功返回记录数nmemb,失败返回0 size_t fwrite(const void *pstr, size_t size, size_t nmemb, FILE *stream);

格式化I/O函数

打印到指定的流中。

打印到指定的缓冲区中,没有指定长度,很容易产生缓冲区溢出错误。

一个参数不是通过可变参数传进来的,而是通过va_list传进来的。

数值字符串转换函数

字符串转10进制整数

字符串转浮点数

根据base,返回不同进制的整数 endptr返回后面未被识别的第一个字符 如果超过范围,则返回它能表示的最大(最小)数,并设置errno

是atof的增强版。

分配内存的函数

分配nmemb个元素的内存空间,每个元素大小是size,并且负责清0,而malloc是不清零的。

内存空间使用一段时间之后需要改变大小,就可以使用这个函数,它会重新找一块足够的内存 空间,把内容复制过来,并释放原来的内存空间。

这个是在栈上分配空间,不需要free,相当于变长数组,这不是C标准,而是POSIX标准。