一:c++程序的一般组织结构
在规模较大的项目中,往往是多人同时编写,所以我们需要多个源程序文件,每个源程序文件(cpp文件)都称为一个编译单元。
c++的语法要求一个类的定义必须出现在所有使用该类的编译单元中(这也符合常理,就和写文章一样,你要用我的文章,你得把我得名字也附到你的文章中)。比较好的方法就是讲类的定义写在头文件中,类的实现写在一个新的cpp中,在这个新的cpp开头包含类的定义那个头文件。(还有一种办法也不错,就是直接将头文件的后缀改成.hpp,将类的定义和实现都写在.hpp文件中,然后在调用类的主函数或其他cpp的开头直接包含这个.hpp文件就好,目前只是在vs2019上试过,其他的编译器还不清楚)。
通常一个项目至少划为三个文件:类的定义文件(*.h),类的实现文件(*.cpp),使用该类的文件(main.cpp或*.cpp)
对于更复杂的程序,有很多类,每个类都有自己的定义文件和实现文件,采取这样的组织结构方便单独编写和编译,最后在连接到一起,同时也可以充分利用类的封装特性,便于我们对其中某个类的修改(方便我们及时定位,因为其他部分我们根本不需要去管)。
下面给出一个例子(摘自郑莉老师的c++语言程序设计)
//文件一,类的定义 point.h
class point{
public:
static void showCount(){};//静态成员函数
point(const point&p){};//拷贝构造函数。
private:
static int count;
};
//文件二,类的实现 point.cpp
#include"point.h"
#include<iostream>
using namespace std;//using namespace std尽量不要写在.h文件中,因为这会使一个命名空间不被察觉的对一个头文件开放。
int point::count=0;//使用类名初始化静态数据成员
//拷贝构造函数的实现
point::point(const point &p){
cout<<"I am copy"<<endl;
}
void point::showCount(){
cout<<"object count="<<count<<endl;
}
//文件3 主函数cpp
#include"point.h"(其实这里包含point.cpp也可以)
#include<iostream>
using namespace std;
int main()
{
point p;
point p1(p);
p.showCount();
return 0;
}
给出一张图分析结构:
从图5-8可以看到,两个.cpp的文件被分别编译生成各自的目标文件. obj,然后再与系统的运行库共同连接生成可执行文件.exe。如果只修改了类的成员函数的实现部分,则只重新编译point.cpp并连接即可,其余的文件几乎可以连看都不用看。想一想,如果是一个语句很多、规模特大的程序,效率就会得到显著的提高。
决定一个声明放在源文件中还是头文件中的一般原则是,将需要分配空间的定义放在源文件中(.cpp文件),例如函数的定义(需要为函数代码分配空间)、命名空间作用域中变量的定义 (需要为变量分配空间)等;
而将不需要分配空间的声明放在头文件(.h文件)中,例如类声明、外部函数的原型声明,外部变量的声明(外部函数和外部变量将在5.6.2节中详细讨论)、基本数据类型常量的声明等。内联函数比较特殊,由于它的内容需要嵌入到每个调用它的函数之中,所以对于那些需要被多个编译单元调用的内联函数,它们的代码应该被各个编译单元可见,这些内联函数的定义应当出现在头文件中。
如果误将分配了空间的定义(using namesapce std)写入头文件中,在多个源文件包含该头文件时,会致空间在不同的编译单元中被分配多次,从而在连接时引发错误。
在《高质量C/C++编程指南》一书中,对此也有说明:
【建议】头文件中只存放“声明”,而不存放“定义”。
在C++语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数。这虽然会带来书写上的方便,但却造成了风格不一致,弊大于利。建议将成员函数的定义分开,不论该函数 体有多么小。
二:外部变量和外部函数
1.外部变量
如果一个变量除了在定义它的源文件中可以使用外,还能被其他文件使用,那么就称这个变量是外部变量。命名空间作用域中定义的变量,默认情况下都是外部变量,但在其他文件中如果需要使用这一变量,需要用extern 关键字加以声明。请看下面的例子。(假设源文件2想要使用源文件1中的全局变量。
方法一:在源文件1中定义一个变量,在源文件2中加extern声明。
//源文件1的头文件
(啥也不写)
//源文件1
int i = 1;
/*void test() {
int i = 1;
} 不要写在函数体内部,作用域的缘故。
*/
//源文件2
#include<iostream>
using namespace std;
extern int i;
int main() {
cout<<i++;
}
方法二:在源文件1定义一个全局变量,把extern写在源文件1的头文件中,在源文件2中直接包含源文件1的头文件即可。
//源文件1的头文件
extern int i;
//源文件1
int i=1;
//源文件2
#include<iostream>
#include"point.h"源文件1的头文件
using namespace std;
int main() {
cout<<i++;
}
对外部变量的声明可以是定义性声明,即在声明的同时定义(分配内存,初始化),也可以是引用性声明(引用在别处定义的变量)。在命名空间作用域中,不用extern关键字声明的变重,都是定义性声明;用extern 关键字声明的变量,如果同时指定了初值,则是定义性声明(extern int i=1;),否则是引用性声明(extern int i)。
谨记:声明可以多次,定义只能一次
外部变量可以有多处声明,但是对变量的定义性声明只能是唯一的.
【建议】不提倡使用全局变量,尽量不要在头文件中出现现象 extern int value 这类声明。
2.外部函数
在所有类之外声明的函数(也就是非成员函数),都是具有命名空间作用域的,如果没有特殊说明,这样的函数都可以在不同的编译单元中被调用,只要在调用之前进行引用性声明(即声明函数原型)即可。和外部变量不一样的是,其实修饰不修饰,最终效果其实都一样。
三:将变量和函数限制在编译单元内。
命名空间作用域中声明的变量和函数,在默认情况下都可以被其他编译单元访问,但有时并不希望一个源文件中定义的命名空间作用域的变量和函数被其他源文件引用,这种需求主要是出于两个原因,
一是出于安全性考虑,不希望将一个只会在文件内使用的内部变量或函数暴露给其他编译单元,就像不希望暴露一个类的私有成员一样;
二是新大工程来说,不同文件之中的、只在文件内使用的变量名很容易重名,如果将它们都暴露出来,在连接时很容易发生名字冲突。
惯例,先通过代码看问题:
文件组织结构:
具体代码示意图(省略了一些代码):可以看出f1和f2中含有同名的test()函数,那么在主函数中调用test()函数会怎样呢?
就会产生二义性的错误,编译器不知道调用哪个函数。
Q1:怎么解决?
R1:将变量和函数限制在编译单元内。
Q2:怎么限制
R2:两种方法,一种是static,一种是匿名命名空间
方法一:static法
对这一问题,曾经的解决办法是,在定义这些变量和函数时使用static关键字,static关键字用来修饰命名空间作用域的变量或函数时,和extern关键字起相反的作用,
它会使得被static修饰的变量和函数无法被其他编译单元引用。从而达成限制。
如下图所示:把其中一个同名函数设置为static,那么别的文件就无法访问这个函数。二义性也就解决了。
方法二:使用匿名的命名空间
这是在2003年发布的ISOc++2.0标准中新提出的方法,在匿名命名空间中定义的变量和函数,都不会暴露给其他编译单元。
namespace{
原来的代码块
}
四:头文件重复包含的问题
重复引入同一头文件的问题:当你在头文件中定义变量或者函数时(注意是定义不是声明,多次声明是没有问题的)多次引入头文件就会报“变量重定义”的错误。
通过具体的代码看错误。
大致结构:(和菱形继承的结构相似)
//file1头文件1
(内容随意)
//file2头文件2
#include"file1.h"
//file3头文件
#include"file1.h"
//主函数cpp源文件
#include<iostream>
#include"file2.h"
#include"file3.h" //可见file3中以及包含了file1,file2中包含又包含了一遍file1,这就引起了重复包含。
方法一:条件编译(#ifndef关键字,编译预处理那块的知识)
在file1.h的头文件中加两行代码
#ifndef FILE1_H //头文件名的大写
#define FILE1_H
这两行代码的意思就是:通过一个唯一的标识符来标记文件是否已参加过编译,如果参加过编译,说明该程序段是被重复包含的,编译时自动忽略
方法二:使用#pragma once
#pramgma once是微软编译器独有的,也是后来才有的,但是他不支持跨平台。如果需要跨平台跨平台的代码,最好使用条件编译,如果想使用#pragma once,
只需在被包含的头文件开头加上#pragma once即可(拿上边的例子就是在file1和file2加上#pragma once就行)