C语言中核心知识点梳理

前言

最近有些时间研究一下Android系统源码了,所以又回过头来回顾一下C语言的一些知识,无论何时我觉得C语言都是值得学习的计算机语言,它里面蕴含着很多基础的知识。

推荐两本入门书籍:C语言之父联合编写的《C程序设计语言 第2版》《C Primer Plus(第6版)》

整数在计算机中的表示

计算机中最终都是二进制的形式表示,二进制的每一位称为bit, bit是二进制数的最小单位, 8个bit为一个字节。另外在某些单片机中还存在半字节(4bit)的概念。

原码表示法

特点:最高位为符号位,对于人来说很直观,对计算机来说比较麻烦。

原码表示法

如上图:六和负六二进制位相加结果为10001100,转换为十进制为12,这个明显不是我们想要的结果,所以对于计算机而言原码表示的二进制数无法直接相加来计算结果,计算过程比较麻烦。

补码表示法

由于上面原码表示法对于计算机而言计算过程比较麻烦,所以引入了补码表示二进制数。

特点:最高位为符号位,当符号位为0的时候和原码表示相同,当符号位为1的时候绝对值按位取反加1

补码表示

第一步:-6的绝对值为6,6的二进制为 0000 0110
第二步:按位取反为1111 1001
第三步:加1为1111 1010 可以看到符号位还是1

这个时候我们来用补码算一下六加负六就会发现二进制结果为 0000 0110 + 1111 1010 = 1 0000 0000 发现成了9位数,最高位溢出被丢掉,所以最终结果为0000 0000。

地址与字节对齐

C语言可以直接对存储器地址进行访问(大部分经过操作系统访问的是逻辑地址、部分嵌入式系统可以访问到物理地址)。

一般对于32位系统而言,一次可以访问1字节、2字节或者4字节。当访问单个字节的时候不存在任何问题、而当每次访问多个字节的时候就有字节对齐问题了,所访问的起始地址必须满足N个字节的倍数,否则可能造成访问性能问题和错误。

字符编码

ASCII编码

我们知道,计算机内部,所有信息最终都是一个二进制值。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从0000 0000到1111 1111。

上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。ASCII 码一共规定了128个字符的编码,比如空格SPACE是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0。

ISO/IEC8859

由于ASCII编码是一个美国国家标准,并不是国际标准,所以后来国际化组织将它标准化定义为ISO/IEC646标准,两者内容没啥区别。

对于英文国家来说ASCII码完全够用了,但是对于希腊、拉丁语等国家就不够用了,所以国际化组织对ISO/IEC646进行了扩展,形成了ISO/IEC8859标准。后来又不够用了就在8859的基础上引入了8859-n(n从1到16),每一种都支持了一定数量的不同字母集。

中文编码

上面编码解决了大部分国家编码问题,然而中国比较特殊,所以我们国家就自己定义了一套字符编码GB2312使用两个字节表示一个汉字,后来又出现了GBK主要扩展了对繁体字的支持。

Unicode标准

虽然各个国家都有了自己所兼容的编码方式,但是问题是他们之间不是完全兼容的,大部分编码基本都兼容ASCII码(前127位),所以编码解析器就可能解析出错形成乱码。为了解决全球编码互通问题,所以出现了Unicode标准。Unicode标准几乎涵盖了所有编码字符集,它可以表示所有字符。

UTF-8编码

上面我们说到了Unicode,似乎到目前所有的编码问题都已经解决,遗憾的是Unicode并不完美,Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。这样就造成了一个问题,出现了Unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode。这个时候随着互联网的普及,强烈要求出现一个统一的编码方式。UTF-8就是在互联网上使用最广的一种Unicode的实现方式。其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。

大端和小端

内存是一个连续的存储空间,当我们要存放一段数据的时候可以有两个方向存放方式,这个方向的最小单位为字节(因为字节为计算机中数据传输的最小单位),也可以理解为字节存放的顺序。

大端和小端

例如我们从内存地址0x1000开始存放数据0x04030201,一般桌面处理器及移动设备都是小端字节序,通信设备中用大端字节序的比较普遍。

内存补齐与对齐

1
2
3
4
5
6
7
8
typedef struct MemAlign  
{
char a[18];
double b;
char c;
int d;
short e;
}MemAlign;

上面结构体占用内存并不是 18 + 8 + 1 + 4 + 2 = 33, sizeof(MemAlign)函数得出结果为40。结果为什么是这样,这就存在这一个内存补齐和对齐问题。

内存补齐

上图是以4字节为对齐基准的,假设内存开始地址为0的位置,由于char数组占用了18个字节,不是4的倍数,所以需要补齐,其他同理。

指针

指针是C语言的灵活,指针(指针变量)代表了一个地址变量。

*号在c语言中既可以是乘法,也可以表示指针变量和取值运算。

1
2
3
int a = 123;
int *p = &a; //指向a变量的地址
int c = *p; //取p所指向地址的值赋给c变量

多级指针

1
2
3
4
5
6
int a = 123;
int *b = &a; //b保存的是a的地址
int **c = &b; //c保存的是b指针的地址
int ***d = &c;

int e = ***d; //e的值是123

函数指针

上面基本数据类型指针保存的就是这个变量的内存地址,同样的函数指针也是保存的这个函数机器码的字节地址。

声明函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
void (*funP)(int);  //声明函数指针

void myFun(int x); //声明函数

int main(){

myFun(100); //调用函数

funP=&myFun; //将myFun函数的地址赋给funP变量

(*funP)(200); //通过函数指针变量来调用函数
}

myFun的函数名与funP函数指针都是一样的,即都是函数指针。myFun函数名是一个函数指针常量,而funP是函数数指针变量。

内联函数

在C语言中,如果一些函数被频繁调用,不断地有函数入栈,即函数栈,会造成栈空间或栈内存的大量消耗。

inline函数声明符声明的函数是内联函数,内联函数是对c语言编译器的暗示,建议编译器对该函数的调用尽可能的快。

1
2
3
inline void fun(int x, int y){
//...
}

inline只适合涵数体内代码简单的函数数使用,不能包含复杂的结构控制语句例如while、switch,并且内联函数本身不能是直接递归函数(自己内部还调用自己的函数)。内联函数实际上是复制为代价换取时间(空间换时间)。

结构体

定义

1
2
3
4
struct point{  //定义了一个点
int x;
int y;
}

结构体实际上是定义一种新的数据类型,上面的point相当于这个类型的名称。定义此类型的变量 struct{ ... } x, y, z;,也可以通过下面方式声明变量。

1
struct point pt;

初始化结构体可以在声明变量的时候struct point pt = {100, 120},也可以通过结构体名.成员的方式。

1
2
3
4
struct Rect{  //定义一个矩形,矩形是由两个坐标决定的
struct point point1;
struct point point2;
}

结构体和函数

下面定义了一个名字为makepoint的函数,返回值为struct point类型,这个函数里面对结构体进行了创建和赋值。

1
2
3
4
5
6
7
8
struct point makepoint(int x, int y){
struct point temp;

temp.x = x;
temp.y = y;

return temp;
}

同样的我们也可以将函数参数声明为结构体类型。

宏定义

C语言中的宏定义类似于起一个别名。

1
2
3
4
5
6
#define M (x*3+x*5)  //宏定义 M 为这段表达式的别名

int main(){
int x = 10;
int b = 2*M; //b = 2 * (10 * 3 + 10 * 5)
}

上面的宏定义属于无参数的宏定义, 也可以给宏定义带参数。

1
2
3
4
5
6
#define M(s1, s2) (x*s1+x*s2)

int main(){
int x = 10;
int b = 2*M(1, 2); //b = 2 * (10 * 1 + 10 * 2)
}

条件编译

条件编译时常用的指令:#if、#else、#elif、#endif。其一般形式为:

1
2
3
4
5
6
7
8
9
#if constant
//Statement sequence
#elif constant1
//Statement sequence
#elif constant2
//Statement sequence
#else
//Statement sequence
#endif

还有一种情况我们会判断是否定义过某个标识符,使用ifdef或者ifndef

1
2
3
4
5
#ifdef 标识符
程序段1
#else
程序段2
#endif

VisualStudio Code编辑器

目前对我来说VisualStudio Code是我经常使用的编辑器,所以我在一开始就想使用它来学习C语言。

第一步:安装C/C++插件:

C/C++插件

第二步:下载mingw64库: https://gcc-mcf.lhmouse.com/

第三步:配置环境变量

MINGW_HOME : D:\Soft\mingw64
Path : %MING_HOME%\bin

第四步:配置C/C++插件的c_cpp_properties.json,添加mingw的include路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/**",
"D:\\Soft\\mingw64\\include"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"intelliSenseMode": "msvc-x64"
}
],
"version": 4
}

第五步:安装 Code Runner 插件,方便编译运行代码。

1
2
3
4
5
6
7
#include <stdio.h>

int main(int argc, char const *argv[])
{
printf("helloworld");
return 0;
}

点击右上角三角形运行按钮

1
2
3
[Running] cd "d:\tempProject\" && gcc test.c -o test && "d:\tempProject\"test
helloworld
[Done] exited with code=0 in 0.95 seconds

资料

VS Code配置C/C++环境请参考《在 Visual Studio Code 下配置 C 语言运行环境》

《C语言标准库参考手册》