cplus6_第9章_内存模型和名称空间

内存模型和名称空间

本文内容包括:

  1. 单独编译;
  2. 存储持续性、作用域和链接性
  3. 定位 new运算符
  4. 名称空间

1、单独编译

一般程序的组织方式如下:

  • **头文件:**包含结构或类或模板的声明、符号常量(#define或const)声明、函数原型、内联函数;
  • 源代码文件1:包含函数定义代码。
  • 源代码文件2:包含调用函数的业务逻辑。

如果编写另一个程序时也用到了这些函数,只需要包含头文件,并将函数的定义文件添加到项目列表或make列表中即可。

在IDE中,不要将头文件加入到项目列表中,也不要在源代码文件中使用#include来包含其它源代码文件。

1.1头文件编写方式

为了避免头文件被重复包含,一般会采取如下方法来进行头文件的编写:

#ifndef XXXX_H_	
#define XXXX_H_
...		//头文件正文
#endif

1.2 Unix系统中编译由多个文件组成的C++程序

image-20210403195227321

1.3 编译器的链接

不同的编译器在编译时为函数生成的修饰名称可能不一样,而名称的不同将使链接器无法将一个编译器生成的函数调用与另一个编译器生成的函数定义匹配。因此,在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的。

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

C++有4种不同的方案来存储各种类型的变量:

image-20210403195626753

**持续性:**描述名称的生存周期。

**作用域:**描述名称在文件的多大范围内可见;

**链接性:**描述名称如何在不同的单元间共享。链接性为外部的名称可在文件中共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为它们不共享。

不同的C++存储方式正是通过存储持续性、作用域和链接性来描述的!

2.1 自动存储持续性

  • 在函数中声明的函数参数和变量的持续性是自动的,作用域为局部,没有链接性。自动变量只在包含它们的代码块或函数中可见。(嵌套代码块含有相同名称的变量时,里层将隐藏外层的同名变量。)
  • 关键字auto在C++11之前及在C语言中,用于显示的指出变量为自动存储类型(局部变量),但由于其用的很少,所以在C++11中,将其用于自动类型推断
  • 程序分配栈(LIFO)空间存储自动变量。
  • 自动变量的生存期仅为在代码块执行或函数被调用时。

2.2 静态存储持续性

  • C++也为静态变量提供了3种链接性:外部链接(可在其它文件中访问)、内部链接(仅在当前文件中访问)、无链接性(尽在当前代码块或函数中访问)。与自动变量不同的是这3种链接性在整个程序运行期间存在。
  • 静态变量存储在固定的内存地址处,并首先都被零初始化,而后编译器看其有无被程序员赋初值,若有则编译器计算该初值(常量、常量表达式、包含函数调用的表达式,前两者在编译时可赋值,后者需待该函数被链接且程序执行时才可赋值)的具体值后,赋值给静态变量,没有被程序员赋值的则继续保持为0。
  • 代码块之外声明的为外部链接,代码块外声明且使用static限定符的是内部链接,代码块内声明且使用static限定符的为无链接性。
  • C++11新增了constexpr关键字,用于创建常量表达式。
int global = 100;	//全局静态变量,外部链接(可在其它文件中使用)。
static int one_file = 50;	//本地静态变量,内部链接(尽在本文件中使用);
int main()
{
    ...
}

void func1(int n)
{
    static int count = 0;	//本地静态变量,无链接性(仅在本函数中使用,但生存期仍为整个程序运行期)。
}

2.3 静态持续性、外部链接性

外部变量的持续性为静态,作用域为整个项目文件。

  • 每个使用外部变量的文件都必须先声明(declaration)它,即使用关键字extern且不进行初始化(因为仅是引用)。
  • 变量只能定义(definition)一次(单定义规则),此时为其分配存储空间,并进行显示初始化或默认初始化为0。需要注意的是,此时既可用extern修饰也可不用extern修饰。即只要被显示初始化的地方肯定是定义而不是引用。
  • 同名的自动变量将隐藏静态变量,所以若要在函数或代码块中使用静态变量,可以先用extern声明它,然后再使用。也可以使用C++11引入的新特性(在同名变量前缀::)来明确使用外部的静态变量。
  • 从数据隔离的角度看应避免滥用全局变量,但在文件间或函数间共享数据时使用全局变量或本地全局变量(具有内部链接属性的静态变量)则尤为方便。此外在表示常量数据时使用全局变量也将带来便利,且可以使用const来防止数据被更改。

2.4 静态持续性、内部链接性

将static限定符用于作用域为整个文件变量时,该变量的链接性将为内部的。

  • 如果两个文件都定义(赋了初值)了同名的全局变量,则编译器会报错。除非其中一个文件用static修饰了该同名变量,此时在该文件中这个变量变成了具有内部链接属性的静态变量,并隐藏了和它同名的位于其它文件的全局变量。所以建议为所有作用域为本文件的全局变量都加上static限定符,这样会避免与其它文件中的同名全局变量相冲突

2.5静态持续性、无链接性

将static限定符用于在代码块中定义的变量,表示该变量虽只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在。因此在两次函数调用之间,静态局部变量的值将保持不变。

如果初始化了静态局部变量,则程序只在第一次运行时进行初始化,以后再调用函数时,将不会像自动变量那样再次被初始化。

2.6 说明符和限定符

在C++中有如下存储说明符:autoregister,static,extern,thread_local,mutable,大部分已经说过。在同一个声明中不能使用多个说明符,但thread_local除外,它可与static或extern结合使用;

  • auto:在C++11之前,可以在说明中使用关键字anto指出变量为自动变量,但在C++11中,auto用于自动类型推断;
  • register:关键字register用于在声明中指示寄存器存储,而在C++中,它只是显示的指出变量是自动的;
  • static:关键字static被用在作用域为整个文件的声明中时表示内部链接,被用于局部声明中时表示存储特性为静态的;
  • extern:关键字extern表面引用声明;
  • thread_local:关键字thread_local指出变量的持续性与其所属线程的持续性相同,thread_local变量之于线程犹如常规静态变量之于整个程序;
  • mutable:关键字mutable的含义将根据const来解释,因此先学习cv-限定符,然后再解释它。

2.6.1 cv-限定符(const、volatile)

const表明其限定的变量所指的内存被初始化后,程序便不能再对它进行修改;

volatile表明编译器不得对其限定的变量所指的地址进行优化,即每次必须从真实地址处取值,而不是寄存器缓存或内存中,常用于指向硬件某个位置。

2.6.2 mutable

它可以指出,即使某结构(或类)变量为const,其某个成员也可以被修改。

struct data
{
    char name[30];
    mutable int age;
    ...
};

const data men = {"leon george", 0, ...};
strcopy(men.name, "George Leon");	//报错!men的const限定符禁止修改men的成员!
men.age--;							//允许,age成员的mutable说明符使得其不受上述限制

2.6.3 const

  • 在默认情况下全局变量的链接性为外部的,但const全局变量的链接性却是内部的。在C++看来就像使用了static说明符一样。
const int x = 10; 等效于 static const int x = 10;
  • 在C++中,预处理器会在展开头文件中的常量前,自动为其添加const限定符,这样展开后,就为每个源文件定义了一个本地全局变量(链接性为内部),否则将违反单定义原则。
  • 但若希望某个常量的链接性为外部的,可以使用extern关键字来覆盖默认的内部链接,即
extern const int age = 18;

此时,必须在所有使用该常量的文件中使用extern关键字来声明它。而定义常规外部变量时不必使用extern,但在使用该变量的其它文件中必须使用extern声明。然而,只有一个文件可对其初始化。

2.6.4 函数的链接性

  • 因为不允许函数定义的嵌套(在一个函数的定义中定义另一个函数),所以函数的存储持续性都是静态的(整个程序允许期间都存在)。
  • 默认情况下,函数的链接性为外部的。实际上也可以在函数原型前加extern关键字指出函数是另一个文件中定义的,但要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译进来或由链接程序搜索某库文件)。
  • 可以在函数原型和函数定义前加static关键字使函数的链接性为内部的,使之只能在一个文件中使用。
  • 在多文件程序中,只能有一个文件包含函数的定义,但使用该函数的每个文件都应该包含其函数原型。
  • 但在使用内联函数时却不需包含其原型,即可以将内联函数的定义放在头文件中,这样包含了头文件的所有文件都有内联函数的定义。
  • C++寻找函数的定义路径顺序:若函数原型为静态,则在当前文件中寻找——>在所有程序文件中搜索——>库文件。所以,若程序定义了和库函数相同的函数,则库函数将被隐藏,但C++不允许用户自定义与库函数同名的函数。

2.6.5 语言的链接性

链接程序要求每个不同的函数都有不同的符号名。在C语言中,因为没有函数重载,所以一个函数对应一个函数名。即C语言的编译器会在函数名前缀下划线的方式为每个函数定义一个函数符号。但在C++中因为函数可重载的原因,同一个函数名称必须有多个不同的符号。例如可能将spiff(int)转换为_spiff_i,而将spiff(double)转换为_spiff_d

当C/C++混合编程时,可以使用函数原型来显示指定要使用的约定:

extern "C" void spiff(int);		//表示在C库文件中寻找
extern "C++" void spiff(int);	//表示在C++库文件中寻找

2.6.6 存储方案和动态分配(new、delete)

编译器使用三块独立的内存用于为变量分配内存:一是用于静态变量(data段和bss段);二是用于自动变量(栈区);三是用于动态存储(堆区)。

由new分配的空间将一直保留在内存,直到使用delete将其释放。但当包含new语句的代码块执行完毕时,如果希望了另一个函数能够使用这块空间,则必须将其地址传递或返回给该函数。

也可以通过声明接收new返回地址变量为外部链接的,并在另一个文件使用extern来引用该地址。

  • new的初始化

    • 内置基本类型标量的初始化:在类型名后加括号并填入初始值:int *ptr = new int (6);
    • 结构体(类)或数组的初始化:在结构(数组)名后加大括号的列表初始化:
    struct where {double x; double y; double z;};
    where * one = new where {2.1, 3.2, 4.3};
    int * ar = new int [4] {1,2,3,4};
    
  • new失败

    • C++11之前,当new失败时返回空指针
    • C++11之后,当new失败时引发异常std::bad_alloc
  • new的函数原型及其替换函数

    • new及new[]分别调用如下分配函数,它们位于全局名称空间中:
    void * operator new(std::size_t);	//new
    void * operator new[](std::size_t);	//new[]
    
    int * pi = new int 将被转化为 int * pi = new(sizeof(int));
    int * pi = new int [4] 将被转化为 int * pi = new(4 * sizeof(int));
    
    • 同样,delete和delete[]调用释放函数:
    void operator delete(void *);	//delete
    void operator delete[](void *);	//delete[]
    
    delete pi; 将被转化为 delete(pi);
    
  • 定位new

    • 通常new在堆(heap)中找到合适的空间用于分配,但也可以让用户自己指定要使用的位置(存储空间)。为了使用定位new,需要首先包含头文件#include <new>,在new后紧跟小括号并填入你希望的空间地址。
    struct chaff{char name[20]; int age;};
    char buffer[100];
    chaff * pt = new (buffer) chaff;	//在buffer数组空间中为结构体分配一个空间。
    //定位new调用一个包含两个参数的函数,即上句等同于chaff * pt = new(sizeof(chaff), buffer);
    
    • 对于定位new分配空间的释放取决于传递给它的地址空间性质。例如若传递给它的是静态空间,则此时用delete释放将会导致错误。
    • 定位new的工作原理是:返回传递给他的地址,并将其强制转化为void *类型,以便可以赋给任意类型指针。
    • 定位new与初始化结合可用于将信息放在特定的硬件地址处。

3、名称空间

在C++中,名称可以是变量、函数、结构、类、枚举等,随着项目的增大,名称相互冲突的可能性也将增加。例如,两个库可能都定义了同名的类或函数等,但定义的方式不兼容,但我又同时需要使用这两个库。

3.1 传统的C++名称空间

  • 声明区域:可在其中进行声明的区域。

image-20210404150443269

  • 潜在作用域和作用域:前者为理论作用域,后者为实际作用域(除却被隐藏的部分)

image-20210404150556244

3.2 新的名称空间特性

一个名称空间中的名称不会域另一个名称空间的相同名称发生冲突,同时允许程序的其它部分使用该名称空间中声明的东西。

3.2.1 名称空间的创建

下面的代码使用关键字namespace创建两个名称空间:

namespace Jack{
    double pail;		//变量声明
    void fetch();		//函数原型
    int pal;			//变量声明
    struct well {...};	//结构声明
}
namespace Jill{
	double bucket(double n) { ... }	//函数定义
    double fetch;					//变量声明
    int pal;						//变量声明
    struct hill { ... };			//结构声明
}
  • 名称空间是全局的,也可以嵌套,但不能位于代码块中。即在名称空间中声明的名称是外部链接的。
  • 除用户自定义的名称空间外,系统默认有一个全局名称空间。即之前描述的全局变量就位于此空间。
  • 任何名称空间中的名称都不会与其它名称空间中的名称发生冲突。
  • 名称空间是开放的,可以随时添加新的名称到已有的名称空间中。例如下面的代码将名称goose添加到已有的Jill名称空间中:
namespace Jill{
	char * goose(const char *);
}
  • 访问给定名称空间中的名称,可以通过作用域解析运算符::
Jack::pail = 12.23;
Jill::hill mole;	//创建一个结构
Jack::fetch();
  • 未被装饰的名称(如pail)称为未限定的名称;包含名称空间的名称(如Jack::pail)称为限定的名称。

3.2.2 using声明和using编译指令

我们不希望每次使用名称时都对它进行限定,因此C++提供了如上两种机制。

  • using声明:由关键字using+被限定的名称组成。例如:using Jill::fetch;

    • 将特定的名称添加到它所属的声明区域中。
    namespace Jill{
    	double bucket(double n) { ... }	//函数定义
        double fetch;					//变量声明
        struct hill { ... };			//结构声明
    }
    char fetch;				//声明一个全局变量
    
    int main()
    {
        using Jill::fetch;	//添加fetch到本地名称空间中
        double fetch;		//error!本地名称空间中已经存在fetch了
        cin >> fetch;		//此处为Jill::fetch,其覆盖了同名的全局fetch
        cin >> ::fetch;		//此处为函数外声明的全局fetch
        ...
    }
    
    • 在函数外使用using声明时,将把名称添加到全局名称空间中。
    void other();
    
    namespace Jill{
    	double bucket(double n) { ... }	//函数定义
        double fetch;					//变量声明
        struct hill { ... };			//结构声明
    }
    using Jill::fetch;				//将fetch添加到全局声明中
    
    int main()
    {
        cin >> fetch;		//此处为Jill::fetch
        other();
        ...
    }
    
    void other()
    {
        cout << fetch;		//此处也为Jill::fetch
    }
    
  • using编译指令

如果说using声明使一个名称可用,那么using编译指令使名称空间中的所有名称可用。由关键字using namespace+名称空间名组成。例如:using namespace Jack;

在全局声明区域中使用using编译指令,使该名称空间中所有名称全局可用;在函数中使用using编译指令,使其中的名称在该函数中可用。

  • 二者的比较

    • 如果某个名称已经在函数中声明了,则不能使用using声明导入相同的名称。然而使用using编译指令却是合法的,只不过该同名名称将被原局部名称所隐藏。
    namespace Jill{
    	double bucket(double n) { ... }	//函数定义
        double fetch;					//变量声明
        struct hill { ... };			//结构声明
    }
    char fetch;							//全局变量
    int main()
    {
        using namespace Jill;
        hill thrill;
        double water = bucket(2);
        double fetch;					//新的局部变量,隐藏了Jill::fetch
        cin >> fetch;					//局部变量fetch
        cin >> ::fetch;					//全局变量fetch
        cin >> Jill::fetch;				//Jill中的fetch
    }
    
    int foom()
    {
        hill top;			//错误!未使用using声明或编译
        Jill::hill crest;	//合法
    }
    

    因为using编译指令会将名称空间中的所有名称都添加进来,而且即使有和程序中声明的名称相同,编译器也不会报错,而只是默默隐藏起来,所以为了安全,还是建议在必要的时候使用using声明。

    int x;
    std::cin >> x;
    std::out << x << std::endl;
    
    或者
        
    using std::cin;
    using std::cout;
    using std::endl;
    int x;
    cin >> x;
    cout << x <<endl;
    

3.2.3 名称空间的其它特性

  • 名称空间的嵌套
namespace elements
{
	namespace fire
	{
		int flame;
		...
	}
	float water;
}

此时名称的引用也是嵌套的:using namespace elememts::fire或者elements::fire::flame

  • 名称空间的另一种嵌套
namespace myth
{
	using Jill::fetch;
	using namespace elements;
	using std::cout;
	using std::cin;
}

假设要访问fetch,但它现在已经属于myth了,所以这样引用:myth::fetch。当然由于它也位于Jill名称空间中,因此也可以称作Jill::fetch。当函数中没有局部变量与之冲突的化,也可以直接引用fetch。

因为using编译指令是可传递的,且myth名称空间包含了elements名称空间,所以当你使用using namesace myth后,elements空间也将被导入。

  • 给名称空间创建别名

假设有名称空间namespace my_very_favorate_things { ... };,但为了便于书写可以为其创建别名如下:namespace mvft = my_very_favorate_things;

也可以使用该技术简化嵌套名称空间的使用:

namespace MEF = myth::elements::fire;
using MEF::flame;

3.21.4 未命名的名称空间

namespace
{
	int ice;
	int bandycoot;
}

因为其么有名称,故无法使用using关键字,所以不能在未命名名称空间所属文件之外的其它文件中使用该名称空间中的名称。这提供了链接性为内部静态变量的替代品。

3.3 名称空间组织方式

  • 头文件中存放名称空间,其中包含的内容为常量、结构定义、函数原型等。下面为头文件names.h的代码
#include <string>
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);
}
  • 源代码文件一:提供了头文件中函数原型对应的定义。且在其文件开头需使用#include “names.h”包含头文件,进而引入原来的名称空间。并在文件正文将函数定义添加进名称空间。下面是代码names.cpp:
#include <iostream>
#include "names.h"

namespace pers
{
    using std::cout;
    using std::cin;
    
    void getPerson(Person & rp)
    {
        ...
    }
    
    void showPerson(const Person & rp)
    {
        ...
    }
}

namespace debts
{
    void getDebt(Debt & rd)
    {
        ...
    }
    
    void showDebt(const Debt & rd)
    {
        ...
    }
    
    double sunDebts(const Debt ar[], int n)
    {
        ...
    }
}
  • 源代码文件二:业务逻辑,使用名称空间中定义的结构和函数。
#include <iostream>
#include "names.h"

void other(void);
void another(void);

int main(void)
{
	using debts::Debt;
    using debts::showDebt;
    Debt golf = {{"leon", "george"}, 120.2};
    showDebt(golf);
    other();
    return 0;
}

void other(void)
{
    using std::cout;
    using std::endl;
    using namespace debts;
    ...
}

void another(void)
{
    using pers::Person;
    Person collector = {"milo", "shru"};
    pers::showPerson(collector);
    ...
}

3.4 名称空间及其用途

image-20210404172155711

使用名称空间的目的是简化大型编程项目的管理工作。

de “names.h”

void other(void);
void another(void);

int main(void)
{
using debts::Debt;
using debts::showDebt;
Debt golf = {{“leon”, “george”}, 120.2};
showDebt(golf);
other();
return 0;
}

void other(void)
{
using std::cout;
using std::endl;
using namespace debts;

}

void another(void)
{
using pers::Person;
Person collector = {“milo”, “shru”};
pers::showPerson(collector);

}


### 3.4 名称空间及其用途

[外链图片转存中...(img-GxzUHhJD-1617528793035)]

使用名称空间的目的是简化大型编程项目的管理工作。

老式头文件(如iostream.h)没有使用名称空间,但新头文件iostream使用了std名称空间。
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 黑客帝国 设计师:白松林 返回首页