存储持续性(storage duration)
定义:指一个变量在存储器中能够存在多长时间;
c++11使用4种不同的方案来存储数据,这些方案的主要不同在于数据保留在内存中的时间。
作用域
定义:描述了名称在文件的多大范围内可见,例如,在函数定义中的变量可在该函数中使用,但不能在其他函数中使用,而在文件中函数定义之前定义的变量则可在所有函数中使用。
链接性
定义:描述名称如何在不同的单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。
自动存储持续性
(1)自动存储持续性:在函数定义中声明的变量(包括函数的参数)的存储持续性为自动的。它们在程序开始执行其所属函数或者代码块时被创建,在执行完函数或者代码块后,它们使用的内存被释放。c++中有两种存储连续性为自动的变量。
当程序开始执行这些变量所属的代码块时,将为其分配内存,当函数结束后,这些变量都将消失(注意:当执行代码块(代码块定义为花括号括起来的一系列语句)时,将为其变量分配内存,但其作用域的起点为其声明位置。)
如果内部代码块中的变量命名未teledeli,而不是websight,使得有两个同名的变量(一个在外部代码块,另一个在内部代码块中)这种情况下,执行内部代码块时,将teledeli解释成局部代码块变量。也就是说,新的定义(内部的teledei变量)隐藏了以前的定义(外部的teledei变量),新定义可见,旧定义暂时不可见,当程序离开该内部代码块后,原来的定义就重新可见。
自动变量和栈
由于自动变量的数目随函数的开始和结束而增减所以程序必须在运行时对自动变量进行管理,常用的方法就是留出一段内存,并将其视为栈,以管理变量的增减。
被称为栈的原因:是由于新数据被象征性地放在原有数据的上面(在相邻的内存单元中,而不是在同一个内存单元中),当程序使用完成后,将其从栈中删除。
栈的默认长度取决于实现,但是编译器通常提供改变栈长度的选项。程序使用两个指针来跟踪栈,一个指针指向栈底——栈开始的地方,一个指针指向栈顶——下一个可用内存单元。当函数被调用时,其自动变量将别放入到栈中,栈顶指针指向变量后面的下一个可用的内存单元。
栈是先进后出的,也就是最后加入到栈中的变量首先被弹出。这种设计简化了参数传递。函数调用时,将其参数的值放在栈顶,然后重新设置栈顶指针。被调用的函数根据其形参描述来确定每个参数的地址。(见下图)
当函数fib()被调用时,传递一个2字节的int和一个4字节的long。这些值被加入到栈中。当fib()开始执行时,它将real和tell同这两个值关联起来。当fib结束时,栈顶指针重新指向以前的位置。新值并没有被删除,但是不再被标记,它们所占的空间将被下一个将值加入到栈中的函数调用所使用。
寄存器变量
静态存储连续性
(2)静态存储连续性:在函数定义外的变量和使用关键字static定义的变量的存储连续性都是静态。它们在程序整个运行过程都存在。c++中有三种存储连续性为静态的变量。函数结束后,栈顶指针被重置为函数调用前的值,从而释放新变量使用的内存。
c++为静态存储持续性变量提供了3种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或者代码块中访问)。这三种链接性都在整个程序执行期间存在,与自动变量相比,它的寿命更长。
由于静态变量的数目在程序运行期间是不变的,所以程序不需要使用特殊的装置(如栈)来管理它们。编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。另外,如果没有显式地初始化静态变量,编译器将把它设置为0,在默认情况下,静态数组和结构将每个元素或者成员的所有位都设置成0。
上述程序中global(在函数定义外的变量)、one_file(函数外部使用关键字static声明的变量)以及count(函数内部使用关键字static声明的变量)都是静态持续变量。在funct1()中声明的变量count的作用域是局部的,没有链接性(要创建没有链接性的静态持续变量,必须在代码块内声明它,并且使用static限定符);这就意味着只能在funct1函数中使用它,这点和自动变量llama一样,与llama不同的是,即使funct()函数没有被执行时,count也留在内存中。
global和one_file的作用域都为整个文件,即从声明位置到文件末尾的范围内都可以被使用,具体而言,就是可以在main()、funct1()、funct2()中使用它们。其中,one_file的链接性为内部(要创建链接性是内部的静态持续变量,必须在代码块外面声明它,并且使用static限定符),只能在包含上述代码的文件中使用它;global的链接性是外部(要创建链接性为外部的静态持续变量,必须在代码块外面声明它),所以可以在程序的其他文件中使用它。
所有的静态存储变量都有以下的初始化特征:未被初始化的静态变量的所有位都被设置为0(不管程序员是否显式地初始化了它,它必须经过这一步),这种变量被称为零初始化的
指出关键字是static的两种用法,,但是含义所有不同:用于局部声明,以指出变量是无链接性的静态变量时,static表示的是存储连续性(区别于自动存储);而由于代码块外的声明时,static表示的内部链接性,而变量已经是静态持续性了。可以理解为关键字重载,关键字的含义取决于上下文。
静态变量的初始化
除了默认的(所有的变量都会被零初始化)零初始化以外,还可以对静态变量进行常量表达式初始化和动态初始化。
零初始化:将变量设置为零,对于标量类型,零被强制转换成合适的类型。例如,在c++代码中,零初始化意味着将变量设置为0,但内部可能采用非零表示,因此指针变量将被初始化相应的内部表示。结构成员被零初始化,且填充位都被设置为零。
零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译处理文件时初始化变量,动态初始化意味着变量在编译后初始化。
首先,所有的变量都被零初始化,不管程序员是否显式地初始化了它,所以首先x、y、z和pi都被零初始化;接下来,如果使用常量表达式初始化了变量(上面的y和z),且编译器仅根据文件内容(包含被包含的头文件)就可以计算表达式,编译器将执行常量表达式初始化,这里y和z分别初始化为5和169;但是要初始化pi,就必须调用atan(),这需要等到该函数链接且程序执行时。
静态持续性、外部链接性
链接性为外部的变量简称为外部变量,它的存储持续性为静态,作用域为整个文件。外部变量是在函数外部定义的,因此对所有函数而言是外部的。例如,可以在main()前面或头文件中定义它们。可以在文件中位于外部变量定义后面的任何函数中使用它,因此外部变量也称全局变量(相对于局部的自动变量)。
单定义规则
一方面,在每个使用外部变量的文件中,都必须声明它;另一个方面,c++有“单定义规则”,变量只能有一次定义。为了满足这种需求,c++提供了两种变量声明。
(1)一种是定义声明或者简称为定义,它给变量分配存储空间;
(2)另一种是引用声明或者简称为声明,它不给变量分配存储空间,因为它们引用已有的变量。
引用声明使用关键字exrern,且不进行初始化,否则,声明为定义,导致内存分配。
如果需要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义(单定义规则),但在使用该变量的其他所有文件中,都必须使用关键字extern声明它。
在此处,所有的文件都使用了在file01.cpp中定义的变量cats和dogs,但是file02.cpp没有声明变量fleas,所以无法访问它。在文件file01.cpp中,关键字extern并非必不可少。即使忽略它效果也相同。
请注意,单定义规则并非意味着不能有多个变量的名称相同。例如,在不同函数中(注意是在函数中)声明的同名自动变量是彼此独立的,它们都有自己的地址。
定义与全局变量同名的局部变量后,局部变量将会隐藏全局变量,在c++中,提供了作用解析运算符(::),将它放在变量名前,该运算符表示使用变量的全局版本。
全局变量和局部变量:
静态连续性、内部连接性
首先再复习一下,将static限定符用于作用域为整个文件的变量中,该变量的链接性是内部。在多个文件程序中,链接性是内部或者是外部差别很大,链接性是内部的变量只能在其所属文件中使用。
上面两个不同的文件,声明了相同名称的外部变量(链接性是外部的),是错误地,因为它违反了单定义规则,有两个定义。
但如果是静态外部变量(使用关键字static,链接性是内部的),就不会引发错误,如下所示,声明了一个与另一个文件中声明的常规外部变量相同,则在该文件中,静态变量将隐藏常规外部变量。
也就是说运行文件file2时,使用关键字static声明的静态外部变量将覆盖文件file1中的声明的常规外部变量。此时使用cout对errors进行输出时,输出的是5,这并没有违反单定义规则,因为关键字static指出标识符errors的链接性是内部,故并非要提供外部定义。
注:在多文件程序中,可以在一个文件(且只能在一个文件中)中定义一个外部变量。使用该变量的其他文件必须使用关键字extern声明它。
可使用外部变量在多文件程序的不同部分之间共享数据;可使用链接性是内部的静态变量在同一个文件中的多个函数之间共享数据(名称空间提供了另一中共享数据的方法)。此外,如果将作用域为整个文件的变量变成是静态的,就不必担心其名称与其他文件中的作用域为整个文件的变量发生冲突。
程序解读:文件twofile2.cpp使用了static限定符定义了与twofile1.cpp(该文件中的dick是常规的外部变量)同名的静态外部变量dick,static限定符将该变量限制在twofile2.cpp中这个文件中,并且覆盖文件twofile1.cpp中定义的全局变量dick。
静态存储联系性、无链接性
回顾无链接性的静态变量定义方式:使用static限定符用于在代码块中定义的变量。在代码块中使用static时,将局部变量的存储连续性为静态,这就意味着该变量只能在该代码块中使用,但它区别于自动存储的是,它在该代码块不处于活动状态时仍然存在。此外,如果初始化了静态局部变量,则程序只在启动时进行一次初始化。
// static.cpp -- using a static local variable
#include <iostream>
// constants
const int ArSize = 10;
// function prototype
void strcount(const char * str);
int main()
{
using namespace std;
char input[ArSize];
char next;
cout << "Enter a line:\n";
cin.get(input, ArSize);
//将一直读取输入,直到到达行尾(输入的字符数在ArSzie-1内,字符型数组结尾字符为\0,占一个空间)或读取了AeSize-1个字符为止。
while (cin)
{
cin.get(next);// 使用cin.get(next)读取行输入后的字符,如果next是换行符,则说明cin.get(input,ArSize)读取了整行;否则说明行中还有字符没有读取。
while (next != '\n') // string didn't fit!
cin.get(next); // dispose of remainder 这行代码处理为多余的字符,把剩下的字符从内存池中读完。
strcount(input);
cout << "Enter next line (empty line to quit):\n";
cin.get(input, ArSize);
}
cout << "Bye\n";
// code to keep window open for MSVC++
/*
cin.clear();
while (cin.get() != '\n')
continue;
cin.get();
*/
return 0;
}
void strcount(const char * str)
{
using namespace std;
static int total = 0; // static local variable
int count = 0; // automatic local variable
cout << "\"" << str <<"\" contains ";
while (*str++) // go to end of string
count++;
total += count;
cout << count << " characters\n";
cout << total << " characters total\n";
}
Enter a line:
dsjaodhwoqia
"dsjaodhwo" contains 9 characters
9 characters total
Enter next line (empty line to quit):
qhoiwhefiqwhefi
"qhoiwhefi" contains 9 characters
18 characters total
Enter next line (empty line to quit):
dheu
"dheu" contains 4 characters
22 characters total
Enter next line (empty line to quit):
" " contains 1 characters
23 characters total
Enter next line (empty line to quit):
Bye
C:\Users\11157\Desktop\Chatroom\x64\Release\Client.exe (进程 17020)已退出,代码为 0。
按任意键关闭此窗口. . .
每次函数被调用时,自动变量count都被重置为0,但是静态变量total只在程序运行时被设置成0,以后在两次函数调用之间,其值保持不变,所以它可以记录读取的字符总数。
说明符和限定符
存储说明符例如
(1)auto:c++11之前是,现在不是,之前是用来声明自动变量,现在用于自动类型推断
(1)register:之前用于在声明中指示寄存器存储,但在c++11中,只是显式地指出变量是自动的
(2)static:用在作用域为整个文件的声明中时,表示内部链接性,被用于局部声明中,表示局部变量的存储持续性是静态的。
(3)extern:关键字extern表明是引用声明,即引用在其他地方定义的变量。
(5)thread_local:指出变量的持续性与其所属线程的持续性相同。thread_local变量之于线程,就想常规静态变量之于整个程序。
(6)mutable:用它指出即使结构(或者类)变量为const,其某个成员也可以被修改。
#include <string.h>
struct data
{
char name[30];
mutable int accesses;//声明成员accesses时使用了mutable限定符
};
const data veep = { "Claybourne Clodde", 0 };
strcpy(veep.name, "Joye Joux");//不允许被修改,veep的const限定符禁止程序修改veep成员
veep.accesses;//允许被修改,,access成员的mutable说明符使得access不受限制
cv-限定符(c只得就是const,v指的就是volatile)
const:内存被初始化后,程序便不能再对其进行修改。
在默认情况下,全局变量的链接性是外部,但是const全局变量的链接性是内部,就像使用了static说明符一样。
这中规则可让程序员变的更轻松,可以将一组常量放在头文件中,并在同一个程序员的多个文件中使用该头文件。那么,预处理器将头文件的内容包含到每个源文件中,所有的源文件都将包含这组常量的定义。所以如果全局const的链接性是外部的话,进行上面的一系列操作则别违背了单定义规则(因为是头文件中的是const常量的定义)。
内部链接性还表明每个文件都有自己的一组常量,而不是所有文件共享一组常量。每个定义都是其他所属文件私有的,这就是能够将常量定义放在头文件中使用的原因。这样,只需在两个源代码文件中包含一个头文件,则它们将获得同一组常量。
如果程序员需要某个const常量的链接性是外部的,可以使用extern关键字来覆盖默认的内部链接性:例如:extern const int states = 50;//定义了一个链接性是外部的const常量
这与常规的外部变量不同的时,定义它时必须使用extern关键字,但是定义常规外部变量则不需要。同时,也只能在一个文件中定义它。
在函数或者代码块汇中声明const时,其作用域是代码块,仅仅当程序执行该代码块时,该常量才有用,这就意味着在函数或者代码块中创建常量时,不用担心其名称与其他地方定义的常量发生冲突。
volatile:表明即使程序没有对内存单元进行修改,其值也可能发生变化。
函数与链接性
和变量一样,函数也有链接性。(默认情况下函数为静态存储,外部链接性)
和c一样,c++不允许在一个函数中定义另外一个函数,所以函数的存储持续性都自动为静态,即在整个函数执行期间都一直存在。
在默认情况下,函数的链接性是外部的,即可以在文件之间共享。(实际上,可以使用关键字extern来指出函数是在另一个文件中定义的,这个操作是可选的)要想程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件。
还可以使用关键字static使得函数的链接性设置为内部,使之只能在一个文件中使用。这个操作需要注意的是必须同时在函数原型和函数定义中使用该关键字。这个操作就意味着该函数只能在这个文件中可见,也意味着可以在其他文件中定义与之同名的函数。
单定义规则也适用于非内联函数,因此对于每个非内联函数,程序只能包含一个定义。
对于链接性是外部的函数来说,这意味在多文件程序中,只能有一个文件(该文件可能是库文件)包含函数的定义(也就意味着不能将其放在头文件中,因为在其他文件中导入该头文件时,就意味着重复定义了该函数),但是使用该函数的每个文件都应该包含该函数的原型(可以将函数原型放在头文件中)。
内联函数不受单定义规则的限制,所以可以将内联函数的定义放在头文件中。这样包含了头文件的每个文件中都有内联函数的定义。c++要求同一函数的所有内联定义都必须相同。
语言链接性
线程存储连续性
(3)线程存储连续性:对于多核处理器,这些CPU可同时处理多个执行任务。这些程序能够处理将计算放在可并行处理的不同线程中。如果使用关键字thread_local声明的,则整个生命周期与所属线程一样长。
动态存储持续性
(4)动态存储持续性:用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或者程序结束为止。这种存储持续性为动态,有时被称为自由存储或者堆。
动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制,所以可以在一个函数分配动态内存,而在另一个函数中将其释放。一般,编译器使用三块独立的内存:一块用于静态变量,一块用于自动变量,另外一块用于动态存储。
虽然存储方案概念不适用于动态内存,但是适用于用来跟踪动态内存的自动和静态指针变量。例如,假设在一个函数中包含下面的语句:
float *p_fees = new float [20];
上面的语句表明由new分配的80个字节(假设float是4个字节)的内存一直保留在内存中,直到使用delete运算符对其进行释放,但是当包含该声明的代码块执行完毕后,p_fees指针将消失。
所以想要另一个函数可使用这80个字节的内容,则必须将其地址传递或返回给该函数。或者将p_fees的链接性声明为外部的,则文件中位于声明后面所有函数都可使用它,另外,在另一个文件中使用extern float * p_fees;便可使用这个指针。
(1) 使用new运算符初始化:
要为内置的标量类型(如int或者double)分配存储空间并初始化,可在类型名后面加上出初始值,并将其用括号括起来:
如:int *pi = new int (6);//*pi 为6
要初始化常规结构或者数组,需要使用大括号的列表初始化,
如:struct where {double x; double y; double x};
where *one = new where {2.5, 5.3, 7.2};
int * ar = new int [4] {2,4,6,7};
还可将列表初始化应用于单值变量:
double * pi = new double {99.99};//*pd 为99.99
(2)new失败:会引发std::bad_alloc异常;
(3)new:运算符、函数和替换函数
(4)定位new运算符
new运算符还要另一种变体,称为定位(placement)new运算符,它可以指定要访问的位置,可以通过使用它来(1)设置内存管理规程;(2)处理需要通过特定地址进行访问的硬件;(3)在特定位置创建对象。
要想使用这个版本地new(定位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;
//new的常规形式
p1 = new chaff; //将结构放入堆中
p3 = new int[20]; //将数组放入堆中
//new的第二种形式
p2 = new (buffer1) chaff; //将结构放在字符数组buffer1中
p4 = new (buffer2) int[20]; //将int数组放在字符数组buffer2中
//这里使用了两个静态数组来为定位运算符提供内存空间。
// 从buffer1中分配空间给结构chaff,从buffer2中分配空间给有一个包含20个元素的int数组。
}
常规new运算符和定位new运算符的区别
// newplace.cpp -- using placement new
#include <iostream>
#include <new> // for placement new
const int BUF = 512;
const int N = 5;
char buffer[BUF]; // chunk of memory buffer是double类型的指针
int main()
{
using namespace std;
double* pd1, * pd2; //声明了两个double类型的指针,pd1和pd2是double类型的指针
int i;
cout << "Calling new and placement new:\n";
pd1 = new double[N]; // use heap //常规new运算符:使用new运算符初始化指针pd1,将浮点数组(注意是数组,因为此处是方括号)放在堆中
pd2 = new (buffer) double[N]; // use buffer array //定位new运算符:使用一个char数组(为静态数组)为定位new运算符提供内存空间,
//将浮点数组放在buffer中,从buffer中分配空间给一个包含5个元素的浮点数组
for (i = 0; i < N; i++)
pd2[i] = pd1[i] = 1000 + 20.0 * i;
cout << "Memory addresses:\n" << " heap: " << pd1
<< " static: " << (void* )buffer << endl;
cout << "Memory contents:\n";
for (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:\n";
double* pd3, * pd4;
pd3 = new double[N]; // find new address
pd4 = new (buffer) double[N]; // overwrite old data
for (i = 0; i < N; i++)
pd4[i] = pd3[i] = 1000 + 40.0 * i;
cout << "Memory contents:\n";
for (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:\n";
delete[] pd1;
pd1 = new double[N];
pd2 = new (buffer + N * sizeof(double)) double[N];
for (i = 0; i < N; i++)
pd2[i] = pd1[i] = 1000 + 60.0 * i;
cout << "Memory contents:\n";
for (i = 0; i < N; i++)
{
cout << pd1[i] << " at " << &pd1[i] << "; ";
cout << pd2[i] << " at " << &pd2[i] << endl;
}
delete[] pd1;
delete[] pd3;
// cin.get();
return 0;
}
在第20行代码中的(void*)buffer的解释:因为buffer是一个char*类型的指针,在前面的笔记《指针》中字符串和指针一节的中有解释,一般来说,给cout一个指针变量,它将输出与指针变量相对应的地址,但是如果是char*指针的话,cout将显示指向的字符串,如果想要显示字符串的地址,必须将这种指针强制转化成另一种指针类型,如(int*),此处使用(void*)进行强制转化也可以。
例如将char类型指针转换为int型指针,并不会改变内存的实际内容,只是修改了解释方式而已。
差别一:内存的分配方式不同:常规在堆中分配,定位new由程序员指定。
还需注意的点时:通过输出来看,第一个常规new将数组p1放在很远的地方,位于动态管理的堆中,起始地址是 00000156A76FBBB0。第二个常规new运算符将查找一个新的内存块,第二个定位new运算符分配跟以前一样的内存块,因为定位运算符使用传递给它的地址(这里是buffer),它不跟踪哪些内存单元已被使用,也不查找未使用的内存块。在第三次调用定位new运算符时,提供了一个从数组buffer开头算起的偏移量,所以分配了新的内存空间。
差别二:是否需要使用delete来释放内存;
delete只能用于指向常规new运算符分配的堆内存,这里的buffer指向的内存时静态内存,位于delete管辖范围之外,所以不能像p1和p3那样使用delete释放,语句delete [ ] pd2;是错的。
如果此处的buffer时由常规new运算符创建的,则可以使用delete来释放整个内存块。
定位new运算符的工作原理:它只是返回出传递给它的地址,并将其强制转换成为void*,以便于可以赋值给任何指针类型。
关于void*:(下面的内存转载自https://blog.csdn.net/qq_44823010/article/details/107452811)
指针的类型不过是解释数据的方式不同罢了,这样的道理也可用于很多场合的强制类型转换,例如将int类型指针转换为char型指针,并不会改变内存的实际内容,只是修改了解释方式而已。而void* 是一种无类型指针,任何类型指针都可以转为void*,它无条件接受各种类型。
而既然是无类型指针,那么就不要尝试做下面的事情:
1.解引用
2.算术运算
由于不知道其解引用操作的内存大小,以及算术运算操作的大小,因此它的结果是未知的。是未知的东西就会导致结果是随机的或者导致程序崩溃。
为何要如此设计?因为对于这种通用型接口,你不知道用户的数据类型是什么,但是你必须能够处理用户的各种类型数据,因而会使用void*。void能包容地接受各种类型的指针。
也就是说,如果你期望接口能够接受任何类型的参数,你可以使用void类型。
但是在具体使用的时候,你必须转换为具体的指针类型。例如,你传入接口的是int*,那么你在使用的时候就应该按照int*使用。
注意
使用void*需要特别注意的是,你必须清楚原始传入的是什么类型,然后转换成对应类型。
通俗地说void*:
这里有一片内存数据,我也不知道什么类型,给你了,你自己想怎么用怎么用吧,不过要用对奥!(即给你钱,你想怎么用就怎么用,但是你不能用到错误的地方,不能违法犯罪)
我这里什么类型都能处理,你给我一片内存数据就可以了。