C语言知识体系梳理讲义(二)

这里引用了《深入理解计算机系统》、《C和指针》、《大话设计模式》等书
第一章在C语言知识体系梳理讲义(一)

《C和指针》上的例子

下面我们来讲一个小例子。其实这个例子我找了好久,最后我选出来了一个比较合适的是来自《C和指针》里面开篇的例子。这篇例子被我放到了CSDN中大家可以运行试试。我们要分析的例子是从标准输入中读取文本并对其进行修改,然后将其写到标准输出中。首先读取一串列标号。这些列表号成对出现,表示输入行的列范围。这串列表号以一个负值结尾,作为结束标志。剩余的输入行被程序读入并打印,然后输入行中被选中范围的字符串被提出来并打印。注意,每行第一列的列表号为0。

《C和指针》源码地址

/*
**这个程序会从标准输入中读取输入行并在标准输出中打印这些输入行,
**每个输入行的后面一行是该行内容的一部分
**
**输入的第一行是一串列标号,串的最后以一个复数结尾。
**这些列标号成对出现,说明需要打印的输入行的列的范围。
**例如,0 3 10 12 -1 表示第0列到第3列,第10列到第12列的内容将被打印。
*/
  • 首先开篇的1到8行是一个对整体文件的注释。一段合格明朗的代码。一定要写出良好的代码规范。这样不仅是对自己程序负责,更是为了对自己程序在今后的修改中更简单易懂。具体的规范将在后面放出图片(采用Google代码规范)。这一块注释讲解了文件的内容、方法以及例子,在具有众多文件的程序中,每一页程序最好在开头都标注文件内容创建时间文件名以及命名空间等信息。(来自《大话设计模式》)有一个小细节是:在我们用VS注释掉自己不需要的内容时都喜欢直接摁ctrl+K+C,消除注释的时候喜欢用ctrl+K+u,但是并不是将其从源文件中删除。如果期间有注释的存在的话就会打破这一平衡。使程序出现奇怪的错误。最好的方法是使用宏定义,即像这样:
#if 0
    statements
#endif

这样在if和endif之间的程序就可以更有效的从程序中去除。接下来我们看预处理指令。9到14行被称为预处理指令,因为他们是由预处理器解释的。下面让我来稍微详细的讲解一下一个C文件被完整编译的过程:

计算机编译过程

下图是我从笔记中截下的图:
计算机编译过程

第一步叫预处理也叫预编译

预处理

让我们打开cmd,首先在栏中输入gcc -E text.c -o text.i,这时我们得出了一个在预处理之后形成的.i文件。这是一个经过修改后的源程序,我们打开看一眼:预处理器将所有的define删除并且展开了所有的宏定义,同时处理掉所有if ifdef undef ifndef endif elif 的预编译指令,在刚才我们所用的方法就是将该文件在预编译阶段处理掉。同时我们也向文件内加载inclde所包含路径的头文件。我给大家打开看一眼我在VSCode里面加载的路径。这个路径我有两套有一套在VS2019文件夹中。但是之前尝试共用一套mingGW时候发生了问题我就偷懒没有取解决。当我们在VS中编译错误的时候编译器会反馈给我们有行号注释的错误,这一部分也是因为在这里编译器给每一行增加了编号。

第二步则是编译阶段

编译

在栏中输入 gcc -S hello.i -o hello.s 这时我们就得到了一个较为完整的汇编程序文本。在这里比之前的文件就精细了很多。学过汇编的同学就会看见这里的一些的低级机器指令。比如pushq 将四字压入栈。popq将四字弹出栈之类的。这个在后续C++ STL讲解中可以很好的发现相同点。在阅读这些汇编代码时候我们可以理解汇编器的优化能力,并分析代码中隐含的低效率。这里是程序较为核心的地区。往往程序就是从这一层次会遭受蠕虫病毒的攻击。当然不是从这个文件里。有特定的软件比如OLLYDBG就可以直接打开exe文件运行时的二进制文件。对这个有兴趣的同学也可以去B站上找一些逆向技术的视频看。我原来看的视频已经失效了所以就不给大家推荐辣!

第三步就是汇编了

汇编

在栏中输入gcc -c text.s -o text.o。这一阶段过后我们就已经离开了可以被人识别的阶段。这些汇编指令被打包成一种叫做可重定位目标的程序。打开.o文件后就会发现里面都是一些乱码。

最后一步就是连接

链接

链接器将各种在.s文件中的.o形式进行组装。比如在.s文件种跳转指令call引用了printf,这个printf函数存在于一个名为prinf.o文件中。最后就得到了我们所要的exe文件即可以用gcc -o text text.o得到exe文件。

继续例子

好的我们书接前文,刚才我讲完了宏定义,让我们再向下进行:

函数原型
int read_column_numbers(int columns[],int max);
void rearrange(char *output,char const *input,int n_columns,int const columns[]);
 

16、17两行这些声明被称作函数原型。他告诉了我们定义函数的特征。C语言文件是按照线性顺序读取的,如果没有这些函数声明,而函数又在主函数的后面就会报错,因为程序指针向上搜索的时候没有找到这个函数。但是也有很多同学喜欢把函数主体放在主函数前面,我感觉这样既不能突出文件的功能,又会使函数变得很乱。在后期维护的时候维护人员会很麻烦。在很多多文件程序中都比较喜欢用pch.h来装载这写头文件宏定义以及函数声明。在我们学习之初一定要注意声明与定义之间的区别。这个大家可以课后去网上详细的了解一下为以后写更大的程序做铺垫。
这里有几个知识点就是形参与实参,数组参数与指针参数的区别。

形参与实参

形参与实参比较容易理解。形参就是只有外形,没有实际变量,用于接受参数。形参会接受实际参数的值。而实参就是实际的量,有具体的数值。在函数调用的时候传入函数的参数就可以叫做实参。

/*
	**读取该串列标号
	*/
	n_columns = read_column_numbers(columns,MAX_COLS);
 
	/*
	**读取,处理和打印剩余的输入行
	*/
	while(gets(input)!=NULL){                      //gets函数从标准输入读取一行文本
	                                                 //并把它存储于作为参数传递给它的数组中
		printf("Original input:%s\n",input);
		rearrange( output ,input,n_columns,columns);
		printf("Rearrangd line:%s\n",output);
	}

在主函数中这两个地方就是向函数传入实参的位置。

数组参数与指针参数

第二点对于数组参数与指针参数的区别。这一点在《C primer plus》中讲的比较清楚也非常详细。大家如果听不明白的话可以课后买一本或者去图书馆借一本。但是这本书和《大话数据结构一样》特别畅销,在图书馆基本上借不到还是得自己偷偷买。
在函数传递的过程中分为传参和传址。传参的时候函数将在调用之初拷贝一份形参,而传址的时候直接将地址赋值给形参。在read_column_numbers中max就是被传参的数据,而指针columns看起来是传递了一个数组的首地址,实际上是传递了数组首地址的拷贝。所以在隐藏条件下传入函数的值也同时改变了原函数的值。对于指针来说指针变量是将原地址传给函数中的指针。在传入指针时无法改变指针变量,擅自更改指针变量会编译失败不能通过编译。

#include<stdio.h>
void mmm(int aa[], int number) {aa[1] = 4;}
void mm(int* bb, int number) {int x = 5;(bb + 3) = &x;}
void m(int* cc, int numebr) { int x = 5; *(cc + 3) = &x;}
int main(void) {
	int aaa[30] = { 0 };
	for (int i = 0; i < 30; i++) {printf("1.%d", aaa[i]);}
	mmm(aaa, 30);
	printf("2.%d", aaa[1]);
	mm(aaa, 30);
	printf("3.%d", aaa[2]);
}

这里通过引用一个错误的案例来介绍。在(bb+3)的地方出现错误:不可修改的左值。而使用*来取地址的话会存在比较答的风险。而作为数组传递来的数值可以简便而轻易的更改。

最后的细碎知识点。

最后我们来稍微了解一下紧凑的表达形式。本文中多次使用了比较紧凑的结构,方便阅读还不影响长度

while(gets(input)!=NULL){}
while( num < max && scanf("%d",&columns[num]) ==1 && columns[num]>=0 )

其实在了解了一些函数的副作用后我们就会发现紧凑表达的方法对我们整体思维都有很大的帮助。令我们感到奇怪的地方是为什么scanf可以返回1,printf可以返回1,实际上printf函数与scanf函数的主要返回值并不是标准输入和输出,我们查看一下函数原型就会发现

int printf(char *fmt, ...)
{
va_list args;
int n;
va_start(args, fmt);
n = vsprintf(sprint_buf, fmt, args);
va_end(args);
write(stdout, sprint_buf, n);
return n;
}

这个printf函数的主要功能居然是统计字数或者说是输出字符的数量。
而scanf函数的返回值为1 0 EOF,而我们通常使用的只是这两个函数的副作用。
再举一个紧凑表达的小例子:

typedef int(*signalCallBack)(int signal,char* buffer);
signalCallBack Callback

乍一看我们会发现我去这是什么东西?????
但是我们从小到大细心发掘一下就会发现这其实是好几层的定义套在一起:

  1. int* signalCallBack —这是一个指针,一个 int类型的指针
  2. int* signalCallBack ()—后面有括号代表什么这是一个函数,一个有参数返回值为int地址的函数
  3. (int signal,char* buffer)—函数具有两个参数,分别是int signal,char* buffer
    那么我们就明确了int(signalCallBack)(int signal,char buffer)其实是一个函数这个函数接受两个值返回一个值加上typedef 后signalCallBack代表了这一类函数的别名,我们就可以进行如下定义signalCallBack Callback
    最后附上Google代码的规范以及C语言基础知识梳理照片谢谢大家观看!!
    (PS:这两个图片是网图侵权提醒我一下我删除掉谢谢)

(附件)Google代码规范

Google代码规范

(附件)C语言知识梳理

C语言知识体系梳理

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值