第十五篇 -- 研究下指针与内存

学习C++宝典中的内容

一、程序中内存的分配方式

在C++程序中内存分为5个区:栈、堆、自由存储区、全局/静态存储区和常量存储区。

  栈:栈区由编译器自动分配和释放,存放函数的参数以及局部变量。其分配运算内置于处理器的指令集中,效率很高。但是可使用的总量有限,一般不会超过1M字节。

  堆:堆区中内存的分配和释放由开发者负责。一般用运算符new分配内存,并用运算符delete释放内存。一个new 要对应一个delete,否则会导致内存泄露。如果开发者没有释放,在程序结束的时候操作系统会自动回收。在堆上可分配的内存比栈上大了很多,且使用非常灵活。

  自由存储区:和堆类似,但是其内存管理是通过库函数malloc和free等进行的。在C程序中经常使用,虽然在C++程序中仍然可以使用,但不如用堆方便。

  全局/静态存储区:存放的是全局变量和静态变量。该存储区分配的内存在这个程序运行期间一直有效,直到程序结束由系统回收。

  常量存储区:存储的是常量,通常不允许修改。在程序中定义的常量以及指针字符串都存储在这里。

例1:

int a = 0;           //全局变量,存储在全局/静态存储区
void Storage_study() {
    std::cout << "
This is Storage_study() function
";

    int b;          //局部变量,存储在栈上
    int *p = new int();      //由运算符new分配的,存储在堆上
    static int d;            //静态变量,存储在全局/静态存储区
    const int e = 0;         //常量,存储在常量存储区
    delete p;                //释放堆中的内存

    std::cout << "
===================Storage_study() end===================
";
}
View Code

C++程序中的内存都要从上面的五个区中分配。不过栈、全局/静态存储区以及常量存储区中的分配是由编译器来进行的,并且在程序运行之前已经分配,因此称之为静态分配;堆以及自由存储区上内存的分配,是在程序运行的过程中进行的,称之为动态内存分配

说明:只有在堆上和自由存储区中分配的内存需要开发者管理。其他存储区中的变量只要定义即可,其内存的分配和释放由编译器负责。

二、在堆上分配内存

在堆上分配内存,要用new关键字,后面跟一个数据类型,为了保存分配出内存的地址,应当使用一个指针指向new的结果int *p = new int;。如果需要对分配的内存进行初始化,则在类型后面加一个括号,并带有初始值。如:int *p = new int(3);

对于不提供显式初始化的情况,如果new的类型是类,则会调用该类的默认构造函数;如果是内部数据类型,则不会被初始化。

用new不但可以为一个变量分配内存,还可以为一个数组分配内存,其方法是在new的类型标识符后面跟一个中括号,其中是数组的长度。如:int *pArr = new int[10];

注意:同数组的定义不同,用运算符new为数组分配内存空间时,其长度可以是变量,而不必是常量。因为动态内存分配在程序运行的时候才分配空间,所以当用new在堆上定义数组的时候,其长度可以是一个变量

例:
int n = 10;

int *parr = new int[n];     //可以用变量作为动态生成的数组的长度

因此当数组的长度不确定的时候,一般都要使用new的方法定义数组。new数组也有其缺点,那就是不能提供显式初始化值。

三、释放堆上的内存

 通过new分配的内存必须由开发者自己去释放,一块内存如果没有被释放,则会一直存在到该应用程序结束。在C++中使用delete来释放内存。对于单个内存,释放语法为:delete 指针名。对于数组,释放语法为:delete []指针名。

如:

int *p = new int;

delete p;

int *pArr = new int [10];

delete []pArr;

void release_hand() {
    std::cout << "
This is release_hand() function
";

    int *pCount = new int;
    std::cout << "Please input the length of the array : " << std::endl;
    std::cin >> *pCount;
    std::cout << "Please input the value of the array : " << std::endl;
    int *pArray = new int[*pCount];
    for (int i = 0; i < *pCount; i++) {
        std::cin >> pArray[i];
    }
    std::cout << "Array's value as below : 
" << std::endl;
    for (int j = 0; j < *pCount; j++) {
        std::cout << "pArray[" << j << "] = " << pArray[j] << "	";
    }

    std::cout << std::endl;
    delete pCount;
    delete pArray;

    std::cout << "
===================release_hand() end===================
";
}
View Code

上述代码中,因为数组的长度依赖于输入,是个变量,因此需要用new的方法。因为int是C++的内部数据类型,因此[]可以忽略。

当心:new 分配的内存,都需要显式的delete。如果缺少了delete,则会造成内存泄露。含有这种错误的代码每被调用一次就丢失一块内存。也许刚开始时系统的内存充足,其影响还不明显,等到程序运行足够长的时间后,系统内存就会被耗尽,程序也就会崩溃。

但是对于不是用new分配的内存地址,则在其上使用delete是不合法的。

int i;   //局部变量
    int *pi = &i;    //指针指向变量
    string str = "abcdef";   //字符串指针
    double *pd = new double(33);  //通过new分配的内存

    delete str;  //错误,str不是一个动态创建的对象
    delete pi;   //错误,pi指向一个实例变量
    delete pd;   //正确
View Code

对于上面两个错误,编译器能检查出对str的删除是错误的,因为编译器知道str并不是一个指针类型。但是对于第二个错误,编译器并不能断定一个指针指向什么类型的对象,不能在编译时检查出来。因此在程序中new和delete出现的次数必须相同,而且对于一块使用new分配的内存,只能delete一次,否则也会造成系统错误。

四、const与指针

const是一个常量修饰符。使用const修饰一个变量时,将使其值不可改变,从而变成一个常量。const也可以在定义指针时使用,不过此时const不仅可以修饰指针变量,也可以修饰指针所指向的数据。

1. 指向const的指针

指向const的指针,指的是一个指针指向的数据是常量,不可以被修改,但指针变量本身可以被修改。其定义格式是在普通指针定义前加上const关键字。如:const int *p;

在上面的定义中标识符p前面的“*”表明p是一个指针,int表明p是一个指向整形数据的指针,而const则表明p所指的数据是一个常量,不能被修改。严格来讲是不能通过指针p间接修改这个数据的,例如:

int a = 0;    //定义变量
const int * pa;   //定义一个const指针
pa = &a;       //可以改变指针指向的地址
*pa = 1;      //错误,不能通过该const指针修改其值
a = 2;       //a是变量,可以修改
View Code

在上面的代码中,试图对*pa的修改是错误的,因为pa是一个指向const的指针。但是a本身还是一个变量,因此可以直接对a进行修改。因此,指向const的指针,这个const限制的是通过指针来修改变量的权限,而不是修改变量的权限,该变量还是可以被修改的。对于符号常量,即用const修饰的变量,只能用指向const修饰的变量,只能用指向const的指针来指向,而不能用普通的指针。例如:

const int a = 10;    //const 变量
int *p1 = &a;       //错误
const int *p2 = &a;  //正确
View Code

指向const的指针,只限制了指向变量不可以被修改,但是指针本身的指向是可以修改的:

int a;
int b;
const int *p = &a;
p = &b;  //修改指针的指向
View Code

提示:在函数定义中,经常用指针来代替数组,对于只读的数组,经常用指向const的指针来限制。

2.const指针

const指针,指的是指针变量本身是一个常量,只能指向定义时所给的那个数据,而不能指向别处,而对被指向的数据是没有影响的。其定义语法是在指针名前加上const关键字,而不是在最前面。如:int a;  int * const p = &a;

当const指针定义之后,可以修改其指向变量的值。

int a;
int * const p = &a;
*p = 1;   //正确,修改其指向的值
int b;
p = &b;   //错误,不可以修改其指向
View Code

因为不能修改const指针的指向,所以在定义的时候必须给一个初始值。下面的定义是错误的:

int * const p;

3. 指向const的const指针

上面讲述了const在两个位置的不同效果,const还可以同时出现在这两个位置,其定义格式为:const 类型名 * const 指针名;

这是前面两种情况的组合,其指向的变量和指向都不可以被修改,例如:

  int a = 0;    //定义一个变量

  const int * const p = &a;   //指向const的const指针

对于这个指针p,下面两种情况的修改都是错误的:

  *p = 1;     //错误

  int b;

  p = &b;    //错误

p表示一个指向const的指针,因此既不能修改指针的指向,也不能修改指针指向地址的变量。

三种const指针的定义很相似,很难以区分,这里有一个简单的技巧:从标识符的开始处读它,并从里向外读,const靠近那个“最靠近的”。因此根据这个理解,第一个定义表示int不可以被改变,第二个表示p不可以被改变,第三个表示int和p都不可以被改变。

五、引用

虽然指针的使用非常灵活和高效,但使用起来却不是非常方便。如果使用不当,很容易导致某些不易察觉的错误。为此,C++引入了引用。

1. 定义引用

引用也是一种数据类型。不过,引用不能独立存在,而只能依附于一个变量。所以定义一个引用,必须要指明是哪个变量的引用。定义一个引用包括目标变量的数据类型、引用修饰符“&”、引用的标识符以及目标变量的标识符,其语法如下:

类型标识符 &引用名 = 目标变量名;

其中类型标识符是目标变量的类型。“&”是引用修饰符,表示定义的是一个引用。而被引用的变量则通过赋值运算符指定。例如:

int a;   //定义一个变量
int &b = a;    //定义一个上述变量的引用
View Code

说明:“&”在这里不是取地址运算符,而是一个引用修饰符。

引用一旦定义,则始终跟其目标变量绑定,而不能改变为其他变量的引用。假设b是变量a的引用,则在b的生命周期内,b始终都是a的引用,而不能再改变为其他变量的引用。另外,引用在其生命周期内完全可以替代其目标变量。也就是说,所有施加于引用上的操作,其效果都等同于直接对引用的目标变量进行操作。而且一旦目标变量的值发生了改变,引用的值也会发生同样的变化。

鉴于引用的不可变更性,以及引用与目标变量的等价性,一个变量的引用也可以看做该变量的别名。定义一个引用只不过是给变量另外起了一个名字。这样两个名字拥有一个实体,对一个名字的操作自然也会影响到另外一个名字。

提示:引用的上述行为和特性类似于const指针。首先,const指针也是定义后不能在指向别的变量的;其次,通过const指针可以间接地修改其目标变量,而且对目标变量的修改也会影响const指针。

2. 常引用

定义引用时也可以用const进行修饰,其结果为一个常引用。其语法如下:

const 类型标识符 &引用名 = 目标变量名;

例如:

int a;   //定义一个变量
const int &b = a;    //定义一个上述变量的引用
a = 1;   //正确,可以修饰变量
b = 2;   //错误,不可以通过常饮用修改变量
View Code

上面语句为a定义了一个const的引用b,这样,就可以限制通过b来修改目标变量a,相当于给了这个别名只读不写的权限。

一般的引用在定义时必须有一个已经存在的变量,而常引用则没有这样的限制,可以定义一个字面常量的常引用。例如:const int &a = 2;

当心:对于符号常量,即被const修饰的变量,其对应的引用必为常引用。否则对于一个变量实体,其一个名字显示不可修改,而另一个名字显示可以修改,这样显然是矛盾的。

六、引用和指针的区别

引用是C++特有的新类型(与C相比)。在很多情况下,引用提供了与指针操作同等的功能。但是引用和指针还是有一些区别的,在使用时应当根据实际情况进行选择。例如:

1. 引用必须与一个变量绑定,不存在没有任何目标的引用。因此如果在使用的过程中有可能出现什么都不指向的情况下,则应该使用指针,可以把一个空值给指针。而若一个变量肯定指向某个对象,不允许为空,则可以使用引用。

2. 引用仅仅是一个变量实体的别名。因此在使用引用之前,不需要检测其合法性。但指针在使用之前必须检测其指向的对象是否为空,不能对空指针进行取值操作。

3. 指针可以重新赋值以重新指向新的对象,引用在初始化之后就不可以改变指向对象了。

4. 指针可以指向数组的地址来替代数组使用,而引用不可以代替数组,引用只能指向数组中的某一个元素。

5. 指针可以用于在堆上生成的对象,delete的对象只能是一个指针,不能是引用。

一个简单指针指向数组的应用--冒泡排序

#include "pch.h"
#include <iostream>
using namespace std;

#define len 10  //define array length equal 10

//function prototype
void Method_one();
void Method_two();
void compare(int &a, int &b);

int break_node = 0;//when break_node is true, jump the loop

int main()
{
    //Bubble sort
    //Method_one();
    Method_two();
    
}

void Method_one() {
    //define an array which length is 10
    int array_sort[len];

    //input array value
    cout << "Please input " << len << " number: " << endl;
    for (int i = 0; i < len; i++)
        cin >> array_sort[i];


    //start sort
    for (int m = len; m > 0; m--) {
        for (int j = 0; j < m - 1; j++) {
            compare(array_sort[j], array_sort[j + 1]);
        }
        if (break_node) {
            break_node = 0;
        }
        else {//while no change data in inner loop, jump out the external loop
            break;
        }
    }

    //output sorted array(high-->low)
    cout << "Sorted array: " << endl;
    for (int k = 0; k < len; k++) {
        cout << array_sort[k] << " ";
    }
    cout << endl;
}

void Method_two() {
    int length = 0;
    cout << "Please input the length of array : " << endl;
    cin >> length;
    cout << "Please input " << length << " values" << endl;
    int *pArr = new int[length];

    //input array value
    for (int i = 0; i < length; i++)
        cin >> pArr[i];

    //start sort
    for (int m = length; m > 0; m--) {
        for (int j = 0; j < m - 1; j++) {
            compare(pArr[j], pArr[j + 1]);
        }
        if (break_node) {
            break_node = 0;
        }
        else {//while no change data in inner loop, jump out the external loop
            break;
        }
    }

    //output sorted array(high-->low)
    cout << "Sorted array: " << endl;
    for (int k = 0; k < length; k++) {
        cout << pArr[k] << " ";
    }
    cout << endl;

    delete pArr;
}

void compare(int &a, int &b) {
    if (a >= b);
    else {
        int temp;
        temp = a;
        a = b;
        b = temp;
        break_node++;
    }
}
View Code
原文地址:https://www.cnblogs.com/smart-zihan/p/11248424.html