我是Pinkstone 发表于 2024-1-25 17:46:16

【停更】C++头文件开发:从入门到入土

本帖最后由 lihl 于 2024-2-23 11:47 编辑


Hey,读者!
你是否为强大的PGP加密叹为观止?
你是否用着一串一串易语言代码编写而成的工具?
有千千万万的工具或多或少地使用了C(pp)语言的头文件进行开发!
本文就以简单的几个样例,让读者学会C++头文件的开发原理。
edit:坚持不下去了,不限期停更。


本活动参加 苦力怕论坛资源 X 教程寒假活动 ,并将文本部分授权给该活动。

特别提示:

[*]若无特殊说明,本文代码使用C++14标准。
[*]为何不使用C++20?C++14有auto关键字,C++17有namespace嵌套,C++20是什么垃圾。
[*]本文仅支持原版C++开发,如果想进阶请尝试boost库。
[*]本文仅代码部分开源,文本部分遵循CC BY-NC-SA 4.0协议进行授权。
[*]本文必须有C++学习基础(至少能理解链表,学习了STL)或者精通C。
[*]由于时间仓促,作者水平有限,无法保证十全十美,敬请勘误或联系作者进行修正。
[*]本文带*的篇目为选读,虽是正常篇目但难度极高。请挑选加粗字体着重记录。
语句速查索引:

语句作用 章节
#ifdef如果某变量被定义,执行下方语句 1
#ifndef如果某变量未被定义,执行下方语句 1
#endif结束#ifdef或#ifndef语句的声明1
#define定义预处理宏2.1.1
#undef取消预处理宏2.1.1
const修饰某变量或函数为常量2.1.2*
constexpr(C++11及以上特性)修饰某变量或函数为常量表达式2.1.2*
static修饰某变量或函数为静态2.1.2* (未详细涉及)
#if布尔判断,如果为true则执行下一步内容3.1
#else必须和#if配套使用,如前列if均未执行则执行else内内容3.1
#elif必须和#if配套使用,效果同#if3.1


喜报:
「每周好帖速递」第六辑 首次收录编程开发版即收录本文,可喜可贺!

更新日志:
2024/2/23 无限期停更
2024/2/4 完成第5页,对第6页进行更名并完成,更改标题“如土”
2024/2/3 完成第1~4页,修正define和undef的错误

恭喜!从这里开始,您即将就会学会自己动手编写一个头文件了!
我们可以从以下这个样例开始您的开发:
#ifndef firsthead
    #define firsthead
#endif

好耶!你已经编写了一个名为“firsthead”的头文件。在学习之前,我们首先要下载一个C++编译器。
作者推荐使用Dev-C++进行编译。
当然,我们还是需要首先了解一下什么是头文件。
头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。

在程序中要使用头文件,需要使用 C 预处理指令 #include 来引用它。前面我们已经看过 stdio.h 头文件,它是编译器自带的头文件。

引用头文件相当于复制头文件的内容,但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。

A simple practice in C 或 C++ 程序中,建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。
*引用自菜鸟教程https://www.runoob.com/cprogramming/c-header-files.html

用人话来说,就是后缀为.h的文件,可以被你使用的C或C++代码引用。
还不能理解?看看你写的代码吧!

#include<bits/stdc++.h> //<--- 这就是头文件引用,引用的是头文件 stdc++.h,位于bits目录
using namespace std;
int main(){
    return 0;
}这段代码里,被引用的是bits目录里的编译器定义的stdc++.h头。
是的,这节我们就只讲一个特殊用法,那就是#include<>和#include""的区别。
简单来说就是:<>用于编译器定义,""用于自定义。我们的样例引用时,都需要用""。
不要说你不想用"",如果不想,可以放到编译器目录下面。
我没有尝试过,后果自负。


你已经认识了什么是.h类型的文件,那就让我们正式地步入学习编写头文件的教程吧。
这节我们学习的是简单运用。
首先认识下#ifdef和#ifndef吧。
当我们定义一个(#define,参见2.1)布尔变量 flag 时,特性为:

#ifdef flag--> 可以执行下一步内容
#ifndef flag--> 不可执行下一步内容,因为flag被定义
可以简单地看到,#ifdef只能判断变量是否被定义,不能判断值。如果被定义,便执行内部内容。#ifndef则相反,如果没有被定义才可执行内部内容。
它们执行的,是关于if关键字的内容。
从以下一个示例,可以获得更详细的内容。读下面的代码,回答k的值。
#define k 32
#ifndef k
      #define k 233
#endif
当执行以上内容时,代码不会报错,因为 k 被定义后判断 k 是否定义返回 false,因此什么都不会执行,k=32.
让我们略加改动,将源代码的第二行改为#ifdef k 时...
哦吼,报错了。显示“ "k" redefined”(即 k 被重复定义)
所以,让我们删去第一行。。。
#ifdef k
      #define k 233
#endif欸,又可以运行了。


等等,观察上面的一些代码,发现了什么?——#endif!
endif的作用,是结束以上的ifdef或者ifndef。
就像你学习过的 if 关键字一样,if(n)def就相当于" if{ "这部分,需要endif来充当一个" } " 以形成一个闭环。
范例请见上文。

这节我们开始学习宏(常量)的运用。

在学习之前,我们再次重申一下头文件的作用,即——

头文件仅用于声明函数和定义宏!

诶嘿,了解就好。为什么要这样呢?因为我们可以编写一套程序:

/*
Filename:test.h
*/
#include<bits/stdc++.h>
int ItIsAVeryImportantVariableWhichNameIs_a=233;


#include<bits/stdc++.h>
#include"test.h"
using namespace std;
/*
一长串未知用途的代码...
*/
#include"test.h"

int main(){
      return 0;
}哦吼,又报错了。这次显示 redefinition of “ 这里省略变量名 ”(意思是重复定义)。

因此,我们需要上一节需要的#ifndef进行判断定义进行头文件保护。这时,我们就用到了——#define!
define的作用是定义一个预处理宏。这里,我们就可以嵌套一层#ifndef了。
用法很简单: #define 宏名称

所以,我们可以对头文件稍加修改,养成一个写头文件即写以下结构的好习惯:
/*
先写代码协议和声明!
*/
#ifndef ________
    #define ________
    /*
    再写你的代码!
    */
#endif欸,等等。
观察一个关系:
#ifndef和#ifdef有#endif收尾。
那#define有什么收尾呢?
认识一下#undef吧。
有的时候,写代码的我们有可能都很懒,想重复使用我们之前已经定义好的宏(例如最大数据范围都使用MAX,最小数据范围都使用MIN等),所以我们需要undef掉我们定义的宏,一方面可以使代码更具有可读性,另一方面是我们真的就不想用了\__(ツ)__/
用法也很简单:#undef 宏名称
例如下面这个随机数生成器:
// Filename:limit.h
#ifdef _max
      #define limitmax 11
#endif

#ifdef _min
      #define limitmin 0
#endif

#ifdef _num
      #define limitnum 10
#endif#include <bits/stdc++.h>
using namespace std;
int main() {
def:
    std::default_random_engine random;
    int range_min,range_max,range_num;
   
i_min:
      printf("左闭右闭区间,range_min: ");
    scanf("%d",&range_min);
    #define _min
    #include"limit.h"
    #undef _min
    if(range_min<limitmin){
            printf("太小了!小于设定区间%d。\n",limitmin);
                goto i_min;
      }
i_max:         
      
    printf("左闭右闭区间,range_max: ");
    scanf("%d",&range_max);
    #define _max
    #include"limit.h"
    #undef _max
    if(range_max>limitmax){
            printf("太大了!大于设定区间%d。\n",limitmax);
                goto i_max;
      }
setrandom:
    std::uniform_int_distribution<int> range(range_min,range_max);
    random.seed(time(0));
i_range:
         printf("生成个数:");
      scanf("%d",&range_num);
      #define _num
    #include"limit.h"
    #undef _num
    if(range_num>limitnum){
            printf("太多了!多于设定区间%d。\n",limitnum);
                goto i_range;
      }
o:
    for (int i=1; i<=range_num; i++) {
      cout << range(random) << endl;
    }
    return 0;
}在limit.h中,我们定义了多个极限,并在文件二的示例中得到了使用。undef的作用,在这里是使代码更美观。
这样,我们就了解并学会了undef的作用。

这一扩展节,我们主要探讨宏在常量方面与const(expr)的区别和作用,以及在大型竞赛中使用的技巧。
上一节中,我们已经学习过了关于define在宏方面的作用。这节我们就来探讨define在常量方面和const(expr)碰撞出的火花。
如果你是OIer,你肯定学过一句话,就是:定义常量用const而并非define,用define会在编译方面出现奇奇怪怪的问题。
我的理解是,千万不要在平时的C++程序(特别是在大型竞赛中)中把define当常量,一定要使用const(expr),因为你不是开发者,你并不知道你这么定义一个宏会发生什么奇怪的错误。
如果你在头文件里头使用常量,我的建议是把const(expr)当作修饰函数或者变量的修饰符而并非常量,你可以把const理解为只读不可修改的变量。define才是真真正正的常量。
以上内容均个人理解。下面我们将要学习什么是常量,什么是常量表达式,以及在定义常量时我应该选择什么。
首先我们要理解什么是常量。常量与变量相对而言,变量是随时可变的一类未知数,如x;而常量不可变,是人为规定的一类已知数。当然,在数学中,大部分常量都是无理数。在编程中也一样,我们可以定义一个int x变量,也可以定义一个const int y。这里的y,就被我们声明了是一个常量。
再说常量表达式。一是变量方面:在C++11标准加入了constexpr。const(编译期常量 或者 运行期常量)含括了constexpr(编译期常量)。我推荐使用constexpr声明变量的原因是,当你声明一个constexpr变量时,他会自动变为const。
可以用下表来解释const和constexpr的关系:


级别/例子静态常量非静态常量普通
0-声明(static) constexpr//
1-类型(const) 类型(const) 类型/
2-名称 名称 [ 如果是函数,嵌套定义(_____)]名称 [ 如果是函数,嵌套定义(_____)]名称 [ 如果是函数,嵌套定义(_____)]


我们可以把constexpr理解为声明层的常量声明,而把const理解为类型层的常量声明。当声明了constexpr可以选择声明const。
因此,变量的命名顺序并不是const constexpr xxx而是constexpr const xxx。


听上面是不是非常复杂?其实是这样的:

很久以前...
1:我们要开发一个常量类型,定义了就不能改了,叫什么名字呢?
2:const(ant)

2011年:
1:我们又开发了一个常量类型,在编译器里他就是一个常量了,它又叫什么名字?
2:嘶... const(ant) expr(ession)

前面我们已经说到了常量表达式。那么,constexpr的命名就是一个望文知义。expression是表达式,这不就是常量表达式吗?
正确的。那么,什么是常量表达式呢?
曾经,我在学编程的时候,恰好有一个问题,就是:
如果我有一个数据范围为1e13:
为什么我必须定义一个const long long MAX = 1e13 再赋值long long a,
而不是直接就赋值long long a呢?
编译器给我一个不能赋值浮点数打消了我的顾虑。
但我没有注意到的问题时,曾几何时,第一种方式竟然也不能赋值。
原因是,C++中的数组赋值必须为一个常量表达式。现在的编译器,优化了这个问题。我们可以通过constexpr声明一个常量表达式,这样就可以通过 包括constexpr声明的函数 的表达式 声明一个数组了。
为什么要使用C++14标准?因为constexpr声明过的函数,在C++11可以递归,在C++14可以像正常的函数一样用局部参数、用逻辑分支。*
因此,在C++11中推出的constexpr,既解决了const只读和常量的语义问题,又解决了多年来赋值常量表达式张冠李戴的问题,是近几年来比较优秀的C++版本。
再次鞭尸C++20,C++20是什么垃圾。
话又又说回来,什么是常量表达式?常量表达式就是能在编译时求值的表达式。C++的类型分为两种,一种是变量(字面值),一种是常量表达式。**
你现在应该养成把const理解为read only的习惯,把constexpr理解为真正的const的习惯了。
C++11中,constexpr函数默认等于了inline constexpr,并且constexpr声明的函数必须有且只有一条return语句。(Note:C++14改为正常函数形式,可见上文*)
并且由于是隐式的内联函数,在引用*中constexpr甚至跑出了比const快5.1044倍的运行速度。
C(pp)中还有指针的特性。在样例**中的特性介绍中,我们可以看到以下样例代码:
const int *p1=nullptr;                //p1是指向整形常量的指针
constexpr int *p2=nullptr;      //p2是指向整形的常量指针
(等效于 int *const p2=nullptr;)
这样,我们可以再次理解:
const是类型,constexpr是声明。


可以理解吗?那就让我们进入下一节的学习吧。
*这里是知乎的这个回答
**这里是CSDN的关于常量表达式的一篇文章


这节我们主要针对 某特定编译器(IDE)和某特定系统的某些特性做浅显讨论,并且基本了解其宏。
看起来很难是不是?你可以参考预处理编译器宏Wiki的表格以了解系统宏。
什么?你问我为什么没有表格?咳咳,那是因为我的表格不全...
Stackoverflow上也有一个实例:
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
   // 32位或64位windows
   #ifdef _WIN64
      //只是64位Windows
   #else
      //32位windows
   #endif
#elif __APPLE__
    #include <TargetConditionals.h>
    #if TARGET_IPHONE_SIMULATOR
         // iOS, tvOS, or watchOS Simulator
    #elif TARGET_OS_MACCATALYST
         // Mac's Catalyst (ports iOS API into Mac, like UIKit).
    #elif TARGET_OS_IPHONE
      // iOS, tvOS, or watchOS device
    #elif TARGET_OS_MAC
      // Other kinds of Apple platforms
    #else
    #   error "未知苹果系统"
    #endif
#elif __ANDROID__
    // __linux__虽然也可以在android上使用
    // 但不能唯一标示出Android.
#elif __linux__
    // linux
#elif __unix__
    // Unix
#elif defined(_POSIX_VERSION)
    // POSIX
#else
#   error "未知编译器"
#endif这部分内容我们就告一段落,接下来我们来了解(仅C++)namespace和namespace嵌套。

namespace大家可能比较熟悉,最常见的还是std命名空间的引用:<using namespace std; >.
什么?你不知道std?那就回去看看 一、文件格式 中的第二行吧。
什么?你还不知道?那请你读一读开头的特别提示。





florence2357 发表于 2024-1-29 09:12:00

好棒的教程

我是Pinkstone 发表于 2024-1-29 09:56:10

florence2357 发表于 2024-1-29 09:12
好棒的教程

其实不然,最近没太多时间更新
页: [1]
查看完整版本: 【停更】C++头文件开发:从入门到入土