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 进行压测。
目录
相关文章
|
1月前
|
存储 Java 编译器
C++:内存管理|new和delete
C++:内存管理|new和delete
|
1月前
|
存储 算法 编译器
【C++ 内存管理 重载new/delete 运算符 新特性】深入探索C++14 新的/删除的省略(new/delete elision)的原理与应用
【C++ 内存管理 重载new/delete 运算符 新特性】深入探索C++14 新的/删除的省略(new/delete elision)的原理与应用
46 0
|
3天前
|
存储 人工智能 程序员
【重学C++】【内存】关于C++内存分区,你可能忽视的那些细节
【重学C++】【内存】关于C++内存分区,你可能忽视的那些细节
35 1
|
30天前
|
存储 Linux C语言
【C++练级之路】【Lv.5】动态内存管理(都2023年了,不会有人还不知道new吧?)
【C++练级之路】【Lv.5】动态内存管理(都2023年了,不会有人还不知道new吧?)
|
30天前
|
安全 程序员 C++
【C++ 基本知识】现代C++内存管理:探究std::make_系列函数的力量
【C++ 基本知识】现代C++内存管理:探究std::make_系列函数的力量
101 0
|
1月前
|
存储 Linux 程序员
【Linux C/C++ 堆内存分布】深入理解Linux进程的堆空间管理
【Linux C/C++ 堆内存分布】深入理解Linux进程的堆空间管理
76 0
|
1月前
|
算法 Java C++
【C/C++ 内存知识扩展】内存不足的可能性分析
【C/C++ 内存知识扩展】内存不足的可能性分析
12 0
|
1月前
|
存储 算法 Linux
深入理解Linux内存管理brk 和 sbrk 与以及使用C++ list实现内存分配器
深入理解Linux内存管理brk 和 sbrk 与以及使用C++ list实现内存分配器
36 0
|
1月前
|
缓存 Linux iOS开发
【C/C++ 集成内存调试、内存泄漏检测和性能分析的工具 Valgrind 】Linux 下 Valgrind 工具的全面使用指南
【C/C++ 集成内存调试、内存泄漏检测和性能分析的工具 Valgrind 】Linux 下 Valgrind 工具的全面使用指南
65 1
|
1月前
|
存储 JSON 监控
Higress Controller**不是将配置信息推送到Istio的内存存储里面的**。
【2月更文挑战第30天】Higress Controller**不是将配置信息推送到Istio的内存存储里面的**。
14 1

热门文章

最新文章