c语言进阶篇:带你全方面学习程序环境和预处理

发布于:2023-01-20 ⋅ 阅读:(7) ⋅ 点赞:(0) ⋅ 评论:(0)

🎈程序的翻译环境和执行环境

在ANSIC的任何一种实现中,存在两个不同的环境。

🍁第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
🍁第二种是执行环境,它用于实际执行代码。

在这里插入图片描述

🎉翻译环境

在这里插入图片描述

组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。

每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。

链接器同时也会引入标准C函数中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,讲其需要的函数也链接到程序中。

编译和链接生成可执行程序是整个程序编译的过程,而单拿出编译这个过程来说,其中又包含几个阶段,具体如下:

可在Linux环境下,使用gcc编译器对编译期间的每一步进行观察;

在这里插入图片描述

🎉编译过程的三个阶段

预编译/预处理--------编译--------汇编

1.预处理 选项 gcc -E test.c -o test.i 预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。

主要完成的动作(文本操作):

  1. 1.头文件的包含(#include)
  2. #define定义的符号的替换并将宏定义删除
  3. 注释的删除

2.编译选项 gcc -S test.c 编译完成之后就停下来,结果保存在test.c中。

主要完成的动作:

将C语言代码转化为汇编代码
其中有四个过程:分别是语法分析、词法分析、语义分析、符号汇总;

而符号汇总这一过程是分别汇总的各个源文件中的全局符号;

另外函数未声明是在编译期间会给出警告或报错。

3.汇编 gcc -c test.c 汇编完成之后就停下来,结果保存在test.o中。

主要完成的动作:

  1. 把汇编代码转化成二进制指令
  2. 把编译期间汇总的符号形成符号表

🎉链接

主要完成的动作:

  1. 1.合并段表
  2. 符号的合并和重定位;也就是在相同的符号中筛选出符号正确的地址

所以如果函数只声明未定义,函数符号在符号表中的地址是没有意义的,通过符号表中的地址无法定位找到函数,会报链接错误!

🎉运行环境

程序执行的过程:

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  2. 程序的执行便开始。接着便调用main函数。
  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
  4. 终止程序。正常终止main函数;也有可能是意外终止。

🎈预处理

🎉预定义符号

预处理符号

__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

这些预定义符号都是语言内置的,是可以直接使用的!

#include<stdio.h>
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("file:%s line:%d data:%s time:%s i:%d\n", __FILE__,
		__LINE__,__DATE__, __TIME__, i);
	}
	return 0;
}


在这里插入图片描述
作用是用于写日志:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int i = 0;
	FILE* pf = fopen("log.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return EXIT_FAILURE;
	}
	i = 0;
	for (i = 0; i < 10; i++)
	{
		fprintf(pf, "file:%s line:%d data:%s time:%s i:%d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

🎉#define

✏️#define定义的标识符

语法:
#define name stuff

注意:在define定义标识符的时候,最好不要加上;

#define MAX 1000
#define reg register          //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)     //用更形象的符号来替换一种实现
#define CASE break;case        //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,
//每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                           date:%s\ttime:%s\n" ,\
                           __FILE__,__LINE__ , \
                           __DATE__,__TIME__ )

#define MAX 1000;
#define STP "hello"
#define print printf("hehe\n");
int main()
{
	int m = MAX;
	printf("%d\n", MAX);
	printf("%s\n", STP);
	print;
	return 0;
}


在这里插入图片描述
在这里插入图片描述

✏️#define定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义 宏(define macro)。

宏的申明方式 :#define name( parament-list ) stuff其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

#include <stdio.h>
#define SQUARE( x ) x * x
int main()
{
    int r = SQUARE(5);
   //r = 5*5;
    //这里存在缺陷:
   int a = SQUARE(5+1);
    //认为是:a = (5+1)*(5+1)?错误,由替换产生的表达式并没有按照预想的次序进行求值
    //a = 5+1*5+1 = 11
    //在宏定义上加上两个括号,这个问题便轻松的解决了:
    return 0;
}

#define SQUARE(x) ((x)*(x))
int main()
{
	int r = SQUARE(5);
	int s = SQUARE(5 + 1);
	printf("%d\n", r);
	printf("%d\n", s);
	return 0;
}

在这里插入图片描述
这里有一个问题:宏定义要不要把整体括号括起来?我们来看一段代码:

#include <stdio.h>
#define DOUBLE(x) (x)+(x)
int main()
{
	int r = 10 * DOUBLE(3);
	printf("%d\n", r);
	return 0;
}


在这里插入图片描述
结果为33,那如果整体有括号呢?

#include <stdio.h>
#define DOUBLE(x) ((x)+(x))
int main()
{
	int r = 10 * DOUBLE(3);
	printf("%d\n", r);
	return 0;
}

在这里插入图片描述
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

✏️#define替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤:

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先 被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

✏️#和##两个预处理的工具

如何把参数插入到字符串中?

#include<stdio.h>
int main()
{
	char* p = "hello ""world\n";
	printf("hello"" world\n");
	printf("%s", p);
	return 0;
}

在这里插入图片描述
字符串是有自动连接的特点的。

#define PRINT(FORMAT, VALUE)\
printf("the value is "FORMAT"\n", VALUE);
...
PRINT("%d", 10);

这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。
另外一个技巧是:
使用 # ,把一个宏参数变成对应的字符串。

#include<stdio.h>
#define PRINT( NAME, TYPE) printf("the value of "#NAME" is "TYPE"\n",NAME);
int main()
{
	double a = 4.0;
	PRINT(a, "%lf");
	int b = 10;
	PRINT(b, "%d");
	return 0;
}

在这里插入图片描述
##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。

#include<stdio.h>

#define CAT(X,Y) X##Y

int main()
{
	int class101 = 100;
	printf("%d\n", CAT(class, 101));
	printf("%d\n", class101);
	return 0;
}

在这里插入图片描述

✏️带副作用的宏参数

在这里插入图片描述
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能 出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。

例如:

x+1;//不带副作用

x++;//带有副作用

在这里插入图片描述

✏️宏和函数的优缺点对比

在这里插入图片描述
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

#define MALLOC(num, type) (type*)malloc((num)*sizeof(type))
#include<stdio.h>
int main()
{
	//malloc(40);
	//malloc(10, int);
	int*p = MALLOC(10, int);
	//int* p = (int*)malloc((10) * sizeof(int));

	return 0;
}

✏️命名约定

一般来讲函数的宏的使用语法很相似;所以语言本身没法帮我们区分二者。

那我们平时的一个习惯是:

把宏名全部大写

函数名不要全部大写

🎉#undef

这条指令用于移除一个宏定义。

#undef NAME //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

在这里插入图片描述

🎉命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。 例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假 定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一 个机器内存大写,我们需要一个数组能够大写。)

#include <stdio.h>
int main()
{
    int array [ARRAY_SIZE];
    int i = 0;
    for(i = 0; i< ARRAY_SIZE; i ++)
   {
        array[i] = i;
   }
    for(i = 0; i< ARRAY_SIZE; i ++)
   {
        printf("%d " ,array[i]);
   }
    printf("\n" );
    return 0;
}

编译指令:

//linux 环境
gcc -D ARRAY_SIZE=10 programe.c

🎉条件编译

常见的条件编译指令:

1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif

2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol

4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif

🎉文件包含

#include 指令可以使另外一个文件被编译;就像它实际出现于 #include 指令的地方一样。

这种替换的方式很简单:

预处理器先删除这条指令,并用包含文件的内容替换。

这样一个源文件被包含10次,那就实际被编译10次。

✏️头文件包含

本地文件包含

#include "filename"

库文件包含

#include <filename.h>

使用<>和“ ”文件包含的区别
<> :直接去库目录(标准路径)下查找, 如果该头文件未找到 ,提示编译错误。
" " :先去代码所在的路径下查找,如果该头文件未找到,再去库目录(标准路径)下查找,如果找不到,提示编译错误。

✏️嵌套文件包含

在这里插入图片描述
如果出现这样的场景

comm.h和comm.c是公共模块。

test1.h和test1.c使用了公共模块。

test2.h和test2.c使用了公共模块。

test.h和test.c使用了test1模块和test2模块。

//每个头文件的开头写:

#ifndef __TEST_H__

#define __TEST_H__

//头文件的内容

#endif   //__TEST_H__

//或者:

#pragma once

//就可以避免头文件的重复引入。 

🎈笔试题

写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
offsetof宏的实现:

#include <stdio.h>
#include <stddef.h>
struct S
{
	char c1;
	int i;
	char c2;
};

#define OFFSETOF(type,m_name) (size_t)&(((type*)0)->m_name)
int main()
{
	struct S s = { 0 };
	printf("%d\n", OFFSETOF(struct S, c1));
	printf("%d\n", OFFSETOF(struct S, i));
	printf("%d\n", OFFSETOF(struct S, c2));

	//printf("%d\n", offsetof(struct S, c1));
	//printf("%d\n", offsetof(struct S, i));
	//printf("%d\n", offsetof(struct S, c2));
	return 0;
}


在这里插入图片描述