successgo / blog

This is Success Go's blog.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

C basics

successgo opened this issue · comments

C basics

C 语言基本知识要点

万丈高楼平地起。

基础不打牢,这座楼必然不稳当。

C 语言有所有语言的共性,有数据类型,有运算符,有控制结构,还有更加灵活的指针。

C 的灵魂所在 -- 指针。

C 语言是面向过程语言,但依然可以有效的组织代码,实现大的系统。

C 语言标准还在发展。从没有标准到 ANSI C,再到 C89, C99,C11,C17 等。

hello world

C 语言程序的基本结构。

主函数 main() {}

可选的头文件的引入 #include <stdio.h>#include "other.h"

可选的函数返回值。

程序示例:

#include <stdio.h>

int main(void)
{
    printf("Hello World\n"); // printf 函数在 stdio.h 标准库中的定义,因为需要引入头文件 stdio.h
    return 0;
}

要点:函数必须先声明才能调用。函数库提供了许多函数,所有需要引入头文件,从而得到函数和其他的常量定义,必要的时候还需要在编译的时候指定函数库的名称,如:数学函数库。

程序如何编译和调试:

# 默认输出  a.out
$ cc helloworld.c

# -o 指定输出的文件名,而不是 a.out
$ cc helloworld.c -o helloworld

# -S 编译输出汇编代码,默认是 at&t 语法的汇编
$ cc -S helloworld.c -o helloworld.s

# -masm=intel 输出 intel 语法的汇编
$ cc -S -o helloworld.s helloworld.c -masm=intel -fverbose-asm

# 带调试信息的编译,可用于 gdb 调试
$ cc -g -o helloworld helloworld.c

# 编译数学函数库的头文件时, -lxx xx 是具体的库名称,如数学函数库名是 m
$ cc -o helloworld helloworld.c -lm

# 查看一个二进制程序链接使用的库 ldd 工具
$ ldd /usr/bin/cc
        linux-vdso.so.1 (0x00007ffc349fd000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fa4a6a49000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa4a6888000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fa4a6bf0000)
        
# learn more: nm, ar

类型,操作符,表达式

  • 变量名

    问:为什么需要变量及变量名?
    答:变量是编程语言不可缺少的元素。当一个变量在稍后要使用,或需要重复使用,因此需要一个名称来标记变量,谓之变量名。

    问:变量名的命名规则是什么?
    答:数字不可以做变量名,数字 5 就是我们认识的数字 5,不可能将它再当做变量名来用;数字也不能做变量的开头,5d 这是不可以做变量名的,但是 d5 这样是可以做变量名的;下划线开头也是可以的,如 _d5,下划线当然也可以出现在中间,如 member_age

  • 数据类型和大小

    说到数据类型,先说一下数据是如何存储的。计算机是一堆电子元器件的组合,它只能存 0 和 1 ,也就是两个状态,我们称为位。一个位只有一个状态,要么是 0 要么是 1 ,就像一个 Switch 开关一样,要么是打开状态要么是关闭状态。为了存储和表示更多数据,我们发明了字节,一个字节等于 8 位。随着计算机的发展,又发明了 Word,谓之为 “字” 。字的大小通常代表了我们所说的计算机系统的位数。如 32 位操作系统,说明是 32 个位是一个字,即 4 个字节;64 位系统,说明是 64 位是一个字,即 8 个字节。字,展现的是计算机可以一次性处理数据的宽度,是寄存器的位数,是一次内存寻址的地址宽度,是指令的操作数的位数如加载数据和存入数据。Intel 4004 芯片是 4 位的字,Intel 8086 是 16 位的字,到目前 Intel 有 32 位和 64 位的字。并且 64 位已经成为主流。

    讲到了字,再说数据类型就简单了。常见的是数据类型是整型 Integer 。整型到底有多长,跟系统的位数有关系,请看下表:

    Type 32 bit 64 bit
    short int 16 bit 16 bit
    int 32 bit 32 bit
    long int 32 bit 64 bit
    long long int 64 bit 64 bit
    size_t 32 bit 64 bit
    void* 32 bit 64 bit

    整型的长度,完全是跟硬件架构有关,更跟编译器的实现有关。C 标准并没有严格规定。简单可以看到,在 32 位和 64 位的系统中,int 型一般 32 位,long long int 一般为 64 位,short int 一般为 16 位。但是在 16 位的系统上,short intint 可能均为 16 位了。

    以程序员的角度来思考,我们可以通过 C 运算符 sizeof 来检测类型的长度。用法如下:

    sizeof(Type)
    sizeof Type

    See intlength.c.

    Learn more about int: stdint.h

    说完整型,再说最接近的 char 型,实际是 1 字节的整型,用于存储 ASCII

    See charlength.c.

    再说说浮点型。浮点型分为单精度和双精度,分别是 floatdouble,同时还有 long double 。同样,我们可以使用 sizeof 来检测这些类型的长度。

    See floatlength.c.

    在生活中,在科学计算中,既有正数也会有负数。在 C 语言中称有符号号和无符号数,分别是用 signedunsigned 表示。有符号的数意思是将最高位置 1 表示负责,0 表示正数;无符号的数则最高位也计作数字。以上所有类型,都可以加上符号前辍,若不加,默认是有符号数。

    See signed.c.

    请看下表:

    Type 32 bit 64 bit
    float --- 32 bit
    double --- 64 bit
    long double --- 128 bit

    Learn more about float point number: IEEE-754.

  • 常量

    有变量,就可能会有不变的量,谓之常量。

    假如需要定义一个常量 PI

    #define PI 3.14

    所有用到 PI 的地方都会被替换成 3.14。这种方式,我叫它符号常量,它只是被拿过来替换一下。

    还有另一种方式,使用 const

    const double PI = 3.14;

    const 的方式,我叫它只读变量,生活中可称之为常量,是不变的变量。两者还是有区别的

    浮点运算,有专门的指令和寄存器,如 movsdxxm0~7addsd

  • 声明

    C 语言要求变量和函数,都要先声明再使用。

    int age;
    char captital = 'G';
    double price = 2.3;

    变量声明时,可以同时进行赋初值。

  • 数学运算符

    在耳濡目染下,我们常见的加、减、乘、除四则运算。
    另外还有取余运算,又叫取模。

    运算符 C 语言表示
    +
    -
    *
    /
    取余 %
  • 关系和逻辑运算符

    关系运算符,常见的有:大于、小于、等于、小于或等于、大于或等于。在汇编层面,利用 CMP 比较指令 和 Jcc 条件转移指令实现关系运算。条件转移指令如:JNEJLE等。比较指令 CMP 做了两件事,其一让两个操作数相减,其二根据相减的结果设置 EFLAGS 状态寄存器的各个状态位。而其后使用的条件转移指令则是读取 EFLAGS 中的各状态位来工作的。

    See more x86-jumps.

    逻辑运算符,常见的有:与、或、非。
    请看下表:

    逻辑运算符 表示
    &&
    ||
    !

    C 语言中的逻辑运算符到了汇编层面以后,则完全是基本的 CMPJcc 指令的组合。

    CPU 电路当中集成了大量的门电路,如:与门、非门、或门。变换交错使用这些基本的门电路可以实现复杂的电路。

  • 类型转换

    在必要的情况下,我们会进行类型转换。有些是 C 语言编译器的自动转换,还有就是程序员可以进行强制类型转换。
    强制转换的语法就是在要进行类型转换的操作数之前加上类型定义,如:

    int a = 100;
    (long int) a;
    (double) a;

    无论是哪一种转换,说到底层,自然是有汇编指令支持的。比如单精度和双精度浮点数之间的转换指令:CVTSD2SSCVTSS2SD。再如整型和双精度浮点型之间的转换指令:CVTSI2SDCVTSD2SI

    CVT: convert
    SS: Scalar Single-precision
    SD: Scalar Double-precision
    SI: Signed Integer

    类型转换,自然会有副作用。比如精度丢失 double => float,比如数据丢失 int => char

  • 自增与自减运算符

    在循环处理中,一般会进行自增或自减处理。汇编指令中有 INCDEC。在 C 语言中提供了运算符 ++--。它相比于加法和减法效率更多。

  • 位运算符

    位运算就是说每一个二进制的位都参与运算。

    位运算分为:

    位运算 C 语言表示
    按位与 &
    按位或 |
    按位取反 ~
    按位异或 ^
    按位左移 <<
    按位右移 >>
  • 赋值运算符与表达式

    变量赋值,使用 =。还有些运算符可以和它一起用。如下:

    int a = 0;
    a += 2;
    a -= 3;
    a *= 4;
    a /= 5;
    a %= 6;
    
    a >>= 1;
    a <<= 2;

    表达式,有:赋值表达式,数学运算表达式。

  • 条件表达式

    普通赋值表达式,涉及一个变量,和一个操作数。
    普通数学运算表达式,涉及一个运算符和两个操作数,并且运算符会出现在两个操作数的中间位置。
    那么,还有一种是三目运算符。形如:a ? b : c。意思是当 a 为真,结果取 b,否则结果取 c。涉及三个操作数,所以称为三目运算。

  • 运算符优先和计算顺序

    生活中都知道,先算乘除再算加减法。
    C 语言中也定义了许多的优先级,规则当然是高优先级的先运算,有括号情况下先运算括号里的表达式。

控制结构

  • statements and blocks

    表达式已经提到过,表达式后面加上 就是完整的语句。例如:

    int a;
    int b = 3;
    a = b + 3;

    多条语句往往可以表达一串完整的逻辑运算过程,可以使用 {} 包括起来构成语句块。例如:

    {
        int i, j;
        i = 1;
        j = 2;
    }

    语句块通常用于控制结构体和循环体。见下。

  • if-else

    如果要判断一个人是否是成年人,我们可以这样写程序:

    int age;
    age = 17;
    
    if (age >= 18) {
        printf("age = %d, is a adult\n", age);
    } else {
        printf("age = %d, is not a adult\n", age);
    }
  • else-if

    如果给定一个数,如果大于 0 ,输出 1 ;如果小于 0 ,输出 -1 ;否则,输出 0 。

    我们可以这样写程序:

    int number;
    number = 22;
    
    if (number > 0) {
        printf("%d\n", 1);
    } else if (number < 0) {
        printf("%d\n", -1);
    } else {
        printf("%d\n", 0);
    }
  • switch

    如果要做一个简易的四则运算的计算器,使用 if-else-if 我们当然可以做,但是我们还有一个选择。

    int a, b;
    char op;
    
    a = 3;
    b = 5;
    op = '+';
    
    switch (op) {
        case '+':
        printf("%d + %d = %d\n", a, b, a+b);
        case '-':
        printf("%d - %d = %d\n", a, b, a-b);
        case '*':
        printf("%d * %d = %d\n", a, b, a*b);
        case '/':
        printf("%d / %d = %d\n", a, b, a/b);
        default:
        printf("Not supported!\n");
    }
  • loops -- while and for

    计算机之所以强大,其一是可以按部就搬的执行程序员录入的程序;其二是可以做枯燥的重复性的计算任务,比如循环。

    循环的要领是,其一是有初值,初值可不设;其二要有结束条件;其三是要有步进器,用于修改循环体中的某个变量。对于已知循环次数的条件,可以使用 whilefor 循环。

    注意:如果初值已满足结束条件,那么便不会进入循环体。

    如果要计算一个数字的阶乘,可以这样做:

    int number;
    int result = 1;
    
    number = 7;
    for (; number > 1; number--) {
        result *= number;
    }

    或这样做:

    int number;
    int result = 1;
    
    number = 7;
    while (number > 1) {
        result *= number;
        number--;
    }

    这两种形式的循环,生成的汇编代码,完全一致。

  • loops -- do-while

    do-while 循环,跟 while 不同。不同在于,首先会进行循环体执行一次,最后再进行循环条件的判断。

    形如:

    do {
        ; // loop body
    } while (loop condition);
  • break and continue

    讲到循环,往往少不了的要跳出循环,使用 break。与之相对的就是 continue

    switch 语法里也需要借助 break 跳出分支。像上面写的简单的四则运算的计算器是有问题的。

  • goto and labels

    另外,可以在一段代码前加上 LABEL:,然后结合 goto LABEL 即可无条件跳转至由 LABEL 标记的代码处。

    goto 尽量少用。

函数与程序结构

普通 C 语言应用程序,必须有一个 main 函数,它是入口函数,那么总不能把所有的功能代码都写进 main 函数吧。并且一块功能单一可被重复使用的代码如何复用呢?答案是函数。

函数让代码分块,函数可以让问题由大化小。函数让程序变得不再平凡,就像超链接让网页伟大一样。

函数可以让一部分人专注写函数,称之为库函数,让我们站在抽象层去编程。

函数像一个黑盒子,提供输入,我们调用函数,以得到执行结果,怎么执行的,我们不用管。

  • 函数基础

    函数三要素:函数名称、函数参数、函数返回值。
    函数名称与变量名称的规则并无两样。
    函数参数,代表了函数的输入。包括有参数名称和参数的数据类型。
    函数的返回值,代表了函数的输出。主是要讲它的返回值类型。可以是任一数据类型,再加上 void 类型表示无返回值。

    ASM on X64
    整型传参利用寄存器:rdi, rsi, rdx, rcx, r8, r9,返回值 rax
    浮点传参利用寄存器:xmm0 ~ xmm7,返回值 xmm0
    更多的输入参数,则是通过堆栈。

  • 外部变量

    到目前为止,所有的变量都是在函数内定义的。包括 main 函数中的变量也是如此。这样的好处是什么:函数内的变量,别的函数是访问不到的,且一旦函数执行结束变量就会被销毁。
    问题来了,如果函数执行结束后,仍然需要保留变量,怎么办?在函数外定义变量。

  • 作用域规则

  • 头文件

  • 静态变量

    静态变量,使用 static 修饰。
    函数外,使用 static 表示本文件内可用,私有变量。
    函数内,使用 static 表示本函数内,永久变量。函数退出后,变量不会消失。

  • 寄存器变量

  • 块结构

  • 初始化

  • 递归

  • C 预处理器

    所谓预处理,是在正式编译 C 语言之前,先处理宏定义,进行宏查找和宏替换。

    很多宏指令,条件宏。

指针与数组

C 的灵活强大在于指针。

printf: %p 用于输出内存地址。

  • 指针与地址

    指针,也是一个变量。
    每一个变量,在程序运行时,系统会为其分配地址,而指针保存的就是变量的地址。如:

    int a = 100;
    int *p;
    p = &a;
    *p = 200;
  • 指针与函数参数

    函数传参是值传递,即传递的是变量的值。

  • 指针与数组

    指针,储存的是地址,是变量,可以保存其他地址。
    数组名称,标记了数组的起始元素的地址,是不变的,不可以修改。

    假如有以下代码:

    int a[] = {1, 1, 2, 3, 5, 7, 13};
    int *p;
    p = &a[0]; // or p = a;
    int i = 0;
    
    a[i];
    *(a+i);
    *(p+i);
    p[i];

    在访问数组的元素时,可以把数组名称当成指针运算,如可以取值,方法有两种:a[i]*(a+i);也可以使用指针来访问,方式也有两种:*(p+i)p[i],并且指针还可以移动,如 p++ 指向下一个数组元素的地址。

    myFunc(char a[]);
    myFunc(char *a);

    这样的用法是完全一致。

  • 字符指针与函数

    char a[] = "hello world";
    char *b = "hello world";

    这两者是不同的。a 里每一个字符或许会变,可修改;b 的里面的字符不可修改,只能重新指向另一个字符串,即只能修改 b 的地址。

    Learn more: string.h.
    strcpy(char *, char *)
    strcmp(char *, char *)

  • 指针的数组,指针的指针

    指针是变量,因此可以将一批指针组合在一起形成数组。即数组的每一个元素都是指针变量。

    举例:

    char *charlines[100];

    定义了一个数组,有 100 个元素,每个元素是指向字符的指针。

  • 多维数组

  • 指针与多维数组

    int a[10][20];
    int *a[10];

    体会两者区别。

  • 命令行参数

    int main(int argc, char *argv[])
    {
        ;
    }
  • 函数指针

    可以将函数作为参数传给另一个参数。传递的是函数的地址。是指向函数的指针。
    如何调用这个函数呢?(* funcPonter)(arg1, arg2)

  • 复杂声明

结构

  • basic sturcture

    结构体,可以将多个数据放在一起,有类型有名称,是一块地址连续的空间。
    如果要定义一个数轴上的点:

    struct point {
        double x;
        double y;
    };
    
    struct point A = { 3.0, 4.0 };
    struct point *p;
    p = &A;
    p->x;
    p->y;
  • other