第一章 初识(重识)C语言

C语言的缺点

  • 代码实现周期长,难度高.
  • 可移植性比较差: 因为它对底层硬件和操作系统的依赖较多。在不同的平台上,C代码可能需要进行适应性修改才能正确运行。这使得在不同系统上部署和维护代码变得更加困难。
  • 对平台库依赖较多: C语言通常依赖于平台特定的标准库和系统调用,这会导致代码在不同平台上的依赖性增加。如果平台之间存在差异,需要花费更多的工作来确保代码在不同系统上的正确性。
  • C 对程序员要求比较高,比如说强类型,将类型的任务交给人而不是像python交给电脑,而python即牺牲电脑性能去帮助人类更轻松编写代码. 但随着设备性能提高,这些牺牲也越来越微不足道

编译器思维

分析代码细节时,应当将自己的逻辑与编译器保持一致
但是许多时候为了方便理解和提高趣味性,会引入很多想象,但是自己一定要明白,你的想象在编译器中是什么逻辑.(比如前面的二维数组可以想象成平面直角坐标系,但是在计算机中它永远是一维存储的)

C编译的过程

  • 预处理: 宏定义和头文件展开、条件编译、去掉注释
  • 编译: 检查语法,翻译成汇编语言
  • 汇编: 翻译成机器码
  • 链接: 将目标文件(包括外部库)整合在一起形成可执行文件

头文件展开只是确保所需要的相关内容都在头文件中找到,并不会将函数原型复制到源文件中.链接才是将静态库的原型复制到源文件中,动态库则不复制,而是实时调用.

第二章 C语言概述

第三章 数据和C

简单的数据的声明

数据类型+变量名

1
2
3
4
5
6
int x : int
int a[3] : int[3]
int a[2][3] : int [2][3]
int *a : int* : 指向int类型的指针
int *a[3] : int *[3] : a是数组,长度为3且是用来存放指针,指针指向类型为int
char *(*a[3])(int, float) : char *(*[3])(int,float) : a是数组,长度是3,用于存放指针,指针指向的是函数,函数的返回值类型是char*

即去掉变量名就能知道数据类型

数据的两个属性:一种数据类型、常量或变量

在C中,所有数据都有1、数据类型,2、要么是变量要么是常量.
即便是标识符为a,我们也可以使用const关键字将它声明为一个常量.

各种数据类型的细节

  • double可以存15位以上,但是%f默认为%.6f输出(float和long double同样),且自动四舍五入.因为浮点数在后六位之后由于2进制存储不能严格精准
  • int可以定义多种进制整数,通过开头识别(0开头为8进,0x开头16进,1-9开头为10进,无法书写2进),且打印的占位符会根据不同进制而不同.如果需要打印进制数标识,占位符需要加井号,例如: %#d
  • 数据类型的最大值可以通过放在limits.h下的宏INT_MAX获得
  • 为了提高可移植性,编码采用stdint.h 中的int32_t (根据目标平台)来代替int,在平台中,会使用typedef来将int32_t对应int别称,提高代码的可读性
  • float的计算只有int的0.3倍以下,因此做非常底层的时候需要考虑尽量不使用
  • char 本质上是一个字节的整形,但是它的合同上会使它自动解析ascII码表中的内容
  • C中没有字符串类型,但可以用组合类型表达字符串char * s char s[]=”abcd”,这两种合同会检查末尾,自动补全\0,因为很多函数处理字符串时,要依赖\0来识别字符串的尾巴,计算长度等(比如%s就会读取到\0停止,因此无法读取\0后的内容.第一种字符串存在只读数据段中,节省空间,但不可修改,编译时,abcd\0将会被编到二进制文件只读数据段(.rodata)中.而第二种不但会和上面一样放到.rodata中,还会复制到栈空间中,比较占空间,但可以随便改.而且,”abcd”可以作为数组的标识符使用,它的类型是char*,内容是数组的首地址.
  • 字符串实际就是字符数组+末尾\0来组合而成.如果 char a[]={‘h’,’e’,’l’,’l’,’o’};那么它将会是普通字符数组,不是真正完整的字符串.但是也可以用%s来匹配,但是如果遇不到\0(看运气)就会一直读下去.
  • 布尔类型也不是基本类型,需要导头文件stdbool.h,非零(包括负数)即真

数据类型隐式转换(兼容地修改合同)

在x=a+b+c中,等号右结合,加号左结合,会先判断a和b的类型,进行隐式转换(往大的转)后计算,再比较计算结果和c类型,隐式转换,最后赋值时比较类型,转换(往左边转),赋值.也就是说隐式转换不完全是往大的转,也有可能转小从而丢失一定数据

1
2
3
4
5
6
7
8
int main(void) {
int a =5;
float b=3.14125;
a=b;
printf("%f",a);//输出0.0000000

return 0;
}

第四章 字符串和格式化输入/输出

第五章 运算符、表达式和语句

赋值运算符 =

如果赋值号右边的表达式是一个变量,那么将会取内容

1
2
3
int* prt=&a;
i=*ptr;
j=ptr;

第二行中*ptr已经被解释为一个变量a,所以取a内容

第三行中ptr被解释为指针变量,所以取指针变量的内容即指向的地址

且c语言严格类型匹配,=左边记得相应匹配类型

求余运算符

可以用于控制整数的范围

1
2
3
for(int i =0 ;i<1000;i++){
printf("%d",i%11);
}

可以将控制只输出1~10

第六章 C控制语句: 循环

第七章 C控制语句: 分支和跳转

switch条件语句

C中的switch条件语句只匹配整型数据,且case不解析变量,只能搭配常量(包括const声明的常量)

switch语句与if不同,它在编译时就已经(根据case生成一张跳转表),运行时将根据表匹配,因此运行效率更高.

case的参数可以是一个范围,写法是 n … m

goto语句

goto语句会降低代码可读性和破坏栈内存的正常排列,因此尽量不使用,除了:
处理各种程序出错情况后的提示或者操作(提高可读性,不影响栈内存)

第八章 字符输入/输出和输入验证

scanf()

函数本身可能会造成类型不匹配,有些编译器会直接提示约束.此函数不匹配时返回0,匹配返回1,可以利用返回值检查
因为scanf会根据标准占位符去键盘缓冲区顺序抓取对应类型的数据并将其删除,返回值就是匹配成功的数量,如果第一个标准占位没匹配到的话,后面也不会匹配,直接失败,返回0.这时候键盘缓冲区的内容将没人拿,可以使用fgets来清除
此外,fgets(*char str, int a , *FILE stream)可以从第三个参数表示的文件流中获得a个字符,存放在str中,a表示最大获取次数,每次获取一个字符,遇到\n停止,遇到空补\0停止,达到a次后停止,因此超出a之后的将截断不进入流中.

printf()

参数2可以是函数、表达式、常量和变量,可以用函数和表达式作参数减少代码量

标准占位符

具体规则查阅2.9.2

补充:

  • %d中间使用-号可以将默认的右对齐改为左对齐,使用0可以填充空白,使用n.m可以设置宽度和小数点后精度
  • scanf(%d%d)中,由于没有分隔,系统会补全\n分隔. 特别的 : scanf(%3d%d)中其实有分隔,将不会补全分隔符.即如果输入12345会scan了123和45

第九章 函数

函数的形参 (parameters) 是函数的实参 (arguments) 的副本.因此,需要在函数内部修改实参的值,需要传递实参的地址,使用指针来接收.

静态函数

静态函数可以使函数访问权限设定为本文件.可以减少与其他文件重名函数的冲突(C++中可以用命名空间解决)

递归函数

自己调用自己,例如阶乘、乘方这种,可以分步运算,恰好下一步的逻辑和这一步完全相同时.

字符串函数

strstr(char[ ] ,char*)

查找子串,是则返回子串,否则返回NULL

strlen(char* )

返回包括\0在内的字符串长度

strncat(char[] , char* , int) 、strncpy(char[] , char* , int)

这里int为第一个char* 的剩余空间长度,一般为sizeof(char* )-strlen(char* )-1
另外,函数将会把第二个参数的字符串写入第一个参数的内存空间,因此第一个参数必须是可写的
strncpy中,如果参数2比参数1短,会在后面全部清空补\0

strncmp(char* ,char* ,int)

比较字符串是否相同,如果相同返回0,第一个参数大返回1,第二个大返回-1.这里的int为比较的位数

strchr和strrchr(char* ,char)

在字符串中找字符,返回这个字符后面的(包括这个字符在内的)字符串.
strchr是从左往右找,strrchr是从右往左找.

strtok(char[] ,const char * )

如果第一个参数为char*字符串,那么将第一个分隔符前面的字符串作为子串返回首地址,并记录位置
如果第二个参数为NULL,将根据记录的位置,将下一个分隔符前面的字符串作为子串返回首地址,并记录位置
因此,函数一般在循环中使用,注意 ,函数将修改原字符串,因此原字符串必须是可修改的数组字符串而不是指针指向的常量

1
2
3
4
5
6
char a []="aaa,bbb,ccc;ddd eee.jjj";
char *p = strtok(a,",; ");
if (p!=NULL){
printf("%s",p);
p=strtok(NULL,",; ");
}

第十章 数组和指针

多维数组

数组在内存上是连续排列的,因此,即便我们将二维理解为一个平面,将三维理解成一个空间,它依旧是一维顺序排列的
其中,1~6是顺序排列的

1
int a[2][3] ={{1,2,3},{4,5,6}}其中,1~6是顺序排列的

在C中规定,编译器在给数组申请内存时,必须确定一维所需的大小,因此在上述代码中,3是必须在声明时写好,就像声明一维数组时我们需要指定int类型(四个字节)一样.编译器并不会根据初始化列表补全这个3.(个人觉得它完全可以帮我们补全,只是这件事交给程序员来做而已)

另外,在以上代码中,是不可以以a[0]={0,1,2}来赋值了.

多维数组的理解

如果只看地址值而不管地址的具体类型
int a[2][3]中,a的类型是int [2] [3] ,也可以说是int**
a[0]的类型是int [3] 也可以说是int*
a[0][0]的类型是int
因此&a[0][0]=a[0]=*a

数组在传递时的细节

数组无法作为参数传递,当数组名作为参数传递时,形参只能接受数组的首元素地址,因此,形参只有数组的首地址和数据类型,没有数组的长度.如果需要,可以额外添加一个参数作为数组长度.或者,如果时字符串的话,可以像很多处理字符串的内置函数一样,判断遇到\0则停止.

指针

在C中,不同类型的指针相互赋值是合法的,但是可能会造成类似+-操作引发问题,因为不同类型的合同不相同,因此++会根据自己类型的合同数据大小偏移相应的位.

指针在声明时尽量赋初始值,否则它有可能指向危险的内存块.一旦操作这个内存块,可能造成未知后果.因此可以在定义时赋值NULL,这是系统保留的安全地址,合法.但是不可赋值.

充分理解指针在声明时和使用时的解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int *p 中,p是一个指针,指向的是int,换句话说,p是一个int*类型

int **q中,q是一个指针,指向的是int*,换句话说,q是一个int**类型

如此,我们就可以让q指向p: q=&p;

int ***b中,b是一个指针,指向的是int**类型,换句话说,b是一个int***类型
如此,我们就可以让b指向q: b=&q;


int a[3];
int (*b)[3];
b=&a;
(*b)[0]=1;
a是一个数组,类型为int [3],但是数组名a单独使用时会被隐式转换为第一个元素的指针,因此它的类型是int*
b是一个指针,为int (*)[3],解析为"指向‘包含三个整型的数组’的指针"
如此,为了类型匹配,我们的a也要变成"指向‘包含三个整型的数组’的指针",也就是&a
至此,就可以用(*b)来代替a
在错误的理解中,会认为a是数组首地址,因此,将数组首地址赋值给指针即可:b=a.但并非如此,因为虽然ab都是指针,但是a的类型和b的类型不匹配.b需要的是整个数组的地址&a,而不是首元素的地址a,下面的b才是需要首元素地址a的情况.
int a[3];
int *b;
b=a;
a是一个数组,类型为int [3],但是数组名a单独使用时会被隐式转换为第一个元素的指针,因此它的类型是int*
b是一个指针,类型为int* 因此,a和b的类型相同.可以让b指向a
相对于前面的办法,我们可以直接用b来代替a,而前面的办法必须要*b,因为b指向的是一整个数组,进行移位时移动的单位是一整个数组.


int a [2];
printf("%p\n", a);//a被发现是一个数组,数组名被解析为首元素的地址,类型为int*
printf("%p\n",&a);//&a被解析为a的首地址,类型为int (*)[3].这里与上一条结果一样

const指针

int * const p 常指针,指针指向不能改变
const int *p 常目标指针 指针权限为只读,无法通过指针修改内存

指针和数组的关系

数组名常解析成一个指向特定类型的指针 a[1]其实就是*(a+1),相反,a如果是一个指针,也可以用a[1]指代 * (a+1)
数组名一旦声明,不可以被赋值,也就是说,数组名作为一个标识符不可以出现在等号的左边

指针函数(函数名本身就是一个函数指针)

int add(int a,int b)是一个函数,函数类型为int (int , int ),但是add也是一个函数指针,指向这个函数,因此可以把add的类型看作是int (*)(int ,int),因此,声明一个函数指针时,int ( * p )(int,int) 可以直接p=add;即可指向,这里与数组有所不同.如果p=&add ,这里编译器会自动去掉&.

第十一章 字符串和字符串函数

字符串

1
2
3
4
//以下三种都是等效的
char manualStr[] = "hello";char manualStr[] = "hello\0";
char* manualStr = "hello";char* manualStr = "hello\0";
char manualStr[] = {'h', 'e', 'l', 'l', 'o', '\0'};

前面两种方法,编译器识别到最后一个不是\0字符,因此会补一个\0,第三种方法需要手动补\0,否则,在使用%s时,将会找不到\0而出错

第十二章 存储类别、链接和内存管理

栈内存

栈(stack)先进后出,保存以下内容

  1. 环境变量 : env的环境变量,比如当前文件夹的路径等等.这使得我们使用open函数不需要输入绝对路径
  2. 命令行参数 : argv数组中存储的由命令行传入的参数将会存储到栈中
  3. 局部变量(包括调用函数)

数据段

数据段的空间不会被释放和增加,它的占用空间和地址一旦编译好后就不会改变
数据段和代码端都在编译时就已经分配内存和加载,相对于栈和堆内存是运行时根据代码分配空间

静态数据

静态数据没有初始化时会被赋值为0

  1. 全局变量 : 可以被其他文件访问
  2. static静态全局变量或函数 : 不可以被其他文件访问的全局变量
  3. static静态局部变量(访问权限为局部,存储位置为数据段):同一条static语句被多次执行,只会执行一次,被称为初始化

堆内存

堆内存只能通过指针来操作
首先,我们可以通过malloc(字节数)或者calloc(数据个数,每个数据占的字节数)来申请内存,且内存块的头部会记录这个连续的被申请的堆空间的大小.函数将返回void型指针,可以在栈空间声明一个指针来接受这个地址
然后,我们可以通过指针来操作这个内存块
最后,我们通过free(指针),程序会通过内存块头部记录的大小,将整个内存块标记为可用,即释放.如果是calloc申请的内存,程序将会自动将内存块的值清零.
另外: 由于堆内存得到时里面的值是随机的,可以用bzero()和memset()来清零或统一初始化值.

变量的局部作用域

变量的作用范围应当声明得尽量小,防止在不应该访问的地方能够访问到变量

函数声明的参数

作用仅在声明这一句,因此参数名可以随意写甚至不写,不影响功能,只影响可读性.

局部代码块的变量

在声明这一句之后的整个代码块

1
2
3
4
5
6
7
8
9
{
printf("%d",a); //声明之前无法找到a
int a=1
{
printf("%d",a);//找到a,打印1
int a =2 ;
printf("%d",a); //找到这个代码块的a,覆盖上一个代码块的a,打印2
}
printf("%d",a);//找到本代码块的a,打印1

全局变量和函数的作用范围

全局的内容可以在文件之间互相访问
文件之间访问global全局变量,需要用extern声明外部变量
访问全局函数则不需要,直接声明外部的函数就能使用
如果希望访问权限限制在本文件(文件全局作用域),可以添加static关键字声明为静态全局变量即可

静态局部变量

由于静态局部变量的访问权限只限本代码块,访问权限和局部变量一致,它的使用场景常常用于多次调用一个函数,函数内部需要计数时.因为static初始化语句只会执行一次.(static内容在编译阶段分配空间在数据段,程序运行时初始化数值)

1
2
3
4
5
6
7
8
9
10
11
void some_function() {
static int static_var = 0; // 静态局部变量
printf("static_var: %d\n", static_var);
static_var++;
}

int main() {
some_function(); // 第一次调用
some_function(); // 第二次调用
return 0;
}

总结: static
1、访问权限上(空间),限制为文件内
2、作用周期上(时间),限制为编译时分配空间,整个程序运行期间有效
3、特殊规则:变量(访问权限也要相同)的初始化语句只会执行一次
4、extern在寻找时,包含的头文件中static的内容优先级会比其他文件更高

1
2
3
4
5
6
7
8
 #include "main.h"//头文件中有hello();其他.c也有hello();定义
extern void hello(void);
int main(void) {
hello();

return 0;
}

1
2
3
4
5
6
7
 static int a =0;

int main(void) {
static int a=3;
printf("%d",a);//将会打印3,因为,虽然a的初始化只会进行一次,但是这两个a的作用范围不同,因此它其实不是同一个a.数据段将会有两个为a分配的空间,会有两个作用空间不同的a,而代码块内的a优先级比代码块外的高.
return 0;
}

第十三章 文件输入/输出

第十四章 结构和其他数据形式

结构体

  1. 结构体和函数的定义不一样,最后需要有分号
  2. 结构体成员可以通过标识符后加小数点来访问,如果是结构体指针,可以用p->name代替(*p).name;
  3. 尽量使用指定成员初始化,有利于不受成员变量后期的变化影响.

尺寸和对齐

计算机在存储结构体成员时会为了提高读取效率,进行对齐操作(根据m值分配地址).因此花的空间会比各个数据类型加起来大,空位补零.结构体的m值是成员变量中m值的最大值.
但是由于计算机的字长各不相同,计算机为了让自己读取效率高而对齐,会有不同的m值分配策略.(8字节数据在32位里m值是4,而在64位里m值是8)
因此为了让不同计算机给同一个结构体分配内存时都相同,我们需要统一规定m值.在变量定义时后面__attribute__((aligned(m)))
方案一,固定每一个成员的m值

1
2
3
4
5
struct node{
int8_t a __attribute__((aligned(1)));
int64_t b __attribute__((aligned(8)));
int16_t c __attribute__((aligned(2)));
};

方案二,压实结构体,成员间不留空隙.

1
2
3
4
5
6
struct node{
int8_t a;
int64_t b;
int16_t c;
}d__attribute__((packed));

如何计算结构体尺寸?

默认情况下,有以下规则:
1、每个结构体成员都需要存储于自身大小倍数的地址上,默认补齐
2、结构体大小必须为最大对齐数的整数倍,也就是说尾巴也会补齐
3、结构体和数组的对齐数应该是它们内部的最大成员的大小

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct A 
{
char c; //1
//1
short s; //2
int i ; //4
double d; //8
};

struct B
{
int i ; //4
int K[5]; //20 对齐数为4
char c ; //1
//3
};

struct C
{
int i; //4
struct B s1;//28 对齐数为4
char c; //1
//3
};

联合体

和结构体的定义和使用符号一致.但是里面的成员变量是互斥的.一般用在结构体中作为互斥属性的成员变量

第十五章 位操作

第十六章 C预处理器和C库

宏替换

由于宏只进行替换,不会作语法判断,因此,使用有参宏替换时一定要小心,参数有可能是一个表达式,会破坏计算优先级.
解决方案为宏定义时给参数用括号保证优先级.

无值宏

无值宏相当于只声明宏而不赋值

条件编译

由于内核源码过多,希望去除不必要的代码,编译阶段就根据条件去除不必要的代码,节省空间
关键字有#if #ifdef # #ifndef
条件编译也可以在编译阶段传入条件而不需要修改代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

int main(void) {
#ifdef A
printf("%d\n",__LINE__);
#endif

#ifdef B
printf("%d\n",__LINE__);

#endif

#ifdef C
printf("%d\n",__LINE__);


#endif

return 0;
}

只需要在编译时选择选择D+定义名(宏定义)

1
gcc c.c -o c -DA

另外,ifndef也是标准头文件防止被重复包含的解决方案,每一个头文件都希望只被包含一次,因此使用ifndef和define配合解决.

第十七章 高级数据表示

system()

函数参数将会直接传递到终端执行,成功执行返回0,否则返回其他数

1
system("open -a Calculator");

使用行号方便调试代码

printf(“[%d]”,LINE);

端序(Endianness)计算机字节的存储排列顺序(CPU决定)

数据是二进制,有高低位.地址也是二进制,有高低位.如果高位地址存数据的高位,那么为小端序存储,反之则是大端序存储

.c文件之间的访问

  1. 可以通过各自的头文件相互包含
  2. 使用extern 声明其他文件的变量和函数

宏替换

  1. 宏替换只会替换#define后面的,前面的不会替换
1
2
3
4
        printf("%f\n",Pi);  //报错
#define Pi 2.14+PP
#define PP 1

  1. 宏替换的顺序没有严格要求
1
2
3
#define Pi 2.14+PP
#define PP 1
printf("%f\n",Pi); //合理

编译选项

文件包含

gcc -I/path/to/includes main.c

C++字符串

查找子串: C++使用find,C#使用IndexOf。
替换子串: 两者都有专门的替换方法,分别是replace和Replace。
截取子字符串: C++使用substr,C#使用Substring。
大小写转换: C++需要使用std::transform,C#则内建提供了ToLower和ToUpper。
去除空白字符: C++通过erase和remove_if,C#通过Trim。
比较字符串: C++使用compare,C#使用CompareTo。
拆分字符串: C++通常使用std::stringstream,C#使用Split。
正则替换: C++使用std::regex,C#使用Regex.Replace。

C++ const

const修饰类,常对象

当函数参数有类对象时,限定函数不修改对象值,可以使用const将参数的权限缩小

const修饰类方法

const对象只能使用const类方法,const方法不可修改对象的成员参数。配合常对象使用。