cpp中的内存管理 静态存储 内存分配与链接性的讨论

在C++中,有三种管理数据内存的方式,自动存储,静态存储和动态存储(动态存储有时也叫作自由存储空间或者堆),在C++11标准中,新增了第四种类型,线程存储。

一、自动存储

在函数内部定义的常规变量使用自动存储空间,称为自动变量,这意味着他们在所属的函数被调用时自动产生,在函数结束时消亡,实际上,自动变量是一个局部变量,作用域为包含他的代码块,自动变量通常存储在栈中,这意味着执行代码时,其中的变量依次加入到栈中,离开代码块时,将按照相反的顺序释放这些变量,先进后出(LIFO)。因此在执行程序的过程中栈将不断的增大或者缩小。

在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。也就是说,如果在main()中声明了一个名为texas的变量,并在函数oil()中也声明了一个名为texas变量,则创建了两个独立的变量只有在定义它们的函数中才能使用它们。对oil( )中的texas 执行的任何操作都不会影响main()中的texas,反之亦然。另外,当程序开始执行这些变量所属的代码块时,将为其分配内存,当函数结束时,这些变量都将消失(注意,执行到代码块时,将为变量分配内存,但其作用域的起点为其声明位置)。

1.1、C++11中的auto

在C++11中auto用于自动类型推断,但是在C语言和之前的C++版本中,auto不是这么用的。它用于显式的之处变量为自动存储。

int fun(int n){
	auto float ford;
}

由于只能将关键字auto用于默认为自动的变量,因此程序员几乎不使用它。它的主要用途是指出当前变量为局部自动变量。在C++11中,这种方式不在合法。

1.2、C++11中的register

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

register int n;

在C+11之前,这个关键字在C++中的用法始终未变,只是随着硬件和编译器变得越来越复杂,这种提示表明变量用得很多,编译器可对其做特殊处理。在C++11中,这种提示作用也失去了,关键字registe只是显式地指出变量是自动的。鉴于关键字register只能用于原本就是自动的变量,使用它的唯一原因是指出程序员想使用一个自动变量,这个变量的名称可能与外部变量相同。这与 auto以前的用途完全相同。然而,保留关键字register的重要原因是,避免使用了该关键字的现有代码非法。

即使是在C语言中,这个关键字也是建议编译器这么做,实际上做不做编译器会有自己的判断。

二、静态存储

静态存储是整个程序执行期间都存在的存储方式,静态存储有两种定义方式,一种是在函数外定义它,一种是在函数中使用static关键字。

在K&R C中,只能初始化静态数组和静态结构,但是在C++和ANSI C中,也可以初始化自动数组和自动结构。

静态存储的变量可能存在于程序的整个生命周期。

和C语言一样,C++也为静态存储持续性变量提供了3种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中访问)。这3种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置(如栈)来管理它们。编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。另外,如果没有显式地初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0。

下面介绍如何创建这三种静态持续变量

int global = 100;
static int one_file = 50;

int main(){
    ...
}
int func1(){
	static int count = 0;
}

正如前面指出的,所有静态持续变量(上述示例中的 global、one_file和 count)在整个程序执行期间都存在。在func1()中声明的变量count 的作用域为局部,没有链接性,这意味着只能在 funct1()函数中使用它,就像自动变量 一样。然而,与自动变量不同的是,即使在 func1()函数没有被执行时,count 也留在内存中。global和 one_file 的作用域都为整个文件,即在从声明位置到文件结尾的范围内都可以被使用。具体地说,可以在main()、func1l()和 func2()中使用它们。由于one_file的链接性为内部,因此只能在包含上述代码的文件中使用它;由于global的链接性为外部,因此可以在程序的其他文件中使用它。

静态变量都将被初始化为0,然后编译器计算常量表达式,如果没有被赋值的话就是被初始化为0了。

1.1、连接性为内部和外部的变量

我们如何在另一个文件中引用全局变量global呢,这需要我们在另一个文件中再次申明一下

// 该申明的意思是使用外部变量global
extern int global;

在多文件程序中,我们定义了外部变量error,在另一个文件中我们只需要省略extern关键字就可以也声明一个同名的变量嘛,答案是不可以,这种做法违背的单变量原则,

// file1
int error = 1;

// file2
int error = 3;

这种做法将失败,因为它违反了单定义规则。file2中的定义试图创建一个外部变量,因此程序将包含errors 的两个定义这是错误。

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

// file1
int error = 1;

// file2
static int error = 3;

这没有违反单定义规则,因为关键字static指出标识符crTors 的链接性为内部,因此并非要提供外部定义

1.2、无连接性的局部变量

在代码块中使用用static时,将导致局部变量的存储持续性为静态的。这意味着虽然该变量只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在。因此在两次函数调用之间,静态局部变量的值将保持不变。另外,如果初始化了静态局部变量,则程序只在启动时进行一次初始化以后再调用函数时,将不会像自动变量那样再次被初始化。

三、动态存储

new和delete运算符提供了一种比自动变量和静态变量更加灵活地方式,他管理一个内存池,这称作自由存储空间(free store)或者堆(heap)。因此数据的生命周期不全收程序或者函数的生存时间控制,与常规变量相比,new和delete给了程序员更大的操作内存的权限。然而,内存管理也更加复杂了。在栈中,自动添加和删除机制使得占用内存总是连续的。但是动态存储会导致内存不连续,分配和跟踪新的内存更加困难

内存泄露是动态存储的主要问题,我们申请的内存没有及时的释放,导致这一块内存在程序的生命周期一直不可用,导致计算机内存不足,out of memery,严重的导致系统崩溃。C++智能指针有助于自动完成指针的回收。

在C语言中我们用malloc()和free()来实现这个功能,现在我们用new和delete来实现。

3.1、分配内存
new data-type
double* pvalue  = NULL; // 初始化为 null 的指针
pvalue  = new double;   // 为变量请求内存

如果自由存储区已被用完,可能无法成功分配内存。所以建议检查 new 运算符是否返回 NULL 指针,并采取以下适当的操作:

double* pvalue  = NULL;
if( !(pvalue  = new double ))
{
   cout << "Error: out of memory." <<endl;
   exit(1);
}

malloc() 函数在 C 语言中就出现了,在 C++ 中仍然存在,但建议尽量不要使用 malloc() 函数。new 与 malloc() 函数相比,其主要的优点是,new 不只是分配了内存,它还创建了对象。

在任何时候,当您觉得某个已经动态分配内存的变量不再需要使用时,您可以使用 delete 操作符释放它所占用的内存,如下所示:

delete pvalue;        // 释放 pvalue 所指向的内存
3.2、为数组分配内存
char* pvalue  = NULL;   // 初始化为 null 的指针
pvalue  = new char[20]; // 为变量请求内存

要删除我们刚才创建的数组,语句如下:

delete [] pvalue;        // 删除 pvalue 所指向的数组

下面是 new 操作符的通用语法,可以为多维数组分配内存,如下所示:

一维数组

// 动态分配,数组长度为 m
int *array=new int [m];
 
//释放内存
delete [] array;

二维数组

int **array
// 假定数组第一维长度为 m, 第二维长度为 n
// 动态分配空间
array = new int *[m];
for( int i=0; i<m; i++ )
{
    array[i] = new int [n]  ;
}
//释放
for( int i=0; i<m; i++ )
{
    delete [] array[i];
}
delete [] array;

三维数组

int ***array;
// 假定数组第一维为 m, 第二维为 n, 第三维为h
// 动态分配空间
array = new int **[m];
for( int i=0; i<m; i++ )
{
    array[i] = new int *[n];
    for( int j=0; j<n; j++ )
    {
        array[i][j] = new int [h];
    }
}
//释放
for( int i=0; i<m; i++ )
{
    for( int j=0; j<n; j++ )
    {
        delete[] array[i][j];
    }
    delete[] array[i];
}
delete[] array;
3.3、创建对象
#include <iostream>
using namespace std;
 
class Box
{
   public:
      Box() { 
         cout << "调用构造函数!" <<endl; 
      }
      ~Box() { 
         cout << "调用析构函数!" <<endl; 
      }
};
 
int main( )
{
   Box* myBoxArray = new Box[4];
 
   delete [] myBoxArray; // 删除数组
   return 0;
}

四、线程存储

当前的多核处理器很常见,一个CPU可以处理多个任务,这让程序能够将计算放在不同的线程中,如果变量是使用关键字thred_local申明的,则这个变量的生命周期和所属的线程一样长,我们暂且不探讨并行编程。

thred_local之于线程就像static之于整个程序,存在于整个生命周期。

五、一些关键词和链接性

volatile

关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。.听起来似乎很神秘,实际上并非如此。例如,可以将一个指针指向某个硬件位置,其中包含了来自串行端口的时间或信息。在这种情况下,硬件(而不是程序)可能修改其中的内容。或者两个程序可能互相影响,共享数据。该关"“键字的作用是为了改善编译器的优化能力。例如,假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。:这种优化假设变量的值在这两次使用之间不会变化。如果不将变量声明为volatile,则编译器将进行这种优化;将变量声明为volatile,相当于告诉编译器,不要进行这种优化。

其实就是CPU在取数据的时候,这个关键字保证了这个数据一定是从内存中取出来的,而不是读取的寄存器或者三级缓存中的。在多线程编程中这种操作保证的数据的完整性。

mutable

这个关键字指出,即使结构或者类变量为const的,其中的某个成员也可以被修改。

struct data {
	char name[30];
	mutable int access;
}

const data veep = {"OK",0};
veep.access = 100;

上面这段程序是可以跑的,也是有正确结果的。

const

这个关键字我们都应该很熟悉了,但是为什么还要在这里讲呢,因为有一个问题,在C语言中即使const修饰全局变量,不影响它的链接性,但是在C++中,有所不同,const全局变量的链接性为内部的。

const int fingers = 10;

C+修改了常量类型的规则,让程序员更轻松。例如,假设将一组常量放在头文件中,并在同一个程序的多个文件中使用该头文件。那么,预处理器将头文件的内容包含到每个源文件中后,所有的源文件都。将包含那一组常量定义,这样的话根据单定义规则,这将出错。也就是说,只有一个文件可以包含声明,其他的声明都需要用extern关键字修饰,另外,extern修饰的全局变量无法初始化,也没必要初始化。

extern const int figures

因此,需要为某个文件使用一组定义,而其他文件使用另一-组声明。然而,由于外部定义的const 数据的链接性为内部的,因此可以在所有文件中使用相同的声明。

内部链接性还意味着,每个文件都有自己的一组常量,而不是所有文件共享一组常量。每个定义都是其所属文件私有的,这就是能够将常量定义放在头文件中的原因。-这样,只要在两个源代码文件中包括同一个头文件,则它们将获得同一组常量。

如果出于某种原因,程序员希望某个常量的链接性为外部的,则可以使用exterm关键字来覆盖默认的内部链接性:

extern const int figures = 10;

在这种情况下,必须在所有使用该常量的文件中使用 extern关键字来声明它。这与常规外部变量不同,定义常规外部变量时,不必使用extern关键字,但在使用该变量的其他文件中必须使用extern。然而,请记住,鉴于单个const在多个文件之间共享,因此只有一个文件可对其进行初始化。

在函数或代码块中声明const时,其作用域为代码块,即仅当程序执行该代码块中的代码时,该常量才是可用的。这意味着在函数或代码块中创建常量时,不必担心其名称与其他地方定义的常量发生冲突。

这一段const的更深层次的解释比较绕,希望大家明白。

函数和链接性

和变量一样,函数也有链接性,虽然可选择的范围比变量小。和C语言一样,C++不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都自动为静态的,即在整个程序执行期间都一直存在。在默认情况下,函数的链接性为外部的,即可以在文件间共享。实际上,可以在函数原型中使用关键字extern-来指出函数是在另一个文件中定义的,不过这是可选的(要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件)。还可以使用关键字 static将函数的链接性设置为内部的,使之只能在一个文件中使用。必须同时在原型和函数定义中使用该关键字

static int private(double x);

这意味着该函数只在这个文件中可见,还意味着可以在其他文件中定义同名的的函数。和变量一样,在定义静态函数的文件中,静态函数将覆盖外部定义,因此即使在外部定义了同名的函数,该文件仍将使用静态函数。

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

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

语言链接性

另一种形式的链接性——称为语言链接性(language linking)也对函数有影响。首先介绍一些背景知识。链接程序要求每个不同的函数都有不同的符号名。在C语言中,一个名称只对应一个函数,因此这很容易实现。为满足内部需要,C语言编译器可能将spiff 这样的函数名翻译为spiff。这种方法被称为C语言链接性(C language linkage)。但在C++中,同一个名称可能对应多个函数,必须将这些函数翻译为不同的符号名称。因此,C++编译器执行名称矫正或名称修饰,为重载函数生成不同的符号名称。例如,可能将spiff (int)转换为spoffi,而将spiff (double,double)〉转换为spiffdd。这种方法被称为C++语言链接(C++ language linkage)。

那么我们要在C++程序中使用C库中的预编译函数,将会出现什么情况呢,

spiff(22);

它在C库中的符号名称为spiff,但是在C++中我们找的是spoff_i,为了解决这种问题,可以用函数原型类指出要使用的约定

extern “C” void spiff(int);
extern void spoff(int);
extern “C++void spaff(int);

第一个原型使用C语言链接性;而后面的两个使用C++语言链接性。第二个原型是通过默认方式指出这一点的,而第三个显式地指出了这一点。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LyaJpunov

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值