内存模型和名称空间是计算机科学中两个重要的概念。
内存模型是指计算机系统在执行程序时,将程序的数据和指令存储在主存储器中的方式。它定义了程序如何访问和操作内存中的数据。常见的内存模型包括单一内存模型(如单线程),共享内存模型(如多线程),分布式内存模型(如分布式系统)。不同的内存模型决定了程序的并发性、可见性和数据一致性等方面的行为。
名称空间是指标识符(如变量、函数、类等)在程序中的可见范围。它用于解决命名冲突的问题,即同一程序中不同部分使用相同的标识符时可能导致的混淆。通过使用不同的名称空间,可以在程序中定义具有相同名称但作用域不同的标识符。例如,在C++中,可以使用命名空间来区分不同的库或模块中的标识符,避免命名冲突。
总结来说,内存模型关注程序如何使用和访问内存,而名称空间关注如何解决标识符的命名冲突。它们都是编程中重要的概念,对于理解程序的执行和组织具有重要意义。
在C++中,内存模型和名称空间也是非常重要的概念。
C++的内存模型是基于共享内存的多线程模型。它允许在多个线程之间共享数据,并通过同步机制(如互斥锁、条件变量等)来确保数据的正确性和一致性。C++提供了多种线程库(如std::thread、std::mutex等),以及原子类型和操作(如std::atomic),用于实现多线程编程。
名称空间在C++中用于解决标识符的命名冲突问题。C++中的名称空间由关键字namespace定义,可以将标识符分组到不同的名称空间中。这样,即使不同的名称空间中有相同的标识符,也不会引起冲突。使用名称空间可以有效地组织代码,并提供更好的可读性和可维护性。同时,C++标准库中的类、函数等也位于std名称空间中。
除了内存模型和名称空间,C++还有其他一些重要的概念,如类、对象、继承、多态等。这些概念构成了C++面向对象编程的基础,使得C++成为一种强大而灵活的编程语言。
和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",而不是
图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 存储持续性、作用域和链接性是关于变量在程序中的生命周期、可见性和访问性的概念。下面对这些概念进行详细解释:
存储持续性(Storage Duration):
作用域(Scope):
链接性(Linkage):
存储类别说明符(Storage Class Specifiers):
auto
:默认的存储类别说明符,用于定义自动变量。static
:用于定义静态变量,具有静态存储持续性。extern
:用于声明具有外部链接性的变量或函数。在一个文件中声明,然后在其他文件中使用。register
:建议编译器将变量存储在寄存器中,以便快速访问。并非所有变量都可以被分配到寄存器中。作用域规则:
链接性规则:
static
关键字可以将全局变量或函数的链接性修改为内部链接性。这样它们只能在定义它们的文件内部使用,无法在其他文件中访问。通过理解存储持续性、作用域和链接性的概念,开发者可以更好地管理变量并确保它们在程序中的正确使用。这有助于避免命名冲突、提高代码的可读性和可维护性,并确保变量的生命周期和可见性符合预期。
// autoscp.cpp -- illustrating scope of automatic variables
#include
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
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.
通过使用名称空间,我们可以更好地组织和管理代码,避免命名冲突,并提高代码的可读性和可维护性。在实际开发中,名称空间的使用非常普遍,并且C++标准库和其他第三方库也使用了名称空间来组织它们的功能。
当存在多个名称空间时,我们可以使用using
语句来简化对名称空间中标识符的访问。using
语句可以将特定的标识符引入当前作用域,使我们可以直接使用这些标识符而无需指定命名空间。
下面是一个示例,演示了如何使用using
语句引入名称空间中的标识符:
#include
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
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
// 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
#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
头文件在正确的位置,并且编译器能够找到它。