正如任何一个划水躺平的本科生一样,在低年级的专业课上解决完了考试,就逐渐将平时用不到的知识忘得一干二净。现在抽出空来,对这些想来还是比较基础性的知识进行回顾,准确得说是对书上的知识进行适合于本人的归纳和转述,使其内化成长时间不容易忘记的个人技能。废话讲完,进入正题。
低年级的时候谈编程,总是一个程序文件,进行基础性的语法训练。在没有接触过真正的大型项目前,一切关于程序工程的设想,都是理论上的。因此下文全部省略前提:书上说。C++程序当然要考虑一个工程。一个完整的程序在没有那么抽象的考虑下,就是一大坨程序文件,它按一定顺序执行,从main函数开始启动。问题是真的是一大坨文件,必须按照一定的规律整理划分。
往极端了想,我什么结构都不要,我就要一条条语句。每条语句对应一个或几个指令,不行吗?理论上你能写出这样的代码也行,反正计算机最终就是要一条一条语句执行。但是这样程序员没法活,只好开始升维,给他来一个降维打击。
那么结构来了。函数,封装了一些过程,具有独立性,把一坨语句打包起个名字,谁爱用谁用,程序员成功少掉几根头发。文件,一些函数放这个文件,另一些函数放那个文件。按照声明+定义+调用的顺序,一个函数定义只能有一次,使用可以在多个文件多次使用,所以声明也会有多次。把这些声明打包成一坨,做成头文件,在需要使用的地方include,程序文件初具结构。
头文件当然要按功能模块来划分,书上叫界面头文件,不太理解为什么叫这个名字,反正就是分得要有意义。要注意一个初学者可能会犯的错误:在头文件中定义具体的函数。这是背离C++程序设计的原则的,尽管可以编译运行。一般的做法是,创建一个test.h,再创建一个具体的test.cpp实现,在任何想要调用的地方a.cpp、b.cpp中#include “test.h”,就可以使用全部其中的函数,类。
接下来,我们要继续细化我们刚刚构建的这个程序结构:增加全局变量。对于变量作用域的我的个人理解,是综合性能、程序正确性的考量。我们想做的只有一件事:在对的时间对的地点拿到正确的变量值,或者给正确的变量赋值。(顺便,这个在我初学react没有接触过别的那种带复杂生命周期的框架的时候,可是痛不欲生,历历在目)。变量什么时候诞生(定义并分配实际内存)、在哪段代码内能使用(作用域)、什么时候被释放(用不了了)是早期这些世界上最聪明的设计者设计语言时就想好的问题。虽然这些人有时候会被晕头转向的本科生称为万恶之源,但是大部分时候我们还是非常敬佩他们。废话也懒得删了。书上提到了一次定义原则,是说全局变量也和函数一样,一次定义,用的地方要声明,从而导致多次声明。这是再说跨文件的、真正全程序通用的全局变量。每个文件内都能声明,要加extern关键字,定义只能有一次(当然是在cpp文件)。仅仅在一个文件内的全局变量,只要在文件内定义就可以了(也就是在函数定义的外部),而每个函数都可以使用这个变量。这是本科生比较熟悉的,初学者动不动就搞一堆全局变量(你说的这个初学者,是不是你自己?)改进的方法是多多传参,不过有的地方需要用传参,有的地方最好还是全局变量,这个尺度原则我也说不好。C++的全局常量是文件的全局常量,关键词const,一个文件内只能定义一次,不同文件内不可以重名。(关于“真正的”全局程序常量,就是存储地址和值都只有一个的常量要用extern const而且要分开声明和定义防止多次引用导致的重复定义,在开销不大的情况下,可以仅使用const ,见https://blog.csdn.net/liuhhaiffeng/article/details/82623785)
还有一些神奇的静态量,只在第一次创建,以后一直保留。静态是一个相对的概念。举例比较简单,变量在某一文件中声明为静态全局变量static int a,那么每次在该文件中遇到别的时候定义的a,就不会再创建新的a了。也能声明静态函数,表示这个函数独属于该函数所在文件,也就是说文件内别的函数可以调用静态函数,但跨文件的调用是不行的。在类中的静态成员静态函数差不多,都是直属于类的存在。因此可以说,全局数据是程序数据,静态全局数据是文件数据,函数内部的局部数据是函数数据。没有静态全局常量,因为本来常量就是独属于文件的。如果搞不明白,可以记住怎么使用:1、全局变量在头文件中用extern声明,在cpp文件中定义。2、静态全局变量在cpp文件中直接用static定义,防止如果写在头文件中,不同include后都会重定义,导致静态数据不再静态。3、全局常量在头文件定义,在cpp中直接使用。4、静态局部变量也是静态量,第一次建立,之后一直存在,在函数内部用static修饰。
经过以上讨论,粗浅地构建了一个函数-文件的框架,至于作用域和生命周期问题,就不再写了。让我们再思考一下还会遇到什么问题。我们定义了1000个函数,当然有1000个函数名,函数名得有意义,总不能取字母随机序。如果有20个函数叫print,做了20种不同格式的输出。a.cpp和b.cpp里都定义了print,那在c.cpp里调用这两个print,就不知道调用的是哪个了。因此引入名空间机制。名空间是凌驾于其他程序文件之上的,要在头文件中定义namespace name{ void a1();},则在使用的时候用name::a1();除非文件开头用了using namespace name;,但两个名空间内有重名函数还是要加名空间。在定义的时候必须要加名空间,如void a2::func(){}。这里产生了一个很重要的思想:模块的思想。名空间是另一种模块化的划分。当然,一般来说一个头文件对应一个名空间,但名空间可以嵌套,如namespace m1{#include “f1.h”; #include “f2.h”},这样在使用m1的时候,就可以用m1::函数名调用f1和f2的函数了。
预编译和条件编译和核心架构没有太大关系,免谈。
头文件卫士#ifndef #endif组合,现在好像有替代方法#pragma once,总之是防止重复定义吧。
最后想想弄了半天到底整了个什么东西。我们努力想把程序文件整理有序,层次分明。函数、文件、模块,基本上是逐渐升级的过程,充当之间联系的种种都被命名。没有真正看过大型项目代码一定不能很好理解,不过好在现在还是理论学习阶段,理解思想就七七八八了。