带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。例如:
x+1;//不带副作用
x++;//带有副作用
MAX宏可以证明具有副作用的参数引起的问题。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
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;
}
宏和函数对比
宏通常被应用于执行简单的运算。
宏的优势:
1、用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数再程序的规模和速度方面更胜一筹。
2、更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之宏可以适用于整型,长整型,浮点型等可以用于>来比较的类型。宏是类型无关的。
3、宏的参数可以出现类型,函数不行。
宏的劣势:
1、每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2、宏是没法调试的。
3、宏由于类型无关,也就不够严谨。
4、宏可能会带来运算优先级的问题,导致程序容易出错。
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
命名约定
一般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者。平时的一个习惯是:
把宏名全部大写,函数名不要全部大写。
#undef
这条指令用于移除一个宏定义。
#include <stdio.h>
#define MAX 100
int main()
{
printf("%d\n", MAX);
#undef MAX
//printf("%d\n", MAX);这句无法使用
return 0;
}
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
命令行定义
许多C的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)(windows不方便操作)
条件编译
在编译一个程序的时候我们如果要将一句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
#include <stdio.h>
#define DEBUG
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = 0;
#ifdef DEBUG
printf("%d ", arr[i]);
#endif DEBUG
}
return 0;
}
常见的条件编译指令
1、
#if 常量表达式
//…
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//...
#endif
#include <stdio.h>
#define DEBUG
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = 0;
#if 0
printf("%d ", arr[i]);
#endif
}
return 0;
}
问:#if 0 #endif 和if(0),有啥区别?什么时候用前者?为什么会存在前者?
ChatAI答:
#if
语句是一个预处理指令,用于在编译前进行条件判断。#if 0
表示条件为假,因为 0
是假值。因此,#if 0
和 #endif
之间的代码不会被编译,类似于注释掉了代码。同时,由于是在编译期间就被判断为假,所以不会出现任何运行时开销。
与之相反, if(0)
是一个运行时判断,每次程序执行到该语句时都会进行判断,再决定是否执行包含在其内部的语句。也就是说,即使是假值,在运行时也会评估条件表达式,因此这会在程序的性能上带来一定的损失。
通常情况下,我们使用 #if 0
来注释掉一些不需要的代码,而不想直接删除这些代码。这种方法可以方便地在需要时恢复这些代码,而不必手动输入或复制粘贴代码。而使用 if(0)
注释掉代码可能会导致一些 IDE 或编辑器对注释代码的报错或语法高亮问题。
所以,一般情况下,我们应该优先使用 #if 0
来注释掉代码,只在某些特殊情况下(比如需要在运行期间动态控制该代码是否执行)才使用 if(0)
。
2、多分支的条件编译
#if 常量表达式
//...
#elif常量表达式
//...
#else
//...
#endif
int main()
{
#if 1==1
printf("haha\n");
#elif 2==1
printf("hehe\n");
#else
printf("hey\n");
#endif
return 0;
}
3、判断是否被定义
#if defined(symbol)&#ifdef(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”
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
linux环境的标准头文件的路径:
/usr/include
VS环境的标准头文件路径:
c:\Program Files (x86)\Microsft Visual Studio 9.0\VC\include
注意按照自己的安装路径去找
库文件包含:
#include <filename>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
推荐《高质量C/C++编程指南》中附录的考试试卷&《C语言深度解剖》
其他预处理指令
#error
#error
是一个预处理器指令,用于在编译时生成一个错误。如果使用了 #error
指令,编译时会停止并显示指定的错误消息。通常,这个指令用于在编译时检查一些条件,如果条件不满足就停止编译过程。例如,以下的代码使用 #error
指令,如果在编译时使用了 Visual Studio 2015,就会显示错误消息:
#if _MSC_VER == 1900
#error "Visual Studio 2015 is not supported."
#endif
在上面的代码中,_MSC_VER
是一个预定义的宏,它表示正在使用的编译器的版本号。如果这个版本号是 1900,也就是 Visual Studio 2015 的版本号,就会生成一个错误,停止编译过程。
#pragma
#pragma
是一个预处理器指令,通常用于向编译器传递一些指示信息。这些信息可以是编译器特定的,也可以是标准化的。#pragma
指令的语法格式如下:
#pragma 指示词
其中,指示词是一个标识符或字符串,用于指定特定的指示操作。#pragma
指令通常用于控制编译器的行为,如开启或关闭某些特性、控制代码生成优化、指定编译器警告等。例如,以下是使用 #pragma
指令控制编译器最优化级别和关闭警告的示例:
#pragma optimize(3)
#pragma warning(disable: 4996, disable: 4101)
在上面的代码中,#pragma optimize(3)
指令告诉编译器使用最高优化级别来生成代码,而 #pragma warning(disable: 4996, disable: 4101)
则告诉编译器关闭警告编号为 4996 和 4101 的警告信息。请注意,具体的 #pragma
指令和其语法格式可能因编译器而异。
#line
#line
是一个预处理器指令,用于设置当前源文件的行号和文件名。这个指令可以用于生成源代码映射文件(source map),也可以用于编写代码生成工具。#line
指令的语法格式如下:
#line 行号 "文件名"
其中,行号和文件名是可选的参数,如果省略这些参数,则会重置行号和文件名为默认值。例如,以下是使用 #line
指令设置源文件的行号和文件名的示例:
#line 42 "example.cpp"
cout << "Hello, World!" << endl;
在上面的代码中,#line 42 "example.cpp"
指令将行号设置为 42,文件名设置为 example.cpp。因此,任何编辑工具或调试器都会将这行代码映射到 example.cpp 的第 42 行。
问:C语言inline
ChatAI答:
在 C 语言中,inline
是一种关键字,用于告知编译器对于函数的调用是否应该被内联。内联函数可以实现函数的调用处替换为函数本身的代码,这一过程可以减少程序运行时函数调用的开销,从而提高程序的运行速度。
使用 inline
声明的函数并不一定会被内联,具体取决于编译器的实现。编译器可能会根据该函数的复杂度、代码大小等因素来作出内联的决策。当然,我们也可以使用 -O3
等编译选项来告诉编译器尽可能地内联函数。
以下是一些使用 inline
的示例:
inline int add(int a, int b){
return a + b;
}
在上述代码中,add
函数被声明为内联函数,以便在需要时可以被内联。
需要注意的是,在 C99 标准中,inline
的语义有所不同。在 C99 中,inline
更多地被认为是一种链接指令,它告诉编译器,这个函数可能在多个源文件中出现,但所有对该函数的调用都应该被替换为一个函数的实例,以便减少链接时的重复代码。
百度笔试题:
写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
考察: offsetof 宏的实现
问:写一个宏offsetof,计算结构体中某变量相对于首地址的偏移,并给出说明
ChatAI答:
定义一个宏 offsetof
可以帮助我们以一种类型安全的方式计算结构体中某个变量的偏移量。由于 offsetof
的具体实现与操作系统和编译器的体系结构和实现有关,因此在此仅提供一种可能的实现方式,具体如下:
#define offsetof(st, m) \
(size_t)(&((st *)0)->m)
其中,st
是指结构体类型,m
是在这个结构体中的成员变量。给定一个结构体的变量 obj
和成员变量 m
,我们可以通过以下方式获取偏移量:
#include <stddef.h> // for offsetof macro
// Example structure
struct Example {
int x;
double y;
char z[10];
};
int main() {
struct Example obj;
size_t offset = offsetof(struct Example, y);
printf("Offset of y: %zu\n", offset);
return 0;
}
输出:
Offset of y: 4
在上述示例中,我们计算了结构体 Example
中成员变量 y
相对于结构体首地址的偏移量。请注意,我们将指针类型强制转换为结构体类型 st
的指针类型,然后通过 &
符号获取成员变量 m
的地址,最后将其转换为 size_t
类型,以表示偏移量。此实现方法也可以用于计算结构体中嵌套结构体的偏移量。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>
struct S
{
char c1;
int a;
char c2;
};
#define OFFSETOF(struct_name,member_name) \
(int)&(((struct_name*)0)->member_name)
int main()
{
//printf("%d\n", offsetof(struct S, c1));
//printf("%d\n", offsetof(struct S, a));
//printf("%d\n", offsetof(struct S, c2));
printf("%d\n", OFFSETOF(struct S, c1));
printf("%d\n", OFFSETOF(struct S, a));
printf("%d\n", OFFSETOF(struct S, c2));
return 0;
}