前言: 之前总是对于c++中的一些关键字的用法理解得云里雾里,此篇便是自己从头总结梳理了一下。从根本上理解和应用好这些关键词。此篇也给自己挖了许多待填的坑,浏览查看《编译原理》。
1. 从c++的程序实现说起
曾对编程了解甚少的时候,就特别好奇一行行字母代码是如何让计算机听话的,是如何转化为计算机“行动”,完成很多不可思议的动作的。后来接触微机原理,再后来读到一些关于计算机实现的硬件原理的书,真是醍醐灌顶,解决了小白的自己对于计算机曾有的的诸多好奇,额,扯远了……
回到话题,不说什么高级语言、机器语言等等了。
所有开始前,贴几个简单代码文件,借以后面辅助说明。
// a.cpp
#include "b.h"
int test();
extern int c;
int main(){
std::cout << b << "\n" << c << std::endl;
test();
return 0;
}
// b.h
#include <iostream>
int b = 100;
//是为了验证一个头文件后缀名并不特殊
// c.cpp
#include <iostream>
int c = 200;
int test(){
std::cout << "I'm in c.pp" << std::endl;
return 0;
}
Linux下编译与执行:
linux$ ls
a.cpp b.h c.cpp
linux$ g++ a.cpp c.cpp
linux$ ls
a.cpp a.out b.h c.cpp
linux$ ./a.out
100
200
I'm in c.cpp
//备注:
$ g++ -E a.cpp c.cpp >xxx //预处理
$ g++ -S a.cpp c.cpp //预处理、编译,生成汇编代码
$ g++ -c a.cpp c.cpp //预处理、编译和汇编,生成文件 a.o 、c.o
$ g++ -o a.cpp c.cpp //预处理、编译、链接,生成可执行文件
就c++程序实现而言,通常需要以下几个步骤:编辑—>编译—>运行
-
编辑
这个过程,就是平时进行的操作,用编辑器写代码,最后保存为一定后缀名的相应c++文件。对于不同的编辑器或系统能够识别保存的c++文件拓展名也不尽相同。常用的有
.cpp
.cxx
.cc
.c++
等。 -
编译
这一过程是要详细了解的重中之重。详细来说,三个方面:预处理过程—>编译过程—>连接过程。
-
预处理过程
该过程就是在编译器对源程序进行编译之前,对程序文本进行的预处理操作,通常就是执行程序里的一些预处理指令。c++的预处理操作指令在程序中都是用 # 引导。
#include //包含一些头文件 #define //定义 #undef //取消定义 #ifdef //如果已经定义,则 #if //如果(为真),则 #elif //或者(为真),则 #ifndef //如果没有定义,则 #endif //结束条件 //可以看出,基本上预处理主要是一些:文件包含、定义宏、条件编译操作。
发现一个有意思的是,由于
#include "b.h"
的操作实质上就相当于把b.h
拷贝过来,或者说展开b.h
中的内容到该文件,因此b.h
的后缀名貌似就不是那么重要了,完全可以使用“b.txt”
,"b.c"
等。最后自己想,用“.h”
当后缀名可能是为了更好区分哪些是头文件,方便后续的编译。因为编译过程g++ a.cpp c.cpp
,是不对头文件编译的。关于另外其中的一些细节,诸如一些特殊预处理命令的操作使用、c++预定义宏,到自己常用到或深有体会时,再来填坑。
-
编译过程
首先不得不说的一个名词是“编译单元”,如编译链接命令
g++ a.cpp c.cpp
,事实上,它是对于每个.cpp
文件的单独编译,有两个编译单元,也即表述为对a.cpp
的编译和对c.cpp
的编译。编译过程,粗略来讲,就是将我们编写的源代码通过编译器,翻译成机器能够识别的机器码,如上例中编译后会生成
a.o
和c.o
两个目标文件。详细一点讲,就是对源程序进行词法和语法分析,映射程序中的各种符号及其它们的属性。再详细一点,就需要读相关编译原理的书了,也是自己挖下待填的又一个坑,不过现在看来,这个过程的实现还是很有意思的。另外正是相应的符号映射、属性及其他一些信息的转化记录,为后面的链接过程提供了相应信息。
备注:
- 如上面提到的生成的
a.o
c.o
,这两个文件是独立的。但可从源文件a.cpp
显而易见地看出,其用到了文件c.pp
中定义的c
变量(外部链接属性)和test()
函数。要实现两者关系的链接,也就需要后面的链接过程了。对于目标文件的链接,貌似是通过符号识别的,脱离了相应函数名,这个待读编译原理的书确定。另,实际的链接过程,当然远不止这些。 - 上面说到定义,与之相应的有个概念叫声明,很值得一提的是,这两个概念十分重要,尤其对于后面要说的extern关键字。重定义会编译出错,重复声明貌似不会出错(有些情况可能出错,待研究)。
- 如上面提到的生成的
-
链接过程
上面已经提到了大致原因和过程,就是将相关的目标文件彼此连接,由于会跨文件,此时极大牵扯到一个编程中常提的几个名词,也是曾困扰我的一个问题,即作用域、链接属性,当然还有与之相关的命名空间等。一些c++常用的关键词也在此发挥巨大作用,体验了其价值所在。
链接方式可以分为两种:静态链接和动态链接
-
静态链接
链接过程是将函数代码从所在地静态链接库里拷贝到最终的可执行程序中。( Linux: .a Windows: .lib )
-
动态链接
函数代码被放到动态链接库或共享对象的某个目标文件里,链接过程所做的仅仅是记录下相应信息。( Linux: .so Windows: .dll )
-
-
额外补充
由于自己编程使用的是Linux系统,目前用到了两种编译途径。
一种是直接使用g++完成编译,但当要构建有些复杂的项目时,会有些力不从心,于是就有第二种利用cmake完成项目编译。
- g++编译
这个是来自于 Free Software Foundation 的 GNU C++编译器。常用语法上面也有提到了。
另外值得一提的是,其中关于 FSF 的来由,Linux的来由的历史比较有意思。
- 利用cmake工具
这个只能说下现在自己的理解,因为自己没有深究其中的具体过程和原理,因此还无法全面把握其整个实现过程。只知道如何编写
CMakeLists.txt
文件和cmake工具的发展来由。个人理解的简而概述就是,cmake工具或者类似的qmake(Qt),其都是为了make服务的。对于一个庞大项目来说,由于其复杂性,用简单的编译工具一一去编译链接不太现实,要是编写一个“统领式的”文件,其文件内容告诉编译器编译整个项目中代码的方法就行了,而这个“统领”文件就是makefile文件,编写完成makefile文件,通过make指令,便可以生成最终相关文件和可执行文件。但问题是,makefile文件编写也比较复杂难写,那就再来个文件,语法和内容尽量简单,然后实现任务是完成对makefile的编写,于是
CMakeLists.txt
文件来了,编写完成好CMakeLists.txt
文件后,通过cmake(或者qmake等其他一些类似工具)生成makefile文件,然后在通过make命令,生成最后编译结果。
-
-
运行
在Linux下,默认生成的可执行程序是
a.out
,运行程序只需输入命令$ ./a.out
便可,需要在可执行程序名称前加上当前目录,这是唯一需要注意的地方。
2. c++关键词
该部分主要牵扯到了两个关键词,即static和extern。
一些名词解释如下:
存储持续性:描述其存储的持续长久。诸如:自动、静态(在整个程序执行间都存在)
作用域:描述其在文件(单元)的多大范围可见。诸如:全局、局部
链接性:描述其在文件(单元)间共享属性。诸如:内部链接(内部共享),外部链接(外部共享),无链接(局限于代码块)
备注:作用域全局的意思,是指在该文件中全局可见,如果在另个文件也可见,需要更改链接属性。
-
static
该关键词可以声明为变量或函数的前缀。根据static的含义,即其声明定义的内容在整个程序执行期间都存在,可以显而易见地想到static会许多特殊的性质。诸如:共享、初始化、隐藏等。
变量
// t.cpp int a; //静态,全局,外部 static int b; //静态,全局,内部 //静态,全局,外部 int stat1(){ int c; //自动,局部,无 static int d; //静态,局部,无 return 0; } //静态,全局,内部 static int stat2(){ return 0; }
-
第一种(a):不在任何函数内,不使用关键字static
存储持续性为静态,作用域为全局,链接性为外部
-
第二种(b):不在任何函数内,使用关键字static
存储持续性为静态,作用域为全局,链接性为内部,只在该文件内可见
-
第三种(c):在代码块中,不使用关键字static
存储持续性为自动,作用域为局部(代码块),链接性为无
-
第四种(d):在代码块中,使用关键字static
存储持续性为静态,作用域为局部(代码块),链接性为无
函数
考虑到不可以在代码块内定义函数,因此函数存储持续性都为静态的,作用域都是全局的,如果不那样的话,它只对自己可见,而无法被其他函数调用,这样的函数没有意义,也无法运行。所以,就去掉上述第三、四种情况。
-
第一种(stat1):不使用关键字static
也即默认状态,此时链性为外部
-
第二种(stat2):使用关键字static
此时链接性为内部,只在该文件内可见
备注:
- 静态变量存储在静态存储区,该存储区的变量值会自动初始化为0,另普通全局变量也存储在这个区,该区内容持续到程序执行结束。
- 从上述可以看出,对变量和函数添加static关键词可以更改其链接属性,使其在别的文件不可见,因此可以起到隐藏的作用,也可以防止不同文件间变量/函数名称命名重复而报错问题发生。该特性也是与存储在静态区的普通全局变量的区别所在。
类的静态成员
其存储在静态存储区,不为所有对象特有。
class myclass{ int x; static int y; //静态成员变量 public: static int test(); //静态成员函数 }; my class::count = 0; //静态成员初始化 int myclass::test(){ //静态成员函数定义(前面不能加static) return 0; }
-
数据成员
- 静态数据成员的数据为所有对象共享。
- 静态数据成员只能在类的声明外部进行初始化:其实初始化的过程就是申请内存的过程,静态数据成员是全局变量,不应该由某个对象控制,包括初始化,加const关键字的除外。相似的一个道理,一般情况下,也最好不在类体内给普通成员变量赋值,因为这是每个对象的事,类体不该管那么多(不过貌似可以编译通过的,aha)。
- 静态数据成员初始化时不加static关键字:避免同一般静态变量混淆。
-
成员函数
- 静态成员函数为全体对象共享,因此不能使用指向某个对象的this指针来调用静态函数。
- 静态成员函数不了解每个对象各自的普通成员变量情况,因此只能直接访问静态成员变量。可以理解为函数参数表里不包含this指针,而普通函数默认隐含this指针。如果想要访问非静态成员变量,需要借用对象名或指向对象的指针。
- 通常,类中有静态数据成员,一般会把访问该成员的函数说明成静态的。
- 静态成员函数可在类体内或类外定义,当在类外定义时,前面不要加static关键字。
备注:对于类的静态成员的访问可以使用一下方式
- 类名 :: 成员
- 对象 . 成员
- 对象指针 -> 成员
-
-
extern
对于extern关键字,主要体现在声明上。上述中提到了变量或函数的链接性问题,其中包含一种外部链接,也即文件间的数据共享。如初始例子中
a.cpp
与c.cpp
两文件数据通信,a.cpp
要使用c.cpp
中定义的变量和函数(当然要是全局、外部链接属性的),就需要用到extern关键字声明,告诉a.cpp
使用的数据去c.cpp
中找,更确切说,在链接过程的时候,建立相应的符号链接。- 声明分为两种:一种是定义声明,简称定义;另一种是引用声明。
- 除非包含extern关键字且无赋值操作,否则都是变量的定义声明。
- extern声明是引用声明,不是定义声明,因此也不分配存储空间 。
写在最后:
- const 常量默认是内部链接,因为其初始化后就不会改变,防止外部链接对值修改后保持不一致。全局变量默认是外部链接,相似的道理。
- 不要把变量的定义放到头文件中,这样容易导致重复定义的错误。
- 尽量使用static关键字把变量定义限制于该文件作用域内。