C语言详解(预编译)

Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~
💥💥个人主页:奋斗的小羊
💥💥所属专栏:C语言

🚀本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为展示我的学习过程及理解。文笔、排版拙劣,望见谅。


目录

  • 前言
    • 1、预定义符号
    • 2、#define定义常量和标识符
    • 3、#define定义宏
    • 4、带有副作用的宏参数
    • 5、宏替换的规则
    • 6、宏和函数的对比
    • 7、# 和
      • 7.1 #运算符
      • 7.2 ##运算符
    • 8、命名的约定
    • 9、#undef
    • 10、命令行定义
    • 11、条件编译
    • 12、头文件的包含
      • 12.1 头文件被包含的方式
        • 12.1.1 本地文件包含
        • 12.1.2 库文件包含
      • 12.2 嵌套文件的包含
  • 总结

前言

本篇文章将详细介绍编译过程中预编译的具体细节
在C语言的学习中部分人可能会忽视这一部分的学习,因为像VS这样相对强大的集成开发环境,我们在写好代码后只需要开始执行即可,所以部分人认为这一部分不值得我们花费时间去学习
其实不然,学习C语言预编译过程可以帮助我们更深入地了解C语言的编译过程和语法特性,提高代码编写的效率和质量,以及拓展编程技能


1、预定义符号

C语言设置了一些预定义符号,可以直接使用,预定义符号也是在预编译阶段处理的

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

例如:

在这里插入图片描述


2、#define定义常量和标识符

#define定义的常量和标识符在预编译阶段完成替换

基本语法:

#define name stuff

特别的,为了区分普通常量这个name我们一般用大写形式
比如:

#define MAX 10000
#define REG register

#define后面的代码理论上讲只能写一行,但是如果后面的代码过长,我们可以使用'\'来实现换行,相当于转义转义字符'\'转义了转义字符'\n'

#define DEBUG_PRINT printf("file:%s\tline:%d\t\date:%s\ttime:%s\n,\__FILE__,__LINE__,\__DATE__,__TIME__)

值得注意的是,行末最好不要加;,在某些场景下是没什么问题,但是在大多数情况下是有语法错误的,所以我们要养成良好的编程习惯,行末不加;


3、#define定义宏

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

#define name(parament_list) stuff

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

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

举例:输入一个数,输出它的平方数

#include <stdio.h>
#define SQUARE(x) x*xint main()
{int n = 0;scanf("%d", &n);int ret = SQUARE(n);printf("%d\n", ret);return 0;
}

在这里插入图片描述

上面的代码看似没有什么问题,但当我们想计算n+1的平方数时,就会出现问题:

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

在这里插入图片描述

这是为什么呢?

原因就是带参数的宏在替换的时候括号内的表达式是不做任何计算的

也就是说,上面替换后的形式是:5 + 1 * 5 + 1,为了解决这个问题,我们可以在定义宏的时候给x加上括号:

#define SQUARE(x) (x)*(x)

这样替换后的结果就变成了:(5 + 1)*(5 + 1),但是这样给单独的参数加括号的形式在某些场景下还是存在问题,比如:

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

在这里插入图片描述

那为了解决这个问题,我们可以(x)+(x)整体加上括号:((x) + (x))

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

在这里插入图片描述

所以,在写宏的时候一定不要吝啬括号


4、带有副作用的宏参数

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

  • x + 1; //不带副作用
  • x++; //带有副作用

上面两个表达式的值是相同的,但是第一个表达式x的本身没有发生改变,而第二个表达式x本身发现了改变,这就是副作用

例如:使用宏实现求两个数的较大值

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

在这里插入图片描述

上面代码中宏参数在宏定义中出现了两次,我们使用MAX(a, b);时没什么问题,但当我们使用MAX(a++, b++);时问题就会出现:

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

在这里插入图片描述

可以发现a和b的值会发生改变,就是表达式求值的时候出现了永久性效果。

与函数对比:

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

请添加图片描述

从上面的代码中可以看出来,带参数的宏替换和函数传参是非常相似的,但是它们的传参是有本质区别的。
带参数的宏替换是直接将参数做整体替换,替换过后的表达式是:((a++)>(b++)?(a++):(b++));而函数参过后的表达式是:(a > b ? a : b)


5、宏替换的规则

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

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

例如:

#include <stdio.h>
#define M 10
#define N M + 2
#define MAX(x, y) ((x)>(y)?(x):(y))int main()
{int ret = MAX(M, N);return 0;
}

MAX(M, N)首先被替换成:((10)>(M + 2)?(10):(M + 2))
然后((10)>(M + 2)?(10):(M + 2))再被替换成:((10)>(10 + 2)?(10):(10 + 2))

注意:

  • 宏参数和#define定义中可以出现其他#define定义的符号,但宏不能实现递归

比如:#define N M + 2这个是可以的,但#define N N + 2是不行的。

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

比如:

#include <stdio.h>
#define M 10
#define N M + 2
#define MAX(x, y) ((x)>(y)?(x):(y))int main()
{printf("MAX(M, N)");return 0;
}

请添加图片描述

可以看到宏MAX(M, N)并没有展开。


6、宏和函数的对比

宏通常被应用于执行简单的运算。

比如在两个数中找较大数,用宏实现更有优势:

#define MAX(x, y) ((x)>(y)?(x):(y))

那为什么不用函数呢?原因有二:

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

但是和函数相比宏还是有劣势的:

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

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

#include <stdio.h>
#define MALLOC(n, type) (type*)malloc(n * sizeof(type))int main()
{//int* p = (int*)malloc(10 * sizeof(int));int* p = MALLOC(10, int);//int *p = (int*)malloc(10 * sizeof(int));return 0;
}

宏和函数的对比:

属性#define定义宏函数
代码长度每次使用时,宏代码都会被插入到程序中,除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方,每次使用这个函数时,都调用那个地方的用一份代码
执行速度更快存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的结果,所以建议宏在书写的时候多写括号函数参数只在函数调用的时候求值一次,它的结果值传递给函数,表达式的求值结果更容易预测
带有副作用的参数参数可能被替换到宏体中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预测的结果函数参数只在传参的时候求值一次,结果更容易控制
参数类型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用任何参数类型函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的
调试宏是不方便调试的函数是可以逐语句调试的
递归宏是不能递归的函数是可以递归的

7、# 和

7.1 #运算符

#运算符将宏的一个参数转换为字符串字面量,它仅允许出现在带参数的宏的替换列表中
#运算符所执行的操作可以理解为“字符串化
比如:当我们有一个变量int a = 10;的时候,我们想打印出:the value of a is 10.
下面是常规写法:

#include <stdio.h>int main()
{int a = 10;printf("the value of a is %d\n", a);return 0;
}

如果我们想把打印的这条代码通过宏替换来实现,该怎么做呢?

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

如果写成上面这种代码很明显并没有解决问题,因为如果我们将n写成%d时并不能打印出a,而只能打印出a的值,那为了能打印出a本身的字面量,我们就可以使用#操作符
如下:

#include <stdio.h>
#define PRINT(format, n) printf("the value of "#n" is "format"\n", n)int main()
{int a = 10;PRINT("%d", a);//printf("the value of "a" is ""%d""\n", a);double b = 3.14;PRINT("%lf", b);//printf("the value of "b" is ""%lf""\n", b);return 0;
}

请添加图片描述

所以我们说:#运算符所执行的操作可以理解为“字符串化”,上面的代码中是将a和b字符串化了。

当n = a的时候,#n 就相当于“a”


7.2 ##运算符

##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。##被称为记号粘合
这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。

比如现在有这么一个问题:当我们写一个函数来求两个数的较大值的时候,不同的类型我们就需要写不同的函数,这样写太繁琐了,我们可以使用宏来简化这件事:

#include <stdio.h>
#define GENERIC(type) \
type type##_max(type x, type y)\
{\return ((x) > (y) ? (x) : (y));\
}GENERIC(int)
//int int_max(int x, int y)
//{
//	return ((x) > (y) ? (x) : (y));
//}GENERIC(double)
//double double_max(double x, double y)
//{
//	return ((x) > (y) ? (x) : (y));
//}int main()
{printf("%d\n", int_max(10, 20));printf("%lf\n", double_max(3.14, 6.28));return 0;
}

请添加图片描述

上面的代码中我们利用宏替换来实现创建不同类型的函数,type##_max中的##操作符将type_max连接成了一个新的标识符


8、命名的约定

一般来讲函数和宏的使用语法很相似,所以语言本身没法帮我们区分二者,那我们平时的习惯是:

  • 把宏名全部大写
  • 函数名不要全部大写或不大写

9、#undef

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

#include <stdio.h>
#define M 10int main()
{printf("%d\n", M);
#undef Mprintf("%d\n", M);return 0;
}

请添加图片描述

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


10、命令行定义

许多C编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程。
例如:当我们根据同一个源文件想要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个一定长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个较大的数组)

编译指令:

//linux  环境演示
gcc -D ARRAY_SIZE=10 programe.c

11、条件编译

满足条件,就参与编译;不满足条件,就不参与编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的,因为我们有条件编译指令:

1.
#if   常量表达式   //常量表达式由预处理器求值
//...
#endif如:
#define _DEBUG_ 1
int main()
{
#if _DEBUG_printf("a");
#endifreturn 0;
}
2.多个分支的条件编译
#if   常量表达式
//...
#elif   常量表达式
//...
#else
//...
#endif如:
#define M 1
int main()
{
#if M == 1printf("a");
#elif M == 2printf("b"):
#elseprintf("C");
#endifreturn 0;
}
3.判断是否被定义
//如果定义了
#if defined(symbol)
#ifdef symbol如:
#define M 2
int main()
{
#ifdef Mprintf("a");
#endifreturn 0;
}//如果没定义
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#ifdef OS_UNIX#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2():#endif
#endif

条件编译通常用于跨平台性代码的编译


12、头文件的包含

12.1 头文件被包含的方式

12.1.1 本地文件包含

一般指自己创建的头文件

#include "filename.h"

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


12.1.2 库文件包含

一般指标准库中头文件的包含

#include <filename.h>

查找策略:
直接去标准路径下去查找,如果找不到就提示编译错误。
那这样是不是就说明,对库文件也可以使用" "的形式包含呢?
答案是可以的。但是这样查找的效率比较低,也不容易区分是库文件还是本地文件


12.2 嵌套文件的包含

我们已经知道,#include指令可以使另外一个文件被编译,就像它实际出现于#include指令的地方一样。
这种替换的方式很简单:预编译器先删除这条指令,并用包含文件的内容替换
一个头文件被包含几次,就会被实际编译几次,如果重复包含,对编译的压力就比较大

#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"int main()
{return 0;
}

如果像上面这样写,test.h文件的内容就会被拷贝5份
如果test.h文件比较大,这样预处理后代码量会剧增,如果工程比较大,有公共使用的文件,被大家都能用,又不做任何的处理,那么后果会不堪设想。
为了解决头文件被重复引入的问题,就要用到条件编译
我们在每个头文件的开头这样写:

#ifndef __FILENAME_H__
#define __FILENAME_H__//...#endif

或者

#pragma once

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


总结

  • 增强对C语言编译过程的整体理解:预编译是C语言编译过程的第一阶段,在预编译阶段可以对源代码进行预处理,如宏定义、头文件包含等。通过学习预编译过程,可以更全面地理解C语言代码的编译过程。
  • 优化代码结构:预编译指令能够简化代码结构、提高代码的重用性和可维护性。学习预编译过程可以帮助程序员更好地利用预编译指令优化代码结构,提高代码的质量。
  • 理解条件编译和跨平台编译:条件编译是预编译指令中的重要功能,可以根据不同条件编译不同的代码。通过学习预编译过程,可以了解如何使用条件编译来实现跨平台编译,提高代码的可移植性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/26455.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

什么是DMZ?路由器上如何使用DMZ?

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 DMZ 📒🚀 DMZ的应用场景💡 路由器设置DMZ🎈 注意事项 🎈⚓️ 相关链接 ⚓️📖 介绍 📖 在网络管理中,DMZ(Demilitarized Zone,隔离区)是一个特殊的网络区域,常用于将公共访问和内部网络隔离开来。DMZ功能允许…

遇到JSON文件就头大?掌握Python这几种方法,让你轻松应对

目录 1、标准库json模块 📄 1.1 json.load()函数介绍 1.2 json.loads()处理字符串 1.3 使用json.dump()写入JSON 1.4 json.dumps()美化输出 1.4 错误处理与编码问题 1.5 高效读取大文件技巧 2、第三方库simplejson加持 🔧 2.1 安装与导入simplejson 2.2 性能优势与…

解决css文本内容为符号不会换行问题

错误样式如上&#xff0c;超出了规定的文本区域。 在css上增加word-wrap: break-word;即可。

电商价格监测对于品牌渠道管控的重要性

当品牌开启经销渠道或涉足电商渠道时&#xff0c;必须着手进行线上线下价格监测。只有监控到电商价格&#xff0c;才能明晰出货后的商品历经多轮市场演绎后的实际价格&#xff0c;进而了解市场需求下的真实低价行为。借助力维网络开发的电商价格监测系统&#xff0c;品牌商能知…

uni-app利用renderjs实现安卓App上jssip+freeswitch+webrtc音视频通话功能

效果图 前置知识 利用renderjs在app端加载for web库 JsSIPFreeSwitchVue实现WebRtc音视频通话 原始模块 <template><viewclass"test-sip":userExtension"userExtension":change:userExtension"JsSIP.handleUserExtenSionChange":tar…

Python邮箱发送如何设置?Python发信方法?

Python邮箱发送邮件需要哪些库&#xff1f;怎么使用Python发信&#xff1f; Python的强大之处在于其丰富的库和模块&#xff0c;使得开发者可以轻松地实现各种功能&#xff0c;包括通过电子邮件发送信息。AokSend将介绍如何在Python中设置和发送电子邮件&#xff0c;以及相关的…

超高频载码体有哪些特点?

载码体由线圈、已编程的芯片&#xff0c;以及电池(在有源读写系统中)组成&#xff0c;具有唯一的电子编码&#xff0c;拥有大容量的存储空间&#xff0c;通常附着于产品载体乃至是产品本身&#xff0c;成为一个随产品移动的移动数据库&#xff0c;可以帮助企业更好的物料、成品…

AH8652:220V转5V非隔离电源芯片

### AH8652&#xff1a;220V转5V非隔离电源芯片&#xff0c;高效转换新选择 #### 引言 随着电子设备对电源稳定性和安全性要求的提高&#xff0c;非隔离电源转换芯片因其简单、高效和成本效益而受到市场的欢迎。AH8652是一款专为220V转5V设计的非隔离电源芯片&#xff0c;以其…

Ubuntu安装Protobuf

以前的版本中&#xff0c;有./configure&#xff0c;所以参照下面的博客链接 Ubuntu安装Protobuf&#xff0c;指定版本_ubuntu更新protobuf-CSDN博客 后来的版本中&#xff0c;没有了./configure文件&#xff0c;需要安装bazel,参照下面的官网链接 protobuf/src/README.md a…

基于 Redis 实现分布式缓存

一、单节点 Redis 的问题 1.1 存在的问题 1、数据丢失问题&#xff1a;Redis 是内存存储&#xff0c;服务重启可能会丢失数据。 2、并发能力问题&#xff1a;单节点 Redis 并发能力虽然不错&#xff0c;但也无法满足如 618 这样的高并发场景。 3、故障恢复问题&#xff1a;如果…

实践分享:鸿蒙跨平台开发实例

先来理解什么是跨平台 提到跨平台&#xff0c;要先理解什么是“平台”&#xff0c;这里的平台&#xff0c;就是指应用程序的运行环境&#xff0c;例如操作系统&#xff0c;或者是Web浏览器&#xff0c;具体的像HarmonyOS、Android、iOS、或者浏览器&#xff0c;都可以叫做平台…

用于云医疗图像的缩略图保持加密方案

论文标题&#xff1a;《Data hiding with thumbnail-preserving encryption for cloud medical images》&#xff0c;作者提出了一种用于云医疗图像的可逆数据隐藏方案&#xff0c;同时保留了缩略图。下面是论文的创新点和算法过程的总结。 一、缩略图保持加密与传统图像加密 …

GD32 MCU超频后无法再次下载程序的解决办法

我们知道&#xff0c;MCU的系统时钟主频就相当于人的心跳或脉搏&#xff0c;为所有的工作单元提供时间基数&#xff0c;所以一般在程序最开始的地方都需要进行主频配置。 GD32固件库中提供了多种宏定义&#xff0c;可以很方便的将系统时钟配置为想要的频率。 GD32固件库中所用…

AI宣传文案软件有哪些?5款AI软件推荐

AI宣传文案软件有哪些&#xff1f;AI宣传文案软件在现代营销和创意产业中扮演着越来越重要的角色&#xff0c;它们凭借先进的自然语言处理、机器学习和深度学习技术&#xff0c;不仅解放了创作者的双手&#xff0c;还大大提升了文案的生成效率和质量。这些软件能够精准捕捉用户…

python快速入门之Flask框架

文章目录 一、pip安装二、接口开发三、测试 一、pip安装 pip install flask 二、接口开发 from flask import Flaskapp Flask(__name__)app.route("/test") def index():return "test"if __name__ __main__:app.run()三、测试 http://127.0.0.1:5000…

redis 08 慢查询日志

1.什么是慢查询日志 2.慢查询和两个参数有关 2.1 2.2 3.例子&#xff1a; 4 参数详细介绍&#xff1a;

ICRA 2024:基于视觉触觉传感器的物体表⾯分类的Sim2Real双层适应⽅法

⼈们通常通过视觉来感知物体表⾯的性质&#xff0c;但有时需要通过触觉信息来补充或替代视觉信息。在机器⼈感知物体属性⽅⾯&#xff0c;基于视觉的触觉传感器是⽬前的最新技术&#xff0c;因为它们可以产⽣与表⾯接触的⾼分辨率 RGB 触觉图像。然⽽&#xff0c;这些图像需要⼤…

如何通过亚马逊测评提升产品竞争力的关键策略

在亚马逊这个全球领先的跨境电商平台上&#xff0c;随着卖家数量的激增&#xff0c;产品间的竞争愈发激烈&#xff0c;为了在市场中脱颖而出&#xff0c;提高产品的竞争力成为了每位卖家必须面对的问题&#xff0c;而在这其中&#xff0c;亚马逊测评作为一种有效的市场策略&…

STM32项目分享:车牌号识别系统

目录 一、前言 二、项目简介 1.功能详解 2.主要器件 三、原理图设计 四、PCB硬件设计 1.PCB图 2.PCB板打样焊接图 五、程序设计 六、实验效果 七、资料内容 项目分享 一、前言 项目成品图片&#xff1a; 哔哩哔哩视频链接&#xff1a; https://www.bilibili.…

python数据分析-房价数据集聚类分析

一、研究背景和意义 随着房地产市场的快速发展&#xff0c;房价数据成为了人们关注的焦点。了解房价的分布特征、影响因素以及不同区域之间的差异对于购房者、房地产开发商、政府部门等都具有重要的意义。通过对房价数据的聚类分析&#xff0c;可以深入了解房价的内在结构和规…