C编译和链接

1、编译和链接详解

1.1、翻译环境

C编译和链接_第1张图片

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

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

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

1.2、详述编译的阶段

处于func.c文件
int g_val = 2016;
int sum(const int x, const int y)
{
    return x + y;
}

处于test.c文件
#include 
int main()
{
 extern int sum(const int x, const int y);
 extern int g_val;
 printf("%d\n", g_val);
 print("%d\n", sum(1,2));
 return 0;
}

        C编译和链接_第2张图片

如何查看编译期间每一步发生了什么:(Linux系统下输入命令行)

        1. 预处理 选项 gcc -E test.c -o test.i        预处理之后产生的结果都放在test.i文件中

        2. 编译 选项 gcc -S test.c -o test.s          编译完成之后结果保存在test.s中

        3. 汇编 选项 gcc -c test.c -o test.o           汇编完成之后结果保存在test.o中    

1.3、运行环境

程序执行过程:

        1. 程序执行必须载入内存中。在有操作系统的环境中,一般这个由操作系统完成;在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成

        2.  程序的执行开始。调用main函数

        3.  开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值

        4. 终止程序。正常终止main函数;也有可能是意外终止

2、预处理详解

2.1、预定义符号

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

printf("file:%s line:%d\n", __FILE__, __LINE__);
printf("date:%d time:%d\n", __DATE__, __TIME__);

        这些预定义符号都是语言内置的

2.2、#define

2.2.1、#define定义标识符

#define NUM 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定义标识符的时候,不要在最后加上 ; 

2.2.2、#define 定义宏

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

#define name( parament-list ) stuff

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中

注意:

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

2.2.3、#define 替换规则

代码1
#define SQUARE_1( x ) x * x
#define SQUARE_2( x ) (x) * (x)

int a = 5;
printf ("%d\n", SQUARE_1(a + 1));
printf ("%d\n", SQUARE_2(a + 1));


代码2
#define DOUBLE_1( x ) (x) + (x)
#define DOUBLE_2( x ) ((x) + (x))

int a = 5;
printf ("%d\n", DOUBLE_1(a) * 5);
printf ("%d\n", DOUBLE_2(a) * 5);

注意 :用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,
避免在使用宏时由于参数中 的操作符或邻近操作符之间不可预料的相互作用

       1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换

       2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换

       3. 最后,再次对结果文件进行扫描,看看它是否包含任何有其他#define定义的符号,如果有,就重复上述处理过程

注意:

       1. 宏不能出现递归

       2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

2.2.4、用宏定义充当注释符号

#include 
#define BSC //
#define BMC /*
#define EMC */ 
注意:EMC已经被是先被注释掉了,BMC成为空

int main()
{
BSC printf("hello world\n");
BMC printf("hello bit\n"); EMC
return 0;
}

结论:预处理期间:先执行去注释,再进行宏替换

2.2.5、用 define 宏定义表达式

#include 
#define INIT_VALUE(a,b) do\
                        { a = 0;\
                          b = 0;\
                        }while(0)

int main()
{
    int flag = 0;
    scanf("%d", &flag);
    int a = 100;
    int b = 200;
    printf("before: %d, %d\n", a, b);
    if(flag){
        INIT_VALUE(a,b);
    }
    else{
        printf("error!\n");
    }
    printf("after: %d, %d\n", a, b);
    return 0;
}

结论:当需要宏进行多语句替换的时候, 推荐使用do-while-zero结构,如若可以,宏方面的东西,推荐少用

2.2.6、#和##

const char* p = "你好吗? ""我很好!\n";
printf("你好吗? ""我很好!\n");
printf("%s", p);
printf(p);

        字符串是有自动连接的特点的

# 的作用:

        # 可以一个宏参数变成对应的字符串

代码1:利用字符串是有自动连接的特点
#define PRINT(FORMAT, VALUE)\
 printf("the value is "FORMAT"\n", VALUE);

PRINT("%d", 10);


代码2:使用 # ,把一个宏参数变成对应的字符串
int i = 10;
#define PRINT(FORMAT, VALUE)\
 printf("the value of " #VALUE "is "FORMAT "\n", VALUE);

PRINT("%d", i+3);

## 的作用:

        ##可以把位于它两边的符号合成一个符号

#define ADD_TO_SUM(num, value) sum_##num += value;

int sum_1 = 1;
ADD_TO_SUM(1, 10);
printf("sum_1=%d\n", sum_1);

注意:这样的连接必须产生一个合法的标识符,否则其结果就是未定义的

2.2.7、带副作用的宏

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )

x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);

2.2.8、宏和函数比较

C编译和链接_第3张图片

2.2.9、命名约定

        把宏名全部大写

        函数名不要全部大写

2.2.10、宏的优缺点

优点:

        1. 增强代码的复用性

        2. 提高性能

缺点:

        1. 不方便调试宏(因为预编译阶段进行了替换)

        2. 导致代码可读性差,可维护性差,容易误用

        3.没有类型安全的检查

有哪些技术替代宏?

        1. 常量定义:换用const enum

        2. 短小函数定义:换用内联函数

2.3、#undef

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

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

例:

代码1
#include 
#define M 10
int main()
{
#define N 100
    printf("%d, %d\n", M, N);
    printf("%d, %d\n", M, N);
    printf("%d, %d\n", M, N);
#undef M //取消M
#undef N //取消N
从该位置开始,M,N,不再被识别
    printf("%d, %d\n", M, N);
    printf("%d, %d\n", M, N);
    printf("%d, %d\n", M, N);
    return 0;
}


代码2
int main()
{
#define Y X*2
#define X 3
    printf("%d\n", Y);//6
#define X 2
	printf("%d\n", Y);//4
	return 0;
}

///
代码3
void print(void);

#include 
int main()
{
#define X 3
#define Y X*2
	print();//4
#undef X
#define X 2
	print();//4
	return 0;
}

void print(void)
{
	printf("%d\n", Y);
}

//
代码4
int main()
{
#define X 3
#define Y X*2
#define X 2
	printf("%d\n", Y);//4
	return 0;
}


代码5
#include 
int main()
{
#define X 3
#define Y X*2
    printf("%d\n", Y);//6
#undef X
#define X 2
    printf("%d\n", Y);//4
    return 0;
}

结论:undef是取消宏的意思,可以用来限定宏的有效范围

2.4、#pragma

#pragma warning(disable:4996) 禁止4996报错

#pragma pack()  设置默认对齐数与内存对齐强相关

2.5、命令行定义

Linux环境演示:gcc -D __DEF__=1 test.c -o test

int main()
{
#ifdef __DEF__
    printf("defined\n");
#else
    printf("not define\n");
#endif
    return 0;
}

2.6、条件编译

常见的条件编译指令:

#if  常量表达式
#endif  常量表达式 
#else
#endif  有#if必带


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


判断是否被定义
#if defined(symbol)
#ifdef symbol

#if !defined(symbol)
#ifndef symbol


嵌套指令
#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

本质认识:

        条件编译,其实就是编译器根据实际情况,对代码进行裁剪。而这里“实际情况”,取决于运行平台,代码本身的业务逻辑等

两个好处:

        1. 可以只保留当前最需要的代码逻辑,其他去掉。可以减少生成的代码大小

        2. 可以写出跨平台的代码,让一个具体的业务,在不同平台编译的时候,可以有同样的表现

2.7、文件包含

2.7.1、头文件被包含的方式

本地文件包含
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标
准位置查找头文件,如果找不到就提示编译错误


标准库文件包含
#include 
查找策略:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误

Linux环境的标准头文件的路径:

        /usr/include

VS环境的标准头文件的路径:

        D\Windows Kits\10\Include\10.0.20348.0\ucrt

        注意按照自己的安装路径去找

库文件也可以使用 “” 的形式包含但是这样做查找的效率就低些,也不容易区分是库文件还是本地文件

2.7.2 、嵌套文件包含

每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__

头文件的内容

#endif   //__TEST_H__


或者:
#pragma once

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

你可能感兴趣的:(c语言,开发语言)