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 int
和int
可能均为 16 位了。以程序员的角度来思考,我们可以通过 C 运算符
sizeof
来检测类型的长度。用法如下:sizeof(Type) sizeof Type
See intlength.c.
Learn more about int:
stdint.h
说完整型,再说最接近的
char
型,实际是 1 字节的整型,用于存储ASCII
。See charlength.c.
再说说浮点型。浮点型分为单精度和双精度,分别是
float
和double
,同时还有long double
。同样,我们可以使用sizeof
来检测这些类型的长度。See floatlength.c.
在生活中,在科学计算中,既有正数也会有负数。在 C 语言中称有符号号和无符号数,分别是用
signed
和unsigned
表示。有符号的数意思是将最高位置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
的方式,我叫它只读变量,生活中可称之为常量,是不变的变量。两者还是有区别的。浮点运算,有专门的指令和寄存器,如
movsd
,xxm0~7
,addsd
。 -
声明
C 语言要求变量和函数,都要先声明再使用。
int age; char captital = 'G'; double price = 2.3;
变量声明时,可以同时进行赋初值。
-
数学运算符
在耳濡目染下,我们常见的加、减、乘、除四则运算。
另外还有取余运算,又叫取模。运算符 C 语言表示 加 + 减 - 乘 * 除 / 取余 % -
关系和逻辑运算符
关系运算符,常见的有:大于、小于、等于、小于或等于、大于或等于。在汇编层面,利用
CMP
比较指令 和Jcc
条件转移指令实现关系运算。条件转移指令如:JNE
、JLE
等。比较指令CMP
做了两件事,其一让两个操作数相减,其二根据相减的结果设置EFLAGS
状态寄存器的各个状态位。而其后使用的条件转移指令则是读取EFLAGS
中的各状态位来工作的。See more x86-jumps.
逻辑运算符,常见的有:与、或、非。
请看下表:逻辑运算符 表示 与 && 或 || 非 ! C 语言中的逻辑运算符到了汇编层面以后,则完全是基本的
CMP
和Jcc
指令的组合。CPU 电路当中集成了大量的门电路,如:与门、非门、或门。变换交错使用这些基本的门电路可以实现复杂的电路。
-
类型转换
在必要的情况下,我们会进行类型转换。有些是 C 语言编译器的自动转换,还有就是程序员可以进行强制类型转换。
强制转换的语法就是在要进行类型转换的操作数之前加上类型定义,如:int a = 100; (long int) a; (double) a;
无论是哪一种转换,说到底层,自然是有汇编指令支持的。比如单精度和双精度浮点数之间的转换指令:
CVTSD2SS
和CVTSS2SD
。再如整型和双精度浮点型之间的转换指令:CVTSI2SD
和CVTSD2SI
。CVT: convert
SS: Scalar Single-precision
SD: Scalar Double-precision
SI: Signed Integer类型转换,自然会有副作用。比如精度丢失
double => float
,比如数据丢失int => char
。 -
自增与自减运算符
在循环处理中,一般会进行自增或自减处理。汇编指令中有
INC
和DEC
。在 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
计算机之所以强大,其一是可以按部就搬的执行程序员录入的程序;其二是可以做枯燥的重复性的计算任务,比如循环。
循环的要领是,其一是有初值,初值可不设;其二要有结束条件;其三是要有步进器,用于修改循环体中的某个变量。对于已知循环次数的条件,可以使用
while
和for
循环。注意:如果初值已满足结束条件,那么便不会进入循环体。
如果要计算一个数字的阶乘,可以这样做:
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