【c++基础】6.内存机制、指针、引用、动态数组及实例

本章从c++的内存机制入手,首先介绍堆栈等内存机制,然后介绍指针、引用这两种核心的地址操作手段,最后介绍数组以及动态数组,在学习数组这种用户自定义类型的同时,加深对指针和内存机制的了解。


【一】内存机制


内存可以将其简单理解为一个存数据的容器,内存中的数据在计算机关闭时将被清空。计算机中的内存一般是按照字节编址的,即一字节一地址,现在(2017-6)计算机的内存已经很大了,pc机最少也有4G了吧,手机至少也有2G了吧,内存支持程序的执行,用于执行时存放c++程序指令和数据。在C++程序中,内存主要分为如下5个存储区:
(1)栈(Stack):该区用于存储函数内的动态变量空间,由编译器负责分配,也就是该区的变量在编译时就已经确定,函数结束栈变量失效。
(2)堆(Heap):该区用于存储由new申请的变量空间,由delete或delete[]负责释放,如果程序员忘了释放,将造成内存泄漏,直到程序运行结束,才能有OS回收(对于小型程序,内存泄漏可能问题不大,但是对于服务器上那些需要持续运行的程序,内存泄漏可能会随着时间增加,最后造成程序崩溃)。
(3)自由存储区(Free Storage):和堆类似,该区用于存储由malloc/calloc/realloc分配,用free释放。如果程序员忘记free了,将造成内存泄漏,直到程序运行结束,才能有OS回收。
(4)全局区/静态区(Global Static Area): 全局变量和静态变量存放区,程序一经编译好,该区域便存在。在C++中,由于全局变量和静态变量编译器会给这些变量自动初始化赋值,所以没有区分了初始化变量和未初始化变量了。由于全局变量一直占据内存空间且不易维护,推荐少用。程序结束时释放。
(5)常量存储区: 这是一块比较特殊的存储区,专门存储不能修改的常量,如const修饰的常数和字符串等(如果采用非正常手段更改当然也是可以的了)。


【二】地址间接操作


变量是直接访问存储空间的方式,关于变量之前已经写过一篇文章简单介绍,可以点击这里查看(传送门),指针和引用是两种间接访问存储单元的方式。

1、指针

不要把指针想的多神秘,其实它也是一个变量,指针也有指针名、指针(指向)类型、指针地址、指针值等属性,只是指针值只能是一个地址。指针的大小都是4个字节,无论它指向什么类型,没有例外。
(1)、取指针内容符号*
取指针内容符号*有两个作用,当它出现在数据类型和变量之间时,表示定义(或申明)指针变量,当它前面没有数据类型时,表示取指针变量指向的地址内容。
(2)、取地址符号&
指针值只能是一个地址,如何为指针赋值呢?你当然可以直接给它赋一个具体的地址值,但是这样意义不大,因为我们不知道你赋给它的地址指向什么地方,也就无法对它进行正确的操作。
有两种方式为指针赋值,其中之一是使用取地址符号“&”取一个变量的地址为指针变量赋值,此时对指针变量的操作将影响原变量的值,因为都是对同一地址进行操作(另一种方式是使用new或者malloc等动态申请的空间地址为指针变量赋值,后面将动态数组时会说到),下面给出取指针内容符号和取变量地址符号的例子:

int*ptr;//申明一个指针
int a=10;
ptr=&a;//取变量a的地址,并将其赋值给指针变量ptr
cout<<*ptr;//取指针变量ptr指向的地址内容并输出

(3)、指针的定义和使用
如上面的例子所示,指针申明语法为:

dataType *varibleName;//也就是:数据类型 *变量名;

赋值:

varibleName=&varibleName2;//用varibleName2的地址为指针赋值
varibleName=new dataType;//用new申请的空间为指针赋值
varibleName=new dataType[n];//new申请n个dataType类型的空间
varibleName=(dataType *)malloc(sizeof(dataType));//使用malloc申请的空间为指针赋值
varibleName=(dataType *)malloc(n*sizeof(dataType));//使用malloc申请n个dataType类型空间

当然你也可以直接定义指针,关于申明和定义的区别,在前面变量和函数的章节就讲过了,就不再赘述。
一个例子:

#include <iostream>
#include <cstdlib>
using namespace std;

int main(){
    int *ptr;
    int *ptr1;
    ptr=new int;
    ptr1=(int *)malloc(sizeof(int));
    *ptr=10;
    *ptr1=20;
    cout<<*ptr<<"\t"<<*ptr1<<endl;
    ptr=new int[2];
    *ptr=1;
    *(ptr+1)=2;
    ptr1=(int *)malloc(2*sizeof(int));
    *ptr1=3;
    *(ptr1+1)=4;
    cout<<*ptr<<"\t"<<*(ptr+1)<<"\t"<<*ptr1<<"\t"<<*(ptr1+1)<<endl;
    return 0;
} 

输出:

10 20
1 2 3 4

2、引用

引用是另一种间接操作地址的方法,引用相当于为变量取一个别名,对引用变量的操作就是对原变量的使操作。
(1)、引用的定义
引用在申明是就必须为其初始化,定义一个变量的引用,实际上就是把变量的地址赋给了引用变量,因此对引用变量的任何操作,实际上就是对原变量的操作,所以说引用就相当于为原变量取了个别名,和无论叫你的名字还是叫你的小名都是叫你本人一样,无论对原变量操作还是对引用变量(别名)操作,都是对同一个地址本身操作。
引用变量是以&符号开始的,但在使用引用变量的时候不必要带着&使用,定义方式如下:

dataType a;//定义一个dataType 类型的变量a
dataType &b=a;//为a取了个别名叫b

(2)、引用变量的初始化
引用变量在定义时就必须完成初始化,一旦绑定了一个对象,就一直和该对象绑定,不能进行修改,这也是为什么一定要初始化的原因。
只能使用一个对象为引用变量初始化,即不能为字面值(也就是常数如123),且对象类型必须与引用变量的类型严格相同,即就算能自动类型转换也不行。
一个例子:

#include <iostream>
using namespace std;
int main(){
    int a=1;
    double b=2;
    bool c=1;
    int &d=a;
    //int &e=b;//错误,b的类型不为int 
    //int &f=c;//错误,c的类型不为int 
    cout<<"a="<<a<<endl;
    cout<<"d="<<d<<endl;
    return 0; 
} 

输出:

a=1
d=1

(3)、引用的使用
引用的使用和变量一样。


【三】动态数组


1、内存动态分配

在程序中的临时变量,静态变量,全局变量都是静态分配的,也就是在编译的时候就已经分配好存储空间的,这些变量是被分配到栈区、全局/静态区、常量区。动态分配,就是运行时才进行分配存储空间,这些变量将被分配到堆区,堆区是专门为程序应用动态分配空间而设置的,这个区才是我们程序员看得见摸得着的地方。

其实它的功能和其他区一样,就是一个按字节编址的地址空间,用于存储数据,我们对存储空间的操作无非就是存取、删除,其他区都是由系统管理的,这个区很多时候都是由程序员管理的。

我们怎么操作这快内存呢?c++中提供了两个运算符:new和delete

new是一个单目符,右操作数为一种数据类型,而不是变量,运算结果为一个地址,使用这个地址的唯一方法是将它赋值给一个指针,指向的东西的大小为右操作数(数据类型)大小。
delete也是一个单目运算符,右操作数为一个指向动态分配的内存的指针。结果为删除指针指向的内存(实质为将指针指向的内存标记为可用),如果不删除,则造成内存泄漏,下次使用new运算符时,这部分内存将不可用,在大量使用动态内存分配的大程序中,如果始终不删除,将可能造成堆内存满了而使程序意外中止。(堆RAM在程序结束运行后会置空,等待下一个程序运行)。
下面以动态数组的定义和使用来说明new、delete以及指针的使用方法做一个综合。

2、动态数组

一般的数组申明只能申明已知长度的数组,也就是说数组长度只能是一个常量。如下:

int a1=6;
int arr1[a1];//错误,因为a不是常量

int arr2[6];//正确
coonst int a2=6;
int arr3[a2];//正确

所谓动态数组,就是在运行时候动态申请空间的数组。动态数组可以事先不知道数组长度,而在运行时才指定数组的长度,如下面的数组长度num可以动态指定。
动态数组空间申请:

int *ptr=new int;             //申请一个int大小的空间
int num;
cin>>num;  //运行是输入需要申请的空间大小
float *ptrArr=new float[num];  //申请num个float大小的空间

释放:

delete ptr;                    //删除单个内存空间
delete ptrArr;                 //删除数组第一个内存空间,其他内存将泄漏
delete [ ] ptrArr;               //删除整个数组内存空间

二维动态数组:
创建:

int **twoDarray:
int xdim;int ydim;
towDarray=new int *[xdim];
for(int i=0;i<xdim;i++)
    twoDarray[i]=new int [ydim];

使用:
使用和赋值是一样的,都是通过下标进行 访问。

for(int i=0;i<xdim;i++){
    for(int j=0;j<ydim;j++){
        twoDarray[i][j]=i*j;
    }
}

释放:

for(int i=0;i<xdim;i++){
    delete [ ] twoDarray[i];
}
delete [ ] twoDarray;

3、一个综合的例子

在图书馆中经常会遇到图书信息录入的问题,在录入图书信息之前,通常不知道图书数目是多少,那么要申请多少空间来存储图书呢,你当然可以申请一个很大的空间来存储,但是这有两个问题,一是如果图书很少,那么很多空间将本浪费,二是随着后来新增图书,到了申请的空间都装不下的地步,你就不得不重新申请空间,将所有数据再录入一次(当然在实际中是不会遇到这样问题的,因为我们通常将信息存储到文件或者数据库中,这样就可以共享了,这个例子只是为了说明动态数组的作用)。有了动态数组,你将不用再担心这两个问题,你可以随着图书的增多动态申请更多空间去存储图书。一个简单的例子如下:

#include <iostream>
#include <string>

#define stepLength 3  //定义图书仓库长度增长步长

using namespace std;

typedef struct{//定义图书结构,简单起见只简单包含图书名字和价格
    string name;
    int price;
}book;

int max = 3;   //定义当前图书仓库最大长度
int counter = 0; //当前图书仓库中的图书数目
book *storage;  //定义图书仓库

void add(string name, int price);//定义添加图书的函数

int main(){
    storage = new book[max];//初始化仓库,申请能存储max个图书的空间
    for (int i=0;i<=9;i++){//添加9本书
        string name = "book" + to_string(i);
        add(name, i);//简单地使用booki和i作为参数填充书本
    }
    cout << "+--------------book list------------+" << endl;
    for (int i = 0; i<counter; i++){   //打印添加的书单
        cout << "| " << storage[i].name << "    " << storage[i].price << endl;
    }
    system("pause");
}

void add(string name, int price){
    if (counter < max){   //判断当前书本数目,若小于仓库大小,说明还有空间,则直接添加书籍
        storage[counter].name = name;
        storage[counter].price = price;
        counter++;
        cout << "书籍-->name:  " << name << ",  price:  " << price << "添加完毕..." << endl;
    }
    else{//否则,说明空间不够了,添加不进去了
        cout << "空间不足,正在申请空间..." << endl;
        max += stepLength;//增大空间容量
        book *temp = new book[max];//申请更大空间
        for (int i = 0; i<counter; i++){//将原来的数据数据复制到新申请的空间中
            temp[i].name = storage[i].name;
            temp[i].price = storage[i].price;
        }
        delete[] storage;//别忘了释放原来的空间,否则将造成内存泄漏
        storage = temp;//将仓库指向新申请的空间
        cout << "空间申请完毕,正在添加书籍..." << endl;
        add(name, price);//再添加刚刚添加不进去那本书
    }
}

程序中初始空间大小为3本书大小,增长步长为3,当要录入9本书是,输出为:

书籍–>name: book0, price: 0添加完毕…
书籍–>name: book1, price: 1添加完毕…
书籍–>name: book2, price: 2添加完毕…
空间不足,正在申请空间…
空间申请完毕,正在添加书籍…
书籍–>name: book3, price: 3添加完毕…
书籍–>name: book4, price: 4添加完毕…
书籍–>name: book5, price: 5添加完毕…
空间不足,正在申请空间…
空间申请完毕,正在添加书籍…
书籍–>name: book6, price: 6添加完毕…
书籍–>name: book7, price: 7添加完毕…
书籍–>name: book8, price: 8添加完毕…
空间不足,正在申请空间…
空间申请完毕,正在添加书籍…
书籍–>name: book9, price: 9添加完毕…
+————–book list————+
| book0 0
| book1 1
| book2 2
| book3 3
| book4 4
| book5 5
| book6 6
| book7 7
| book8 8
| book9 9
请按任意键继续…

可见,当录入3本书时,空间就被填满了,程序会自动申请更多空间(+3个),以满足需求,在实际中肯定不会将步长设置为3,这太短了,因为新申请一次空间的开销是很大的,需要将原来的数据从新填入新申请的空间中,步长多少还要看实际应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值