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

在之前,说过自动存储(随函数内声明而建立,函数消亡而消亡)、静态存储(全局都存在)、动态存储(一般是用new)。

 

但是,涉及到多文件的时候,存储类别如何影响信息在文件间的共享。

 

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

 

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

 

静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。C++有三种存储持续性为静态的变量(①外部链接性②内部链接性③无链接性

 

线程存储持续性(C++11当前,多核处理器很常见,这些CPU可同时处理多个执行任务。这让程序能够将计算放在可并行的不同线程中。如果变量是使用关键字thread_local声明的,则其生命周期和所属线程一样长,本书不讨论并行编程。——完。全。看。不。懂!

 

动态存储持续性:new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时候称为自由存储(free store)或堆(heap)。

 

 

 

 

作用域(scope描述了名称(比如说一个变量名)在文件(翻译单元)的多大范围内可见。例如,函数定义的变量,可在该函数中使用,但不能在其他函数中使用;而在文件中函数定义之前(比如说像没有加static关键字的那种全局变量),则可以在所有函数中使用。

 

链接性(linkage)就是指描述名称如何在不同翻译单元之间共享。首先,因为自动变量只能在函数内部/块内部使用,所以不具备链接性。链接性为外部的变量,可以在文件之间进行共享,链接性为内部的变量,只能在该文件使用。

 

 

C++的作用域有多种:

①作用域为全局(又称为文件作用域)(例如全局变量),从声明的位置到文件尾;

②代码块,例如while循环内声明的变量只能在循环内部使用;

③静态变量(全局的归全局,函数内部声明但加static的则在函数内部);

④类中声明的成员则在类中可用;

⑤在在函数原型作用域(function prototype scope)(函数原型)声明的名称,只能在函数原型使用,因此我们通常不在函数原型之内加入变量名);

⑥名称空间中声明的变量的作用域为整个名称空间(又因为名称空间已经引入到C++语言之中,因此全局作用域是名称空间作用域的特例why)。

 

 

另外,C++函数,由于不能在代码块内声明(比如想想之前怎么声明和定义函数的),所以,函数的作用域是整个类或整个名称空间的(包括全局的)。

 

 

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

 

持续性:

默认情况下,函数中声明的函数参数、或变量的存储持续性为自动,作用域为局部,没有链接性

 

①代码块内变量只能代码块内能使用;

②函数内的代码块内的名称,假如和代码块外的名称冲突,那么在代码块内声明名称后,会暂时隐藏代码块外的名称,直到代码块结束。

例如:

int a=1;

cout<<a<<endl;

{
cout<<a<<endl;

int a=10;

cout<<a<<endl;

}

cout<<a<endl;

输出结果依次是:11101

 

 

早期的auto

C++11之中,auto用于自动类型推断。

C语言和早起的C++版本之中,auto的含义与现在不同,它用于显式的指出变量为自动存储。由于只能这么做,所以程序员几乎不使用它。

在现在,这种方法是非法的。

 

 

 

 

自动变量和栈:

由于自动变量的数目随着函数的开始和结束而增减,因此程序必须在运行时对自动变量进行管理。

常用的方法是留出一段内存,并将其视为,以管理变量的增减。

 

之所以称之为栈:是因为新数据被象征性的放在了原有数据的上面(比如栈道就是一块块修过去,架在基础之上),也就是说,新数据和原有数据,是在相邻的内存单元之中,而不是同一个内存单元中(具体每个内存单元多大,需要看类型格式)。但程序使用完后,将其从栈中删除。

 

栈的默认长度:取决于实现,但编译器通常提供改变栈长度的选项。

程序使用两个指针来跟踪栈。一个指针指向栈底——栈开始的位置,另一个指针指向栈顶——下一个可用内存单元。

当函数被调用时,其自动变量就被加入到栈中,栈顶指针指向变量后面下一个可用的内存单元。

函数结束时,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。

 

栈是LIFO(后进先出:last in first out)的,即最后加入栈中的变量首先被弹出。这种设计简化了参数传递。

 

函数调用将其参数的值放在栈顶,然后重新设置栈顶指针。被调用的参数根据其形参描述(即参数列表里面的类型)来确定每个参数的地址。

 

例如void abc(int a,double b);  会先将参数a放在栈顶(例如在我的电脑上内存大小为4字节),然后将参数b放在a的上面(在我电脑上是8字节),这个时候,指向栈顶的指针指向参数b后面的可用内存单元。

abc()函数结束时,指针会重新指向在加入ab之前的位置。

 

新值没有被删除,但不会被标记,它们所占用的空间,会在下一个将值加入栈中的函数调用所使用。(这里未说明函数调用可能传递的其他信息,如返回地址

 

 

寄存器变量(C++11无效):

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

register int a=1;

这可以提高访问变量的速度(因为不用读内存)。

但这是在C++11之前。在C++11之后,关键字register只是显式的指出变量是自动,跟以前的auto用法是一样的(参考上面),因此无需使用。

 

 

 

 

静态持续变量:

C语言一样,C++也为静态持续变量提供了3种链接性:

①外部链接性(可在其他文件中访问);

②内部链接性(在当前文件声明后至文件尾都可以访问);

③无链接性(只能在当前函数或者当前块访问,例如被static关键字限定);

 

这三种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。由于静态变量的数目在整个程序运行期间是不变的(因为没有new,因为也一直存在),因此程序无需使用特殊的装置(自动变量的栈和自由存储的堆)来管理它们。

 

编译器将分配固定的内存块来存储所有的静态变量,这些变量将在整个程序执行期间一直存在。

另外,如果没有显式地初始化静态变量,编译器将把它设置为0.

 

在默认情况下,静态数组和结构,将每个元素或成员的所有位都设置为0

 

 

注:传统的K&R C不允许初始化自动数组和结构(例如int a[5]),但允许初始化静态数组和结构(例如static int a[5]={1,2,3,4,5}这样)。有些旧的C++编译器也存在这个情况。但ANSC CC++允许对自动数组和结构进行初始化。

 

 

在多文件的情况下,创建三种链接性的静态持续变量:

①具有外部链接性:在一个文件的所有块(函数)外面创建。(于是所有文件都能用);

②具有内部链接性:在①的基础上,前面加关键字static(于是本文件内所有块都能用);

③无链接性:在某个块内创建,并加关键字static(不加的话就是自动变量了)(当前块内可用);

如代码:

#include<iostream>

int a = 1;	//外部链接性
static int b = 2;	//内部链接性

int main()
{
	static int c = 3;	//无链接性
}



静态变量的初始化:

零初始化:例如没初始化;

常量表达式初始化:遇见可以直接计算结果的表达式,将直接计算;

动态初始化:比如说计算不出结果是什么,那么就动态初始化,即在编译后再初始化。

 

①和②统称静态初始化(和动态初始化对应)。

 

另,C++11新增关键字contexpr,这是增加了创建常量表达式的方式。但,书里没提到!而且,这个似乎用的很少很少很少。

 

 

 

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

链接性为外部的变量,通常称为是外部变量(因为其他文件也可用),存储持续性为静态,作用域为全局。

可以在main()函数外定义他们,或者在头文件中定义他们。

于是,可以在任何位于外部变量定义后面的任何函数使用他们,因此外部变量也称为全局变量

 

 

单定义规则(One Definition Rule, ODR:指出,变量只能有一次定义。

 

 

使用外部变量需要注意的几点:

①在每个使用该外部变量的文件中,都要声明它;

 

②但不能多次定义他(函数内部声明的同名局部变量除外,此时外部变量将被隐藏);

 

③因此,声明的方式C++提供了两种:

1定义声明(简称为 定义),它给变量分配存储空间;

2引用声明(简称为 声明),它不给变量分配存储空间,因为它引用已有的变量(如之前提过的引用变量,他是已有变量的别名,共享一个内存地址和值)。

 

④在使用外部变量时,首先需要定义,然后在其他文件用的时候在文件开始进行声明(也可以考虑在头文件中声明,但个人觉得这样并不好用)。定义和声明的方式为,加关键字extern

例如:extern int a=1;

 

⑤需要注意的是,有关键字extern的初始化,会被认为是定义;

而无关键字extern的初始化,会被认为是声明。

定义也可以不加关键字extern,效果是相同的。

如果涉及多个文件中使用外部变量,只需要在一个文件中进行定义(切勿在多个文件之中进行定义),在其他文件使用的文件中进行声明(必须使用关键字extern

 

如代码(每个文件前有双斜杠注明文件名):

//标头.h
#ifndef  aa_aa
#define aa_aa
void abc();
#endif // ! aa_aa

//源.cpp
#include<iostream>
#include"标头.h"
int a = 1;	//外部链接性,可加extern,也可以不加
static int b = 2;	//内部链接性

int main()
{
	using namespace std;
	static int c = 3;	//无链接性
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
	cout << "——————" << endl;
	abc();
	system("pause");
	return 0;
}

//源1.cpp
#include<iostream>
#include"标头.h"
extern int a;

void abc()
{
	using namespace std;
	cout << a << endl;
}

输出:

1
2
3
——————
1
请按任意键继续. . .

在头文件定义/声明一个外部变量可能存在的问题(貌似一般不要这么做):

这里是本人亲身体验的感觉,但认知不一定正确;

在头文件之中定义的步骤如下:

1)首先,在一个头文件里声明一个外部变量,并定义(初始化);

2)然后在某一个(无需是第一个)使用这个外部变量的.cpp文件里,引用这个头文件(例如#include"标头.h"),且在其他文件里,不能再引用这个头文件了(否则会造成多次定义问题);

3)在其他使用这个外部变量的.cpp文件里,使用声明外部变量(例如extern int a;)

4)这样的话,即可在头文件里定义一个外部变量。

 

在头文件之中,声明的步骤如下:

1)先在头文件里声明;

2)在某个.cpp文件里,定义这个外部变量(但务必记住,不能引用这个头文件);

3)在其他使用这个外部变量的.cpp文件里,引用这个头文件。

 

但存在很多问题:

1)定义/声明这个外部变量的头文件,要么只能定义,要么只能声明;

2)若是定义,则只能头文件只能被一个cpp文件引用;若是声明,则头文件不能被定义的那个.cpp文件引用。

3)也就是说,无论是在头文件里定义还是声明外部变量,都需要注意头文件引用问题。所以个人感觉并不好用(除非有一大堆外部变量,需要专门创建一个头文件进行外部声明,即extern int a;extern int b;这样这样的,才有节约功夫的价值),也容易引起编译器错误(例如非正确引用了该头文件导致多次定义之类的错误)。

 

 

⑦如果在哪个.cpp文件里无需使用这个外部变量,那么就不需要使用外部声明(extern xxx xxx);

 

⑧可以在函数(或块)的内部声明(非定义)外部变量(即在使用的时候再声明,但定义一定要在函数外部)。但若这样做,那么在同一个文件中,这个函数(块)的外部(例如另一个函数中),将无法使用这个外部变量(除非也进行声明)。

 

如代码:

//源.cpp
#include<iostream>
int a = 1;

void abc();
void def();
int main()
{
	using namespace std;
	abc();	//调用两个不同的函数
	def();
	system("pause");
	return 0;
}

//源1.cpp
#include<iostream>

void abc()
{
	using namespace std;
	extern int a;	//函数内部引用声明a
	cout << a << endl;
	a++;	//a++后再输出a,证明abc()和def()函数是同一个外部变量a
}

void def()
{
	using namespace std;
	extern int a;	//若不声明,则def()内部无法使用外部变量a
	cout << a << endl;
}

输出:


1
2
请按任意键继续. . .

另外,在源1.cpp里面,也可以在两个函数之前加入extern int a;这行代码,虽有重复之嫌疑,但是好像并没有什么坏处。

 

⑨假如在def内部声明变量a。例如int a=5;  那么这个时候,自动变量a将全局变量a隐藏,输出的是自动变量a的值5

a的修改也是修改的自动变量a,等def()函数结束,自动变量a消亡,全局变量a再次出现,其值为隐藏时的值。

 

 

 

全局变量和局部变量:

全局变量的好处所有函数都能访问,无需通过指针/引用等传递。

全局变量的缺点由于易于访问,会带来程序不可靠的代价(因为每个函数都可以修改它)。

按照书上所说,程序越能避免对数据进行不必要的访问,就越能保证数据的完整性(按预想的那样使用)。

通常来说,应使用局部变量,在需要知晓/修改的时候,才传递这个数据,而不应不加区分地使用全局变量来使数据可用。

 

于是,OOP(面向对象编程)在数据隔离方面又向前迈进了一步。(保证了数据的安全和可靠)

 

全局变量的用处例如多个函数会访问同一块数据(且这个数据通常为常量数据),例如month字符串数组表示月份,month[0]="January"等这样。在其声明的时候,加上const关键字,以确保这个字符串不被修改。如:

const char month[12]={......};

如果是指针,还需要额外在字符串数组名字前加入const关键字,确保指针只指向初始化时的字符串,如:

const char* const month[12]={.....};

第一个const用于确保字符串不被修改,第二个const确保month这个字符串数组不指向别的(例如month++这样)。

 

 

 

 

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

在定义和声明外部变量的基础上,在前面加入关键字static,那么这个变量将变为具有内部链接性的变量。

例如:在上面那段代码中,将

//源.cpp

#include<iostream>

int a = 1;//这个时候,a为外部变量

 

改为

//源.cpp

#include<iostream>

static int a = 1; //这个时候,a为具有内部链接性的外部变量

 

这个时候,变量a只能在源.cpp这个文件之内使用(但可以在这个文件内所有函数中使用)。

 

 

假如有一个全局变量int a,你需要在x.cpp文件里使用一个具有内部链接性的外部变量(静态外部变量),且这个变量名同样也为“a”,那么应该这样写:

static int a;

这个时候,这个静态外部变量,会将全局变量隐藏起来。

 

这样,可以防止具有外部链接性的变量(全局变量)和静态外部变量之间的冲突。

 

 

关于数据共享:

全局变量可让数据在多个文件之间共享;

静态外部变量可让数据在一个文件中的多个函数之间共享。

 

代码很简单,就不写了。参考上面同时有全局变量、静态外部变量、和自动变量的那个代码。

 

 

 

静态持续性、无链接性:

静态局部变量:在代码块(一般是函数)之中(区分于静态外部变量的在函数外),声明,并被static关键字所限定的变量,具有静态持续性、无链接性。

例如:

void abc()

{

using namespace std;

static int a = 1; //无链接性的静态变量

cout << a << endl;

a++;

}

 

在这段代码之中,变量a即为无链接性的局部变量。

 

在多次调用abc()这个函数时,某次调用时,a的值为上一次退出时a的值。

例如,第一次调用,输出1,退出函数时,a2

第二次调用,输出2,退出函数时,a3

依此类推。

 

静态局部变量的特点:

①不会随着函数的消亡而消亡;

②只会在程序启动的时候,进行一次初始化,在后面将不会再次被初始化(不像自动变量那样,每次调用程序都会初始化一次);

 

关于static用在静态局部变量时的使用方法,可以参考之前写的(五十九)自动存储、静态存储、动态存储

 

 

 

说明符和限定符:

之前遇见过关键字conststaticexternauto等。这些有的是 存储说明符(storage class specifier)或 cv-限定符(cv-qualifier),这些C++关键字提供了其他有关存储的信息。

 

存储说明符包括:

①auto(但C++11中不是,之前是显式表示是自动变量);

②register(寄存器变量,但C++11也不支持了,只是显式的声明是自动变量);

static(用于说明是静态外部变量或者静态局部变量);

extern(用于声明全局变量,非定义);

⑤thread_local(C++11新增);

⑥mutable(待定,后面解释);

 

其中,thread_local变量指面向线程,它的作用,就像常规静态变量和程序之间的关系一样,其持续性和所属线程的持续性相同。

可和staticextern结合使用。

 

cv-限定符包括:

const

②volatile;

 

cv就是constvolatile的首字母)。最常用是的const,表示内存初始化后,程序不能对其进行修改。

 

关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。

 

原因在于,比如某个指针指向了某个硬件位置(其中包含了来自串行端口的时间或信息),而某个硬件是可能修改这个信息的(但并不是程序自己修改);

又或者是有两个程序,共享某段信息(其中一个const限定,不能修改,另一个没限定,便可能修改);

在这些情况中,即使指针被const所限定,其指向的内容依然可能变化。

 

其次,编译器为了优化处理进程,假如一个变量,短时间内被连续使用了两次(这两次中间并没有被程序修改,或者已经被const限定),编译器可能就会将这个值放入在寄存器之中(因为这样读取更快),但又因为上面的内容,虽然程序本身没有修改,但不代表该变量就没有变化,这就可能读取存储在寄存器中的值是错误的。

因此加入volatile,可以让编译器知道,不要把这个变量放在寄存器之中,这样就避免了因为这种优化反而导致的错误。

 

 

mutable:

之前有提到mutable(英文意思是可变的、易变的)这个存储说明符,可以用它来指出,即使结构(或者是类)的变量为const所限定,其某个成员的值依然可以被修改。

注:除了结构和类,似乎也没什么合适的使用地方了。

如代码:

//源.cpp
#include<iostream>

struct abc
{
	int a;
	mutable int b;
};


int main()
{
	using namespace std;
	const abc m = { 1,5 };
	//m.a++;	//这行会提示,表达式必须是可修改的左值
	m.b++;	//但这行就不会提示
	cout << m.b << endl;
	system("pause");
	return 0;
}

输出的结果即为6,而不是初始化的5

 

 

 

const

C++中,const限定符对 默认存储类型 有所影响。例如,一个全局变量(在多个文件中可用),加入了const限定符后,其链接性变成内部的(即成为了一个静态外部变量),就像使用了static关键字一样。

例如:

const int a = 10;

static const int a = 5;

是等价的。

 

之所以会这样,是因为假如把const int a=10;放在一个头文件之中,然后多个文件引用这个头文件。假设不将其设置为静态外部变量,那么就会导致违反 单定义规则 ,而如果是每个文件都将其视为一个静态外部变量,就不会存在这样的问题。

 

这是C++修改了常量类型规则之后的结果。这样的做法会让程序员更轻松(可以把一些常量放在一个头文件之中,需要使用这些常量的cpp文件只需要引用这些头文件即可)。

 

需要注意的几点:

①这种对默认存储类型的影响,只影响全局变量,不影响静态外部变量(因为const int a=1;和static const int a=1;的效果是一样的)和静态局部变量(在代码块内部,不涉及到多文件),或者是自动变量(也是在代码块内部);

 

②如果需要需要改变其内部链接性,在其前面加关键字extern,那么在这个文件之中声明的常量,又变成了全局变量,例如:extern const int a = 5;

 

③在②的基础上,其他文件如果需要引用这个变量的话,和正常引用并没有区别。只是在定义的时候有了变化(正常情况下的定义,是无需加extern的);

 

 

 

函数和链接性:

函数,也是具有链接性的。

 

首先,由于C++语言不允许在一个函数中定义另一个函数,因此,不存在无链接性的函数。

 

默认情况下,函数的链接性是外部的,即全局可用。

在加了static关键字的情况下,函数的链接性是内部的。

 

如:

static void abc(int); //函数原型

static void abc(int a) //函数定义

{

std::cout << a << std::endl;

}

 

这个abc()函数,便只能在其所在cpp文件内部使用。

注意:原型和函数定义之前都需要加入关键字static。

函数的链接性是内部的情况下,可以在其他文件中声明同名函数(不会影响这个链接性为内部的函数)。同时,这个函数会让链接性为外部的同名函数隐藏。

 

 

单定义规则 也适合 非内联函数。因此对于每个非内联函数,程序只能包含一个定义(不是声明)。对于链接性为外部的函数来说,这意味着在多文件程序中,只能有一个文件(该文件可能是库文件,而不是用户自己提供的)包含该函数的定义,但使用该函数的每个文件都应该包含该函数的函数原型。

 

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

 

如代码:

//1.h
#pragma once
#include<iostream>
inline void ab(int a)
{
	std::cout << a << std::endl;
}
void abc(int a);
inline void de()
{
	std::cout << "111" << std::endl;
}

//1.cpp
#include"1.h"

int main()
{
	using namespace std;
	ab(1);
	abc(2);
	de();
	system("pause");
	return 0;
}

//2.cpp
#include"1.h"

inline void ab(int a,int b)
{
	std::cout << a+b << std::endl;
}

void abc(int a)
{
	ab(a);
	ab(a, 2);
}

输出:


1
2
4
111
请按任意键继续. . .

解释:

①头文件1.h引用头文件iostream,于是每个引用1.h的cpp文件,都相当于引用了iostream这个头文件;

 

②头文件1.h定义了内联函数ab()、de()和声明了函数abc(),于是引用该头文件的文件,相当于有了两个内联函数定义和一个函数声明;

 

③1.cpp之中,引用了头文件1.h,于是可以调用两个不同名内联函数和abc()函数;

 

④2.cpp之中,引用了头文件1.h,于是abc()函数中可以调用内联函数ab();

 

⑤注意,因为引用了头文件1.h,因此这两个文件都不能再次声明同名内联函数了,但可以声明其重载函数;

 

⑥在2.cpp中,声明了重载内联函数inline void ab(int a,int b),

 

 

C++查找函数的方式:

假如程序中某个文件调用一个函数,按如下顺序进行:

①若该文件中的函数原型指出该函数是静态的,则编译器只在该文件内查找函数定义;

②否则,编译器(包括链接程序)将在所有程序文件中查找(因为非①的情况下,只可能是因为该函数是全局的);

③如果查找到两个定义,编译器提示出错(静态的情况下,只在文件内查找,不会查找到其他文件中的同名函数);

④如果还没查找到,编译器将在库中查找(所以如果定义了一个和库中的同名函数,将优先使用自己的(第②和③步),而不是库中的;

⑤然而,C++保留了标准库函数的名称,这意味着程序员不应使用它们(这是说不应该使用这些库函数的名称作为同名函数吗?

⑥有些编译器-链接程序(到底是编译器还是连接程序?)要求显式地指出要搜索哪些库。

 

 

 

语言的链接性:

不是很懂,大概是讲,函数名在链接程序链接时,要求每个函数的名字都不一样,于是,不同的函数会翻译成不同的函数名,比如加个下划线什么的。

C语言中,一个函数名只能对应一个函数,所以C语言编译器只需要(这是一种可能,不是每个都这么做)把函数名之前加个下划线即可,例如函数abc变成_abc。

 

C++语言中,一个函数名可能对应多个函数(例如链接性外部和内部的同名函数、函数重载等),C++编译器必须执行 名称矫正 或 名称修饰 (注,虽然看过,但不是很懂),为其生成不同的名字(特别是重载函数)。

某一种办法是,根据其参数类型。例如void abc(int);变成_abc_i,而void abc(double,double);变成_abc_d_d这样的。

如果要在C++语言中使用C语言库中预编译的函数(两种变化方法不同,例如_abc和_abc_i),需要这么做:

extern "C" void abc(int);

就是说,这个函数原型使用C语言链接性;

而:

extern void abc(int);

extern "C++" void abc(int);

就是使用C++语言链接性。前者是使用默认的,后者是显式说明,但实质一样。

不过为什么要加extern,不明白,是因为表示是引用全局变量么?

 

 

 

存储方案和动态分配:

之前有五种C++为变量分配内存的方案(自动变量、寄存器变量和三种静态的)(不包括线程的)。

 

除此之外,还有用运算符new(在C语言之中是malloc())分配的内存,这种内存称为 动态内存 

 

动态内存使用运算符newdelete来控制。而不是通过作用域和链接性规则控制(因此即使一个函数消亡了,在其内部new的内存也依然存在),于是可以在一个函数中new,另一个函数中delete

 

动态内存并非LIFO(自动变量是),其分配和释放熟顺序要取决于newdelete在何时以何种方式来使用。

 

通常,编译器使用三块独立的内存:

①用于静态变量;

②用于自动变量(栈);

③用于动态存储(堆heap);

 

虽然存储方案不适合动态内存,但适合用于跟踪动态内存的自动和静态指针变量。例如在函数之中new了一个指针a,然后有一个外界的指针b被这个指针赋值(于是他们指向的地址相同,都是这个new出来的指针),那么即使指针a所在函数消亡,这个指针b依然可以继续使用这个new出来的地址。

 

也可以将指针a声明为一个链接性为外部的指针(像正常声明一个静态持续性,链接性为外部的变量那样,例如int *a;),那么在其他文件,就可以正常使用这个指针了(也需要声明,例如extern int*a,和普通的声明并没有什么区别)。

 

 

关于new

①声明并初始化:

用括号括起来初始化的值,一个值用括号“()”或“{}”,多个值(需要C++11支持)用大写括号“{}”。

如:

int *a = new int(5); //效果是在new基础上赋值5

int *b = new int[3]{ 1,3,5 }; //效果是new一个数组的基础上,给数组成员分别赋值1、3、5

struct abc { int a; int b; };

abc *a = new abc{ 1,3 }; //new一个结构a(使用abc的结构定义),并给成员赋值1和3。注意,结构和上面的数组,都需要C++11支持才可以。

double*b = new double{}; //这个会认为在new的时候初始化了(没括号是认为没有初始化)。大括号或者小括号都可以,值为0

 

new失败时:

以前是返回空指针,现在引发异常std::bad_alloc。具体要见第十五章(所以太远咯);

 

 

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

运算符newnew[] 分别调用以下函数:

void * abc new (std::size_t) ;

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

这些函数被称为 分配函数 ,他们位于全局名称空间之中。同样,也有由deletedelete[]调用的 释放函数 ,如:

void * abc delete (std::size_t);

void * abc delete[] (std::size_t) ;

他们将使用第11章讨论的运算符重载语法。

 

另外,std::size_t 是一个typedef(函数定义),对应适合的整型。

似乎指,就像这样void * abc new(int); 么?不懂

 

对于下面这样的基本语句:

int *pi = new int;

将被转换为:

int *pi = new (sizeof(int) );

 

而int *pa = new int[10];  将被转换为 int *pa = new (10*sizeof (int) );

 

运算符new可以包含初始值(上面刚刚说过)。因此,使用new运算符时,可能不仅仅是调用new()函数。——还是不懂

 

delete同理,语句:

delete pi;

被转化为:

delete (pi);

C++将这些函数称为可替换的。

也就意味着,如果有足够的知识和意愿,可为newdelete提供替换函数,并根据需要对其进行定制。

例如,可以定义作用域为类的替换函数,并对其进行定制,以满足该类的内存分配需求。在代码中,仍使用new运算符,但它将调用您定义的new()函数。

 

注:以上整个③都真心不太懂是什么意思。

 

 

 

 

④定位new运算符

通常,new是在堆(heap)中找到一个足以能够满足要求的内存块。

 

但有时候,我们需要对new的内存的位置有一定要求,于是,new运算符还有一种变体,并成为定位new运算符,它让程序员可以指定要使用的位置。

 

程序员可能使用这种特性来设置其内存管理规程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。

 

要使用定位new特性,首先要包含头文件new(#include<new>这样),它提供了这种版本的new运算符的原型;

然后将new运算符用于提供了所需地址的参数。除了需要指定参数外,句法与常规new运算符相同。

 

具体的说,使用定位new运算符时,变量后面可以有方括号,也可以没有。

 

new运算符的用法有四种(包括定位与非定位):

①int *a=new int;

②int *a=new int[5];

③int *a = new (地址) int;

④int *a = new (地址) int[5];

 

③和④是定位new运算符。

 

另外,③和④中的地址,指的是从提供的该地址开始,即可以理解为,指针指向该地址。

至于这个地址指向是堆(heap,动态内存)或者是栈或者是其他,C++程序是不管的,它将这个任务交给了使用他的程序员。

 

另外,因为不一定指向堆,所以也不一定可以被delete。因为假如指向静态变量的话,delete这个指针,会出错。

 

如代码;

#include<iostream>
using namespace std;

const int m = 3;
const int n = 20;

int main()
{
	const int* p[n];	//声明一个指针数组p
	int *a = new int;	//第一种new
	int *b = new int[m];	//第二种new
	//以上是常规new
	int *c = new(p)int;	//第三种new
	int *d = new(p)int[m];	//第四种new
	//以上两种的定位new

	//以下输出地址
	cout << "p = " << p << endl;
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
	cout << "d = " << d << endl;
	cout << endl;

	//换一个地址定位:
	double *e = new(p + 2)double;		//定位在p指针再加2个int宽度的位置(即p[2]),注意这里的+2,加的是int的宽度(跟p同类型,而不是跟后面的double同类型)
	cout << "e = " << e << endl;
	cout << "p[2] = " << &p[2] << endl;	//如果加的不是int宽度(而是double宽度的话),那么这里的地址应该和e不同
	cout << "e-2 = " << e - 2 << endl;	//如果加的double宽度,那么这个值就应该和p是同一个地址(因为-2是往左偏移2个double宽度),事实上,这个地址和p不同
cout << endl;

	//使用sizeof
	double*f = new(p + sizeof(p))double;	//p是p[20],p是int类型(一个int占4字节)所以sizeof(p)是80,所以f应该和p[80]是一个地址(+0是和p[0])在同一个地址
	cout << "f = " << f << endl;
	cout << "p[80] = " << &p[80] << endl;	//虽然p并没有第81个成员,但是可以用这样来输出地址
	
	system("pause");
	return 0;
}

输出:


p = 0033FE64
a = 00770858
b = 00770D50
c = 0033FE64
d = 0033FE64

e = 0033FE6C
p[2] = 0033FE6C
e-2 = 0033FE5C

f = 0033FFA4
p[80] = 0033FFA4
请按任意键继续. . .

总结:

ab是普通new,所以在动态内存之中(0033打头);

 

②因为cd是定位new,且定位是的p的位置,所以pcd的地址是一样的;

 

③又因为p是自动变量,所以cd也指向自动变量的内存区域(0014打头);

 

e相对c来说,在括号里+2。这里的+2,相对而言是偏移了2int宽度的内存地址(相对p)来说,如果后面是double,那么就是偏移了2double宽度的内存地址。

 

⑤括号里的是地址,若有加减,偏移的是有地址的变量的类型的宽度,例如上面pint,那么+ / -都是以int为单位,+1是往右偏移1int的宽度。

 

new出来的ab指针,是可以delete的,但是cdef指针,是不能delete的(因为指向位置不是动态内存);


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值