内存模型和名称空间
内存模型和名称空间是计算机科学中两个重要的概念。
内存模型是指计算机系统在执行程序时,将程序的数据和指令存储在主存储器中的方式。它定义了程序如何访问和操作内存中的数据。常见的内存模型包括单一内存模型(如单线程),共享内存模型(如多线程),分布式内存模型(如分布式系统)。不同的内存模型决定了程序的并发性、可见性和数据一致性等方面的行为。
名称空间是指标识符(如变量、函数、类等)在程序中的可见范围。它用于解决命名冲突的问题,即同一程序中不同部分使用相同的标识符时可能导致的混淆。通过使用不同的名称空间,可以在程序中定义具有相同名称但作用域不同的标识符。例如,在C++中,可以使用命名空间来区分不同的库或模块中的标识符,避免命名冲突。
总结来说,内存模型关注程序如何使用和访问内存,而名称空间关注如何解决标识符的命名冲突。它们都是编程中重要的概念,对于理解程序的执行和组织具有重要意义。
在C++中,内存模型和名称空间也是非常重要的概念。
C++的内存模型是基于共享内存的多线程模型。它允许在多个线程之间共享数据,并通过同步机制(如互斥锁、条件变量等)来确保数据的正确性和一致性。C++提供了多种线程库(如std::thread、std::mutex等),以及原子类型和操作(如std::atomic),用于实现多线程编程。
名称空间在C++中用于解决标识符的命名冲突问题。C++中的名称空间由关键字namespace定义,可以将标识符分组到不同的名称空间中。这样,即使不同的名称空间中有相同的标识符,也不会引起冲突。使用名称空间可以有效地组织代码,并提供更好的可读性和可维护性。同时,C++标准库中的类、函数等也位于std名称空间中。
除了内存模型和名称空间,C++还有其他一些重要的概念,如类、对象、继承、多态等。这些概念构成了C++面向对象编程的基础,使得C++成为一种强大而灵活的编程语言。
9.1 单独编译
和C语言一样,C++也允许甚至鼓励程序员将组件函数放在独立的文件中。第1章介绍过,可以单独编译这些文件,然后将它们链接成可执行的程序。通常,C++编译器既编译程序,也管理链接器。如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接。这使得大程序的管理更便捷。另外,大多数C++环境都提供了其他工具来帮助管理。例如,UNIX和Linux系统都具有make程序,可以跟踪程序依赖的文件以及这些文件的最后修改时间。运行make时,如果它检测到上次编译后修改了源文件,make将记住重新构建程序所需的步骤。大多数集成开发环境(包括Embarcadero C++ Builder、Microsoft Visual C++、Apple Xcode和Freescale CodeWarrior)都在Project菜单中提供了类似的工具。
现在看一个简单的示例。我们不是要从中了解编译的细节(这取决于实现),而是要重点介绍更通用的方面,如设计。
例如,假设程序员决定分解程序清单7.12中的程序,将支持函数放在一个独立的文件中。清单7.12将直角坐标转换为极坐标,然后显示结果。不能简单地以main()之后的虚线为界,将原来的文件分为两个。问题在于,main()和其他两个函数使用了同一个结构声明,因此两个文件都应包含该声明。简单地将它们输入进去无疑是自找麻烦。即使正确地复制了结构声明,如果以后要作修改,则必须记住对这两组声明都进行修改。简而言之,将一个程序放在多个文件中将引出新的问题。
谁希望出现更多的问题呢?C和C++的开发人员都不希望,因此他们提供了#include来处理这种情况。与其将结构声明加入到每一个文件中,不如将其放在头文件中,然后在每一个源代码文件中包含该头文件。这样,要修改结构声明时,只需在头文件中做一次改动即可。另外,也可以将函数原型放在头文件中。因此,可以将原来的程序分成三部分。
头文件:包含结构声明和使用这些结构的函数的原型。
源代码文件:包含与结构有关的函数的代码。
源代码文件:包含调用与结构相关的函数的代码。
这是一种非常有用的组织程序的策略。例如,如果编写另一个程序时,也需要使用这些函数,则只需包含头文件,并将函数文件添加到项目列表或make列表中即可。另外,这种组织方式也与OOP方法一致。一个文件(头文件)包含了用户定义类型的定义;另一个文件包含操纵用户定义类型的函数的代码。这两个文件组成了一个软件包,可用于各种程序中。
请不要将函数定义或变量声明放到头文件中。这样做对于简单的情况可能是可行的,但通常会引来麻烦。例如,如果在头文件包含一个函数定义,然后在其他两个文件(属于同一个程序)中包含该头文件,则同一个程序中将包含同一个函数的两个定义,除非函数是内联的,否则这将出错。
下面列出了头文件中常包含的内容。
函数原型。
使用#define或const定义的符号常量。
结构声明。
类声明。
模板声明。
内联函数。
将结构声明放在头文件中是可以的,因为它们不创建变量,而只是在源代码文件中声明结构变量时,告诉编译器如何创建该结构变量。同样,模板声明不是将被编译的代码,它们指示编译器如何生成与源代码中的函数调用相匹配的函数定义。被声明为const的数据和内联函数有特殊的链接属性(稍后将介绍),因此可以将其放在头文件中,而不会引起问题。
程序清单9.1、程序清单9.2和程序清单9.3是将程序清单7.12分成几个独立部分后得到的结果。注意,在包含头文件时,我们使用"coordin.h",而不是<coodin.h>。如果文件名包含在尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,这取决于编译器)。如果没有在那里找到头文件,则将在标准位置查找。因此在包含自己的头文件时,应使用引号而不是尖括号。
图9.1简要地说明了在UNIX系统中将该程序组合起来的步骤。注意,只需执行编译命令CC即可,其他步骤将自动完成。g++和gpp命令行编译器以及Borland C++命令行编译器(bcc32.exe)的行为类似。Apple Xcode、Embarcadero C++ Builder和Microsoft Visual C++基本上执行同样的步骤,但正如第1章介绍的,启动这个过程的方式不同——使用能够创建项目并将其与源代码文件关联起来的菜单。注意,只需将源代码文件加入到项目中,而不用加入头文件。这是因为#include指令管理头文件。另外,不要使用#include来包含源代码文件,这样做将导致多重声明。
/ coordin.h -- structure templates and function prototypes // structure templates #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 COORDIN_H_
和#define COORDIN_H_
这两个指令一起用来创建一个预处理器变量,以确保头文件的内容只被编译一次。如果COORDIN_H_
这个预处理器变量尚未定义,则执行下面的代码;否则,跳过代码块。#endif
指令用于结束条件编译块。
- 结构模板:
struct polar
定义了一个极坐标结构,具有两个成员变量distance
和angle
,分别表示距离原点的距离和与原点的方向。struct rect
定义了一个直角坐标结构,具有两个成员变量x
和y
,分别表示与原点的水平和垂直距离。
- 函数原型:
polar rect_to_polar(rect xypos)
是一个函数原型,用于将直角坐标转换为极坐标。它接受一个rect
类型的参数xypos
,返回一个polar
结构。void show_polar(polar dapos)
是一个函数原型,用于显示极坐标的值。它接受一个polar
类型的参数dapos
,不返回任何值(void
)。
通过包含这个头文件,其他源代码文件可以访问 polar
和 rect
结构,并使用 rect_to_polar
和 show_polar
函数,而无需重新定义结构或函数原型。
这种组织方式使得代码更模块化和可复用。可以将该头文件作为一个软件包,供多个源代码文件使用。如果需要修改结构定义或函数原型,只需在头文件中修改一次即可,所有包含该头文件的源代码文件都会自动获得更新。这样可以提高代码的可维护性和开发效率。
9.2 存储持续性、作用域和链接性
9.2 存储持续性、作用域和链接性是关于变量在程序中的生命周期、可见性和访问性的概念。下面对这些概念进行详细解释:
- 存储持续性(Storage Duration):
- 存储持续性指的是变量在内存中的存在时间。
- 根据存储持续性,变量可以分为三种类型:自动变量(automatic)、静态变量(static)和动态分配变量(dynamic)。
- 自动变量:也称为局部变量,其存在于函数或代码块的作用域内,并通过栈来分配和释放内存。它们的存储持续性与所属的作用域一致。
- 静态变量:在程序执行期间都存在,在函数或代码块的作用域之外定义,在全局数据区分配内存。具有静态存储持续性。
- 动态分配变量:使用特殊的函数(如malloc())在堆上分配内存,可以手动控制其存储持续性。
- 作用域(Scope):
- 作用域指的是变量的可见性范围,即在哪些部分的代码中可以访问到该变量。
- 分为局部作用域和全局作用域。局部作用域是变量在函数或代码块内可见,全局作用域是变量在整个程序中可见。
- 在C语言中,可以使用代码块来创建局部作用域。
- 链接性(Linkage):
- 链接性指的是同一标识符在不同文件之间的链接关系,即是否可以在多个文件中共享标识符。
- 分为外部链接性、内部链接性和无链接性。
- 外部链接性:可以在多个源文件之间共享的标识符。可以通过在一个文件中声明,然后在其他文件中使用。
- 内部链接性:在单个源文件内部共享的标识符。只能在定义该标识符的文件中使用。
- 无链接性:只在当前作用域内可见的标识符,无法在其他文件或作用域中使用。
当然,我可以继续解释有关存储持续性、作用域和链接性的其他方面。
- 存储类别说明符(Storage Class Specifiers):
- 在C语言中,可以使用存储类别说明符来控制变量的存储持续性和链接性。常用的存储类别说明符包括:
auto
:默认的存储类别说明符,用于定义自动变量。static
:用于定义静态变量,具有静态存储持续性。extern
:用于声明具有外部链接性的变量或函数。在一个文件中声明,然后在其他文件中使用。register
:建议编译器将变量存储在寄存器中,以便快速访问。并非所有变量都可以被分配到寄存器中。
- 作用域规则:
- 在C语言中,变量的作用域由其声明的位置决定。
- 局部变量的作用域限定为声明它们的代码块内部。即使存在同名的全局变量,局部变量也会屏蔽全局变量。
- 全局变量的作用域从其声明的位置开始一直延伸到整个程序的末尾。在不同的文件中可以通过外部链接性进行共享。
- 链接性规则:
- 在C语言中,全局变量和函数具有外部链接性。
- 使用
static
关键字可以将全局变量或函数的链接性修改为内部链接性。这样它们只能在定义它们的文件内部使用,无法在其他文件中访问。 - 局部变量默认情况下没有链接性,只能在所属的代码块内部使用。
通过理解存储持续性、作用域和链接性的概念,开发者可以更好地管理变量并确保它们在程序中的正确使用。这有助于避免命名冲突、提高代码的可读性和可维护性,并确保变量的生命周期和可见性符合预期。
// autoscp.cpp -- illustrating scope of automatic variables #include <iostream> void oil(int x); int main() { using namespace std; int texas = 31; int year = 2011; cout << "In main(), texas = " << texas << ", &texas = "; cout << &texas << endl; cout << "In main(), year = " << year << ", &year = "; cout << &year << endl; oil(texas); cout << "In main(), texas = " << texas << ", &texas = "; cout << &texas << endl; cout << "In main(), year = " << year << ", &year = "; cout << &year << endl; // cin.get(); return 0; } void oil(int x) { using namespace std; int texas = 5; cout << "In oil(), texas = " << texas << ", &texas = "; cout << &texas << endl; cout << "In oil(), x = " << x << ", &x = "; cout << &x << endl; { // start a block int texas = 113; cout << "In block, texas = " << texas; cout << ", &texas = " << &texas << endl; cout << "In block, x = " << x << ", &x = "; cout << &x << endl; } // end a block cout << "Post-block texas = " << texas; cout << ", &texas = " << &texas << endl; }
这是一个展示了自动变量作用域的示例程序。
在主函数main()
中,定义了两个整型变量texas
和year
,并输出它们的值和内存地址。然后调用了oil()
函数,并将texas
作为参数传递给了oil()
函数。之后再次输出texas
和year
的值和内存地址。
在oil()
函数中,重新定义了一个局部变量texas
,并输出它的值和内存地址,以及参数x
的值和内存地址。接着进入一个代码块,在代码块中定义了另一个局部变量texas
,并输出它的值和内存地址,以及参数x
的值和内存地址。代码块结束后,继续输出外部的局部变量texas
的值和内存地址。
总结一下输出结果:
- 在
main()
函数中,texas
的值是31,内存地址不确定;year
的值是2011,内存地址不确定。 - 在
oil()
函数中,第一个局部变量texas
的值是5,内存地址不确定;参数x
的值是31(来自main()
函数中的texas
),内存地址不确定。 - 在代码块中,局部变量
texas
的值是113,内存地址不确定;参数x
的值是31(来自main()
函数中的texas
),内存地址不确定。 - 代码块结束后,输出外部的局部变量
texas
的值是5,内存地址不确定。
这个程序展示了自动变量在不同作用域中的行为。通过定义具有相同名称但位于不同作用域的变量,可以在不同的代码块中使用不同的值,而且不会相互干扰。
名称空间
名称空间(Namespace)是一种用于组织代码和标识符的机制,在C++中被广泛使用。它可以避免命名冲突,将相关的类、函数、变量等封装到一个逻辑上相关的单元中。
通过使用名称空间,可以将代码划分为不同的逻辑单元,每个单元有自己的名称空间,并可以在全局范围或其他名称空间中定义标识符。这样,即使存在相同名称的标识符,只要它们位于不同的名称空间,就不会发生冲突。
下面是一个简单的示例,演示了如何使用名称空间:
#include <iostream> namespace A { void func() { std::cout << "This is func() in namespace A." << std::endl; } } namespace B { void func() { std::cout << "This is func() in namespace B." << std::endl; } } int main() { A::func(); // 调用A命名空间中的func() B::func(); // 调用B命名空间中的func() return 0; }
在上面的示例中,我们定义了两个名称空间:A
和B
。每个名称空间中都有一个叫做func()
的函数,用于输出不同的消息。在main()
函数中,我们使用::
操作符来指定调用哪个名称空间中的func()
函数。
输出结果为:
This is func() in namespace A. This is func() in namespace B. • 1 • 2
通过使用名称空间,我们可以更好地组织和管理代码,避免命名冲突,并提高代码的可读性和可维护性。在实际开发中,名称空间的使用非常普遍,并且C++标准库和其他第三方库也使用了名称空间来组织它们的功能。
当存在多个名称空间时,我们可以使用using
语句来简化对名称空间中标识符的访问。using
语句可以将特定的标识符引入当前作用域,使我们可以直接使用这些标识符而无需指定命名空间。
下面是一个示例,演示了如何使用using
语句引入名称空间中的标识符:
#include <iostream> namespace A { void func() { std::cout << "This is func() in namespace A." << std::endl; } } namespace B { void func() { std::cout << "This is func() in namespace B." << std::endl; } } int main() { using A::func; // 使用using语句引入A命名空间中的func() func(); // 调用A命名空间中的func() B::func(); // 仍然可以使用限定名称调用B命名空间中的func() return 0; }
在上面的示例中,我们使用using A::func;
语句将名称空间A中的func()
函数引入了main()
函数的作用域。这样,我们就可以直接使用func()
来调用A命名空间中的函数。
输出结果为:
This is func() in namespace A. This is func() in namespace B.
除了引入单个标识符,还可以使用using namespace
语句引入整个名称空间中的所有标识符。但是要注意,使用using namespace
可能导致命名冲突和可读性降低,因此最好只在必要时使用。
#include <iostream> namespace A { void func() { std::cout << "This is func() in namespace A." << std::endl; } } namespace B { void func() { std::cout << "This is func() in namespace B." << std::endl; } } int main() { using namespace A; // 使用using namespace引入整个A命名空间 func(); // 调用A命名空间中的func() B::func(); // 仍然可以使用限定名称调用B命名空间中的func() return 0; }
输出结果与之前示例相同。使用using namespace
语句可以方便地访问一个名称空间中的所有标识符,但需要注意避免名称冲突。
// namesp.h #include <string> // create the pers and debts namespaces namespace pers { struct Person { std::string fname; std::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.h
)中的 pers
和 debts
两个命名空间的定义。
在 pers
命名空间中,定义了一个 Person
结构体和与之相关的函数 getPerson
和 showPerson
。
在 debts
命名空间中,使用了 using namespace pers;
的语句,表示在 debts
命名空间中可以直接访问 pers
命名空间中的标识符。同时,在 debts
命名空间中定义了一个 Debt
结构体和与之相关的函数 getDebt
、showDebt
和 sumDebts
。
这样的设计使得在 debts
命名空间中可以使用 Person
这个结构体,而不需要加上 pers::
的前缀。
下面是一个示例,演示如何使用 namesp.h
头文件中的 pers
和 debts
命名空间:
#include <iostream> #include "namesp.h" int main() { using namespace std; using namespace debts; Person person; getPerson(person); showPerson(person); Debt debt; getDebt(debt); showDebt(debt); Debt debts[] = {debt}; double totalDebt = sumDebts(debts, 1); cout << "Total debt: " << totalDebt << endl; return 0; }
在上面的示例中,我们使用 using namespace
语句引入了 debts
命名空间,以及标准库的 std
命名空间。然后,我们可以直接使用 Person
结构体、getPerson
和 showPerson
函数,无需加上命名空间前缀。
注意,在编译时,要确保 namesp.h
头文件在正确的位置,并且编译器能够找到它