内存模型与名称空间

内存模型与名称空间

这是最近阅读c++ primer plus的学习笔记,第九章内容主要是了解多文件编译的过程,和编译后各文件中变量的生存时间,作用域,以及链接性的内容,分清变量的自动、静态、动态等存储方式、局部变量、全局变量的区别、内部外部链接性如何体现等。此外C++提供了名称空间的方式主要是为了解决大规模程序不同的库模板等标识符冲突的问题。

1. C++的单独编译

1.1 单独编译和源代码的拆分

大型文件通常将代码分成多个文件来编写
结构如下:

  • 头文件:包含结构声明和使用这些结构的函数的原型的声明
  • 源文件1:主要用来按照不同功能写一些函数
  • 源文件2:main函数,包含头文件,来调用其他所有函数

头文件主要包含如下内容

  • 函数声明
  • 使用define或const定义的符号常量
  • 结构声明
  • 类声明
  • 模板声明
  • 内联声明

不要将函数定义或者变量声明写到头文件中
如果其他两个cpp文件同时包含一个头文件时,那么里面的函数给或者变量会出现重定义的错误

根据文件进行单独编译,将两个源代码文件和新的头文件一起进行编译和连接,
将生成一个可执行程序。

图片暂时屏蔽记得恢复
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y8Afmnvg-1649441272356)()]

1.2 预编译中包含的头文件

预处理器编译指令

注意:在同一个文件中只能将同一个头文件包含一次

但是有可能在不知情的情况下将同一个头文件包含两次,比如包含的头文件A.h和B.h,但是A.h也包含B.h头文件,这样B.h中的内容就会被重复定义,出现错误!所以为了防止这种情况的发生,经常使用#ifndef(即if not define)的方法。

#ifndef COORDIN_H_
#define COORDIN_H_
//place include file contents here
#endif
  1. 编译器在首次遇到该文件时,名称COOEDIN_H_还没有被定义(通常采用大写,并加入一些下划线,以创建一个在其他地方不太可能被定义的名称),这样编译器会读取#ifndef到#endif之间的内容(这时#define COORDIN_H_将这个名称定义了一次)。
  2. 之后如果遇到其他包含coorin.h的文件,编译器知道COORDIN_H_已经被定义过了,所以会跳到#endif后面一行,这样就忽略的这个头文件的内容,避免了被重定义出现错误。

2. C++中变量的存储方式

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

2.1 C++的存储类别

C++使用四种不同的方式来存储数据,区别在于数据保留在内存中的时间不同

以下几个分类描述的是变量在内存中存储时间的差别

  • 自动存储 ———— 函数定义内使用的变量(花括号内定义和函数参数),在函数或代码块被执行时创建,执行完释放。寄存器、栈:函数参数值,局部变量
  • 静态存储 ———— 函数外定义的变量和staic定义变量,在整个程序运行过程都存在。全局区:全局变量、静态变量、常量
  • 线程存储
  • 动态存储 ———— new运算符分配的内存将一直存在,直到delete将其释放或程序结束为止。

2.3 C++的作用域

作用域描述了一个变量、函数或结构体等名称或标识符在文件内的多大范围内可见。

作用域分类

  • 局部(代码块)作用据:代码块(花括号内)
  • 全局(文件)作用域 :开始位置到文件末尾
  • 函数原型作用域 :仅函数声明的形参列表的圆括号内
  • 函数作用域:整个类或是整个名称空间(包括全局的),但不能是局部的。

静态变量(static关键字修饰的变量)存储在全局区,它在整个程序运行过程中 都是存在的,至于它是全局还是局部的,取决于它是在何处被定义的。

函数定义、函数调用、函数原型声明
函数原型声明时,只需要告诉编译器有几个形参,形参的类型是什么,至于形参叫什么名字,可以不写,编译器不关心

2.4 C++的链接性

链接性描述了标识符或名称能否在不同翻译单元(一个.cpp文件可以包含多个.h文件,他们合称为一个翻译单元

链接性

  • 外部 :变量名前加extern,可以在其他文件中访问
  • 内部:只能在当前文件中访问
  • 无链接性:比如自动变量,不能共享,只能在当前函数或代码块中访问

2.5 总结

变量的存储方式

存储类别存储期作用域链接声明方式
自动自动代码块代码块内
寄存器自动代码块代码块内,使用关键字register
静态、无链接静态代码块代码块内,使用关键字static
静态、外部链接静态文件外部不在任何函数内
静态、内部链接静态文件内部不在任何函数内,使用关键字static

2.6 具体的几个示例

2.6.1 自动变量

默认情况下的函数参数局部变量,变量存在栈区(存在时间为自动),作用域为局部,无连接性。

// autoscp.cpp -- illustrating scope of automatic variables
#include <iostream>
void oil(int x);
int main()
{
    using namespace std;
   // 1. 第一次定义texas自动局部变量 
    int texas = 31;
    int year = 2011;
    cout << "In main(), texas = " << texas << ", &texas = ";
    cout << &texas << endl;
    cout << "In main(), year = " << year << ", &year = ";
    cout << &year << endl;

   // 2.
    oil(texas);

  // 1.
    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;
    //函数内部的texas为局部变量,与main函数内同名的texas相互不影响,占用两个不同的内存单元
    //2. 当储程序从main()执行到oil(x)时,新的定义int texas = 5;隐藏了以前的定义int texas = 31;
    //新定义可见,旧定义暂时不可见
    cout << "In oil(), texas = " << texas << ", &texas = ";
    cout << &texas << endl;
    cout << "In oil(), x = " << x << ", &x = ";
    cout << &x << endl;
    {                               // start a block
      //2.1 当程序运行到内部代码块时,新的定义int texas = 5;不可见,
      // 他被一个更新的定义int texas = 113;所替代

      //2.2 但是变量x仍然可见,因为该代码块中没有定义x变量
        int texas = 113;
        cout << "In block, texas = " << texas;
        cout << ", &texas = " << &texas << endl;
                cout << "In block, x = " << x << ", &x = ";
        cout << &x << endl;
        //当程序离开该代码时,释放最新的定义int texas = 113;使用的内存
    }       
                               // end a block
    //2. 之后第二个定义的int texas = 5;再次可见
    cout << "Post-block texas = " << texas;
    cout << ", &texas = " << &texas << endl;
}
   //函数运行结束之后,第二次函数内定义的int texas = 5;被释放,
   // 1. 第一次在main()定义的int texas = 31;再次可见

代码运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SH8iwZJX-1649441272360)(2022-04-07-15-53-34.png)]

深入理解自动变量和栈
自动变量随函数的开始和结束而增减,常用的方法是留出一段内存,将其视为栈(后进先出),以管理变量的增减

新数据被象征性的放在原有数据的上面,(是在两个不同的内存单元中,而不是在同一个内存单元中)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-baJRbI8v-1649441272361)(2022-04-07-16-24-06.png)]

2.6.2 静态持续变量

三种静态持续变量

...
int global;           //外部静态变量
static int  one_file; //内部静态变量
int main()
{
  ...
}
...
void func1(int n)
{
  static int count = 0; //无链接性静态变量
  int llama = 0;
  ...
}
...
void func2()
{
  ...
}


三者的区别在于

  • global一直存在且可以被外部其他文件访问
  • one_file只能本文件的函数访问,不能被其他文件访问
  • count的作用域为局部,只能在func1()函数中使用,但是与自动变量llama的区别在于, count的内存不会被释放,一直存在。

注意:static的两种用法

static int one_file;
对于代码块外的声明,(本身已经是全局变量了,是静态的,在全局区),这时static表示内部连接性,只能在本文件中使用。

static int count = 0;
对于代码块内的局部变量,static表示的是存储持续性,与自动方式不同,不会在代码块结束被释放。

注意:所有静态变量默认都是初始化为0的

2.6.3 静态外部变量(即全局变量)

单定义规则

如果在多个文件中使用外部变量,只需在一个文件中进行该变量的定义,但是在其他所有文件中,都必须使用关键字extern声明它。(定义的时候编译器黑变量分配存储空间,声明的时候不分配内存,而是引用已有的变量)

//file1.cpp
int cats = 20;  //定义全局变量
...
int main()
{
...
}

//file2.cpp
extern int cats;  //声明外部变量
...
void func1()
{
  extern int cats;  //声明外部变量,函数内的修改变cats得值
} 

void func2()
{
   int cats;  //定义与全局变量同名的局部变量,局部变量将隐藏全局变量,cats(自动,作用域为代码块)的改变不会影响file1.cpp中cats(静态,外部,作用域为所有文件均有效)的值。
   cout <<::cats;  // ::双冒号为作用域解析运算符,放在变量的前面,表示使用变量的全局版本。将输出file1.cpp中的cats的值。
} 
2.6.4 静态内部变量(即单个文件内的全局变量)

(外部静态变量)全局变量可以在多个文件内共享数据区,而内部静态变量可以在同一个文件中的多个函数间共享数据区。
另外,用static关键字修饰可以避免与其他文件中同名的全局变量发生冲突,在本文件内,将隐藏其他文件同名的全局变量,使用内部静态变量,可以避免发生数据冲突。

示例如下

// twofile1.cpp 
int tom = 3;        //定义一个全局变量(外部静态)
int dick = 30;          // 定义全局变量(外部静态)
static int harry = 300; // 定义一个只在本文年内有效的内部静态变量,

// twofile2.cpp 
extern int tom;// 声明全局变量,与file.cpp中的tom相同。
static int dick = 10;  // 与file.cpp中全局变量dick同名,用static声明,在本文件内隐藏相应的全局变量,避免数据冲突。
int harry = 200;     // 定义一个全局变量

2.6.5 静态局部变量(无链接性)(主要与自动局部变量相区别)

在代码块中使用static定义变量时,能够使局部变量的存储时间为静态的,与自动的局部变量不同,不会随着代码块的结束释放内存,它将一直存在知道整个程序结束。
注意:静态局部变量的定义语句只会在函数第一次调用时执行一次,再次调用该函数时,它将不会再次被初始化,而自动变量在每次调用函数时都会被重新初始化

2.6.5 使用new和delete来进行动态的存储分配

1. 使用new运算符初始化

C++98

//初始化单个变量
int *pi = new int (6)//圆括号里放初始值
double *pd = new double (99.99);

C++11的编译器支持花括号表达

//初始化单值变量
int *pin = new int {6};
double *pd0 = new double {99.99};
//初始化数组
int *ar = new int [4] {2,4,6,7}; //方括号放数组元素个数,花括号里放初始值
//初始化结构体
struct where 
{
  double x;
  double y;
  double z;
};
where *one = new where {2.5,5.3,7.2};

  1. new运算符失败时
  • 返回空指针
  • 引发异常std::bad_alloc
  1. new和delete运算符本质上都是一个函数

4.定位new运算符

  • 常规new运算符:在堆(heap)里找到一个能够满足要求的内存块
  • 定位new运算符,可以自己指定要使用的内存位置。(需要包含new文件)(定位:placement)) 圆括号放地址即可
#include<new>
struct chaff
{
  char dross[20];
  int slag;
};
char buffer1[50];
char buffer2[500];
int main()
{
  char *p1,*p2;
  int *p3,*p4;
  //the reguler forms of new
  p1 = new chaff;   //place structure in heap
  p3 = new int[20]; //place int array in heap
  //the forms of placement new
  p2 = new (butter1) chaff; //place structure in butter1
  p4 = new (butter2) int[20];//place int array in butter2
  
}

示例: regular new and placement new

#include<iostream>
#include<new>
using namespace std;
const int BUF = 512;
const int N = 5;
char buffer[BUF];

int main(void)
{
	double* pd1, *pd2;
	cout << "calling new and plcaement new: " << endl;
	pd1 = new double[N];            //regular new
	pd2 = new (buffer) double[N];  // placement new
	for (int i = 0; i < N; i++)
	{
		pd2[i] = pd1[i] = 1000 + 20.0 * i;
	}
	// buffer是char型,直接打印地址打不出来,应该加(void*)强转一下,就可以输出地址值
	cout << "pd1 = " << pd1 << ", buffer = " << (void*)buffer << endl; 

	for (int i = 0; i < N; i++)
	{
		cout << pd1[i] << " at " << &pd1[i] << "; ";
		cout << pd2[i] << " at " << &pd2[i] << endl;
	}

	cout << "\nCalling new and placement new a second time: " << endl;
	double* pd3, * pd4;
	pd3 = new double[N];            // new开辟新空间
	pd4 = new(buffer) double[N];    //定位new运算符开辟的空间,和pd2是同一个位置
	for (int i = 0; i < N; i++)
	{
       pd4[i] = pd3[i] = 1000 + 40.0 * i;
	}
	for (int i = 0; i < N; i++)
	{
		cout << pd3[i] << " at " << &pd3[i] << "; ";
		cout << pd4[i] << " at " << &pd4[i] << endl;
	}
	cout << "\nCalling new and placement new a third time: " << endl;
	delete[] pd1;   //释放掉pd1
	pd1 = new double [N]; //重新分配pd1指向的内存空间
	pd2 = new(buffer + N*sizeof(double)) double[N];   //pd2指向buffer偏移5个单元的内存

	for (int i = 0; i < N; i++)
	{
		pd2[i] = pd1[i] = 1000 + 60.0 * i;
	}
	// buffer是char型,直接打印地址打不出来,应该加(void*)强转一下,就可以输出地址值
	cout << "pd1 = " << pd1 << ", buffer = " << (void*)buffer << endl;

	for (int i = 0; i < N; i++)
	{
		cout << pd1[i] << " at " << &pd1[i] << "; ";
		cout << pd2[i] << " at " << &pd2[i] << endl;
	}

	delete[] pd1;
	delete[] pd3;
	
//	delete[] pd2;  这个例子不能用delete释放pd2的内存空间,这是因为char buffer[BUF];在函数体外,申请的是一块静态内存,位于全局区
	//             而delete只能释放处于堆区的动态内存,数组buffer处于delete的管辖区之外,会报错。

	return 0;
}

3. C++的相关限定符

存储说明符
auto 自动变量
register 寄存器变量
static 内部链接性或静态存储持续性
extern 声明外部变量
thread_local 与线程相关
mutable 用来指出即使结构(或类)变量为const,某个特别的成员也可以被修改。

CV-限定符
volatile :将变量声明为volatile,阻止编译器进行某种优化,(这种优化可可能使硬件端口上的数据出现错误)
const:在默认的情况下,全局变量的链接性为外部的,但const全局变量的链接性为内部的,(和加static说明符的情况基本类似)

原因是C++修改了常量类型的规则,例如将一组常量写在头文件中,并在多个头文件中使用了该头文件,那么预处理器将头文件的内容包含到每个源文件中,所有的头文件都将包含这个全局变量的定义,根据单定义的规则,这将出错。但是const将键列性修改为内部,这时每个文件都可以在文件内部使用这一组常量,而不用一个一个去extern,节省了工作量,非常方便,这就是为什么能够将常量定义放在头文件中的原因。

函数的链接性

  • 在默认情况下,函数的链接性为外部,即可以在多文件间共享,声明一下就可以用了。
  • 还可以使用static关键字内部链接性,只能在本文件中使用,这也意味着可以在其他文件中定义同名的函数而不会出现冲突。

语言的链接性
在C++中,函数重载时(同一个名称对应多个函数),C++编译器其实是通过增加"_”将其修改为不同的函数名称,这种方法称为C++语言链接(C++ language linkage)。

4. 名称空间

4.1 名称空间基本知识内容

名称空间的作用:避免命名冲突,使我们可以在上下文中带调用来自不同库的相同符号,而不必一一将他们的名字改成不同的。名称空间可以帮助我们更好的控制名称的作用域。

关于名称空间的基本知识点

  1. 名称空间写在哪?———— 定义在全局作用域下。(即写在函数体外,不能写在函数体内)
  2. 名称空间里可以写什么? ———— 变量、函数、结构体、类… (声明和定义都可以写)
namespace jack
{
  double pail;       //variable declaration
  void fetch();      //function prototype
  int pal            //variable declaration
  struct Well{...};  //structure declaration
}

namespace Jill
{
  double bucket(double n){...}; //function definition
  double fetch;  // variable declaration
  int pal;       //variable declaration
  struct Hill{...}; //structure declaration
}

  1. 名称空间可以参套名称空间,通过多个::来进行访问即可。
namespace A{
  int a = 50;
  namespace B{
    int a = 2;
  }
}
  
int main()
{
  cout << B:a <<endl;    //访问空间B的变量a
  cout << B::C::a <<endl;//访问空间C的变量a

  return 0;
}
  1. 名称空间是开放的,可以随时向里面添加新的内容(两个同名的名称空间,编译器自动做合并操作,视作同一个空间)
namespace B{
  int b = 0;
}

namespace B{
  char c;
}

//两个B会自动的合并成一个空间
  1. 名称空间可以是匿名的,(即没有名字的名称空间),直接访问里面的变量就可以,只是相当于全局变量加了个static(链接性设置为内部的)
namespace{
  int b = 0;   //就相当于static int b = 0; 
}

int main()
{
  cout << b << endl;  
}

  1. 名称空间可以有别名,这在名称空间嵌套时可以简化我们的表达
namespace A{
  int a = 50;
  namespace B{
    int a = 2;
  }
}
  
int main()
{
  namespace SNS = A::B;
  
  cout << B::C::a <<endl;//访问空间C的变量a
  cout << SNS::a <<endl;//访问空间C的变量a  ,嵌套比较多时这样写更方便
  return 0;
}

4.2 作用域解析运算符

作用域解析运算符::是运算符中等级最高的,它有三个作用:
1.全局作用域符
2. 类作用域符
3. 名称空间作用域符

作用域解析运算符::的具体用法如下

#include<iostream>
using namespace std;

namespace fun1 {
	int num = 1;     
//也是定义在全局空间中的,但是由于在名称空间fun中,所以不会和(int num = 2;  //全局变量)冲突
	double d = 1.99;
}

namespace fun2 {
	int num = 5;
	namespace fun3 {
		int num = 6;
	}
}

int num = 2;  //全局变量

int main(void)
{
	int num = 3; //局部变量
	{
		int num = 4;
		cout << num << endl;   //代码块内局部优先,num = 4
	}
	cout << num << endl;          // 局部变量, num = 3
	cout << ::num << endl;        //解析到全局变量,num = 2
	cout << fun1::num << endl;    //解析到fun1名称空间,num = 1
	cout << fun2::num << endl;    //解析到fun2名称空间,num = 5
	cout << fun2::fun3::num<< endl;  //解析到fun2中嵌套的fun3名称空间,num = 6
	return 0;
}

4.3 using声明和using编译指令

这两种机制是为了简化名称空间中名称的使用,名称空间里很多东西都用::写起来比较麻烦。
区别:

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

using 声明

namespace Jill{
  double bucket(double n){...}; //function definition
  double fetch;  //variable declaration
  struct Hill{...};  //structure declaration
}
char fetch; // global variable

int main()
{
  using Jill::fetch;  //put fetch into local namespace
  double fetch; //error! Already have a local fetch (using Jill::fetch;)
  cin >> fetch; //read a value intto Jill::fetch
  cin>> ::fetch; //read a value into global fetch(char fetch;)
}

在代码块内使用using声明,就相当于是使这个名称成为了一个局部变量,那么再次定一个局部变量double fetch;时就会报错,局部变量重定义了,而全局变量char fetch;则由于在代码块内,被局部变量暂时隐藏了。

void other()
namespace Jill
{
  double bucket(double n){...}
  double fetch;
  struct Hill{...};
}
using Jill::fetch; //put fetch into global namespace
int main()
{
  cin >> fetch; //read a value into Jill::fetch
  other();
  ...
}
void other()
{
  cout << fetch;  // display Jill::fetch
}

在函数外使用using声明时,就使名称成为全局变量

using 编译指令

类似的,在函数体外使用using编译指令,可以是该名称空间的名称全局可用
在函数体内使用using编译指令,使该空间名称在该函数内可用。但是不会与局部变量相冲突,局部变量会隐藏名称空间中重名的变量名称。

#include<iostream>
#include<new>
using namespace std;

namespace fun1 {
	int num = 1;     
//也是定义在全局空间中的,但是由于在名称空间fun中,所以不会和(int num = 2;  //全局变量)冲突
	double d = 1.99;
}

int num = 2;  //全局变量

int main(void)
{
	
	int num = 3;
	cout << num << endl;  // num = 3
	cout << ::num << endl; //num = 2
	cout << fun1::num << endl;  //num = 1

	return 0;
}

一般来说,使用using声明更多一些,(更安全),因为有相同的局部变量名会报错,而编译指令方式不会,容易造成混乱。

  • 23
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值