C++ Primer Plus 第6版 读书笔记(9)第 9章 函数——内存模型和名称空间

本文涉及的产品
云原生数据库 PolarDB 分布式版,标准版 2核8GB
简介: C++ Primer Plus 第6版 读书笔记(9)第 9章 函数——内存模型和名称空间

内存模型和名称空间

内存模型和名称空间是计算机科学中两个重要的概念。

内存模型是指计算机系统在执行程序时,将程序的数据和指令存储在主存储器中的方式。它定义了程序如何访问和操作内存中的数据。常见的内存模型包括单一内存模型(如单线程),共享内存模型(如多线程),分布式内存模型(如分布式系统)。不同的内存模型决定了程序的并发性、可见性和数据一致性等方面的行为。

名称空间是指标识符(如变量、函数、类等)在程序中的可见范围。它用于解决命名冲突的问题,即同一程序中不同部分使用相同的标识符时可能导致的混淆。通过使用不同的名称空间,可以在程序中定义具有相同名称但作用域不同的标识符。例如,在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

这个头文件的目的是提供结构模板和函数原型,以便其他源代码文件可以使用它们而不需要重新定义。下面是对每个部分的详细分析:

  1. 条件编译指令:
  • #ifndef COORDIN_H_#define COORDIN_H_ 这两个指令一起用来创建一个预处理器变量,以确保头文件的内容只被编译一次。如果 COORDIN_H_ 这个预处理器变量尚未定义,则执行下面的代码;否则,跳过代码块。
  • #endif 指令用于结束条件编译块。
  1. 结构模板:
  • struct polar 定义了一个极坐标结构,具有两个成员变量 distanceangle,分别表示距离原点的距离和与原点的方向。
  • struct rect 定义了一个直角坐标结构,具有两个成员变量 xy,分别表示与原点的水平和垂直距离。
  1. 函数原型:
  • polar rect_to_polar(rect xypos) 是一个函数原型,用于将直角坐标转换为极坐标。它接受一个 rect 类型的参数 xypos,返回一个 polar 结构。
  • void show_polar(polar dapos) 是一个函数原型,用于显示极坐标的值。它接受一个 polar 类型的参数 dapos,不返回任何值(void)。

通过包含这个头文件,其他源代码文件可以访问 polarrect 结构,并使用 rect_to_polarshow_polar 函数,而无需重新定义结构或函数原型。

这种组织方式使得代码更模块化和可复用。可以将该头文件作为一个软件包,供多个源代码文件使用。如果需要修改结构定义或函数原型,只需在头文件中修改一次即可,所有包含该头文件的源代码文件都会自动获得更新。这样可以提高代码的可维护性和开发效率。

9.2 存储持续性、作用域和链接性

9.2 存储持续性、作用域和链接性是关于变量在程序中的生命周期、可见性和访问性的概念。下面对这些概念进行详细解释:

  1. 存储持续性(Storage Duration):
  • 存储持续性指的是变量在内存中的存在时间。
  • 根据存储持续性,变量可以分为三种类型:自动变量(automatic)、静态变量(static)和动态分配变量(dynamic)。
  • 自动变量:也称为局部变量,其存在于函数或代码块的作用域内,并通过栈来分配和释放内存。它们的存储持续性与所属的作用域一致。
  • 静态变量:在程序执行期间都存在,在函数或代码块的作用域之外定义,在全局数据区分配内存。具有静态存储持续性。
  • 动态分配变量:使用特殊的函数(如malloc())在堆上分配内存,可以手动控制其存储持续性。
  1. 作用域(Scope):
  • 作用域指的是变量的可见性范围,即在哪些部分的代码中可以访问到该变量。
  • 分为局部作用域和全局作用域。局部作用域是变量在函数或代码块内可见,全局作用域是变量在整个程序中可见。
  • 在C语言中,可以使用代码块来创建局部作用域。
  1. 链接性(Linkage):
  • 链接性指的是同一标识符在不同文件之间的链接关系,即是否可以在多个文件中共享标识符。
  • 分为外部链接性、内部链接性和无链接性。
  • 外部链接性:可以在多个源文件之间共享的标识符。可以通过在一个文件中声明,然后在其他文件中使用。
  • 内部链接性:在单个源文件内部共享的标识符。只能在定义该标识符的文件中使用。
  • 无链接性:只在当前作用域内可见的标识符,无法在其他文件或作用域中使用。
    当然,我可以继续解释有关存储持续性、作用域和链接性的其他方面。
  1. 存储类别说明符(Storage Class Specifiers):
  • 在C语言中,可以使用存储类别说明符来控制变量的存储持续性和链接性。常用的存储类别说明符包括:
  • auto:默认的存储类别说明符,用于定义自动变量。
  • static:用于定义静态变量,具有静态存储持续性。
  • extern:用于声明具有外部链接性的变量或函数。在一个文件中声明,然后在其他文件中使用。
  • register:建议编译器将变量存储在寄存器中,以便快速访问。并非所有变量都可以被分配到寄存器中。
  1. 作用域规则:
  • 在C语言中,变量的作用域由其声明的位置决定。
  • 局部变量的作用域限定为声明它们的代码块内部。即使存在同名的全局变量,局部变量也会屏蔽全局变量。
  • 全局变量的作用域从其声明的位置开始一直延伸到整个程序的末尾。在不同的文件中可以通过外部链接性进行共享。
  1. 链接性规则:
  • 在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()中,定义了两个整型变量texasyear,并输出它们的值和内存地址。然后调用了oil()函数,并将texas作为参数传递给了oil()函数。之后再次输出texasyear的值和内存地址。

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;
}

在上面的示例中,我们定义了两个名称空间:AB。每个名称空间中都有一个叫做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)中的 persdebts 两个命名空间的定义。

pers 命名空间中,定义了一个 Person 结构体和与之相关的函数 getPersonshowPerson

debts 命名空间中,使用了 using namespace pers; 的语句,表示在 debts 命名空间中可以直接访问 pers 命名空间中的标识符。同时,在 debts 命名空间中定义了一个 Debt 结构体和与之相关的函数 getDebtshowDebtsumDebts

这样的设计使得在 debts 命名空间中可以使用 Person 这个结构体,而不需要加上 pers:: 的前缀。

下面是一个示例,演示如何使用 namesp.h 头文件中的 persdebts 命名空间:

#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 结构体、getPersonshowPerson 函数,无需加上命名空间前缀。

注意,在编译时,要确保 namesp.h 头文件在正确的位置,并且编译器能够找到它

相关实践学习
Polardb-x 弹性伸缩实验
本实验主要介绍如何对PolarDB-X进行手动收缩扩容,了解PolarDB-X 中各个节点的含义,以及如何对不同配置的PolarDB-x 进行压测。
相关文章
|
9天前
|
存储 C语言 C++
C++:C/C++内存管理
C++:C/C++内存管理
22 0
|
11天前
|
Rust 安全 程序员
Rust与C++:内存管理与安全性的比较
本文将对Rust和C++两种编程语言在内存管理和安全性方面进行深入比较。我们将探讨Rust如何通过其独特的所有权系统和生命周期管理来消除内存泄漏和悬挂指针等常见问题,并对比C++在这方面的挑战。此外,我们还将讨论Rust的类型系统和编译器如何在编译时捕获许多常见的运行时错误,从而提高代码的安全性。
|
3天前
|
存储 C语言 C++
【c++】C/C++内存管理
【c++】C/C++内存管理
【c++】C/C++内存管理
|
4天前
|
存储 编译器 程序员
近4w字吐血整理!只要你认真看完【C++编程核心知识】分分钟吊打面试官(包含:内存、函数、引用、类与对象、文件操作)
近4w字吐血整理!只要你认真看完【C++编程核心知识】分分钟吊打面试官(包含:内存、函数、引用、类与对象、文件操作)
|
4天前
|
程序员 编译器 C++
C++核心编程一:内存分区模型(持续更新)
C++核心编程一:内存分区模型(持续更新)
|
9天前
|
存储 编译器 C语言
C语言:内存函数
C语言:内存函数
|
24天前
|
安全 C++ 开发者
c++动态内存管理(二)
c++动态内存管理(二)
75 0
|
24天前
|
存储 安全 算法
c++动态内存管理(一)
C++ 动态内存管理 在 C++ 中,动态内存管理是一个核心概念,它允许在运行时分配和释放内存。以下是 C++ 动态内存管理需要掌握的关键知识点:
96 0
|
2月前
|
存储 程序员 编译器
C/C++程序内存区域划分以及各区域的介绍
C/C++程序内存区域划分以及各区域的介绍
|
3天前
|
C++
【c++】模板---类模板
【c++】模板---类模板