C C++最新【C C++】详解程序环境和预处理(什么是程序环境(1),2024年最新2024C C++进阶学习资料

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

运行结果:

那么这些预定义符号有什么用?

  • 如果一个工程特别复杂,这时去调试时可能会无从下手。所以需要代码在运行的过程中记录一些日志信息,通过日志信息分析程序哪里出了问题,再进行排查就如同瓮中捉鳖。

🍌#define

理解预处理,那#define肯定要理解,这个相信大家都用到过

💦#define定义标识符
#define NAME stuff

用法演示:

#include <stdio.h>
 
#define TIMES 100
 
int main(void) {
    int t = TIMES;
    printf("%d\n", t);
 
    return 0;
}

运行结果:100
在预处理阶段会把 TIMES 替换为 100。预处理结束后 int t = TIMES 就没有TIMES 了,会变为 int t = 100。

// 预处理前
int t = TIMES;
// 预处理后
int t = 100;

当然了, #define 定义的符号可不仅仅只有数字,还可以用来做很多事,比如

1.#define REG register        //给关键字register,创建一个简短的名字
2.#define DEAD_LOOP for(;;)   //用更形象的符号来替换一种实现

① #define REG register,给关键字 register,创建一个简短的名字:

#define REG register
 
int main(void) {
    register int num = 0;
    REG int num = 0; // 这里REG就等于register
 
    return 0;
}

② #define DEAD_LOOP for(;;),用更形象的符号来替换一种实现:

#define DEAD_LOOP for(;;)
 
int main(void) {
    DEAD_LOOP // 预处理后替换为 for(;;); 
        ; // 循环体循环的是一条空语句
 
    DEAD_LOOP; // 那么可以这么写,这个分号就是循环体,循环的是一个空语句
 
    return 0;
}

③ #define CASE break;case ,在写case语句的时候自动字上break(很巧妙的偷懒):

#define CASE break;case     // 在写case语句的时候自动字上break
 
int main(void) {
    int n = 0;
    //switch (n) {
    //    case 1:
    //        break;
    //    case 2:
    //        break;
    //    case 3:
    //        break;
    //}
 
    switch (n) {
        case 1: // 第一个case不能替换
        CASE 2: // 相当于 break; case 2:
        CASE 3: // 相当于 break; case 3:
    }
 
    return 0;
}

有个细节,再前面 #define 定义标识符时,为什么末尾没有加上分号呢?

这是因为,分号也会被当作替换内容替换到文本当中,可能会导致出现错误:

#define _CRT_SECURE_NO_WARNINGS 1
 
#include <stdio.h>
 
#define TIMES 100;
 
int main(void) {
    int a, b;
    if (a > 10)
        b = TIMES; // b = 100;;
    else //else没有匹配对象
        b = -TIMES; // b = 100;;
 
    return 0;
}

所以,在 #define 定义标识符时,尽量不要在末尾加分号!(必须加的情况除外)

💦#define定义宏
#define NAME(parament-list) stuff

#define 机制允许把参数替换到文本中,这种实现通常被称为宏(macro)或 定义宏(define macro),parament-list 是一个由逗号隔开的符号表,他们可能出现在 stuff 中。

注意:

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

用法演示:3*3=9

#include <stdio.h>
 
#define SQUARE(X) X*X
 
int main(void) {
    printf("%d\n", SQUARE(3)); // printf("%d\n", 3 * 3);
 
    return 0;
}

那么,(3+1) 的结果是什么?

#include <stdio.h>
 
#define SQUARE(X) X*X
 
int main(void) {
    printf("%d\n", SQUARE(3+1));
 
    return 0;
}

运行结果:7

这是因为替换是在预处理阶段时替换,表达式真正计算出结果是在运行时计算。所以先替换:3+1*3+1=7

如果想获得 3+1 相乘(也就是得到 4×4 = 16) 的结果,我们需要给他们添加括号:

#include <stdio.h>
 
// 整体再括一个括号,严谨
#define SQUARE(X) ((X)*(X))
 
int main(void) {
    printf("%d\n", SQUARE(3+1));
 
    return 0;
}

另外,整体再套一个括号!让代码更加严谨,防止产生不必要的错误。比如,,我希望得到 10* DOUBLE,可能会得到以下情况:

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

💦#define替换规则

在程序中扩展 #define 定义符号或宏时,需要涉及的步骤如下:

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

注意事项:

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

🍊#和##

我们知道,宏是把参数替换到文本中。那么如何把参数插入到字符串中呢?
比如这种情况,使用函数是根本做不到的:

void print(int x) {
    printf("变量?的值是%d\n", ?) 函数根本做不到
}
 
int main(void) {
    int a = 10;
    // 打印内容:变量a的值是10
    print(a);
 
    int b = 20;
    // 打印内容:变量b的值是20
    print(b);
 
    int c = 30;
    // 打印内容:变量c的值是30
    print(c);
 
    return 0;
}

这种情况,就可以用 来实现。

💦#
#    //把一个宏参数变成对应的字符串

#把一个宏参数变成对应的字符串。

使用 # 解决上面的问题:

#include <stdio.h>
#define PRINT(X) printf("变量"#X"的值是%d\n", X);
// #X 就会变成 X内容所定义的字符串
 
int main(void) {
    // 打印内容:变量a的值是10
    int a = 10;
    PRINT(a); // printf("变量""a""的值是%d\n", a);
 
    // 打印内容:变量b的值是20
    int b = 20;
    PRINT(b); // printf("变量""b"的值是%d\n", b);
 
    // 打印内容:变量c的值是30
    int c = 30;
    PRINT(c); // printf("变量""c""的值是%d\n", c);
 
    return 0;
}

运行结果:

**改进:**让程序不仅仅支持打印整数,还可以打印其他类型的数(比如浮点数):

#include <stdio.h>
#define PRINT(X, FORMAT) printf("变量"#X"的值是 "FORMAT"\n", X);
 
int main(void) {
    // 打印内容:变量a的值是10
    int a = 10;
    PRINT(a, "%d");
 
    // 打印内容:变量f的值是5.5
    float f = 5.5f;
    PRINT(f, "%.1f"); //printf("变量""f""的值是 ""%.1f""\n", f);
 
    return 0;
}

运行结果:

💦##
##   //把位于它两边的符号合并成一个符号

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

用法演示:

#include <stdio.h>
 
#define CAT(X,Y) X##Y
 
int main(void) {
    int vs2003 = 100;
 
    printf("%d\n", CAT(vs, 2003)); // printf("%d\n", vs2003);
 
    return 0;
}

运行结果:

100

🍋#undef

#undef NAME	   //移除一个宏定义

用于移除一个宏定义。

用法演示:用完 M 之后移除该定义

#include <stdio.h>
 
#define M 100
 
int main(void) {
    int a = M;
    printf("%d\n", M);
#undef M // 移除宏定义
 
    return 0;
}

🥝宏和函数对比

举个例子:在两数中找较大值

 用宏:

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

 用函数:

#include <stdio.h>
 
int Max(int x, int y) {
    return x > y ? x : y;
}
 
int main(void) {
    int a = 10;
    int b = 20;
    int m = Max(a, b);
    printf("%d\n", m);
 
    return 0;
}

那么,宏和函数那种更好呢?

答案是宏

  • 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹
  • 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之,宏可以适用于整型、长整型、浮点型等可以用于比较的类型。因为宏与类型无关的。

当然,宏也有劣势的地方:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  2. 宏不能调试。
  3. 宏由于类型无关,因为没有类型检查,所以不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

所以,如果一个运算的逻辑足够简单,建议使用宏。反之,如果一个运算的逻辑足够复杂,建议使用函数。

🍍文件包含

我们已经知道,#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 模块。

这样最终程序中就会出现多份 comm.h 的内容,会造成文件内容的重复。
那么如何避免头文件的重复引入呢?
使用条件编译指令,每个头文件的开头写:

#ifndef __TEST_H__
#define __TEST_H__
// 头文件的内容
#endif

还有一种非常简单的方法:

#pragma once // 让头文件即使被包含多次,也只编译一份

七、常考面试题

1. 由多个源文件组成的C程序,经过编辑、预处理、编译、链接等阶段会生成最终的可执行程序。下面哪个阶段可以发现被调用的函数未定义?( )

A.预处理
B.编译
C.链接
D.执行

【答案】:C

【解析】:

预处理只会处理**#开头的语句,编译阶段只校验语法**,链接时才会去找实体,所以是链接时出错的,故选C。这里附上每个步骤的具体操作方式:

  1. **预处理:**相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有头文件(都已经被展开了)、宏定义(都已经替换了),没有条件编译指令(该屏蔽的都屏蔽掉了),没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。
  2. **编译:**将预处理完的文件逐一进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。编译是针对单个文件编译的,只校验本文件的语法是否有问题,不负责寻找实体。
  3. **链接:**通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。 链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体在此过程中会发现被调用的函数未被定义。需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的

2. test.c文件中包括如下语句:

#define INT_PTR int*
typedef int*int_ptr;
INT_PTR a,b;
int_ptr c,d;

其中定义的四个变量,哪个变量不是指针类型?( )

A.a
B.b
C.c
D.d

【答案】:B

【解析】:

预处理的#define是查找替换,所以替换过后的语句是**int\* a, b;**

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体**。在此过程中会发现被调用的函数未被定义。需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的


2. test.c文件中包括如下语句:

#define INT_PTR int*
typedef int*int_ptr;
INT_PTR a,b;
int_ptr c,d;

其中定义的四个变量,哪个变量不是指针类型?( )

A.a
B.b
C.c
D.d

【答案】:B

【解析】:

预处理的#define是查找替换,所以替换过后的语句是**int\* a, b;**

[外链图片转存中…(img-WephBqJc-1715722687265)]
[外链图片转存中…(img-b0PjFspo-1715722687266)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值