C语言最终讲:预处理详解
C语言最终讲:预处理详解
- 1.预定义符号
- 2.#define定义常量
- 3.#define定义宏
- 4.带有副作用的宏参数
- 5.宏替换的规则
- 6.宏和函数的对比
- 6.1宏的优势
- 6.1.1\符号
- 6.2宏的劣势
- 7.#和##
- 7.1#运算符
- 7.2##运算符
- 8.命名约定
- 9.#undef
- 10.命令行定义
- 11.条件编译
- 12.头文件的包含
- 12.1本地文件包含
- 12.2库文件包含
- 12.3嵌套文件包含
- 13.其他预处理指令
结语:这一讲是C语言基础知识的最后一讲了,后续将会学习数据结构相关的知识,坚持不易,希望各位都能坚持在自己所干的事情上,我们共勉
这一讲讲的是预处理相关的内容,上一讲虽然我们已经了解了一些关于#define相关的知识,这一讲我们讲详细阐述它的作用和缺陷
1.预定义符号
C语言中有着一些预定义符号,可以直接使用,是在预处理阶段进行处理的:
__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
它们的使用方法为:
int main()
{printf("%s\n", __FILE__);//打印当前文件所在的位置printf("%d\n", __LINE__);//打印行号printf("%s\n", __DATE__);//打印日期printf("%s\n", __TIME__);//打印时间//printf("%d\n", __STDC__);//err,表明VS并不完全遵循ANSI C标准return 0;
}
运行结果如下:
2.#define定义常量
我们知道,define定义的常量是直接替换的,所以有着下面的几处用法:
#define TEST 20 //方法1,直接定义常量
#define I int//方法2:可以为关键字替换成一个更简便的名字
#define CASE break;case//方法3:这是一种很“奇葩”的用法,建议还是别用了int main()
{//使用起来也很方便,方法1:int a = TEST;//直接使用即可//方法2:I b = 20;printf("%d\n", b);//方法3:int input = 1;scanf("%d", &input);switch (input){case 1:CASE 2 ://这样后面就不用加break了CASE 3 :break;}return 0;
}
对于加;问题,解答如下:
#define ROW 20;//假设#define定义时加上了;int main()
{int a = ROW;//这是a就会被替换成int a = 20;;这是后面就会有两个;,特殊情况下会出现错误//总结:加上;可能会出现错误,不加;肯定不会出现错误,所以还是不要加上;return 0;
}
3.#define定义宏
定义方式:
#define name( parament-list ) stuff
name为宏名称
parament-list是由逗号隔开的符号表,可能出现在stuff中
stuff可以是一个计算方式,也可以是一个指令等等
//需要注意的是,符号表的左括号必须要和name紧密相连,否则括号里的内容也会被当成是stuff中的一部分
使用实例:
#define MUL(x) x*x//定义了一个宏,实现x的平方int main()
{int a = 5;int b = MUL(a);printf("b = %d\n", b);//结果为25return 0;
}
看起来使用起来很好用,但是我们也要注意符号的优先级问题,如下:
#define ADD(x) x+xint main()
{int a = 5;int b = ADD(a) * ADD(a);printf("%d\n", b);//我们可能会误以为结果为10 * 10,这不就是100吗,但是并非如此//b会被替换成int b = 5+ 5*5 +5,根据符号优先级问题,结果为35return 0;
}
知道错误就要改正,所以改正方法如下:
#define ADD(x) ((x)+(x)) //在x上加一个括号,因为x为表达式,如果传入5+5,也要被括起来//总体上再加上一个括号,是为了将它们算成一个整体int main()
{int a = 5;int b = ADD(a) * ADD(a);printf("%d\n", b);//这样才能计算出一个整体的值,此时结果为100return 0;
}
所以,对于宏的定义,我们不能吝惜括号
4.带有副作用的宏参数
什么是带有副作用呢,其实就是表达式求值会改变原来的参数的值,如下:
x + 1;//不带副作⽤
x++;//带有副作⽤
为了验证此写法的危害性,我们举例来说明:
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{int x = 5;int y = 8;int z = MAX(x++, y++);//这里会被替换成((x++)>(y++) ? (x++):(y++))//先看问号左边,后置++,先使用后++,5>8为假,所以看的是y++的值,还是先使用后++,但是要注意的是//由于++,此时y的值已经变成了9,所以z被赋值为9,y再++,值位10//而x只++了一次,因为第二次只对y进行了++,所以x的值为6printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?//所以输出的结果应该为6 10 9return 0;
}
5.宏替换的规则
1.在调用宏时,首先看参数中是否包含由#define定义的符号,如果有,它们首先被替换:
#define R 2
#define RR R+2
//其中RR中包含了一个常量的定义R,所以R先被替换成2,再计算RR
2.替换文本随后会被插入到原来文本所在的位置
3.最后还会对文件进行扫描,看是否仍然存在#define定义的符号,如果有,继续进行上面的两个步骤
对于宏,需要注意的是,它并不能出现递归
6.宏和函数的对比
上面我们了解了宏,那么既然宏这么好用,那我们是不是就能够将宏代替为函数使用呢,肯定是不能的,下面我们来分析宏相较于函数的利与弊
6.1宏的优势
1.对于简单的任务,宏有着天然的优势:
#define MAX(a, b) ((a)>(b)?(a):(b))
1.首先,宏的参数是类型无关的,也就是说,宏参数能够接受任意类型的值,在特定条件下是很好用的
2.其次,宏在使用过程中也是一个简单的替换,函数不同,函数在使用前后会产生空间的创建和销毁,使用效率较慢
2.相较于函数,宏有着函数不能够实现的情况:
//如果我们要使用函数进行空间的开辟:
int* Mal(int a, int size)
{//我们首先会将返回值和参数确定,也就是说,它们的类型都是固定的,用于整形开辟的函数就不能用于浮点型类型的开辟return (int*)malloc(a * sizeof(int));
}//但是当我们使用宏时:
#define MAL(num, type) \(type*)malloc(num * sizeof(type))
int main()
{//使用函数开辟空间int* pa = Mal(10, sizeof(int));//假设我们要开辟10个整形的空间//使用宏开辟空间int* pb = MAL(10, int);//1.我们可以将int直接作为参数传入//2.此时宏的使用包含的情况更多,更好用return 0;
}
6.1.1\符号
上面我们使用了一个\符号,我们看看这是怎么个事:
#define MAL(num, type) \(type*)malloc(num * sizeof(type))
//这个宏定义中使用了\符号,它是一个换行符,当宏里的内容过多时,就可以在后边加上一个\符号进行换行
//但是要注意的是,\后边什么也不能有,包括空格
//可以理解为:一个\抵消了一个\n,所以后边不能跟任何东西
6.2宏的劣势
1.每次使用宏时,一份宏代码就会被插入到程序中,如果宏很长的话,会大幅增加程序的长度
2.宏在预处理阶段就被替换,不能够被调试
3.宏由于类型无关,所以也容易出现问题
4.宏可能造成运算符优先级的问题
对于宏和函数的一个对比,我们可以看图:
图不是很好,大家对付着看
7.#和##
7.1#运算符
#所实现的操作被称为“字符串化”,作用是将宏的一个参数转换成字符串变量:
#define PRINT(n) printf("The value of " #n " is %d", n);int main()
{int a = 10;PRINT(a);//此时会打印出:The value of a is 10//预处理阶段,代码会被替换成:printf("the value of ""a" " is %d", a);//也就是说,#n就表示"n",对于printf,多个""会被合并成一个return 0;
}
7.2##运算符
该运算符被称为记号粘合,可以将两边的符号合并成一个符号
使用方法如下:
//当我们要求两个数的最大值时:
//使用函数完成较为繁琐,因为不同类型需要分别进行处理:
int int_max(int x, int y)
{return x > y ? x : y;
}float float_max(float x, float y)
{return x > y ? x:y;
}//我们可以使用宏来实现:
#define MAX(type) \type type##_max(type x, type y)\{ \return x > y ? x : y; \}MAX(int);//这时我们可以依靠宏来创建一个函数,函数名为int_maxint main()
{//使用方法也比较简单:int ret1 = int_max(2, 3);//我们直接使用创建好的函数即可printf("%d\n", ret1);return 0;
}
8.命名约定
在命名宏和函数时,有着一个不成文的约定:
1.把宏名全部大写
2.函数名不要全部大写
9.#undef
用于移除一个宏定义,使用方法如下:
#define A 20int main()
{int a = A;printf("%d\n", a);//20#undef Aint b = A;//errreturn 0;
}
10.命令行定义
在不同的场景下,我们定义的数组长度需求可能不同,所以许多C的编译器提供了一种命令行定义的能力,允许在命令行中,也就是使用前对一些变量进行赋值
例子:
int main()
{int array[ARRAY_SIZE];int i = 0;for (i = 0; i < ARRAY_SIZE; i++){array[i] = i;}for (i = 0; i < ARRAY_SIZE; i++){printf("%d ", array[i]);}printf("\n");return 0;
}
编译指令:
//linux 环境演⽰
gcc - D ARRAY_SIZE = 10 programe.c
这里表示给ARRAY_SIZE赋值为10,然后再执行程序
11.条件编译
对于一些为了调试而使用的代码,删除了可惜,保留了又费事,所以我们可以通过条件编译来选择性编译
常见的条件编译指令:
1.
#if 常量表达式//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__//..
#endif2.多个分⽀的条件编译
#if 常量表达式//...
#elif 常量表达式//...
#else//...
#endif3.判断是否被定义
#if defined(symbol)
#ifdef symbol#if !defined(symbol)
#ifndef symbol4.嵌套指令
#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif
总结:
1.#if和 #endif是一体的,它们两个必须同时使用
2.#if和 #elif就相当于if else
3.#if defined(symbol)表示如果定义了symbol,也可以写成:#ifdef symbol
4.#if !defined(symbol)表示如果没有定义symbol,也可以写成:#ifndef symbol
它的原理为:
//使用原理为:
//当条件为真时,保留代码
//条件为假时,删除代码
int main()
{
#if 1printf("haha\n");//条件为真,保留代码
#elif 0printf("hehe\n");//条件为假,删除代码
#endifreturn 0;
}
//所以上面的代码在预处理之后就变成了:
int main()
{printf("haha\n");//条件为真,保留代码return 0;
}
12.头文件的包含
头文件有两种包含方式:本地文件包含和库文件包含,它们的包含方式是什么呢,我们来看:
12.1本地文件包含
#include "filename"
使用“”来包含
查找策略:
1.现在源文件目录下查找,也就是这个路径:
2.如果没有找到,在库函数头文件中进行查找
3.如果找不到,提示编译错误
Linux环境下标准头文件的路径:
/usr/include
VS环境下(现在VS环境下头文件的路径应该会有差异,这里只当成一个代表):
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径
12.2库文件包含
直接去标准路径下查找,如果没有找到就提示编译错误
那么我们就要问了,是不是库文件的包含也可以使用""呢,原理上来说可以,但是缺点也很明显:
1.效率低
2.这样就不能够分辨出哪个是包含的是标准库文件,哪个是包含本地文件
12.3嵌套文件包含
当我们使用#include包含多个头文件时,会发生什么呢:
在预处理阶段,竟然会有5个头文件被写入了.c文件中,因为头文件的包含其实就是将头文件的内容写入.c文件中,情况如下:
struct Stu
{int id;char name[20];
};
struct Stu
{int id;char name[20];
};
struct Stu
{int id;char name[20];
};
struct Stu
{int id;char name[20];
};
struct Stu
{int id;char name[20];
};#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{return 0;
}
为了避免这种情况,所以我们用到了:条件编译,方法如下:
//头文件的改进如下:
//方法1:条件编译
#ifndef __TEST_H__
#define __TEST_H__//也就是说,如果没有__TEST_H__的话,定义一个__TEST_H__,这样后续就有了__TEST_H__,就不会再包含这个头文件了
struct Stu
{int id;char name[20];
};
#endif//方法2:
#pragma once
//这是一个预处理指令,用于告诉编译器该头文件之应该被包含一次,通常推荐放在头文件开头
13.其他预处理指令
#error
#pragma
#line
...
不做介绍,⾃⼰去了解
可以参考《C语言深度解剖》一书进行学习
相关文章:
C语言最终讲:预处理详解
C语言最终讲:预处理详解 1.预定义符号2.#define定义常量3.#define定义宏4.带有副作用的宏参数5.宏替换的规则6.宏和函数的对比6.1宏的优势6.1.1\符号 6.2宏的劣势 7.#和##7.1#运算符7.2##运算符 8.命名约定9.#undef10.命令行定义11.条件编译12.头文件的包含12.1本地…...
Mysql的底层实现逻辑
Mysql5.x和Mysql8性能的差异 整体性能有所提高, 在非高并发场景下,他们2这使用区别不大,性能没有明显的区别。 只有高并发时,mysql8才体现他的优势。 2. Mysql数据存储结构Innodb逻辑结构 数据选用B树结构存储数据࿰…...
Node安装配置
一、下载 Node官网下载地址:https://nodejs.org/en/ 二、安装 双击上面的msi扩展安装包开始安装,基本一路Next就行了 推荐安装目录自定义,最好不要放在C盘 检查安装是否成功 Win R 快捷键,输入 cmd 打开命令窗口输…...
Django里的ModelForm组件
ModelForm组件 自动生成HTML标签 自动读取关联数据表单验证 保留之前提交的数据 错误提示数据库进行:新建,修改 步骤如下: 创建类 # 在 views.py 文件里# 创建一个类 class AssetModelForm(forms.ModelForm):class Meta:model models.…...
深入理解C语言:main函数的奥秘
在C语言中,main函数是每个程序的入口点,起着至关重要的作用。本文将深入探讨main函数的工作原理,包括其参数、返回值、以及如何从main启动程序的执行。通过实际代码示例,读者将更深入地理解main函数在C语言编程中的核心地位。 第一…...
selenium自动化测试入门 —— Alert/Confirm/Prompt 弹出窗口处理!
一、Alert/Confirm/Prompt弹出窗口特征说明 Alert弹出窗口: 提示用户信息只有确认按钮,无法通过页面元素定位,不关闭窗口无法在页面上做其他操作。 Confirm 弹出窗口: 有确认和取消按钮,该弹出窗口无法用页面元素定…...
探索JavaScript 18:新特性全解析
JavaScript,作为全球最广泛使用的编程语言之一,每年的更新都备受开发者社区的关注。2023年,JavaScript的新版标准——ECMAScript 2023(也称为JavaScript 18)引入了多项激动人心的新特性,这些特性不仅优化了…...
Python第二语言(七、Python模块)
目录 1. 什么是模块 2. 基本语法 2.1 模块的导入方式 2.2 基本语法 import 模块名 2.3 基本语法 from 模块名 import 功能名 2.4 基本语法as 别名 3. 自定义模块 4. 调用自定义模块时,如何让其模块中的函数不被调用(__name__) 5. 调…...
介绍单例模式
描述 保证一个类只有一个实例,并且提供一个全局访问点 场景: 重量级的对象,不需要多个实例,如线程池,数据库连接池 实现 1. 懒汉模式 延迟加载的方式 只有在真正使用的时候,才开始实例化线程安全问题…...
【C++修行之道】类和对象(五)日期类的实现、const成员、取地址及const和取地址操作符重载
目录 一、 日期类的实现 Date.h 1.1 GetMonthDay函数(获取某年某月的天数) 问:这个函数为什么不和其他的函数一样放在Date.cpp文件中实现呢? 1.2 CheckDate函数(检查日期有效性)、Print函数(…...
来腾讯第4天,我已经焦虑昏了啊!
大家好,我是白露啊。 今天在看到一个实习生在抱怨,给我笑惨了。 标题是:“腾讯实习第4天,焦虑昏了”! 他写道:“怎么办啊牛爷爷们,什么都不会。业务看不懂,文档看不懂,…...
MacOS升级ruby版本
MacOS自带ruby版本是2.x,可以通过“ruby -v”查看版本号 $ ruby -v ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.x86_64-darwin22]homebrew安装的ruby版本号可以通过“brew info ruby”命令参看 $ brew info ruby > ruby: stable 3.3.2 (bottled)…...
【MySQL数据库基础】
🌈个人主页:努力学编程’ ⛅个人推荐:基于java提供的ArrayList实现的扑克牌游戏 |C贪吃蛇详解 ⚡学好数据结构,刷题刻不容缓:点击一起刷题 🌙心灵鸡汤:总有人要赢,为什么不能是我呢 …...
QT系列教程(9) 主窗口学习
简介 任何界面应用都有一个主窗口,今天我们谈谈主窗口相关知识。一个主窗口包括菜单栏,工具栏,状态栏,以及中心区域等部分。我们先从菜单栏说起 菜单栏 我们创建一个主窗口应用程序, 在ui文件里的菜单栏里有“在这里输入”的一个…...
【C++进阶】深入STL之 栈与队列:数据结构探索之旅
📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C “ 登神长阶 ” 🤡往期回顾🤡:模拟实现list与迭代器 🌹🌹期待您的关注 🌹🌹 ❀stack和queue Ǵ…...
SpringBoot发邮件服务如何配置?怎么使用?
SpringBoot发邮件需要的参数?邮件发送性能如何优化? 在SpringBoot项目中配置发邮件服务是一个常见的需求,它允许我们通过应用程序发送通知、验证邮件或其他类型的邮件。AokSend将详细介绍如何在SpringBoot中配置发邮件服务。 SpringBoot发邮…...
AutoCAD Mechanical机械版专业的计算机辅助设计软件安装包下载安装!
AutoCAD机械版作为一款专业的计算机辅助设计软件,不仅具备卓越的二维绘图功能,更是拥有令人瞩目的3D建模工具,为机械设计师们提供了前所未有的创作空间。 在AutoCAD机械版的3D建模环境中,用户可以借助一系列简洁明了的命令&#…...
json.load报错AttributeError: ‘str‘ object has no attribute ‘load‘
with open(json_file, r) as f:data json.load(f)要写个简单的数据处理脚本,报错AttributeError: ‘str’ object has no attribute ‘load’,查看了一下,路径正确,查了半天博客,不知道错在哪里。 回头一看 jsons_pa…...
单词记忆(第二周)
transplant: trans - plant 移植 perceive: per - ceive 察觉 paraphrase: para - ph - rase 释义 prospect: pro - s - pect 前景 access: ac - cess 进入,通道,访问 generous; gene - rous 慷慨的,丰富的 lecture: lec - ture 讲座 …...
RAG:如何从0到1搭建一个RAG应用
通过本文你可以了解到: 什么是RAG?如何搭建一个RAG应用?目前开源的RAG应用有哪些? 大模型学习参考: 1.大模型学习资料整理:大模型学习资料整理:如何从0到1学习大模型,搭建个人或企业…...
leetcode:67二进制求和
题目链接:67. 二进制求和 - 力扣(LeetCode) class Solution { public:string addBinary(string a, string b) {int stralen a.size(), strblen b.size();int curtc;int Maxlen max(stralen, strblen);vector<int> stra;vector<i…...
大模型日报2024-06-10
大模型日报 2024-06-10 大模型资讯 无需矩阵乘法的语言模型在亿参数规模上表现优异 摘要: 研究表明,无需矩阵乘法的语言模型在亿参数规模上仍能保持顶级性能。这一发现挑战了传统神经网络依赖矩阵乘法的观点,展示了在GPU优化之外的新可能性。 博弈论助力…...
【博士每天一篇文献-综述】Modularity in Deep Learning A Survey
阅读时间:2023-12-8 1 介绍 年份:2023 作者:孙浩哲,布朗克斯医疗卫生系统 会议: Science and Information Conference 引用量:4 论文主要探讨了深度学习中的模块化(modularity)概念…...
Sentinel不使用控制台基于注解限流,热点参数限流
目录 一、maven依赖 二、控制台 三、基于注解限流 四、热点参数限流 五、使用JMeter验证 一、maven依赖 需要注意,使用的版本需要和你的SpringBoot版本匹配!! Spring-Cloud直接添加如下依赖即可,baba已经帮你指定好版本了。…...
HTML做成一个端午节炫酷页面
做成端午节页面之前,先了解一下端午节的由来: 1.起源与历史: 端午节起源于中国,始于春秋战国时期,至今已有2000多年历史。 最初是古代百越地区(长江中下游及以南一带)崇拜龙图腾的部族举行图…...
解决Ubuntu系统/usr/lib/xorg/Xorg占用显卡内存问题原创
在Ubuntu系统中,/usr/lib/xorg/Xorg进程占用显卡内存的问题可能会影响系统性能,特别是在使用GPU进行计算任务时。以下是一些解决方法,可以帮助你减少或解决这个问题: 1. 更新显卡驱动 首先,确保你使用的是最新版本的…...
【Activiti7系列】基于Spring Security的Activiti7工作流管理系统简介及实现(附源码)(下篇)
作者:后端小肥肠 上篇:【Activiti7系列】基于Spring Security的Activiti7工作流管理系统简介及实现(上篇)_spring security activiti7-CSDN博客 目录 1.前言 2. 核心代码 2.1. 流程定义模型管理 2.1.1. 新增流程定义模型数据 …...
解密Spring Boot:深入理解条件装配与条件注解
文章目录 一、条件装配概述1.1 条件装配的基本原理1.2 条件装配的作用 二、常用注解2.1 ConditionalOnClass2.2 ConditionalOnBean2.3 ConditionalOnProperty2.4 ConditionalOnExpression2.5 ConditionalOnMissingBean 三、条件装配的实现原理四、实际案例 一、条件装配概述 1…...
【数据结构与算法】使用数组实现栈:原理、步骤与应用
💓 博客主页:倔强的石头的CSDN主页 📝Gitee主页:倔强的石头的gitee主页 ⏩ 文章专栏:《数据结构与算法》 期待您的关注 目录 一、引言 🎄栈(Stack)是什么? …...
cell的复用机制和自定义cell
cell的复用机制和自定义cell UITableView 在学习cell之前,我们需要先了解UITableView。UITableView继承于UIScrollView,拥有两个两个相关协议 UITableViewDelegate和UITableViewDataSource,前者用于显示单元格,设置行高以及对单…...
让顾客心动的句子/江苏seo网络
http://hi.baidu.com/hieda/blog/item/2c583354419edf57574e0068.html 台湾半导体加工业之父---张忠谋 张忠谋张忠谋简历: 1931年出生于浙江宁波 1952年获美国麻省理工学院机械系硕士学位 1958年进入美国德州仪器公司 1964年获斯坦福大学电机系博士学位 1972年升任德…...
做网站怎样投放广告/360优化大师最新版的功能
内部类访问特点: 1.内部类可以直接访问外部类中的成员。 2.外部类要访问内部类,必须建立内部类的对象。 一般用于类的设计: 分析事物时,发现该事物描述中还有事物,而且这个事物还在访问被描述事物的内容。 这时就是还有…...
信息最全的网站/内容营销策略有哪些
看以下例子: select * from ( select * from b left join c on xxxx left join d on xxxx left join e on xxxx) as a where a.xxxx 由于a是一个很复杂的东西,关键a是别名出来的。 那这种写法将会非常耗时。 但是如果将select * from b left join …...
如何做做网站/海外网站seo优化
依旧是如何找到递归表达形式。这个我目前也没有更好的方法,只能遇到一道题目,就解决一道,想不到就抄别人的,变成自己的,积累经验。 这道题目中提到的对齐方法,可以任意的加空格,用‘-’表示。那…...
营销型企业网站群策略/谷歌浏览器下载手机版官网中文
逻辑运算符:&与|或~非“&”和“|”操作符可比较两个标量或两个同阶矩阵.对于矩阵来说必须符合规则,如果A和B都是0-1矩阵,则A&B或A|B也都是0-1矩阵,这个0-1矩阵的元素是A和B对应元素之间逻辑…...
那个做头像的网站好/网络公司网络推广服务
两三年过去了,中间考研,毕业等事宜,一直没有再动过博客。 原本自己的文笔和表达能力就有限,中间一直不断学习新东西,更觉得没有精力去经营博客。 看到很多人能写出高质量博客深感羡慕,自己也是一枚菜鸡&…...