• 第9章、内存模型和名称空间


    C++名称空间是一种控制访问权的方式。

    9.1、单独编译

    #ifndef指令可以保证同一个文件中只能将同一个头文件包含一次。

    在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的。如果有源代码,通常可以用自己的编译器重新编译源代码来消除链接错误。

    头文件中常包含的内容:
    1、函数原型
    2、使用#define或const定义的符号常量
    3、结构声明
    4、类声明
    5、模板声明
    6、内联函数

    头文件管理

    在同一个文件中只能将同一个头文件包含一次,但是有时候在不知情的情况下将头文件包含多次。C++有一种技术可以避免多次包含同一个头文件。它是基于预处理器编译指令#ifndef(即 if not defined)的。下面的代码段意味着仅当以前没有使用预处理器编译指令#define定义名称DIALOG_H时,才处理#ifndef和endif之间的语句:

    #ifndef DIALOG_H
    #define DIALOG_H
    
    ...
    
    #endif // DIALOG_H
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    多个库的链接

    C++标准允许每个编译器设计人员以他认为合适的方式实现名称修饰,因此由不同编译器创建的二进制模块很可能无法正确地链接。在链 接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的。如果有源码,通常用自己的编译器重新编译源代码来消除链接错误。

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

    C++使用三种(C++11有四种)不同方案来存储数据,这些方案的区别就在于数据保留在内存中的时间。

    1、自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或者代码块时,它们使用的内存被释放。C++有两种存储持续性为自动的变量(自动、寄存器)

    2、静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。C++有三种存储持续性为静态的变量。

    3、线程存储持续性:如果变量时使用关键字thread_local声明的,则其生命周期与所属的线程一样长。

    4、动态存储持续性:用New运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。

    作用域和链接

    作用域描述了名称在文件的多大范围内可见。链接性描述了名称如何在不同单元间共享。链接性为外部的名称可以在文件间共享,链接性为内部的名称可以在一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享。

    C++变量的作用域有多种。作用域为局部的变量只在定义它的代码块中可用。代码块使用花括号括起的一系列语句。作用域为全局(文件作用域)的变量在定义位置到文件结尾之间都可以用。自动变量的作用域为局部,静态变量的作用域是全局还是局部取决于它是如何被定义的。在函数原型作用域中使用的名称只在包含只在包含参数列表的括号内可用。在类中声明的成员的作用域为整个类。在名称空间中声明的变量的作用域为整个名称空间。
    C++函数的作用域可以是整个类或整个名称空间(包括全局的),但不能是局部的。

    自动存储持续性

    在默认情况下,在函数声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。当程序开始执行这些变量所属的代码块时,将为其分配内存;当函数结束时,这些变量都将消失。
    如果在代码块中定义了变量,则该变量的存在时间和作用域将被限制在代码块内。
    在这里插入图片描述
    如果将内部代码块的websight命名为teledeli,使得有两个同名变量。
    新的定义隐藏了旧的定义,新定义可见,旧的定义暂时不可见,当程序离开内部代码块,回到外部代码块,旧的定义重新可见。
    在这里插入图片描述

    C++ 11 的auto

    auto关键字用于自动类型推断

    1、自动变量的初始化

    可以使用任何在声明时其值为已知的表达式来初始化自动变量。

    int w;//w的值不确定的
    int x=5;//利用数字初始化
    int big=INT_MAX-1;//利用常量表达式初始化
    int y=w*2;
    cin>>w;
    int z=3*w;//使用w的新值;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2、自动变量和栈

    由于自动变量的数目随着函数的开始和结束而增减,因此程序必须在运行时对自动变量进行管理。常用的方法就是留出一段内存,并将其视为栈,以管理变量的增减。之所以被称为栈,是由于新数据被象征性地放在原有数据的上面(也就是说,是在相邻的内存单元中,而不是在同一个内存单元中),当程序使用完之后,将其从栈中删除。栈的默认长度取决于实现,但编译器通常提供改变栈长度的选项。程序使用两个指针来跟踪栈,一个指针指向栈底——栈的开始位置,另一个指针指向堆顶——下一个可用的内存单元。当函数被调用时,其自动变量将被加入到栈中,栈顶指针指向变量后面的下一个可用的内存单元。函数结束时,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。

    栈是LIFO(先进后出)的,即最后加入到栈中的变量首先被弹出。
    在这里插入图片描述

    3、寄存器变量

    关键字register最初是C语言引入的,它建议编译器使用CPU寄存器来存储自动变量。

    register int count_fast;//申请一个寄存器变量
    
    • 1

    这旨在提高访问变量的速度。
    C++11之后,这种提示作用失效了,关键字register只是显式地指出变量是自动的。鉴于关键字register只能用于原本就是自动的变量,使用它的唯一原因是,指出程序员想使用一个自动变量,这个变量名称可能与外部变量相同。

    9.2.3 静态持续变量

    C++为静态存储持续性变量提供三种链接性:外部链接性(可以在其他文件中访问)、内部链接性(只能当前文件访问)和无链接性(只能在当前函数或代码块中访问)。如果没有显式地初始化静态变量,编译器将把它设置为0,在默认情况下,静态数组和结构将每个元素或成员的所有位置都设置为0。

    想要创建链接性为外部的静态持续变量,必须在代码块的外部声明它;要创建链接性为内部的静态持续变量,必须在代码块的外面声明它;并使用static限定符;要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static限定符。例子如下:

    int global=1000;//静态持续变量,链接性为外部,可以在程序的其他文件中使用
    static int one_file=50;//静态持续变量,链接性为内部,只能在当前文件中使用
    int main(){
    ...
    }
    void func1(int n){
    	static int count =0;//静态持续变量 作用域局部,没有链接性,意味着只能在本函数使用,与llama不同的是,count在函数没有被执行时也留在内存中
    	int llama=0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    所有的静态持续变量都有这种特征:未被初始化的静态变量的所有位都被设置为0。这种变量被称为零初始化的。
    在这里插入图片描述

    静态变量的初始化chlbd

    除了默认的零初始化外,还可以对静态变量进行常量表达式初始化和动态初始化。零初始化意味着将变量设置为零。对于标量类型,零将被强制转换为合适的类型。例如,C++代码中,空指针用0表示,但内部可能采用非零表示,因此指针变量将被初始化相应的内部表示。结构成员被零初始化,且填充位都被设置为零。

    零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译器处理文件(翻译单元)时初始化变量。动态初始化意味着变量将在编译后初始化。

    #include<cmath>
    int x;//零初始化
    int y=5;//常量表达式初始化
    long z=13*13;//常量表达式初始化
    const double pi=4.0*atan(1.0);//动态初始化 这里调用atan()函数,需要等到该函数被链接且程序执行时。
    //常量表达式 并非只能是字面常量的算术表达式,例如它还可以使用sizeof 运算符
    int encough= 2* sizeof(long)+1;//常量表达式初始化
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    链接性为外部的变量通常简称为外部变量,它们的存储持续性为静态,作用域为整个文件。外部变量也称为全局变量(相当于局部的自动变量)。

    1、单定义规则

    C++的单定义规则是:变量只能有一次定义。
    为了满足单定义规则,C++提供两种变量声明。一种是定义声明(简称定义),它给变量分配存储空间;另一种是引用声明简称声明,它不给变量分配存储空间,因为它引用已有的变量。

    引用声明使用关键字extern,且不进行初始化;否则,声明为定义,导致分配存储空间。

    double up;//定义,up是0
    extern int blem;//blem在其他地方被定义了,这里是声明
    extern char gr='z';//因为这里是初始化,所以是定义
    
    • 1
    • 2
    • 3

    如果要在多个文件中使用外部变量,只需要在一个文件中包含该变量的定义(单定义规则),但在使用该变量的其他所有文件中,都必须使用extern声明它:
    在这里插入图片描述
    单定义规则并非意味着不能有多个变量的名称相同。例如,在不同函数中声明的同名自动变量是彼此独立的,它们都有自己的地址。局部变量可能隐藏同名的全局变量。虽然程序中可以包含多个同名变量,但每个变量都只有一个定义。

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

    链接性为内部的变量只能在其所属的文件中使用;但常规外部变量都具有外部链接性,即是可以在其他文件中使用。

    如果一个文件中定义了一个静态外部变量,其名称与另一个文件中声明的常规外部变量相同,则在该文件中,静态变量将隐藏常规外部变量:

    //file1
    int errors=20;//常规外部变量
    int main(){
    	return 0;
    }
    
    //file2
    static int errors=5;//只在file2 中可见
    void func(){
    	cout<<errors;//输出5
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    9.2.6 静态存储持续性、无链接性

    无链接性的局部变量。创建方式:将限定符static用于在代码块中的变量,该局部变量是静态的存储持续性,变量只在代码块中可用,但它在该代码块不处于活动状态时仍然存在。如果初始化了静态局部变量,则程序只需要在启动时初始化,后面调用时,将不像自动变量再次被初始化。

    9.2.7 说明符和限定符

    auto 在C++11中用于自动类型推断。关键字register用于声明中指示寄存器存储,而在C++11中,它只是显式地指出变量是自动的。关键字extern表示是引用声明,即声明引用在其他地方定义的变量。关键字thread_local指出变量的持续性于其所属线程持续性相同thread_local变量之于线程,如常规静态变量之于整个程序。

    关键字volatile表明,即使程序代码没有对内存单元进行修改,其值有可能发生变化。将变量声明为volatile,相当于告诉编译器,不要进行优化,防止编译器进行某种优化。

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

    struct data{
    	char name[30];
    	mutable int access;
    };
    const data veep={"name",2};
    strcpy(veep.name,"joy");//不允许 
    veep.access++;//允许
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    veep的const限定符禁止程序修改veep的成员,但是access成员的mutable说明符使得access不受这种限制。

    const限定符对默认存储类型稍有影响。在默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的。在C++看来,全局const定义就像static说明符一样。

    const int fingers=10;//等同于 static const int fingers=10;
    int main(void){
    
    }
    
    • 1
    • 2
    • 3
    • 4

    只有一个文件可以声明fin,其他文件必须使用extern关键字提供引用声明,另外只有未使用extern关键字的声明才能进行初始化。
    在这里插入图片描述
    main.cpp

    #include <iostream>
    #include"file.h"
    using namespace std;
    const int fin=10;
    int main()
    {
    
    //    cout << fin << endl;
        func();
        system("pause");
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    file.h

    #ifndef FILE_H
    #define FILE_H
    #include<iostream>
    using namespace std;
    extern const int fin;//不能被初始化,此处只能声明
    void func(){
        cout<<fin;
    }
    
    #endif // FILE_H
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    另外
    如果希望某个常量的链接性为外部,则可以使用extern关键字来覆盖默认的内部链接性

    extern const int status=50;//定义,外部链接性
    
    • 1

    在这种情况下,必须在所有使用该常量的文件中使用extern关键字来声明它。

    9.2.8 函数和链接性

    可以使用static关键字将函数的链接性设置为内部,使之只能在一个文件中使用。必须同时在原型和函数定义中使用该关键字:

    static int func();
    
    static int func(){
    ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这意味着该函数只在这个文件中可见,还意味着可以在其他文件中定义同名函数。

    内联函数不受单定义规则约束,允许程序员能够将内联函数的定义放在头文件中。这样包含了头文件的每个文件都有内联函数的定义。然而,C++要求同一个函数的所有内联定义都必须相同。

    如果程序在某个文件调用一个函数,C++将到哪里寻找函数定义呢?如果该文件中的函数原型 指出该函数是静态的,则编译器将只在该文件中查找函数定义;否则,编译器将在所有程序文件中查找。

    9.2.9 语言链接性

    想要在C++调用C库中预编译的函数。

    void func();//C库函数
    
    extern “C”  void func();//使用C协议查找
    extern  void func();//默认方式C++语言链接性
    extern “C++void func();//显式 C++语言链接性
    
    • 1
    • 2
    • 3
    • 4
    • 5

    存储方案和动态分配

    使用C++ 运算符new分配内存称为动态内存。动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。通常编译器使用三块独立的内存:一块用于静态变量、一块用于自动变量、一块用于动态存储。
    在程序结束时,由new分配的内存通常被释放,不过有例外。最好方法是使用delete释放内存。

    1.使用new运算符初始化

    int *pi=new int(6);//*pi 设置6  内置类型(int float )分配内存空间 小括号
    
    struct where {int x;int y;};// C++11 初始化结构体 或数组
    where *one =new where{2,3};
    int *arr=new int[4]{1,2,3,4};
    
    int *pin= new int {6};//C++ 11 *pin 设置6  列表初始化的方式用于单值变量 花括号
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    new失败时
    C++11会发出std::bad_alloc异常

    4、定位new运算符
    new负责在堆中找到一个足以能够满足要求的内存块。new运算符还有一种变体,称为定位new运算符,它让你能够指定要使用的位置。程序员可能使用这种特性来设置其内存管理规程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。

    #include<new>
    struct chaff{
    	char dross[20];
    	int slag;
    };
    char buf1[50];
    char buf2[100];
    int main(){
    	chaff *p1,*p2;
    	int *p3,*p4;
    	p1=new chaff;//在堆 定位结构体
    	p3=new int[20];//在堆内存,定位int 数组
        p2=new(buf1) chaff;//在buf1的内存,定位结构体
        p4=new(buf2) int[20];//在buf2的内存,定位Int 数组 buf2分配20个元素空间给p4
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    char buf[512];
    double *pd1,*pd2;
    pd1=new double[5];
    pd2=new (buf)double[5];
    for(int i=0;i<5;++i)
    	pd2[i]=pd1[i]=0.1*i+2*i;
    
    cout <<(void *)buf;
    pd1是double指针,buf是char指针,void *对buf强制转换,得到地址。如果不使用void *,cout输出一个字符串。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    delete只能指向常规New分配的堆内存。

    定位new运算符只是返回传递给它们的地址,并将其强制类型转换为void *,以便能够赋给任何指针类型。

    定位new的其他形式

    就像常规new调用一个接收一个参数的new()函数一样,标准定位new调用一个接收两个参数的new()函数

    int *p1=new int;//请求 new(sizeof(int))
    int *p2=new(buf) int;//请求 new(sizeof(int),buf)
    int *p3=new(buf) int[40];//请求 new(40*sizeof(int),buf)
    
    • 1
    • 2
    • 3

    定位new函数不可替换,但是可以重载,它至少需要接收两个参数,其中第一个总是std::size_t,指定了请求的字节数。

    名称空间

    声明区域是可以在其中进行声明的区域。变量的潜在作用域从声明点开始,到其声明区域的结尾。

    新的名称空间特性
    C++ 通过定义一种新的声明区域来创建命名的名称空间,这样做的目的之一是提供一个声明区域。一个名称空间中的名称不会和另一个名称空间的相同名称冲突,同时允许程序的其他部分使用该名称空间中声明的东西。

    名称空间可以是全局的,也可以位于另一个名称空间,但不能位于代码块中。

    任何名称空间中的名称都不会与其他名称空间中的名称发生冲突。

    访问给定名称空间中的名称,通过作用域解析运算符::,使用名称空间来限定该名称:

    namespace Jack{
    	double pail;
    	void fetch();
    }
    
    Jack::pail=24;//给变量赋值
    jill::hill mole;//创建一个hill类型的变量
    jack::fetch();//函数调用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    未被装饰的名称(如pail)称为未限定的名称;包含名称空间的名称(如jack::pail)称为限定的名称。

    using 声明和using编译指令

    using声明使得特定的标识符可用,using编译指令使得整个名称空间可用。

    using Jill::fetch;
    
    • 1

    using声明将特定的名称添加到它所属的声明区域中。

    namespace Jill{
    	double fetch();
    	struct Hill{...};
    	
    }
    char fetch;
    int main(){
    	using Jill::fetch;//将fetch加入本地命名空间
    	double fetch;//错误,已经有了本地fetch
    	cin>>fetch;// Jill::fetch
    	cin>>::fetch;//全局fetch
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    using声明将名称添加到局部声明区域,fetch覆盖同名的全局变量。

    在函数的外面使用using声明时,将把名称空间添加到全局名称空间中:

    void other();
    namespace jill{
    	double fetch;
    	double hill{...};
    }
    using jill::fetch;//将fetch添加到全局名称空间
    int main(){
    	cin>>fetch;//jill::fetch
    	other();
    }
    
    void other(){
    	cout<< fetch;//jill::fetch
    
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    using编译指令使所有的名称空间都可用。using编译指令由名称空间名和它前面的关键字using namespace 组成,它使名称空间中的所有名称都可用,而不需要使用作用域解析运算符:

    using namespace jack;//使得jack里面的所有名称都可用
    
    • 1

    在全局声明区域中使用using编译指令,将使该名称空间的名称全局可见。

    #include <iostream>// 在std名称空间的地址名
    using namespace std;//使得名称全局可用
    
    • 1
    • 2

    using编译指令和using声明,增加了名称冲突的可能性。如果有名称空间jack和jill,并在代码中使用作用域解析运算符,则不会存在二义性:

    jack::pal=3;
    jill::pal=10;
    
    • 1
    • 2

    变量jack::pal和jill::pal是不同的标识符,表示不同的内存单元。然而,如果使用using声明,情况将发生变化;

    using jack::pal;
    using jill::pal;
    pal=5;//使用哪一个?现在有冲突
    
    • 1
    • 2
    • 3

    编译器不允许同时使用两个using声明,这样是会导致二义性。

    namespace jill{
    	double bucket(double n){...}
    	double fetch;
    	struct hill{...};
    }
    char fetch;//全局名称空间
    int main(){
    	using namespace jill;// 导入名称空间里的所有名称
    	hill thill;//创建jill::hill 结构体类型
    	double water=bucket(2);//jill::bucket
    	double fetch;//不会报错,隐藏jill::fetch
    	cin>>fetch;//局部fetch
    	cin>>::fetch;//全局fetch
    	cin>>jill::fetch;
    }
    int foom(){
    	hill top;//报错
    	jill::hill crest;//可用
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    main函数中。名称jill::fetch被放在局部名称空间中,但其作用域不是局部的,因此不会覆盖全局的fetch。

    名称空间的一些指导原则:

    1、使用在已命名的名称空间中声明的变量,而不是使用外部变量。
    2、使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
    3、如果开发了一个函数库或者类库,将其放在一个名称空间中。事实上,C++当前提倡将标准函数库放在名称空间std中,这种做法扩展到了来自C语言中的函数。例如,头文件math.h是与C语言兼容的,没有使用名称空间,但是C++头文件cmath应将各种数学库函数放在名称空间std中。实际上,并非所有的编译器都完成了这种过渡。
    4、仅将编译指令using作为一种将旧代码转换为名称空间的权宜之计。
    5、不要在头文件中使用using编译指令。首先,这样做掩盖了要让哪些名称可用;另外,包含头文件的顺序可能影响程序的行为。如果非要使用编译指令using,应将其放在所有预处理器编译指令#include之后。
    6、导入名称时,首选使用作用域解析运算符或者using声明的方法
    7、对于using声明,首选将其作用域设置为局部而不是全局。

  • 相关阅读:
    轻松学习 Spring 事务
    230. 二叉搜索树中第K小的元素
    在亚马逊云科技控制台上创建 Amazon Cognito 用户池
    KubeSphere 多行日志采集方案深度探索 
    CSP-J 2023 T3 一元二次方程 解题报告
    [nlp] 自然语言理解基准 ATIS Snips
    AUTOSAR从入门到精通番外篇(八)-C语言常用技巧50个
    改进YOLOv7系列:25.YOLOv7 加入RepVGG模型结构,重参数化 极简架构
    区间调度问题 ----- 贪心算法
    苹果电脑壁纸软件Irvue for mac激活
  • 原文地址:https://blog.csdn.net/qq_30457077/article/details/124781808