摘要:在编写一个项目工程时,显然你大可把所有的代码都写在一个main函数里面,但是这对后续的修改和维护、代码功能的理解会造成很大的不便。构建代码库(Structuring Codebase)是围绕着软件工程处理 如何清晰地实现代码组织 和 如何实现功能模块化 需求提出的处理方法。围绕着这个需求,本章内容将着重讲解如何通过 命名空间 和 头文件 的方法进行代码组织。
命名空间本质上就是一种特殊的作用域。因此,在了解命名空间前必须要知道什么是作用域。作用域这个概念在介绍变量的生命周期的时候已经引入,相信对作用域都有模糊的概念,但是并没有做详细的定义。
作用域:指C++程序中变量、函数、类和其他标识符的可见性和可访问性。它决定了这些标识符的寿命和范围。 通常包括:局部作用域、全局作用域、命名空间和类作用域。
局部作用域:被定义在 函数 或者 代码块{ } 内部的变量具有局部作用域。它们无法被外界访问。当然他们的生命周期也仅限于这个函数内部或者带模块内。
全局作用域:相对地,被定义在 函数 或者 代码块{ } 外部的变量具有全局作用域。它们可以在任意位置被访问。当然他们的生命周期也伴随着整个程序的执行。
#include<iostream>
// 全局变量
int globalVar = 10;
void localExapmle(){
int localVar = 99;
std::cout << "local Var: " << localVar << std::endl; // 输出:local Var: 99
std::cout << "global Var: " << globalVar << std::endl; // 输出:global Var: 10
}
int main(){
localExapmle();
// std::cout << "local Var: " << localVar << std::endl; 会报错,显示变量未定义
std::cout << "global Var: " << globalVar << std::endl; // 输出:global Var: 10
}
命名空间本质上就是一种介于全局作用域和局部作用域的特殊的作用域。被使用 命名空间关键字 namespace:
#include<iostream>
// 命名空间的定义
namespace NameofNamespace{
int namespaceVar = 5;
}
int main(){
// 通过命名空间操作符 :: 访问命名空间内部变量
std::cout << NameofNamespace::namespaceVar << std::endl; //输出:5
}
定义在 代码块{ } 内部的变量 (e.g. namespaceVar) 具有命名空间作用域。它们无法被外界访问,但是可通过命名空间操作符::进行访问。当然他们的生命周期也伴随着整个程序的执行。
细心的同学已经发现 std::cout 本质上也是一种命名空间,在很多代码中以下代码段:
#include<iostream>
using namespace std;
namespace NameofNamespace{
int namespaceVar = 5;
}
int main(){
// 通过 using namespace std 指定整个文件的命名空间都默认为 std,因此使用cout和endl都不需要在前面加入命名空间;
cout << NameofNamespace::namespaceVar << endl; //输出:5
}
通过 using namespace std 指定整个文件的命名空间都默认为 std,因此使用cout和endl都不需要在前面加入命名空间;当然,也可使用关键字 using namespace 指定整个文件的命名空间都默认为自定义的命名空间,如:NameofNamespace 或者同时指定多个命名空间。
#include<iostream>
// 自定义命名空间
namespace NameofNamespace{
int namespaceVar = 5;
}
using namespace NameofNamespace; // 使用命名空间需要在自定义命名空间之后
using namespace std; // 同时定义多个命名空间
int main(){
// 已经指定整个文件的命名空间,因此使用namespaceVar,cout和endl都不需要在前面加入命名空间;
cout << namespaceVar << endl; //输出:5
}
当然,命名空间也支持嵌套,通过 外层命名空间::内层命名空间::变量名 来访问,如:
#include<iostream>
using namespace std; // 同时定义多个命名空间
// 自定义命名空间
namespace Outer{ //外层命名空间
namespace Inner{ //内层命名空间
int innerVar = 66;
}
int outerVar = 5;
}
int main(){
cout << "outerVar: " << Outer::outerVar << endl; //输出:outerVar: 5
cout << "innerVar: " << Outer::Inner::innerVar << endl; //输出:innerVar: 66
}
上一节我们讲解了通过命名空间操作符 :: 可以访问命名空间内部变量,但应该不少人在类的使用时也见过其他类似的操作符,如 -> 和 . ,他们和命名空间操作符 :: 有什么关系或者差异呢?
操作符 :: 可以结合类名称用于访问类中定义的的静态变量或者用于定义类中已经声明的函数,该运算符把类名视为一种特殊的命名空间;
操作符 . 用于访问对象的成员,相比较于操作符 :: ,操作符 . 的操作内容是类实例化的对象(如例子中的obj,而非类MyClass);
操作符 -> 用于访问对象的成员,相比较于操作符 :: ,操作符 -> 的操作内容是类实例化的指针对象(如例子中的obj1,而非类MyClass);
#include <iostream>
class MyClass {
public:
static int staticMember;
int nonStaticMember;
void printer();
MyClass(int value) : nonStaticMember(value) {}
};
int MyClass::staticMember = 7;
// int MyClass::nonStaticMember = 7; 报错,非静态变量不能在类的外部定义;
void MyClass::printer(){std::cout << "I am function! " << std::endl;};
int main() {
MyClass obj(10);
MyClass* obj1;
std::cout << "Static member with ::: " << MyClass::staticMember << std::endl;
std::cout << "Static member with .: " << obj.staticMember << std::endl;
std::cout << "Static member with ->: " << obj1->staticMember << std::endl;
std::cout << "Non-static member: " << obj.nonStaticMember << std::endl;
}
// Output:
// Static member with namespace: 7
// Static member with .: 7
// Static member with ->: 7
// Non-static member: 10
头文件,或者说头文件-源文件的代码构建方式是另外一种常见的优化代码结构方法。有利于提高功能模块化、代码可读性和提高维护效率。
头文件通常扩展名为.h或.hpp,负责声明多个源文件所需的类、函数和变量。它们充当代码不同部分之间的接口,使管理依赖关系和快速了解代码功能变得更容易,并减少重复代码。
// example.h
#ifndef EXAMPLE_H
#define EXAMPLE_H
class Example {
public:
void printMessage();
};
#endif
扩展名为.cpp的源文件负责实现相应头文件中定义的实际功能。它们包括所需的头文件,并提供函数和类方法定义。
// example.cpp
#include "example.h"
#include <iostream>
void Example::printMessage() {
std::cout << "Hello, code splitting!" << std::endl;
}
显然,把功能函数/类写在独立的cpp中,与main函数分离,功能模块化效果大大提升,也更容易理解代码的功能。而分离编译可以有效地提高代码的维护效率。
因为main函数所在的文件和功能cpp文件独立存在,当修改功能时仅仅需要修改cpp文件。因此分离编译的功能就是把main.cpp和功能cpp文件独立编译为不同的对象文件(.o文件),然后再通过连接对象文件,构建最后的可执行文件(.exe文件)。这意味着当只修改某个功能cpp文件时,只需要重新生成它所对应的对象文件(.o文件),而其他文件不需要重新编译,这将大幅度提升编译效率。
# Compile each source file into an object file
g++ -c main.cpp -o main.o
g++ -c example.cpp -o example.o
# Link object files together to create the executable
g++ main.o example.o -o my_program
构建代码库(Structuring Codebase)是围绕着软件工程处理 如何清晰地实现代码组织 和 如何实现功能模块化 需求提出的处理方法。围绕着这个需求,本章内容将着重讲解如何通过 命名空间 和 头文件 的方法进行代码组织。