C++内存模型和名称空间

单独编译

我们可以将一个程序放在多个文件中,然后通过头文件#include来相互关联。

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

在包含头文件时,我们使用"xx.h"而不是<xx.h>,因为对于前者,编译器将在储存标准头文件的主机系统的文件系统中查找;而对于后者,编译器将首先查找当前工作目录或源代码目录。

下面是一个coordin.h的例子:

#ifndef COORDIN_H_     //一个文件只能将同一个头文件包含一次
#define COORDIN_H_

struct polar{
  double distance;
  double angle;
};

struct rect{
  double x;
  double y;
};

polar rect_to_polar(rect xypos);
void show_polar(polar dapos);

#endif

下面是file1.cpp文件:

#include <iostream>
#include "coordin.h"
using namespace std;
int main(){
  rect rplace;
  polar pplace;
  
  while (cin>>rplace.x>>rplace.y){
    pplace=rect_to_polar(rplace);
    show_polar(pplace);
  }
}

下面是file2.cpp文件:

#include <iostream>
#include <cmath>
#include "coordin.h"

polar rect_to_polar(rect xypos){
  using namespace std;
  polar answer;
  answer.distance=sqrt(xypos.x*xypos.x+xypos.y*xypos.y);
  answer.angle=atan2(xypos.y,xypos.x);
  return answer;
}

void show_polar(polar dapos){
  using namespace std;
  const double Rad_to_deg=57.3;
  cout<<"distance="<<dapos.distance;
  cout<<",angle="<<dapos.angle*Rad_to_deg;
  cout<<" degrees\n";
}

将这两个源代码文件和新的头文件一起进行编译和链接,可以生成一个可执行文件。

建立项目后,编译器一般会自动处理这一切。

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

1.作用域和链接

作用域描述了名称在文件的多大范围中可见。例如,函数中定义的变量只在该函数中可见,但不能在其他函数中可见;而在文件中的函数定义之前定义的变量则可在所有函数中使用。

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

2.自动存储持续性

函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。

如果在main函数里面定义一个变量n,那么再在work函数中定义一个变量n,这两个变量互不影响,它们相互独立。

当程序开始执行时,系统为其分配内存;函数结束时,n会消失。

如果在main中开始一个新的代码块,并在其中定义了新的变量m,则n在内部和外部代码块中都是可见的,而m只有在内部代码块可见。

如果内部代码块中定义的变量也叫n,那么新的定义将屏蔽以前的定义,新定义可见,旧定义暂时不可见。

register int count_fast;

register关键字建议编译器使用CPU寄存器来存储自动变量,它也显示地指出这个变量是自动的。

自动变量是用栈来管理的。

3.静态持续变量

C++为静态存储持续变量提供了3种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中访问)。

由于静态变量的数目在程序运行期间不会改变,因此程序不需要特殊装置(如栈)来管理它们。编译器会为它们分配固定的内存。如果没有初始化它们,编译器会将它们设置为0;之后将不会再初始化它们。

···
int global=100;//静态变量,外部链接性
static int one_file=50;//静态变量,内部链接性
int main(){
···
}
int fun(int n){
  static int count=0;//静态变量,无链接性
  //第一次初始化后,将不再执行这句初始化语句
  ···
}

4.静态持续性、外部链接性

链接性为外部的变量被称为外部变量,也称全局变量。

一方面,在每个使用外部变量的文件中,都必须声明它;另一方面,C++又规定变量只能有一次定义。为此,C++提供了两种变量声明。一种是定义声明,给变量分配存储空间;另一种是引用声明,不给变量分配存储空间。

引用声明关键字extern,且不进行初始化。如果初始化,此声明将成为定义。

double up;//定义
extern int blem;//声明
extern char gr='z';//定义

如果要在多个文件中使用外部变量,只需要在一个文件中包含该变量的定义,但在使用该变量的所有其他文件中,都必须使用关键字extern声明它。

局部变量可以屏蔽全局变量。

5.静态持续性、内部链接性

链接性为内部的静态变量可以在同一个文件中的多个函数间共享数据,只需要在定义前加上关键字static即可。

6.静态存储持续性、无链接性

正如前面所说,在代码块中定义无链接性的静态变量,在实际运行中定义只会被执行一次,之后不会再触发初始化。

7.说明符和限定符

下面提供一些存储说明符:
(1)auto(表示是局部变量,不要使用)
(2)register
(3)static
(4)extern
(5)mutable(表示即使结构(或类)是const,但其某个成员仍然可以被修改)

在C++看来,被const修饰的全局变量的链接性是内部的。也就是说,C++认为,全局const定义就像使用了static一样。

而假设我们将一组常量放在头文件中,预处理器将头文件的内容包含到每个源文件中后,所有的源文件都将包含常量的定义。

8.函数和链接性

函数也有链接性,但是所有函数的存储持续性都是静态的。

默认情况下,函数的链接性是外部的,可以在文件中共享。可以在函数原型中使用关键字extern来指出这个函数是在另一个文件中定义的,不过这是可选的。

不过也可以使用关键字static将函数的链接性设置为内部的,使之只能在内部使用。同时必须在原型和定义中都使用static。

9.语言链接性

由于C和C++对于函数名称的处理方式不同,C++调用C语言库中的函数需要语言链接。

extern "C" void spiff(int);  //C
extern void spiff(int);  //C++
extern "C++" void spiff(int);  //C++

10.存储方案和动态分配

动态内存由new和delete控制,而不是由作用域和链接性控制。可以在一个函数中分配动态内存,在另一个函数中将其释放。通常,编译器使用三块独立的内存:一块用于静态变量、一块用于自动变量、一块用于动态存储。

虽然存储方案不适合动态内存,但适用于跟踪动态内存的静态或自动指针变量。

float *p=new float[20];

new分配的内存将一直保留在内存中,直到delete将其释放。而当包含p的代码块执行完毕时,p就消失了。而如果将p的链接性声明为外部的,则文件中位于该声明后面的所有函数都可以使用它。在另一个文件中用extern声明,也可以在该文件中使用它。

A.使用new运算符初始化

int *pi=new int(6);  //*pi set to 6
double *pd=new double(99.99);

这种括号语法也适合有构造函数的类。

如果new的时候使用了[],最好释放的时候也用[]。

String * p=new String[3];
delete [] p;

B.new失败时

new可能找不到请求的内存量,这时会触发异常。

C.new:运算符、函数和替换函数

运算符new和new[]分别调用如下函数:

void * operator new(std::size_t);
void * operator new[](std::size_t);

delete和delete[]也有调用的释放函数:

void operator delete(void *);
void operator delete[](void *);

我们平常所写的基本语句:

int * pi=new int;
int * pa=new int[40];
delete pi;

会被转换成

int * pi=new(sizeof(int));
int * pa=new(40*sizeof(int));
delete(pi);

D.定位new运算符

通常new负责在堆中找到一个合适的内存块。但是定位new运算符能让程序员指定要使用的位置。要使用它,首先需要包含头文件new,除需要指定参数外,句法和常规new运算符相同。

#include<new>
struct chaff{
  char dross[20];
  int slag;
};
char buffer1[50];
char buffer2[500];
int main(){
  chaff *p1,*p2;
  int *p3,*p4;
  p1=new chaff;  //常规new
  p3=new int[20];  //常规new
  p2=new (buffer1) chaff;  //把chaff放进buffer1
  p4=new (buffer2) int[20];  //把int数组放进buffer2
}

名称空间

在C++中,名称可以是变量、函数、结构、枚举、类以及结构和类的成员,这些名称有相互冲突的可能。如果有两个库都定义了List,Tree类,用户如果要使用第一个库的List类和另一个库的Tree类,就导致了冲突。C++提供了名称空间工具,以便更好控制名称的作用域。

(1)传统的C++名称空间

声明区域是可以在其中进行声明的区域;潜在作用域指变量的声明点开始,到其声明区域的结尾;而作用域是指变量对程序可见的范围(有时可能会被内层代码块屏蔽)。

C++关于全局变量和局部变量定义了一种名称空间层次。每个声明区域都可以声明名称,这些名称独立于在其他声明区域中声明的名称。

(2)新的名称空间特性

C++新增了一项功能,即通过定义一种新的声明区域来创建命名的名称空间,目的是提供一个声明名称的区域。一个名称空间中的名称不会和另外一个名称空间的名称冲突,同时允许程序的其他部分使用该名称空间中声明的内容。

namespace Jack{
  double pail;
  void fetch();
  int pal;
  struct Well{...};
}

namespace Jill{
  double bucket(double n){ ... }
  double fetch;
  int pal;
  struct Hill{...}
}

名称空间是可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。在默认情况下,名称空间中声明的名称的链接性是外部的。(除非引用了常量)

名称空间是开放的,可以把名称加入到已有的名称空间中,也可以提供函数的具体实现。

namespace Jill{
  char * goose(const char *);
}

namespace Jack{
  void fetch(){
  ...
  }
}

使用作用域解析运算符::来访问名称空间中的名称。

Jack::pail=12.34;
Jill::Hill mole;
Jack::fetch();

A.using声明和using编译指令

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

namespace Jill{
  double bucket(double n){ ... }
  double fetch;
  int pal;
  struct Hill{...}
}

char fetch;
int main(){
  using Jill::fetch;// put fetch into local namespace
  double fetch;//error! Already have a local fetch
  cin>>::fetch;//read a value into global fetch
}
#include<iostream>
using namespace std;//using编译指令也可以放在代码块里面
using jack::pal;
using jill::pal;
pal=4;//不可以,编译器不知道是哪个pal

B.using编译指令和using声明的比较

using编译指令导入一个名称空间和使用多个using声明使不一样的。

在使用using声明时,如果某个名称已经在函数中声明了,就不能用using声明导入相同的名称。

使用using编译指令时,如果导入一个已经在函数中声明的名称,则局部名称将屏蔽名称空间名,就像屏蔽全局变量一样。

namespace Jill{
  double bucket(double n){ ... }
  double fetch;
  int pal;
  struct Hill{...}
}

char fetch;
int main(){
  using namespace Jill;
  Hill Thrill;
  double water = bucket(2);
  double fetch;  //屏蔽了Jill::fetch
  cin>>fetch; //局部变量
  cin>>::fetch; //全局变量
  cin>>Jill::fetch; //名称空间
}

一般来说,using声明要比using编译指令更安全。

C.名称空间的其他特性

可以将名称空间声明进行嵌套:

namespace elements{
  namespace fire{
    int flame;
    ...
  }
  float water;
}
...
elements::fire::flame=1;
using namespace elements::fire;

也可以在名称空间中使用using编译指令和using声明。

namespace myth{
  using Jill::fetch;
  using namespace elements;
  using std::cout;
  using std::cin;
}

如果要访问Jill::fetch,由于它现在在名称空间myth里面,可以这么访问它:

std::cin>>myth::fetch;
std::cout<<Jill::fetch; //also ok

using namespace myth;
cin>>fetch;  //如果不存在冲突

下面的语句将导入名称空间myth和elements.

using namespace myth; //因为myth包含了elements

可以给名称空间起别名。

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

D.未命名的名称空间

可以通过省略名称空间的名称来创建未命名的名称空间。

namespace {
  int ice;
  int bandycost;
}

这样的名称空间中声明的名称的潜在作用域为:从声明点到声明区域末尾。

也可以认为,它提供了链接性为内部的静态变量的替代品。

E.名称空间及其前途

一些指导原则:
(1)使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。
(2)使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
(3)如果开发了一个函数库或类库,将其放在一个名称空间中。
(4)仅将编译指令using作为一种将旧代码转换为使用名称空间的权宜之计。
(5)不要在头文件中使用using编译指令。
(6)导入名称时,首选使用作用域解析运算符或using声明的办法。
(7)对于using声明,首选将其作用域设置为局部而不是全局。

一些老式头文件(如iostream.h)没有使用名称空间,但新头文件iostream使用std名称空间。

猜你喜欢

转载自blog.csdn.net/dxy18861848756/article/details/113605053
今日推荐