程序环境和预处理

本章重点:

程序的翻译环境
程序的执行环境
详解:C语言程序的编译+链接
预定义符号介绍
预处理指令 #define
宏和函数的对比
预处理操作符#和##的介绍
命令定义
预处理指令 #include
预处理指令 #undef
条件编译

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

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

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

图解编译+链接

C代码(文本文件)                                                                                   二进制的信息(二进制文件)

test.c(源文件)---------编译---目标文件test.obj ---|--链接----->test.exe----------运行-------->

add.c(源文件)---------编译---目标文件add.obj----|

|-----------------------------------------------------------------------------------------------------------------------|

|                                    翻译环境                                     |                   执行环境                                |

|-----------------------------------------------------------------------------------------------------------------------|

编译(编译器)                          链接(链接器)                         运行

1.预编译    2.编译     3.汇编

详解编译+链接

编译本身也分为几个阶段:Linux系统下举例

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

  • #include头文件包含(头文件代码替换#include这一条语句)
  • 注释删除(使用空格替换注释)
  • #define

2. 编译 C语言代码翻译成汇编语言:选项 gcc -S test.c 编译完成之后就停下来,结果保存在test.s

  1. 语法分析
  2. 词法分析
  3. 语义分析
  4. 符号汇总(函数,main,全局变量,函数声明)

3. 汇编 把汇编代码转换成二进制指令:gcc -c test.c 汇编完成之后就停下来,结果保存在test.o中。(Linux下的.o文件是windows下的.obj文件)

  • 形成符号表(符号 | 地址)函数声明的地址为无效的地址

链接:

  1. 合并段表(不同源文件的.obj文件合并到一起)
  2. 符号表的合并和重定位(不同源文件符号表合并,重复的重定位有效的)

翻译环境

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code) .obj(英文全称为object)
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

VIM学习资料:类似linux下的Notepad++
简明VIM练级攻略:
https://coolshell.cn/articles/5426.html
给程序员的VIM速查卡
https://coolshell.cn/articles/5479.html

运行环境

程序执行的过程:

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

注: 介绍一本书《程序员的自我修养》

预处理详解

预定义符号

  • __FILE__      //进行编译的源文件
  • __LINE__     //文件当前的行号
  • __DATE__    //文件被编译的日期
  • __TIME__    //文件被编译的时间
  • __FUNCTION__ //语句所在的函数名字
  • __STDC__    //如果编译器遵循ANSI C标准,其值为1,否则未定义

VS2019未定义

Linux gcc 遵循ANSI C标准,其值为1

代码应用:写文件日志

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

找到文件存放的目录

预处理指令

#define定义标识符

语法:

#define name stuff

举个栗子:

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

#define MAX 100;
#define MAX 100
 
建议不要加上 ; ,这样容易导致问题。 比如下面的场景:

    if (condition)max = MAX;elsemax = 0;

这里会出现语法错误。

#define 定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(definemacro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
如:

#define _CRT_SECURE_NO_WARNINGS
#define SQUARE( x ) x*x
#include <stdio.h>
int main()
{int ret = SQUARE(5);//int ret = 5 * 5;printf("%d",ret);//25return 0;
}

这个宏接收一个参数 x . 如果在上述声明之后,你把SQUARE( 5 );置于程序中,预处理器就会用下面这个表达式替换上面的表达式:5 * 5

警告: 这个宏存在一个问题: 观察下面的代码段:

#define _CRT_SECURE_NO_WARNINGS
#define SQUARE( x ) x*x
#include <stdio.h>
int main()
{int ret = SQUARE(4+1);printf("%d",ret);return 0;
}

乍一看,你可能觉得这段代码将打印25这个值。 事实上,它将打印9. 为什么?
替换文本时,参数x被替换成4 + 1,所以这条语句实际上变成了: printf ("%d\n",4 + 1 * 4 + 1 );
这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。
在宏定义上加上两个括号,这个问题便轻松的解决了:
#define SQUARE(x) (x) * (x)
这样预处理之后就产生了预期的效果:25

#define _CRT_SECURE_NO_WARNINGS
#define SQUARE(x) (x)*(x)
#include <stdio.h>
int main()
{int ret = SQUARE(4+1);printf("%d",ret);return 0;
}

定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。

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

这将打印什么值呢?
warning: 看上去,好像打印100,但事实上打印的是55. 我们发现替换之后:printf ("%d\n",10 * (5) + (5));

乘法运算先于宏定义的加法,所以出现了55
这个问题的解决办法是在宏定义表达式两边加上一对括号就可以了

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

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

#define 替换规则

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

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

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索;例如printf("MAX=%d",MAX);双引号中的MAX不被替换

#和##

#把参数插入到字符串中

C语言中字符串是有自动连接的特点的

代码例子:

#include <stdio.h>
int main()
{printf("hello world\n");printf("hello " "world\n");printf("hel""lo " "world\n");return 0;
}
/*结果
hello world
hello world
hello world
*/

要解决print函数无法改变字符串的问题

例如:

void print(int a)
{printf("the value of a is %d\n",a);
}#include <stdio.h>
int main()
{int a = 10;int b = 20;print(a);print(b);return 0;
}
/*结果
the value of a is 10
the value of a is 20
*/

想要结果为

the value of a is 10
the value of b is 20

利用函数做不到,所以使用宏

#define PRINT(X) printf("the value of "#X" is %d\n",X)
#include <stdio.h>
int main()
{int a = 10;int b = 20;PRINT(a);PRINT(b);return 0;
}
/*结果
the value of a is 10
the value of b is 20
*/

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

代码例子:

#define CAT(X,Y) X##Y
#include <stdio.h>
int main()
{int NEWyear = 2021;printf("%d\n", NEWyear);//2021printf("%d\n", CAT(NEW,year));//2021
}
/*结果
2021
2021
*/

带副作用的宏参数

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

x+1;             //不带副作用
x++;             //带有副作用
MAX宏可以证明具有副作用的参数所引起的问题。

副作用代码:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{int a = 1;int b = a + 1;    //代码1int b = ++a;    //代码2:副作用:对b一样,但是改变了a的值return 0;
}

问题:输出的结果是什么?

#define _CRT_SECURE_NO_WARNINGS
#define MAX(X,Y)  ((X)>(Y)?(X):(Y))
#include <stdio.h>
int main()
{int a = 10;int b = 11;int max = MAX(a++,b++);printf("%d\n",max );printf("%d\n",a );printf("%d\n",b );return 0;
}

 结果和解析:

#define _CRT_SECURE_NO_WARNINGS
#define MAX(X,Y)  ((X)>(Y)?(X):(Y))
#include <stdio.h>
int main()
{int a = 10;int b = 11;int max = MAX(a++,b++);//((X)>(Y)?(X):(Y))//int max=((a++)>(b++)?(a++):(b++));//(a++)>(b++)   10>11?  --->0  a=11 b=12//(b++)   max=12   b=13//12//11//13printf("%d\n",max );printf("%d\n",a );printf("%d\n",b );return 0;
}

宏和函数对比

宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个

为什么不用函数来完成这个任务? 原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。

反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的

#define _CRT_SECURE_NO_WARNINGS
//宏-类型无关-更加灵活些
#define MAX(X,Y)  ((X)>(Y)?(X):(Y))
//函数要定义多个类型函数
int Max(int x,int y)
{return (x > y ? x : y);
}
float Max2(float x, float y)
{return (x > y ? x : y);
}#include <stdio.h>
int main()
{int a = 10;int b = 11;float c = 3.14f;float d = 6.48f;int max = MAX(a, b);printf("%d\n",max );max = Max(a, b);printf("%d\n", max);float max2 = Max2(c,d);printf("%.2f\n", max2);return 0;
}

程序的规模和速度:每一句C语言代码都对应一列或多列汇编代码

//宏对应的汇编代码int max = MAX(a, b);
00CD52B0  mov         eax,dword ptr [a]
00CD52B3  cmp         eax,dword ptr [b]
00CD52B6  jle         main+63h (0CD52C3h)
00CD52B8  mov         ecx,dword ptr [a]
00CD52BB  mov         dword ptr [ebp-10Ch],ecx
00CD52C1  jmp         main+6Ch (0CD52CCh)
00CD52C3  mov         edx,dword ptr [b]
00CD52C6  mov         dword ptr [ebp-10Ch],edx
00CD52CC  mov         eax,dword ptr [ebp-10Ch]
00CD52D2  mov         dword ptr [max],eax 
//函数对应的汇编代码max = Max(a, b);
00CD52E6  mov         eax,dword ptr [b]
00CD52E9  push        eax
00CD52EA  mov         ecx,dword ptr [a]
00CD52ED  push        ecx
00CD52EE  call        _Max (0CD13B6h)  //调用函数Max
00CD52F3  add         esp,8
00CD52F6  mov         dword ptr [max],eax _Max:
00CD13B6  jmp         Max (0CD1840h) int Max(int x,int y)
{
00CD1840  push        ebp
00CD1841  mov         ebp,esp
00CD1843  sub         esp,0C4h
00CD1849  push        ebx
00CD184A  push        esi
00CD184B  push        edi
00CD184C  lea         edi,[ebp-0C4h]
00CD1852  mov         ecx,31h
00CD1857  mov         eax,0CCCCCCCCh
00CD185C  rep stos    dword ptr es:[edi]
00CD185E  mov         ecx,offset _9DFA4ABB_test@c (0CDC003h)
00CD1863  call        @__CheckForDebuggerJustMyCode@4 (0CD1316h)  return (x > y ? x : y);
00CD1868  mov         eax,dword ptr [x]
00CD186B  cmp         eax,dword ptr [y]
00CD186E  jle         _main+1Bh (0CD187Bh)
00CD1870  mov         ecx,dword ptr [x]
00CD1873  mov         dword ptr [ebp-0C4h],ecx
00CD1879  jmp         _main+24h (0CD1884h)
00CD187B  mov         edx,dword ptr [y]
00CD187E  mov         dword ptr [ebp-0C4h],edx
00CD1884  mov         eax,dword ptr [ebp-0C4h]
}
00CD188A  pop         edi
00CD188B  pop         esi
00CD188C  pop         ebx
00CD188D  add         esp,0C4h
00CD1893  cmp         ebp,esp
00CD1895  call        __RTC_CheckEsp (0CD123Fh)
00CD189A  mov         esp,ebp
00CD189C  pop         ebp
00CD189D  ret

当然和宏相比函数也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。(预编译时已经完成了替换)
3. 宏由于类型无关,也就不够严谨。(没有类型检查)
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。(传递表达式会有副作用,函数没有)

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

#define SIZEOF(type) sizeof(type)
int main()
{int ret = SIZEOF(int);//替换为int ret=sizeof(int);return 0;
}
#include <stdio.h>
#include <stdlib.h>
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{int* p = (int*)malloc(10*sizeof(int));int* p = MALLOC(10, int);return 0;
}

宏和函数的一个对比

#define定义宏

函数

每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长

函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码

更快

存在函数的调用和返回的额外开销,所以相对慢一些

宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。

函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。

参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。

函数参数只在传参的时候求值一次,结果更容易控制。

宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。

函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。

宏是不方便调试的

函数是可以逐语句调试的

宏是不能递归的

函数是可以递归的

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

那我们平时的一个习惯是:把宏名全部大写 函数名不要全部大写

C99  C++  inline - 内联函数   结合宏和函数的优点

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

#define MAX 100
#include <stdio.h>
int main()
{printf("%d",MAX);
#undef MAX
//C:\Users\93983\source\repos\test_1_19\test.c(8,18): error C2065: “MAX”: 未声明的标识符printf("%d", MAX);return 0;
}

命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。

(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)

Linux  test.c文件下

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

编译指令:

gcc test.c -D SZ=10

条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

#include <stdio.h>
#define __DEBUG__ int main()
{int i = 0;int arr[10] = { 0 };for (i = 0; i < 10; i++){arr[i] = i;
#ifdef __DEBUG__//如果__DEBUG__定义了,下面语句参加编译,否则不参加编译printf("%d\n", arr[i]);//为了观察数组是否赋值成功
#endif //__DEBUG__}return 0;
}

常见的条件编译指令:

1.单个分支的条件编译

#if 常量表达式
//...
#endif

//常量表达式由预处理器求值。

常量表达式为真则下列语句参加编译,为假下列语句就不参加编译
如:

#define __DEBUG__ 1
#if __DEBUG__
//..
#endif

2.多个分支的条件编译

#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

3.判断是否被定义

如果定义过symbol,下面语句参加编译,否则不参加

#if defined(symbol)       //写法1
语句;
#endif#ifdef symbol         //等价写法2
语句;
#endif

如果没有定义过symbol,下面语句参加编译,否则不参加

#if !defined(symbol)         //写法1
语句;
#endif#ifndef symbol            //等价写法2
语句;
#endif

4.嵌套指令

#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif

文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次。
 
头文件被包含的方式:

  • 本地文件包含
#include "filename"

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

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

/usr/include

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

VS2013:C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\include

VS2019:C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt(我的路径)

普遍方法:https://blog.csdn.net/aiqq136/article/details/112808553

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

库文件包含

#include <filename.h>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用  " " 的形式包含? 答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

嵌套文件包含

如果出现这样的场景:

  • comm.h和comm.c是公共模块。
  • test1.h和test1.c使用了公共模块。
  • test2.h和test2.c使用了公共模块。
  • test.h和test.c使用了test1模块和test2模块。
  • 这样最终程序中就会出现两份comm.h的内容。
  • 这样就造成了文件内容的重复。


如何解决这个问题? 答案:条件编译

每个头文件的开头写:

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
int Add(int x,int y);#endif

或者:写法2

#pragma once
//头文件的内容
int Add(int x,int y);

就可以避免头文件的重复引入。
注: 推荐《高质量C/C++编程指南》中附录的考试试卷(很重要)。

笔试题:

1. 头文件中的 ifndef/define/endif是干什么用的?防止头文件被多次包含
2. #include <filename.h> 和 #include "filename.h"有什么区别?查找位置,查找策略不一样

其他预处理指令

#error
#pragma
#line
...
不做介绍,自己去了解。

#pragma pack()在结构体部分介绍。

参考《C语言深度解剖》学习

bilibiliclass76-80_C语言_程序的编译(预处理操作)+链接相关推荐

  1. C语言进阶第十篇【程序的编译(预处理操作)+链接】

    ✅作者简介:大家好我是@每天都要敲代码,一位材料转码农的选手,希望一起努力,一起进步!

  2. 程序运行背后的那些事 ~ 【程序的编译(预处理操作)+链接】

  3. 2.c语言编译预处理,c语言第03章-编译预处理2.ppt

    c语言第03章-编译预处理2 第3章 编译预处理 编译预处理是指,编译时,首先对编译预处理命令进行处理,然后再将预处理后的中间结果进行编译,以得到目标代码. 教学目的: 掌握#define.#incl ...

  4. C 语言编程 — 程序的编译流程

    目录 文章目录 目录 文章目录 C 程序的编译流程 预处理 编译 汇编 链接 编译多个源文件 文章目录 <C 语言编程 - GCC 工具链> <C 语言编程 - 程序的编译流程> ...

  5. [C/C++]C语言的程序环境和预处理

    本文主要讲述C语言的程序环境和预处理. 一.程序的翻译环境和执行环境 存在源代码转换为可执行的机器指令的翻译环境和实际执行代码的执行环境,两个不同的环境.       1.翻译环境 每个源文件通过编译 ...

  6. c语言程序源代码_程序的编译、链接和执行

    同学们总是抱怨每次见到一道面试题都很难把它转化为程序源代码.然而不幸的是,即使是程序源代码对于计算机来说也还是太高级了.要想让计算机执行一段程序,我们必须把它翻译成最底层的机器指令才行.这其中要经历很 ...

  7. C语言之程序环境和预处理

    重点 程序的翻译环境 程序的执行环境 详解:C语言程序的编译+链接 预定义符号介绍 预处理指令 #define 宏和函数的对比 预处理操作符#和##的介绍 命令定义 预处理指令 #include 预处 ...

  8. C语言程序设计 | 程序环境和预处理:翻译环境和执行环境、宏、条件编译

    程序环境和预处理: 翻译环境和执行环境 宏 条件编译 翻译环境和执行环境 在ANSI C的任何一种实现中,存在两种不同的环境. 第一种是翻译环境,在这个环境中源代码被转换成可执行的机器指令.第二种是执 ...

  9. 史上最强C语言教程----程序的编译与预处理(2)

    目录 3.预处理详解 3.1 预定义符号 3.2 #define 3.2.1 #define 定义标识符 3.2.2 #define 定义宏 3.2.3 #define 替换规则 3.2.4 #和## ...

最新文章

  1. ORA-19502: write error on file xxxxx, block number xxxx
  2. .net实现md5加密 sha1加密 sha256加密 sha384加密 sha512加密 des加密解密
  3. docker mysql 无权限_Docker 中级篇
  4. python做数据库界面_python数据库界面设计
  5. 判断一个字符串是否为回文的递归算法
  6. 前端学习(711):数组导读
  7. 手把手教你写竞品分析
  8. 深入浅出PE文件格式---自己动手打造PE Show
  9. 【前端】【cornerstionjs】Cornerstone加载base64表示的jpg图像
  10. 解决虚拟机在能ping通网关情况下出现From 192.168.1.10: icmp_seq=1 Redirect Network(New nexthop: 192.168.1.1)问题
  11. 微信开发者工具之WXS和简单组件
  12. 7-49 打印学生选课清单 (25 分)
  13. 如何将html转化成mp4,怎么把mov转换成mp4格式?方法很简单,1分钟完成转换
  14. 锐捷商通v6数据库服务器位置,热烈庆祝我校开通IPv6资源
  15. SQL1005N 数据库别名 ECM 已存在于本地数据库目录或系统数据库目录中。
  16. P3324 [SDOI2015]星际战争二分答案+网络流
  17. 如何使用Node.js来制作电子音乐-编写我们的旋律
  18. nc6400 在bios中打开SATA模式就会蓝屏呢
  19. 【ODX介绍】-3.1-ODX-D,ODX-F,ODX-C,ODX-V,ODX-M的XSD文件
  20. direct3d D3DXCreateTextureFromResource 加载png

热门文章

  1. GAN 对抗生成网络代码实现
  2. 高等工程热力学复习05
  3. 【ShoppingPeeker】-基于Webkit内核的爬虫蜘蛛引擎 ShoppingWebCrawler的姊妹篇-可视化任务Web管理...
  4. vue element-ui Tabs 标签页实现【更多】功能
  5. 云电脑是否能在电脑上玩
  6. 远景html制造机模板,10.11仿冒声卡驱动:经历无数个夜晚终于制作成功,自己总结了一套制作教程分享给大家...
  7. 群发邮件软件是什么?如何高效把邮件群发出去?
  8. SpringBoot Shiro 配置自定义密码加密器
  9. 开源推荐:碰撞检测算法fcl实现python-fcl
  10. 阿里巴巴风鸣:敲着代码,操着产品、运营的心