CPP-09-编译访问

C++允许甚至鼓励程序员将组件函数放在独立的文件中,甚至可以单独编译这些文件,然后将它们链接成可执行的程序

可以将原来的程序分成三部分

  • 头文件:包含结构声明使用这些结构的函数的原型
  • 源代码文件:包含与结构有关的函数的代码。
  • 源代码文件:包含调用与结构相关的函数的代码。

请不要将函数定义变量声明放到头文件中,例如,如果在头文件包含一个函数定义,然后在其他两个文件(属于同一个程序)中包含该头文件,则同一个程序中将包含同一个函数的两个定义,除非函数是内联的,否则将出错。下面列出了头文件中常包含的内容

  • 函数原型
  • 使用#defineconst定义的符号常量
  • 结构声明
  • 类声明
  • 模板声明
  • 内联函数

查找头文件规则

  1. 如果文件名包含在尖括号< >中,则 C++编译器将在存储标准头文件的主机系统的文件系统中查找;
  2. 如果文件名包含在双引号" "中,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,这取决于编译器)。如果没有在那里找到头文件,则将在标准位置查找。因此在包含自己的头文件时,应使用" "而不是< >

头文件定义结构体定于与与结构体相关的函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef COORDIN_H_
#define COORDIN_H_

struct polar
{
double distance; //distance from origin
double angle; //direction from origin
};

struct rect
{
double x; //horizontal distance from origin
double y; //vertical distance from origin
};

//prototypes
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);

#endif

在同一个文件中只能将同一个头文件包含一次。这个规则很容易在不知情的情况下将头文件包含多次,所以使用 #ifndef,这种方法并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容。

1
2
3
4
5
#ifndef XXXX
#define XXXX

// place include file content
#endif

为什么上面使用 COORDIN_H_这么奇怪的名字?

根据include文件名来选择名称,并加上一些下划线,以创建一个在其他地方不太可能被定义的名称

file1.cpp 调用与结构相关的函数的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <cmath>
#include "coordin.h"
using namespace std;

polar rect_to_polar(rect xypos)
{
polar result;
result.distance = sqrt(xypos.x * xypos.x + xypos.y * xypos.y);
return result;
}

void show_polar(polar dapos)
{
const double RAD_TO_DEG = 57.295;
cout << "show:" << dapos.angle * RAD_TO_DEG << endl;
}

file2.cpp 定义与结构体相关的函数定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include "coordin.h"
using namespace std;

int main()
{
rect rplace;
polar pplace;
rplace.x = 10.1;
rplace.y = 20.2;
pplace = rect_to_polar(rplace); //转换
show_polar(pplace); //打印
return 0;
}

查看编译过程

1
2
3
4
5
#make clean && make
rm -rf build/*.o demo
g++ -Wall -Werror -std=c++11 -c src/file1.cpp -o build/file1.o
g++ -Wall -Werror -std=c++11 -c src/file2.cpp -o build/file2.o
g++ build/file1.o build/file2.o -o demo
cpp_compile_0731_1625

当函数被调用时,其自动变量将被加入到栈中,栈顶指针指向变量后面的下一个可用的内存单元。函数结束时,栈 顶指针被重置为函数被调用前的值,从而释放新变量使用的内存

cpp_compile_0731_1708

被调用的函数根据其形参描述来确定每个参数的地 址。

  1. 函数fib( )被调用时,传递一个2字节的int和一个 4字节的long。函数地址 0x1089
  2. 将名称 realtell同参数关联起来,函数调用将其参数的值放在栈顶,然后重新设置栈顶指针
  3. fib( )结束时,栈顶指针重新指向以前的位置。新值没有被删除,但不再被标记,它们所占据的空间将被下一个将值加入到栈中的函数调用所使用

C++也为静态存储持续性变量提供了3种链接性

1
2
3
4
5
6
7
8
9
10
11
12
...
int global = 1000; //static duration, external linkage
static int one_file = 50; //static duration, internal linkage
int main()
{
...
}
void funct1(int n)
{
static int count = 0; //static duration, no linkage
int llama = 0;
}
  • 外部链接性(可在其他文件中访问)
  • 内部链接性(只能在当前文件中访问)
  • 无链接性(只能在当前函数或代码块中访问)

编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。另外,如果没有显式地 初始化静态变量,编译器将把它设置为0

零初始化和常量表达式初始化被统称为静态初始化,在编译器处理文件(翻译单元)时初始化变量。动态初始化意味着变量将在编译后初始化。

C++提供了两种变量声明。

  • 一种是定义声明(defining declaration)或简称为定义(definition),它给变量分配存储空间;
  • 另一种是引用声(referencing declaration)或简称为声明(declaration),它不给变量分配存储空间,因为它引用已有的变量。

引用声明使用关键字extern,且不进行初始化;否则,声明为定义,分配存储空间

1
2
3
double up;		//definition, up is 0
extren int blem; //blem defined elsewhere
extern char gr = 'z'; //definition because initialized

cv 限定符 代表 constvolatile

const 表明内存被初始化后,程序便不 能再对它进行修改

volatile表明即使程序代码没有对内存单元进行修改,其值也可能发生变化

mutable 即使结构(或类)变量为 const,其某个成员也可以被修改

1
2
3
4
5
6
7
8
9
struct data
{
char name[30];
mutable int accesses;
};

const data veep = {"hello", 12};
strcpy(veep.name, "hello world"); //not allowed "const char *" 类型的实参与 "char *" 类型的形参不兼容
veep.accesses++; //allowed

全局const定义就像使用了static说明符一样,下面两者的内存存储是一样的

1
2
const int fingers = 10;
static const int fingers = 10;

C++不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都自动为静态的,在整个程序执行期间都是一直存在的。函数的链接性为外部的,即可以在文件间共享。

  • 可以使用关键字 extern 来指定函数在另一个文件中定义的(可选)
  • 可以使用 static 将函数的连接性设置为内部的,使之只能在一个文件中使用

命名空间

第一个文件 namesp.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <string>
using namespace std;

namespace pers
{
struct Person
{
string fname;
string lname;
};

void getPerson(Person &);
void showPerson(const Person &);
};

namespace debts
{
using namespace pers;
struct Debt
{
Person name;
double amount;
};

void getDebt(Debt &);
void showDebt(const Debt &);
double sumDebts(const Debt ar[], int n);
};

第二个文件 namesp.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include "namesp.h"
using namespace std;

namespace pers
{
void getPerson(Person & rp)
{
rp.fname = "hello";
rp.lname = "world";
}

void showPerson(const Person &rp)
{
cout << rp.fname << ", " << rp.lname << endl;
}
};

namespace debts
{
void getDebt(Debt & rd)
{
getPerson(rd.name);
cout << rd.amount << endl;
}

void showDebt(const Debt & rd)
{
showPerson(rd.name);
cout << rd.name.fname << rd.amount << endl;
}

double sumDebts(const Debt ar[], int n)
{
double total = 0;
for(int i = 0; i < n; i++) total += ar[i].amount;
return total;
}
};

第三个文件 namespp.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include "namesp.h"

void other(void);
void another(void);

int main(void)
{
using debts::Debt;
using debts::showDebt;
Debt golf = {{"hello", "world"}, 120.0};
showDebt(golf);
other();
another();
return 0;
}

void other(void)
{
using namespace debts; //debts 包含 pers,所以这里可以使用 Person
Person dg = {"Doodles", "Glister"};
showPerson(dg);
cout << endl;

Debt zippy[3];
for(int i = 0; i < 3; i++) getDebt(zippy[i]);
for(int i = 0; i < 3; i++) showDebt(zippy[i]);
cout << sumDebts(zippy, 3) << endl;
}

void another(void)
{
using pers::Person; //使用具体结构
Person collector = {"Milo", "RightShift"};
pers::showPerson(collector); //使用命名空间调用
cout << endl;
}

命名空间建议

  • 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量
  • 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量
  • 如果开发了一个函数库或类库,将其放在一个名称空间中。事实上,C当前提倡将标准函数库放在名称空间std中,这种做法扩展到了来自C语言中的函数。例如,头文件math.h是与C语言兼容的, 没有使用名称空间,但C头文件cmath应将各种数学库函数放在名称空间std中
  • 不要在头文件中使用using编译指令
    • 这样做掩盖了要让哪些名称可用;
    • 包含头文件的顺序可能影响程序的行为。如果非 要使用编译指令using,应将其放在所有预处理器编译指令#include 之后。
  • 导入名称时,首选使用作用域解析运算符或using声明的方法
  • 对于using声明,首选将其作用域设置为局部而不是全局

参考链接

  1. 《C++ Primer Plus》