__FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这些预定义符号都是语言内置的
举个例子:
#define是一种预处理指令,他有两种用法:
语法: #define name stuff
举个例子:
#define 是完全替换,比如
所以在定义的时候,为了强调他是一个整体,需要自己带上括号:
注意:由于是完全替换,在define定义标识符的时候,不要在最后加 ; 否则替换的时候会将 ; 也替换过去,会导致语法错误
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常会被解释为宏(macro)或定义宏(define macro)
下面是宏的声明方式:
#define name( parament-list ) stuff
其中的parament-list是一个由逗号隔开的符号表,他们可能出现在stuff中
注意:
参数列表的左括号必须与name紧邻
如果两者之间有任何空白存在,参数列表就会被释解释为stuff的一部分
如:
#define定义宏也是完全替换,比如:
为了防止出现失误,我们在声明的时候需要加上括号:
我们在写宏的时候,如果逻辑需要,我们可以加上足够多的括号来使宏变得完整
在程序中扩展#define定义符号和宏时,需要涉及几个步骤
注意:
如何把参数插入到字符串中?、
我们发现字符串是有自动连接的特点的
假设有这样的代码:
我们如何用宏来实现printf的功能呢,这里我们使用#
他的替换是周怎么完成的呢
这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中
使用#,把一个宏参数变成对应的字符串
比如:代码中的#N会被预处理器处理为:“N”
所以“#N”即被处理为““N””
##可以把位于他两边的符号合成一个符号 他允许宏定义从分离的文本片段创建标识符
注意:这样的连接必须产生一个合法的标识符,否则其结果就是未定义的
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的有永久性效果
x+1;//不带副作用 x++;//带副作用
MAX宏可以证明具有副作用的参数所引起的问题
这段代码输出的结果是什么?
这里我们得知道预处理之后的结果是什么:
这段代码是证明执行的呢?
宏通常被应用于执行简单的运算
比如在两个数中找出较大的一个
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
原因有二:
宏的缺点:当然和函数相比,宏也有劣势的地方:
宏有时候可以做函数做不到的事情,比如:宏的参数可以出现类型,但是函数做不到
建议:
如果逻辑比较简单,可以使用宏来实现
如果计算逻辑比较负责,那么就使用函数实现
C99之后,C++引入了内联函数的概念 inline关键字
内联函数具有函数和宏的双重优点:
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者
那我们平时的一个习惯是:
这条指令用于移除一个宏定义
许多C的编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处
(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一 个机器内存大写,我们需要一个数组能够大写。)
在编译一个程序序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的,因为我们有条件编译指令
条件编译就是:满足条件就编译,不满足条件就不编译
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译
1. #if 常量表达式 //... #endif //常量表达式由预处理器求值 如: #define __DEBUG__ 1 #if __DEBUG__ //.. #endif
表达式为真则编译,为假则不编译
2.多个分支的条件编译 #if 常量表达式 //... #elif 常量表达式 //... #else //... #endif
只会选择以一个#if或者#elif执行
3.判断是否被定义 #if defined(symbol) #ifdef symbol #if !defined(symbol) #ifndef symbol
判断(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
注意:#if 与 #endif 是配套使用的,同时出现,同时消失
我们已经知道,#include 指令可以使另外一个文件被编译,就像它实际出现于 #include 指令的地方一样
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换 这样一个源文件被包含10次,那就实际被编译10次
头文件的包含一般有两种方式:
1.包含本地文件(自己的.h文件) #include "xxx.h"(用双引号) 2.包含标准库中的文件 #include <xxx.h> (用尖括号)
查找策略:
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件
#include包括""和<>这两种情况 ""是在用户工作目录下寻找(用户的工作目录是通过编译器指定的) <>是找系统标准库函数,通过系统环境变量指定系统库目录
如果找不到就提示编译错误
如果出现这样的场景
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__ //头文件的内容 #endif //__TEST_H__
或者
#pragma once
就可以避免头文件的重复引入
在ANSI C的任何一种实现环境中,存在两个不同的环境
预编译的过程:
所有的预处理指令都是在预编译阶段处理的 (文本操作)
假如有下面一段代码
array[index] = (index+4)*(2+6)
将源代码程序输入扫描器,扫描器的任务就是简单的进行词法分析,把代码中的字符分割成一系列的记号(关键字、标识符、字面量、特殊字符等)
上面程序进行词法分析后得到了16个记号:
生成一棵语法树
一个工程中可以包含多个.c文件,如何在一个.c文件中调用另一个.c文件中的函数呢
这里我们了解一个概念叫做符号汇总
假设有这样的代码
进行符号汇总
注意:符号汇总只能汇总全局变量
把汇编代码翻译成了二进制的指令,生成了.o文件(目标文件)
假设给汇总的符号给上地址,生成一个符号表
程序执行的过程:
作为程序员,每天被bug支配着,当然应该了解下对手了。
bug原意本来为昆虫的意思,1947年9月9日,葛丽丝·霍普(Grace Hopper)发现了第一个电脑上的bug。当在Mark II计算机上工作时,整个团队都搞不清楚为什么电脑不能正常运作了。经过大家的深度挖掘,发现原来是一只飞蛾意外飞入了一台电脑内部而引起的故障(如图所示)。这个团队把错误解除了,并在日记本中记录下了这一事件。也因此,人们逐渐开始用“bug”来称呼计算机中的隐错。
所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了;如果问心有愧,就必然需要掩盖,那就一定会有迹象,迹象越多就越容易顺藤而上,这就是推理的途径
顺着这条途径顺流而下就是犯罪,逆流而上就是真相
一名优秀的程序员是一名出色的侦探
每一次尝试都是尝试破案的过程
调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序
错误的一个过程。
发现程序错误:程序员自己、测试人员、用户
测试人员站在用户的角度,测试的是发布版本
代码:
#include <stdio.h>
int main()
{
char *p = "hello world";
printf("%s\n", p);
return 0;
}
上述代码在Debug环境的结果展示
上述代码在Release环境的结果展示
Debug和Release反汇编展示对比
所以我们说调试就是在Debug版本的环境中,找代码中潜伏的问题的一个过程
那编译器进行了哪些优化呢? 请看如下代码:
#include <stdio.h>
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<=12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
如果是 debug 模式去编译,程序的结果是死循环。
如果是 release 模式去编译,程序没有死循环。
那他们之间有什么区别呢?
就是因为优化导致的
在环境中选择 debug 选项,才能使代码正常调试
最常使用的几个快捷键:
F5
F9
F10
F11
CTRL + F5
在笔记本电脑上可以配合使用FN键
在调试开始之后,用于观察变量的值
在调试开始之后,用于观察内存信息
通过调用堆栈,可以清晰的反映函数的调用关系以及当前调用所处的位置
可以查看当前运行环境的寄存器的使用信息
多多动手,尝试调试,才能进步
assert函数是C语言标准库<assert.h>中的一个函数,函数原型为:
void assert(int expression)
该函数输入参数只有一个int类型参数,返回值为void类型
assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行
用法总结与注意事项
常量指针是指针指向的内容是常量,可以有以下两种定义方式
const int* n;
int const* n;
常量指针说的是不能通过这个指针改变变量的值,但是可以通过其他的引用来改变变量的值
int a=5;
const int* n=&a;
a=6;
常量指针指向的值不能改变,但是这并不意味着指针本身不能改变,常量指针可以指向其他的地址
int a=5;
int b=6;
const int* n=&a;
n=&b;
指针常量是指指针本身是个常量,不能再指向其他的地址,写法如下
int*const n;
需要注意的是,指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向改地址的指针来修改
int a=5;
int*p=&a;
int* const n=&a;
*p=8;
区分常量指针和指针常量的关键就在于星号(*)的位置,我们以星号为分界线
如果我们将星号读作"指针",将const读作‘常量'的话,内容正好符合。
直接看错误提示信息(双击),解决问题,或者凭借经验就可以搞定
看错误提示信息,主要再代码中找到错误信息中的标识符,然后定位问题所在
一般是标识符名不存在或者拼写错误
借助调试,逐步定位问题
作为初学编程的各位小伙伴们,肯定已经或多或少地听说过编程思维这个词了,那么到底什么是编程思维呢
编程语言
表面含义,编程就是以各式的编程语言来编译代码,类似于英语,也是一门语言。那么作为语言,英语有诸多的语法,那么编程语言同样有一定的语法。毕竟写出来的代码是需要让机器看懂的。
编程思维
那么思维是什么呢,简单说就是做一件事情,脑海里要能够搭建起一个简单的框架,然后再填填补补。构建这个框架的思维就是编程思维,这要求咱们在编程前必须阅读并理解需求,不能只停留在代码的层面,要全局思考,结果会使得代码简洁又高效。
举例
For example,这是一段利用C语言写出最简单的逆序输出,显然,很通俗易懂,但是当输入n个内容,这段代码显然不适用了。
#include <stdio.h>
int main() {
int a, b, c, d, e, f, g, h, i, j;
scanf("%d %d %d %d %d %d %d %d %d %d",&a,&b,&c,&d,&e,&f,&g,&h,&i,&j);
printf("%d %d %d %d %d %d %d %d %d %d", j, i, h, g, f, e, d, c, b, a);
}
那么就可以尝试使用下面这段码
#include<stdio.h>
int main()
{
int arr[10] = {0};
for(int i = 9;i>=0;i--)
{
scanf("%d",&arr[i]);
}
for(int i = 0;i<10;i++)
{
printf("%d ",arr[i]);
}
return 0;
}
显然,这段代码就有了一定的框架结构,这就是编程思维的外在体现。